diff --git a/data/rest-data.go b/data/rest-data.go index 5dc7e45..174710a 100644 --- a/data/rest-data.go +++ b/data/rest-data.go @@ -28,6 +28,7 @@ type RestData struct { WorkflowPermissions WorkflowPermissions Insights si.SecurityInsights Releases []ReleaseData + SecurityAdvisories []SecurityAdvisory Rulesets []Ruleset contents RepoContent ghClient *github.Client @@ -66,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 { @@ -77,6 +87,7 @@ func (r *RestData) Setup() error { r.loadSecurityInsights() _ = r.getWorkflowPermissions() _ = r.getReleases() + _ = r.getSecurityAdvisories() return nil } @@ -353,6 +364,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/evaluations.go b/evaluation_plans/osps/vuln_management/evaluations.go index bc74716..48ca0fd 100644 --- a/evaluation_plans/osps/vuln_management/evaluations.go +++ b/evaluation_plans/osps/vuln_management/evaluations.go @@ -85,7 +85,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 a8a9741..67c5a31 100644 --- a/evaluation_plans/osps/vuln_management/steps.go +++ b/evaluation_plans/osps/vuln_management/steps.go @@ -61,6 +61,19 @@ 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.SecurityAdvisories != nil { + return layer4.Passed, "Security advisory publishing is enabled" + } + + return layer4.Failed, "Security advisory publishing is not enabled" +} + func hasPrivateVulnerabilityReporting(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) { data, message := reusable_steps.VerifyPayload(payloadData) if message != "" { diff --git a/evaluation_plans/osps/vuln_management/steps_test.go b/evaluation_plans/osps/vuln_management/steps_test.go index 1e71642..389ec05 100644 --- a/evaluation_plans/osps/vuln_management/steps_test.go +++ b/evaluation_plans/osps/vuln_management/steps_test.go @@ -165,6 +165,84 @@ func TestHasVulnerabilityDisclosurePolicy(t *testing.T) { } } +func TestHasPublicVulnerabilityDisclosure(t *testing.T) { + tests := []struct { + name string + payloadData any + apiResponse []byte + apiError error + expectedResult layer4.Result + expectedMessage string + }{ + { + name: "Security advisory publishing is enabled with advisories", + expectedResult: layer4.Passed, + expectedMessage: "Security advisory publishing is enabled", + payloadData: data.Payload{ + RestData: &data.RestData{ + SecurityAdvisories: []data.SecurityAdvisory{ + { + 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: "Security advisory publishing is enabled with no advisories", + expectedResult: layer4.Passed, + expectedMessage: "Security advisory publishing is enabled", + payloadData: data.Payload{ + RestData: &data.RestData{ + SecurityAdvisories: []data.SecurityAdvisory{}, + }, + GraphqlRepoData: &data.GraphqlRepoData{}, + }, + apiResponse: []byte(`[]`), + apiError: nil, + }, + { + name: "Security advisory publishing is not enabled", + expectedResult: layer4.Failed, + expectedMessage: "Security advisory publishing is not enabled", + payloadData: data.Payload{ + RestData: &data.RestData{ + SecurityAdvisories: nil, + }, + GraphqlRepoData: &data.GraphqlRepoData{}, + }, + apiResponse: []byte(`[]`), + apiError: nil, + }, + { + 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) { + 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) + }) + } +} + func TestHasPrivateVulnerabilityReporting(t *testing.T) { tests := []struct { name string