diff --git a/data/payload.go b/data/payload.go index ae141892..befd1ada 100644 --- a/data/payload.go +++ b/data/payload.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/google/go-github/v71/github" + "github.com/google/go-github/v74/github" "github.com/privateerproj/privateer-sdk/config" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" @@ -18,8 +18,8 @@ type Payload struct { RepositoryMetadata RepositoryMetadata DependencyManifestsCount int IsCodeRepo bool - - client *githubv4.Client + SecurityPosture SecurityPosture + client *githubv4.Client } func Loader(config *config.Config) (payload any, err error) { @@ -32,7 +32,7 @@ func Loader(config *config.Config) (payload any, err error) { &oauth2.Token{AccessToken: config.GetString("token")}, ))) - repositoryMetadata, err := loadRepositoryMetadata(ghClient, config.GetString("owner"), config.GetString("repo")) + repo, repositoryMetadata, err := loadRepositoryMetadata(ghClient, config.GetString("owner"), config.GetString("repo")) if err != nil { return nil, err } @@ -52,6 +52,11 @@ func Loader(config *config.Config) (payload any, err error) { return nil, err } + securityPosture, err := buildSecurityPosture(repo, *rest) + if err != nil { + return nil, err + } + return any(Payload{ GraphqlRepoData: graphql, RestData: rest, @@ -60,6 +65,7 @@ func Loader(config *config.Config) (payload any, err error) { DependencyManifestsCount: dependencyManifestsCount, IsCodeRepo: isCodeRepo, client: client, + SecurityPosture: securityPosture, }), nil } diff --git a/data/repository_metadata.go b/data/repository_metadata.go index f2213d9c..5fcd6edc 100644 --- a/data/repository_metadata.go +++ b/data/repository_metadata.go @@ -3,7 +3,7 @@ package data import ( "context" - "github.com/google/go-github/v71/github" + "github.com/google/go-github/v74/github" ) type RepositoryMetadata interface { @@ -42,18 +42,18 @@ func (r *GitHubRepositoryMetadata) IsMFARequiredForAdministrativeActions() *bool return r.ghOrg.TwoFactorRequirementEnabled } -func loadRepositoryMetadata(ghClient *github.Client, owner, repo string) (data RepositoryMetadata, err error) { +func loadRepositoryMetadata(ghClient *github.Client, owner, repo string) (ghRepo *github.Repository, data RepositoryMetadata, err error) { repository, _, err := ghClient.Repositories.Get(context.Background(), owner, repo) if err != nil { - return &GitHubRepositoryMetadata{}, err + return repository, &GitHubRepositoryMetadata{}, err } organization, _, err := ghClient.Organizations.Get(context.Background(), owner) if err != nil { - return &GitHubRepositoryMetadata{ + return repository, &GitHubRepositoryMetadata{ ghRepo: repository, }, nil } - return &GitHubRepositoryMetadata{ + return repository, &GitHubRepositoryMetadata{ ghRepo: repository, ghOrg: organization, }, nil diff --git a/data/repository_metadata_test.go b/data/repository_metadata_test.go index e4017fde..94a78d2f 100644 --- a/data/repository_metadata_test.go +++ b/data/repository_metadata_test.go @@ -3,7 +3,7 @@ package data import ( "testing" - "github.com/google/go-github/v71/github" + "github.com/google/go-github/v74/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" ) @@ -81,7 +81,7 @@ func TestLoadRepositoryMetadata(t *testing.T) { testCase.responses..., ) ghClient := github.NewClient(mockClient) - repoMetadata, err := loadRepositoryMetadata(ghClient, testCase.owner, testCase.repo) + _, repoMetadata, err := loadRepositoryMetadata(ghClient, testCase.owner, testCase.repo) if testCase.expectedRepoError { assert.Error(t, err) } else { diff --git a/data/rest-data.go b/data/rest-data.go index 361fa701..456a6975 100644 --- a/data/rest-data.go +++ b/data/rest-data.go @@ -10,7 +10,7 @@ import ( "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" - "github.com/google/go-github/v71/github" + "github.com/google/go-github/v74/github" "github.com/ossf/si-tooling/v2/si" "github.com/privateerproj/privateer-sdk/config" ) @@ -347,9 +347,9 @@ func (r *RestData) GetRulesets(branchName string) []Ruleset { // to distinguish between programming, markup, data, and prose content types for more nuanced // repository classification. func (r *RestData) IsCodeRepo() (bool, error) { - languages, _, err := r.ghClient.Repositories.ListLanguages(context.Background(), r.owner, r.repo) - if err != nil { - return false, err - } - return len(languages) > 0, nil + languages, _, err := r.ghClient.Repositories.ListLanguages(context.Background(), r.owner, r.repo) + if err != nil { + return false, err + } + return len(languages) > 0, nil } diff --git a/data/rest-data_test.go b/data/rest-data_test.go index 546c706f..5edc07de 100644 --- a/data/rest-data_test.go +++ b/data/rest-data_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/google/go-github/v71/github" + "github.com/google/go-github/v74/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" ) diff --git a/data/security-posture.go b/data/security-posture.go new file mode 100644 index 00000000..05a13733 --- /dev/null +++ b/data/security-posture.go @@ -0,0 +1,62 @@ +package data + +import ( + "github.com/google/go-github/v74/github" + "github.com/ossf/si-tooling/v2/si" +) + +// SecurityPosture defines an interface for accessing security-related metadata about a repository. +type SecurityPosture interface { + PreventsPushingSecrets() bool + ScansForSecrets() bool + DefinesPolicyForHandlingSecrets() bool +} + +type RepoSecurityPosture struct { + restData RestData + preventsSecretPushing bool + scansForSecrets bool + definesPolicyForHandlingSecrets bool +} + +func buildSecurityPosture(repository *github.Repository, rd RestData) (SecurityPosture, error) { + securityConfig := repository.GetSecurityAndAnalysis() + if securityConfig == nil { + return &RepoSecurityPosture{ + restData: rd, + }, nil + } + secretsScanningStatus := securityConfig.GetSecretScanning().GetStatus() + insightsClaimsSecretsTooling := insightsClaimsSecretsTooling(rd.Insights) + return &RepoSecurityPosture{ + restData: rd, + preventsSecretPushing: secretsScanningStatus == "enabled" || insightsClaimsSecretsTooling, + scansForSecrets: secretsScanningStatus == "enabled" || insightsClaimsSecretsTooling, + // TODO: consider if SecurityInsights should have a policy doc field in ProjectDocumentation to handle this + // definesPolicyForHandlingSecrets: rd.SecurityInsights != nil && .... + }, nil +} + +func insightsClaimsSecretsTooling(insights si.SecurityInsights) bool { + if insights.Repository.Security.Tools == nil { + return false + } + for _, tool := range insights.Repository.Security.Tools { + if tool.Type == "secret-scanning" { + return true + } + } + return false +} + +func (rsp *RepoSecurityPosture) PreventsPushingSecrets() bool { + return rsp.preventsSecretPushing +} + +func (rsp *RepoSecurityPosture) ScansForSecrets() bool { + return rsp.scansForSecrets +} + +func (rsp *RepoSecurityPosture) DefinesPolicyForHandlingSecrets() bool { + return rsp.definesPolicyForHandlingSecrets +} diff --git a/data/security-posture_test.go b/data/security-posture_test.go new file mode 100644 index 00000000..91948e91 --- /dev/null +++ b/data/security-posture_test.go @@ -0,0 +1,94 @@ +package data + +import ( + "testing" + + "github.com/google/go-github/v74/github" + "github.com/ossf/si-tooling/v2/si" + "github.com/stretchr/testify/assert" +) + +func TestRepoSecurityPostureMethods(t *testing.T) { + rsp := &RepoSecurityPosture{ + preventsSecretPushing: true, + scansForSecrets: true, + definesPolicyForHandlingSecrets: false, + } + + assert.True(t, rsp.PreventsPushingSecrets()) + assert.True(t, rsp.ScansForSecrets()) + assert.False(t, rsp.DefinesPolicyForHandlingSecrets()) +} + +func TestBuildSecurityPosture_NoSecurityConfig(t *testing.T) { + repo := &github.Repository{} + rd := RestData{} + sp, err := buildSecurityPosture(repo, rd) + assert.NoError(t, err) + assert.NotNil(t, sp) + assert.False(t, sp.PreventsPushingSecrets()) + assert.False(t, sp.ScansForSecrets()) + assert.False(t, sp.DefinesPolicyForHandlingSecrets()) +} + +func TestBuildSecurityPosture_SecretScanningEnabled(t *testing.T) { + repo := &github.Repository{ + SecurityAndAnalysis: &github.SecurityAndAnalysis{ + SecretScanning: &github.SecretScanning{ + Status: github.Ptr("enabled"), + }, + }, + } + rd := RestData{} + sp, err := buildSecurityPosture(repo, rd) + assert.NoError(t, err) + assert.True(t, sp.PreventsPushingSecrets()) + assert.True(t, sp.ScansForSecrets()) +} + +func TestBuildSecurityPosture_SecretScanningDisabledButInsightsTooling(t *testing.T) { + repo := &github.Repository{ + SecurityAndAnalysis: &github.SecurityAndAnalysis{ + SecretScanning: &github.SecretScanning{ + Status: github.Ptr("disabled"), + }, + }, + } + rd := RestData{ + Insights: si.SecurityInsights{ + Repository: si.Repository{ + Security: si.SecurityInfo{ + Tools: []si.Tool{ + {Type: "secret-scanning"}, + }, + }, + }, + }, + } + sp, err := buildSecurityPosture(repo, rd) + assert.NoError(t, err) + assert.True(t, sp.PreventsPushingSecrets()) + assert.True(t, sp.ScansForSecrets()) +} + +func TestInsightsClaimsSecretsTooling(t *testing.T) { + insights := si.SecurityInsights{ + Repository: si.Repository{ + Security: si.SecurityInfo{ + Tools: []si.Tool{ + {Type: "secret-scanning"}, + {Type: "other-tool"}, + }, + }, + }, + } + assert.True(t, insightsClaimsSecretsTooling(insights)) + + insights.Repository.Security.Tools = []si.Tool{ + {Type: "other-tool"}, + } + assert.False(t, insightsClaimsSecretsTooling(insights)) + + insights.Repository.Security.Tools = nil + assert.False(t, insightsClaimsSecretsTooling(insights)) +} diff --git a/evaluation_plans/osps/build_release/evaluations.go b/evaluation_plans/osps/build_release/evaluations.go index 74eb491c..ee4b7fd5 100644 --- a/evaluation_plans/osps/build_release/evaluations.go +++ b/evaluation_plans/osps/build_release/evaluations.go @@ -174,3 +174,33 @@ func OSPS_BR_06() (evaluation *layer4.ControlEvaluation) { return } + +func OSPS_BR_07() (evaluation *layer4.ControlEvaluation) { + evaluation = &layer4.ControlEvaluation{ + ControlID: "OSPS-BR-07", + } + + evaluation.AddAssessment( + "OSPS-BR-07.01", + "The project MUST prevent the unintentional storage of unencrypted sensitive data, such as secrets and credentials, in the version control system.", + []string{ + "Maturity Level 1", + }, + []layer4.AssessmentStep{ + secretScanningInUse, + }, + ) + + evaluation.AddAssessment( + "OSPS-BR-07.02", + "The project MUST define a policy for managing secrets and credentials used by the project. The policy should include guidelines for storing, accessing, and rotating secrets and credentials.", + []string{ + "Maturity Level 3", + }, + []layer4.AssessmentStep{ + reusable_steps.NotImplemented, + }, + ) + + return +} diff --git a/evaluation_plans/osps/build_release/steps.go b/evaluation_plans/osps/build_release/steps.go index 4107c984..bb8a2553 100644 --- a/evaluation_plans/osps/build_release/steps.go +++ b/evaluation_plans/osps/build_release/steps.go @@ -307,3 +307,18 @@ func distributionPointsUseHTTPS(payloadData any, _ map[string]*layer4.Change) (r } return layer4.Passed, "All distribution points use HTTPS" } + +func secretScanningInUse(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.SecurityPosture.PreventsPushingSecrets() && data.SecurityPosture.ScansForSecrets() { + return layer4.Passed, "Secret scanning is enabled and prevents pushing secrets" + } else if data.SecurityPosture.PreventsPushingSecrets() || data.SecurityPosture.ScansForSecrets() { + return layer4.Failed, "Secret scanning is only partially enabled" + } else { + return layer4.Failed, "Secret scanning is not enabled" + } +} diff --git a/go.mod b/go.mod index c37f0b6d..4d60a01c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/goccy/go-yaml v1.18.0 - github.com/google/go-github/v71 v71.0.0 + github.com/google/go-github/v74 v74.0.0 github.com/migueleliasweb/go-github-mock v1.4.0 github.com/ossf/gemara v0.7.1 github.com/ossf/si-tooling/v2 v2.0.5-0.20250508212737-7ddcc8c43db9 @@ -18,6 +18,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/defenseunicorns/go-oscal v0.6.3 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-github/v71 v71.0.0 // indirect github.com/google/go-github/v73 v73.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 81c56bfb..148bc2a1 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+W github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= github.com/google/go-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= +github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= +github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=