syft/internal/capabilities
Alex Goodman da745b13e8
feat(golang): add extended-stdlib scope and module patterns for symbol capture (#5154)
* feat(golang): add extended-stdlib scope and include patterns for symbol capture

`golang.capture-symbols` decides how much symbol data lands in the SBOM for grype's reachability analysis. It's `none`, `stdlib`, or `all` today, and the useful middle is missing: `stdlib` stops at the standard library, `all` multiplies SBOM size.

A new `extended-stdlib` configurable covers stdlib plus everything under `golang.org/x/`:

```yaml
golang:
  capture-symbols: extended-stdlib
```

Also, a new `capture-symbols-include` configurable for modules that are noisy in your binaries but not everyone's. It's unioned with whatever the scope selects, so it only ever widens:

```yaml
golang:
  capture-symbols: extended-stdlib
  capture-symbols-include:
    - github.com/klauspost/**
```

Patterns are standard doublestar globs, which matters because module paths carry `/v2`-style suffixes:

```yaml
golang:
  capture-symbols-include:
    - github.com/klauspost/*     # compress, but not compress/v2
    - github.com/klauspost/**    # both
    - k8s.io/client-go           # exact match only
```

Ordering is `none` < `stdlib` < `extended-stdlib` < `all`. The existing three values
and the `none` default are unchanged, and the include list is inert under `none`.
Presets compile into glob lists internally, so a single matcher answers "does this
module get symbols" instead of a preset branch sitting next to a separate glob branch.

An unrecognized `capture-symbols` value still falls back to `none`, but warns now
instead of doing it silently. A malformed include pattern warns and gets skipped.

One thing worth a look beyond the feature: the `Symbols` field description in the JSON
schema was wrong after this (it claimed only `all` and `stdlib` populate anything), and
that description lives in the already-published `16.1.10`. Rather than bump a version for
a sentence, `16.1.10` is amended in place and `schema/json/README.md` grows an explicit
exception for description-only changes: descriptions only, no shape change of any kind,
`$id` unchanged. Anything else still needs a bump. Happy to split that into its own PR if
you'd rather review the policy separately.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* refactor(golang): rename capture-symbols-include to capture-symbols-modules

The key's entries are go module paths, and `-include` sitting next to `capture-symbols` reads as plausibly taking symbol or package names instead. Those spellings parse and match nothing, which is quieter than the confusion `-include` was picked to avoid, so the name now says what the list holds.

`golang.CatalogerConfig.CaptureSymbolsModules` and `WithCaptureSymbolsModules` rename with it. Nothing behavioral changes; the key is new in this PR so there is no compatibility surface.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* feat(golang): match capture-symbols-modules across major version suffixes

`github.com/anchore/*` covered `github.com/anchore/syft` and silently stopped covering it the day it became `github.com/anchore/syft/v2`. The config keeps parsing, nothing warns, and symbols quietly go missing from the SBOM. Exact paths had the same hole: `github.com/klauspost/compress` did not cover `compress/v2` either, so no spelling short of `**` survived a major bump.

A major version suffix is part of a module's path but not part of its identity, so patterns are now matched against the module path both with and without it, using `module.SplitPathVersion` from `golang.org/x/mod` (already a direct dep, already used in this package for `PseudoVersion`).

```yaml
golang:
  capture-symbols-modules:
    - github.com/klauspost/*          # compress and compress/v2
    - github.com/klauspost/compress   # same module at every major version
    - github.com/klauspost/compress/v2  # v2 alone
```

Only a trailing suffix is a version, which is Go's own rule. In `github.com/anchore/syft/v2/thing` the `v2` is an ordinary path element naming a major subdirectory a nested module lives in, so it stays literal and `github.com/anchore/**/thing` is how you reach it. `/v0` and `/v1` are not valid suffixes and are left alone.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-07 12:44:42 +00:00
..

Cataloger Capabilities Documentation

This documentation describes the format and structure of cataloger capabilities YAML files.

File Organization

Capabilities are organized as follows:

  • Cataloger capabilities: Located in syft/pkg/cataloger/*/capabilities.yaml (one file per ecosystem, alongside the cataloger source code: golang/capabilities.yaml, python/capabilities.yaml, etc.)
  • Application configuration: Located in internal/capabilities/appconfig.yaml

Each capabilities.yaml file is partially auto-generated. Run go generate ./internal/capabilities to regenerate.

  • Fields marked AUTO-GENERATED will be updated during regeneration
  • All capabilities sections are MANUAL - edit these to describe cataloger behavior

Capability Sections

There are two types of capability sections depending on cataloger type:

1. Generic Catalogers (type: generic)

  • Have capabilities at the PARSER level
  • Each parser function has its own capabilities section
  • Allows different parsers within the same cataloger to have different capabilities

2. Custom Catalogers (type: custom)

  • Have capabilities at the CATALOGER level
  • Single capabilities section for the entire cataloger

Capabilities Format

Capabilities use a field-based format with defaults and optional conditional overrides:

capabilities:
  - field: <field-name>           # dot-notation path (e.g., "license", "dependency.depth")
    default: <value>              # value when no conditions match
    conditions:                   # optional - conditional overrides evaluated in order
      - when: {ConfigField: val}  # when these config fields match (AND logic)
        value: <override-value>   # use this value instead
        comment: "explanation"    # optional - why this condition exists
    evidence:                     # optional - source code references
      - "StructName.FieldName"
    comment: "explanation"        # optional - general field explanation

Detector Conditions

Detectors (used by custom catalogers) can have optional conditions that control when they are active. This allows a single cataloger to have different detection behavior based on configuration.

Structure

detectors:
  - method: glob                 # AUTO-GENERATED - detection method
    criteria: ["**/*.jar"]       # AUTO-GENERATED - patterns to match
    comment: "always active"     # MANUAL - optional explanation
  - method: glob
    criteria: ["**/*.zip"]
    conditions:                  # MANUAL - when this detector is active
      - when: {IncludeZipFiles: true}  # config fields that must match
        comment: "optional explanation"
    comment: "ZIP detection requires config"

Notes

  • Conditions reference fields from the cataloger's config struct
  • Multiple conditions in the array use OR logic (any condition can activate)
  • Multiple fields in a when clause use AND logic (all must match)
  • Detectors without conditions are always active
  • Only custom catalogers support detectors with conditions

Condition Evaluation

  • Conditions are evaluated in array order (first match wins)
  • Multiple fields in a when clause use AND logic (all must match)
  • Multiple conditions in the array use OR logic (first matching condition)
  • If no conditions match, the default value is used

Capability Fields

Standard capability field names and their value types:

license (boolean)

Whether license information is available.

Examples:

default: true                 # always available
default: false                # never available
default: false                # requires configuration
  conditions:
    - when: {SearchRemoteLicenses: true}
      value: true

dependency.depth (array of strings)

Which dependency depths can be discovered.

Values: direct (immediate deps), indirect (transitive deps)

Examples:

default: [direct]                    # only immediate dependencies
default: [direct, indirect]          # full transitive closure
default: []                          # no dependency information

dependency.edges (string)

Relationships between nodes and completeness of the dependency graph.

Values:

  • "" - dependencies found but no edges between them
  • "flat" - single level of dependencies with edges to root package only
  • "reduced" - transitive reduction (redundant edges removed)
  • "complete" - all relationships with accurate direct and indirect edges

Examples:

default: complete
default: ""

dependency.kinds (array of strings)

Types of dependencies that can be discovered.

Values: runtime, dev, build, test, optional

Examples:

default: [runtime]                   # production dependencies only
default: [runtime, dev]              # production and development
default: [runtime, dev, build]       # all dependency types
default: [runtime]                   # with conditional dev deps
  conditions:
    - when: {IncludeDevDeps: true}
      value: [runtime, dev]

package_manager.files.listing (boolean)

Whether file listings are available (which files belong to the package).

Examples:

default: true
default: false
  conditions:
    - when: {CaptureOwnedFiles: true}
      value: true

package_manager.files.digests (boolean)

Whether file digests/checksums are included in listings.

Examples:

default: true
default: false

package_manager.package_integrity_hash (boolean)

Whether a hash for verifying package integrity is available.

Examples:

default: true
default: false

Examples

Simple cataloger with no configuration

capabilities:
  - name: license
    default: true
    comment: "license field always present in package.json"
  - name: dependency.depth
    default: [direct]
  - name: dependency.edges
    default: ""
  - name: dependency.kinds
    default: [runtime]
    comment: "devDependencies not parsed by this cataloger"
  - name: package_manager.files.listing
    default: false
  - name: package_manager.files.digests
    default: false
  - name: package_manager.package_integrity_hash
    default: false

Cataloger with configuration-dependent capabilities

capabilities:
  - name: license
    default: false
    conditions:
      - when: {SearchLocalModCacheLicenses: true}
        value: true
        comment: "searches for licenses in GOPATH mod cache"
      - when: {SearchRemoteLicenses: true}
        value: true
        comment: "fetches licenses from proxy.golang.org"
    comment: "license scanning requires configuration"
  - name: dependency.depth
    default: [direct, indirect]
  - name: dependency.edges
    default: flat
  - name: dependency.kinds
    default: [runtime, dev]
  - name: package_manager.files.listing
    default: false
  - name: package_manager.files.digests
    default: false
  - name: package_manager.package_integrity_hash
    default: true
    evidence:
      - "GolangBinaryBuildinfoEntry.H1Digest"