diff --git a/cmd/guaccollect/cmd/osv.go b/cmd/guaccollect/cmd/osv.go index 6fe4f9a0c5..1db61f31d0 100644 --- a/cmd/guaccollect/cmd/osv.go +++ b/cmd/guaccollect/cmd/osv.go @@ -118,8 +118,8 @@ func validateOSVFlags( blobAddr, interval string, poll bool, - pubToQueue bool) (osvOptions, error) { - + pubToQueue bool, +) (osvOptions, error) { var opts osvOptions opts.graphqlEndpoint = graphqlEndpoint @@ -152,8 +152,8 @@ func getPackageQuery(client graphql.Client) (func() certifier.QueryComponents, e } func initializeNATsandCertifier(ctx context.Context, blobAddr, pubsubAddr string, - poll, publishToQueue bool, interval time.Duration, query certifier.QueryComponents) { - + poll, publishToQueue bool, interval time.Duration, query certifier.QueryComponents, +) { logger := logging.FromContext(ctx) blobStore, err := blob.NewBlobStore(ctx, blobAddr) @@ -210,7 +210,7 @@ func initializeNATsandCertifier(ctx context.Context, blobAddr, pubsubAddr string wg.Add(1) go func() { defer wg.Done() - if err := certify.Certify(ctx, query, emit, errHandler, poll, time.Minute*time.Duration(interval)); err != nil { + if err := certify.Certify(ctx, query, emit, errHandler, poll, time.Minute*time.Duration(interval), false); err != nil { logger.Fatal(err) } done <- true diff --git a/cmd/guacone/cmd/osv.go b/cmd/guacone/cmd/osv.go index fd075b1475..5cd8417782 100644 --- a/cmd/guacone/cmd/osv.go +++ b/cmd/guacone/cmd/osv.go @@ -180,7 +180,7 @@ var osvCmd = &cobra.Command{ wg.Add(1) go func() { defer wg.Done() - if err := certify.Certify(ctx, packageQuery, emit, errHandler, opts.poll, opts.interval); err != nil { + if err := certify.Certify(ctx, packageQuery, emit, errHandler, opts.poll, opts.interval, false); err != nil { logger.Errorf("Unhandled error in the certifier: %s", err) } done <- true diff --git a/cmd/guacone/cmd/scorecard.go b/cmd/guacone/cmd/scorecard.go index 8c6642917c..f0ad8e40bb 100644 --- a/cmd/guacone/cmd/scorecard.go +++ b/cmd/guacone/cmd/scorecard.go @@ -26,17 +26,15 @@ import ( "time" "github.com/Khan/genqlient/graphql" + "github.com/guacsec/guac/pkg/certifier" + "github.com/guacsec/guac/pkg/certifier/certify" sc "github.com/guacsec/guac/pkg/certifier/components/source" + "github.com/guacsec/guac/pkg/certifier/scorecard" "github.com/guacsec/guac/pkg/cli" "github.com/guacsec/guac/pkg/collectsub/client" csub_client "github.com/guacsec/guac/pkg/collectsub/client" - "github.com/guacsec/guac/pkg/ingestor" - - "github.com/guacsec/guac/pkg/certifier" - "github.com/guacsec/guac/pkg/certifier/scorecard" - - "github.com/guacsec/guac/pkg/certifier/certify" "github.com/guacsec/guac/pkg/handler/processor" + "github.com/guacsec/guac/pkg/ingestor" "github.com/guacsec/guac/pkg/logging" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -48,6 +46,7 @@ type scorecardOptions struct { poll bool interval time.Duration csubClientOptions client.CsubClientOptions + useScorecardAPI bool } var scorecardCmd = &cobra.Command{ @@ -62,6 +61,7 @@ var scorecardCmd = &cobra.Command{ viper.GetBool("csub-tls"), viper.GetBool("csub-tls-skip-verify"), viper.GetBool("poll"), + viper.GetBool("use-scorecard-api"), ) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -95,7 +95,6 @@ var scorecardCmd = &cobra.Command{ // running and getting the scorecard checks scorecardCertifier, err := scorecard.NewScorecardCertifier(scorecardRunner) - if err != nil { fmt.Printf("unable to create scorecard certifier: %v\n", err) _ = cmd.Help() @@ -105,7 +104,6 @@ var scorecardCmd = &cobra.Command{ // scorecard certifier is the certifier that gets the scorecard data graphQL // setting "daysSinceLastScan" to 0 does not check the timestamp on the scorecard that exist query, err := sc.NewCertifier(gqlclient, 0) - if err != nil { fmt.Printf("unable to create scorecard certifier: %v\n", err) _ = cmd.Help() @@ -125,7 +123,6 @@ var scorecardCmd = &cobra.Command{ emit := func(d *processor.Document) error { totalNum += 1 err := ingestor.Ingest(ctx, d, opts.graphqlEndpoint, transport, csubClient) - if err != nil { return fmt.Errorf("unable to ingest document: %v", err) } @@ -149,7 +146,7 @@ var scorecardCmd = &cobra.Command{ wg.Add(1) go func() { defer wg.Done() - if err := certify.Certify(ctx, query, emit, errHandler, opts.poll, opts.interval); err != nil { + if err := certify.Certify(ctx, query, emit, errHandler, opts.poll, opts.interval, opts.useScorecardAPI); err != nil { logger.Errorf("Unhandled error in the certifier: %s", err) } done <- true @@ -180,7 +177,8 @@ func validateScorecardFlags( interval string, csubTls, csubTlsSkipVerify, - poll bool, + poll, + useScorecardAPI bool, ) (scorecardOptions, error) { var opts scorecardOptions opts.graphqlEndpoint = graphqlEndpoint @@ -193,6 +191,7 @@ func validateScorecardFlags( opts.csubClientOptions = csubOpts opts.poll = poll + opts.useScorecardAPI = useScorecardAPI i, err := time.ParseDuration(interval) if err != nil { return opts, err @@ -204,4 +203,6 @@ func validateScorecardFlags( func init() { certifierCmd.AddCommand(scorecardCmd) + scorecardCmd.Flags().Bool("use-scorecard-api", false, "use the scorecard API") + viper.BindPFlag("use-scorecard-api", scorecardCmd.Flags().Lookup("use-scorecard-api")) } diff --git a/internal/testing/cmd/pubsub_test/cmd/osv.go b/internal/testing/cmd/pubsub_test/cmd/osv.go index a886f675c9..eac81cf1b9 100644 --- a/internal/testing/cmd/pubsub_test/cmd/osv.go +++ b/internal/testing/cmd/pubsub_test/cmd/osv.go @@ -56,7 +56,6 @@ var osvCmd = &cobra.Command{ viper.GetBool("poll"), viper.GetInt("interval"), ) - if err != nil { fmt.Printf("unable to validate flags: %v\n", err) _ = cmd.Help() @@ -158,7 +157,7 @@ func initializeNATsandCertifier(ctx context.Context, opts options) { wg.Add(1) go func() { defer wg.Done() - if err := certify.Certify(ctx, packageQueryFunc(), emit, errHandler, opts.poll, time.Minute*time.Duration(opts.interval)); err != nil { + if err := certify.Certify(ctx, packageQueryFunc(), emit, errHandler, opts.poll, time.Minute*time.Duration(opts.interval), false); err != nil { logger.Fatal(err) } done <- true diff --git a/internal/testing/mocks/scorecard.go b/internal/testing/mocks/scorecard.go index 07ae4e9277..67c2d13c87 100644 --- a/internal/testing/mocks/scorecard.go +++ b/internal/testing/mocks/scorecard.go @@ -10,9 +10,9 @@ package mocks import ( + bytes "bytes" reflect "reflect" - pkg "github.com/ossf/scorecard/v4/pkg" gomock "go.uber.org/mock/gomock" ) @@ -40,16 +40,16 @@ func (m *MockScorecard) EXPECT() *MockScorecardMockRecorder { } // GetScore mocks base method. -func (m *MockScorecard) GetScore(repoName, commitSHA, tag string) (*pkg.ScorecardResult, error) { +func (m *MockScorecard) GetScore(repoName, commitSHA, tag string, useScorecardAPI bool) (*bytes.Buffer, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetScore", repoName, commitSHA, tag) - ret0, _ := ret[0].(*pkg.ScorecardResult) + ret := m.ctrl.Call(m, "GetScore", repoName, commitSHA, tag, useScorecardAPI) + ret0, _ := ret[0].(*bytes.Buffer) ret1, _ := ret[1].(error) return ret0, ret1 } // GetScore indicates an expected call of GetScore. -func (mr *MockScorecardMockRecorder) GetScore(repoName, commitSHA, tag any) *gomock.Call { +func (mr *MockScorecardMockRecorder) GetScore(repoName, commitSHA, tag, useScorecardAPI any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScore", reflect.TypeOf((*MockScorecard)(nil).GetScore), repoName, commitSHA, tag) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScore", reflect.TypeOf((*MockScorecard)(nil).GetScore), repoName, commitSHA, tag, useScorecardAPI) } diff --git a/pkg/certifier/certifier.go b/pkg/certifier/certifier.go index a9939ec4ba..030106bb7b 100644 --- a/pkg/certifier/certifier.go +++ b/pkg/certifier/certifier.go @@ -26,7 +26,7 @@ type Certifier interface { // push to the docChannel to be ingested. // Note: there is an implicit contract with "QueryComponents" where the compChan type must be the same as // the one used by "components" - CertifyComponent(ctx context.Context, components interface{}, docChannel chan<- *processor.Document) error + CertifyComponent(ctx context.Context, components interface{}, docChannel chan<- *processor.Document, useScorecardAPI bool) error } type QueryComponents interface { diff --git a/pkg/certifier/certify/certify.go b/pkg/certifier/certify/certify.go index ac2ff65692..22a933968c 100644 --- a/pkg/certifier/certify/certify.go +++ b/pkg/certifier/certify/certify.go @@ -52,8 +52,7 @@ func RegisterCertifier(c func() certifier.Certifier, certifierType certifier.Cer // Certify queries the graph DB to get the components to scan. Utilizing the registered certifiers, // it generates new nodes and attestations. -func Certify(ctx context.Context, query certifier.QueryComponents, emitter certifier.Emitter, handleErr certifier.ErrHandler, poll bool, interval time.Duration) error { - +func Certify(ctx context.Context, query certifier.QueryComponents, emitter certifier.Emitter, handleErr certifier.ErrHandler, poll bool, interval time.Duration, useScorecardAPI bool) error { runCertifier := func() error { // compChan to collect query components compChan := make(chan interface{}, BufferChannelSize) @@ -70,7 +69,7 @@ func Certify(ctx context.Context, query certifier.QueryComponents, emitter certi for !componentsCaptured { select { case d := <-compChan: - if err := generateDocuments(ctx, d, emitter, handleErr); err != nil { + if err := generateDocuments(ctx, d, emitter, handleErr, useScorecardAPI); err != nil { return fmt.Errorf("generate certifier documents error: %w", err) } case err := <-errChan: @@ -84,7 +83,7 @@ func Certify(ctx context.Context, query certifier.QueryComponents, emitter certi } for len(compChan) > 0 { d := <-compChan - if err := generateDocuments(ctx, d, emitter, handleErr); err != nil { + if err := generateDocuments(ctx, d, emitter, handleErr, useScorecardAPI); err != nil { logger.Errorf("generate certifier documents error: %v", err) } } @@ -118,7 +117,7 @@ func Certify(ctx context.Context, query certifier.QueryComponents, emitter certi // generateDocuments runs CertifyVulns as a goroutine to scan and generates attestations that // are emitted as processor documents to be ingested -func generateDocuments(ctx context.Context, collectedComponent interface{}, emitter certifier.Emitter, handleErr certifier.ErrHandler) error { +func generateDocuments(ctx context.Context, collectedComponent interface{}, emitter certifier.Emitter, handleErr certifier.ErrHandler, useScorecardAPI bool) error { // docChan to collect artifacts docChan := make(chan *processor.Document, BufferChannelSize) // errChan to receive error from collectors @@ -129,7 +128,7 @@ func generateDocuments(ctx context.Context, collectedComponent interface{}, emit for _, certifier := range documentCertifier { c := certifier() go func() { - errChan <- c.CertifyComponent(ctx, collectedComponent, docChan) + errChan <- c.CertifyComponent(ctx, collectedComponent, docChan, useScorecardAPI) }() } diff --git a/pkg/certifier/certify/certify_test.go b/pkg/certifier/certify/certify_test.go index 2dc01be5db..560cbc20e2 100644 --- a/pkg/certifier/certify/certify_test.go +++ b/pkg/certifier/certify/certify_test.go @@ -30,8 +30,7 @@ import ( "github.com/guacsec/guac/pkg/logging" ) -type mockQuery struct { -} +type mockQuery struct{} // NewMockQuery initializes the mockQuery to query for tests func newMockQuery() certifier.QueryComponents { @@ -44,8 +43,7 @@ func (q *mockQuery) GetComponents(ctx context.Context, compChan chan<- interface return nil } -type mockUnknownQuery struct { -} +type mockUnknownQuery struct{} // NewMockQuery initializes the mockQuery to query for tests func newMockUnknownQuery() certifier.QueryComponents { @@ -170,7 +168,6 @@ func TestCertify(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := logging.WithLogger(context.Background()) if tt.poll { var cancel context.CancelFunc @@ -184,7 +181,7 @@ func TestCertify(t *testing.T) { return nil } - err := Certify(ctx, tt.query, emit, errHandler, tt.poll, time.Second*1) + err := Certify(ctx, tt.query, emit, errHandler, tt.poll, time.Second*1, false) if (err != nil) != tt.wantErr { t.Errorf("Certify() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/pkg/certifier/osv/osv.go b/pkg/certifier/osv/osv.go index 0787bc160d..8e10a488ec 100644 --- a/pkg/certifier/osv/osv.go +++ b/pkg/certifier/osv/osv.go @@ -22,16 +22,14 @@ import ( "net/http" "time" - jsoniter "github.com/json-iterator/go" - osv_scanner "github.com/google/osv-scanner/pkg/osv" - intoto "github.com/in-toto/in-toto-golang/in_toto" - "github.com/guacsec/guac/pkg/certifier" attestation_vuln "github.com/guacsec/guac/pkg/certifier/attestation" "github.com/guacsec/guac/pkg/certifier/components/root_package" "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/version" + intoto "github.com/in-toto/in-toto-golang/in_toto" + jsoniter "github.com/json-iterator/go" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary @@ -60,7 +58,7 @@ func NewOSVCertificationParser() certifier.Certifier { // CertifyComponent takes in the root component from the gauc database and does a recursive scan // to generate vulnerability attestations -func (o *osvCertifier) CertifyComponent(ctx context.Context, rootComponent interface{}, docChannel chan<- *processor.Document) error { +func (o *osvCertifier) CertifyComponent(ctx context.Context, rootComponent interface{}, docChannel chan<- *processor.Document, _ bool) error { packageNodes, ok := rootComponent.([]*root_package.PackageNode) if !ok { return ErrOSVComponenetTypeMismatch @@ -112,7 +110,6 @@ func generateDocument(packNodes []*root_package.PackageNode, vulns []osv_scanner } func createAttestation(packageNode *root_package.PackageNode, vulns []osv_scanner.MinimalVulnerability, currentTime time.Time) *attestation_vuln.VulnerabilityStatement { - attestation := &attestation_vuln.VulnerabilityStatement{ StatementHeader: intoto.StatementHeader{ Type: intoto.StatementInTotoV01, diff --git a/pkg/certifier/osv/osv_test.go b/pkg/certifier/osv/osv_test.go index 629c14a168..fb6dd291f3 100644 --- a/pkg/certifier/osv/osv_test.go +++ b/pkg/certifier/osv/osv_test.go @@ -23,14 +23,13 @@ import ( "time" osv_scanner "github.com/google/osv-scanner/pkg/osv" - attestation_vuln "github.com/guacsec/guac/pkg/certifier/attestation" - "github.com/guacsec/guac/pkg/certifier/components/root_package" - intoto "github.com/in-toto/in-toto-golang/in_toto" - "github.com/guacsec/guac/internal/testing/dochelper" "github.com/guacsec/guac/internal/testing/testdata" + attestation_vuln "github.com/guacsec/guac/pkg/certifier/attestation" + "github.com/guacsec/guac/pkg/certifier/components/root_package" "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/logging" + intoto "github.com/in-toto/in-toto-golang/in_toto" ) func TestOSVCertifier_CertifyVulns(t *testing.T) { @@ -150,7 +149,7 @@ func TestOSVCertifier_CertifyVulns(t *testing.T) { defer close(docChan) defer close(errChan) go func() { - errChan <- o.CertifyComponent(ctx, tt.rootComponent, docChan) + errChan <- o.CertifyComponent(ctx, tt.rootComponent, docChan, false) }() numCollectors := 1 certifiersDone := 0 diff --git a/pkg/certifier/scorecard/README.md b/pkg/certifier/scorecard/README.md new file mode 100644 index 0000000000..286b16b66a --- /dev/null +++ b/pkg/certifier/scorecard/README.md @@ -0,0 +1,29 @@ +# Scorecard Certifier + +The Scorecard Certifier is a component that generates scorecard attestations for repositories. It uses the [OpenSSF Scorecard](https://github.com/ossf/scorecard) to evaluate the security posture of a repository. + +## How It Works + +### Initialization + +The `NewScorecardCertifier` function initializes the scorecard certifier. It checks if the `GITHUB_AUTH_TOKEN` is set in the environment. If not, it returns an error. The token is used to access the GitHub API. + +### Certifying Components + +The `CertifyComponent` function takes a `source.SourceNode` as input and generates a scorecard attestation. It uses the `GetScore` function to retrieve the scorecard data for the repository. + +### Using the Scorecard Library and GitHub Auth Token + +The `GetScore` function first checks if the `useScorecardAPI` flag is set to `true`. If it is, it calls the Scorecard API to retrieve the scorecard data. If the API call fails, it uses the Scorecard library and the GitHub auth token to retrieve the scorecard data. + +### Using the Scorecard API + +The Scorecard API is a public API that provides access to scorecard data. It can be used to retrieve scorecard data for any repository, regardless of whether the user has access to the GitHub repository. However, the API might fail if the repository does not exist in its database. + +### Differences + +The main difference between using the GitHub auth token/Scorecard library and the Scorecard API is that the GitHub auth token/Scorecard library requires access to the GitHub repository, while the Scorecard API does not. + +The Scorecard API is also more efficient than using the GitHub auth token/Scorecard library, as it does not need to download the entire repository. + +If the `useScorecardAPI` flag is not set, or the Scorecard API call fails, the certifier will default to using the GitHub auth token/Scorecard library. \ No newline at end of file diff --git a/pkg/certifier/scorecard/scorecard.go b/pkg/certifier/scorecard/scorecard.go index 1673c30b54..276418f4be 100644 --- a/pkg/certifier/scorecard/scorecard.go +++ b/pkg/certifier/scorecard/scorecard.go @@ -16,16 +16,12 @@ package scorecard import ( - "bytes" "context" "fmt" "os" "github.com/guacsec/guac/pkg/certifier" "github.com/guacsec/guac/pkg/certifier/components/source" - "github.com/ossf/scorecard/v4/docs/checks" - "github.com/ossf/scorecard/v4/log" - "github.com/guacsec/guac/pkg/handler/processor" ) @@ -38,7 +34,7 @@ type scorecard struct { var ErrArtifactNodeTypeMismatch = fmt.Errorf("rootComponent type is not *source.SourceNode") // CertifyComponent is a certifier that generates scorecard attestations -func (s scorecard) CertifyComponent(_ context.Context, rootComponent interface{}, docChannel chan<- *processor.Document) error { +func (s scorecard) CertifyComponent(_ context.Context, rootComponent interface{}, docChannel chan<- *processor.Document, useScorecardAPI bool) error { if docChannel == nil { return fmt.Errorf("docChannel cannot be nil") } @@ -62,23 +58,13 @@ func (s scorecard) CertifyComponent(_ context.Context, rootComponent interface{} return fmt.Errorf("source repo cannot be empty") } - score, err := s.scorecard.GetScore(sourceNode.Repo, sourceNode.Commit, sourceNode.Tag) + score, err := s.scorecard.GetScore(sourceNode.Repo, sourceNode.Commit, sourceNode.Tag, useScorecardAPI) if err != nil { return fmt.Errorf("error getting scorecard result: %w", err) } - var scorecardResults bytes.Buffer - docs, err := checks.Read() - if err != nil { - return fmt.Errorf("error getting scorecard docs: %w", err) - } - - if err = score.AsJSON2(true, log.DefaultLevel, docs, &scorecardResults); err != nil { - return fmt.Errorf("error getting scorecard results: %w", err) - } - res := processor.Document{ - Blob: scorecardResults.Bytes(), + Blob: score.Bytes(), Format: processor.FormatJSON, Type: processor.DocumentScorecard, SourceInformation: processor.SourceInformation{ diff --git a/pkg/certifier/scorecard/scorecardRunner.go b/pkg/certifier/scorecard/scorecardRunner.go index 9cad48515f..b493d3f519 100644 --- a/pkg/certifier/scorecard/scorecardRunner.go +++ b/pkg/certifier/scorecard/scorecardRunner.go @@ -16,25 +16,31 @@ package scorecard import ( + "bytes" "context" "fmt" + "net/http" + "strings" + jsoniter "github.com/json-iterator/go" "github.com/ossf/scorecard/v4/checker" "github.com/ossf/scorecard/v4/checks" + doccheck "github.com/ossf/scorecard/v4/docs/checks" "github.com/ossf/scorecard/v4/log" sc "github.com/ossf/scorecard/v4/pkg" ) +var json = jsoniter.ConfigCompatibleWithStandardLibrary + // scorecardRunner is a struct that implements the Scorecard interface. type scorecardRunner struct { ctx context.Context } -func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.ScorecardResult, error) { +func (s scorecardRunner) GetScore(repoName, commitSHA, tag string, useScorecardAPI bool) (*bytes.Buffer, error) { // Can't use guacs standard logger because scorecard uses a different logger. defaultLogger := log.NewLogger(log.DefaultLevel) repo, repoClient, ossFuzzClient, ciiClient, vulnsClient, err := checker.GetClients(s.ctx, repoName, "", defaultLogger) - if err != nil { return nil, fmt.Errorf("error, failed to get clients: %w", err) } @@ -63,12 +69,12 @@ func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.Scorecar if err := repoClient.InitRepo(repo, commitSHA, 0); err != nil { return nil, fmt.Errorf("error, failed to initialize repoClient: %w", err) } + defer repoClient.Close() releases, err := repoClient.ListReleases() if err != nil { return nil, fmt.Errorf("error, failed to run releases: %w", err) - } for _, release := range releases { @@ -79,6 +85,15 @@ func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.Scorecar } } + if useScorecardAPI { + apiResult, err := s.callScorecardAPI(repoName, commitSHA) + if err == nil { + return apiResult, nil + } + defaultLogger.Info(fmt.Sprintf("scorecardAPI failed, using the github API, repoName = %s, err = %s", repoName, err.Error())) + } + + // This happens either if we do not want to use the scorecard API or, the call to the scorecard API failed. res, err := sc.RunScorecard(s.ctx, repo, commitSHA, 0, enabledChecks, repoClient, ossFuzzClient, ciiClient, vulnsClient) if err != nil { return nil, fmt.Errorf("error, failed to run scorecard: %w", err) @@ -87,7 +102,18 @@ func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.Scorecar // The commit SHA can be invalid or the repo can be private. return nil, fmt.Errorf("error, failed to get scorecard data for repo %v, commit SHA %v", res.Repo.Name, commitSHA) } - return &res, nil + + var scorecardResults bytes.Buffer + docs, err := doccheck.Read() + if err != nil { + return nil, fmt.Errorf("error getting scorecard docs: %w", err) + } + + if err = res.AsJSON2(true, log.DefaultLevel, docs, &scorecardResults); err != nil { + return nil, fmt.Errorf("error getting scorecard results: %w", err) + } + + return &scorecardResults, nil } func NewScorecardRunner(ctx context.Context) (Scorecard, error) { @@ -95,3 +121,35 @@ func NewScorecardRunner(ctx context.Context) (Scorecard, error) { ctx, }, nil } + +func (s scorecardRunner) callScorecardAPI(repoName string, commitSHA string) (*bytes.Buffer, error) { + var jsonResult bytes.Buffer + encoder := json.NewEncoder(&jsonResult) + + splitName := strings.Split(repoName, "/") + if len(splitName) != 3 { + return nil, fmt.Errorf("error, invalid repo name format: %s", repoName) + } + + apiURL := fmt.Sprintf("https://api.securityscorecards.dev/projects/%s/%s/%scommit=%s", splitName[0], splitName[1], splitName[2], commitSHA) + resp, err := http.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("failed to call Scorecard API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("scorecard API returned status: %s", resp.Status) + } + + var apiResult sc.JSONScorecardResultV2 + if err := json.NewDecoder(resp.Body).Decode(&apiResult); err != nil { + return nil, fmt.Errorf("failed to decode Scorecard API response: %w", err) + } + + if err := encoder.Encode(apiResult); err != nil { + return nil, err + } + + return &jsonResult, nil +} diff --git a/pkg/certifier/scorecard/scorecardRunner_test.go b/pkg/certifier/scorecard/scorecardRunner_test.go index 8c11d21627..4fef61b394 100644 --- a/pkg/certifier/scorecard/scorecardRunner_test.go +++ b/pkg/certifier/scorecard/scorecardRunner_test.go @@ -26,24 +26,26 @@ import ( func Test_scorecardRunner_GetScore(t *testing.T) { newsc, _ := NewScorecardRunner(context.Background()) tests := []struct { - name string - sc Scorecard - repoName string - commit string - tag string - wantErr bool + name string + sc Scorecard + repoName string + commit string + tag string + wantErr bool + useScorecardAPI bool }{{ - name: "actual test", + name: "with the scorecard library/github auth token", sc: newsc, repoName: "github.com/ossf/scorecard", commit: "98316298749fdd62d3cc99423baec45ae11af662", tag: "", }, { - name: "actual test", - sc: newsc, - repoName: "github.com/ossf/scorecard", - commit: "HEAD", - tag: "v4.10.4", + name: "with the scorecard API", + sc: newsc, + repoName: "github.com/ossf/scorecard", + commit: "HEAD", + tag: "v4.10.4", + useScorecardAPI: true, }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -55,12 +57,12 @@ func Test_scorecardRunner_GetScore(t *testing.T) { t.Fatalf("GITHUB_AUTH_TOKEN is not set") } t.Setenv("GITHUB_AUTH_TOKEN", ghToken) - got, err := test.sc.GetScore(test.repoName, test.commit, test.tag) + got, err := test.sc.GetScore(test.repoName, test.commit, test.tag, test.useScorecardAPI) if (err != nil) != test.wantErr { t.Errorf("GetScore() error = %v, wantErr %v", err, test.wantErr) return } - t.Logf("scorecard result: %v", got.Repo.Name) + t.Logf("scorecard result: %v", got) }) } } diff --git a/pkg/certifier/scorecard/scorecard_test.go b/pkg/certifier/scorecard/scorecard_test.go index 4acd05b498..a02e46a169 100644 --- a/pkg/certifier/scorecard/scorecard_test.go +++ b/pkg/certifier/scorecard/scorecard_test.go @@ -16,9 +16,11 @@ package scorecard import ( + "bytes" "context" "fmt" "reflect" + "strings" "testing" "time" @@ -26,14 +28,13 @@ import ( "github.com/guacsec/guac/pkg/certifier" "github.com/guacsec/guac/pkg/certifier/components/source" "github.com/guacsec/guac/pkg/handler/processor" - "github.com/ossf/scorecard/v4/pkg" "go.uber.org/mock/gomock" ) type mockScorecard struct{} -func (m mockScorecard) GetScore(repoName, commitSHA, tag string) (*pkg.ScorecardResult, error) { - return &pkg.ScorecardResult{}, nil +func (m mockScorecard) GetScore(repoName, commitSHA, tag string, useScorecardAPI bool) (*bytes.Buffer, error) { + return &bytes.Buffer{}, nil } func TestNewScorecard(t *testing.T) { @@ -91,8 +92,9 @@ func Test_CertifyComponent(t *testing.T) { sourceNode *source.SourceNode } type args struct { - rootComponent interface{} - docChannel chan<- *processor.Document + rootComponent interface{} + docChannel chan<- *processor.Document + useScorecardAPI bool } tests := []struct { name string @@ -184,8 +186,8 @@ func Test_CertifyComponent(t *testing.T) { t.Run(test.name, func(t *testing.T) { ctrl := gomock.NewController(t) sc := mocks.NewMockScorecard(ctrl) - sc.EXPECT().GetScore(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(a, b, c string) (*pkg.ScorecardResult, error) { + sc.EXPECT().GetScore(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(a, b, c string, d bool) (*bytes.Buffer, error) { if test.getScoreShouldReturnErr { return nil, fmt.Errorf("error") } @@ -196,7 +198,7 @@ func Test_CertifyComponent(t *testing.T) { scorecard: sc, ghToken: test.fields.ghToken, } - if err := s.CertifyComponent(ctx, test.args.rootComponent, test.args.docChannel); (err != nil) != test.wantErr { + if err := s.CertifyComponent(ctx, test.args.rootComponent, test.args.docChannel, test.args.useScorecardAPI); (err != nil) != test.wantErr { t.Errorf("CertifyComponent() error = %v, wantErr %v", err, test.wantErr) } }) @@ -209,9 +211,11 @@ func TestCertifyComponentDefaultCase(t *testing.T) { ctrl := gomock.NewController(t) scMock := mocks.NewMockScorecard(ctrl) - scMock.EXPECT().GetScore(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(a, b, c string) (*pkg.ScorecardResult, error) { - return &pkg.ScorecardResult{}, nil + scMock.EXPECT().GetScore(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(a, b, c string, d bool) (*bytes.Buffer, error) { + buf := &bytes.Buffer{} + buf.WriteString(strings.Repeat("a", 101)) + return buf, nil }).AnyTimes() // Create a mock source.SourceNode to use as input @@ -231,7 +235,7 @@ func TestCertifyComponentDefaultCase(t *testing.T) { // valid input docChannel := make(chan *processor.Document, 2) - err := sc.CertifyComponent(ctx, source, docChannel) + err := sc.CertifyComponent(ctx, source, docChannel, false) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/pkg/certifier/scorecard/types.go b/pkg/certifier/scorecard/types.go index fd81077c6e..f58a7f9f55 100644 --- a/pkg/certifier/scorecard/types.go +++ b/pkg/certifier/scorecard/types.go @@ -16,10 +16,10 @@ package scorecard import ( - sc "github.com/ossf/scorecard/v4/pkg" + "bytes" ) // Scorecard is an interface for the scorecard library. This can also be mocked for testing. type Scorecard interface { - GetScore(repoName, commitSHA, tag string) (*sc.ScorecardResult, error) + GetScore(repoName, commitSHA, tag string, useScorecardAPI bool) (*bytes.Buffer, error) }