Open-Source Wikis

/

Trivy

/

How to contribute

/

Patterns and conventions

aquasecurity/trivy

Patterns and conventions

This page is a quick tour of the conventions you will see when reading and writing code in pkg/. None of them are unusual on their own, but they are pervasive enough to be worth calling out.

Error handling

  • Wrap with xerrors.Errorf. The codebase uses golang.org/x/xerrors for error wrapping rather than the standard library fmt.Errorf. The pattern is return xerrors.Errorf("failed to do X: %w", err). Migration to fmt.Errorf has been considered but not done — match the surrounding code.
  • Sentinel errors live next to the type they describe. Examples: analyzer.ErrUnknownOS, analyzer.ErrPkgAnalysis, analyzer.ErrNoPkgsDetected (pkg/fanal/analyzer/analyzer.go). Compare with errors.Is.
  • types.UserError and types.ExitError in pkg/types/error.go carry user-facing messages and exit codes. The CLI in cmd/trivy/main.go checks for these via errors.As.
  • errcheck is strict. The lint config (.golangci.yaml) sets check-blank: true — you cannot drop an error with _, _ = f(). Either handle it or use a defer helper that logs.
  • Don't shadow err across goroutines. The codebase uses golang.org/x/sync/errgroup for parallel work; collect errors through the group, not via shared variables.

Registration patterns

A common pattern in fanal and IaC is "register from init()":

func init() {
    analyzer.RegisterAnalyzer(&apkAnalyzer{})
}

This means importing a package can change global state. The convention is to expose an aggregator package (e.g., pkg/fanal/analyzer/all/all.go) that imports every analyzer for its side effects. Top-level binaries import the aggregator instead of every individual analyzer.

The same shape is used for:

  • IaC scanners (pkg/iac/scanners/<provider>/)
  • Cloud providers (pkg/iac/providers/)
  • Report writers (pkg/report/)
  • Secret rules (pkg/fanal/secret/builtin-rules.go)

Flag handling

Trivy's flag system is custom (pkg/flag/):

  1. Each domain owns a *FlagGroup (e.g., ImageFlagGroup, ScanFlagGroup).
  2. Each command in pkg/commands/app.go composes flag groups via flag.Flags{...}.AddFlags(cmd).
  3. After Cobra parses, flag.Flags{...}.ToOptions(args) materializes a typed flag.Options struct.
  4. Downstream code consumes flag.Options (and never raw viper/pflag references).

When you add a new flag:

  • Define it on the appropriate group in pkg/flag/<area>_flags.go.
  • Add a unit test in the matching <area>_flags_test.go.
  • Bind it to viper if it should be configurable via env var or config file (the Flag.ConfigName field controls this).
  • Document it in the user-facing docs (docs/).

Report and Result shapes

pkg/types/report.go is the single source of truth for the report shape. Adding a new finding kind means:

  1. Add it to types.Result (e.g., a new slice).
  2. Update the renderers in pkg/report/ (table, JSON, SARIF, CycloneDX, SPDX, template, GitHub).
  3. Update pkg/rpc/convert.go to round-trip the new field over RPC.
  4. Update fixtures in integration/testdata/.

Logging

Use the structured logger via pkg/log:

log.Info("Detected OS", log.String("family", string(family)), log.String("name", name))
log.Warn("EOSL OS detected", log.Err(err))
log.Debug("Walked file", log.String("path", path))

Avoid fmt.Printf and the standard library logger in production code paths. The progress bar in pkg/cheggaaa/... (re-exported in helpers) is the only sanctioned non-log output.

Concurrency

  • golang.org/x/sync/errgroup for parallel work with cancellation.
  • golang.org/x/sync/semaphore to bound concurrency (used heavily by the analyzer registry to cap parallel file analysis with --parallel).
  • pkg/parallel/ and pkg/semaphore/ wrap these for common cases.
  • Avoid raw goroutines without a waitgroup or errgroup.

Filesystem abstractions

  • The walker uses fs.FS (io/fs) interfaces wherever possible. Image layers are exposed as a mapfs.FS (pkg/mapfs/) so layered overlays Just Work.
  • For testing, github.com/spf13/afero is used. New code should prefer io/fs if it can.

Time

Don't use time.Now() directly in code paths that produce timestamps in reports. Use clock.Now(ctx) from pkg/clock. Tests can override the clock for deterministic output.

Cobra command shape

Each subcommand is built by a New<Name>Command(globalFlags *flag.GlobalFlagGroup) *cobra.Command function. The body:

  1. Builds a flags struct (<area>Flags := flag.Flags{...}).
  2. Returns a &cobra.Command{...} with RunE that calls into artifact.Run(...) or another runner.
  3. Calls flags.AddFlags(cmd) and flags.Bind(cmd) for viper.

Match this shape when adding a new command.

Naming

  • Filenames are snake_case.go. Test files end in _test.go.
  • Type names are CamelCase; package-private helpers are camelCase.
  • Constants are typed strings (e.g., analyzer.Type) rather than iota enums.

Imports

Goimports is implicit in golangci-lint. The convention is three groups: standard library, third-party, then github.com/aquasecurity/trivy/.... Most files already follow this.

Generated code

Generated files are committed:

  • rpc/**/*.pb.go and rpc/**/*.twirp.go — generated from .proto files via buf (buf.gen.yaml).
  • pkg/iac/rego/embed.go — embedded Rego policies (built into the binary).
  • pkg/k8s/scanner/builtin/... — generated Kubernetes resource handlers.

Regenerate with mage build:rpc or by running buf generate. Check the magefiles/ for the latest invocation.

Documentation

User-facing markdown lives under docs/ and is built with MkDocs. Per-package Go doc comments are useful for reviewers but are not the primary user-facing surface.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Trivy wiki | Factory