|  | // 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" | 
|  | "errors" | 
|  | "fmt" | 
|  | "io/fs" | 
|  |  | 
|  | "golang.org/x/vulndb/cmd/vulnreport/log" | 
|  | "golang.org/x/vulndb/internal/issues" | 
|  | "golang.org/x/vulndb/internal/report" | 
|  | ) | 
|  |  | 
|  | // command represents a subcommand of vulnreport. | 
|  | type command interface { | 
|  | // name outputs the string used to invoke the subcommand. | 
|  | name() string | 
|  | // usage outputs strings indicating how to use the subcommand. | 
|  | usage() (args string, desc string) | 
|  | setuper | 
|  | // parseArgs takes in the raw args passed to the command line, | 
|  | // and converts them to a representation understood by "run". | 
|  | // This function need not be one-to-one: there may be more | 
|  | // inputs than args or vice-versa. | 
|  | parseArgs(_ context.Context, args []string) (inputs []string, _ error) | 
|  | lookup(context.Context, string) (any, error) | 
|  | skip(any) string | 
|  | run(context.Context, any) error | 
|  | // close cleans up state and/or completes tasks that should occur | 
|  | // after run is called on all inputs. | 
|  | close() error | 
|  | inputType() string | 
|  | } | 
|  |  | 
|  | // run executes the given command on the given raw arguments. | 
|  | func run(ctx context.Context, c command, args []string, env environment) (err error) { | 
|  | if err := c.setup(ctx, env); err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | stats := &counter{} | 
|  | defer func() { | 
|  | if cerr := c.close(); cerr != nil { | 
|  | err = errors.Join(err, cerr) | 
|  | } | 
|  | if total := stats.total(); total > 0 { | 
|  | log.Infof("%s: processed %d %s(s) (success=%d; skip=%d; error=%d)", c.name(), total, c.inputType(), stats.succeeded, stats.skipped, stats.errored) | 
|  | } | 
|  | if stats.errored > 0 { | 
|  | err = errors.Join(err, fmt.Errorf("errored on %d inputs", stats.errored)) | 
|  | } | 
|  | }() | 
|  |  | 
|  | inputs, err := c.parseArgs(ctx, args) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | log.Infof("%s: operating on %d %s(s)", c.name(), len(inputs), c.inputType()) | 
|  |  | 
|  | for _, input := range inputs { | 
|  | in, err := c.lookup(ctx, input) | 
|  | if err != nil { | 
|  | stats.errored++ | 
|  | log.Errf("%s: lookup %s failed: %s", c.name(), input, err) | 
|  | continue | 
|  | } | 
|  |  | 
|  | if reason := c.skip(in); reason != "" { | 
|  | stats.skipped++ | 
|  | log.Infof("%s: skipping %s (%s)", c.name(), toString(in), reason) | 
|  | continue | 
|  | } | 
|  |  | 
|  | log.Infof("%s %s", c.name(), input) | 
|  | if err := c.run(ctx, in); err != nil { | 
|  | stats.errored++ | 
|  | log.Errf("%s: %s", c.name(), err) | 
|  | continue | 
|  | } | 
|  | stats.succeeded++ | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | type counter struct { | 
|  | skipped   int | 
|  | succeeded int | 
|  | errored   int | 
|  | } | 
|  |  | 
|  | func (c *counter) total() int { | 
|  | return c.skipped + c.succeeded + c.errored | 
|  | } | 
|  |  | 
|  | func toString(in any) string { | 
|  | switch v := in.(type) { | 
|  | case *yamlReport: | 
|  | return fmt.Sprintf("report %s", v.Report.ID) | 
|  | case *issues.Issue: | 
|  | return fmt.Sprintf("issue #%d", v.Number) | 
|  | default: | 
|  | return fmt.Sprintf("%v", v) | 
|  | } | 
|  | } | 
|  |  | 
|  | type setuper interface { | 
|  | // setup populates state needed to run a command. | 
|  | setup(context.Context, environment) error | 
|  | } | 
|  |  | 
|  | func setupAll(ctx context.Context, env environment, fs ...setuper) error { | 
|  | for _, f := range fs { | 
|  | if err := f.setup(ctx, env); err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | type closer interface { | 
|  | close() error | 
|  | } | 
|  |  | 
|  | func closeAll(cs ...closer) error { | 
|  | for _, c := range cs { | 
|  | if err := c.close(); err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | const ( | 
|  | filenameArgs = "[filename | github-id] ..." | 
|  | ghIssueArgs  = "[github-id] ..." | 
|  | ) | 
|  |  | 
|  | // filenameParser implements the "parseArgs" function of the command | 
|  | // interface, and can be used by commands that operate on YAML filenames. | 
|  | type filenameParser struct { | 
|  | fsys fs.FS | 
|  | } | 
|  |  | 
|  | func (f *filenameParser) setup(_ context.Context, env environment) error { | 
|  | f.fsys = env.ReportFS() | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (*filenameParser) inputType() string { | 
|  | return "report" | 
|  | } | 
|  |  | 
|  | func (f *filenameParser) parseArgs(_ context.Context, args []string) (filenames []string, allErrs error) { | 
|  | if len(args) == 0 { | 
|  | return nil, fmt.Errorf("no arguments provided") | 
|  | } | 
|  | for _, arg := range args { | 
|  | fname, err := argToFilename(arg, f.fsys) | 
|  | if err != nil { | 
|  | log.Err(err) | 
|  | continue | 
|  | } | 
|  | filenames = append(filenames, fname) | 
|  | } | 
|  |  | 
|  | if len(filenames) == 0 { | 
|  | return nil, fmt.Errorf("could not parse any valid filenames from arguments") | 
|  | } | 
|  |  | 
|  | return filenames, nil | 
|  | } | 
|  |  | 
|  | func argToFilename(arg string, fsys fs.FS) (string, error) { | 
|  | if _, err := fs.Stat(fsys, arg); err != nil { | 
|  | // If arg isn't a file, see if it might be an issue ID | 
|  | // with an existing report. | 
|  | for _, padding := range []string{"", "0", "00", "000"} { | 
|  | m, _ := fs.Glob(fsys, "data/*/GO-*-"+padding+arg+".yaml") | 
|  | if len(m) == 1 { | 
|  | return m[0], nil | 
|  | } | 
|  | } | 
|  | return "", fmt.Errorf("could not parse argument %q: not a valid filename or issue ID with existing report: %w", arg, err) | 
|  | } | 
|  | return arg, nil | 
|  | } | 
|  |  | 
|  | func (f *filenameParser) lookup(_ context.Context, filename string) (any, error) { | 
|  | r, err := report.ReadStrict(f.fsys, filename) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return &yamlReport{Report: r, Filename: filename}, nil | 
|  | } | 
|  |  | 
|  | type noSkip bool | 
|  |  | 
|  | func (noSkip) skip(any) string { | 
|  | return "" | 
|  | } | 
|  |  | 
|  | type yamlReport struct { | 
|  | *report.Report | 
|  | Filename string | 
|  | } |