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 }