From af3fa377709a68996c5f6efb387c1bdd39cad54b Mon Sep 17 00:00:00 2001 From: neilnaveen <42328488+neilnaveen@users.noreply.github.com> Date: Thu, 30 May 2024 15:12:03 -0500 Subject: [PATCH] Including the Scorecard API - Fixes #1892 - Updated tests - Added a README for certifier/scorecard Signed-off-by: neilnaveen <42328488+neilnaveen@users.noreply.github.com> --- cmd/guaccollect/cmd/osv.go | 10 +-- cmd/guacone/cmd/osv.go | 2 +- cmd/guacone/cmd/scorecard.go | 23 +++---- internal/testing/cmd/pubsub_test/cmd/osv.go | 3 +- internal/testing/mocks/scorecard.go | 12 ++-- pkg/certifier/certifier.go | 2 +- pkg/certifier/certify/certify.go | 11 ++-- pkg/certifier/certify/certify_test.go | 9 +-- pkg/certifier/osv/osv.go | 9 +-- pkg/certifier/osv/osv_test.go | 9 ++- pkg/certifier/scorecard/README.md | 29 ++++++++ pkg/certifier/scorecard/scorecard.go | 20 +----- pkg/certifier/scorecard/scorecardRunner.go | 66 +++++++++++++++++-- .../scorecard/scorecardRunner_test.go | 30 +++++---- pkg/certifier/scorecard/scorecard_test.go | 28 ++++---- pkg/certifier/scorecard/types.go | 4 +- 16 files changed, 169 insertions(+), 98 deletions(-) create mode 100644 pkg/certifier/scorecard/README.md diff --git a/cmd/guaccollect/cmd/osv.go b/cmd/guaccollect/cmd/osv.go index 6fe4f9a0c52..1db61f31d09 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 fd075b1475e..5cd84177827 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 8c6642917c6..f0ad8e40bb8 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 a886f675c9d..eac81cf1b9b 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 07ae4e9277a..67c2d13c873 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 a9939ec4baf..030106bb7b0 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 ac2ff656920..22a933968cf 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 2dc01be5db7..560cbc20e2a 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 0787bc160de..8e10a488ec4 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 629c14a168e..fb6dd291f31 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 00000000000..286b16b66a2 --- /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 1673c30b54a..276418f4be1 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 9cad48515f6..b493d3f519c 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 8c11d216271..4fef61b3945 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 4acd05b4989..a02e46a1692 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 fd81077c6ec..f58a7f9f550 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) }