| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| "os" |
| "strings" |
| |
| "golang.org/x/exp/slices" |
| "golang.org/x/vulndb/cmd/vulnreport/log" |
| "golang.org/x/vulndb/internal/idstr" |
| "golang.org/x/vulndb/internal/issues" |
| "golang.org/x/vulndb/internal/osv" |
| "golang.org/x/vulndb/internal/report" |
| "golang.org/x/vulndb/internal/symbols" |
| "golang.org/x/vulndb/internal/triage/priority" |
| ) |
| |
| type creator struct { |
| assignee string |
| created []*yamlReport |
| |
| // If non-zero, use this review status |
| // instead of the default for new reports. |
| reviewStatus report.ReviewStatus |
| |
| *fixer |
| *xrefer |
| *suggester |
| } |
| |
| func (c *creator) setup(ctx context.Context, env environment) (err error) { |
| user := *user |
| if user == "" { |
| user = os.Getenv("GITHUB_USER") |
| } |
| c.assignee = user |
| |
| rs, ok := report.ToReviewStatus(*reviewStatus) |
| if !ok { |
| return fmt.Errorf("invalid -status=%s", rs) |
| } |
| c.reviewStatus = rs |
| |
| c.fixer = new(fixer) |
| c.xrefer = new(xrefer) |
| if *useAI { |
| c.suggester = new(suggester) |
| } |
| return setupAll(ctx, env, c.fixer, c.xrefer, c.suggester) |
| } |
| |
| func (c *creator) skip(input any) string { |
| iss := input.(*issues.Issue) |
| |
| if c.assignee != "" && iss.Assignee != c.assignee { |
| return fmt.Sprintf("assignee = %q, not %q", iss.Assignee, c.assignee) |
| } |
| |
| return skip(iss, c.xrefer) |
| } |
| |
| func skip(iss *issues.Issue, x *xrefer) string { |
| if iss.HasLabel(labelOutOfScope) { |
| return "out of scope" |
| } |
| |
| if iss.HasLabel(labelDuplicate) { |
| return "duplicate issue" |
| } |
| |
| if iss.HasLabel(labelSuggestedEdit) { |
| return "suggested edit" |
| } |
| |
| // indicates that there is already a report for this |
| // vuln but the report needs to be updated |
| if iss.HasLabel(labelNeedsAlias) { |
| return "existing report needs alias" |
| } |
| |
| if iss.HasLabel(labelPossiblyNotGo) { |
| return "possibly not Go" |
| } |
| |
| if x.rc.HasReport(iss.Number) { |
| return "already has report" |
| } |
| |
| return "" |
| } |
| |
| func (c *creator) newReportFromIssue(ctx context.Context, iss *issues.Issue) error { |
| r, err := c.reportFromMeta(ctx, &reportMeta{ |
| id: iss.NewGoID(), |
| excluded: excludedReason(iss), |
| modulePath: modulePath(iss), |
| aliases: aliases(iss), |
| reviewStatus: reviewStatusOf(iss, c.reviewStatus), |
| originalCVE: originalCVE(iss), |
| }) |
| if err != nil { |
| return err |
| } |
| if r.Withdrawn != nil { |
| return fmt.Errorf("new report should not be created for withdrawn vulnerability; close issue #%d as excluded:OUT_OF_SCOPE instead", iss.Number) |
| } |
| return c.write(ctx, r) |
| } |
| |
| func originalCVE(iss *issues.Issue) string { |
| aliases := aliases(iss) |
| if iss.HasLabel(labelFirstParty) && len(aliases) == 1 && idstr.IsCVE(aliases[0]) { |
| return aliases[0] |
| } |
| return "" |
| } |
| |
| func reviewStatusOf(iss *issues.Issue, reviewStatus report.ReviewStatus) report.ReviewStatus { |
| d := defaultReviewStatus(iss) |
| // If a valid review status is provided, it overrides the priority label. |
| if reviewStatus != 0 { |
| if d != reviewStatus { |
| log.Warnf("issue #%d: should be %s based on label(s) but this was overridden with the -status=%s flag", iss.Number, d, reviewStatus) |
| } |
| return reviewStatus |
| } |
| return d |
| } |
| |
| func defaultReviewStatus(iss *issues.Issue) report.ReviewStatus { |
| if iss.HasLabel(labelHighPriority) || |
| iss.HasLabel(labelDirect) || |
| iss.HasLabel(labelFirstParty) { |
| return report.Reviewed |
| } |
| |
| return report.Unreviewed |
| } |
| |
| func (c *creator) metaToSource(ctx context.Context, meta *reportMeta) report.Source { |
| if cveID := meta.originalCVE; cveID != "" { |
| log.Infof("%s: creating original report for Go-CNA-assigned %s", meta.id, cveID) |
| return report.OriginalCVE(cveID) |
| } |
| |
| if src := c.sourceFromBestAlias(ctx, meta.aliases, *preferCVE); src != nil { |
| log.Infof("%s: picked %s as best source alias (from [%s])", meta.id, src.SourceID(), |
| strings.Join(meta.aliases, ", ")) |
| return src |
| } |
| |
| log.Infof("%s: no suitable alias found, creating basic report", meta.id) |
| return report.Original() |
| } |
| |
| func (c *creator) rawReport(ctx context.Context, meta *reportMeta) *report.Report { |
| return report.New(c.metaToSource(ctx, meta), c.pxc, |
| report.WithGoID(meta.id), |
| report.WithModulePath(meta.modulePath), |
| report.WithAliases(meta.aliases), |
| report.WithReviewStatus(meta.reviewStatus), |
| report.WithUnexcluded(meta.unexcluded), |
| ) |
| } |
| |
| func (c *creator) reportFromMeta(ctx context.Context, meta *reportMeta) (*yamlReport, error) { |
| // Find the underlying module if the "module" provided is actually a package path. |
| if module, err := c.pxc.FindModule(meta.modulePath); err == nil { // no error |
| meta.modulePath = module |
| } |
| meta.aliases = c.allAliases(ctx, meta.aliases) |
| raw := c.rawReport(ctx, meta) |
| |
| if meta.excluded != "" { |
| raw = &report.Report{ |
| ID: meta.id, |
| Modules: []*report.Module{ |
| { |
| Module: meta.modulePath, |
| }, |
| }, |
| Excluded: meta.excluded, |
| CVEs: raw.CVEs, |
| GHSAs: raw.GHSAs, |
| } |
| } |
| |
| // The initial quick triage algorithm doesn't know about all |
| // affected modules, so double check the priority after the |
| // report is created. |
| if raw.IsUnreviewed() { |
| pr, _ := c.reportPriority(raw) |
| if pr.Priority == priority.High { |
| log.Warnf("%s: re-generating; vuln is high priority and should be REVIEWED; reason: %s", raw.ID, pr.Reason) |
| meta.reviewStatus = report.Reviewed |
| raw = c.rawReport(ctx, meta) |
| } |
| } |
| |
| fname, err := raw.YAMLFilename() |
| if err != nil { |
| return nil, err |
| } |
| r := &yamlReport{Report: raw, Filename: fname} |
| |
| // Find any additional aliases referenced by the source aliases. |
| r.addMissingAliases(ctx, c.aliasFinder) |
| |
| if c.suggester != nil { |
| suggestions, err := c.suggest(ctx, r, 1) |
| if err != nil { |
| r.AddNote(report.NoteTypeCreate, "failed to get AI-generated suggestions") |
| log.Warnf("%s: failed to get AI-generated suggestions: %v", r.ID, err) |
| } else { |
| log.Infof("%s: applying AI-generated suggestion", r.ID) |
| r.applySuggestion(suggestions[0]) |
| } |
| } |
| |
| if *populateSymbols { |
| log.Infof("%s: attempting to auto-populate symbols (this may take a while...)", r.ID) |
| if err := symbols.Populate(r.Report, false); err != nil { |
| r.AddNote(report.NoteTypeCreate, "failed to auto-populate symbols") |
| log.Warnf("%s: could not auto-populate symbols: %s", r.ID, err) |
| } else { |
| if err := r.checkSymbols(); err != nil { |
| log.Warnf("%s: auto-populated symbols have error(s): %s", r.ID, err) |
| } |
| } |
| } |
| |
| switch { |
| case raw.IsExcluded(): |
| // nothing |
| case raw.IsUnreviewed(): |
| r.removeUnreachableRefs() |
| default: |
| // Regular, full-length reports. |
| addTODOs(r) |
| if xrefs := c.xref(r); len(xrefs) != 0 { |
| log.Infof("%s: found cross-references: %s", r.ID, xrefs) |
| } |
| } |
| return r, nil |
| } |
| |
| func (r *yamlReport) removeUnreachableRefs() { |
| r.Report.References = slices.DeleteFunc(r.Report.References, func(r *report.Reference) bool { |
| resp, err := http.Head(r.URL) |
| if err != nil { |
| return true |
| } |
| defer resp.Body.Close() |
| return resp.StatusCode == http.StatusNotFound |
| }) |
| } |
| |
| func (c *creator) write(ctx context.Context, r *yamlReport) error { |
| if r.IsReviewed() || r.IsExcluded() { |
| if err := c.fileWriter.write(r); err != nil { |
| return err |
| } |
| } else { // unreviewed |
| addNotes := true |
| if err := c.fixAndWriteAll(ctx, r, addNotes); err != nil { |
| return err |
| } |
| } |
| c.created = append(c.created, r) |
| return nil |
| } |
| |
| const ( |
| labelDuplicate = "duplicate" |
| labelDirect = "Direct External Report" |
| labelSuggestedEdit = "Suggested Edit" |
| labelNeedsAlias = "NeedsAlias" |
| labelTriaged = "triaged" |
| labelHighPriority = "high priority" |
| labelFirstParty = "first party" |
| labelPossiblyNotGo = "possibly not Go" |
| labelOutOfScope = "excluded: OUT_OF_SCOPE" |
| ) |
| |
| func excludedReason(iss *issues.Issue) report.ExcludedReason { |
| for _, label := range iss.Labels { |
| if reason, ok := report.FromLabel(label); ok { |
| return reason |
| } |
| } |
| return "" |
| } |
| |
| func modulePath(iss *issues.Issue) string { |
| for _, p := range strings.Fields(iss.Title) { |
| if p == "x/vulndb:" { |
| continue |
| } |
| if strings.HasSuffix(p, ":") || strings.Contains(p, "/") { |
| // Remove backslashes. |
| return strings.ReplaceAll(strings.TrimSuffix(p, ":"), "\"", "") |
| } |
| } |
| return "" |
| } |
| |
| func aliases(iss *issues.Issue) (aliases []string) { |
| for _, p := range strings.Fields(iss.Title) { |
| if idstr.IsAliasType(p) { |
| aliases = append(aliases, strings.TrimSuffix(p, ",")) |
| } |
| } |
| return aliases |
| } |
| |
| // Data that can be combined with a source vulnerability |
| // to create a new report. |
| type reportMeta struct { |
| id string |
| modulePath string |
| aliases []string |
| excluded, unexcluded report.ExcludedReason |
| reviewStatus report.ReviewStatus |
| originalCVE string |
| } |
| |
| const todo = "TODO: " |
| |
| // addTODOs adds "TODO" comments to unfilled fields of r. |
| func addTODOs(r *yamlReport) { |
| if r.Excluded != "" { |
| return |
| } |
| if len(r.Modules) == 0 { |
| r.Modules = append(r.Modules, &report.Module{ |
| Packages: []*report.Package{{}}, |
| }) |
| } |
| for _, m := range r.Modules { |
| if m.Module == "" { |
| m.Module = todo + "affected module path" |
| } |
| if len(m.Versions) == 0 { |
| m.Versions = report.Versions{ |
| report.Introduced(todo + "introduced version (blank if unknown)"), |
| report.Fixed(todo + "fixed version"), |
| } |
| } |
| if m.VulnerableAt == nil { |
| m.VulnerableAt = report.VulnerableAt(todo + "a version at which the package is vulnerable") |
| } |
| if len(m.Packages) == 0 { |
| m.Packages = []*report.Package{ |
| { |
| Package: todo + "affected package path(s) - blank if all", |
| }, |
| } |
| } |
| for _, p := range m.Packages { |
| if p.Package == "" { |
| p.Package = todo + "affected package path" |
| } |
| if len(p.Symbols) == 0 { |
| p.Symbols = []string{todo + "affected symbol(s) - blank if all"} |
| } |
| } |
| } |
| if r.Summary == "" { |
| r.Summary = todo + "short (one phrase) summary of the form '<Problem> in <module>(s)'" |
| } |
| if r.Description == "" { |
| r.Description = todo + "description of the vulnerability" |
| } |
| if len(r.Credits) == 0 { |
| r.Credits = []string{todo + "who discovered/reported this vulnerability (optional)"} |
| } |
| if r.CVEMetadata == nil && len(r.CVEs) == 0 { |
| r.CVEs = []string{todo + "CVE id(s) for this vulnerability"} |
| } |
| if r.CVEMetadata != nil && r.CVEMetadata.CWE == "" { |
| r.CVEMetadata.CWE = todo + "CWE ID" |
| } |
| addReferenceTODOs(r) |
| } |
| |
| // addReferenceTODOs adds a TODO for each important reference type not |
| // already present in the report. |
| func addReferenceTODOs(r *yamlReport) { |
| todos := []*report.Reference{ |
| {Type: osv.ReferenceTypeAdvisory, URL: "TODO: canonical security advisory"}, |
| {Type: osv.ReferenceTypeReport, URL: "TODO: issue tracker link"}, |
| {Type: osv.ReferenceTypeFix, URL: "TODO: PR or commit (commit preferred)"}} |
| |
| types := make(map[osv.ReferenceType]bool) |
| for _, r := range r.References { |
| types[r.Type] = true |
| } |
| for _, todo := range todos { |
| if !types[todo.Type] { |
| r.References = append(r.References, todo) |
| } |
| } |
| } |