From 126383ec380740e26ba2a44ea334f2451988496b Mon Sep 17 00:00:00 2001 From: Zohayb Bhatti Date: Fri, 26 Sep 2025 16:34:36 -0500 Subject: [PATCH 1/4] Implement OSPS-VM-04.01: Public vulnerability disclosure assessment - Add hasPublicVulnerabilityDisclosure function to check for public disclosure mechanisms - Update OSPS-VM-04.01 assessment steps (IsActive + hasPublicVulnerabilityDisclosure) - Add comprehensive test coverage with 5 test cases: * GitHub security policy enabled * Security Insights policy URL present * No disclosure mechanisms available * Both mechanisms present (GitHub takes priority) * Invalid payload handling - Add stubGraphqlRepoWithSecurityPolicy helper function following existing patterns Closes #34 --- .../osps/vuln_management/evaluations.go | 3 +- .../osps/vuln_management/steps.go | 17 ++++ .../osps/vuln_management/steps_test.go | 98 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/evaluation_plans/osps/vuln_management/evaluations.go b/evaluation_plans/osps/vuln_management/evaluations.go index 852e6c4..f726d94 100644 --- a/evaluation_plans/osps/vuln_management/evaluations.go +++ b/evaluation_plans/osps/vuln_management/evaluations.go @@ -83,7 +83,8 @@ func OSPS_VM_04() (evaluation *layer4.ControlEvaluation) { "Maturity Level 3", }, []layer4.AssessmentStep{ - reusable_steps.NotImplemented, + reusable_steps.IsActive, + hasPublicVulnerabilityDisclosure, }, ) diff --git a/evaluation_plans/osps/vuln_management/steps.go b/evaluation_plans/osps/vuln_management/steps.go index eec0f75..826e64d 100644 --- a/evaluation_plans/osps/vuln_management/steps.go +++ b/evaluation_plans/osps/vuln_management/steps.go @@ -60,3 +60,20 @@ func hasVulnerabilityDisclosurePolicy(payloadData any, _ map[string]*layer4.Chan return layer4.Passed, "Vulnerability disclosure policy was specified in Security Insights data" } + +func hasPublicVulnerabilityDisclosure(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) { + data, message := reusable_steps.VerifyPayload(payloadData) + if message != "" { + return layer4.Unknown, message + } + + if data.GraphqlRepoData.Repository.IsSecurityPolicyEnabled { + return layer4.Passed, "Public vulnerability disclosure available via GitHub security policy" + } + + if data.Insights.Project.Vulnerability.SecurityPolicy != "" { + return layer4.Passed, "Public vulnerability disclosure available via security policy in Security Insights data" + } + + return layer4.Failed, "No public vulnerability disclosure mechanism found" +} diff --git a/evaluation_plans/osps/vuln_management/steps_test.go b/evaluation_plans/osps/vuln_management/steps_test.go index b43cefe..d099554 100644 --- a/evaluation_plans/osps/vuln_management/steps_test.go +++ b/evaluation_plans/osps/vuln_management/steps_test.go @@ -164,3 +164,101 @@ func TestHasVulnerabilityDisclosurePolicy(t *testing.T) { }) } } + +func stubGraphqlRepoWithSecurityPolicy(isSecurityPolicyEnabled bool) *data.GraphqlRepoData { + repo := &data.GraphqlRepoData{} + repo.Repository.IsSecurityPolicyEnabled = isSecurityPolicyEnabled + return repo +} + +func TestHasPublicVulnerabilityDisclosure(t *testing.T) { + tests := []struct { + name string + payloadData any + expectedResult layer4.Result + expectedMessage string + }{ + { + name: "Public disclosure via GitHub security policy", + expectedResult: layer4.Passed, + expectedMessage: "Public vulnerability disclosure available via GitHub security policy", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: si.Project{ + Vulnerability: si.VulnReport{ + SecurityPolicy: "", + }, + }, + }, + }, + GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(true), + }, + }, + { + name: "Public disclosure via Security Insights policy", + expectedResult: layer4.Passed, + expectedMessage: "Public vulnerability disclosure available via security policy in Security Insights data", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: si.Project{ + Vulnerability: si.VulnReport{ + SecurityPolicy: "https://github.com/example/repo/blob/main/SECURITY.md", + }, + }, + }, + }, + GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(false), + }, + }, + { + name: "No public disclosure mechanism", + expectedResult: layer4.Failed, + expectedMessage: "No public vulnerability disclosure mechanism found", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: si.Project{ + Vulnerability: si.VulnReport{ + SecurityPolicy: "", + }, + }, + }, + }, + GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(false), + }, + }, + { + name: "Both mechanisms available - GitHub policy takes priority", + expectedResult: layer4.Passed, + expectedMessage: "Public vulnerability disclosure available via GitHub security policy", + payloadData: data.Payload{ + RestData: &data.RestData{ + Insights: si.SecurityInsights{ + Project: si.Project{ + Vulnerability: si.VulnReport{ + SecurityPolicy: "https://github.com/example/repo/blob/main/SECURITY.md", + }, + }, + }, + }, + GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(true), + }, + }, + { + name: "Invalid payload", + expectedResult: layer4.Unknown, + expectedMessage: "Malformed assessment: expected payload type data.Payload, got string (invalid_payload)", + payloadData: "invalid_payload", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, message := hasPublicVulnerabilityDisclosure(test.payloadData, nil) + assert.Equal(t, test.expectedResult, result) + assert.Equal(t, test.expectedMessage, message) + }) + } +} From 91c311a13c0573d06ba498618dfd0ff106ddae87 Mon Sep 17 00:00:00 2001 From: Zohayb Bhatti Date: Tue, 30 Sep 2025 16:46:52 -0500 Subject: [PATCH 2/4] feat: implement public vulnerability disclosure check using GitHub Security Advisories API - Replace security policy check with published security advisories check - Add SecurityAdvisory struct and REST API integration in rest-data.go - Update hasPublicVulnerabilityDisclosure to count published advisories - Add comprehensive tests for all scenarios (0, 1, multiple advisories) - Addresses Eddie's feedback about checking actual public disclosure evidence --- data/rest-data.go | 22 ++++- .../osps/vuln_management/steps.go | 15 ++-- .../osps/vuln_management/steps_test.go | 83 ++++++++----------- 3 files changed, 63 insertions(+), 57 deletions(-) diff --git a/data/rest-data.go b/data/rest-data.go index a3d966d..ebdbb4b 100644 --- a/data/rest-data.go +++ b/data/rest-data.go @@ -16,7 +16,16 @@ import ( ) type HttpClient interface { - Do(req *http.Request) (*http.Response, error) + Do(req *http.Request) (*http.Response, error) +} + +type SecurityAdvisory struct { + GhsaId string `json:"ghsa_id"` + CveId string `json:"cve_id"` + Summary string `json:"summary"` + Severity string `json:"severity"` + State string `json:"state"` + PublishedAt string `json:"published_at"` } type RestData struct { @@ -27,6 +36,7 @@ type RestData struct { WorkflowPermissions WorkflowPermissions Insights si.SecurityInsights Releases []ReleaseData + SecurityAdvisories []SecurityAdvisory Rulesets []Ruleset contents RepoContent ghClient *github.Client @@ -76,6 +86,7 @@ func (r *RestData) Setup() error { r.loadSecurityInsights() _ = r.getWorkflowPermissions() _ = r.getReleases() + _ = r.getSecurityAdvisories() return nil } @@ -339,6 +350,15 @@ func (r *RestData) getWorkflowPermissions() error { return err } +func (r *RestData) getSecurityAdvisories() error { + endpoint := fmt.Sprintf("%s/repos/%s/%s/security-advisories?state=published", APIBase, r.owner, r.repo) + responseData, err := r.MakeApiCall(endpoint, true) + if err != nil { + return err + } + return json.Unmarshal(responseData, &r.SecurityAdvisories) +} + func (r *RestData) GetRulesets(branchName string) []Ruleset { endpoint := fmt.Sprintf("%s/repos/%s/%s/rules/branches/%s", APIBase, r.owner, r.repo, branchName) responseData, err := r.MakeApiCall(endpoint, true) diff --git a/evaluation_plans/osps/vuln_management/steps.go b/evaluation_plans/osps/vuln_management/steps.go index 826e64d..ff32862 100644 --- a/evaluation_plans/osps/vuln_management/steps.go +++ b/evaluation_plans/osps/vuln_management/steps.go @@ -1,6 +1,7 @@ package vuln_management import ( + "fmt" "slices" "github.com/ossf/gemara/layer4" @@ -67,13 +68,13 @@ func hasPublicVulnerabilityDisclosure(payloadData any, _ map[string]*layer4.Chan return layer4.Unknown, message } - if data.GraphqlRepoData.Repository.IsSecurityPolicyEnabled { - return layer4.Passed, "Public vulnerability disclosure available via GitHub security policy" - } - - if data.Insights.Project.Vulnerability.SecurityPolicy != "" { - return layer4.Passed, "Public vulnerability disclosure available via security policy in Security Insights data" + advisoryCount := len(data.RestData.SecurityAdvisories) + if advisoryCount > 0 { + if advisoryCount == 1 { + return layer4.Passed, "Found 1 published security advisory" + } + return layer4.Passed, fmt.Sprintf("Found %d published security advisories", advisoryCount) } - return layer4.Failed, "No public vulnerability disclosure mechanism found" + return layer4.Failed, "No published security advisories found" } diff --git a/evaluation_plans/osps/vuln_management/steps_test.go b/evaluation_plans/osps/vuln_management/steps_test.go index d099554..74c5c4d 100644 --- a/evaluation_plans/osps/vuln_management/steps_test.go +++ b/evaluation_plans/osps/vuln_management/steps_test.go @@ -165,12 +165,6 @@ func TestHasVulnerabilityDisclosurePolicy(t *testing.T) { } } -func stubGraphqlRepoWithSecurityPolicy(isSecurityPolicyEnabled bool) *data.GraphqlRepoData { - repo := &data.GraphqlRepoData{} - repo.Repository.IsSecurityPolicyEnabled = isSecurityPolicyEnabled - return repo -} - func TestHasPublicVulnerabilityDisclosure(t *testing.T) { tests := []struct { name string @@ -179,71 +173,62 @@ func TestHasPublicVulnerabilityDisclosure(t *testing.T) { expectedMessage string }{ { - name: "Public disclosure via GitHub security policy", + name: "One published security advisory", expectedResult: layer4.Passed, - expectedMessage: "Public vulnerability disclosure available via GitHub security policy", + expectedMessage: "Found 1 published security advisory", payloadData: data.Payload{ RestData: &data.RestData{ - Insights: si.SecurityInsights{ - Project: si.Project{ - Vulnerability: si.VulnReport{ - SecurityPolicy: "", - }, + SecurityAdvisories: []data.SecurityAdvisory{ + { + GhsaId: "GHSA-xxxx-xxxx-xxxx", + Summary: "Test advisory", + State: "published", + PublishedAt: "2024-01-01T00:00:00Z", }, }, }, - GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(true), + GraphqlRepoData: &data.GraphqlRepoData{}, }, }, { - name: "Public disclosure via Security Insights policy", + name: "Multiple published security advisories", expectedResult: layer4.Passed, - expectedMessage: "Public vulnerability disclosure available via security policy in Security Insights data", + expectedMessage: "Found 3 published security advisories", payloadData: data.Payload{ RestData: &data.RestData{ - Insights: si.SecurityInsights{ - Project: si.Project{ - Vulnerability: si.VulnReport{ - SecurityPolicy: "https://github.com/example/repo/blob/main/SECURITY.md", - }, + SecurityAdvisories: []data.SecurityAdvisory{ + { + GhsaId: "GHSA-xxxx-xxxx-xxxx", + Summary: "First advisory", + State: "published", + PublishedAt: "2024-01-01T00:00:00Z", }, - }, - }, - GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(false), - }, - }, - { - name: "No public disclosure mechanism", - expectedResult: layer4.Failed, - expectedMessage: "No public vulnerability disclosure mechanism found", - payloadData: data.Payload{ - RestData: &data.RestData{ - Insights: si.SecurityInsights{ - Project: si.Project{ - Vulnerability: si.VulnReport{ - SecurityPolicy: "", - }, + { + GhsaId: "GHSA-yyyy-yyyy-yyyy", + Summary: "Second advisory", + State: "published", + PublishedAt: "2024-02-01T00:00:00Z", + }, + { + GhsaId: "GHSA-zzzz-zzzz-zzzz", + Summary: "Third advisory", + State: "published", + PublishedAt: "2024-03-01T00:00:00Z", }, }, }, - GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(false), + GraphqlRepoData: &data.GraphqlRepoData{}, }, }, { - name: "Both mechanisms available - GitHub policy takes priority", - expectedResult: layer4.Passed, - expectedMessage: "Public vulnerability disclosure available via GitHub security policy", + name: "No published security advisories", + expectedResult: layer4.Failed, + expectedMessage: "No published security advisories found", payloadData: data.Payload{ RestData: &data.RestData{ - Insights: si.SecurityInsights{ - Project: si.Project{ - Vulnerability: si.VulnReport{ - SecurityPolicy: "https://github.com/example/repo/blob/main/SECURITY.md", - }, - }, - }, + SecurityAdvisories: []data.SecurityAdvisory{}, }, - GraphqlRepoData: stubGraphqlRepoWithSecurityPolicy(true), + GraphqlRepoData: &data.GraphqlRepoData{}, }, }, { From daf847944102bba9dd796c24459959993b525814 Mon Sep 17 00:00:00 2001 From: Zohayb Bhatti Date: Tue, 30 Sep 2025 16:54:16 -0500 Subject: [PATCH 3/4] fix: resolve staticcheck QF1008 lint error by removing embedded field reference - Change data.RestData.SecurityAdvisories to data.SecurityAdvisories - Fixes golangci-lint staticcheck QF1008 error - RestData is embedded in Payload struct, so direct access is possible --- evaluation_plans/osps/vuln_management/steps.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evaluation_plans/osps/vuln_management/steps.go b/evaluation_plans/osps/vuln_management/steps.go index a087ad2..695da03 100644 --- a/evaluation_plans/osps/vuln_management/steps.go +++ b/evaluation_plans/osps/vuln_management/steps.go @@ -68,7 +68,7 @@ func hasPublicVulnerabilityDisclosure(payloadData any, _ map[string]*layer4.Chan return layer4.Unknown, message } - advisoryCount := len(data.RestData.SecurityAdvisories) + advisoryCount := len(data.SecurityAdvisories) if advisoryCount > 0 { if advisoryCount == 1 { return layer4.Passed, "Found 1 published security advisory" @@ -100,4 +100,4 @@ func hasPrivateVulnerabilityReporting(payloadData any, _ map[string]*layer4.Chan } return layer4.Failed, "No private vulnerability reporting contact method found in Security Insights data" -} \ No newline at end of file +} From c57b3be943447a5fbaa74743245c433eb9902116 Mon Sep 17 00:00:00 2001 From: Zohayb Bhatti Date: Thu, 2 Oct 2025 22:00:06 -0500 Subject: [PATCH 4/4] Implement hasPublicVulnerabilityDisclosure check for OSPS-VM-04 - Added function to check if security advisory publishing is enabled - Uses SecurityAdvisories != nil to detect feature availability - Added comprehensive test coverage with HTTP mocking - Positioned SecurityAdvisory struct below RestData per code review Signed-off-by: Zohayb Bhatti --- data/rest-data.go | 18 +++--- .../osps/vuln_management/steps.go | 11 +--- .../osps/vuln_management/steps_test.go | 59 +++++++++---------- 3 files changed, 39 insertions(+), 49 deletions(-) diff --git a/data/rest-data.go b/data/rest-data.go index 8929d0b..174710a 100644 --- a/data/rest-data.go +++ b/data/rest-data.go @@ -19,15 +19,6 @@ type HttpClient interface { Do(req *http.Request) (*http.Response, error) } -type SecurityAdvisory struct { - GhsaId string `json:"ghsa_id"` - CveId string `json:"cve_id"` - Summary string `json:"summary"` - Severity string `json:"severity"` - State string `json:"state"` - PublishedAt string `json:"published_at"` -} - type RestData struct { owner string repo string @@ -76,6 +67,15 @@ type WorkflowPermissions struct { CanApprovePullRequest bool `json:"can_approve_pull_request_reviews"` } +type SecurityAdvisory struct { + GhsaId string `json:"ghsa_id"` + CveId string `json:"cve_id"` + Summary string `json:"summary"` + Severity string `json:"severity"` + State string `json:"state"` + PublishedAt string `json:"published_at"` +} + var APIBase = "https://api.github.com" func (r *RestData) Setup() error { diff --git a/evaluation_plans/osps/vuln_management/steps.go b/evaluation_plans/osps/vuln_management/steps.go index 695da03..67c5a31 100644 --- a/evaluation_plans/osps/vuln_management/steps.go +++ b/evaluation_plans/osps/vuln_management/steps.go @@ -1,7 +1,6 @@ package vuln_management import ( - "fmt" "slices" "github.com/ossf/gemara/layer4" @@ -68,15 +67,11 @@ func hasPublicVulnerabilityDisclosure(payloadData any, _ map[string]*layer4.Chan return layer4.Unknown, message } - advisoryCount := len(data.SecurityAdvisories) - if advisoryCount > 0 { - if advisoryCount == 1 { - return layer4.Passed, "Found 1 published security advisory" - } - return layer4.Passed, fmt.Sprintf("Found %d published security advisories", advisoryCount) + if data.SecurityAdvisories != nil { + return layer4.Passed, "Security advisory publishing is enabled" } - return layer4.Failed, "No published security advisories found" + return layer4.Failed, "Security advisory publishing is not enabled" } func hasPrivateVulnerabilityReporting(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) { diff --git a/evaluation_plans/osps/vuln_management/steps_test.go b/evaluation_plans/osps/vuln_management/steps_test.go index 4b19001..389ec05 100644 --- a/evaluation_plans/osps/vuln_management/steps_test.go +++ b/evaluation_plans/osps/vuln_management/steps_test.go @@ -169,67 +169,57 @@ func TestHasPublicVulnerabilityDisclosure(t *testing.T) { tests := []struct { name string payloadData any + apiResponse []byte + apiError error expectedResult layer4.Result expectedMessage string }{ { - name: "One published security advisory", + name: "Security advisory publishing is enabled with advisories", expectedResult: layer4.Passed, - expectedMessage: "Found 1 published security advisory", + expectedMessage: "Security advisory publishing is enabled", payloadData: data.Payload{ RestData: &data.RestData{ SecurityAdvisories: []data.SecurityAdvisory{ { - GhsaId: "GHSA-xxxx-xxxx-xxxx", - Summary: "Test advisory", - State: "published", - PublishedAt: "2024-01-01T00:00:00Z", + GhsaId: "GHSA-1234-5678-9012", + CveId: "CVE-2024-12345", + Summary: "Test advisory", + Severity: "high", + State: "published", }, }, }, GraphqlRepoData: &data.GraphqlRepoData{}, }, + apiResponse: []byte(`[{"ghsa_id":"GHSA-1234-5678-9012","cve_id":"CVE-2024-12345","summary":"Test advisory","severity":"high","state":"published","published_at":"2024-01-01T00:00:00Z"}]`), + apiError: nil, }, { - name: "Multiple published security advisories", + name: "Security advisory publishing is enabled with no advisories", expectedResult: layer4.Passed, - expectedMessage: "Found 3 published security advisories", + expectedMessage: "Security advisory publishing is enabled", payloadData: data.Payload{ RestData: &data.RestData{ - SecurityAdvisories: []data.SecurityAdvisory{ - { - GhsaId: "GHSA-xxxx-xxxx-xxxx", - Summary: "First advisory", - State: "published", - PublishedAt: "2024-01-01T00:00:00Z", - }, - { - GhsaId: "GHSA-yyyy-yyyy-yyyy", - Summary: "Second advisory", - State: "published", - PublishedAt: "2024-02-01T00:00:00Z", - }, - { - GhsaId: "GHSA-zzzz-zzzz-zzzz", - Summary: "Third advisory", - State: "published", - PublishedAt: "2024-03-01T00:00:00Z", - }, - }, + SecurityAdvisories: []data.SecurityAdvisory{}, }, GraphqlRepoData: &data.GraphqlRepoData{}, }, + apiResponse: []byte(`[]`), + apiError: nil, }, { - name: "No published security advisories", + name: "Security advisory publishing is not enabled", expectedResult: layer4.Failed, - expectedMessage: "No published security advisories found", + expectedMessage: "Security advisory publishing is not enabled", payloadData: data.Payload{ RestData: &data.RestData{ - SecurityAdvisories: []data.SecurityAdvisory{}, + SecurityAdvisories: nil, }, GraphqlRepoData: &data.GraphqlRepoData{}, }, + apiResponse: []byte(`[]`), + apiError: nil, }, { name: "Invalid payload", @@ -241,6 +231,11 @@ func TestHasPublicVulnerabilityDisclosure(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { + if payload, ok := test.payloadData.(data.Payload); ok { + payload = data.NewPayloadWithHTTPMock(payload, test.apiResponse, 200, test.apiError) + test.payloadData = payload + } + result, message := hasPublicVulnerabilityDisclosure(test.payloadData, nil) assert.Equal(t, test.expectedResult, result) assert.Equal(t, test.expectedMessage, message) @@ -370,4 +365,4 @@ func TestHasPrivateVulnerabilityReporting(t *testing.T) { assert.Equal(t, test.expectedMessage, message) }) } -} \ No newline at end of file +}