@@ -18,19 +18,147 @@ package scorecard
1818import (
1919 "context"
2020 "fmt"
21-
21+ "github.com/guacsec/guac/pkg/logging"
2222 "github.com/ossf/scorecard/v4/checker"
2323 "github.com/ossf/scorecard/v4/checks"
2424 "github.com/ossf/scorecard/v4/log"
2525 sc "github.com/ossf/scorecard/v4/pkg"
26+ "io"
27+ "net/http"
28+ "net/url"
29+ "os"
30+ "strings"
31+ "time"
2632)
2733
34+ const githubPrefix = "github.com/"
35+
2836// scorecardRunner is a struct that implements the Scorecard interface.
2937type scorecardRunner struct {
3038 ctx context.Context
3139}
3240
41+ // normalizeRepoName ensures the repo name has the github.com/ prefix required by the Scorecard API.
42+ // If the prefix is missing, it is added. If the repo name already has the prefix, it is returned as-is.
43+ func normalizeRepoName (repoName string ) string {
44+ if strings .HasPrefix (repoName , githubPrefix ) {
45+ return repoName
46+ }
47+ return githubPrefix + repoName
48+ }
49+
3350func (s scorecardRunner ) GetScore (repoName , commitSHA , tag string ) (* sc.ScorecardResult , error ) {
51+ logger := logging .FromContext (s .ctx )
52+ repoName = normalizeRepoName (repoName )
53+
54+ // First try API approach
55+ logger .Debugf ("Attempting to fetch scorecard from API for repo: %s, commit: %s" , repoName , commitSHA )
56+ result , err := s .getScoreFromAPI (repoName , commitSHA , tag )
57+ if err == nil {
58+ logger .Infof ("Successfully fetched scorecard from API for repo: %s" , repoName )
59+ return result , nil
60+ }
61+
62+ // Log API failure and check if we can fallback to local computation
63+ logger .Warnf ("API fetch failed for repo %s: %v" , repoName , err )
64+
65+ // Check if GitHub token is available for local computation
66+ if _ , ok := os .LookupEnv ("GITHUB_AUTH_TOKEN" ); ! ok {
67+ logger .Errorf ("Cannot fall back to local computation - GITHUB_AUTH_TOKEN not set" )
68+ return nil , fmt .Errorf ("scorecard API failed and GITHUB_AUTH_TOKEN not available for local computation: %w" , err )
69+ }
70+
71+ logger .Infof ("Falling back to local computation for repo: %s" , repoName )
72+ result , err = s .computeScore (repoName , commitSHA , tag )
73+ if err != nil {
74+ logger .Errorf ("Failed to compute scorecard locally for repo %s: %v" , repoName , err )
75+ }
76+ return result , err
77+ }
78+
79+ func (s scorecardRunner ) getScoreFromAPI (repoName , commitSHA , tag string ) (* sc.ScorecardResult , error ) {
80+ logger := logging .FromContext (s .ctx )
81+
82+ // If tag is provided without a valid commitSHA, skip API and use local computation
83+ // The API cannot resolve tags, but computeScore can look up the commit for a tag
84+ if (commitSHA == "" || commitSHA == "HEAD" ) && tag != "" {
85+ logger .Debugf ("Tag %s provided without commit SHA - skipping API, will use local computation" , tag )
86+ return nil , fmt .Errorf ("tag provided without commit SHA; falling back to local computation for tag %s" , tag )
87+ }
88+
89+ baseURL , err := url .JoinPath ("https://api.securityscorecards.dev" , "projects" , repoName )
90+ if err != nil {
91+ return nil , err
92+ }
93+
94+ // If commitSHA is provided, try with it first
95+ if commitSHA != "" && commitSHA != "HEAD" {
96+ urlWithCommit := baseURL + "?commit=" + commitSHA
97+ result , err := s .fetchFromAPI (urlWithCommit )
98+ if err == nil {
99+ return result , nil
100+ }
101+ logger .Debugf ("API call with commit %s failed, retrying without commit: %v" , commitSHA , err )
102+ }
103+
104+ result , err := s .fetchFromAPI (baseURL )
105+ if err != nil {
106+ return nil , err
107+ }
108+ return result , nil
109+ }
110+
111+ func (s scorecardRunner ) fetchFromAPI (apiURL string ) (* sc.ScorecardResult , error ) {
112+ logger := logging .FromContext (s .ctx )
113+ logger .Debugf ("Making API request to: %s" , apiURL )
114+
115+ httpClient := & http.Client {
116+ Timeout : 30 * time .Second ,
117+ }
118+
119+ req , err := http .NewRequestWithContext (s .ctx , http .MethodGet , apiURL , nil )
120+ if err != nil {
121+ return nil , fmt .Errorf ("failed to create request: %w" , err )
122+ }
123+
124+ req .Header .Set ("User-Agent" , "guac-scorecard-certifier/1.0" )
125+ req .Header .Set ("Accept" , "application/json" )
126+
127+ resp , err := httpClient .Do (req )
128+ if err != nil {
129+ return nil , fmt .Errorf ("scorecard request failed: %w" , err )
130+ }
131+ defer func () {
132+ if resp != nil && resp .Body != nil {
133+ _ = resp .Body .Close ()
134+ }
135+ }()
136+
137+ logger .Debugf ("API response status code: %d" , resp .StatusCode )
138+
139+ if resp .StatusCode == http .StatusNotFound {
140+ return nil , fmt .Errorf ("scorecard not found in API" )
141+ }
142+
143+ if resp .StatusCode >= 400 {
144+ body , _ := io .ReadAll (resp .Body )
145+ return nil , fmt .Errorf ("API returned status %d: %s" , resp .StatusCode , string (body ))
146+ }
147+
148+ // Use scorecard's built-in JSON parser, which is experimental
149+ // but still better then rolling out your own type
150+ result , _ , err := sc .ExperimentalFromJSON2 (resp .Body )
151+ if err != nil {
152+ return nil , fmt .Errorf ("failed to decode API response: %w" , err )
153+ }
154+
155+ return & result , nil
156+ }
157+
158+ func (s scorecardRunner ) computeScore (repoName , commitSHA , tag string ) (* sc.ScorecardResult , error ) {
159+ logger := logging .FromContext (s .ctx )
160+ logger .Infof ("Starting local scorecard computation for repo: %s, commit: %s, tag: %s" , repoName , commitSHA , tag )
161+
34162 // Can't use guacs standard logger because scorecard uses a different logger.
35163 defaultLogger := log .NewLogger (log .DefaultLevel )
36164 repo , repoClient , ossFuzzClient , ciiClient , vulnsClient , err := checker .GetClients (s .ctx , repoName , "" , defaultLogger )
@@ -79,10 +207,12 @@ func (s scorecardRunner) GetScore(repoName, commitSHA, tag string) (*sc.Scorecar
79207 }
80208 }
81209
210+ logger .Debugf ("Running %d scorecard checks locally" , len (enabledChecks ))
82211 res , err := sc .RunScorecard (s .ctx , repo , commitSHA , 0 , enabledChecks , repoClient , ossFuzzClient , ciiClient , vulnsClient )
83212 if err != nil {
84213 return nil , fmt .Errorf ("error, failed to run scorecard: %w" , err )
85214 }
215+
86216 if res .Repo .Name == "" {
87217 // The commit SHA can be invalid or the repo can be private.
88218 return nil , fmt .Errorf ("error, failed to get scorecard data for repo %v, commit SHA %v" , res .Repo .Name , commitSHA )
0 commit comments