| 1 | + | // Copyright 2023 OpenSSF Scorecard Authors |
| 2 | + | // |
| 3 | + | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | + | // you may not use this file except in compliance with the License. |
| 5 | + | // You may obtain a copy of the License at |
| 6 | + | // |
| 7 | + | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | + | // |
| 9 | + | // Unless required by applicable law or agreed to in writing, software |
| 10 | + | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | + | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | + | // See the License for the specific language governing permissions and |
| 13 | + | // limitations under the License. |
| 14 | + | |
| 15 | + | package ossfuzz |
| 16 | + | |
| 17 | + | import ( |
| 18 | + | "encoding/json" |
| 19 | + | "errors" |
| 20 | + | "fmt" |
| 21 | + | "io" |
| 22 | + | "net/http" |
| 23 | + | "net/url" |
| 24 | + | "strings" |
| 25 | + | "sync" |
| 26 | + | "time" |
| 27 | + | |
| 28 | + | "github.com/ossf/scorecard/v4/clients" |
| 29 | + | ) |
| 30 | + | |
| 31 | + | const ( |
| 32 | + | StatusURL = "https://oss-fuzz-build-logs.storage.googleapis.com/status.json" |
| 33 | + | ) |
| 34 | + | |
| 35 | + | var ( |
| 36 | + | errUnreachableStatusFile = errors.New("could not fetch OSS Fuzz status file") |
| 37 | + | errMalformedURL = errors.New("malformed repo url") |
| 38 | + | ) |
| 39 | + | |
| 40 | + | type client struct { |
| 41 | + | err error |
| 42 | + | projects map[string]bool |
| 43 | + | statusURL string |
| 44 | + | once sync.Once |
| 45 | + | } |
| 46 | + | |
| 47 | + | type ossFuzzStatus struct { |
| 48 | + | Projects []struct { |
| 49 | + | RepoURI string `json:"main_repo"` |
| 50 | + | } `json:"projects"` |
| 51 | + | } |
| 52 | + | |
| 53 | + | // CreateOSSFuzzClient returns a client which implements RepoClient interface. |
| 54 | + | func CreateOSSFuzzClient(ossFuzzStatusURL string) clients.RepoClient { |
| 55 | + | return &client{ |
| 56 | + | statusURL: ossFuzzStatusURL, |
| 57 | + | projects: map[string]bool{}, |
| 58 | + | } |
| 59 | + | } |
| 60 | + | |
| 61 | + | // CreateOSSFuzzClientEager returns a OSS Fuzz Client which has already fetched and parsed the status file. |
| 62 | + | func CreateOSSFuzzClientEager(ossFuzzStatusURL string) (clients.RepoClient, error) { |
| 63 | + | c := client{ |
| 64 | + | statusURL: ossFuzzStatusURL, |
| 65 | + | projects: map[string]bool{}, |
| 66 | + | } |
| 67 | + | c.once.Do(func() { |
| 68 | + | c.init() |
| 69 | + | }) |
| 70 | + | if c.err != nil { |
| 71 | + | return nil, c.err |
| 72 | + | } |
| 73 | + | return &c, nil |
| 74 | + | } |
| 75 | + | |
| 76 | + | // Search implements RepoClient.Search. |
| 77 | + | func (c *client) Search(request clients.SearchRequest) (clients.SearchResponse, error) { |
| 78 | + | c.once.Do(func() { |
| 79 | + | c.init() |
| 80 | + | }) |
| 81 | + | var sr clients.SearchResponse |
| 82 | + | if c.err != nil { |
| 83 | + | return sr, c.err |
| 84 | + | } |
| 85 | + | if c.projects[request.Query] { |
| 86 | + | sr.Hits = 1 |
| 87 | + | } |
| 88 | + | return sr, nil |
| 89 | + | } |
| 90 | + | |
| 91 | + | func (c *client) init() { |
| 92 | + | b, err := fetchStatusFile(c.statusURL) |
| 93 | + | if err != nil { |
| 94 | + | c.err = err |
| 95 | + | return |
| 96 | + | } |
| 97 | + | if err = parseStatusFile(b, c.projects); err != nil { |
| 98 | + | c.err = err |
| 99 | + | return |
| 100 | + | } |
| 101 | + | } |
| 102 | + | |
| 103 | + | func parseStatusFile(contents []byte, m map[string]bool) error { |
| 104 | + | status := ossFuzzStatus{} |
| 105 | + | if err := json.Unmarshal(contents, &status); err != nil { |
| 106 | + | return fmt.Errorf("parse status file: %w", err) |
| 107 | + | } |
| 108 | + | for i := range status.Projects { |
| 109 | + | repoURI := status.Projects[i].RepoURI |
| 110 | + | normalizedRepoURI, err := normalize(repoURI) |
| 111 | + | if err != nil { |
| 112 | + | continue |
| 113 | + | } |
| 114 | + | m[normalizedRepoURI] = true |
| 115 | + | } |
| 116 | + | return nil |
| 117 | + | } |
| 118 | + | |
| 119 | + | func fetchStatusFile(uri string) ([]byte, error) { |
| 120 | + | //nolint:gosec // URI comes from a constant or a test HTTP server, not user input |
| 121 | + | resp, err := http.Get(uri) |
| 122 | + | if err != nil { |
| 123 | + | return nil, fmt.Errorf("http.Get: %w", err) |
| 124 | + | } |
| 125 | + | defer resp.Body.Close() |
| 126 | + | if resp.StatusCode >= 400 { |
| 127 | + | return nil, fmt.Errorf("%s: %w", resp.Status, errUnreachableStatusFile) |
| 128 | + | } |
| 129 | + | b, err := io.ReadAll(resp.Body) |
| 130 | + | if err != nil { |
| 131 | + | return nil, fmt.Errorf("io.ReadAll: %w", err) |
| 132 | + | } |
| 133 | + | return b, nil |
| 134 | + | } |
| 135 | + | |
| 136 | + | func normalize(rawURL string) (string, error) { |
| 137 | + | u, err := url.Parse(rawURL) |
| 138 | + | if err != nil { |
| 139 | + | return "", fmt.Errorf("url.Parse: %w", err) |
| 140 | + | } |
| 141 | + | const splitLen = 2 |
| 142 | + | split := strings.SplitN(strings.Trim(u.Path, "/"), "/", splitLen) |
| 143 | + | if len(split) != splitLen { |
| 144 | + | return "", fmt.Errorf("%s: %w", rawURL, errMalformedURL) |
| 145 | + | } |
| 146 | + | org := split[0] |
| 147 | + | repo := strings.TrimSuffix(split[1], ".git") |
| 148 | + | return fmt.Sprintf("%s/%s/%s", u.Host, org, repo), nil |
| 149 | + | } |
| 150 | + | |
| 151 | + | // URI implements RepoClient.URI. |
| 152 | + | func (c *client) URI() string { |
| 153 | + | return c.statusURL |
| 154 | + | } |
| 155 | + | |
| 156 | + | // InitRepo implements RepoClient.InitRepo. |
| 157 | + | func (c *client) InitRepo(inputRepo clients.Repo, commitSHA string, commitDepth int) error { |
| 158 | + | return fmt.Errorf("InitRepo: %w", clients.ErrUnsupportedFeature) |
| 159 | + | } |
| 160 | + | |
| 161 | + | // IsArchived implements RepoClient.IsArchived. |
| 162 | + | func (c *client) IsArchived() (bool, error) { |
| 163 | + | return false, fmt.Errorf("IsArchived: %w", clients.ErrUnsupportedFeature) |
| 164 | + | } |
| 165 | + | |
| 166 | + | // LocalPath implements RepoClient.LocalPath. |
| 167 | + | func (c *client) LocalPath() (string, error) { |
| 168 | + | return "", fmt.Errorf("LocalPath: %w", clients.ErrUnsupportedFeature) |
| 169 | + | } |
| 170 | + | |
| 171 | + | // ListFiles implements RepoClient.ListFiles. |
| 172 | + | func (c *client) ListFiles(predicate func(string) (bool, error)) ([]string, error) { |
| 173 | + | return nil, fmt.Errorf("ListFiles: %w", clients.ErrUnsupportedFeature) |
| 174 | + | } |
| 175 | + | |
| 176 | + | // GetFileContent implements RepoClient.GetFileContent. |
| 177 | + | func (c *client) GetFileContent(filename string) ([]byte, error) { |
| 178 | + | return nil, fmt.Errorf("GetFileContent: %w", clients.ErrUnsupportedFeature) |
| 179 | + | } |
| 180 | + | |
| 181 | + | // GetBranch implements RepoClient.GetBranch. |
| 182 | + | func (c *client) GetBranch(branch string) (*clients.BranchRef, error) { |
| 183 | + | return nil, fmt.Errorf("GetBranch: %w", clients.ErrUnsupportedFeature) |
| 184 | + | } |
| 185 | + | |
| 186 | + | // GetDefaultBranch implements RepoClient.GetDefaultBranch. |
| 187 | + | func (c *client) GetDefaultBranch() (*clients.BranchRef, error) { |
| 188 | + | return nil, fmt.Errorf("GetDefaultBranch: %w", clients.ErrUnsupportedFeature) |
| 189 | + | } |
| 190 | + | |
| 191 | + | // GetDefaultBranchName implements RepoClient.GetDefaultBranchName. |
| 192 | + | func (c *client) GetDefaultBranchName() (string, error) { |
| 193 | + | return "", fmt.Errorf("GetDefaultBranchName: %w", clients.ErrUnsupportedFeature) |
| 194 | + | } |
| 195 | + | |
| 196 | + | // ListCommits implements RepoClient.ListCommits. |
| 197 | + | func (c *client) ListCommits() ([]clients.Commit, error) { |
| 198 | + | return nil, fmt.Errorf("ListCommits: %w", clients.ErrUnsupportedFeature) |
| 199 | + | } |
| 200 | + | |
| 201 | + | // ListIssues implements RepoClient.ListIssues. |
| 202 | + | func (c *client) ListIssues() ([]clients.Issue, error) { |
| 203 | + | return nil, fmt.Errorf("ListIssues: %w", clients.ErrUnsupportedFeature) |
| 204 | + | } |
| 205 | + | |
| 206 | + | // ListReleases implements RepoClient.ListReleases. |
| 207 | + | func (c *client) ListReleases() ([]clients.Release, error) { |
| 208 | + | return nil, fmt.Errorf("ListReleases: %w", clients.ErrUnsupportedFeature) |
| 209 | + | } |
| 210 | + | |
| 211 | + | // ListContributors implements RepoClient.ListContributors. |
| 212 | + | func (c *client) ListContributors() ([]clients.User, error) { |
| 213 | + | return nil, fmt.Errorf("ListContributors: %w", clients.ErrUnsupportedFeature) |
| 214 | + | } |
| 215 | + | |
| 216 | + | // ListSuccessfulWorkflowRuns implements RepoClient.ListSuccessfulWorkflowRuns. |
| 217 | + | func (c *client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) { |
| 218 | + | return nil, fmt.Errorf("ListSuccessfulWorkflowRuns: %w", clients.ErrUnsupportedFeature) |
| 219 | + | } |
| 220 | + | |
| 221 | + | // ListCheckRunsForRef implements RepoClient.ListCheckRunsForRef. |
| 222 | + | func (c *client) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) { |
| 223 | + | return nil, fmt.Errorf("ListCheckRunsForRef: %w", clients.ErrUnsupportedFeature) |
| 224 | + | } |
| 225 | + | |
| 226 | + | // ListStatuses implements RepoClient.ListStatuses. |
| 227 | + | func (c *client) ListStatuses(ref string) ([]clients.Status, error) { |
| 228 | + | return nil, fmt.Errorf("ListStatuses: %w", clients.ErrUnsupportedFeature) |
| 229 | + | } |
| 230 | + | |
| 231 | + | // ListWebhooks implements RepoClient.ListWebhooks. |
| 232 | + | func (c *client) ListWebhooks() ([]clients.Webhook, error) { |
| 233 | + | return nil, fmt.Errorf("ListWebhooks: %w", clients.ErrUnsupportedFeature) |
| 234 | + | } |
| 235 | + | |
| 236 | + | // SearchCommits implements RepoClient.SearchCommits. |
| 237 | + | func (c *client) SearchCommits(request clients.SearchCommitsOptions) ([]clients.Commit, error) { |
| 238 | + | return nil, fmt.Errorf("SearchCommits: %w", clients.ErrUnsupportedFeature) |
| 239 | + | } |
| 240 | + | |
| 241 | + | // Close implements RepoClient.Close. |
| 242 | + | func (c *client) Close() error { |
| 243 | + | return nil |
| 244 | + | } |
| 245 | + | |
| 246 | + | // ListProgrammingLanguages implements RepoClient.ListProgrammingLanguages. |
| 247 | + | func (c *client) ListProgrammingLanguages() ([]clients.Language, error) { |
| 248 | + | return nil, fmt.Errorf("ListProgrammingLanguages: %w", clients.ErrUnsupportedFeature) |
| 249 | + | } |
| 250 | + | |
| 251 | + | // ListLicenses implements RepoClient.ListLicenses. |
| 252 | + | func (c *client) ListLicenses() ([]clients.License, error) { |
| 253 | + | return nil, fmt.Errorf("ListLicenses: %w", clients.ErrUnsupportedFeature) |
| 254 | + | } |
| 255 | + | |
| 256 | + | // GetCreatedAt implements RepoClient.GetCreatedAt. |
| 257 | + | func (c *client) GetCreatedAt() (time.Time, error) { |
| 258 | + | return time.Time{}, fmt.Errorf("GetCreatedAt: %w", clients.ErrUnsupportedFeature) |
| 259 | + | } |
| 260 | + | |