-
Notifications
You must be signed in to change notification settings - Fork 367
feat(pipeline): Support GitHub Actions commit enumeration #6012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // Package githubactions implements an enricher that generates GIT ranges | ||
| // for GitHub Actions advisories from SemVer or Ecosystem ranges. | ||
| package githubactions | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/google/osv.dev/go/internal/worker/pipeline" | ||
| "github.com/google/osv.dev/go/logger" | ||
| "github.com/ossf/osv-schema/bindings/go/osvschema" | ||
| ) | ||
|
|
||
| // EcosystemGitHubActions is the canonical OSV ecosystem name for GitHub Actions. | ||
| const EcosystemGitHubActions = "GitHub Actions" | ||
|
|
||
| type Enricher struct{} | ||
|
|
||
| var _ pipeline.Enricher = (*Enricher)(nil) | ||
|
|
||
| // ExtractGitHubRepoURL extracts the canonical "https://github.com/{owner}/{repo}" from action names. | ||
| // It handles standard action names (e.g. "actions/checkout"), nested sub-actions | ||
| // (e.g. "docker/build-push-action/v2"), and guards against path traversal. | ||
| func ExtractGitHubRepoURL(actionName string) (string, error) { | ||
| trimmed := strings.TrimSpace(actionName) | ||
|
|
||
| // Strip common prefixes if present. | ||
| if after, ok := strings.CutPrefix(trimmed, "https://github.com/"); ok { | ||
| trimmed = after | ||
| } else if after, ok := strings.CutPrefix(trimmed, "http://github.com/"); ok { | ||
| trimmed = after | ||
| } else if after, ok := strings.CutPrefix(trimmed, "github.com/"); ok { | ||
| trimmed = after | ||
| } | ||
|
|
||
| trimmed = strings.Trim(trimmed, "/") | ||
| parts := strings.Split(trimmed, "/") | ||
| if len(parts) < 2 { | ||
| return "", fmt.Errorf("invalid action name %q: expected owner/repo", actionName) | ||
| } | ||
|
|
||
| owner, repo := parts[0], parts[1] | ||
|
|
||
| // Strip version tag suffix if embedded (e.g. "actions/checkout@v4") | ||
| repo, _, _ = strings.Cut(repo, "@") | ||
|
|
||
| // Strip .git suffix if present | ||
| repo = strings.TrimSuffix(repo, ".git") | ||
|
|
||
| if owner == "" || repo == "" { | ||
| return "", fmt.Errorf("empty owner or repo segment in action %q", actionName) | ||
| } | ||
|
|
||
| if owner == "." || owner == ".." || repo == "." || repo == ".." || | ||
| strings.Contains(owner, "..") || strings.Contains(repo, "..") { | ||
|
Comment on lines
+57
to
+58
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| return "", fmt.Errorf("invalid path traversal segment in action %q", actionName) | ||
| } | ||
|
|
||
| return fmt.Sprintf("https://github.com/%s/%s", owner, repo), nil | ||
| } | ||
|
|
||
| // Enrich inspects vulnerabilities for affected packages in the "GitHub Actions" ecosystem | ||
| // and injects a Range_GIT range for each SEMVER or ECOSYSTEM range. | ||
| func (*Enricher) Enrich(ctx context.Context, vuln *osvschema.Vulnerability, _ *pipeline.EnrichParams) error { | ||
| for _, affected := range vuln.GetAffected() { | ||
| pkg := affected.GetPackage() | ||
| if pkg.GetEcosystem() != EcosystemGitHubActions { | ||
| continue | ||
| } | ||
|
|
||
| repoURL, err := ExtractGitHubRepoURL(pkg.GetName()) | ||
| if err != nil { | ||
| logger.WarnContext(ctx, "failed to extract GitHub repo URL for action", | ||
| slog.String("vuln_id", vuln.GetId()), | ||
| slog.String("ecosystem", pkg.GetEcosystem()), | ||
| slog.String("name", pkg.GetName()), | ||
| slog.Any("error", err), | ||
| ) | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| var gitRanges []*osvschema.Range | ||
| for _, r := range affected.GetRanges() { | ||
| if r.GetType() != osvschema.Range_SEMVER && r.GetType() != osvschema.Range_ECOSYSTEM { | ||
| continue | ||
| } | ||
|
|
||
| // Check if duplicate GIT range already exists | ||
| if gitRangeExists(affected.GetRanges(), repoURL, r.GetEvents()) || | ||
| gitRangeExists(gitRanges, repoURL, r.GetEvents()) { | ||
| continue | ||
| } | ||
|
|
||
| eventsCopy := make([]*osvschema.Event, len(r.GetEvents())) | ||
| for i, e := range r.GetEvents() { | ||
| eventsCopy[i] = &osvschema.Event{ | ||
| Introduced: e.GetIntroduced(), | ||
| Fixed: e.GetFixed(), | ||
| LastAffected: e.GetLastAffected(), | ||
| Limit: e.GetLimit(), | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually need to convert these from version strings to git commit SHAs ( I think you'll still need to do some form of normalisation of the git tags to match the version - a quick look at some of the Github Actions repos is that the tags often have a leading |
||
| } | ||
|
|
||
| gitRanges = append(gitRanges, &osvschema.Range{ | ||
| Type: osvschema.Range_GIT, | ||
| Repo: repoURL, | ||
| Events: eventsCopy, | ||
| }) | ||
| } | ||
| affected.Ranges = append(affected.Ranges, gitRanges...) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func gitRangeExists(ranges []*osvschema.Range, repoURL string, events []*osvschema.Event) bool { | ||
| return slices.ContainsFunc(ranges, func(r *osvschema.Range) bool { | ||
| return r.GetType() == osvschema.Range_GIT && r.GetRepo() == repoURL && eventsEqual(r.GetEvents(), events) | ||
| }) | ||
| } | ||
|
|
||
| func eventsEqual(a, b []*osvschema.Event) bool { | ||
| if len(a) != len(b) { | ||
| return false | ||
| } | ||
| for i := range a { | ||
| if a[i].GetIntroduced() != b[i].GetIntroduced() || | ||
| a[i].GetFixed() != b[i].GetFixed() || | ||
| a[i].GetLastAffected() != b[i].GetLastAffected() || | ||
| a[i].GetLimit() != b[i].GetLimit() { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we could use this constant from
osv-schema/bindings/go/osvconstants