diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6b375178..9e8e396c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,12 @@ version: 2 updates: - package-ecosystem: "gomod" # See documentation for possible values - directory: "/" # Location of package manifests + # The glob covers the root module and every nested component module + # (components///go.mod); new component waves are picked up + # automatically. + directories: + - "/" + - "/components/*/*" schedule: interval: "weekly" groups: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7fd4054..7c100c52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,9 +27,11 @@ jobs: cache: true cache-dependency-path: go.sum - name: Run golangci-lint - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 - with: - version: v2.12.0 + # make lint installs the Makefile-pinned golangci-lint and runs it + # for the root module and every nested component module; the + # golangci-lint-action's bare root invocation never descends into + # components/. + run: make lint test: name: Test @@ -63,33 +65,6 @@ jobs: - name: Build lite Bomly binary run: go build -tags "bomly_external_syft,bomly_external_grype" -o /tmp/bomly-lite ./cmd/bomly - pinned-build: - name: Pinned build (workspace off) - # Component-module pseudo-versions only resolve after a wave PR merges, - # so this pins-only guard runs on pushes to main rather than on PRs. - if: github.event_name == 'push' - runs-on: ubuntu-latest - env: - GOWORK: off - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - cache: true - cache-dependency-path: go.sum - - name: Build with module pins only - run: go build ./cmd/bomly - - name: Build lite variant with module pins only - # Mirrors the regular build job's lite variant so a stale pin in a - # future extracted Syft/Grype component module fails here instead of - # only in GoReleaser. - run: go build -tags "bomly_external_syft,bomly_external_grype" ./cmd/bomly - - name: Test with module pins only - run: go test ./internal/... ./cmd/... - format: name: Format runs-on: ubuntu-latest @@ -123,14 +98,40 @@ jobs: set -euo pipefail go mod tidy git diff --exit-code -- go.mod go.sum - - name: Forbid replace directives + - name: Check component module metadata drift + shell: bash + run: | + set -euo pipefail + for modfile in components/*/*/go.mod; do + [ -e "$modfile" ] || continue + dir="$(dirname "$modfile")" + (cd "$dir" && go mod tidy) + done + git diff --exit-code -- components/ + - name: Enforce the replace-directive allowlist shell: bash + # The root module intentionally replaces each in-repo component + # module with its ./components/ directory (documented trade-off: + # remote `go install ...@latest` is unsupported; install from + # releases or a clone). Any other replace target in the root, and + # any replace at all in a component module, is forbidden. + # `go mod edit -json` sees single-line and block-form directives + # alike, which a line-oriented grep would miss. run: | set -euo pipefail - if grep -E '^replace ' go.mod; then - echo "go.mod must not contain replace directives" >&2 + bad="$(go mod edit -json | jq -r '.Replace[]? | select(.New.Path | startswith("./components/") | not) | .Old.Path')" + if [ -n "$bad" ]; then + echo "root go.mod may only replace in-repo ./components/ modules; found: $bad" >&2 exit 1 fi + for modfile in components/*/*/go.mod; do + [ -e "$modfile" ] || continue + count="$(go mod edit -json "$modfile" | jq '.Replace | length')" + if [ "$count" != "0" ] && [ "$count" != "null" ]; then + echo "$modfile must not contain replace directives" >&2 + exit 1 + fi + done generated-docs: name: Generated docs drift diff --git a/.github/workflows/portable-assurance.yml b/.github/workflows/portable-assurance.yml index 9b35aaf3..b138c80b 100644 --- a/.github/workflows/portable-assurance.yml +++ b/.github/workflows/portable-assurance.yml @@ -34,7 +34,9 @@ jobs: failed=0 for iteration in 1 2; do echo "portable suite iteration ${iteration}" - if go test ./... -count=1; then + # make test covers the root module and every nested component + # module; root `go test ./...` alone would skip components/. + if make test GOFLAGS=-count=1; then completed="${iteration}" else failed="${iteration}" @@ -84,7 +86,7 @@ jobs: echo "Open the **Repeat portable suite twice** step to find the failing package and test. To reproduce the same check locally:" echo echo '```sh' - echo "go test ./... -count=1" + echo "make test GOFLAGS=-count=1" echo '```' } >> "${GITHUB_STEP_SUMMARY}" @@ -116,7 +118,7 @@ jobs: failed=0 for iteration in $(seq 1 10); do echo "Java suite iteration ${iteration}" - if go test ./internal/detectors/gradle ./internal/detectors/maven ./internal/detectors/sbt ./internal/analyzers/jvmreach -count=1; then + if go test ./internal/detectors/gradle ./internal/detectors/maven ./internal/detectors/sbt github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach -count=1; then completed="${iteration}" else failed="${iteration}" @@ -137,7 +139,9 @@ jobs: failed=0 for iteration in $(seq 1 5); do echo "complete suite iteration ${iteration}" - if go test ./... -count=1; then + # make test covers the root module and every nested component + # module; root `go test ./...` alone would skip components/. + if make test GOFLAGS=-count=1; then completed="${iteration}" else failed="${iteration}" @@ -262,8 +266,8 @@ jobs: echo "Open the failed step for the test name or release target. Reproduce the test checks with:" echo echo '```sh' - echo "go test ./internal/detectors/gradle ./internal/detectors/maven ./internal/detectors/sbt ./internal/analyzers/jvmreach -count=1" - echo "go test ./... -count=1" + echo "go test ./internal/detectors/gradle ./internal/detectors/maven ./internal/detectors/sbt github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach -count=1" + echo "make test GOFLAGS=-count=1" echo '```' echo echo "For a failed release build, use the operating system, architecture, and variant shown in the table with the build command from **Cross-build release targets**." diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1b2e4617..985d19df 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -5,14 +5,6 @@ project_name: bomly metadata: mod_timestamp: "{{ .CommitTimestamp }}" -# The whole release run — before hooks (`go mod download`, `make licenses`) -# and builds alike — resolves dependencies from go.mod pins only; the -# committed go.work is a local development convenience. A build-scoped -# GOWORK=off would leave the hooks resolving workspace-tip components and -# ship a license tree that does not match the released binaries. -env: - - GOWORK=off - before: hooks: - go mod download diff --git a/AGENTS.md b/AGENTS.md index 875786da..8d1baad8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ make generate # regenerate config reference, JSON schemas, schema doc Always run `make test` after changes. All tests must pass before marking work is done. If you change `internal/cli/config.go`, `internal/output/*`, or `internal/registry/support.go`, or bump the pinned `bomly-dev/bomly-sdk` version (its catalog or support-matrix data feeds the generated docs), also run `make generate` and commit the docs drift. -`go.mod` pins released versions and must not contain `replace` directives on main (CI enforces this). The committed `go.work` lists in-repo modules only (root now; `components/*` as waves land). Local cross-repo SDK development: `go work use ../bomly-sdk` (never commit that entry). +`go.mod` pins released versions. The only `replace` directives allowed on main point in-repo component modules at their `./components//` directories (CI enforces this allowlist; component go.mods carry no replaces at all). Trade-off, accepted deliberately: remote `go install ...@latest` is unsupported for replace-carrying modules — users install from releases, package managers, or a clone. Local cross-repo SDK development: `go work init . ../bomly-sdk` (never commit `go.work`). ### Git Worktrees @@ -50,7 +50,7 @@ See [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) for full detail (the | `internal/detectors/*` | Concrete dependency resolution per ecosystem (gomod, gradle, maven, node, python, sbom, syft) | | `internal/matchers/*` | External enrichment matchers (osv, grype, deps.dev, scorecard; ClearlyDefined and eol run as external matcher plugins); the shared cache lives in `bomly-sdk/filecache` | | `internal/auditors/*` | Policy evaluators and audit-only logic (policy, noop) | -| `internal/analyzers/*` | Built-in reachability analyzers (govulncheck, jsreach) | +| `components/analyzers/*` | Built-in reachability analyzers as nested component modules (govulncheck, jsreach, pyreach, jvmreach) | | `internal/baseline` | Portable package-finding baseline codec and audit-integrated policy-status resolver | | `internal/remediation` | Canonical vulnerability fix status, version, detector-hint validation, and occurrence suggestions | | `internal/sbom` | SBOM codec (SPDX 2.3, CycloneDX) | @@ -82,14 +82,14 @@ Runtime preparation is owned by `internal/engine`: build the filtered registry o - Shared helper code (bounded filesystem/subprocess ops, file cache, subprocess logging, detector/matcher helpers, test kit) lives in `bomly-sdk` subpackages: `system`, `filecache`, `logkit`, `detectorkit`, `matcherkit`, `testkit`. Do not reintroduce CLI-internal copies. - Extracted components will live under `components///` as separate Go modules with their own `go.mod`, tagged per module as `components///vX.Y.Z`. -- The committed `go.work` puts the repo in workspace mode for local development; waves add `use ./components/...` entries. Release and pinned builds run with `GOWORK=off` (GoReleaser sets it explicitly; CI's `pinned-build` job verifies the module pins alone still build on pushes to `main`). +- Component modules under `components/` are consumed through root `go.mod` `require` + directory `replace` entries, so every build — local, CI, and GoReleaser — resolves them from the checkout itself. There is no committed workspace file and no separate pinned-build guard; the regular build and test jobs cover the only resolution mode that exists. - Each extraction wave lands as **one atomic PR**: move the code into its component module, add the `use` entry, and keep the root module compiling in the same change. - `scripts/release-components.sh` (also `make release-components`) is the release train: component modules version in lockstep with the CLI — after a CLI release tag exists, the script tags every component module at that same version (idempotent; unchanged modules get empty releases by design); `--apply` (ARGS="--apply") creates and pushes the tags and prints the root `go get` pin bumps for the follow-up PR. ### Package Boundaries - `internal/detectors/*` must not import `internal/engine` or `internal/registry`. Concrete detectors may depend on `internal/detectors` (name constants), the SDK and its helper subpackages (`system` for bounded filesystem and subprocess operations, `detectorkit` for shared detector helpers), and local helpers. -- Built-in analyzers may depend on the SDK and its helper subpackages (`system` for bounded filesystem and subprocess operations, `filecache`, `logkit`), and local helpers. They must not import `internal/engine` or `internal/registry`. +- Built-in analyzers live in nested component modules under `components/analyzers//` (own `go.mod`, consumed by `internal/composition` via the root `go.mod` require + directory replace pair). They may depend on the SDK and its helper subpackages (`system` for bounded filesystem and subprocess operations, `filecache`, `logkit`), and local helpers. They must not import any `internal/*` package. - `internal/detectors` owns detector-facing contracts such as `Detector`, `DetectorDescriptor`, `ResolveGraphRequest`, and detector helper functions. - The SDK owns neutral shared identifiers and support metadata that would otherwise create package cycles, including ecosystems, package managers, detector types, and support-matrix data. - `internal/baseline` owns the baseline document and matching implementation. It depends on the SDK policy contracts and must not be imported by `internal/engine`. diff --git a/CLAUDE.md b/CLAUDE.md index 5a487699..f90b0de6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ make generate # regenerate config reference, JSON schemas, schema doc Always run `make test` after changes. All tests must pass before marking work is done. If you change `internal/cli/config.go`, `internal/output/*`, or `internal/registry/support.go`, or bump the pinned `bomly-dev/bomly-sdk` version (its catalog or support-matrix data feeds the generated docs), also run `make generate` and commit the docs drift. -`go.mod` pins released versions and must not contain `replace` directives on main (CI enforces this). The committed `go.work` lists in-repo modules only (root now; `components/*` as waves land). Local cross-repo SDK development: `go work use ../bomly-sdk` (never commit that entry). +`go.mod` pins released versions. The only `replace` directives allowed on main point in-repo component modules at their `./components//` directories (CI enforces this allowlist; component go.mods carry no replaces at all). Trade-off, accepted deliberately: remote `go install ...@latest` is unsupported for replace-carrying modules — users install from releases, package managers, or a clone. Local cross-repo SDK development: `go work init . ../bomly-sdk` (never commit `go.work`). ### Git Worktrees @@ -50,7 +50,7 @@ See [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) for full detail (the | `internal/detectors/*` | Concrete dependency resolution per ecosystem (gomod, gradle, maven, node, python, sbom, syft) | | `internal/matchers/*` | External enrichment matchers (osv, grype, deps.dev, scorecard; ClearlyDefined and eol run as external matcher plugins); the shared cache lives in `bomly-sdk/filecache` | | `internal/auditors/*` | Policy evaluators and audit-only logic (policy, noop) | -| `internal/analyzers/*` | Built-in reachability analyzers (govulncheck, jsreach) | +| `components/analyzers/*` | Built-in reachability analyzers as nested component modules (govulncheck, jsreach, pyreach, jvmreach) | | `internal/baseline` | Portable package-finding baseline codec and audit-integrated policy-status resolver | | `internal/remediation` | Canonical vulnerability fix status, version, detector-hint validation, and occurrence suggestions | | `internal/sbom` | SBOM codec (SPDX 2.3, CycloneDX) | @@ -82,14 +82,14 @@ Runtime preparation is owned by `internal/engine`: build the filtered registry o - Shared helper code (bounded filesystem/subprocess ops, file cache, subprocess logging, detector/matcher helpers, test kit) lives in `bomly-sdk` subpackages: `system`, `filecache`, `logkit`, `detectorkit`, `matcherkit`, `testkit`. Do not reintroduce CLI-internal copies. - Extracted components will live under `components///` as separate Go modules with their own `go.mod`, tagged per module as `components///vX.Y.Z`. -- The committed `go.work` puts the repo in workspace mode for local development; waves add `use ./components/...` entries. Release and pinned builds run with `GOWORK=off` (GoReleaser sets it explicitly; CI's `pinned-build` job verifies the module pins alone still build on pushes to `main`). +- Component modules under `components/` are consumed through root `go.mod` `require` + directory `replace` entries, so every build — local, CI, and GoReleaser — resolves them from the checkout itself. There is no committed workspace file and no separate pinned-build guard; the regular build and test jobs cover the only resolution mode that exists. - Each extraction wave lands as **one atomic PR**: move the code into its component module, add the `use` entry, and keep the root module compiling in the same change. - `scripts/release-components.sh` (also `make release-components`) is the release train: component modules version in lockstep with the CLI — after a CLI release tag exists, the script tags every component module at that same version (idempotent; unchanged modules get empty releases by design); `--apply` (ARGS="--apply") creates and pushes the tags and prints the root `go get` pin bumps for the follow-up PR. ### Package Boundaries - `internal/detectors/*` must not import `internal/engine` or `internal/registry`. Concrete detectors may depend on `internal/detectors` (name constants), the SDK and its helper subpackages (`system` for bounded filesystem and subprocess operations, `detectorkit` for shared detector helpers), and local helpers. -- Built-in analyzers may depend on the SDK and its helper subpackages (`system` for bounded filesystem and subprocess operations, `filecache`, `logkit`), and local helpers. They must not import `internal/engine` or `internal/registry`. +- Built-in analyzers live in nested component modules under `components/analyzers//` (own `go.mod`, consumed by `internal/composition` via the root `go.mod` require + directory replace pair). They may depend on the SDK and its helper subpackages (`system` for bounded filesystem and subprocess operations, `filecache`, `logkit`), and local helpers. They must not import any `internal/*` package. - `internal/detectors` owns detector-facing contracts such as `Detector`, `DetectorDescriptor`, `ResolveGraphRequest`, and detector helper functions. - The SDK owns neutral shared identifiers and support metadata that would otherwise create package cycles, including ecosystems, package managers, detector types, and support-matrix data. - `internal/baseline` owns the baseline document and matching implementation. It depends on the SDK policy contracts and must not be imported by `internal/engine`. diff --git a/Makefile b/Makefile index 07d57a60..88af34fe 100644 --- a/Makefile +++ b/Makefile @@ -28,12 +28,26 @@ $(GOLANGCI_LINT): Makefile lint: $(GOLANGCI_LINT) $(GOLANGCI_LINT) run + @for m in $(COMPONENT_MODULES); do \ + echo "==> golangci-lint run $$m..."; \ + (cd "$$m" && "$(GOLANGCI_LINT)" run) || exit 1; \ + done install-hooks: git config core.hooksPath .githooks +# Component modules (components//) are separate Go modules, so +# root `go test ./...` does not reach them; iterate them explicitly. The root +# go.mod's directory replace directives keep the per-module runs coherent +# with the root build. +COMPONENT_MODULES=$(dir $(wildcard components/*/*/go.mod)) + test: go test ./... + @for m in $(COMPONENT_MODULES); do \ + echo "==> go test $$m..."; \ + (cd "$$m" && go test ./...) || exit 1; \ + done smoke: go test -tags "smoke" ./test/smoke/ -v -count=1 -timeout 15m $(if $(ARGS),$(ARGS),) diff --git a/internal/analyzers/govulncheck/analyzer.go b/components/analyzers/govulncheck/analyzer.go similarity index 89% rename from internal/analyzers/govulncheck/analyzer.go rename to components/analyzers/govulncheck/analyzer.go index 5b632805..3473532d 100644 --- a/internal/analyzers/govulncheck/analyzer.go +++ b/components/analyzers/govulncheck/analyzer.go @@ -43,6 +43,7 @@ func (a Analyzer) Descriptor() model.AnalyzerDescriptor { SupportedManagers: []model.PackageManager{model.PackageManagerGoMod}, SupportedLanguages: []model.Language{model.LanguageGo}, SupportedTiers: []model.ReachabilityTier{model.TierSymbol, model.TierPackage}, + Capabilities: []string{model.CapabilityPackageUpdates}, } } @@ -103,7 +104,7 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. // Unknown so consumers know the analyzer was attempted. logger.Info("govulncheck: no module roots discovered; marking all Go vulnerabilities as unknown") annotateAllUnknown(req, "no-module-root-discovered", time.Now()) - return resultForRequest(), nil + return finishResult(req, resultFromRequest(req)), nil } logger.Info("govulncheck: starting reachability analysis", @@ -170,9 +171,9 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. zap.Duration("duration", time.Since(overallStart)), ) - out := resultForRequest() + out := resultFromRequest(req) out.AnalyzerStats = map[string]model.ReachabilityStats{Name: stats} - return out, nil + return finishResult(req, out), nil } // runWithCache returns (result, fromCache, error) for one module. Cache @@ -203,7 +204,7 @@ func (a Analyzer) runWithCache( } if cache != nil { if err := cache.set(moduleDir, runner.Name(), result); err != nil { - logger.Debug("govulncheck: cache write failed (non-fatal)", + logger.Warn("govulncheck: cache write failed (non-fatal)", zap.String("module_root", moduleDir), zap.Error(err)) } @@ -218,13 +219,17 @@ func (a Analyzer) cache() *resultCache { if a.DisableCache { return nil } - return newResultCache(a.CacheDir, a.CacheTTL) + return newResultCache(a.CacheDir, a.CacheTTL, a.logger()) } func (a Analyzer) logger() *zap.Logger { return ensureLogger(a.Logger) } -func resultForRequest() model.AnalyzeResult { - return model.AnalyzeResult{AnalyzerRuns: []string{Name}} +// resultFromRequest returns the legacy-path result: the (in-place +// annotated) request registry plus this analyzer's run marker. Returning +// the registry keeps annotations visible across a managed-plugin process +// boundary, where in-place mutation of req.Registry is not. +func resultFromRequest(req model.AnalyzeRequest) model.AnalyzeResult { + return model.AnalyzeResult{Registry: req.Registry, AnalyzerRuns: []string{Name}} } // vulnerabilitiesForDependency returns the registry vulnerabilities for a @@ -511,3 +516,33 @@ func failureReason(err error) string { return "runner-error" } } + +// finishResult applies the package-updates delta protocol to out. When the +// host accepts deltas (req.AcceptPackageUpdates), the analyzer returns only +// the registry packages it annotated instead of the full registry; the host +// folds them back in with sdk.ApplyPackageUpdates. The annotation this +// analyzer writes -- filling Vulnerability.Reachability on existing +// (Source, ID)-keyed vulnerabilities that had none -- is exactly what the +// host-side merge (Package.MergeFrom) expresses, so the delta path is +// equivalent to the legacy in-place path. The one in-place behavior the merge +// cannot express is replacing a Reachability annotation already written by a +// DIFFERENT analyzer; built-in analyzer dispatch is language-disjoint, so no +// two built-ins annotate the same package. +func finishResult(req model.AnalyzeRequest, out model.AnalyzeResult) model.AnalyzeResult { + if !req.AcceptPackageUpdates || req.Registry == nil { + return out + } + out.Registry = nil + for _, pkg := range req.Registry.All() { + if pkg == nil { + continue + } + for _, vuln := range pkg.Vulnerabilities { + if vuln.Reachability != nil && vuln.Reachability.Analyzer == Name { + out.PackageUpdates = append(out.PackageUpdates, pkg) + break + } + } + } + return out +} diff --git a/internal/analyzers/govulncheck/analyzer_test.go b/components/analyzers/govulncheck/analyzer_test.go similarity index 95% rename from internal/analyzers/govulncheck/analyzer_test.go rename to components/analyzers/govulncheck/analyzer_test.go index 7f881da1..46eb6185 100644 --- a/internal/analyzers/govulncheck/analyzer_test.go +++ b/components/analyzers/govulncheck/analyzer_test.go @@ -70,7 +70,7 @@ func TestAnalyzerMarksReachableFromGovulncheckHit(t *testing.T) { vuln := model.Vulnerability{ID: "GO-2024-1", Source: "osv", ParsedSeverity: "high"} g, registry := newGoGraph(moduleDir, vuln) - a := Analyzer{Runner: &fakeRunner{ + a := Analyzer{DisableCache: true, Runner: &fakeRunner{ result: RunnerResult{ Findings: map[string]Finding{ "GO-2024-1": { @@ -119,7 +119,7 @@ func TestAnalyzerMarksUnreachableWhenImportedButNotCalled(t *testing.T) { vuln := model.Vulnerability{ID: "GO-2024-2", Source: "osv", ParsedSeverity: "high"} g, registry := newGoGraph(moduleDir, vuln) - a := Analyzer{Runner: &fakeRunner{ + a := Analyzer{DisableCache: true, Runner: &fakeRunner{ result: RunnerResult{ Findings: map[string]Finding{ "GO-2024-2": {OSV: "GO-2024-2", ImportedBy: true, CalledBy: false}, @@ -142,7 +142,7 @@ func TestAnalyzerMarksUnreachableTierPackageWhenModuleNotImported(t *testing.T) g, registry := newGoGraph(moduleDir, vuln) // Runner returns nothing — no findings, no imported modules. - a := Analyzer{Runner: &fakeRunner{result: RunnerResult{}}} + a := Analyzer{DisableCache: true, Runner: &fakeRunner{result: RunnerResult{}}} _, err := a.Analyze(context.Background(), model.AnalyzeRequest{Graph: g, Registry: registry, ProjectPath: moduleDir}) if err != nil { t.Fatal(err) @@ -158,7 +158,7 @@ func TestAnalyzerDegradesToUnknownOnRunnerError(t *testing.T) { vuln := model.Vulnerability{ID: "GO-2024-4", Source: "osv", ParsedSeverity: "high"} g, registry := newGoGraph(moduleDir, vuln) - a := Analyzer{Runner: &fakeRunner{err: errors.New("govulncheck binary not found")}} + a := Analyzer{DisableCache: true, Runner: &fakeRunner{err: errors.New("govulncheck binary not found")}} _, err := a.Analyze(context.Background(), model.AnalyzeRequest{Graph: g, Registry: registry, ProjectPath: moduleDir}) if err != nil { t.Fatalf("Analyze should not error on runner failure: %v", err) @@ -183,7 +183,7 @@ func TestAnalyzerBridgesCVEToGOIDViaAliases(t *testing.T) { } g, registry := newGoGraph(moduleDir, vuln) - a := Analyzer{Runner: &fakeRunner{ + a := Analyzer{DisableCache: true, Runner: &fakeRunner{ result: RunnerResult{ Findings: map[string]Finding{ "GO-2024-5": { diff --git a/internal/analyzers/govulncheck/cache.go b/components/analyzers/govulncheck/cache.go similarity index 84% rename from internal/analyzers/govulncheck/cache.go rename to components/analyzers/govulncheck/cache.go index 4e05a4c2..0fa76ffa 100644 --- a/internal/analyzers/govulncheck/cache.go +++ b/components/analyzers/govulncheck/cache.go @@ -11,6 +11,7 @@ import ( "time" cachepkg "github.com/bomly-dev/bomly-sdk/filecache" + "go.uber.org/zap" "github.com/bomly-dev/bomly-sdk/system" ) @@ -47,35 +48,44 @@ type cachedRunnerResult struct { // newResultCache constructs a result cache rooted at dir. If dir is // empty, the OS user cache directory is used. Errors creating the cache -// directory are non-fatal — they return a nil resultCache that the caller -// can use without checks. -func newResultCache(dir string, ttl time.Duration) *resultCache { +// directory are non-fatal — they log one WARN and return a nil +// resultCache that the caller can use without checks. +func newResultCache(dir string, ttl time.Duration, logger *zap.Logger) *resultCache { + logger = ensureLogger(logger) if ttl <= 0 { ttl = defaultCacheTTL } root := dir if root == "" { - root = defaultCacheRoot() - } - if root == "" { - return nil + defaultRoot, err := defaultCacheRoot() + if err != nil { + logger.Warn("govulncheck: result cache disabled: user cache directory unavailable (non-fatal)", + zap.Error(err)) + return nil + } + root = defaultRoot } store, err := cachepkg.NewFileCache(root, ttl) if err != nil { + logger.Warn("govulncheck: result cache disabled: cache initialization failed (non-fatal)", + zap.String("dir", root), zap.Error(err)) return nil } return &resultCache{store: store} } // defaultCacheRoot returns the platform-appropriate cache directory for -// govulncheck analyzer results, or "" if the user cache directory cannot -// be determined. -func defaultCacheRoot() string { +// govulncheck analyzer results, or an error when the user cache +// directory cannot be determined. +func defaultCacheRoot() (string, error) { base, err := os.UserCacheDir() - if err != nil || base == "" { - return "" + if err != nil { + return "", err + } + if base == "" { + return "", errors.New("user cache directory is empty") } - return filepath.Join(base, "bomly", "analyzers", "govulncheck") + return filepath.Join(base, "bomly", "analyzers", "govulncheck"), nil } // keyFor builds a stable cache key for one module run. The key folds diff --git a/internal/analyzers/govulncheck/cache_test.go b/components/analyzers/govulncheck/cache_test.go similarity index 86% rename from internal/analyzers/govulncheck/cache_test.go rename to components/analyzers/govulncheck/cache_test.go index 79151531..8c99e008 100644 --- a/internal/analyzers/govulncheck/cache_test.go +++ b/components/analyzers/govulncheck/cache_test.go @@ -7,11 +7,13 @@ import ( "testing" model "github.com/bomly-dev/bomly-sdk" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" ) func TestResultCacheRoundTrip(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) if cache == nil { t.Fatal("newResultCache returned nil for a writable dir") } @@ -40,7 +42,7 @@ func TestResultCacheRoundTrip(t *testing.T) { func TestResultCacheIsolatesByRunnerName(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) moduleDir := newGoModuleDir(t) if err := cache.set(moduleDir, "builtin", RunnerResult{Findings: map[string]Finding{"A": {OSV: "A"}}}); err != nil { @@ -53,7 +55,7 @@ func TestResultCacheIsolatesByRunnerName(t *testing.T) { func TestResultCacheInvalidatesOnGoSumChange(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) moduleDir := newGoModuleDir(t) // Seed go.sum so checksum is stable across writes. @@ -145,3 +147,18 @@ func TestAnalyzerDisableCacheAlwaysRunsRunner(t *testing.T) { t.Errorf("DisableCache should re-run runner per call; got %d calls", runner.called) } } + +func TestNewResultCacheWarnsWhenInitFails(t *testing.T) { + core, logs := observer.New(zap.WarnLevel) + blocker := filepath.Join(t.TempDir(), "blocker") + if err := os.WriteFile(blocker, []byte("not a directory"), 0o600); err != nil { + t.Fatalf("write blocker file: %v", err) + } + cache := newResultCache(filepath.Join(blocker, "nested"), 0, zap.New(core)) + if cache != nil { + t.Fatal("expected nil cache when the cache root cannot be created") + } + if got := logs.FilterLevelExact(zap.WarnLevel).Len(); got != 1 { + t.Fatalf("expected exactly one WARN log, got %d: %v", got, logs.All()) + } +} diff --git a/internal/analyzers/govulncheck/discover.go b/components/analyzers/govulncheck/discover.go similarity index 100% rename from internal/analyzers/govulncheck/discover.go rename to components/analyzers/govulncheck/discover.go diff --git a/components/analyzers/govulncheck/go.mod b/components/analyzers/govulncheck/go.mod new file mode 100644 index 00000000..44d6ff86 --- /dev/null +++ b/components/analyzers/govulncheck/go.mod @@ -0,0 +1,32 @@ +module github.com/bomly-dev/bomly-cli/components/analyzers/govulncheck + +go 1.26.3 + +require ( + github.com/bomly-dev/bomly-sdk v0.3.0 + go.uber.org/zap v1.28.0 + golang.org/x/vuln v1.6.0 +) + +require ( + github.com/anchore/packageurl-go v0.2.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/oklog/run v1.1.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect + golang.org/x/text v0.40.0 // indirect + golang.org/x/tools v0.48.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/components/analyzers/govulncheck/go.sum b/components/analyzers/govulncheck/go.sum new file mode 100644 index 00000000..0c9a068d --- /dev/null +++ b/components/analyzers/govulncheck/go.sum @@ -0,0 +1,103 @@ +github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= +github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= +github.com/bomly-dev/bomly-sdk v0.3.0 h1:JtC7qZ9yq3r4fUYyq7e/Os4f9wGMa4Qot8U0l9MepFA= +github.com/bomly-dev/bomly-sdk v0.3.0/go.mod h1:yn1LBkoHG9gDBXKyRj0UNJo0BlXl8Bj9Ymb3WKLIh78= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A= +golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +golang.org/x/vuln v1.6.0 h1:FeMO9Rm/HwyduOztbvKcOw+zvDEPr4I4aQNSfevFcKY= +golang.org/x/vuln v1.6.0/go.mod h1:bWlG2493/sjR7ksvicBgMrznH3eYQEyK8ifUYBrqUbg= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/components/analyzers/govulncheck/module.go b/components/analyzers/govulncheck/module.go new file mode 100644 index 00000000..e3aeed00 --- /dev/null +++ b/components/analyzers/govulncheck/module.go @@ -0,0 +1,19 @@ +package govulncheck + +import ( + "context" + + sdk "github.com/bomly-dev/bomly-sdk" +) + +// Module returns the govulncheck analyzer as an execution-neutral sdk.Module. +// The Bomly CLI composition embeds it directly; the same value could back a +// managed plugin binary via sdk.ServeModule. +func Module() sdk.Module { + return sdk.Module{Kind: sdk.PluginKindAnalyzer, Analyzer: &sdk.AnalyzerModule{ + Descriptor: Analyzer{}.Descriptor(), + New: func(_ context.Context, host sdk.HostContext) (sdk.Analyzer, error) { + return Analyzer{Logger: host.Logger()}, nil + }, + }} +} diff --git a/components/analyzers/govulncheck/module_test.go b/components/analyzers/govulncheck/module_test.go new file mode 100644 index 00000000..a2475b70 --- /dev/null +++ b/components/analyzers/govulncheck/module_test.go @@ -0,0 +1,96 @@ +package govulncheck + +import ( + "context" + "reflect" + "testing" + + model "github.com/bomly-dev/bomly-sdk" + "github.com/bomly-dev/bomly-sdk/conformance" +) + +// TestConformance runs the SDK conformance suite against the module. No +// manifest is supplied: the analyzer ships embedded in the CLI, not as a +// packaged plugin. +func TestConformance(t *testing.T) { + conformance.Test(t, conformance.Config{Module: Module()}) +} + +// TestModuleDescriptorMatchesAnalyzer pins the module descriptor to the +// analyzer's own Descriptor so the two can never drift. +func TestModuleDescriptorMatchesAnalyzer(t *testing.T) { + if !reflect.DeepEqual(Module().Analyzer.Descriptor, Analyzer{}.Descriptor()) { + t.Fatal("module descriptor differs from Analyzer{}.Descriptor()") + } +} + +// clearAnalyzedAt blanks the wall-clock annotation timestamps so two runs of +// the same analysis compare equal. +func clearAnalyzedAt(reg *model.PackageRegistry) { + for _, pkg := range reg.All() { + for i := range pkg.Vulnerabilities { + if r := pkg.Vulnerabilities[i].Reachability; r != nil { + r.AnalyzedAt = "" + } + } + } +} + +// TestPackageUpdatesEquivalence verifies the package-updates delta protocol: +// applying the returned PackageUpdates onto a pristine copy of the input +// registry yields exactly the registry the legacy in-place path produces. +func TestPackageUpdatesEquivalence(t *testing.T) { + moduleDir := newGoModuleDir(t) + vuln := model.Vulnerability{ID: "GO-2024-1", Source: "osv", ParsedSeverity: "high"} + runnerResult := RunnerResult{ + Findings: map[string]Finding{ + "GO-2024-1": { + OSV: "GO-2024-1", + CalledBy: true, + ImportedBy: true, + Symbols: []model.AffectedSymbol{{Symbol: "Decode", Package: "example.com/lib"}}, + }, + }, + } + + legacyGraph, legacyReg := newGoGraph(moduleDir, vuln) + legacy := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + legacyRes, err := legacy.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: legacyGraph, Registry: legacyReg, ProjectPath: moduleDir, + }) + if err != nil { + t.Fatalf("legacy Analyze err: %v", err) + } + if len(legacyRes.PackageUpdates) != 0 { + t.Fatalf("legacy path returned %d package updates, want 0", len(legacyRes.PackageUpdates)) + } + if legacyRes.Registry != legacyReg { + t.Fatalf("legacy path must return the annotated request registry (got %p, want %p): plugin-boundary hosts cannot see in-place mutation", legacyRes.Registry, legacyReg) + } + + deltaGraph, deltaReg := newGoGraph(moduleDir, vuln) + delta := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + deltaRes, err := delta.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: deltaGraph, Registry: deltaReg, ProjectPath: moduleDir, + AcceptPackageUpdates: true, + }) + if err != nil { + t.Fatalf("delta Analyze err: %v", err) + } + if deltaRes.Registry != nil { + t.Fatal("delta path returned a full registry; want PackageUpdates only") + } + if len(deltaRes.PackageUpdates) == 0 { + t.Fatal("delta path returned no package updates") + } + + _, pristineReg := newGoGraph(moduleDir, vuln) + merged := model.ApplyPackageUpdates(pristineReg, deltaRes.PackageUpdates) + + clearAnalyzedAt(legacyReg) + clearAnalyzedAt(merged) + if !reflect.DeepEqual(legacyReg.All(), merged.All()) { + t.Fatalf("delta-applied registry differs from legacy registry:\nlegacy: %+v\nmerged: %+v", + legacyReg.All(), merged.All()) + } +} diff --git a/internal/analyzers/govulncheck/parse.go b/components/analyzers/govulncheck/parse.go similarity index 73% rename from internal/analyzers/govulncheck/parse.go rename to components/analyzers/govulncheck/parse.go index 062fc65d..d47f44f2 100644 --- a/internal/analyzers/govulncheck/parse.go +++ b/components/analyzers/govulncheck/parse.go @@ -3,6 +3,7 @@ package govulncheck import ( "bufio" "encoding/json" + "fmt" "strings" model "github.com/bomly-dev/bomly-sdk" @@ -90,7 +91,7 @@ func parseGovulncheckJSON(data []byte) (RunnerResult, error) { mergeFinding(result.Findings, result.ImportedModules, *env.Finding) } if err := scanner.Err(); err != nil { - return RunnerResult{}, err + return RunnerResult{}, fmt.Errorf("scan govulncheck JSON stream: %w", err) } for id, f := range result.Findings { @@ -114,11 +115,24 @@ func mergeFinding(into map[string]Finding, modules map[string]struct{}, src find return } - // govulncheck trace order: index 0 is the entry frame (e.g. main.main); - // the last frame is the call site of the vulnerable symbol. + // govulncheck trace order (x/vuln internal/govulncheck.Finding.Trace): + // index 0 is the imported vulnerable symbol (the sink) and the last + // frame is the entry point. Module-level findings carry a single frame + // with only a module; package-level findings a single frame with module + // and package but no symbol. + sink := src.Trace[0] + if sink.Module != "" { + current.Modules = appendUnique(current.Modules, sink.Module) + } + // Record imported modules only from frames that name a package: a + // module-level frame proves the module is required, not that any of + // its packages is imported. + // The SDK's CallPath contract is entry point → sink (Frames[0] is the + // entry point), the reverse of govulncheck's trace order. frames := make([]model.CallFrame, 0, len(src.Trace)) - for _, t := range src.Trace { - if t.Module != "" { + for i := len(src.Trace) - 1; i >= 0; i-- { + t := src.Trace[i] + if t.Module != "" && t.Package != "" { modules[t.Module] = struct{}{} } frames = append(frames, model.CallFrame{ @@ -128,20 +142,28 @@ func mergeFinding(into map[string]Finding, modules map[string]struct{}, src find Position: positionToSDK(t.Position), }) } - last := src.Trace[len(src.Trace)-1] - current.CalledBy = true - current.ImportedBy = true - if last.Module != "" { - current.Modules = appendUnique(current.Modules, last.Module) - } - sym := model.AffectedSymbol{ - Symbol: last.Function, - Kind: symbolKind(last), - Package: last.Package, - Module: last.Module, + switch { + case sink.Function != "": + // Symbol-level finding: a call path into the vulnerable symbol. + current.CalledBy = true + current.ImportedBy = true + sym := model.AffectedSymbol{ + Symbol: sink.Function, + Kind: symbolKind(sink), + Package: sink.Package, + Module: sink.Module, + } + current.Symbols = appendUniqueSymbol(current.Symbols, sym) + current.CallPaths = append(current.CallPaths, model.CallPath{Sink: sym, Frames: frames}) + case sink.Package != "": + // Package-level finding: the vulnerable package is imported but + // no call into a vulnerable symbol was found. + current.ImportedBy = true + default: + // Module-level finding: the vulnerable module is required but the + // vulnerable package is not imported. Record the module (above) + // and nothing else. } - current.Symbols = appendUniqueSymbol(current.Symbols, sym) - current.CallPaths = append(current.CallPaths, model.CallPath{Sink: sym, Frames: frames}) into[src.OSV] = current } diff --git a/internal/analyzers/govulncheck/parse_fuzz_test.go b/components/analyzers/govulncheck/parse_fuzz_test.go similarity index 100% rename from internal/analyzers/govulncheck/parse_fuzz_test.go rename to components/analyzers/govulncheck/parse_fuzz_test.go diff --git a/internal/analyzers/govulncheck/parse_test.go b/components/analyzers/govulncheck/parse_test.go similarity index 81% rename from internal/analyzers/govulncheck/parse_test.go rename to components/analyzers/govulncheck/parse_test.go index 8d18884f..b73f025d 100644 --- a/internal/analyzers/govulncheck/parse_test.go +++ b/components/analyzers/govulncheck/parse_test.go @@ -10,8 +10,8 @@ const sampleGovulncheckJSON = ` {"config":{"protocol_version":"v1.0.0"}} {"progress":{"message":"loaded packages"}} {"osv":{"id":"GO-2024-1234","aliases":["CVE-2024-1234","GHSA-aaaa-bbbb-cccc"],"summary":"oops"}} -{"finding":{"osv":"GO-2024-1234","fixed_version":"v1.2.3","trace":[{"module":"example.com/app","package":"main","function":"main","position":{"filename":"main.go","line":12,"column":4}},{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","position":{"filename":"decode.go","line":99}}]}} -{"finding":{"osv":"GO-2024-1234","trace":[{"module":"example.com/app","package":"main","function":"handler","position":{"filename":"handler.go","line":7}},{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","position":{"filename":"decode.go","line":99}}]}} +{"finding":{"osv":"GO-2024-1234","fixed_version":"v1.2.3","trace":[{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","position":{"filename":"decode.go","line":99}},{"module":"example.com/app","package":"main","function":"main","position":{"filename":"main.go","line":12,"column":4}}]}} +{"finding":{"osv":"GO-2024-1234","trace":[{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","position":{"filename":"decode.go","line":99}},{"module":"example.com/app","package":"main","function":"handler","position":{"filename":"handler.go","line":7}}]}} {"finding":{"osv":"GO-2024-9999"}} ` diff --git a/internal/analyzers/govulncheck/runner.go b/components/analyzers/govulncheck/runner.go similarity index 100% rename from internal/analyzers/govulncheck/runner.go rename to components/analyzers/govulncheck/runner.go diff --git a/internal/analyzers/govulncheck/runner_library.go b/components/analyzers/govulncheck/runner_library.go similarity index 81% rename from internal/analyzers/govulncheck/runner_library.go rename to components/analyzers/govulncheck/runner_library.go index 4878cca7..7ec4f8d0 100644 --- a/internal/analyzers/govulncheck/runner_library.go +++ b/components/analyzers/govulncheck/runner_library.go @@ -78,20 +78,25 @@ func (r libraryRunner) Run(ctx context.Context, moduleDir string) (RunnerResult, return parseGovulncheckJSON(stdout.Bytes()) } -// isVulnsFound reports whether the wrapped error is the -// "vulnerabilities found" sentinel govulncheck returns when it discovers -// at least one finding. The error message is the canonical signal; the -// library uses an unexported type so we match on text. +// isVulnsFound reports whether err is the "vulnerabilities found" +// sentinel govulncheck returns when it discovers at least one finding. +// scan.Cmd.Wait documents that its error wraps an error implementing +// ExitCode() int; exit code 3 is "vulnerabilities found". The message +// comparisons walk the unwrap chain as a fallback for runners that +// surface the plain exec message instead. func isVulnsFound(err error) bool { if err == nil { return false } - type sentinel interface{ Error() string } - if typed, ok := errors.AsType[sentinel](err); ok { - msg := typed.Error() - // govulncheck's "exit code 3" surfaces here as either - // "exit status 3" (when shelling out to the toolchain) or as - // the in-process equivalent the library prints. + type exitCoder interface { + error + ExitCode() int + } + if coder, ok := errors.AsType[exitCoder](err); ok && coder.ExitCode() == 3 { + return true + } + for unwrapped := err; unwrapped != nil; unwrapped = errors.Unwrap(unwrapped) { + msg := unwrapped.Error() if msg == "exit status 3" || msg == "vulnerabilities found" { return true } diff --git a/components/analyzers/govulncheck/runner_library_test.go b/components/analyzers/govulncheck/runner_library_test.go new file mode 100644 index 00000000..a95db01c --- /dev/null +++ b/components/analyzers/govulncheck/runner_library_test.go @@ -0,0 +1,39 @@ +package govulncheck + +import ( + "errors" + "fmt" + "testing" +) + +// exitCode3Error mimics x/vuln's unexported exitCodeError: an error +// carrying ExitCode() int that scan.Cmd.Wait wraps. +type exitCode3Error struct{ code int } + +func (e exitCode3Error) Error() string { return fmt.Sprintf("govulncheck exit code %d", e.code) } + +func (e exitCode3Error) ExitCode() int { return e.code } + +func TestIsVulnsFound(t *testing.T) { + cases := []struct { + name string + err error + want bool + }{ + {name: "nil", err: nil, want: false}, + {name: "bare exit status 3", err: errors.New("exit status 3"), want: true}, + {name: "unrelated", err: errors.New("go: no such tool"), want: false}, + {name: "exit code 3 sentinel", err: exitCode3Error{code: 3}, want: true}, + {name: "wrapped exit code 3", err: fmt.Errorf("govulncheck: %w", exitCode3Error{code: 3}), want: true}, + {name: "doubly wrapped exit code 3", err: fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", exitCode3Error{code: 3})), want: true}, + {name: "wrapped exit code 1", err: fmt.Errorf("govulncheck: %w", exitCode3Error{code: 1}), want: false}, + {name: "wrapped exec message", err: fmt.Errorf("govulncheck: %w", errors.New("exit status 3")), want: true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := isVulnsFound(tc.err); got != tc.want { + t.Errorf("isVulnsFound(%v) = %v, want %v", tc.err, got, tc.want) + } + }) + } +} diff --git a/components/analyzers/govulncheck/testdata/govulncheck.json b/components/analyzers/govulncheck/testdata/govulncheck.json new file mode 100644 index 00000000..9ea99933 --- /dev/null +++ b/components/analyzers/govulncheck/testdata/govulncheck.json @@ -0,0 +1,7 @@ +{"config":{"protocol_version":"v1.0.0"}} +{"progress":{"message":"loaded packages"}} +{"osv":{"id":"GO-2024-1234","aliases":["CVE-2024-1234","GHSA-aaaa-bbbb-cccc"],"summary":"fixture advisory"}} +{"finding":{"osv":"GO-2024-1234","fixed_version":"v1.2.3","trace":[{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","receiver":"*Decoder","position":{"filename":"decode.go","line":99}},{"module":"example.com/app","package":"main","function":"main","position":{"filename":"main.go","line":12,"column":4}}]}} +{malformed fixture record} +{"finding":{"osv":"GO-2024-1234","trace":[{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","receiver":"*Decoder","position":{"filename":"decode.go","line":99}},{"module":"example.com/app","package":"main","function":"handler","position":{"filename":"handler.go","line":7}}]}} +{"finding":{"osv":"GO-2024-9999"}} diff --git a/internal/analyzers/govulncheck/testdata/module/go.mod b/components/analyzers/govulncheck/testdata/module/go.mod similarity index 100% rename from internal/analyzers/govulncheck/testdata/module/go.mod rename to components/analyzers/govulncheck/testdata/module/go.mod diff --git a/internal/analyzers/govulncheck/testdata/module/main.go b/components/analyzers/govulncheck/testdata/module/main.go similarity index 100% rename from internal/analyzers/govulncheck/testdata/module/main.go rename to components/analyzers/govulncheck/testdata/module/main.go diff --git a/internal/analyzers/govulncheck/testdata/module/nested/file.go b/components/analyzers/govulncheck/testdata/module/nested/file.go similarity index 100% rename from internal/analyzers/govulncheck/testdata/module/nested/file.go rename to components/analyzers/govulncheck/testdata/module/nested/file.go diff --git a/internal/analyzers/govulncheck/testdata_test.go b/components/analyzers/govulncheck/testdata_test.go similarity index 100% rename from internal/analyzers/govulncheck/testdata_test.go rename to components/analyzers/govulncheck/testdata_test.go diff --git a/internal/analyzers/jsreach/analyzer.go b/components/analyzers/jsreach/analyzer.go similarity index 93% rename from internal/analyzers/jsreach/analyzer.go rename to components/analyzers/jsreach/analyzer.go index 41e4ad0e..61ae44c7 100644 --- a/internal/analyzers/jsreach/analyzer.go +++ b/components/analyzers/jsreach/analyzer.go @@ -49,6 +49,7 @@ func (a Analyzer) Descriptor() model.AnalyzerDescriptor { SupportedManagers: []model.PackageManager{model.PackageManagerNPM, model.PackageManagerPNPM, model.PackageManagerYarn}, SupportedLanguages: []model.Language{model.LanguageJavaScript, model.LanguageTypeScript}, SupportedTiers: []model.ReachabilityTier{model.TierPackage}, + Capabilities: []string{model.CapabilityPackageUpdates}, } } @@ -121,7 +122,7 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. if len(hierarchies) == 0 { logger.Info("jsreach: no npm project roots discovered; marking all npm vulnerabilities as unknown") annotateAllUnknown(req, "no-project-root-discovered", time.Now()) - return resultFromRequest(req), nil + return finishResult(req, resultFromRequest(req)), nil } logger.Info("jsreach: starting reachability analysis", @@ -187,7 +188,7 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. out := resultFromRequest(req) out.AnalyzerStats = map[string]model.ReachabilityStats{Name: stats} - return out, nil + return finishResult(req, out), nil } type workspaceClosure struct { @@ -360,7 +361,7 @@ func (a Analyzer) runWithCache( } if cache != nil { if err := cache.set(projectDir, runner.Name(), runner.Version(), result); err != nil { - logger.Debug("jsreach: cache write failed (non-fatal)", + logger.Warn("jsreach: cache write failed (non-fatal)", zap.String("project_root", projectDir), zap.Error(err)) } @@ -375,7 +376,7 @@ func (a Analyzer) cache() *resultCache { if a.DisableCache { return nil } - return newResultCache(a.CacheDir, a.CacheTTL) + return newResultCache(a.CacheDir, a.CacheTTL, a.logger()) } func resultFromRequest(req model.AnalyzeRequest) model.AnalyzeResult { @@ -650,3 +651,33 @@ func failureReason(err error) string { return "runner-error" } } + +// finishResult applies the package-updates delta protocol to out. When the +// host accepts deltas (req.AcceptPackageUpdates), the analyzer returns only +// the registry packages it annotated instead of the full registry; the host +// folds them back in with sdk.ApplyPackageUpdates. The annotation this +// analyzer writes -- filling Vulnerability.Reachability on existing +// (Source, ID)-keyed vulnerabilities that had none -- is exactly what the +// host-side merge (Package.MergeFrom) expresses, so the delta path is +// equivalent to the legacy in-place path. The one in-place behavior the merge +// cannot express is replacing a Reachability annotation already written by a +// DIFFERENT analyzer; built-in analyzer dispatch is language-disjoint, so no +// two built-ins annotate the same package. +func finishResult(req model.AnalyzeRequest, out model.AnalyzeResult) model.AnalyzeResult { + if !req.AcceptPackageUpdates || req.Registry == nil { + return out + } + out.Registry = nil + for _, pkg := range req.Registry.All() { + if pkg == nil { + continue + } + for _, vuln := range pkg.Vulnerabilities { + if vuln.Reachability != nil && vuln.Reachability.Analyzer == Name { + out.PackageUpdates = append(out.PackageUpdates, pkg) + break + } + } + } + return out +} diff --git a/internal/analyzers/jsreach/analyzer_test.go b/components/analyzers/jsreach/analyzer_test.go similarity index 100% rename from internal/analyzers/jsreach/analyzer_test.go rename to components/analyzers/jsreach/analyzer_test.go diff --git a/internal/analyzers/jsreach/cache.go b/components/analyzers/jsreach/cache.go similarity index 85% rename from internal/analyzers/jsreach/cache.go rename to components/analyzers/jsreach/cache.go index d90f2389..b9fa722e 100644 --- a/internal/analyzers/jsreach/cache.go +++ b/components/analyzers/jsreach/cache.go @@ -10,6 +10,7 @@ import ( "time" cachepkg "github.com/bomly-dev/bomly-sdk/filecache" + "go.uber.org/zap" "github.com/bomly-dev/bomly-sdk/system" ) @@ -48,35 +49,44 @@ type cachedRunnerResult struct { // newResultCache constructs a result cache rooted at dir. If dir is // empty, the OS user cache directory is used. Errors creating the -// cache directory are non-fatal — they return a nil resultCache that -// the caller can use without checks. -func newResultCache(dir string, ttl time.Duration) *resultCache { +// cache directory are non-fatal — they log one WARN and return a nil +// resultCache that the caller can use without checks. +func newResultCache(dir string, ttl time.Duration, logger *zap.Logger) *resultCache { + logger = ensureLogger(logger) if ttl <= 0 { ttl = defaultCacheTTL } root := dir if root == "" { - root = defaultCacheRoot() - } - if root == "" { - return nil + defaultRoot, err := defaultCacheRoot() + if err != nil { + logger.Warn("jsreach: result cache disabled: user cache directory unavailable (non-fatal)", + zap.Error(err)) + return nil + } + root = defaultRoot } store, err := cachepkg.NewFileCache(root, ttl) if err != nil { + logger.Warn("jsreach: result cache disabled: cache initialization failed (non-fatal)", + zap.String("dir", root), zap.Error(err)) return nil } return &resultCache{store: store} } // defaultCacheRoot returns the platform-appropriate cache directory -// for jsreach analyzer results, or "" if the user cache directory -// cannot be determined. -func defaultCacheRoot() string { +// for jsreach analyzer results, or an error when the user cache +// directory cannot be determined. +func defaultCacheRoot() (string, error) { base, err := os.UserCacheDir() - if err != nil || base == "" { - return "" + if err != nil { + return "", err + } + if base == "" { + return "", errors.New("user cache directory is empty") } - return filepath.Join(base, "bomly", "analyzers", "jsreach") + return filepath.Join(base, "bomly", "analyzers", "jsreach"), nil } // keyFor builds a stable cache key for one project pass. Folds every diff --git a/internal/analyzers/jsreach/cache_test.go b/components/analyzers/jsreach/cache_test.go similarity index 87% rename from internal/analyzers/jsreach/cache_test.go rename to components/analyzers/jsreach/cache_test.go index 48c34d82..2a83c91b 100644 --- a/internal/analyzers/jsreach/cache_test.go +++ b/components/analyzers/jsreach/cache_test.go @@ -7,11 +7,13 @@ import ( "testing" model "github.com/bomly-dev/bomly-sdk" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" ) func TestResultCacheRoundTrip(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) if cache == nil { t.Fatal("newResultCache returned nil for a writable dir") } @@ -42,7 +44,7 @@ func TestResultCacheRoundTrip(t *testing.T) { func TestResultCacheIsolatesByRunnerName(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) projectDir := newNPMProjectDir(t) if err := cache.set(projectDir, "builtin", "1.0", RunnerResult{ImportedPackages: map[string]struct{}{"a": {}}}); err != nil { @@ -55,7 +57,7 @@ func TestResultCacheIsolatesByRunnerName(t *testing.T) { func TestResultCacheIsolatesByRunnerVersion(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) projectDir := newNPMProjectDir(t) if err := cache.set(projectDir, "builtin", "1.0", RunnerResult{ImportedPackages: map[string]struct{}{"a": {}}}); err != nil { @@ -68,7 +70,7 @@ func TestResultCacheIsolatesByRunnerVersion(t *testing.T) { func TestResultCacheInvalidatesOnLockfileChange(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) projectDir := newNPMProjectDir(t) // Seed package-lock.json so checksum is stable across writes. @@ -159,3 +161,18 @@ func TestAnalyzerDisableCacheAlwaysRunsRunner(t *testing.T) { t.Errorf("DisableCache should re-run runner per call; got %d calls", runner.called) } } + +func TestNewResultCacheWarnsWhenInitFails(t *testing.T) { + core, logs := observer.New(zap.WarnLevel) + blocker := filepath.Join(t.TempDir(), "blocker") + if err := os.WriteFile(blocker, []byte("not a directory"), 0o600); err != nil { + t.Fatalf("write blocker file: %v", err) + } + cache := newResultCache(filepath.Join(blocker, "nested"), 0, zap.New(core)) + if cache != nil { + t.Fatal("expected nil cache when the cache root cannot be created") + } + if got := logs.FilterLevelExact(zap.WarnLevel).Len(); got != 1 { + t.Fatalf("expected exactly one WARN log, got %d: %v", got, logs.All()) + } +} diff --git a/internal/analyzers/jsreach/discover.go b/components/analyzers/jsreach/discover.go similarity index 100% rename from internal/analyzers/jsreach/discover.go rename to components/analyzers/jsreach/discover.go diff --git a/internal/analyzers/jsreach/dynamicimports.go b/components/analyzers/jsreach/dynamicimports.go similarity index 100% rename from internal/analyzers/jsreach/dynamicimports.go rename to components/analyzers/jsreach/dynamicimports.go diff --git a/internal/analyzers/jsreach/entrypoints.go b/components/analyzers/jsreach/entrypoints.go similarity index 90% rename from internal/analyzers/jsreach/entrypoints.go rename to components/analyzers/jsreach/entrypoints.go index 40b8a78d..fce7e5f0 100644 --- a/internal/analyzers/jsreach/entrypoints.go +++ b/components/analyzers/jsreach/entrypoints.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "github.com/bomly-dev/bomly-sdk/system" ) @@ -148,9 +149,17 @@ func binEntryStrings(raw json.RawMessage) []string { } var m map[string]string if err := json.Unmarshal(raw, &m); err == nil { - out := make([]string, 0, len(m)) - for _, value := range m { - if value != "" { + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + // Emit in sorted key order so the entry list (and everything + // derived from it — logs, cache keys, fuzz determinism) is + // stable across runs. + sort.Strings(names) + out := make([]string, 0, len(names)) + for _, name := range names { + if value := m[name]; value != "" { out = append(out, value) } } @@ -175,8 +184,15 @@ func walkJSONStrings(raw json.RawMessage, emit func(string)) { } var asObject map[string]json.RawMessage if err := json.Unmarshal(raw, &asObject); err == nil { - for _, child := range asObject { - walkJSONStrings(child, emit) + // Walk object members in sorted key order so emission order is + // deterministic (Go map iteration is randomized). + keys := make([]string, 0, len(asObject)) + for key := range asObject { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + walkJSONStrings(asObject[key], emit) } } } diff --git a/components/analyzers/jsreach/entrypoints_fuzz_test.go b/components/analyzers/jsreach/entrypoints_fuzz_test.go new file mode 100644 index 00000000..248a7adb --- /dev/null +++ b/components/analyzers/jsreach/entrypoints_fuzz_test.go @@ -0,0 +1,50 @@ +package jsreach + +import ( + "encoding/json" + "reflect" + "testing" + + testutil "github.com/bomly-dev/bomly-sdk/testkit" +) + +// FuzzEntryPointStrings verifies that the package.json entry-point +// helpers never panic and produce deterministic output for arbitrary +// (valid, malformed, or truncated) JSON input within the shared fuzz +// input bound. The helpers tolerate any shape by design, so every +// input is expected to succeed; determinism is the real contract — +// walkJSONStrings and binEntryStrings walk JSON objects, and emission +// order must not depend on Go's randomized map iteration. +func FuzzEntryPointStrings(f *testing.F) { + for _, seed := range []string{ + `"./index.js"`, + `{"my-cli": "./cli.js", "other": "./other.js"}`, + `{".": {"import": "./esm/index.js", "require": "./cjs/index.js"}, "./util": "./util.js"}`, + `["./a.js", {"b": "./b.js"}, ["./c.js"]]`, + `{"browser": {"./fs": false}}`, + `{"unterminated": "./x.js"`, + `null`, + `42`, + ``, + } { + f.Add([]byte(seed)) + } + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > testutil.MaxFuzzInputSize { + return + } + raw := json.RawMessage(data) + helpers := map[string]func(json.RawMessage) []string{ + "browserEntryStrings": browserEntryStrings, + "exportsEntryStrings": exportsEntryStrings, + "binEntryStrings": binEntryStrings, + } + for name, helper := range helpers { + first := helper(raw) + second := helper(raw) + if !reflect.DeepEqual(first, second) { + t.Fatalf("%s changed result for identical input: first=%v second=%v", name, first, second) + } + } + }) +} diff --git a/internal/analyzers/jsreach/entrypoints_test.go b/components/analyzers/jsreach/entrypoints_test.go similarity index 100% rename from internal/analyzers/jsreach/entrypoints_test.go rename to components/analyzers/jsreach/entrypoints_test.go diff --git a/components/analyzers/jsreach/go.mod b/components/analyzers/jsreach/go.mod new file mode 100644 index 00000000..69c5d294 --- /dev/null +++ b/components/analyzers/jsreach/go.mod @@ -0,0 +1,29 @@ +module github.com/bomly-dev/bomly-cli/components/analyzers/jsreach + +go 1.26.3 + +require ( + github.com/bomly-dev/bomly-sdk v0.3.0 + github.com/evanw/esbuild v0.28.1 + go.uber.org/zap v1.28.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/anchore/packageurl-go v0.2.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/oklog/run v1.1.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/components/analyzers/jsreach/go.sum b/components/analyzers/jsreach/go.sum new file mode 100644 index 00000000..d60c0c54 --- /dev/null +++ b/components/analyzers/jsreach/go.sum @@ -0,0 +1,93 @@ +github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= +github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= +github.com/bomly-dev/bomly-sdk v0.3.0 h1:JtC7qZ9yq3r4fUYyq7e/Os4f9wGMa4Qot8U0l9MepFA= +github.com/bomly-dev/bomly-sdk v0.3.0/go.mod h1:yn1LBkoHG9gDBXKyRj0UNJo0BlXl8Bj9Ymb3WKLIh78= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc= +github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/analyzers/jsreach/importgraph.go b/components/analyzers/jsreach/importgraph.go similarity index 100% rename from internal/analyzers/jsreach/importgraph.go rename to components/analyzers/jsreach/importgraph.go diff --git a/internal/analyzers/jsreach/importgraph_fuzz_test.go b/components/analyzers/jsreach/importgraph_fuzz_test.go similarity index 100% rename from internal/analyzers/jsreach/importgraph_fuzz_test.go rename to components/analyzers/jsreach/importgraph_fuzz_test.go diff --git a/internal/analyzers/jsreach/importgraph_test.go b/components/analyzers/jsreach/importgraph_test.go similarity index 100% rename from internal/analyzers/jsreach/importgraph_test.go rename to components/analyzers/jsreach/importgraph_test.go diff --git a/components/analyzers/jsreach/module.go b/components/analyzers/jsreach/module.go new file mode 100644 index 00000000..de0d5b15 --- /dev/null +++ b/components/analyzers/jsreach/module.go @@ -0,0 +1,19 @@ +package jsreach + +import ( + "context" + + sdk "github.com/bomly-dev/bomly-sdk" +) + +// Module returns the jsreach analyzer as an execution-neutral sdk.Module. +// The Bomly CLI composition embeds it directly; the same value could back a +// managed plugin binary via sdk.ServeModule. +func Module() sdk.Module { + return sdk.Module{Kind: sdk.PluginKindAnalyzer, Analyzer: &sdk.AnalyzerModule{ + Descriptor: Analyzer{}.Descriptor(), + New: func(_ context.Context, host sdk.HostContext) (sdk.Analyzer, error) { + return Analyzer{Logger: host.Logger()}, nil + }, + }} +} diff --git a/components/analyzers/jsreach/module_test.go b/components/analyzers/jsreach/module_test.go new file mode 100644 index 00000000..6ff9dfef --- /dev/null +++ b/components/analyzers/jsreach/module_test.go @@ -0,0 +1,98 @@ +package jsreach + +import ( + "context" + "path/filepath" + "reflect" + "testing" + + model "github.com/bomly-dev/bomly-sdk" + "github.com/bomly-dev/bomly-sdk/conformance" +) + +// TestConformance runs the SDK conformance suite against the module. No +// manifest is supplied: the analyzer ships embedded in the CLI, not as a +// packaged plugin. +func TestConformance(t *testing.T) { + conformance.Test(t, conformance.Config{Module: Module()}) +} + +// TestModuleDescriptorMatchesAnalyzer pins the module descriptor to the +// analyzer's own Descriptor so the two can never drift. +func TestModuleDescriptorMatchesAnalyzer(t *testing.T) { + if !reflect.DeepEqual(Module().Analyzer.Descriptor, Analyzer{}.Descriptor()) { + t.Fatal("module descriptor differs from Analyzer{}.Descriptor()") + } +} + +// clearAnalyzedAt blanks the wall-clock annotation timestamps so two runs of +// the same analysis compare equal. +func clearAnalyzedAt(reg *model.PackageRegistry) { + for _, pkg := range reg.All() { + for i := range pkg.Vulnerabilities { + if r := pkg.Vulnerabilities[i].Reachability; r != nil { + r.AnalyzedAt = "" + } + } + } +} + +// TestPackageUpdatesEquivalence verifies the package-updates delta protocol: +// applying the returned PackageUpdates onto a pristine copy of the input +// registry yields exactly the registry the legacy in-place path produces. +func TestPackageUpdatesEquivalence(t *testing.T) { + projectDir := newNPMProjectDir(t) + vuln := model.Vulnerability{ID: "GHSA-test", Source: "osv", ParsedSeverity: "high"} + runnerResult := RunnerResult{ + ImportedPackages: map[string]struct{}{"lodash": {}}, + EntryPoints: []string{filepath.Join(projectDir, "index.js")}, + SourceFiles: 1, + } + seed := func() (*model.Graph, *model.PackageRegistry) { + g, reg := newSeed() + addNPMDep(t, g, reg, projectDir, "", "lodash", "1.0.0", vuln) + addNPMDep(t, g, reg, projectDir, "", "left-pad", "1.0.0", vuln) + return g, reg + } + + legacyGraph, legacyReg := seed() + legacy := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + legacyRes, err := legacy.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: legacyGraph, Registry: legacyReg, ProjectPath: projectDir, + }) + if err != nil { + t.Fatalf("legacy Analyze err: %v", err) + } + if len(legacyRes.PackageUpdates) != 0 { + t.Fatalf("legacy path returned %d package updates, want 0", len(legacyRes.PackageUpdates)) + } + if legacyRes.Registry != legacyReg { + t.Fatalf("legacy path must return the annotated request registry (got %p, want %p): plugin-boundary hosts cannot see in-place mutation", legacyRes.Registry, legacyReg) + } + + deltaGraph, deltaReg := seed() + delta := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + deltaRes, err := delta.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: deltaGraph, Registry: deltaReg, ProjectPath: projectDir, + AcceptPackageUpdates: true, + }) + if err != nil { + t.Fatalf("delta Analyze err: %v", err) + } + if deltaRes.Registry != nil { + t.Fatal("delta path returned a full registry; want PackageUpdates only") + } + if len(deltaRes.PackageUpdates) == 0 { + t.Fatal("delta path returned no package updates") + } + + _, pristineReg := seed() + merged := model.ApplyPackageUpdates(pristineReg, deltaRes.PackageUpdates) + + clearAnalyzedAt(legacyReg) + clearAnalyzedAt(merged) + if !reflect.DeepEqual(legacyReg.All(), merged.All()) { + t.Fatalf("delta-applied registry differs from legacy registry:\nlegacy: %+v\nmerged: %+v", + legacyReg.All(), merged.All()) + } +} diff --git a/internal/analyzers/jsreach/runner.go b/components/analyzers/jsreach/runner.go similarity index 100% rename from internal/analyzers/jsreach/runner.go rename to components/analyzers/jsreach/runner.go diff --git a/internal/analyzers/jsreach/runner_library.go b/components/analyzers/jsreach/runner_library.go similarity index 79% rename from internal/analyzers/jsreach/runner_library.go rename to components/analyzers/jsreach/runner_library.go index 7f734a8a..f5ab1d65 100644 --- a/internal/analyzers/jsreach/runner_library.go +++ b/components/analyzers/jsreach/runner_library.go @@ -5,6 +5,7 @@ import ( "fmt" "path/filepath" "runtime/debug" + "time" "github.com/evanw/esbuild/pkg/api" "go.uber.org/zap" @@ -109,25 +110,53 @@ func (r libraryRunner) Run(ctx context.Context, projectDir string) (RunnerResult }, } - // Honor cancellation by surfacing it as a runner error. esbuild - // itself doesn't take a context; we check before/after so a - // long-running pass still cancels at boundary. + // Honor cancellation mid-build. esbuild's Build call doesn't take + // a context, but its incremental Context API exposes Cancel(), so + // we run one Rebuild on a goroutine and cancel it when ctx is + // done. Dispose always runs so the context's service goroutines + // don't leak. if err := ctx.Err(); err != nil { return RunnerResult{}, err } - result := api.Build(options) + buildCtx, ctxErr := api.Context(options) + if ctxErr != nil { + return RunnerResult{}, fmt.Errorf("esbuild context: %s", summarizeMessages(ctxErr.Errors, 3)) + } + defer buildCtx.Dispose() + + resultCh := make(chan api.BuildResult, 1) + go func() { resultCh <- buildCtx.Rebuild() }() + + var result api.BuildResult + select { + case result = <-resultCh: + case <-ctx.Done(): + // Cancel only cuts short a build that is already in flight; if + // the goroutine above hasn't started Rebuild's build yet the + // call is a no-op, so retry until Rebuild returns. A canceled + // build finishes promptly with a "The build was canceled" + // error, which we fold into the cancellation error here. + for { + buildCtx.Cancel() + select { + case <-resultCh: + return RunnerResult{}, ctx.Err() + case <-time.After(10 * time.Millisecond): + } + } + } if err := ctx.Err(); err != nil { return RunnerResult{}, err } if len(result.Errors) > 0 { // esbuild errors usually mean syntactically broken sources or - // genuinely missing files. We log them at debug and keep - // going; whatever metafile we got back is still useful for a - // best-effort import set. + // genuinely missing files. We warn and keep going; whatever + // metafile we got back is still useful for a best-effort + // import set. preview := summarizeMessages(result.Errors, 3) - r.logger.Debug("jsreach: esbuild reported errors (continuing on best-effort)", + r.logger.Warn("jsreach: esbuild reported errors (continuing on best-effort)", zap.String("project_dir", projectDir), zap.Int("error_count", len(result.Errors)), zap.String("preview", preview)) diff --git a/internal/analyzers/jsreach/runner_testdata_test.go b/components/analyzers/jsreach/runner_testdata_test.go similarity index 79% rename from internal/analyzers/jsreach/runner_testdata_test.go rename to components/analyzers/jsreach/runner_testdata_test.go index cf5afae9..3e97731e 100644 --- a/internal/analyzers/jsreach/runner_testdata_test.go +++ b/components/analyzers/jsreach/runner_testdata_test.go @@ -53,6 +53,30 @@ func TestLibraryRunnerWalksJSTestdata(t *testing.T) { } } +func TestLibraryRunnerHonorsCancelledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := NewRunner(nil).Run(ctx, jsProjectFixture("entrypoints")) + if !errors.Is(err, context.Canceled) { + t.Fatalf("err = %v, want context.Canceled", err) + } +} + +func TestLibraryRunnerCancelRacingBuild(t *testing.T) { + // Cancel concurrently with Run so the cancellation lands anywhere + // between context creation and build completion — including before + // Rebuild has started an active build, the window where a single + // Cancel call would be a no-op. Either outcome is valid: the build + // finished first (nil error) or cancellation won (context.Canceled). + // The invariant is that Run returns and never reports anything else. + ctx, cancel := context.WithCancel(context.Background()) + go cancel() + _, err := NewRunner(nil).Run(ctx, jsProjectFixture("entrypoints")) + if err != nil && !errors.Is(err, context.Canceled) { + t.Fatalf("err = %v, want nil or context.Canceled", err) + } +} + func TestJSDynamicImportDetectionFromTestdata(t *testing.T) { if !detectDynamicImports(jsProjectFixture("entrypoints")) { t.Fatal("dynamic fixture was not detected") diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/browser.js b/components/analyzers/jsreach/testdata/projects/entrypoints/browser.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/browser.js rename to components/analyzers/jsreach/testdata/projects/entrypoints/browser.js diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/cjs.js b/components/analyzers/jsreach/testdata/projects/entrypoints/cjs.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/cjs.js rename to components/analyzers/jsreach/testdata/projects/entrypoints/cjs.js diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/cli.js b/components/analyzers/jsreach/testdata/projects/entrypoints/cli.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/cli.js rename to components/analyzers/jsreach/testdata/projects/entrypoints/cli.js diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/dynamic.ts b/components/analyzers/jsreach/testdata/projects/entrypoints/dynamic.ts similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/dynamic.ts rename to components/analyzers/jsreach/testdata/projects/entrypoints/dynamic.ts diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/esm.js b/components/analyzers/jsreach/testdata/projects/entrypoints/esm.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/esm.js rename to components/analyzers/jsreach/testdata/projects/entrypoints/esm.js diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/package.json b/components/analyzers/jsreach/testdata/projects/entrypoints/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/package.json rename to components/analyzers/jsreach/testdata/projects/entrypoints/package.json diff --git a/internal/analyzers/jsreach/testdata/projects/entrypoints/util.js b/components/analyzers/jsreach/testdata/projects/entrypoints/util.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/entrypoints/util.js rename to components/analyzers/jsreach/testdata/projects/entrypoints/util.js diff --git a/internal/analyzers/jsreach/testdata/projects/static/build/ignored.js b/components/analyzers/jsreach/testdata/projects/static/build/ignored.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/static/build/ignored.js rename to components/analyzers/jsreach/testdata/projects/static/build/ignored.js diff --git a/internal/analyzers/jsreach/testdata/projects/static/index.js b/components/analyzers/jsreach/testdata/projects/static/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/static/index.js rename to components/analyzers/jsreach/testdata/projects/static/index.js diff --git a/internal/analyzers/jsreach/testdata/projects/static/package.json b/components/analyzers/jsreach/testdata/projects/static/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/projects/static/package.json rename to components/analyzers/jsreach/testdata/projects/static/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/index.js b/components/analyzers/jsreach/testdata/workspaces/npm-array/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/index.js rename to components/analyzers/jsreach/testdata/workspaces/npm-array/index.js diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/index.js b/components/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/index.js rename to components/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/index.js diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/package.json b/components/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/package.json rename to components/analyzers/jsreach/testdata/workspaces/npm-array/nested/children/leaf/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/nested/package.json b/components/analyzers/jsreach/testdata/workspaces/npm-array/nested/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/nested/package.json rename to components/analyzers/jsreach/testdata/workspaces/npm-array/nested/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/package.json b/components/analyzers/jsreach/testdata/workspaces/npm-array/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/package.json rename to components/analyzers/jsreach/testdata/workspaces/npm-array/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/index.js b/components/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/index.js rename to components/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/index.js diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/package.json b/components/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/package.json rename to components/analyzers/jsreach/testdata/workspaces/npm-array/packages/app/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/index.js b/components/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/index.js rename to components/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/index.js diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/package.json b/components/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/package.json rename to components/analyzers/jsreach/testdata/workspaces/npm-array/packages/shared/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/index.js b/components/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/index.js rename to components/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/index.js diff --git a/internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/package.json b/components/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/package.json rename to components/analyzers/jsreach/testdata/workspaces/npm-array/packages/unused/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/package.json b/components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/package.json rename to components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/app/package.json b/components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/app/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/app/package.json rename to components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/app/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/deep/child/package.json b/components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/deep/child/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/deep/child/package.json rename to components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/deep/child/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/excluded/package.json b/components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/excluded/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/excluded/package.json rename to components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/excluded/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/private/child/package.json b/components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/private/child/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/private/child/package.json rename to components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/packages/private/child/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/pnpm-workspace.yaml b/components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/pnpm-workspace.yaml similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/pnpm-patterns/pnpm-workspace.yaml rename to components/analyzers/jsreach/testdata/workspaces/pnpm-patterns/pnpm-workspace.yaml diff --git a/internal/analyzers/jsreach/testdata/workspaces/standalone/index.js b/components/analyzers/jsreach/testdata/workspaces/standalone/index.js similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/standalone/index.js rename to components/analyzers/jsreach/testdata/workspaces/standalone/index.js diff --git a/internal/analyzers/jsreach/testdata/workspaces/standalone/package.json b/components/analyzers/jsreach/testdata/workspaces/standalone/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/standalone/package.json rename to components/analyzers/jsreach/testdata/workspaces/standalone/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/yarn-object/package.json b/components/analyzers/jsreach/testdata/workspaces/yarn-object/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/yarn-object/package.json rename to components/analyzers/jsreach/testdata/workspaces/yarn-object/package.json diff --git a/internal/analyzers/jsreach/testdata/workspaces/yarn-object/packages/app/package.json b/components/analyzers/jsreach/testdata/workspaces/yarn-object/packages/app/package.json similarity index 100% rename from internal/analyzers/jsreach/testdata/workspaces/yarn-object/packages/app/package.json rename to components/analyzers/jsreach/testdata/workspaces/yarn-object/packages/app/package.json diff --git a/internal/analyzers/jsreach/workspace_test.go b/components/analyzers/jsreach/workspace_test.go similarity index 100% rename from internal/analyzers/jsreach/workspace_test.go rename to components/analyzers/jsreach/workspace_test.go diff --git a/internal/analyzers/jsreach/workspace_testdata_test.go b/components/analyzers/jsreach/workspace_testdata_test.go similarity index 100% rename from internal/analyzers/jsreach/workspace_testdata_test.go rename to components/analyzers/jsreach/workspace_testdata_test.go diff --git a/internal/analyzers/jvmreach/analyzer.go b/components/analyzers/jvmreach/analyzer.go similarity index 92% rename from internal/analyzers/jvmreach/analyzer.go rename to components/analyzers/jvmreach/analyzer.go index abe225a8..1eae1d18 100644 --- a/internal/analyzers/jvmreach/analyzer.go +++ b/components/analyzers/jvmreach/analyzer.go @@ -52,6 +52,7 @@ func (a Analyzer) Descriptor() model.AnalyzerDescriptor { model.LanguageGroovy, }, SupportedTiers: []model.ReachabilityTier{model.TierPackage}, + Capabilities: []string{model.CapabilityPackageUpdates}, } } @@ -114,7 +115,7 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. if len(hierarchies) == 0 { logger.Info("jvmreach: no JVM project roots discovered; marking all JVM vulnerabilities as unknown") annotateAllUnknown(req, "no-project-root-discovered", time.Now()) - return resultFromRequest(req), nil + return finishResult(req, resultFromRequest(req)), nil } logger.Info("jvmreach: starting reachability analysis", @@ -179,7 +180,7 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. out := resultFromRequest(req) out.AnalyzerStats = map[string]model.ReachabilityStats{Name: stats} - return out, nil + return finishResult(req, out), nil } type moduleClosure struct { @@ -370,7 +371,7 @@ func (a Analyzer) runWithCache( } if cache != nil { if err := cache.set(projectDir, runner.Name(), runner.Version(), result); err != nil { - logger.Debug("jvmreach: cache write failed (non-fatal)", + logger.Warn("jvmreach: cache write failed (non-fatal)", zap.String("project_root", projectDir), zap.Error(err)) } @@ -382,7 +383,7 @@ func (a Analyzer) cache() *resultCache { if a.DisableCache { return nil } - return newResultCache(a.CacheDir, a.CacheTTL) + return newResultCache(a.CacheDir, a.CacheTTL, a.logger()) } func resultFromRequest(req model.AnalyzeRequest) model.AnalyzeResult { @@ -620,3 +621,33 @@ func failureReason(err error) string { return "runner-error" } } + +// finishResult applies the package-updates delta protocol to out. When the +// host accepts deltas (req.AcceptPackageUpdates), the analyzer returns only +// the registry packages it annotated instead of the full registry; the host +// folds them back in with sdk.ApplyPackageUpdates. The annotation this +// analyzer writes -- filling Vulnerability.Reachability on existing +// (Source, ID)-keyed vulnerabilities that had none -- is exactly what the +// host-side merge (Package.MergeFrom) expresses, so the delta path is +// equivalent to the legacy in-place path. The one in-place behavior the merge +// cannot express is replacing a Reachability annotation already written by a +// DIFFERENT analyzer; built-in analyzer dispatch is language-disjoint, so no +// two built-ins annotate the same package. +func finishResult(req model.AnalyzeRequest, out model.AnalyzeResult) model.AnalyzeResult { + if !req.AcceptPackageUpdates || req.Registry == nil { + return out + } + out.Registry = nil + for _, pkg := range req.Registry.All() { + if pkg == nil { + continue + } + for _, vuln := range pkg.Vulnerabilities { + if vuln.Reachability != nil && vuln.Reachability.Analyzer == Name { + out.PackageUpdates = append(out.PackageUpdates, pkg) + break + } + } + } + return out +} diff --git a/internal/analyzers/jvmreach/analyzer_test.go b/components/analyzers/jvmreach/analyzer_test.go similarity index 100% rename from internal/analyzers/jvmreach/analyzer_test.go rename to components/analyzers/jvmreach/analyzer_test.go diff --git a/internal/analyzers/jvmreach/cache.go b/components/analyzers/jvmreach/cache.go similarity index 77% rename from internal/analyzers/jvmreach/cache.go rename to components/analyzers/jvmreach/cache.go index 5d670b64..62c5e628 100644 --- a/internal/analyzers/jvmreach/cache.go +++ b/components/analyzers/jvmreach/cache.go @@ -10,6 +10,7 @@ import ( "time" cachepkg "github.com/bomly-dev/bomly-sdk/filecache" + "go.uber.org/zap" "github.com/bomly-dev/bomly-sdk/system" ) @@ -29,30 +30,46 @@ type cachedRunnerResult struct { DynamicImportsDetected bool `json:"dynamic_imports_detected,omitempty"` } -func newResultCache(dir string, ttl time.Duration) *resultCache { +// newResultCache constructs a result cache rooted at dir. If dir is +// empty, the OS user cache directory is used. Errors creating the +// cache directory are non-fatal — they log one WARN and return a nil +// resultCache that the caller can use without checks. +func newResultCache(dir string, ttl time.Duration, logger *zap.Logger) *resultCache { + logger = ensureLogger(logger) if ttl <= 0 { ttl = defaultCacheTTL } root := dir if root == "" { - root = defaultCacheRoot() - } - if root == "" { - return nil + defaultRoot, err := defaultCacheRoot() + if err != nil { + logger.Warn("jvmreach: result cache disabled: user cache directory unavailable (non-fatal)", + zap.Error(err)) + return nil + } + root = defaultRoot } store, err := cachepkg.NewFileCache(root, ttl) if err != nil { + logger.Warn("jvmreach: result cache disabled: cache initialization failed (non-fatal)", + zap.String("dir", root), zap.Error(err)) return nil } return &resultCache{store: store} } -func defaultCacheRoot() string { +// defaultCacheRoot returns the platform-appropriate cache directory +// for jvmreach analyzer results, or an error when the user cache +// directory cannot be determined. +func defaultCacheRoot() (string, error) { base, err := os.UserCacheDir() - if err != nil || base == "" { - return "" + if err != nil { + return "", err + } + if base == "" { + return "", errors.New("user cache directory is empty") } - return filepath.Join(base, "bomly", "analyzers", "jvmreach") + return filepath.Join(base, "bomly", "analyzers", "jvmreach"), nil } func keyFor(projectDir, runnerName, runnerVersion string) (cachepkg.Key, error) { diff --git a/internal/analyzers/jvmreach/cache_test.go b/components/analyzers/jvmreach/cache_test.go similarity index 80% rename from internal/analyzers/jvmreach/cache_test.go rename to components/analyzers/jvmreach/cache_test.go index 58f340e7..287f39d0 100644 --- a/internal/analyzers/jvmreach/cache_test.go +++ b/components/analyzers/jvmreach/cache_test.go @@ -7,11 +7,13 @@ import ( "testing" model "github.com/bomly-dev/bomly-sdk" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" ) func TestResultCacheRoundTrip(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) if cache == nil { t.Fatal("newResultCache returned nil") } @@ -41,7 +43,7 @@ func TestResultCacheRoundTrip(t *testing.T) { func TestResultCacheInvalidatesOnBuildFileChange(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) projectDir := newJVMProjectDir(t) pom := filepath.Join(projectDir, "pom.xml") if err := cache.set(projectDir, "fake", "1.0", RunnerResult{}); err != nil { @@ -89,3 +91,18 @@ func TestAnalyzerWithCacheServesSecondCallFromCache(t *testing.T) { t.Errorf("cached path did not produce a reachable annotation: %+v", r) } } + +func TestNewResultCacheWarnsWhenInitFails(t *testing.T) { + core, logs := observer.New(zap.WarnLevel) + blocker := filepath.Join(t.TempDir(), "blocker") + if err := os.WriteFile(blocker, []byte("not a directory"), 0o600); err != nil { + t.Fatalf("write blocker file: %v", err) + } + cache := newResultCache(filepath.Join(blocker, "nested"), 0, zap.New(core)) + if cache != nil { + t.Fatal("expected nil cache when the cache root cannot be created") + } + if got := logs.FilterLevelExact(zap.WarnLevel).Len(); got != 1 { + t.Fatalf("expected exactly one WARN log, got %d: %v", got, logs.All()) + } +} diff --git a/internal/analyzers/jvmreach/discover.go b/components/analyzers/jvmreach/discover.go similarity index 97% rename from internal/analyzers/jvmreach/discover.go rename to components/analyzers/jvmreach/discover.go index 18a10de6..a4bcdb5a 100644 --- a/internal/analyzers/jvmreach/discover.go +++ b/components/analyzers/jvmreach/discover.go @@ -232,6 +232,9 @@ func readGradleModules(root string) []jvmModule { for _, module := range seen { modules = append(modules, module) } + // Sort so callers (and fuzz determinism checks) see a stable order + // regardless of Go's randomized map iteration. + sort.Slice(modules, func(i, j int) bool { return modules[i].Dir < modules[j].Dir }) return modules } diff --git a/components/analyzers/jvmreach/discover_fuzz_test.go b/components/analyzers/jvmreach/discover_fuzz_test.go new file mode 100644 index 00000000..f6aa43fb --- /dev/null +++ b/components/analyzers/jvmreach/discover_fuzz_test.go @@ -0,0 +1,86 @@ +package jvmreach + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + testutil "github.com/bomly-dev/bomly-sdk/testkit" +) + +// FuzzReadMavenProject verifies that the Maven pom.xml module reader +// never panics and produces deterministic results for arbitrary +// (valid, malformed, or truncated) XML input within the shared fuzz +// input bound. The reader is file-backed, so each iteration writes the +// input as pom.xml in a fresh temp dir and reads it back twice. +func FuzzReadMavenProject(f *testing.F) { + for _, seed := range []string{ + "", + `com.exampleapp`, + `com.examplechildcore../escape`, + `a`, + `not xml at all`, + "\xff\xfe", + } { + f.Add([]byte(seed)) + } + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > testutil.MaxFuzzInputSize { + return + } + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "pom.xml"), data, 0o600); err != nil { + t.Fatalf("write pom.xml: %v", err) + } + first, firstOK := readMavenProject(dir) + second, secondOK := readMavenProject(dir) + if firstOK != secondOK { + t.Fatalf("read changed success state: first=%v second=%v", firstOK, secondOK) + } + if !firstOK { + return + } + if !reflect.DeepEqual(first, second) { + t.Fatal("read changed result for identical input") + } + }) +} + +// FuzzReadGradleModules verifies that the Gradle settings module reader +// never panics and produces deterministic results for arbitrary +// (valid, malformed, or truncated) settings-script input within the +// shared fuzz input bound. Determinism matters here because the reader +// collects modules through a map; its output order must not depend on +// Go's randomized map iteration. +func FuzzReadGradleModules(f *testing.F) { + for _, seed := range []string{ + "", + `include ':core', ':app'`, + "include(\":a\")\ninclude ':b'\nproject(':a').projectDir = file('modules/a')\n", + `include ':escape'` + "\n" + `project(':escape').projectDir = file('../outside')`, + `include "unterminated`, + "rootProject.name = 'demo'\n/* include ':commented' */\n", + } { + f.Add([]byte(seed)) + } + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > testutil.MaxFuzzInputSize { + return + } + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "settings.gradle"), data, 0o600); err != nil { + t.Fatalf("write settings.gradle: %v", err) + } + first := readGradleModules(root) + second := readGradleModules(root) + if !reflect.DeepEqual(first, second) { + t.Fatal("read changed result for identical input") + } + for _, module := range first { + if !pathContainsRoot(module.Dir, root) { + t.Fatalf("module dir %q escapes root %q", module.Dir, root) + } + } + }) +} diff --git a/internal/analyzers/jvmreach/dynamicimports.go b/components/analyzers/jvmreach/dynamicimports.go similarity index 100% rename from internal/analyzers/jvmreach/dynamicimports.go rename to components/analyzers/jvmreach/dynamicimports.go diff --git a/components/analyzers/jvmreach/go.mod b/components/analyzers/jvmreach/go.mod new file mode 100644 index 00000000..ed46bc64 --- /dev/null +++ b/components/analyzers/jvmreach/go.mod @@ -0,0 +1,27 @@ +module github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach + +go 1.26.3 + +require ( + github.com/bomly-dev/bomly-sdk v0.3.0 + go.uber.org/zap v1.28.0 +) + +require ( + github.com/anchore/packageurl-go v0.2.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/oklog/run v1.1.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/components/analyzers/jvmreach/go.sum b/components/analyzers/jvmreach/go.sum new file mode 100644 index 00000000..2fe7f472 --- /dev/null +++ b/components/analyzers/jvmreach/go.sum @@ -0,0 +1,89 @@ +github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= +github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= +github.com/bomly-dev/bomly-sdk v0.3.0 h1:JtC7qZ9yq3r4fUYyq7e/Os4f9wGMa4Qot8U0l9MepFA= +github.com/bomly-dev/bomly-sdk v0.3.0/go.mod h1:yn1LBkoHG9gDBXKyRj0UNJo0BlXl8Bj9Ymb3WKLIh78= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/analyzers/jvmreach/importscanner.go b/components/analyzers/jvmreach/importscanner.go similarity index 100% rename from internal/analyzers/jvmreach/importscanner.go rename to components/analyzers/jvmreach/importscanner.go diff --git a/components/analyzers/jvmreach/importscanner_fuzz_test.go b/components/analyzers/jvmreach/importscanner_fuzz_test.go new file mode 100644 index 00000000..b9fd0ace --- /dev/null +++ b/components/analyzers/jvmreach/importscanner_fuzz_test.go @@ -0,0 +1,40 @@ +package jvmreach + +import ( + "bytes" + "reflect" + "testing" + + testutil "github.com/bomly-dev/bomly-sdk/testkit" +) + +// FuzzScanImports verifies that the JVM import scanner never panics and +// produces deterministic results for arbitrary (valid, malformed, or +// truncated) source input within the shared fuzz input bound. +func FuzzScanImports(f *testing.F) { + for _, seed := range []string{ + "", + "import java.util.List;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n", + "/* block\nimport hidden.In.Comment;\n*/\nimport real.Thing; // tail\n", + "package com.example;\nimport static org.junit.Assert.assertTrue;\nimport scala.collection.mutable._\n", + "import unterminated\n/* open comment\nimport swallowed\n", + } { + f.Add([]byte(seed)) + } + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > testutil.MaxFuzzInputSize { + return + } + first, firstErr := scanImports(bytes.NewReader(data)) + second, secondErr := scanImports(bytes.NewReader(data)) + if (firstErr == nil) != (secondErr == nil) { + t.Fatalf("scan changed success state: first=%v second=%v", firstErr, secondErr) + } + if firstErr != nil { + return + } + if !reflect.DeepEqual(first, second) { + t.Fatal("scan changed result for identical input") + } + }) +} diff --git a/internal/analyzers/jvmreach/importscanner_test.go b/components/analyzers/jvmreach/importscanner_test.go similarity index 100% rename from internal/analyzers/jvmreach/importscanner_test.go rename to components/analyzers/jvmreach/importscanner_test.go diff --git a/components/analyzers/jvmreach/module.go b/components/analyzers/jvmreach/module.go new file mode 100644 index 00000000..2ca491a8 --- /dev/null +++ b/components/analyzers/jvmreach/module.go @@ -0,0 +1,19 @@ +package jvmreach + +import ( + "context" + + sdk "github.com/bomly-dev/bomly-sdk" +) + +// Module returns the jvmreach analyzer as an execution-neutral sdk.Module. +// The Bomly CLI composition embeds it directly; the same value could back a +// managed plugin binary via sdk.ServeModule. +func Module() sdk.Module { + return sdk.Module{Kind: sdk.PluginKindAnalyzer, Analyzer: &sdk.AnalyzerModule{ + Descriptor: Analyzer{}.Descriptor(), + New: func(_ context.Context, host sdk.HostContext) (sdk.Analyzer, error) { + return Analyzer{Logger: host.Logger()}, nil + }, + }} +} diff --git a/components/analyzers/jvmreach/module_test.go b/components/analyzers/jvmreach/module_test.go new file mode 100644 index 00000000..4c7bc751 --- /dev/null +++ b/components/analyzers/jvmreach/module_test.go @@ -0,0 +1,96 @@ +package jvmreach + +import ( + "context" + "reflect" + "testing" + + model "github.com/bomly-dev/bomly-sdk" + "github.com/bomly-dev/bomly-sdk/conformance" +) + +// TestConformance runs the SDK conformance suite against the module. No +// manifest is supplied: the analyzer ships embedded in the CLI, not as a +// packaged plugin. +func TestConformance(t *testing.T) { + conformance.Test(t, conformance.Config{Module: Module()}) +} + +// TestModuleDescriptorMatchesAnalyzer pins the module descriptor to the +// analyzer's own Descriptor so the two can never drift. +func TestModuleDescriptorMatchesAnalyzer(t *testing.T) { + if !reflect.DeepEqual(Module().Analyzer.Descriptor, Analyzer{}.Descriptor()) { + t.Fatal("module descriptor differs from Analyzer{}.Descriptor()") + } +} + +// clearAnalyzedAt blanks the wall-clock annotation timestamps so two runs of +// the same analysis compare equal. +func clearAnalyzedAt(reg *model.PackageRegistry) { + for _, pkg := range reg.All() { + for i := range pkg.Vulnerabilities { + if r := pkg.Vulnerabilities[i].Reachability; r != nil { + r.AnalyzedAt = "" + } + } + } +} + +// TestPackageUpdatesEquivalence verifies the package-updates delta protocol: +// applying the returned PackageUpdates onto a pristine copy of the input +// registry yields exactly the registry the legacy in-place path produces. +func TestPackageUpdatesEquivalence(t *testing.T) { + projectDir := newJVMProjectDir(t) + vuln := model.Vulnerability{ID: "GHSA-test", Source: "osv", ParsedSeverity: "high"} + runnerResult := RunnerResult{ + ImportedArtifacts: map[string]struct{}{"com.fasterxml.jackson.core:jackson-databind": {}}, + SourceFiles: 1, + } + seed := func() (*model.Graph, *model.PackageRegistry) { + g, reg := newSeed() + addJVMDep(t, g, reg, projectDir, "com.fasterxml.jackson.core", "jackson-databind", "1.0.0", vuln) + addJVMDep(t, g, reg, projectDir, "log4j", "log4j", "1.0.0", vuln) + return g, reg + } + + legacyGraph, legacyReg := seed() + legacy := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + legacyRes, err := legacy.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: legacyGraph, Registry: legacyReg, ProjectPath: projectDir, + }) + if err != nil { + t.Fatalf("legacy Analyze err: %v", err) + } + if len(legacyRes.PackageUpdates) != 0 { + t.Fatalf("legacy path returned %d package updates, want 0", len(legacyRes.PackageUpdates)) + } + if legacyRes.Registry != legacyReg { + t.Fatalf("legacy path must return the annotated request registry (got %p, want %p): plugin-boundary hosts cannot see in-place mutation", legacyRes.Registry, legacyReg) + } + + deltaGraph, deltaReg := seed() + delta := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + deltaRes, err := delta.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: deltaGraph, Registry: deltaReg, ProjectPath: projectDir, + AcceptPackageUpdates: true, + }) + if err != nil { + t.Fatalf("delta Analyze err: %v", err) + } + if deltaRes.Registry != nil { + t.Fatal("delta path returned a full registry; want PackageUpdates only") + } + if len(deltaRes.PackageUpdates) == 0 { + t.Fatal("delta path returned no package updates") + } + + _, pristineReg := seed() + merged := model.ApplyPackageUpdates(pristineReg, deltaRes.PackageUpdates) + + clearAnalyzedAt(legacyReg) + clearAnalyzedAt(merged) + if !reflect.DeepEqual(legacyReg.All(), merged.All()) { + t.Fatalf("delta-applied registry differs from legacy registry:\nlegacy: %+v\nmerged: %+v", + legacyReg.All(), merged.All()) + } +} diff --git a/internal/analyzers/jvmreach/modules_test.go b/components/analyzers/jvmreach/modules_test.go similarity index 100% rename from internal/analyzers/jvmreach/modules_test.go rename to components/analyzers/jvmreach/modules_test.go diff --git a/internal/analyzers/jvmreach/modules_testdata_test.go b/components/analyzers/jvmreach/modules_testdata_test.go similarity index 100% rename from internal/analyzers/jvmreach/modules_testdata_test.go rename to components/analyzers/jvmreach/modules_testdata_test.go diff --git a/internal/analyzers/jvmreach/prefixmap.go b/components/analyzers/jvmreach/prefixmap.go similarity index 100% rename from internal/analyzers/jvmreach/prefixmap.go rename to components/analyzers/jvmreach/prefixmap.go diff --git a/internal/analyzers/jvmreach/prefixmap_test.go b/components/analyzers/jvmreach/prefixmap_test.go similarity index 100% rename from internal/analyzers/jvmreach/prefixmap_test.go rename to components/analyzers/jvmreach/prefixmap_test.go diff --git a/internal/analyzers/jvmreach/runner.go b/components/analyzers/jvmreach/runner.go similarity index 100% rename from internal/analyzers/jvmreach/runner.go rename to components/analyzers/jvmreach/runner.go diff --git a/internal/analyzers/jvmreach/runner_library.go b/components/analyzers/jvmreach/runner_library.go similarity index 93% rename from internal/analyzers/jvmreach/runner_library.go rename to components/analyzers/jvmreach/runner_library.go index fa1af64e..78b03b25 100644 --- a/internal/analyzers/jvmreach/runner_library.go +++ b/components/analyzers/jvmreach/runner_library.go @@ -29,9 +29,13 @@ func (libraryRunner) Name() string { return "library" } func (libraryRunner) Version() string { return runnerSchemaVersion } func (r libraryRunner) Run(ctx context.Context, projectDir string) (RunnerResult, error) { - if info, err := os.Stat(projectDir); err != nil || !info.IsDir() { + info, err := os.Stat(projectDir) + if err != nil { return RunnerResult{}, fmt.Errorf("project dir not accessible: %w", err) } + if !info.IsDir() { + return RunnerResult{}, fmt.Errorf("project dir not accessible: %q is not a directory", projectDir) + } r.logger.Debug("jvmreach: executing in-process runner", zap.String("project_dir", projectDir), diff --git a/internal/analyzers/jvmreach/runner_testdata_test.go b/components/analyzers/jvmreach/runner_testdata_test.go similarity index 100% rename from internal/analyzers/jvmreach/runner_testdata_test.go rename to components/analyzers/jvmreach/runner_testdata_test.go diff --git a/internal/analyzers/jvmreach/sources.go b/components/analyzers/jvmreach/sources.go similarity index 92% rename from internal/analyzers/jvmreach/sources.go rename to components/analyzers/jvmreach/sources.go index 06ec6881..0a580231 100644 --- a/internal/analyzers/jvmreach/sources.go +++ b/components/analyzers/jvmreach/sources.go @@ -52,6 +52,10 @@ func walkSourceFiles(root string, fn func(path string) error) (skipped []string, skippedSet := make(map[string]struct{}) walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { + // Permission errors and similar issues skip the offending + // entry without aborting the whole walk: reachability here + // is deliberately best-effort (mirroring pyreach), and + // tier-3 "unreachable" is documented as not meaning safe. if d != nil && d.IsDir() { return filepath.SkipDir } diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/app/build.gradle b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/app/build.gradle similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/app/build.gradle rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/app/build.gradle diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/app/src/main/java/com/example/app/Application.java b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/app/src/main/java/com/example/app/Application.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/app/src/main/java/com/example/app/Application.java rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/app/src/main/java/com/example/app/Application.java diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/build.gradle b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/build.gradle similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/build.gradle rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/build.gradle diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/build.gradle b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/build.gradle similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/build.gradle rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/build.gradle diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/src/main/java/com/example/shared/Helper.java b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/src/main/java/com/example/shared/Helper.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/src/main/java/com/example/shared/Helper.java rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/libs/common/src/main/java/com/example/shared/Helper.java diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/settings.gradle b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/settings.gradle similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/settings.gradle rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/settings.gradle diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-groovy/unused/build.gradle b/components/analyzers/jvmreach/testdata/modules/gradle-groovy/unused/build.gradle similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-groovy/unused/build.gradle rename to components/analyzers/jvmreach/testdata/modules/gradle-groovy/unused/build.gradle diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/build.gradle.kts b/components/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/build.gradle.kts similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/build.gradle.kts rename to components/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/build.gradle.kts diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/src/main/kotlin/com/example/App.kt b/components/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/src/main/kotlin/com/example/App.kt similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/src/main/kotlin/com/example/App.kt rename to components/analyzers/jvmreach/testdata/modules/gradle-kotlin/app/src/main/kotlin/com/example/App.kt diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/build.gradle.kts b/components/analyzers/jvmreach/testdata/modules/gradle-kotlin/build.gradle.kts similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/build.gradle.kts rename to components/analyzers/jvmreach/testdata/modules/gradle-kotlin/build.gradle.kts diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/build.gradle.kts b/components/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/build.gradle.kts similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/build.gradle.kts rename to components/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/build.gradle.kts diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/src/main/kotlin/com/example/shared/Helper.kt b/components/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/src/main/kotlin/com/example/shared/Helper.kt similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/src/main/kotlin/com/example/shared/Helper.kt rename to components/analyzers/jvmreach/testdata/modules/gradle-kotlin/nested/shared/src/main/kotlin/com/example/shared/Helper.kt diff --git a/internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/settings.gradle.kts b/components/analyzers/jvmreach/testdata/modules/gradle-kotlin/settings.gradle.kts similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/gradle-kotlin/settings.gradle.kts rename to components/analyzers/jvmreach/testdata/modules/gradle-kotlin/settings.gradle.kts diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/app/pom.xml b/components/analyzers/jvmreach/testdata/modules/maven-reactor/app/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/app/pom.xml rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/app/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/app/src/main/java/com/example/app/App.java b/components/analyzers/jvmreach/testdata/modules/maven-reactor/app/src/main/java/com/example/app/App.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/app/src/main/java/com/example/app/App.java rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/app/src/main/java/com/example/app/App.java diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/pom.xml b/components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/pom.xml rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/pom.xml b/components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/pom.xml rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/src/main/java/com/example/shared/Helper.java b/components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/src/main/java/com/example/shared/Helper.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/src/main/java/com/example/shared/Helper.java rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/shared/src/main/java/com/example/shared/Helper.java diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/pom.xml b/components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/pom.xml rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/src/main/java/com/example/shared/specific/Specific.java b/components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/src/main/java/com/example/shared/specific/Specific.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/src/main/java/com/example/shared/specific/Specific.java rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/libs/specific/src/main/java/com/example/shared/specific/Specific.java diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/pom.xml b/components/analyzers/jvmreach/testdata/modules/maven-reactor/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/pom.xml rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/unused/pom.xml b/components/analyzers/jvmreach/testdata/modules/maven-reactor/unused/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/unused/pom.xml rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/unused/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/modules/maven-reactor/unused/src/main/java/com/example/unused/Unused.java b/components/analyzers/jvmreach/testdata/modules/maven-reactor/unused/src/main/java/com/example/unused/Unused.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/maven-reactor/unused/src/main/java/com/example/unused/Unused.java rename to components/analyzers/jvmreach/testdata/modules/maven-reactor/unused/src/main/java/com/example/unused/Unused.java diff --git a/internal/analyzers/jvmreach/testdata/modules/sbt-standalone/build.sbt b/components/analyzers/jvmreach/testdata/modules/sbt-standalone/build.sbt similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/sbt-standalone/build.sbt rename to components/analyzers/jvmreach/testdata/modules/sbt-standalone/build.sbt diff --git a/internal/analyzers/jvmreach/testdata/modules/sbt-standalone/src/main/scala/com/example/App.scala b/components/analyzers/jvmreach/testdata/modules/sbt-standalone/src/main/scala/com/example/App.scala similarity index 100% rename from internal/analyzers/jvmreach/testdata/modules/sbt-standalone/src/main/scala/com/example/App.scala rename to components/analyzers/jvmreach/testdata/modules/sbt-standalone/src/main/scala/com/example/App.scala diff --git a/internal/analyzers/jvmreach/testdata/projects/dynamic/child/pom.xml b/components/analyzers/jvmreach/testdata/projects/dynamic/child/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/dynamic/child/pom.xml rename to components/analyzers/jvmreach/testdata/projects/dynamic/child/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/projects/dynamic/child/src/main/java/com/example/child/Child.java b/components/analyzers/jvmreach/testdata/projects/dynamic/child/src/main/java/com/example/child/Child.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/dynamic/child/src/main/java/com/example/child/Child.java rename to components/analyzers/jvmreach/testdata/projects/dynamic/child/src/main/java/com/example/child/Child.java diff --git a/internal/analyzers/jvmreach/testdata/projects/dynamic/pom.xml b/components/analyzers/jvmreach/testdata/projects/dynamic/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/dynamic/pom.xml rename to components/analyzers/jvmreach/testdata/projects/dynamic/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/projects/dynamic/src/main/java/com/example/App.java b/components/analyzers/jvmreach/testdata/projects/dynamic/src/main/java/com/example/App.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/dynamic/src/main/java/com/example/App.java rename to components/analyzers/jvmreach/testdata/projects/dynamic/src/main/java/com/example/App.java diff --git a/internal/analyzers/jvmreach/testdata/projects/static/build/ignored.java b/components/analyzers/jvmreach/testdata/projects/static/build/ignored.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/static/build/ignored.java rename to components/analyzers/jvmreach/testdata/projects/static/build/ignored.java diff --git a/internal/analyzers/jvmreach/testdata/projects/static/pom.xml b/components/analyzers/jvmreach/testdata/projects/static/pom.xml similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/static/pom.xml rename to components/analyzers/jvmreach/testdata/projects/static/pom.xml diff --git a/internal/analyzers/jvmreach/testdata/projects/static/src/main/java/App.java b/components/analyzers/jvmreach/testdata/projects/static/src/main/java/App.java similarity index 100% rename from internal/analyzers/jvmreach/testdata/projects/static/src/main/java/App.java rename to components/analyzers/jvmreach/testdata/projects/static/src/main/java/App.java diff --git a/internal/analyzers/pyreach/analyzer.go b/components/analyzers/pyreach/analyzer.go similarity index 88% rename from internal/analyzers/pyreach/analyzer.go rename to components/analyzers/pyreach/analyzer.go index 620a73c0..b4c28f63 100644 --- a/internal/analyzers/pyreach/analyzer.go +++ b/components/analyzers/pyreach/analyzer.go @@ -59,6 +59,7 @@ func (a Analyzer) Descriptor() model.AnalyzerDescriptor { }, SupportedLanguages: []model.Language{model.LanguagePython}, SupportedTiers: []model.ReachabilityTier{model.TierPackage}, + Capabilities: []string{model.CapabilityPackageUpdates}, } } @@ -129,7 +130,7 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. if len(projectRoots) == 0 { logger.Info("pyreach: no Python project roots discovered; marking all Python vulnerabilities as unknown") annotateAllUnknown(req, "no-project-root-discovered", time.Now()) - return resultForRequest(), nil + return finishResult(req, resultFromRequest(req)), nil } logger.Info("pyreach: starting reachability analysis", @@ -198,9 +199,9 @@ func (a Analyzer) Analyze(ctx context.Context, req model.AnalyzeRequest) (model. zap.Duration("duration", time.Since(overallStart)), ) - out := resultForRequest() + out := resultFromRequest(req) out.AnalyzerStats = map[string]model.ReachabilityStats{Name: stats} - return out, nil + return finishResult(req, out), nil } func (a Analyzer) logger() *zap.Logger { return ensureLogger(a.Logger) } @@ -234,7 +235,7 @@ func (a Analyzer) runWithCache( } if cache != nil { if err := cache.set(projectDir, runner.Name(), runner.Version(), result); err != nil { - logger.Debug("pyreach: cache write failed (non-fatal)", + logger.Warn("pyreach: cache write failed (non-fatal)", zap.String("project_root", projectDir), zap.Error(err)) } @@ -249,11 +250,15 @@ func (a Analyzer) cache() *resultCache { if a.DisableCache { return nil } - return newResultCache(a.CacheDir, a.CacheTTL) + return newResultCache(a.CacheDir, a.CacheTTL, a.logger()) } -func resultForRequest() model.AnalyzeResult { - return model.AnalyzeResult{AnalyzerRuns: []string{Name}} +// resultFromRequest returns the legacy-path result: the (in-place +// annotated) request registry plus this analyzer's run marker. Returning +// the registry keeps annotations visible across a managed-plugin process +// boundary, where in-place mutation of req.Registry is not. +func resultFromRequest(req model.AnalyzeRequest) model.AnalyzeResult { + return model.AnalyzeResult{Registry: req.Registry, AnalyzerRuns: []string{Name}} } // applyOutcome reports per-vuln Reachability outcomes for telemetry. @@ -482,3 +487,33 @@ func failureReason(err error) string { return "runner-error" } } + +// finishResult applies the package-updates delta protocol to out. When the +// host accepts deltas (req.AcceptPackageUpdates), the analyzer returns only +// the registry packages it annotated instead of the full registry; the host +// folds them back in with sdk.ApplyPackageUpdates. The annotation this +// analyzer writes -- filling Vulnerability.Reachability on existing +// (Source, ID)-keyed vulnerabilities that had none -- is exactly what the +// host-side merge (Package.MergeFrom) expresses, so the delta path is +// equivalent to the legacy in-place path. The one in-place behavior the merge +// cannot express is replacing a Reachability annotation already written by a +// DIFFERENT analyzer; built-in analyzer dispatch is language-disjoint, so no +// two built-ins annotate the same package. +func finishResult(req model.AnalyzeRequest, out model.AnalyzeResult) model.AnalyzeResult { + if !req.AcceptPackageUpdates || req.Registry == nil { + return out + } + out.Registry = nil + for _, pkg := range req.Registry.All() { + if pkg == nil { + continue + } + for _, vuln := range pkg.Vulnerabilities { + if vuln.Reachability != nil && vuln.Reachability.Analyzer == Name { + out.PackageUpdates = append(out.PackageUpdates, pkg) + break + } + } + } + return out +} diff --git a/internal/analyzers/pyreach/analyzer_test.go b/components/analyzers/pyreach/analyzer_test.go similarity index 92% rename from internal/analyzers/pyreach/analyzer_test.go rename to components/analyzers/pyreach/analyzer_test.go index b60eae8d..6519a10d 100644 --- a/internal/analyzers/pyreach/analyzer_test.go +++ b/components/analyzers/pyreach/analyzer_test.go @@ -310,3 +310,22 @@ func TestAnalyzerMarksUnknownWhenNoProjectRootDiscovered(t *testing.T) { t.Errorf("reason = %q, want no-project-root-discovered", r.Reason) } } + +// TestFindProjectRootBailsOutInsideVendoredTree pins the vendored-tree +// guard: a dependency location below .venv/site-packages must not walk +// upward and attribute the surrounding application project to the +// installed dependency. +func TestFindProjectRootBailsOutInsideVendoredTree(t *testing.T) { + projectDir := newPythonProjectDir(t) + depDir := filepath.Join(projectDir, ".venv", "lib", "python3.11", "site-packages", "requests") + if err := os.MkdirAll(depDir, 0o755); err != nil { + t.Fatal(err) + } + if got := findProjectRoot(depDir); got != "" { + t.Errorf("findProjectRoot(%q) = %q, want empty (vendored tree)", depDir, got) + } + // Control: an application source path still resolves to the project. + if got := findProjectRoot(filepath.Join(projectDir, "app.py")); got != projectDir { + t.Errorf("findProjectRoot(app.py) = %q, want %q", got, projectDir) + } +} diff --git a/internal/analyzers/pyreach/cache.go b/components/analyzers/pyreach/cache.go similarity index 85% rename from internal/analyzers/pyreach/cache.go rename to components/analyzers/pyreach/cache.go index d34d5856..a694942d 100644 --- a/internal/analyzers/pyreach/cache.go +++ b/components/analyzers/pyreach/cache.go @@ -10,6 +10,7 @@ import ( "time" cachepkg "github.com/bomly-dev/bomly-sdk/filecache" + "go.uber.org/zap" "github.com/bomly-dev/bomly-sdk/system" ) @@ -43,35 +44,44 @@ type cachedRunnerResult struct { // newResultCache constructs a result cache rooted at dir. If dir is // empty, the OS user cache directory is used. Errors creating the -// cache directory are non-fatal — they return a nil resultCache that -// the caller can use without checks. -func newResultCache(dir string, ttl time.Duration) *resultCache { +// cache directory are non-fatal — they log one WARN and return a nil +// resultCache that the caller can use without checks. +func newResultCache(dir string, ttl time.Duration, logger *zap.Logger) *resultCache { + logger = ensureLogger(logger) if ttl <= 0 { ttl = defaultCacheTTL } root := dir if root == "" { - root = defaultCacheRoot() - } - if root == "" { - return nil + defaultRoot, err := defaultCacheRoot() + if err != nil { + logger.Warn("pyreach: result cache disabled: user cache directory unavailable (non-fatal)", + zap.Error(err)) + return nil + } + root = defaultRoot } store, err := cachepkg.NewFileCache(root, ttl) if err != nil { + logger.Warn("pyreach: result cache disabled: cache initialization failed (non-fatal)", + zap.String("dir", root), zap.Error(err)) return nil } return &resultCache{store: store} } // defaultCacheRoot returns the platform-appropriate cache directory -// for pyreach analyzer results, or "" if the user cache directory -// cannot be determined. -func defaultCacheRoot() string { +// for pyreach analyzer results, or an error when the user cache +// directory cannot be determined. +func defaultCacheRoot() (string, error) { base, err := os.UserCacheDir() - if err != nil || base == "" { - return "" + if err != nil { + return "", err + } + if base == "" { + return "", errors.New("user cache directory is empty") } - return filepath.Join(base, "bomly", "analyzers", "pyreach") + return filepath.Join(base, "bomly", "analyzers", "pyreach"), nil } // keyFor builds a stable cache key for one project pass. Folds every diff --git a/internal/analyzers/pyreach/cache_test.go b/components/analyzers/pyreach/cache_test.go similarity index 85% rename from internal/analyzers/pyreach/cache_test.go rename to components/analyzers/pyreach/cache_test.go index 529affff..464cb46c 100644 --- a/internal/analyzers/pyreach/cache_test.go +++ b/components/analyzers/pyreach/cache_test.go @@ -7,11 +7,13 @@ import ( "testing" model "github.com/bomly-dev/bomly-sdk" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" ) func TestResultCacheRoundTrip(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) if cache == nil { t.Fatal("newResultCache returned nil for a writable dir") } @@ -42,7 +44,7 @@ func TestResultCacheRoundTrip(t *testing.T) { func TestResultCacheIsolatesByRunnerVersion(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) projectDir := newPythonProjectDir(t) if err := cache.set(projectDir, "library", "1.0", RunnerResult{ImportedDistributions: map[string]struct{}{"a": {}}}); err != nil { @@ -55,7 +57,7 @@ func TestResultCacheIsolatesByRunnerVersion(t *testing.T) { func TestResultCacheInvalidatesOnLockfileChange(t *testing.T) { dir := t.TempDir() - cache := newResultCache(dir, 0) + cache := newResultCache(dir, 0, nil) projectDir := newPythonProjectDir(t) lockfile := filepath.Join(projectDir, "requirements.txt") @@ -142,3 +144,18 @@ func TestAnalyzerDisableCacheAlwaysRunsRunner(t *testing.T) { t.Errorf("DisableCache should re-run runner per call; got %d calls", runner.called) } } + +func TestNewResultCacheWarnsWhenInitFails(t *testing.T) { + core, logs := observer.New(zap.WarnLevel) + blocker := filepath.Join(t.TempDir(), "blocker") + if err := os.WriteFile(blocker, []byte("not a directory"), 0o600); err != nil { + t.Fatalf("write blocker file: %v", err) + } + cache := newResultCache(filepath.Join(blocker, "nested"), 0, zap.New(core)) + if cache != nil { + t.Fatal("expected nil cache when the cache root cannot be created") + } + if got := logs.FilterLevelExact(zap.WarnLevel).Len(); got != 1 { + t.Fatalf("expected exactly one WARN log, got %d: %v", got, logs.All()) + } +} diff --git a/internal/analyzers/pyreach/discover.go b/components/analyzers/pyreach/discover.go similarity index 93% rename from internal/analyzers/pyreach/discover.go rename to components/analyzers/pyreach/discover.go index 041526bc..1c463285 100644 --- a/internal/analyzers/pyreach/discover.go +++ b/components/analyzers/pyreach/discover.go @@ -84,15 +84,10 @@ func findProjectRoot(start string) string { return "" } if isInsideVendoredTree(dir) { - // Walking through a venv / site-packages would attribute - // dep source to the project. Bail out when we recognize - // we're below such a directory. - parent := filepath.Dir(dir) - if parent == dir { - return "" - } - dir = parent - continue + // Walking upward out of a venv / site-packages tree would + // attribute an installed dependency's source location to the + // surrounding application project. Bail out instead. + return "" } if hasProjectMarker(dir) { return dir diff --git a/internal/analyzers/pyreach/dynamicimports.go b/components/analyzers/pyreach/dynamicimports.go similarity index 100% rename from internal/analyzers/pyreach/dynamicimports.go rename to components/analyzers/pyreach/dynamicimports.go diff --git a/components/analyzers/pyreach/go.mod b/components/analyzers/pyreach/go.mod new file mode 100644 index 00000000..8e0f0500 --- /dev/null +++ b/components/analyzers/pyreach/go.mod @@ -0,0 +1,27 @@ +module github.com/bomly-dev/bomly-cli/components/analyzers/pyreach + +go 1.26.3 + +require ( + github.com/bomly-dev/bomly-sdk v0.3.0 + go.uber.org/zap v1.28.0 +) + +require ( + github.com/anchore/packageurl-go v0.2.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.8.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/oklog/run v1.1.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.83.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/components/analyzers/pyreach/go.sum b/components/analyzers/pyreach/go.sum new file mode 100644 index 00000000..2fe7f472 --- /dev/null +++ b/components/analyzers/pyreach/go.sum @@ -0,0 +1,89 @@ +github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= +github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= +github.com/bomly-dev/bomly-sdk v0.3.0 h1:JtC7qZ9yq3r4fUYyq7e/Os4f9wGMa4Qot8U0l9MepFA= +github.com/bomly-dev/bomly-sdk v0.3.0/go.mod h1:yn1LBkoHG9gDBXKyRj0UNJo0BlXl8Bj9Ymb3WKLIh78= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/analyzers/pyreach/importscanner.go b/components/analyzers/pyreach/importscanner.go similarity index 96% rename from internal/analyzers/pyreach/importscanner.go rename to components/analyzers/pyreach/importscanner.go index 1ba64182..1ab2dcd9 100644 --- a/internal/analyzers/pyreach/importscanner.go +++ b/components/analyzers/pyreach/importscanner.go @@ -62,10 +62,12 @@ func scanImports(r io.Reader) (map[string]struct{}, error) { } continue } - // Detect opening triple-quote on this line. - if i := indexAny(line, `"""`, `'''`); i >= 0 { - marker := line[i : i+3] - rest := line[i+3:] + // Detect opening triple-quote on this line. Inspect the + // comment-stripped text so a marker inside a trailing comment + // cannot flip the block state. + if i := indexAny(stripped, `"""`, `'''`); i >= 0 { + marker := stripped[i : i+3] + rest := stripped[i+3:] // If the closing triple-quote appears on the same line, // it's a single-line docstring — not a block. Ignore it // unless it's the only content; in either case, do not diff --git a/components/analyzers/pyreach/importscanner_fuzz_test.go b/components/analyzers/pyreach/importscanner_fuzz_test.go new file mode 100644 index 00000000..bafacc12 --- /dev/null +++ b/components/analyzers/pyreach/importscanner_fuzz_test.go @@ -0,0 +1,40 @@ +package pyreach + +import ( + "bytes" + "reflect" + "testing" + + testutil "github.com/bomly-dev/bomly-sdk/testkit" +) + +// FuzzScanImports verifies that the Python import scanner never panics +// and produces deterministic results for arbitrary (valid, malformed, or +// truncated) source input within the shared fuzz input bound. +func FuzzScanImports(f *testing.F) { + for _, seed := range []string{ + "", + "import os\nimport requests, flask\nfrom django.db import models\n", + "\"\"\"docstring\nimport hidden\n\"\"\"\nimport real # trailing \"\"\" comment\n", + "from . import sibling\nfrom .. import parent\nimport a.b.c as abc\n", + "import unterminated\nx = '''\nimport swallowed\n", + } { + f.Add([]byte(seed)) + } + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > testutil.MaxFuzzInputSize { + return + } + first, firstErr := scanImports(bytes.NewReader(data)) + second, secondErr := scanImports(bytes.NewReader(data)) + if (firstErr == nil) != (secondErr == nil) { + t.Fatalf("scan changed success state: first=%v second=%v", firstErr, secondErr) + } + if firstErr != nil { + return + } + if !reflect.DeepEqual(first, second) { + t.Fatal("scan changed result for identical input") + } + }) +} diff --git a/internal/analyzers/pyreach/importscanner_test.go b/components/analyzers/pyreach/importscanner_test.go similarity index 100% rename from internal/analyzers/pyreach/importscanner_test.go rename to components/analyzers/pyreach/importscanner_test.go diff --git a/components/analyzers/pyreach/module.go b/components/analyzers/pyreach/module.go new file mode 100644 index 00000000..e842b6c1 --- /dev/null +++ b/components/analyzers/pyreach/module.go @@ -0,0 +1,19 @@ +package pyreach + +import ( + "context" + + sdk "github.com/bomly-dev/bomly-sdk" +) + +// Module returns the pyreach analyzer as an execution-neutral sdk.Module. +// The Bomly CLI composition embeds it directly; the same value could back a +// managed plugin binary via sdk.ServeModule. +func Module() sdk.Module { + return sdk.Module{Kind: sdk.PluginKindAnalyzer, Analyzer: &sdk.AnalyzerModule{ + Descriptor: Analyzer{}.Descriptor(), + New: func(_ context.Context, host sdk.HostContext) (sdk.Analyzer, error) { + return Analyzer{Logger: host.Logger()}, nil + }, + }} +} diff --git a/components/analyzers/pyreach/module_test.go b/components/analyzers/pyreach/module_test.go new file mode 100644 index 00000000..d6c54d5a --- /dev/null +++ b/components/analyzers/pyreach/module_test.go @@ -0,0 +1,96 @@ +package pyreach + +import ( + "context" + "reflect" + "testing" + + model "github.com/bomly-dev/bomly-sdk" + "github.com/bomly-dev/bomly-sdk/conformance" +) + +// TestConformance runs the SDK conformance suite against the module. No +// manifest is supplied: the analyzer ships embedded in the CLI, not as a +// packaged plugin. +func TestConformance(t *testing.T) { + conformance.Test(t, conformance.Config{Module: Module()}) +} + +// TestModuleDescriptorMatchesAnalyzer pins the module descriptor to the +// analyzer's own Descriptor so the two can never drift. +func TestModuleDescriptorMatchesAnalyzer(t *testing.T) { + if !reflect.DeepEqual(Module().Analyzer.Descriptor, Analyzer{}.Descriptor()) { + t.Fatal("module descriptor differs from Analyzer{}.Descriptor()") + } +} + +// clearAnalyzedAt blanks the wall-clock annotation timestamps so two runs of +// the same analysis compare equal. +func clearAnalyzedAt(reg *model.PackageRegistry) { + for _, pkg := range reg.All() { + for i := range pkg.Vulnerabilities { + if r := pkg.Vulnerabilities[i].Reachability; r != nil { + r.AnalyzedAt = "" + } + } + } +} + +// TestPackageUpdatesEquivalence verifies the package-updates delta protocol: +// applying the returned PackageUpdates onto a pristine copy of the input +// registry yields exactly the registry the legacy in-place path produces. +func TestPackageUpdatesEquivalence(t *testing.T) { + projectDir := newPythonProjectDir(t) + vuln := model.Vulnerability{ID: "GHSA-test", Source: "osv", ParsedSeverity: "high"} + runnerResult := RunnerResult{ + ImportedDistributions: map[string]struct{}{"requests": {}}, + SourceFiles: 1, + } + seed := func() (*model.Graph, *model.PackageRegistry) { + g, reg := newSeed() + addPyDep(t, g, reg, projectDir, "requests", "1.0.0", vuln) + addPyDep(t, g, reg, projectDir, "flask", "1.0.0", vuln) + return g, reg + } + + legacyGraph, legacyReg := seed() + legacy := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + legacyRes, err := legacy.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: legacyGraph, Registry: legacyReg, ProjectPath: projectDir, + }) + if err != nil { + t.Fatalf("legacy Analyze err: %v", err) + } + if len(legacyRes.PackageUpdates) != 0 { + t.Fatalf("legacy path returned %d package updates, want 0", len(legacyRes.PackageUpdates)) + } + if legacyRes.Registry != legacyReg { + t.Fatalf("legacy path must return the annotated request registry (got %p, want %p): plugin-boundary hosts cannot see in-place mutation", legacyRes.Registry, legacyReg) + } + + deltaGraph, deltaReg := seed() + delta := Analyzer{DisableCache: true, Runner: &fakeRunner{result: runnerResult}} + deltaRes, err := delta.Analyze(context.Background(), model.AnalyzeRequest{ + Graph: deltaGraph, Registry: deltaReg, ProjectPath: projectDir, + AcceptPackageUpdates: true, + }) + if err != nil { + t.Fatalf("delta Analyze err: %v", err) + } + if deltaRes.Registry != nil { + t.Fatal("delta path returned a full registry; want PackageUpdates only") + } + if len(deltaRes.PackageUpdates) == 0 { + t.Fatal("delta path returned no package updates") + } + + _, pristineReg := seed() + merged := model.ApplyPackageUpdates(pristineReg, deltaRes.PackageUpdates) + + clearAnalyzedAt(legacyReg) + clearAnalyzedAt(merged) + if !reflect.DeepEqual(legacyReg.All(), merged.All()) { + t.Fatalf("delta-applied registry differs from legacy registry:\nlegacy: %+v\nmerged: %+v", + legacyReg.All(), merged.All()) + } +} diff --git a/internal/analyzers/pyreach/moduletodist.go b/components/analyzers/pyreach/moduletodist.go similarity index 100% rename from internal/analyzers/pyreach/moduletodist.go rename to components/analyzers/pyreach/moduletodist.go diff --git a/internal/analyzers/pyreach/moduletodist_test.go b/components/analyzers/pyreach/moduletodist_test.go similarity index 100% rename from internal/analyzers/pyreach/moduletodist_test.go rename to components/analyzers/pyreach/moduletodist_test.go diff --git a/internal/analyzers/pyreach/runner.go b/components/analyzers/pyreach/runner.go similarity index 100% rename from internal/analyzers/pyreach/runner.go rename to components/analyzers/pyreach/runner.go diff --git a/internal/analyzers/pyreach/runner_library.go b/components/analyzers/pyreach/runner_library.go similarity index 94% rename from internal/analyzers/pyreach/runner_library.go rename to components/analyzers/pyreach/runner_library.go index a4d20b43..07f6625f 100644 --- a/internal/analyzers/pyreach/runner_library.go +++ b/components/analyzers/pyreach/runner_library.go @@ -41,9 +41,13 @@ func (libraryRunner) Name() string { return "library" } func (libraryRunner) Version() string { return runnerSchemaVersion } func (r libraryRunner) Run(ctx context.Context, projectDir string) (RunnerResult, error) { - if info, err := os.Stat(projectDir); err != nil || !info.IsDir() { + info, err := os.Stat(projectDir) + if err != nil { return RunnerResult{}, fmt.Errorf("project dir not accessible: %w", err) } + if !info.IsDir() { + return RunnerResult{}, fmt.Errorf("project dir not accessible: %q is not a directory", projectDir) + } r.logger.Debug("pyreach: executing in-process runner", zap.String("project_dir", projectDir), diff --git a/internal/analyzers/pyreach/runner_library_test.go b/components/analyzers/pyreach/runner_library_test.go similarity index 100% rename from internal/analyzers/pyreach/runner_library_test.go rename to components/analyzers/pyreach/runner_library_test.go diff --git a/internal/analyzers/pyreach/sources.go b/components/analyzers/pyreach/sources.go similarity index 100% rename from internal/analyzers/pyreach/sources.go rename to components/analyzers/pyreach/sources.go diff --git a/internal/analyzers/pyreach/testdata/project/app.py b/components/analyzers/pyreach/testdata/project/app.py similarity index 100% rename from internal/analyzers/pyreach/testdata/project/app.py rename to components/analyzers/pyreach/testdata/project/app.py diff --git a/internal/analyzers/pyreach/testdata/project/build/ignored.py b/components/analyzers/pyreach/testdata/project/build/ignored.py similarity index 100% rename from internal/analyzers/pyreach/testdata/project/build/ignored.py rename to components/analyzers/pyreach/testdata/project/build/ignored.py diff --git a/internal/analyzers/pyreach/testdata/project/dynamic.py b/components/analyzers/pyreach/testdata/project/dynamic.py similarity index 100% rename from internal/analyzers/pyreach/testdata/project/dynamic.py rename to components/analyzers/pyreach/testdata/project/dynamic.py diff --git a/internal/analyzers/pyreach/testdata/project/pkg/helpers.py b/components/analyzers/pyreach/testdata/project/pkg/helpers.py similarity index 100% rename from internal/analyzers/pyreach/testdata/project/pkg/helpers.py rename to components/analyzers/pyreach/testdata/project/pkg/helpers.py diff --git a/internal/analyzers/pyreach/testdata/project/pyproject.toml b/components/analyzers/pyreach/testdata/project/pyproject.toml similarity index 100% rename from internal/analyzers/pyreach/testdata/project/pyproject.toml rename to components/analyzers/pyreach/testdata/project/pyproject.toml diff --git a/internal/analyzers/pyreach/testdata/requirements-project/requirements-prod.txt b/components/analyzers/pyreach/testdata/requirements-project/requirements-prod.txt similarity index 100% rename from internal/analyzers/pyreach/testdata/requirements-project/requirements-prod.txt rename to components/analyzers/pyreach/testdata/requirements-project/requirements-prod.txt diff --git a/internal/analyzers/pyreach/testdata/requirements-project/src/app.py b/components/analyzers/pyreach/testdata/requirements-project/src/app.py similarity index 100% rename from internal/analyzers/pyreach/testdata/requirements-project/src/app.py rename to components/analyzers/pyreach/testdata/requirements-project/src/app.py diff --git a/internal/analyzers/pyreach/testdata/static-project/app.py b/components/analyzers/pyreach/testdata/static-project/app.py similarity index 100% rename from internal/analyzers/pyreach/testdata/static-project/app.py rename to components/analyzers/pyreach/testdata/static-project/app.py diff --git a/internal/analyzers/pyreach/testdata/static-project/dist/ignored.py b/components/analyzers/pyreach/testdata/static-project/dist/ignored.py similarity index 100% rename from internal/analyzers/pyreach/testdata/static-project/dist/ignored.py rename to components/analyzers/pyreach/testdata/static-project/dist/ignored.py diff --git a/internal/analyzers/pyreach/testdata/static-project/setup.cfg b/components/analyzers/pyreach/testdata/static-project/setup.cfg similarity index 100% rename from internal/analyzers/pyreach/testdata/static-project/setup.cfg rename to components/analyzers/pyreach/testdata/static-project/setup.cfg diff --git a/internal/analyzers/pyreach/testdata_test.go b/components/analyzers/pyreach/testdata_test.go similarity index 100% rename from internal/analyzers/pyreach/testdata_test.go rename to components/analyzers/pyreach/testdata_test.go diff --git a/dev-docs/ARCHITECTURE.md b/dev-docs/ARCHITECTURE.md index 06228ecf..5479d4d1 100644 --- a/dev-docs/ARCHITECTURE.md +++ b/dev-docs/ARCHITECTURE.md @@ -561,6 +561,23 @@ The former `internal/system`, `internal/matchers/cache`, and `internal/testutil` Do not reintroduce CLI-internal copies of these helpers; new shared helper code goes into the appropriate SDK subpackage. +### Decision: built-in analyzers are nested component modules consumed via replace directives + +Wave 1 of the component-extraction program moved the four reachability analyzers from `internal/analyzers/` to `components/analyzers//`, each a nested Go module (`github.com/bomly-dev/bomly-cli/components/analyzers/`) that depends only on the released SDK plus its own third-party dependencies. Each module exports `Module() sdk.Module`; `internal/composition`'s analyzer entries delegate to it, so entry names, kinds, and default-enabled flags are unchanged and the registry wiring is untouched. + +The root module consumes the component modules with a `require` + directory `replace` pair per module: + +``` +require github.com/bomly-dev/bomly-cli/components/analyzers/ v0.0.0 +replace github.com/bomly-dev/bomly-cli/components/analyzers/ => ./components/analyzers/ +``` + +Two consumption models were evaluated. The first — workspace-only consumption (committed `go.work`, no root requires) plus an automated post-merge release train that tags component modules and lands root pins before every root release tag — keeps `go.mod` replace-free and preserves remote `go install`, but at the cost of a pinned-build guard with a skip window, a workspace-unaware `go mod tidy` needing conditional CI logic, and a release pipeline whose correctness depends on strict tag ordering. The final decision takes the `replace` model for simplicity: every build — local, CI, GoReleaser — resolves the components from the checkout itself, `go mod tidy` works normally, there is no unpinned window or ordering constraint, and no committed `go.work` is needed. The accepted trade-off is that `go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest` no longer works: Go refuses remote installs of modules whose `go.mod` carries `replace` directives. Users install from release archives, package managers, or a clone; the docs say so explicitly. + +CI enforces a replace allowlist in the modules-drift job (via `go mod edit -json`, which catches block-form directives too): the root `go.mod` may only replace in-repo `./components/` paths, and component `go.mod` files may contain no replaces at all. + +The analyzers also adopted the SDK package-updates delta protocol (`CapabilityPackageUpdates`): every annotation they write is filling `Vulnerability.Reachability` on existing `(Source, ID)`-keyed registry vulnerabilities, which is exactly what `Package.MergeFrom` expresses, so when the host sets `AcceptPackageUpdates` they return only the packages they touched. The legacy full-registry path returns the annotated request registry (required across the managed-plugin process boundary, where in-place mutation is invisible), and per-module equivalence tests pin delta-applied output to the legacy output. The one in-place behavior the merge cannot express — replacing a `Reachability` written by a *different* analyzer — cannot occur between built-ins because analyzer dispatch is language-disjoint. + ### Decision: syft-JSON SBOM ingest is removed; sniffing is retained for the migration error Syft's proprietary JSON SBOM format is no longer an accepted `--sbom` ingest input. It had exactly one consumer in the codebase — the SBOM ingest detector — while the syft detector itself always shells out with `-o spdx-json`. The lite build (`bomly_external_syft`) never actually ingested it either: its fallback re-ran the generic decoder, which returned a nil document for the syft target, so `ToGraph(nil)` hard-failed with an unhelpful `sbom document is nil` error. The change therefore unifies full and lite behavior on one explicit, actionable rejection; the compatibility impact is on full builds only, which previously decoded the format. Removing the decode path made `internal/detectors/sbom` build-tag-free and dropped its `anchore/syft` dependency. @@ -654,7 +671,7 @@ Cache failures are non-fatal. The command should warn and continue rather than f | `internal/detectors` | Detector contracts and ecosystem implementations | | `internal/auditors` | Policy evaluators and finding creation | | `internal/baseline` | Portable package-finding baseline codec and audit policy-status resolver | -| `internal/analyzers` | Reachability analyzers (govulncheck for Go, jsreach for JS/TS, pyreach for Python, jvmreach for JVM languages) that annotate `sdk.Vulnerability.Reachability` on registry packages | +| `components/analyzers` | Reachability analyzers as nested component modules (govulncheck for Go, jsreach for JS/TS, pyreach for Python, jvmreach for JVM languages) that annotate `sdk.Vulnerability.Reachability` on registry packages | | `internal/matchers` | Matcher contracts plus shared enrichment helpers used by built-in matchers | | `internal/engine/diff` | Diff pipeline orchestration and audit delta classification | | `internal/engine/explain` | Dependency path traversal | diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 6af98a77..f0a98cbf 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -16,12 +16,6 @@ On Windows: winget install Bomly.BomlyCLI ``` -If you have Go on `PATH`: - -```bash -go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest -``` - Or download a prebuilt archive from [GitHub Releases](https://github.com/bomly-dev/bomly-cli/releases) and put `bomly` on your `PATH`. See [Installation](INSTALLATION.md) for Linux packages, Scoop, checksums, and pinned versions. Verify: ```bash diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 29ae66af..c87d8a77 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -163,15 +163,18 @@ Expand-Archive -Path $archive -DestinationPath . Each archive also contains `LICENSE`, `NOTICE`, and a `licenses/` directory with third-party license text. -### `go install` +### Build from source -Use this path if you already have Go on `PATH`: +Remote `go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest` is not supported: the repository hosts its built-in components as nested modules wired up with `replace` directives, and `go install` refuses remote builds of modules that use `replace`. Clone and build instead if you have Go on `PATH`: ```bash -go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest +git clone https://github.com/bomly-dev/bomly-cli.git +cd bomly-cli +go build -o bomly ./cmd/bomly +# Move `bomly` somewhere on your PATH. ``` -`go install` builds the full Bomly binary with builtin Syft and Grype support. It does not install `bomly-lite`. +This builds the full Bomly binary with builtin Syft and Grype support. Add `-tags "bomly_external_syft,bomly_external_grype"` to build `bomly-lite` instead. ## `bomly` vs `bomly-lite` @@ -268,7 +271,7 @@ Use the package manager that installed Bomly: - WinGet: `winget upgrade Bomly.BomlyCLI` - Scoop: `scoop update bomly` - Linux packages: install the newer package artifact with your system package manager. -- Go: re-run `go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest`. +- Built from source: `git pull` the clone and rebuild (`go build -o bomly ./cmd/bomly`). - Install script: re-run the same script, optionally with `BOMLY_VERSION`. Check the current version before and after: diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index 3bc311a0..3dcfbdd5 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -33,7 +33,7 @@ SARIF 2.1.0 output uploads to the GitHub Security tab (and any other SARIF-aware ## Package managers and distribution -Official install channels: Homebrew tap (`bomly-dev/tap`), WinGet (`Bomly.BomlyCLI`), Scoop, install scripts (`bomly.dev/install.sh`), Linux packages, `go install`, and prebuilt release archives with checksums and SLSA provenance. +Official install channels: Homebrew tap (`bomly-dev/tap`), WinGet (`Bomly.BomlyCLI`), Scoop, install scripts (`bomly.dev/install.sh`), Linux packages, source builds from a clone, and prebuilt release archives with checksums and SLSA provenance. → [Installation](INSTALLATION.md) ## Plugins diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index a0348207..5a35eb48 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -98,11 +98,7 @@ curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin curl -sSfL https://get.anchore.io/grype | sh -s -- -b /usr/local/bin ``` -Or switch to the full `bomly` binary (Syft and Grype linked in): - -```bash -go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest -``` +Or switch to the full `bomly` binary (Syft and Grype linked in): install it from [a release archive or package manager](INSTALLATION.md). Remote `go install ...@latest` is not supported for this repository. ## "plugin protocol mismatch" diff --git a/docs/detectors/maven/maven.md b/docs/detectors/maven/maven.md index cf10f7a7..e3a952af 100644 --- a/docs/detectors/maven/maven.md +++ b/docs/detectors/maven/maven.md @@ -109,7 +109,7 @@ For Maven packages, the analyzer is `jvmreach` at **Tier-3 (package)**. It walks For multi-module reactors, `jvmreach` reads parent `` declarations recursively and follows source namespace imports between consumed sibling modules before attributing external artifacts. -If a missing prefix produces a false-negative for a direct import, add the mapping to `internal/analyzers/jvmreach/prefixmap.go` (one-line PR). +If a missing prefix produces a false-negative for a direct import, add the mapping to `components/analyzers/jvmreach/prefixmap.go` (one-line PR). ## Limitations diff --git a/docs/detectors/python/pip.md b/docs/detectors/python/pip.md index be5706e6..6ec9c018 100644 --- a/docs/detectors/python/pip.md +++ b/docs/detectors/python/pip.md @@ -112,7 +112,7 @@ installs. Missing or malformed source data stays unknown. > **Experimental.** Reachability is opt-in via `--analyze`. The feature is stable in shape but may evolve; ecosystem coverage is expanding. -For pip-managed packages, the analyzer is `pyreach` at **Tier-3 (package)**. It walks every `.py` file under the project root, records imports, and maps module names to PyPI distribution names. See [REACHABILITY.md](../../REACHABILITY.md#unreachable-is-not-safe) and the module-to-distribution map in `internal/analyzers/pyreach/moduletodist.go`. +For pip-managed packages, the analyzer is `pyreach` at **Tier-3 (package)**. It walks every `.py` file under the project root, records imports, and maps module names to PyPI distribution names. See [REACHABILITY.md](../../REACHABILITY.md#unreachable-is-not-safe) and the module-to-distribution map in `components/analyzers/pyreach/moduletodist.go`. ## Limitations diff --git a/go.mod b/go.mod index 4ff959fe..66d3abe1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 - github.com/evanw/esbuild v0.28.1 github.com/github/go-spdx/v2 v2.7.0 github.com/glebarez/sqlite v1.11.0 github.com/hashicorp/go-hclog v1.6.3 @@ -25,13 +24,14 @@ require ( github.com/spf13/pflag v1.0.10 go.uber.org/zap v1.28.0 golang.org/x/term v0.45.0 - golang.org/x/vuln v1.6.0 google.golang.org/grpc v1.83.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/evanw/esbuild v0.28.1 // indirect golang.org/x/net v0.57.0 // indirect + golang.org/x/vuln v1.6.0 // indirect google.golang.org/protobuf v1.36.12 // indirect ) @@ -105,6 +105,10 @@ require ( github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/bomly-dev/bomly-cli/components/analyzers/govulncheck v0.0.0 + github.com/bomly-dev/bomly-cli/components/analyzers/jsreach v0.0.0 + github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach v0.0.0 + github.com/bomly-dev/bomly-cli/components/analyzers/pyreach v0.0.0 github.com/bomly-dev/bomly-sdk v0.3.0 github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect @@ -327,3 +331,11 @@ require ( modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.54.0 // indirect ) + +replace github.com/bomly-dev/bomly-cli/components/analyzers/govulncheck => ./components/analyzers/govulncheck + +replace github.com/bomly-dev/bomly-cli/components/analyzers/jsreach => ./components/analyzers/jsreach + +replace github.com/bomly-dev/bomly-cli/components/analyzers/pyreach => ./components/analyzers/pyreach + +replace github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach => ./components/analyzers/jvmreach diff --git a/go.work b/go.work deleted file mode 100644 index 08f3c018..00000000 --- a/go.work +++ /dev/null @@ -1,3 +0,0 @@ -go 1.26.3 - -use . diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index 1887531d..00000000 --- a/go.work.sum +++ /dev/null @@ -1,335 +0,0 @@ -cloud.google.com/go/accessapproval v1.8.8/go.mod h1:RFwPY9JDKseP4gJrX1BlAVsP5O6kI8NdGlTmaeDefmk= -cloud.google.com/go/accesscontextmanager v1.9.7/go.mod h1:i6e0nd5CPcrh7+YwGq4bKvju5YB9sgoAip+mXU73aMM= -cloud.google.com/go/aiplatform v1.114.0/go.mod h1:W5yMrpIuHG/CSK8iF7XnwIfCJu6dcLRQ0cTqGR5vwwE= -cloud.google.com/go/analytics v0.30.1/go.mod h1:V/FnINU5kMOsttZnKPnXfKi6clJUHTEXUKQjHxcNK8A= -cloud.google.com/go/apigateway v1.7.7/go.mod h1:j1bCmrUK1BzVHpiIyTApxB7cRyhivKzltqLmp6j6i7U= -cloud.google.com/go/apigeeconnect v1.7.7/go.mod h1:ftGK3nca0JePiVLl0A6alaMjKdOc5C+sAkFMyH2RH8U= -cloud.google.com/go/apigeeregistry v0.10.0/go.mod h1:SAlF5OhKvyLDuwWAaFAIVJjrEqKRrGTPkJs+TWNnSqg= -cloud.google.com/go/appengine v1.9.7/go.mod h1:y1XpGVeAhbsNzHida79cHbr3pFRsym0ob8xnC8yphbo= -cloud.google.com/go/area120 v0.9.7/go.mod h1:5nJ0yksmjOMfc4Zpk+okWfJ3A1004FvB82rfia+ZLaY= -cloud.google.com/go/artifactregistry v1.19.0/go.mod h1:UEAPCgHDFC1q+A8nnVxXHPEy9KCVOeavFBF1fEChQvU= -cloud.google.com/go/asset v1.22.0/go.mod h1:q80JP2TeWWzMCazYnrAfDf36aQKf1QiKzzpNLflJwf8= -cloud.google.com/go/assuredworkloads v1.13.0/go.mod h1:o/oHEOnUlribR+uJWTKQo8A5RhSl9K9FNeMOew4TJ3M= -cloud.google.com/go/automl v1.15.0/go.mod h1:U9zOtQb8zVrFNGTuW3BfxeqmLyeleLgT9B12EaXfODg= -cloud.google.com/go/baremetalsolution v1.4.0/go.mod h1:K6C6g4aS8LW95I0fEHZiBsBlh0UxwDLGf+S/vyfXbvg= -cloud.google.com/go/batch v1.14.0/go.mod h1:oeQveyG6NDS/ks2ilOP4LzKRmuIaI7GLe0CkR7WF6pk= -cloud.google.com/go/beyondcorp v1.2.0/go.mod h1:sszcgxpPPBEfLzbI0aYCTg6tT1tyt3CmKav3NZIUcvI= -cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= -cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= -cloud.google.com/go/billing v1.21.0/go.mod h1:ZGairB3EVnb3i09E2SxFxo50p5unPaMTuo1jh6jW9js= -cloud.google.com/go/binaryauthorization v1.10.0/go.mod h1:WOuiaQkI4PU/okwrcREjSAr2AUtjQgVe+PlrXKOmKKw= -cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= -cloud.google.com/go/channel v1.21.0/go.mod h1:8v3TwHtgLmFxTpL2U+e10CLFOQN8u/Vr9RhYcJUS3y8= -cloud.google.com/go/cloudbuild v1.25.0/go.mod h1:lCu+T6IPkobPo2Nw+vCE7wuaAl9HbXLzdPx/tcF+oWo= -cloud.google.com/go/clouddms v1.8.8/go.mod h1:QtCyw+a73dlkDb2q20aTAPvfaTZCepDDi6Gb1AKq0a4= -cloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4= -cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w= -cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= -cloud.google.com/go/contactcenterinsights v1.17.4/go.mod h1:kZe6yOnKDfpPz2GphDHynxk/Spx+53UX/pGf+SmWAKM= -cloud.google.com/go/container v1.45.0/go.mod h1:eB6jUfJLjne9VsTDGcH7mnj6JyZK+KOUIA6KZnYE/ds= -cloud.google.com/go/containeranalysis v0.14.2/go.mod h1:FjppROiUtP9cyMegdWdY/TsBSGc6kqh1GjA2NOJXXL8= -cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= -cloud.google.com/go/dataflow v0.11.1/go.mod h1:3s6y/h5Qz7uuxTmKJKBifkYZ3zs63jS+6VGtSu8Cf7Y= -cloud.google.com/go/dataform v0.12.1/go.mod h1:atGS8ReRjfNDUQib0X/o/7Gi2bqHI2G7/J86LKiGimE= -cloud.google.com/go/datafusion v1.8.7/go.mod h1:4dkFb1la41qCEXh1AzYtFwl842bu2ikTUXyKhjvFCb0= -cloud.google.com/go/datalabeling v0.9.7/go.mod h1:EEUVn+wNn3jl19P2S13FqE1s9LsKzRsPuuMRq2CMsOk= -cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA= -cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= -cloud.google.com/go/dataqna v0.9.8/go.mod h1:2lHKmGPOqzzuqCc5NI0+Xrd5om4ulxGwPpLB4AnFgpA= -cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= -cloud.google.com/go/datastream v1.15.1/go.mod h1:aV1Grr9LFon0YvqryE5/gF1XAhcau2uxN2OvQJPpqRw= -cloud.google.com/go/deploy v1.27.3/go.mod h1:7LFIYYTSSdljYRqY3n+JSmIFdD4lv6aMD5xg0crB5iw= -cloud.google.com/go/dialogflow v1.74.0/go.mod h1:jlKHmd3/KdvWWhGZjoCnWQAQNOMHOhDK6DQ430p3T1I= -cloud.google.com/go/dlp v1.28.0/go.mod h1:C3od1fIK8lf7Kr62aU1Uh0z4OL5Z8s3do3znAiEupAw= -cloud.google.com/go/documentai v1.39.0/go.mod h1:KmlLO93F7GRU8dENXRxvt+7V8o7eCG6Y6WDitKbcYJs= -cloud.google.com/go/domains v0.10.7/go.mod h1:T3WG/QUAO/52z4tUPooKS8AY7yXaFxPYn1V3F0/JbNQ= -cloud.google.com/go/edgecontainer v1.4.4/go.mod h1:yyNVHsCKtsX/0mqFdbljQw0Uo660q2dlMPaiqYiC2Tg= -cloud.google.com/go/errorreporting v0.4.0/go.mod h1:dZGEhqzdHZSRxxWLVjC3Ue5CVaROzvP58D9rU6zbBfw= -cloud.google.com/go/essentialcontacts v1.7.7/go.mod h1:ytycWAEn/aKUMRKQPMVgMrAtphEMgjbzL8vFwM3tqXs= -cloud.google.com/go/eventarc v1.18.0/go.mod h1:/6SDoqh5+9QNUqCX4/oQcJVK16fG/snHBSXu7lrJtO8= -cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= -cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= -cloud.google.com/go/functions v1.19.7/go.mod h1:xbcKfS7GoIcaXr2FSwmtn9NXal1JR4TV6iYZlgXffwA= -cloud.google.com/go/gkebackup v1.8.1/go.mod h1:GAaAl+O5D9uISH5MnClUop2esQW4pDa2qe/95A4l7YQ= -cloud.google.com/go/gkeconnect v0.12.5/go.mod h1:wMD2RXcsAWlkREZWJDVeDV70PYka1iEb9stFmgpw+5o= -cloud.google.com/go/gkehub v0.16.0/go.mod h1:ADp27Ucor8v81wY+x/5pOxTorxkPj/xswH3AUpN62GU= -cloud.google.com/go/gkemulticloud v1.6.0/go.mod h1:bGpd4o/Z5Z/XFlaojkgdVisHRwb+fLJvUPzsmV0I9ok= -cloud.google.com/go/gsuiteaddons v1.7.8/go.mod h1:DBKNHH4YXAdd/rd6zVvtOGAJNGo0ekOh+nIjTUDEJ5U= -cloud.google.com/go/iap v1.11.3/go.mod h1:+gXO0ClH62k2LVlfhHzrpiHQNyINlEVmGAE3+DB4ShU= -cloud.google.com/go/ids v1.5.7/go.mod h1:N3ZQOIgIBwwOu2tzyhmh3JDT+kt8PcoKkn2BRT9Qe4A= -cloud.google.com/go/iot v1.8.7/go.mod h1:HvVcypV8LPv1yTXSLCNK+YCtqGHhq+p0F3BXETfpN+U= -cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= -cloud.google.com/go/language v1.14.6/go.mod h1:7y3J9OexQsfkWNGCxhT+7lb64pa60e12ZCoWDOHxJ1M= -cloud.google.com/go/lifesciences v0.10.7/go.mod h1:v3AbTki9iWttEls/Wf4ag3EqeLRHofploOcpsLnu7iY= -cloud.google.com/go/managedidentities v1.7.7/go.mod h1:nwNlMxtBo2YJMvsKXRtAD1bL41qiCI9npS7cbqrsJUs= -cloud.google.com/go/maps v1.26.0/go.mod h1:+auempdONAP8emtm48aCfNo1ZC+3CJniRA1h8J4u7bY= -cloud.google.com/go/mediatranslation v0.9.7/go.mod h1:mz3v6PR7+Fd/1bYrRxNFGnd+p4wqdc/fyutqC5QHctw= -cloud.google.com/go/memcache v1.11.7/go.mod h1:AU1jYlUqCihxapcJ1GGMtlMWDVhzjbfUWBXqsXa4rBg= -cloud.google.com/go/metastore v1.14.8/go.mod h1:h1XI2LpD4ohJhQYn9TwXqKb5sVt6KSo47ft96SiFF1s= -cloud.google.com/go/networkconnectivity v1.20.0/go.mod h1:9MzGwD4ljiq+Z2Pg3ue27OEewCuHz7IUfw1fITrIdSw= -cloud.google.com/go/networkmanagement v1.21.0/go.mod h1:clG/5Yt0wQ57qSH6Yh7oehQYlobHw3F6nb3Pn4ig5hU= -cloud.google.com/go/networksecurity v0.11.0/go.mod h1:JLgDsg4tOyJ3eMO8lypjqMftbfd60SJ+P7T+DUmWBsM= -cloud.google.com/go/notebooks v1.12.7/go.mod h1:uR9pxAkKmlNloibMr9Q1t8WhIu4P2JeqJs7c064/0Mo= -cloud.google.com/go/optimization v1.7.7/go.mod h1:OY2IAlX23o52qwMAZ0w65wibKuV12a4x6IHDTCq6kcU= -cloud.google.com/go/orchestration v1.11.10/go.mod h1:tz7m1s4wNEvhNNIM3JOMH0lYxBssu9+7si5MCPw/4/0= -cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0= -cloud.google.com/go/osconfig v1.15.1/go.mod h1:NegylQQl0+5m+I+4Ey/g3HGeQxKkncQ1q+Il4DZ8PME= -cloud.google.com/go/oslogin v1.14.7/go.mod h1:NB6NqBHfDMwznePdBVX+ILllc1oPCdNSGp5u/WIyndY= -cloud.google.com/go/phishingprotection v0.9.7/go.mod h1:JTI4HNGyAbWolBoNOoCyCF0e3cqPNrYnlievHU49EwE= -cloud.google.com/go/policytroubleshooter v1.11.7/go.mod h1:JP/aQ+bUkt4Gz6lQXBi/+A/6nyNRZ0Pvxui5Xl9ieyk= -cloud.google.com/go/privatecatalog v0.10.8/go.mod h1:BkLHi+rtAGYBt5DocXLytHhF0n6F03Tegxgty40Y7aA= -cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= -cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= -cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= -cloud.google.com/go/recaptchaenterprise/v2 v2.21.0/go.mod h1:HxQYqZC2/zl2CvKN7jJEv71vEdDi1GMGNUiZxnpiuVI= -cloud.google.com/go/recommendationengine v0.9.7/go.mod h1:snZ/FL147u86Jqpv1j95R+CyU5NvL/UzYiyDo6UByTM= -cloud.google.com/go/recommender v1.13.6/go.mod h1:y5/5womtdOaIM3xx+76vbsiA+8EBTIVfWnxHDFHBGJM= -cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4= -cloud.google.com/go/resourcemanager v1.10.7/go.mod h1:rScGkr6j2eFwxAjctvOP/8sqnEpDbQ9r5CKwKfomqjs= -cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= -cloud.google.com/go/retail v1.25.1/go.mod h1:J75G8pd+DH0SHueL9IJw7Y5d2VhTsjFsk+F1t9f8jXc= -cloud.google.com/go/run v1.15.0/go.mod h1:rgFHMdAopLl++57vzeqA+a1o2x0/ILZnEacRD6nC0EA= -cloud.google.com/go/scheduler v1.11.8/go.mod h1:bNKU7/f04eoM6iKQpwVLvFNBgGyJNS87RiFN73mIPik= -cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= -cloud.google.com/go/security v1.19.2/go.mod h1:KXmf64mnOsLVKe8mk/bZpU1Rsvxqc0Ej0A6tgCeN93w= -cloud.google.com/go/securitycenter v1.38.1/go.mod h1:Ge2D/SlG2lP1FrQD7wXHy8qyeloRenvKXeB4e7zO6z0= -cloud.google.com/go/servicedirectory v1.12.7/go.mod h1:gOtN+qbuCMH6tj2dqlDY3qQL7w3V0+nkWaZElnJK8Ps= -cloud.google.com/go/shell v1.8.7/go.mod h1:OTke7qc3laNEW5Jr5OV9VR3IwU5x5VqGOE6705zFex4= -cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= -cloud.google.com/go/speech v1.29.0/go.mod h1:wtUmIS/h0ZYU6cPA9klcyST3f6i2FdnvNDqENjrRDds= -cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= -cloud.google.com/go/talent v1.8.4/go.mod h1:3yukBXUTVFNyKcJpUExW/k5gqEy8qW6OCNj7WdN0MWo= -cloud.google.com/go/texttospeech v1.16.0/go.mod h1:AeSkoH3ziPvapsuyI07TWY4oGxluAjntX+pF4PJ2jy0= -cloud.google.com/go/tpu v1.8.4/go.mod h1:ul0cyWSHr6jHGZYElZe6HvQn35VY93RAlwpDiSBRnPA= -cloud.google.com/go/translate v1.12.7/go.mod h1:wwJp14NZyWvcrFANhIXutXj0pOBkYciBHwSlUOykcjI= -cloud.google.com/go/video v1.27.1/go.mod h1:xzfAC77B4vtnbi/TT3UUxEjCa/+Ehy5EA8w470ytOig= -cloud.google.com/go/videointelligence v1.12.7/go.mod h1:XAk5hCMY+GihxJ55jNoMdwdXSNZnCl3wGs2+94gK7MA= -cloud.google.com/go/vision/v2 v2.9.6/go.mod h1:lJC+vP15D5znJvHQYjEoTKnpToX1L93BUlvBmzM0gyg= -cloud.google.com/go/vmmigration v1.10.0/go.mod h1:LDztCWEb+RwS1bPg4Xzt0fcJS9kVrFxa3ejhH7OW9vg= -cloud.google.com/go/vmwareengine v1.3.6/go.mod h1:ps0rb+Skgpt9ppHYC0o5DqtJ5ld2FyS8sAqtbHH8t9s= -cloud.google.com/go/vpcaccess v1.8.7/go.mod h1:9RYw5bVvk4Z51Rc8vwXT63yjEiMD/l7XyEaDyrNHgmk= -cloud.google.com/go/webrisk v1.11.2/go.mod h1:yH44GeXz5iz4HFsIlGeoVvnjwnmfbni7Lwj1SelV4f0= -cloud.google.com/go/websecurityscanner v1.7.7/go.mod h1:ng/PzARaus3Bj4Os4LpUnyYHsbtJky1HbBDmz148v1o= -cloud.google.com/go/workflows v1.14.3/go.mod h1:CC9+YdVI2Kvp0L58WajHpEfKJxhrtRh3uQ0SYWcmAk4= -codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= -codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= -codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= -cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= -git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Microsoft/cosesign1go v1.4.0/go.mod h1:1La/HcGw19rRLhPW0S6u55K6LKfti+GQSgGCtrfhVe8= -github.com/Microsoft/didx509go v0.0.3/go.mod h1:wWt+iQsLzn3011+VfESzznLIp/Owhuj7rLF7yLglYbk= -github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s= -github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0= -github.com/anchore/bubbly v0.2.1/go.mod h1:o6a9aH3/fCn+mb5g1T1tBbQZ4hGR0yO5/uD1d2Vtago= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.2/go.mod h1:dLREOeW66eVaaGIOi2ZlLHDgkR3nuJ02rd00j0YSlBE= -github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3/go.mod h1:vBfBu24Ka3/5UZtepbTV0gnc9VPLT8ok+0oDDaYAzn4= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.10/go.mod h1:Diyyyz0b43X13pdi1mVMqlTwDjOmRbJMvDsqnduUYWM= -github.com/aws/aws-sdk-go-v2/service/iam v1.53.6/go.mod h1:RJNVc52A0K41fCDJOnsCLeWJf8mwa0q30fM3CfE9U18= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.20/go.mod h1:ihZMtPTKoX/ugQRHbui6zNdSgVYN1KY2Dgwb2d3hXlc= -github.com/aws/aws-sdk-go-v2/service/sns v1.39.13/go.mod h1:RwF6Xnba8PlINxJUQq1IAWeon6IglvqsnhNqV8QsQjk= -github.com/aws/aws-sdk-go-v2/service/sqs v1.42.24/go.mod h1:Ql9ziDutk8ERAN9HMaYANCW3lop451ppebkxEJMLCTM= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.12.0/go.mod h1:046/oLyFlYdAghYQE2yHXi/E//VM5Cf3/dFmA+3CZ0c= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/carabiner-dev/attestation v0.2.1/go.mod h1:O84vF84RZG3pJO/6BYrPs718bZviHF5DKajP1HsrDpw= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/checkpoint-restore/checkpointctl v1.5.0/go.mod h1:y5HRs1ZWQUZGyEuthlTHmTJN9PUMOjlaH6JvVaNq9kE= -github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI= -github.com/checkpoint-restore/go-criu/v7 v7.2.0/go.mod h1:u0LCWLg0w4yqqu14aXhiB4YD3a1qd8EcCEg7vda5dwo= -github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= -github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/containerd/btrfs/v2 v2.0.0/go.mod h1:swkD/7j9HApWpzl8OHfrHNxppPd9l44DFZdF94BUj9k= -github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI= -github.com/containerd/go-dmverity v0.1.0/go.mod h1:vYevYgfeVF244IpQCKshXLvyKoRHwoVrFG0HV6GrUrs= -github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U= -github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= -github.com/containerd/nri v0.12.0/go.mod h1:TGAfPLH4a+qwbv0PxsefPiR+PobYecDj2aXMtz7GQcg= -github.com/containerd/otelttrpc v0.1.0/go.mod h1:XhoA2VvaGPW1clB2ULwrBZfXVuEWuyOd2NUD1IM0yTg= -github.com/containerd/protobuild v0.3.0/go.mod h1:5mNMFKKAwCIAkFBPiOdtRx2KiQlyEJeMXnL5R1DsWu8= -github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containerd/zfs/v2 v2.0.0/go.mod h1:fnUDKF98iYuQqLvNdoXs9MXjtfhRWp1nxSgRf7VZH8s= -github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= -github.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c= -github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= -github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/deiu/gon3 v0.0.0-20241212124032-93153c038193/go.mod h1:EdezkFZtCJELxMo+YIX5B5i5ofz9U+n+xSxWku6mOS0= -github.com/deiu/rdf2go v0.0.0-20241212211204-b661ba0dfd25/go.mod h1:AAL3UBTBShUaH3y68LyhlSjz6S6DoHoMSpAWvnCiTCs= -github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/certtostore v1.0.6/go.mod h1:2N0ZPLkGvQWhYvXaiBGq02r71fnSLfq78VKIWQHr1wo= -github.com/google/deck v0.0.0-20230104221208-105ad94aa8ae/go.mod h1:DoDv8G58DuLNZF0KysYn0bA/6ZWhmRW3fZE2VnGEH0w= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= -github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/intel/goresctrl v0.12.0/go.mod h1:5GWtmPY4BWl/a9rU8apGED9Xul5b5WoLtg/qOWaghWU= -github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= -github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= -github.com/josephspurrier/goversioninfo v1.5.0/go.mod h1:6MoTvFZ6GKJkzcdLnU5T/RGYUbHQbKpYeNP0AgQLd2o= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= -github.com/knqyf263/go-plugin v0.9.0/go.mod h1:2z5lCO1/pez6qGo8CvCxSlBFSEat4MEp1DrnA+f7w8Q= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326/go.mod h1:nfqkuSNlsk1bvti/oa7TThx4KmRMBmSxf3okHI9wp3E= -github.com/linuxkit/virtsock v0.0.0-20241009230534-cb6a20cc0422/go.mod h1:JLgfq4XMVbvfNlAXla/41lZnp21O72a/wWHGJefAvgQ= -github.com/lyft/protoc-gen-star/v2 v2.0.4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= -github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= -github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= -github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI= -github.com/opencontainers/cgroups v0.0.4/go.mod h1:s8lktyhlGUqM7OSRL5P7eAW6Wb+kWPNvt4qvVfzA5vs= -github.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= -github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= -github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94= -github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -github.com/rychipman/easylex v0.0.0-20160129204217-49ee7767142f/go.mod h1:MZ2GRTcqmve6EoSbErWgCR+Ash4p8Gc5esHe8MDErss= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= -github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M= -github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= -github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= -github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= -github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= -github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= -github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= -github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis= -go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= -go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.67.0/go.mod h1:xOd0/OgHjAtW47zPn48sC7n/pUxunDQfDc9qG3ZtSn0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= -gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= -gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20260226221140-a57be14db171/go.mod h1:9amqk/8LQWEC4RjyUxMx1DebyQ7hZB9gvl67bHmgZ2E= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= -google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= -gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= -k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= -k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= -k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk= -k8s.io/cri-api v0.36.0/go.mod h1:1gMX7udEAiRCWGS4uxscdbxq6vufwhZt38Ri+XH6P00= -k8s.io/cri-client v0.36.0/go.mod h1:sMNSZqkBxzc/8IqPQyVg+QaKnntLn6bnP5xjOQ9OX6U= -k8s.io/cri-streaming v0.36.0/go.mod h1:AGYm+qv2gm7CTj9Gotc6CxPy7xEvyJGVvc3RhWNK5NQ= -k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= -k8s.io/streaming v0.36.0/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= -k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= -modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= -mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= -rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= -tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= diff --git a/internal/analyzers/govulncheck/testdata/govulncheck.json b/internal/analyzers/govulncheck/testdata/govulncheck.json deleted file mode 100644 index bf035a3a..00000000 --- a/internal/analyzers/govulncheck/testdata/govulncheck.json +++ /dev/null @@ -1,7 +0,0 @@ -{"config":{"protocol_version":"v1.0.0"}} -{"progress":{"message":"loaded packages"}} -{"osv":{"id":"GO-2024-1234","aliases":["CVE-2024-1234","GHSA-aaaa-bbbb-cccc"],"summary":"fixture advisory"}} -{"finding":{"osv":"GO-2024-1234","fixed_version":"v1.2.3","trace":[{"module":"example.com/app","package":"main","function":"main","position":{"filename":"main.go","line":12,"column":4}},{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","receiver":"*Decoder","position":{"filename":"decode.go","line":99}}]}} -{malformed fixture record} -{"finding":{"osv":"GO-2024-1234","trace":[{"module":"example.com/app","package":"main","function":"handler","position":{"filename":"handler.go","line":7}},{"module":"github.com/foo/bar","version":"v1.0.0","package":"github.com/foo/bar","function":"Decode","receiver":"*Decoder","position":{"filename":"decode.go","line":99}}]}} -{"finding":{"osv":"GO-2024-9999"}} diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 2ddfdfdf..f9212972 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -11,10 +11,10 @@ import ( "fmt" "time" - "github.com/bomly-dev/bomly-cli/internal/analyzers/govulncheck" - "github.com/bomly-dev/bomly-cli/internal/analyzers/jsreach" - "github.com/bomly-dev/bomly-cli/internal/analyzers/jvmreach" - "github.com/bomly-dev/bomly-cli/internal/analyzers/pyreach" + "github.com/bomly-dev/bomly-cli/components/analyzers/govulncheck" + "github.com/bomly-dev/bomly-cli/components/analyzers/jsreach" + "github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach" + "github.com/bomly-dev/bomly-cli/components/analyzers/pyreach" "github.com/bomly-dev/bomly-cli/internal/matchers/depsdev" osvmatcher "github.com/bomly-dev/bomly-cli/internal/matchers/osv" "github.com/bomly-dev/bomly-cli/internal/matchers/scorecard" @@ -225,42 +225,30 @@ func scorecardEntry() Entry { } func govulncheckEntry() Entry { - return analyzerEntry("govulncheck", func(deps Deps) sdk.Analyzer { - return govulncheck.Analyzer{Logger: deps.logger()} - }) + return analyzerEntry("govulncheck", govulncheck.Module) } func jsReachEntry() Entry { - return analyzerEntry("jsreach", func(deps Deps) sdk.Analyzer { - return jsreach.Analyzer{Logger: deps.logger()} - }) + return analyzerEntry("jsreach", jsreach.Module) } func pyReachEntry() Entry { - return analyzerEntry("pyreach", func(deps Deps) sdk.Analyzer { - return pyreach.Analyzer{Logger: deps.logger()} - }) + return analyzerEntry("pyreach", pyreach.Module) } func jvmReachEntry() Entry { - return analyzerEntry("jvmreach", func(deps Deps) sdk.Analyzer { - return jvmreach.Analyzer{Logger: deps.logger()} - }) + return analyzerEntry("jvmreach", jvmreach.Module) } -func analyzerEntry(name string, build func(Deps) sdk.Analyzer) Entry { +// analyzerEntry wraps a component module constructor from +// components/analyzers/. The component builds its analyzer from the +// registration HostContext (logger included), so the entry ignores Deps. +func analyzerEntry(name string, module func() sdk.Module) Entry { return Entry{ Name: name, Kind: sdk.PluginKindAnalyzer, Implementation: ImplementationNative, DefaultEnabled: true, - Module: func(deps Deps) sdk.Module { - return sdk.Module{Kind: sdk.PluginKindAnalyzer, Analyzer: &sdk.AnalyzerModule{ - Descriptor: sdk.AnalyzerDescriptor{Name: name}, - New: func(_ context.Context, _ sdk.HostContext) (sdk.Analyzer, error) { - return build(deps), nil - }, - }} - }, + Module: func(Deps) sdk.Module { return module() }, } } diff --git a/internal/support/prose/detectors/maven.md b/internal/support/prose/detectors/maven.md index 6e8a6d04..dc8ce1db 100644 --- a/internal/support/prose/detectors/maven.md +++ b/internal/support/prose/detectors/maven.md @@ -87,7 +87,7 @@ For Maven packages, the analyzer is `jvmreach` at **Tier-3 (package)**. It walks For multi-module reactors, `jvmreach` reads parent `` declarations recursively and follows source namespace imports between consumed sibling modules before attributing external artifacts. -If a missing prefix produces a false-negative for a direct import, add the mapping to `internal/analyzers/jvmreach/prefixmap.go` (one-line PR). +If a missing prefix produces a false-negative for a direct import, add the mapping to `components/analyzers/jvmreach/prefixmap.go` (one-line PR). ## Limitations diff --git a/internal/support/prose/detectors/pip.md b/internal/support/prose/detectors/pip.md index af6dd3f0..29dd277d 100644 --- a/internal/support/prose/detectors/pip.md +++ b/internal/support/prose/detectors/pip.md @@ -90,7 +90,7 @@ installs. Missing or malformed source data stays unknown. > **Experimental.** Reachability is opt-in via `--analyze`. The feature is stable in shape but may evolve; ecosystem coverage is expanding. -For pip-managed packages, the analyzer is `pyreach` at **Tier-3 (package)**. It walks every `.py` file under the project root, records imports, and maps module names to PyPI distribution names. See [REACHABILITY.md](../../REACHABILITY.md#unreachable-is-not-safe) and the module-to-distribution map in `internal/analyzers/pyreach/moduletodist.go`. +For pip-managed packages, the analyzer is `pyreach` at **Tier-3 (package)**. It walks every `.py` file under the project root, records imports, and maps module names to PyPI distribution names. See [REACHABILITY.md](../../REACHABILITY.md#unreachable-is-not-safe) and the module-to-distribution map in `components/analyzers/pyreach/moduletodist.go`. ## Limitations diff --git a/scripts/run-fuzz.sh b/scripts/run-fuzz.sh index c5df6dee..f082318f 100755 --- a/scripts/run-fuzz.sh +++ b/scripts/run-fuzz.sh @@ -8,8 +8,13 @@ FUZZTIME="${FUZZTIME:-60s}" # run there. targets=( "github.com/bomly-dev/bomly-cli/internal/config FuzzLoadFile" - "github.com/bomly-dev/bomly-cli/internal/analyzers/govulncheck FuzzParseGovulncheckJSON" - "github.com/bomly-dev/bomly-cli/internal/analyzers/jsreach FuzzExtractImportedPackages" + "github.com/bomly-dev/bomly-cli/components/analyzers/govulncheck FuzzParseGovulncheckJSON" + "github.com/bomly-dev/bomly-cli/components/analyzers/jsreach FuzzExtractImportedPackages" + "github.com/bomly-dev/bomly-cli/components/analyzers/jsreach FuzzEntryPointStrings" + "github.com/bomly-dev/bomly-cli/components/analyzers/pyreach FuzzScanImports" + "github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach FuzzScanImports" + "github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach FuzzReadMavenProject" + "github.com/bomly-dev/bomly-cli/components/analyzers/jvmreach FuzzReadGradleModules" "github.com/bomly-dev/bomly-cli/internal/detectors/cargo FuzzDepGraphFromCargoLock" "github.com/bomly-dev/bomly-cli/internal/detectors/cocoapods FuzzDepGraphFromPodfileLock" "github.com/bomly-dev/bomly-cli/internal/detectors/composer FuzzDepGraphFromComposerLock" diff --git a/test/assurance/PARSER_FUZZING.md b/test/assurance/PARSER_FUZZING.md index d3b4379d..519a377a 100644 --- a/test/assurance/PARSER_FUZZING.md +++ b/test/assurance/PARSER_FUZZING.md @@ -19,6 +19,8 @@ the reader inventory, cache behavior, and intentional exclusions. | Package identifiers and plugin paths | package URL canonicalization, plugin path sanitizers | | SBOM | automatic SPDX and CycloneDX decoding; Syft JSON identification and deterministic rejection (the format is no longer ingested) | | Analyzer output | govulncheck JSON stream, esbuild metafile | +| Analyzer source scanning | Python import scanner, JVM import scanner | +| Analyzer project configuration | package.json entry-point helpers (jsreach), Maven pom.xml module reader and Gradle settings module reader (jvmreach) | | Node lockfiles | npm, pnpm, Yarn, Bun | | Node project configuration | package.json, pnpm-workspace.yaml, and .npmrc behind the package-manager warning checks | | Python lockfiles | Poetry, uv, Pipenv | @@ -35,8 +37,10 @@ oversized structures within the bound, and arbitrary path/reference text. - Command-backed detectors are exercised through fake-binary unit tests and smoke tests. Their parsers are fuzzed only when the command output has an isolated, deterministic in-process parser. -- Maven, Gradle, and SBT XML/tree output is coupled to command execution and - does not currently expose a pure parser boundary. +- Maven, Gradle, and SBT XML/tree *command output* is coupled to command + execution and does not currently expose a pure parser boundary. The + jvmreach analyzer's file-backed pom.xml and Gradle settings readers are a + separate surface and are fuzzed (see the native-target table above). - Archive extraction uses Go standard-library readers plus explicit path containment checks; hostile archive path behavior remains covered by security tests coordinated with the threat-model work. diff --git a/test/assurance/repository_input_limits_test.go b/test/assurance/repository_input_limits_test.go index 817e7ff6..0def2ff4 100644 --- a/test/assurance/repository_input_limits_test.go +++ b/test/assurance/repository_input_limits_test.go @@ -20,7 +20,7 @@ var unboundedWholeFileReadCalls = []string{ func TestRepositoryParsersDoNotUseUnboundedWholeFileReads(t *testing.T) { root := assuranceRepositoryRoot(t) for _, relativeRoot := range []string{ - "internal/analyzers", + "components/analyzers", "internal/detectors", "internal/registry", } {