gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/tools/nogo/cli/cli.go (about)

     1  // Copyright 2019 The gVisor 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 cli implements a basic command line interface.
    16  package cli
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  
    26  	"github.com/google/subcommands"
    27  	"golang.org/x/sys/unix"
    28  	yaml "gopkg.in/yaml.v2"
    29  	"gvisor.dev/gvisor/runsc/flag"
    30  	"gvisor.dev/gvisor/tools/nogo/check"
    31  	"gvisor.dev/gvisor/tools/nogo/config"
    32  	"gvisor.dev/gvisor/tools/nogo/facts"
    33  	"gvisor.dev/gvisor/tools/nogo/flags"
    34  )
    35  
    36  // openOutput opens an output file.
    37  func openOutput(filename string, def *os.File) (*os.File, error) {
    38  	if filename == "" {
    39  		if def != nil {
    40  			return def, nil
    41  		}
    42  		filename = "/dev/null" // Sink.
    43  	}
    44  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
    45  	if err != nil {
    46  		// See above.
    47  		return nil, err
    48  	}
    49  	return f, nil
    50  }
    51  
    52  // closeOutput closes an output if necessary.
    53  //
    54  // If an error occurs during close, this function will panic.
    55  func closeOutput(w io.Writer) {
    56  	if c, ok := w.(io.Closer); ok {
    57  		if err := c.Close(); err != nil {
    58  			panic(err)
    59  		}
    60  	}
    61  }
    62  
    63  // failure exits with the given failure message.
    64  func failure(fmtStr string, v ...any) subcommands.ExitStatus {
    65  	fmt.Fprintf(os.Stderr, fmtStr+"\n", v...)
    66  	return subcommands.ExitFailure
    67  }
    68  
    69  // isTerminal return true if the file is a terminal.
    70  func isTerminal(w io.Writer) bool {
    71  	f, ok := w.(*os.File)
    72  	if !ok {
    73  		return false
    74  	}
    75  	_, err := unix.IoctlGetTermios(int(f.Fd()), unix.TCGETS)
    76  	return err == nil
    77  }
    78  
    79  // collectAllFiles collects all files from a directory tree.
    80  func collectAllFiles(dir string) (files []string, err error) {
    81  	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
    82  		if err == nil && !info.IsDir() {
    83  			files = append(files, path)
    84  		}
    85  		return nil
    86  	})
    87  	return
    88  }
    89  
    90  // checkCommon is a common set of flags for check-like commands.
    91  type checkCommon struct {
    92  	Facts    string
    93  	Findings string
    94  	Text     bool
    95  }
    96  
    97  // setFlags may be called by embedding types.
    98  //
    99  // Note that the default file names here depend on the command name. See init
   100  // at the bottom, where this files will be registered if they exist already.
   101  func (c *checkCommon) setFlags(fs *flag.FlagSet, commandType string) {
   102  	fs.StringVar(&c.Facts, "facts", fmt.Sprintf(".nogo.%s.facts", commandType), "facts output file (optional)")
   103  	fs.StringVar(&c.Findings, "findings", "", "findings output file (optional)")
   104  	fs.BoolVar(&c.Text, "text", false, "force text output (by default, only if output is a terminal)")
   105  }
   106  
   107  // execute runs the common bits for a check command.
   108  func (c *checkCommon) execute(fn func() (check.FindingSet, facts.Serializer, error)) error {
   109  	// Open outputs.
   110  	factsOutput, err := openOutput(c.Facts, nil)
   111  	if err != nil {
   112  		return fmt.Errorf("opening facts: %w", err)
   113  	}
   114  	defer closeOutput(factsOutput)
   115  	findingsOutput, err := openOutput(c.Findings, os.Stdout)
   116  	if err != nil {
   117  		return fmt.Errorf("opening findings: %w", err)
   118  	}
   119  	defer closeOutput(findingsOutput)
   120  
   121  	// Perform the analysis.
   122  	findings, factData, err := fn()
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	// Save the data.
   128  	if err := factData.Serialize(factsOutput); err != nil {
   129  		return fmt.Errorf("writing facts: %w", err)
   130  	}
   131  	if !c.Text && !isTerminal(findingsOutput) {
   132  		// Write in the default internal format (GOB encoded).
   133  		if err := check.WriteFindingsTo(findingsOutput, findings, false /* json */); err != nil {
   134  			return fmt.Errorf("writing findings: %w", err)
   135  		}
   136  	} else {
   137  		// Use a human readable text.
   138  		for _, finding := range findings {
   139  			fmt.Fprintf(findingsOutput, "%s\n", finding.String())
   140  		}
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // Check implements subcommands.Command for the "check" command.
   147  type Check struct {
   148  	checkCommon
   149  	Package string
   150  	Binary  string
   151  }
   152  
   153  // Name implements subcommands.Command.Name.
   154  func (*Check) Name() string {
   155  	return "check"
   156  }
   157  
   158  // Synopsis implements subcommands.Command.Synopsis.
   159  func (*Check) Synopsis() string {
   160  	return "Generate facts and findings for a specific named package and sources."
   161  }
   162  
   163  // Usage implements subcommands.Command.Usage.
   164  func (*Check) Usage() string {
   165  	return `check <srcs...>
   166  
   167  	Generates facts and findings for a specific named package and sources.
   168  	This command should generally be considered a "low-level" command, and
   169  	it is recommend that you use bundle or mod instead.
   170  
   171  `
   172  }
   173  
   174  // SetFlags implements subcommands.Command.SetFlags.
   175  func (c *Check) SetFlags(fs *flag.FlagSet) {
   176  	c.setFlags(fs, "check")
   177  	fs.StringVar(&c.Package, "package", "", "package for analysis (required)")
   178  }
   179  
   180  // Execute implements subcommands.Command.Execute.
   181  func (c *Check) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
   182  	if c.Package == "" {
   183  		c.Package = "main" // Default, no imports.
   184  	}
   185  
   186  	// Perform the analysis.
   187  	if err := c.execute(func() (check.FindingSet, facts.Serializer, error) {
   188  		return check.Package(c.Package /* path */, fs.Args() /* srcs */)
   189  	}); err != nil {
   190  		return failure("%v", err)
   191  	}
   192  
   193  	return subcommands.ExitSuccess
   194  }
   195  
   196  // Bundle implements subcommands.Command for the "bundle" command.
   197  type Bundle struct {
   198  	checkCommon
   199  	Root   string
   200  	Prefix string
   201  	Filter string
   202  }
   203  
   204  // Name implements subcommands.Command.Name.
   205  func (*Bundle) Name() string {
   206  	return "bundle"
   207  }
   208  
   209  // Synopsis implements subcommands.Command.Synopsis.
   210  func (*Bundle) Synopsis() string {
   211  	return "Generate facts and findings for a set of sources."
   212  }
   213  
   214  // Usage implements subcommands.Command.Usage.
   215  func (*Bundle) Usage() string {
   216  	return `bundle <srcs...>
   217  
   218  	Generates facts and findings for a collection of source files. Each
   219  	package name is inferred from the path, assuming a standard package
   220  	structure. The stripped prefix is determined by regular expression.
   221  
   222  `
   223  }
   224  
   225  // SetFlags implements subcommands.Command.SetFlags.
   226  func (b *Bundle) SetFlags(fs *flag.FlagSet) {
   227  	b.setFlags(fs, "bundle")
   228  	fs.StringVar(&b.Root, "root", "", "root regular expression (for package discovery)")
   229  	fs.StringVar(&b.Prefix, "prefix", "", "package prefix to apply (for complete names)")
   230  }
   231  
   232  // Execute implements subcommands.Command.Execute.
   233  func (b *Bundle) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
   234  	// Perform the analysis.
   235  	if err := b.execute(func() (check.FindingSet, facts.Serializer, error) {
   236  		// Discover the correct common root.
   237  		srcRootPrefix, err := check.FindRoot(fs.Args(), b.Root)
   238  		if err != nil {
   239  			return nil, nil, err
   240  		}
   241  		// Split into packages.
   242  		sources := make(map[string][]string)
   243  		for pkg, srcs := range check.SplitPackages(fs.Args(), srcRootPrefix) {
   244  			path := pkg
   245  			if b.Prefix != "" {
   246  				path = b.Prefix + "/" + path // Subpackage.
   247  			}
   248  			sources[path] = append(sources[path], srcs...)
   249  		}
   250  		return check.Bundle(sources)
   251  	}); err != nil {
   252  		return failure("%v", err)
   253  	}
   254  
   255  	return subcommands.ExitSuccess
   256  }
   257  
   258  // Stdlib implements subcommands.Command for the "stdlib" command.
   259  type Stdlib struct {
   260  	checkCommon
   261  }
   262  
   263  // Name implements subcommands.Command.Name.
   264  func (*Stdlib) Name() string {
   265  	return "stdlib"
   266  }
   267  
   268  // Synopsis implements subcommands.Command.Synopsis.
   269  func (*Stdlib) Synopsis() string {
   270  	return "Generate facts and findings for the standard library."
   271  }
   272  
   273  // Usage implements subcommands.Command.Usage.
   274  func (*Stdlib) Usage() string {
   275  	return `stdlib
   276  
   277  	Generates facts and findings for the standard library. This wraps
   278  	bundle with a mechansim that discovers the standard library source.
   279  
   280  `
   281  }
   282  
   283  // SetFlags implements subcommands.Command.SetFlags.
   284  func (s *Stdlib) SetFlags(fs *flag.FlagSet) {
   285  	s.setFlags(fs, "stdlib")
   286  }
   287  
   288  // Execute implements subcommands.Command.Execute.
   289  func (s *Stdlib) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
   290  	if fs.NArg() != 0 {
   291  		return subcommands.ExitUsageError // Need no arguments.
   292  	}
   293  
   294  	if err := s.execute(func() (check.FindingSet, facts.Serializer, error) {
   295  		root, err := flags.Env("GOROOT")
   296  		if err != nil {
   297  			return nil, nil, err
   298  		}
   299  		root = path.Join(root, "src")
   300  		srcs, err := collectAllFiles(root)
   301  		if err != nil {
   302  			return nil, nil, err
   303  		}
   304  		return check.Bundle(check.SplitPackages(srcs, root))
   305  	}); err != nil {
   306  		return failure("%v", err)
   307  	}
   308  
   309  	return subcommands.ExitSuccess
   310  }
   311  
   312  // Filter implements subcommands.Command for the "filter" command.
   313  type Filter struct {
   314  	Configs flags.StringList
   315  	Output  string
   316  	Text    bool
   317  	Test    bool
   318  }
   319  
   320  // Name implements subcommands.Command.Name.
   321  func (*Filter) Name() string {
   322  	return "filter"
   323  }
   324  
   325  // Synopsis implements subcommands.Command.Synopsis.
   326  func (*Filter) Synopsis() string {
   327  	return "Filters findings based on merged configurations."
   328  }
   329  
   330  // Usage implements subcommands.Command.Usage.
   331  func (*Filter) Usage() string {
   332  	return `filter [findings...]
   333  
   334  	Merges the set of provided configurations and applies to all findings.
   335  	The filtered findings are merged and written to the output.
   336  
   337  `
   338  }
   339  
   340  // SetFlags implements subcommands.Command.SetFlags.
   341  func (f *Filter) SetFlags(fs *flag.FlagSet) {
   342  	fs.Var(&f.Configs, "config", "filter configuration files (in JSON format)")
   343  	fs.StringVar(&f.Output, "output", "", "findings output (in JSON format by default, unless attached to a terminal)")
   344  	fs.BoolVar(&f.Text, "text", false, "force text format in all cases (even not attached to a terminal)")
   345  	fs.BoolVar(&f.Test, "test", false, "exit with non-zero status if findings are not empty")
   346  }
   347  
   348  func loadFindings(filename string) (check.FindingSet, error) {
   349  	r, err := os.Open(filename)
   350  	if err != nil {
   351  		return nil, fmt.Errorf("unable to open input: %w", err)
   352  	}
   353  	inputFindings, err := check.ExtractFindingsFrom(r, false /* json */)
   354  	if err != nil {
   355  		// Seek to reread the file.
   356  		if _, err := r.Seek(0, os.SEEK_SET); err != nil {
   357  			return nil, fmt.Errorf("unable to reseek in findings %q: %w", filename, err)
   358  		}
   359  		// Attempt to interpret as a json input.
   360  		inputFindings, err = check.ExtractFindingsFrom(r, true /* json */)
   361  		if err != nil {
   362  			return nil, fmt.Errorf("unable to extract findings from %q: %w", filename, err)
   363  		}
   364  	}
   365  	return inputFindings, nil
   366  }
   367  
   368  func loadConfig(filename string) (*config.Config, error) {
   369  	f, err := os.Open(filename)
   370  	if err != nil {
   371  		return nil, fmt.Errorf("unable to open config: %w", err)
   372  	}
   373  	var newConfig config.Config // For current file.
   374  	dec := yaml.NewDecoder(f)
   375  	dec.SetStrict(true)
   376  	if err := dec.Decode(&newConfig); err != nil {
   377  		return nil, fmt.Errorf("unable to decode %q: %w", filename, err)
   378  	}
   379  	return &newConfig, nil
   380  }
   381  
   382  func loadConfigs(filenames []string) (*config.Config, error) {
   383  	config := &config.Config{
   384  		Global:    make(config.AnalyzerConfig),
   385  		Analyzers: make(map[string]config.AnalyzerConfig),
   386  	}
   387  	for _, filename := range filenames {
   388  		next, err := loadConfig(filename)
   389  		if err != nil {
   390  			return nil, err
   391  		}
   392  		config.Merge(next)
   393  	}
   394  	if err := config.Compile(); err != nil {
   395  		return nil, fmt.Errorf("error compiling config: %w", err)
   396  	}
   397  	return config, nil
   398  }
   399  
   400  // Execute implements subcommands.Command.Execute.
   401  func (f *Filter) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
   402  	// Open and merge all configuations.
   403  	config, err := loadConfigs(f.Configs)
   404  	if err != nil {
   405  		return failure("unable to load configurations: %v", err)
   406  	}
   407  
   408  	// Open the output file.
   409  	output, err := openOutput(f.Output, os.Stdout)
   410  	if err != nil {
   411  		return failure("opening output: %v", err)
   412  	}
   413  	defer closeOutput(output)
   414  
   415  	// Load and filter available findings.
   416  	var filteredFindings check.FindingSet
   417  	for _, filename := range fs.Args() {
   418  		// Note that this applies a caching strategy to the filtered
   419  		// findings, because *this is by far the most expensive part of
   420  		// evaluation*. The set of findings is large and applying the
   421  		// configuration is complex. Therefore, we segment this cache
   422  		// on each individual raw findings input file and the
   423  		// configuration files. Note that this cache is keyed on all
   424  		// the configuration files and each individual raw findings, so
   425  		// is guaranteed to be safe. This allows us to reuse the same
   426  		// filter result many times over, because e.g. all standard
   427  		// library findings will be available to all packages.
   428  		inputFindings, err := loadFindings(filename)
   429  		if err != nil {
   430  			return failure("unable to load findings from %q: %v", filename, err)
   431  		}
   432  		for _, finding := range inputFindings {
   433  			if ok := config.ShouldReport(finding); ok {
   434  				filteredFindings = append(filteredFindings, finding)
   435  			}
   436  		}
   437  	}
   438  
   439  	// Write the output.
   440  	if !f.Text && !isTerminal(output) {
   441  		if err := check.WriteFindingsTo(output, filteredFindings, true /* json */); err != nil {
   442  			return failure("write findings: %v", err)
   443  		}
   444  	} else {
   445  		for _, finding := range filteredFindings {
   446  			fmt.Fprintf(output, "%s\n", finding.String())
   447  		}
   448  	}
   449  
   450  	// Treat the run as a test?
   451  	if (f.Text || isTerminal(output)) && f.Test && len(filteredFindings) == 0 {
   452  		fmt.Fprintf(output, "PASS\n")
   453  	}
   454  	if f.Test && len(filteredFindings) > 0 {
   455  		return subcommands.ExitFailure
   456  	}
   457  
   458  	return subcommands.ExitSuccess
   459  }
   460  
   461  // Main is the main entrypoint.
   462  func Main() {
   463  	subcommands.Register(&Check{}, "")
   464  	subcommands.Register(&Bundle{}, "")
   465  	subcommands.Register(&Stdlib{}, "")
   466  	subcommands.Register(&Filter{}, "")
   467  	subcommands.Register(subcommands.HelpCommand(), "")
   468  	subcommands.Register(subcommands.FlagsCommand(), "")
   469  	flag.CommandLine.Parse(os.Args[1:])
   470  	os.Exit(int(subcommands.Execute(context.Background())))
   471  }