From 6c7247b4862d9f24b0f7504c04857f72db539b91 Mon Sep 17 00:00:00 2001 From: Travis Truman Date: Wed, 7 May 2025 18:53:07 -0400 Subject: [PATCH] chore: integrate against new version of si-tooling Signed-off-by: Travis Truman --- evaluation_plans/osps/build_release/steps.go | 120 +++++--- .../osps/build_release/steps_test.go | 71 +++-- evaluation_plans/osps/docs/evaluations.go | 2 +- evaluation_plans/osps/docs/steps.go | 23 +- evaluation_plans/osps/docs/steps_test.go | 200 +++++++++++++ evaluation_plans/osps/governance/steps.go | 18 +- .../osps/governance/steps_test.go | 274 ++++++++++++++++++ .../osps/vuln_management/steps.go | 17 +- .../osps/vuln_management/steps_test.go | 161 +++++++--- .../reusable_steps/evaluations.go | 6 +- .../reusable_steps/evaluations_test.go | 102 +++---- go.mod | 5 +- go.sum | 6 +- 13 files changed, 794 insertions(+), 211 deletions(-) create mode 100644 evaluation_plans/osps/docs/steps_test.go create mode 100644 evaluation_plans/osps/governance/steps_test.go diff --git a/evaluation_plans/osps/build_release/steps.go b/evaluation_plans/osps/build_release/steps.go index 9516bde..27575ce 100644 --- a/evaluation_plans/osps/build_release/steps.go +++ b/evaluation_plans/osps/build_release/steps.go @@ -174,49 +174,95 @@ func releaseHasUniqueIdentifier(payloadData interface{}, _ map[string]*layer4.Ch return layer4.Passed, "All releases found have a unique name" } -func getLinks(data data.Payload) []string { +func getLinksFromProjectDocumentation(data data.Payload) (urls []string) { + doc := data.Insights.Project.Documentation + if doc == nil { + return urls + } + if doc.DetailedGuide != nil { + urls = append(urls, doc.DetailedGuide.String()) + } + if doc.CodeOfConduct != nil { + urls = append(urls, doc.CodeOfConduct.String()) + } + if doc.QuickstartGuide != nil { + urls = append(urls, doc.QuickstartGuide.String()) + } + if doc.ReleaseProcess != nil { + urls = append(urls, doc.ReleaseProcess.String()) + } + if doc.SignatureVerification != nil { + urls = append(urls, doc.SignatureVerification.String()) + } + return urls +} + +func getLinks(data data.Payload) (links []string) { si := data.Insights - links := []string{ - si.Header.URL, - si.Header.ProjectSISource, - si.Project.Homepage, - si.Project.Roadmap, - si.Project.Funding, - si.Project.Documentation.DetailedGuide, - si.Project.Documentation.CodeOfConduct, - si.Project.Documentation.QuickstartGuide, - si.Project.Documentation.ReleaseProcess, - si.Project.Documentation.SignatureVerification, - si.Project.Vulnerability.BugBountyProgram, - si.Project.Vulnerability.SecurityPolicy, - si.Repository.URL, - si.Repository.License.URL, - si.Repository.Security.Assessments.Self.Evidence, + + if len(si.Header.URL.String()) > 0 { + links = append(links, si.Header.URL.String()) } - if data.RepositoryMetadata.OrganizationBlogURL() != nil { - links = append(links, *data.RepositoryMetadata.OrganizationBlogURL()) + + if si.Header.ProjectSISource != nil && len(si.Header.ProjectSISource.String()) > 0 { + links = append(links, si.Header.ProjectSISource.String()) } - for _, repo := range si.Project.Repositories { - links = append(links, repo.URL) + + if si.Project != nil { + for _, repo := range si.Project.Repositories { + links = append(links, repo.Url.String()) + } + links = append(links, getLinksFromProjectDocumentation(data)...) + if si.Project.HomePage != nil { + links = append(links, si.Project.HomePage.String()) + } + if si.Project.Roadmap != nil { + links = append(links, si.Project.Roadmap.String()) + } + if si.Project.Funding != nil { + links = append(links, si.Project.Funding.String()) + } + + if si.Project.VulnerabilityReporting.BugBountyProgram != nil { + links = append(links, si.Project.VulnerabilityReporting.BugBountyProgram.String()) + } + if si.Project.VulnerabilityReporting.SecurityPolicy != nil { + links = append(links, si.Project.VulnerabilityReporting.SecurityPolicy.String()) + } } + if si.Repository != nil { + if len(si.Repository.Url.String()) > 0 { + links = append(links, si.Repository.Url.String()) + } + if len(si.Repository.License.Url.String()) > 0 { + links = append(links, si.Repository.License.Url.String()) + } - for _, repo := range si.Repository.Security.Assessments.ThirdParty { - links = append(links, repo.Evidence) + for _, tool := range si.Repository.SecurityPosture.Tools { + links = append(links, tool.Results.Adhoc.Location.String()) + links = append(links, tool.Results.CI.Location.String()) + links = append(links, tool.Results.Release.Location.String()) + } + for _, repo := range si.Repository.SecurityPosture.Assessments.ThirdPartyAssessment { + links = append(links, repo.Evidence.String()) + } + if si.Repository.SecurityPosture.Assessments.Self.Evidence != nil { + links = append(links, si.Repository.SecurityPosture.Assessments.Self.Evidence.String()) + } } - for _, tool := range si.Repository.Security.Tools { - links = append(links, tool.Results.Adhoc.Location) - links = append(links, tool.Results.CI.Location) - links = append(links, tool.Results.Release.Location) + if data.RepositoryMetadata != nil && data.RepositoryMetadata.OrganizationBlogURL() != nil { + links = append(links, *data.RepositoryMetadata.OrganizationBlogURL()) } + return links } func insecureURI(uri string) bool { - if !strings.HasPrefix(uri, "https://") || - !strings.HasPrefix(uri, "ssh:") || - !strings.HasPrefix(uri, "git:") || - !strings.HasPrefix(uri, "git@") { + if strings.HasPrefix(uri, "https://") || + strings.HasPrefix(uri, "ssh:") || + strings.HasPrefix(uri, "git:") || + strings.HasPrefix(uri, "git@") { return false } return true @@ -260,7 +306,7 @@ func insightsHasSlsaAttestation(payloadData interface{}, _ map[string]*layer4.Ch return layer4.Unknown, message } - attestations := data.Insights.Repository.Release.Attestations + attestations := data.Insights.Repository.ReleaseDetails.Attestations for _, attestation := range attestations { if attestation.PredicateURI == "https://slsa.dev/provenance/v1" { @@ -275,17 +321,15 @@ func distributionPointsUseHTTPS(payloadData interface{}, _ map[string]*layer4.Ch if message != "" { return layer4.Unknown, message } - - distributionPoints := data.Insights.Repository.Release.DistributionPoints - - if len(distributionPoints) == 0 { + if data.Insights.Repository.ReleaseDetails == nil || (data.Insights.Repository.ReleaseDetails != nil && len(data.Insights.Repository.ReleaseDetails.DistributionPoints) == 0) { return layer4.NotApplicable, "No official distribution points found in Security Insights data" } + distributionPoints := data.Insights.Repository.ReleaseDetails.DistributionPoints var badURIs []string for _, point := range distributionPoints { - if insecureURI(point.URI) { - badURIs = append(badURIs, point.URI) + if insecureURI(point.Uri) { + badURIs = append(badURIs, point.Uri) } } if len(badURIs) > 0 { diff --git a/evaluation_plans/osps/build_release/steps_test.go b/evaluation_plans/osps/build_release/steps_test.go index 8e4470a..d25ab12 100644 --- a/evaluation_plans/osps/build_release/steps_test.go +++ b/evaluation_plans/osps/build_release/steps_test.go @@ -6,13 +6,13 @@ import ( "slices" "testing" + "github.com/ossf/si-tooling/v2/si" + "github.com/revanite-io/pvtr-github-repo/data" "github.com/rhysd/actionlint" "github.com/stretchr/testify/assert" ) - -var goodWorkflowFile = -`name: OSPS Baseline Scan +var goodWorkflowFile = `name: OSPS Baseline Scan on: [workflow_dispatch] @@ -38,9 +38,7 @@ jobs: -v ${{ github.workspace }}/docker_output:/evaluation_results \ eddieknight/pvtr-github-repo:latest` - -var badWorkflowFile = -`name: OSPS Baseline Scan +var badWorkflowFile = `name: OSPS Baseline Scan on: [workflow_dispatch] @@ -66,25 +64,23 @@ jobs: -v ${{ github.workspace }}/docker_output:/evaluation_results \ eddieknight/pvtr-github-repo:latest` - type testingData struct { - expectedResult bool - workflowFile string + expectedResult bool + workflowFile string assertionMessage string } +func TestCicdSanitizedInputParameters(t *testing.T) { -func TestCicdSanitizedInputParameters (t * testing.T) { - - testData := []testingData { + testData := []testingData{ { - expectedResult: false, - workflowFile: badWorkflowFile, + expectedResult: false, + workflowFile: badWorkflowFile, assertionMessage: "Untrusted input not detected", }, { - expectedResult: true, - workflowFile: goodWorkflowFile, + expectedResult: true, + workflowFile: goodWorkflowFile, assertionMessage: "Untrusted input detected where it should not have been", }, } @@ -100,11 +96,9 @@ func TestCicdSanitizedInputParameters (t * testing.T) { } } - func TestVariableExtraction(t *testing.T) { - var testScript = - `echo ${{github.event.issue.title }} + var testScript = `echo ${{github.event.issue.title }} if ${{ github.event.commits.arbitrary.data.message}} -ne 0 then echo "Checkout report image" ${{ githubnodotevent.commits.arbitrary.data.message}} @@ -115,9 +109,8 @@ func TestVariableExtraction(t *testing.T) { assert.Equal(t, slices.Contains(varNames, "github.event.issue.title"), true, "Variable extraction failed") assert.Equal(t, slices.Contains(varNames, "github.event.commits.arbitrary.data.message"), true, "Variable extraction failed") - -} +} func TestMultipleVariables(t *testing.T) { @@ -129,8 +122,7 @@ func TestMultipleVariables(t *testing.T) { } - -func TestRegex ( t * testing.T ) { +func TestRegex(t *testing.T) { expression, err := regexp.Compile(regex) if err != nil { @@ -138,8 +130,37 @@ func TestRegex ( t * testing.T ) { return } - assert.Equal(t, expression.Match([]byte("github.event.issue.title")), true, "regex match failed" ) - assert.Equal(t, expression.Match([]byte("github.event.commits.arbitrary.data.message")), true, "regex match failed" ) + assert.Equal(t, expression.Match([]byte("github.event.issue.title")), true, "regex match failed") + assert.Equal(t, expression.Match([]byte("github.event.commits.arbitrary.data.message")), true, "regex match failed") } +func TestGetLinks(t *testing.T) { + payload := data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Header: si.Header{}, + Repository: &si.Repository{}, + }, + }, + } + links := getLinks(payload) + assert.Equal(t, len(links), 0, "getLinks should return an empty slice when no links are present") +} +func TestInsecureURI(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + {"http://example.com", true}, + {"https://example.com", false}, + {"ftp://example.com", true}, + {"mailto:", true}, + } + for _, data := range testData { + t.Run(data.input, func(t *testing.T) { + result := insecureURI(data.input) + assert.Equal(t, result, data.expected, fmt.Sprintf("Expected %v for input %s", data.expected, data.input)) + }) + } +} diff --git a/evaluation_plans/osps/docs/evaluations.go b/evaluation_plans/osps/docs/evaluations.go index 69b670c..be53cee 100644 --- a/evaluation_plans/osps/docs/evaluations.go +++ b/evaluation_plans/osps/docs/evaluations.go @@ -147,7 +147,7 @@ func OSPS_DO_06() (evaluation *layer4.ControlEvaluation) { []layer4.AssessmentStep{ reusable_steps.HasMadeReleases, reusable_steps.HasSecurityInsightsFile, - hasDependencyManagementPolicy, + reusable_steps.HasDependencyManagementPolicy, }, ) diff --git a/evaluation_plans/osps/docs/steps.go b/evaluation_plans/osps/docs/steps.go index a05a69b..2d0d3f2 100644 --- a/evaluation_plans/osps/docs/steps.go +++ b/evaluation_plans/osps/docs/steps.go @@ -25,8 +25,8 @@ func hasUserGuides(payloadData interface{}, _ map[string]*layer4.Change) (result if message != "" { return layer4.Unknown, message } - - if data.Insights.Project.Documentation.DetailedGuide == "" { + doc := data.Insights.Project.Documentation + if doc == nil || doc.DetailedGuide == nil || len(doc.DetailedGuide.String()) == 0 { return layer4.Failed, "User guide was NOT specified in Security Insights data" } @@ -39,7 +39,7 @@ func acceptsVulnReports(payloadData interface{}, _ map[string]*layer4.Change) (r return layer4.Unknown, message } - if data.Insights.Project.Vulnerability.ReportsAccepted { + if data.Insights.Project.VulnerabilityReporting.ReportsAccepted { return layer4.Passed, "Repository accepts vulnerability reports" } @@ -51,23 +51,10 @@ func hasSignatureVerificationGuide(payloadData interface{}, _ map[string]*layer4 if message != "" { return layer4.Unknown, message } - - if data.Insights.Project.Documentation.SignatureVerification == "" { + doc := data.Insights.Project.Documentation + if doc == nil || doc.SignatureVerification == nil || len(doc.SignatureVerification.String()) == 0 { return layer4.Failed, "Signature verification guide was NOT specified in Security Insights data" } return layer4.Passed, "Signature verification guide was specified in Security Insights data" } - -func hasDependencyManagementPolicy(payloadData interface{}, _ map[string]*layer4.Change) (result layer4.Result, message string) { - data, message := reusable_steps.VerifyPayload(payloadData) - if message != "" { - return layer4.Unknown, message - } - - if data.Insights.Repository.Documentation.DependencyManagement == "" { - return layer4.Failed, "Dependency management policy was NOT specified in Security Insights data" - } - - return layer4.Passed, "Dependency management policy was specified in Security Insights data" -} diff --git a/evaluation_plans/osps/docs/steps_test.go b/evaluation_plans/osps/docs/steps_test.go new file mode 100644 index 0000000..d2dd9c4 --- /dev/null +++ b/evaluation_plans/osps/docs/steps_test.go @@ -0,0 +1,200 @@ +package docs + +import ( + "testing" + + "github.com/ossf/si-tooling/v2/si" + "github.com/revanite-io/pvtr-github-repo/data" + "github.com/revanite-io/sci/pkg/layer4" + "github.com/stretchr/testify/assert" +) + +func TestHasUserGuides(t *testing.T) { + emptyURL := si.NewURL("") + arbitraryURL := si.NewURL("https://example.com/user-guide") + + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "No documentation provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "User guide was NOT specified in Security Insights data", + }, + { + name: "Detailed guide is empty", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + Documentation: &si.ProjectDocumentation{ + DetailedGuide: &emptyURL, + }, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "User guide was NOT specified in Security Insights data", + }, + { + name: "Detailed guide is provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + Documentation: &si.ProjectDocumentation{ + DetailedGuide: &arbitraryURL, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "User guide was specified in Security Insights data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := hasUserGuides(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} +func TestAcceptsVulnReports(t *testing.T) { + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "Vulnerability reports not accepted", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + VulnerabilityReporting: si.VulnerabilityReporting{ + ReportsAccepted: false, + }, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Repository does not accept vulnerability reports", + }, + { + name: "Vulnerability reports accepted", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + VulnerabilityReporting: si.VulnerabilityReporting{ + ReportsAccepted: true, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "Repository accepts vulnerability reports", + }, + { + name: "Vulnerability reporting data missing", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Repository does not accept vulnerability reports", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := acceptsVulnReports(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} +func TestHasSignatureVerificationGuide(t *testing.T) { + emptyURL := si.NewURL("") + arbitraryURL := si.NewURL("https://example.com/signature-verification-guide") + + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "No documentation provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Signature verification guide was NOT specified in Security Insights data", + }, + { + name: "Signature verification guide is empty", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + Documentation: &si.ProjectDocumentation{ + SignatureVerification: &emptyURL, + }, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Signature verification guide was NOT specified in Security Insights data", + }, + { + name: "Signature verification guide is provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + Documentation: &si.ProjectDocumentation{ + SignatureVerification: &arbitraryURL, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "Signature verification guide was specified in Security Insights data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := hasSignatureVerificationGuide(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} diff --git a/evaluation_plans/osps/governance/steps.go b/evaluation_plans/osps/governance/steps.go index 1c7872c..af45f24 100644 --- a/evaluation_plans/osps/governance/steps.go +++ b/evaluation_plans/osps/governance/steps.go @@ -37,11 +37,12 @@ func hasRolesAndResponsibilities(payloadData interface{}, _ map[string]*layer4.C return layer4.Unknown, message } - if data.Insights.Repository.Documentation.Governance == "" { - return layer4.Failed, "Roles and responsibilities were NOT specified in Security Insights data" + doc := data.Insights.Repository.Documentation + if doc != nil && doc.Governance != nil && len(doc.Governance.String()) > 0 { + return layer4.Passed, "Roles and responsibilities were specified in Security Insights data" } - return layer4.Passed, "Roles and responsibilities were specified in Security Insights data" + return layer4.Failed, "Roles and responsibilities were NOT specified in Security Insights data" } func hasContributionGuide(payloadData interface{}, _ map[string]*layer4.Change) (result layer4.Result, message string) { @@ -49,12 +50,15 @@ func hasContributionGuide(payloadData interface{}, _ map[string]*layer4.Change) if message != "" { return layer4.Unknown, message } + projDoc := data.Insights.Project.Documentation + repoDoc := data.Insights.Repository.Documentation - if data.Insights.Project.Documentation.CodeOfConduct != "" && data.Insights.Repository.Documentation.Contributing != "" { + if projDoc != nil && projDoc.CodeOfConduct != nil && len(projDoc.CodeOfConduct.String()) > 0 && + repoDoc != nil && repoDoc.ContributingGuide != nil && len(repoDoc.ContributingGuide.String()) > 0 { return layer4.Passed, "Contributing guide specified in Security Insights data (Bonus: code of conduct location also specified)" } - if data.Repository.ContributingGuidelines.Body != "" && data.Insights.Project.Documentation.CodeOfConduct != "" { + if data.Repository.ContributingGuidelines.Body != "" && projDoc != nil && projDoc.CodeOfConduct != nil && len(projDoc.CodeOfConduct.String()) > 0 { return layer4.Passed, "Contributing guide was found via GitHub API (Bonus: code of conduct was specified in Security Insights data)" } @@ -70,8 +74,8 @@ func hasContributionReviewPolicy(payloadData interface{}, _ map[string]*layer4.C if message != "" { return layer4.Unknown, message } - - if data.Insights.Repository.Documentation.ReviewPolicy != "" { + repoDoc := data.Insights.Repository.Documentation + if repoDoc != nil && repoDoc.ReviewPolicy != nil && len(repoDoc.ReviewPolicy.String()) > 0 { return layer4.Passed, "Code review guide was specified in Security Insights data" } diff --git a/evaluation_plans/osps/governance/steps_test.go b/evaluation_plans/osps/governance/steps_test.go new file mode 100644 index 0000000..42a5577 --- /dev/null +++ b/evaluation_plans/osps/governance/steps_test.go @@ -0,0 +1,274 @@ +package governance + +import ( + "testing" + + "github.com/ossf/si-tooling/v2/si" + "github.com/revanite-io/pvtr-github-repo/data" + "github.com/revanite-io/sci/pkg/layer4" + "github.com/stretchr/testify/assert" +) + +func TestHasContributionReviewPolicy(t *testing.T) { + emptyURL := si.NewURL("") + arbitraryURL := si.NewURL("https://example.com/review-policy") + + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "No documentation provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Code review guide was NOT specified in Security Insights data", + }, + { + name: "Review policy is empty", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + Documentation: &si.RepositoryDocumentation{ + ReviewPolicy: &emptyURL, + }, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Code review guide was NOT specified in Security Insights data", + }, + { + name: "Review policy is provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + Documentation: &si.RepositoryDocumentation{ + ReviewPolicy: &arbitraryURL, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "Code review guide was specified in Security Insights data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := hasContributionReviewPolicy(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} + +func TestHasRolesAndResponsibilities(t *testing.T) { + emptyGovernance := si.NewURL("") + arbitraryGovernance := si.NewURL("https://example.com/governance") + + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "No documentation provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Roles and responsibilities were NOT specified in Security Insights data", + }, + { + name: "Governance is empty", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + Documentation: &si.RepositoryDocumentation{ + Governance: &emptyGovernance, + }, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Roles and responsibilities were NOT specified in Security Insights data", + }, + { + name: "Governance is provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + Documentation: &si.RepositoryDocumentation{ + Governance: &arbitraryGovernance, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "Roles and responsibilities were specified in Security Insights data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := hasRolesAndResponsibilities(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} + +func TestProjectAdminsListed(t *testing.T) { + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "No administrators provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Project admins were NOT specified in Security Insights data", + }, + { + name: "Administrators list is empty", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + Administrators: []si.Contact{}, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Project admins were NOT specified in Security Insights data", + }, + { + name: "Administrators are provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + Administrators: []si.Contact{ + { + Name: "admin1", + Primary: false, + }, + { + Primary: true, + Name: "admin2", + }, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "Project admins were specified in Security Insights data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := projectAdminsListed(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} + +func TestCoreTeamIsListed(t *testing.T) { + tests := []struct { + name string + payload interface{} + expectedResult layer4.Result + expectedMsg string + }{ + { + name: "No core team provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{}, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Core team was NOT specified in Security Insights data", + }, + { + name: "Core team list is empty", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + CoreTeam: []si.Contact{}, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMsg: "Core team was NOT specified in Security Insights data", + }, + { + name: "Core team is provided", + payload: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + CoreTeam: []si.Contact{ + { + Name: "core1", + Primary: false, + }, + { + Primary: true, + Name: "core2", + }, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMsg: "Core team was specified in Security Insights data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, message := coreTeamIsListed(tt.payload, nil) + assert.Equal(t, tt.expectedResult, result, "Unexpected result") + assert.Equal(t, tt.expectedMsg, message, "Unexpected message") + }) + } +} diff --git a/evaluation_plans/osps/vuln_management/steps.go b/evaluation_plans/osps/vuln_management/steps.go index 41a99a2..253532c 100644 --- a/evaluation_plans/osps/vuln_management/steps.go +++ b/evaluation_plans/osps/vuln_management/steps.go @@ -15,12 +15,12 @@ func hasSecContact(payloadData interface{}, _ map[string]*layer4.Change) (result } // TODO: Check for a contact email in SECURITY.md - - if data.Insights.Project.Vulnerability.Contact.Email != "" { + proj := data.Insights.Project + if proj != nil && proj.VulnerabilityReporting.Contact != nil && data.Insights.Project.VulnerabilityReporting.Contact.Email.String() != "" { return layer4.Passed, "Security contacts were specified in Security Insights data" } - for _, champion := range data.Insights.Repository.Security.Champions { - if champion.Email != "" { + for _, champion := range data.Insights.Repository.SecurityPosture.Champions { + if champion.Email != nil && len(champion.Email.String()) > 0 { return layer4.Passed, "Security contacts were specified in Security Insights data" } } @@ -28,18 +28,17 @@ func hasSecContact(payloadData interface{}, _ map[string]*layer4.Change) (result return layer4.Failed, "Security contacts were not specified in Security Insights data" } - func sastToolDefined(payloadData interface{}, _ map[string]*layer4.Change) (result layer4.Result, message string) { data, message := reusable_steps.VerifyPayload(payloadData) if message != "" { return layer4.Unknown, message } - for _,tool := range data.Insights.Repository.Security.Tools { + for _, tool := range data.Insights.Repository.SecurityPosture.Tools { if tool.Type == "SAST" { - - enabled := []bool { tool.Integration.Adhoc, tool.Integration.CI, tool.Integration.Release } - + + enabled := []bool{tool.Integration.Adhoc, tool.Integration.Ci, tool.Integration.Release} + if slices.Contains(enabled, true) { return layer4.Passed, "Static Application Security Testing documented in Security Insights" } diff --git a/evaluation_plans/osps/vuln_management/steps_test.go b/evaluation_plans/osps/vuln_management/steps_test.go index c61923b..7a8dbcf 100644 --- a/evaluation_plans/osps/vuln_management/steps_test.go +++ b/evaluation_plans/osps/vuln_management/steps_test.go @@ -10,29 +10,28 @@ import ( ) type testingData struct { - expectedResult layer4.Result - expectedMessage string - payloadData interface{} + expectedResult layer4.Result + expectedMessage string + payloadData interface{} assertionMessage string } - func TestSastToolDefined(t *testing.T) { - + testData := []testingData{ { - expectedResult: layer4.Passed, - expectedMessage: "Static Application Security Testing documented in Security Insights", + expectedResult: layer4.Passed, + expectedMessage: "Static Application Security Testing documented in Security Insights", assertionMessage: "Test for SAST integration enabled", - payloadData: data.Payload{ - RestData: &data.RestData { + payloadData: data.Payload{ + RestData: &data.RestData{ Insights: si.SecurityInsights{ - Repository: si.Repository{ - Security: si.SecurityInfo{ - Tools: []si.Tool{ + Repository: &si.Repository{ + SecurityPosture: si.SecurityPosture{ + Tools: []si.SecurityTool{ { Type: "SAST", - Integration: si.Integration{ + Integration: si.SecurityToolIntegration{ Adhoc: true, }, }, @@ -42,18 +41,17 @@ func TestSastToolDefined(t *testing.T) { }, }, }, - }, { - expectedResult: layer4.Failed, - expectedMessage: "No Static Application Security Testing documented in Security Insights", + expectedResult: layer4.Failed, + expectedMessage: "No Static Application Security Testing documented in Security Insights", assertionMessage: "Test for SAST integration present but not explicitly enabled", - payloadData: data.Payload{ - RestData: &data.RestData { + payloadData: data.Payload{ + RestData: &data.RestData{ Insights: si.SecurityInsights{ - Repository: si.Repository{ - Security: si.SecurityInfo{ - Tools: []si.Tool{ + Repository: &si.Repository{ + SecurityPosture: si.SecurityPosture{ + Tools: []si.SecurityTool{ { Type: "SAST", }, @@ -63,18 +61,17 @@ func TestSastToolDefined(t *testing.T) { }, }, }, - }, { - expectedResult: layer4.Failed, - expectedMessage: "No Static Application Security Testing documented in Security Insights", + expectedResult: layer4.Failed, + expectedMessage: "No Static Application Security Testing documented in Security Insights", assertionMessage: "Test for Non SAST tool defined", - payloadData: data.Payload{ - RestData: &data.RestData { + payloadData: data.Payload{ + RestData: &data.RestData{ Insights: si.SecurityInsights{ - Repository: si.Repository{ - Security: si.SecurityInfo{ - Tools: []si.Tool{ + Repository: &si.Repository{ + SecurityPosture: si.SecurityPosture{ + Tools: []si.SecurityTool{ { Type: "NotSast", }, @@ -84,31 +81,113 @@ func TestSastToolDefined(t *testing.T) { }, }, }, - }, { - expectedResult: layer4.Failed, - expectedMessage: "No Static Application Security Testing documented in Security Insights", + expectedResult: layer4.Failed, + expectedMessage: "No Static Application Security Testing documented in Security Insights", assertionMessage: "Test for no tools defined", - payloadData: data.Payload{ - RestData: &data.RestData { + payloadData: data.Payload{ + RestData: &data.RestData{ Insights: si.SecurityInsights{ - Repository: si.Repository{ - Security: si.SecurityInfo{ - }, + Repository: &si.Repository{ + SecurityPosture: si.SecurityPosture{}, }, }, }, }, - }, } - + for _, test := range testData { result, message := sastToolDefined(test.payloadData, nil) assert.Equal(t, test.expectedResult, result, test.assertionMessage) assert.Equal(t, test.expectedMessage, message, test.assertionMessage) } - -} \ No newline at end of file + +} + +func TestHasSecContact(t *testing.T) { + arbitraryEmail := si.NewEmail("champion@example.com") + + tests := []struct { + name string + payloadData interface{} + expectedResult layer4.Result + expectedMessage string + }{ + { + name: "Valid contact in VulnerabilityReporting", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + VulnerabilityReporting: si.VulnerabilityReporting{ + Contact: &si.Contact{ + Name: "Security Team", + Email: &arbitraryEmail, + }, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMessage: "Security contacts were specified in Security Insights data", + }, + { + name: "Valid contact in Champions", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{ + SecurityPosture: si.SecurityPosture{ + Champions: []si.Contact{ + { + Name: "Security Champion", + Email: &arbitraryEmail, + }, + }, + }, + }, + }, + }, + }, + expectedResult: layer4.Passed, + expectedMessage: "Security contacts were specified in Security Insights data", + }, + { + name: "No security contacts specified", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: &si.Project{ + VulnerabilityReporting: si.VulnerabilityReporting{}, + }, + Repository: &si.Repository{ + SecurityPosture: si.SecurityPosture{ + Champions: []si.Contact{}, + }, + }, + }, + }, + }, + expectedResult: layer4.Failed, + expectedMessage: "Security contacts were not specified in Security Insights data", + }, + { + name: "Invalid payload data", + payloadData: nil, + expectedResult: layer4.Unknown, + expectedMessage: "Malformed assessment: expected payload type data.Payload, got ()", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, message := hasSecContact(test.payloadData, nil) + assert.Equal(t, test.expectedResult, result) + assert.Equal(t, test.expectedMessage, message) + }) + } +} diff --git a/evaluation_plans/reusable_steps/evaluations.go b/evaluation_plans/reusable_steps/evaluations.go index 23b7fa9..fff4047 100644 --- a/evaluation_plans/reusable_steps/evaluations.go +++ b/evaluation_plans/reusable_steps/evaluations.go @@ -97,8 +97,10 @@ func HasDependencyManagementPolicy(payloadData interface{}, _ map[string]*layer4 if message != "" { return layer4.Unknown, message } - - if len(payload.Insights.Repository.Documentation.DependencyManagement) > 0 { + repoDoc := payload.Insights.Repository.Documentation + if repoDoc != nil && + repoDoc.DependencyManagementPolicy != nil && + len(repoDoc.DependencyManagementPolicy.String()) > 0 { return layer4.Passed, "Found dependency management policy in documentation" } diff --git a/evaluation_plans/reusable_steps/evaluations_test.go b/evaluation_plans/reusable_steps/evaluations_test.go index b143f88..6191b0a 100644 --- a/evaluation_plans/reusable_steps/evaluations_test.go +++ b/evaluation_plans/reusable_steps/evaluations_test.go @@ -10,89 +10,57 @@ import ( ) type testingData struct { - expectedResult layer4.Result - expectedMessage string - payloadData interface{} - assertionMessage string + expectedResult layer4.Result + expectedMessage string + repoDocumentation *si.RepositoryDocumentation + name string + assertionMessage string } func TestHasDependencyManagementPolicySomethin(t *testing.T) { + depManagement := si.NewURL("https://example.com/dependency-management") + emptyDepManagement := si.NewURL("") + nilRepoDocumentation := (*si.RepositoryDocumentation)(nil) + + payload := data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Repository: &si.Repository{}, + }, + }, + } - //Ick, remind me to never use anonymous structs in my code testData := []testingData{ { - expectedResult: layer4.Passed, + expectedResult: layer4.Passed, expectedMessage: "Found dependency management policy in documentation", - payloadData: data.Payload{ - RestData: &data.RestData { - Insights: si.SecurityInsights{ - Repository: si.Repository{ - Documentation: struct { - Contributing string `yaml:"contributing-guide"` - DependencyManagement string `yaml:"dependency-management-policy"` - Governance string `yaml:"governance"` - ReviewPolicy string `yaml:"review-policy"` - SecurityPolicy string `yaml:"security-policy"` - }{ - DependencyManagement: "https://example.com/dependency-management", - }, - }, - }, - }, + repoDocumentation: &si.RepositoryDocumentation{ + DependencyManagementPolicy: &depManagement, }, - assertionMessage: "Happy Path failed", + name: "Dependency management policy found when present", }, { - expectedResult: layer4.Failed, + expectedResult: layer4.Failed, expectedMessage: "No dependency management file found", - payloadData: data.Payload{ - RestData: &data.RestData { - Insights: si.SecurityInsights{ - Repository: si.Repository{ - Documentation: struct { - Contributing string `yaml:"contributing-guide"` - DependencyManagement string `yaml:"dependency-management-policy"` - Governance string `yaml:"governance"` - ReviewPolicy string `yaml:"review-policy"` - SecurityPolicy string `yaml:"security-policy"` - }{ - DependencyManagement: "", - }, - }, - }, - }, + repoDocumentation: &si.RepositoryDocumentation{ + DependencyManagementPolicy: &emptyDepManagement, }, + name: "fail when policy is empty", assertionMessage: "Empty string check failed", }, { - expectedResult: layer4.Failed, - expectedMessage: "No dependency management file found", - payloadData: data.Payload{ - RestData: &data.RestData { - Insights: si.SecurityInsights{ - Repository: si.Repository{ - Documentation: struct { - Contributing string `yaml:"contributing-guide"` - DependencyManagement string `yaml:"dependency-management-policy"` - Governance string `yaml:"governance"` - ReviewPolicy string `yaml:"review-policy"` - SecurityPolicy string `yaml:"security-policy"` - }{ - DependencyManagement: *new(string), // empty string pointer effectively nil value - }, - }, - }, - }, - }, - assertionMessage: "Null String check failed", + expectedResult: layer4.Failed, + expectedMessage: "No dependency management file found", + repoDocumentation: nilRepoDocumentation, + assertionMessage: "Null String check failed", }, } - for _, test := range testData { - result, message := HasDependencyManagementPolicy(test.payloadData, nil) - assert.Equal(t, test.expectedResult, result, test.assertionMessage) - assert.Equal(t, test.expectedMessage, message, test.assertionMessage) + t.Run(test.name, func(t *testing.T) { + payload.Insights.Repository.Documentation = test.repoDocumentation + result, message := HasDependencyManagementPolicy(payload, nil) + assert.Equal(t, test.expectedResult, result, test.assertionMessage) + assert.Equal(t, test.expectedMessage, message, test.assertionMessage) + }) } - - -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 813e1f4..2d9f9f9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.4 require ( github.com/google/go-github/v71 v71.0.0 - github.com/ossf/si-tooling/v2 v2.0.5-0.20250328034800-657dc9aa9920 + github.com/ossf/si-tooling/v2 v2.0.4 github.com/privateerproj/privateer-sdk v1.2.0 github.com/revanite-io/sci v0.3.4 github.com/rhysd/actionlint v1.7.7 @@ -15,6 +15,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-yaml v1.17.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ) @@ -62,3 +63,5 @@ require ( // replace github.com/privateerproj/privateer-sdk => ../../privateerproj/privateer-sdk // replace github.com/revanite-io/sci => ../sci + +replace github.com/ossf/si-tooling/v2 => github.com/trumant/si-tooling/v2 v2.0.0-20250509003328-896a6bc61b6f diff --git a/go.sum b/go.sum index 1861d77..bf4eb61 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= +github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= @@ -61,8 +63,6 @@ github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebG github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/ossf/si-tooling/v2 v2.0.5-0.20250328034800-657dc9aa9920 h1:iT96I36tXMHMPcSvxLtfi6970MAEK3xlDMZGSxuhJLA= -github.com/ossf/si-tooling/v2 v2.0.5-0.20250328034800-657dc9aa9920/go.mod h1:LVl8Dz/65RjijQHXDxgfHn1h19nRNckswUDMjBB/pWY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -106,6 +106,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/trumant/si-tooling/v2 v2.0.0-20250509003328-896a6bc61b6f h1:9UcyIPqMdJ/87NKi0BHwu8L0pDZPIJVQ2rMonsHwe5Q= +github.com/trumant/si-tooling/v2 v2.0.0-20250509003328-896a6bc61b6f/go.mod h1:I7UDEAfNwoT2iwZrvORukgkGLKeD/cgVhHtcLPpaS6c= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=