@@ -18,7 +18,13 @@ package scorecard
1818import (
1919 "context"
2020 "fmt"
21+ "io"
22+ "net/http"
23+ "net/url"
24+ "os"
25+ "time"
2126
27+ "github.com/guacsec/guac/pkg/logging"
2228 "github.com/ossf/scorecard/v4/checker"
2329 "github.com/ossf/scorecard/v4/checks"
2430 "github.com/ossf/scorecard/v4/log"
@@ -31,6 +37,102 @@ type scorecardRunner struct {
3137}
3238
3339func (s scorecardRunner ) GetScore (repoName , commitSHA , tag string ) (* sc.ScorecardResult , error ) {
40+ logger := logging .FromContext (s .ctx )
41+
42+ // First try API approach
43+ logger .Debugf ("Attempting to fetch scorecard from API for repo: %s, commit: %s" , repoName , commitSHA )
44+ result , err := s .getScoreFromAPI (repoName , commitSHA , tag )
45+ if err == nil {
46+ logger .Infof ("Successfully fetched scorecard from API for repo: %s" , repoName )
47+ return result , nil
48+ }
49+
50+ // Log API failure and check if we can fallback to local computation
51+ logger .Warnf ("API fetch failed for repo %s: %v" , repoName , err )
52+
53+ // Check if GitHub token is available for local computation
54+ if _ , ok := os .LookupEnv ("GITHUB_AUTH_TOKEN" ); ! ok {
55+ logger .Errorf ("Cannot fall back to local computation - GITHUB_AUTH_TOKEN not set" )
56+ return nil , fmt .Errorf ("scorecard API failed and GITHUB_AUTH_TOKEN not available for local computation: %w" , err )
57+ }
58+
59+ logger .Infof ("Falling back to local computation for repo: %s" , repoName )
60+ result , err = s .computeScore (repoName , commitSHA , tag )
61+ if err != nil {
62+ logger .Errorf ("Failed to compute scorecard locally for repo %s: %v" , repoName , err )
63+ }
64+ return result , err
65+ }
66+
67+ func (s scorecardRunner ) getScoreFromAPI (repoName , commitSHA , tag string ) (* sc.ScorecardResult , error ) {
68+ logger := logging .FromContext (s .ctx )
69+
70+ // The Scorecard API only supports commit SHAs, not tags.
71+ // If a tag is provided without a commitSHA, we cannot use the API
72+ // and must fall back to local computation to avoid returning incorrect results.
73+ if (commitSHA == "" || commitSHA == "HEAD" ) && tag != "" {
74+ logger .Debugf ("Cannot use API for tag %s without commit SHA - will fall back to local computation" , tag )
75+ return nil , fmt .Errorf ("scorecard API does not support tags; commit SHA required for tag %s" , tag )
76+ }
77+
78+ url , err := url .JoinPath ("https://api.securityscorecards.dev" , "projects" , repoName )
79+ if err != nil {
80+ return nil , err
81+ }
82+
83+ if commitSHA != "" && commitSHA != "HEAD" {
84+ url += "?commit=" + commitSHA
85+ }
86+
87+ logger .Debugf ("Making API request to: %s" , url )
88+
89+ httpClient := & http.Client {
90+ Timeout : 30 * time .Second ,
91+ }
92+
93+ req , err := http .NewRequestWithContext (s .ctx , http .MethodGet , url , nil )
94+ if err != nil {
95+ return nil , fmt .Errorf ("failed to create request: %w" , err )
96+ }
97+
98+ req .Header .Set ("User-Agent" , "guac-scorecard-certifier/1.0" )
99+ req .Header .Set ("Accept" , "application/json" )
100+
101+ resp , err := httpClient .Do (req )
102+ if err != nil {
103+ return nil , fmt .Errorf ("scorecard request failed: %w" , err )
104+ }
105+ defer func () {
106+ if resp != nil && resp .Body != nil {
107+ _ = resp .Body .Close ()
108+ }
109+ }()
110+
111+ logger .Debugf ("API response status code: %d" , resp .StatusCode )
112+
113+ if resp .StatusCode == http .StatusNotFound {
114+ return nil , fmt .Errorf ("Scorecard for repo %s not found in scorecard API" , repoName )
115+ }
116+
117+ if resp .StatusCode >= 400 {
118+ body , _ := io .ReadAll (resp .Body )
119+ return nil , fmt .Errorf ("API returned status %d: %s" , resp .StatusCode , string (body ))
120+ }
121+
122+ // Use scorecard's built-in JSON parser, which is experimental
123+ // but still better then rolling out your own type
124+ result , _ , err := sc .ExperimentalFromJSON2 (resp .Body )
125+ if err != nil {
126+ return nil , fmt .Errorf ("failed to decode API response: %w" , err )
127+ }
128+
129+ return & result , nil
130+ }
131+
132+ func (s scorecardRunner ) computeScore (repoName , commitSHA , tag string ) (* sc.ScorecardResult , error ) {
133+ logger := logging .FromContext (s .ctx )
134+ logger .Infof ("Starting local scorecard computation for repo: %s, commit: %s, tag: %s" , repoName , commitSHA , tag )
135+
34136 // Can't use guacs standard logger because scorecard uses a different logger.
35137 defaultLogger := log .NewLogger (log .DefaultLevel )
36138 repo , repoClient , ossFuzzClient , ciiClient , vulnsClient , err := checker .GetClients (s .ctx , repoName , "" , defaultLogger )
@@ -79,10 +181,12 @@ func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.Scorecar
79181 }
80182 }
81183
184+ logger .Debugf ("Running %d scorecard checks locally" , len (enabledChecks ))
82185 res , err := sc .RunScorecard (s .ctx , repo , commitSHA , 0 , enabledChecks , repoClient , ossFuzzClient , ciiClient , vulnsClient )
83186 if err != nil {
84187 return nil , fmt .Errorf ("error, failed to run scorecard: %w" , err )
85188 }
189+
86190 if res .Repo .Name == "" {
87191 // The commit SHA can be invalid or the repo can be private.
88192 return nil , fmt .Errorf ("error, failed to get scorecard data for repo %v, commit SHA %v" , res .Repo .Name , commitSHA )
@@ -94,4 +198,4 @@ func NewScorecardRunner(ctx context.Context) (Scorecard, error) {
94198 return scorecardRunner {
95199 ctx ,
96200 }, nil
97- }
201+ }
0 commit comments