From 946acbc683b9886b849e1c12fd4c945875d3f25c Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 19:22:36 -0700 Subject: [PATCH 01/14] refactor(engine): remove unused pre/post-resolve hooks The pre-resolve and post-resolve hook subsystem had no production implementations, was never registered in the builder, exposed no plugin kind, and was not surfaced in the CLI, MCP, config, or docs. Only test doubles exercised it, so it executed as a no-op on every run. Remove the internal/engine/hooks package, the registry registration and accessor methods, the pipeline runPre/runPost stages and context helpers, the diff and explain call sites, and the hook tests. Update the pipeline-order descriptions and package trees in CLAUDE.md, AGENTS.md, and docs/ARCHITECTURE.md accordingly. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 4 +- CLAUDE.md | 8 +- docs/ARCHITECTURE.md | 4 +- internal/engine/diff/diff.go | 2 - internal/engine/hooks/hooks.go | 81 ----------------- internal/engine/pipeline.go | 45 +--------- internal/engine/pipeline_explain.go | 12 --- internal/engine/pipeline_test.go | 132 ---------------------------- internal/engine/registry.go | 53 +---------- internal/engine/types.go | 12 --- 10 files changed, 11 insertions(+), 342 deletions(-) delete mode 100644 internal/engine/hooks/hooks.go diff --git a/AGENTS.md b/AGENTS.md index 2a4d0593..f7511b87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full detail. Component ma | `internal/cli` | Cobra root + all commands (`scan`, `explain`, `diff`, `plugin`, `version`) | | `sdk` | Unified domain types: `Dependency` (detection graph nodes), `Package` (PURL-keyed matching artifacts in `PackageRegistry`), `Vulnerability` (OSV-aligned), reference-style `Finding`, plus neutral package/ecosystem/support identifiers. See `docs/MODELS.md`. | | `internal/detectors` | Detector contracts, descriptors, requests/results, and detector-only helpers | -| `internal/engine` | Pipeline, engine, consolidation, auditors, matchers, hooks, and orchestration | +| `internal/engine` | Pipeline, engine, consolidation, auditors, matchers, and orchestration | | `internal/registry` | Canonical support/discovery registry and built-in engine registry wiring | | `internal/detectors/*` | Concrete dependency resolution per ecosystem (gomod, gradle, maven, node, python, sbom, syft) | | `internal/matchers/*` | External enrichment matchers and shared matcher cache (osv, grype, deps.dev, ClearlyDefined, eol, scorecard) | @@ -53,7 +53,7 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full detail. Component ma | `internal/testutil` | Test helpers (fake binary builder) | | `internal/system` | OS-level helpers | -Scan pipeline: `runtimePreparation → subprojectDiscovery → preResolveHooks → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment on the consolidated graph) → audit → postResolveHooks → format`. +Scan pipeline: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment on the consolidated graph) → audit → format`. Runtime preparation is owned by `internal/engine`: build the filtered registry once, index the execution target with that same registry, and reuse the prepared runtime for `scan`, `diff`, `explain`, license enrichment, and auditing. The CLI resolves raw execution targets and flags, but it must not discover subprojects with a separate registry. diff --git a/CLAUDE.md b/CLAUDE.md index 5777e9bd..155a6828 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,8 +60,6 @@ internal/engine/ Pipeline core (pipeline.go, engine.go), Registr internal/engine/consolidation/ Cross-subproject graph consolidation, manifest dedup, enrichment sync (ConsolidateGraphs, ManifestDedupPriority, SyncConsolidatedEnrichmentToManifests) -internal/engine/hooks/ Pre-/post-resolve hook contract + executor (Descriptor, - PreResolveHook, PostResolveHook, RunPre, RunPost) internal/registry/ Support/discovery registry; built-in wiring in builder.go internal/matchers/* External enrichment: osv, grype, deps.dev, ClearlyDefined, eol, scorecard internal/matchers/cache File-based cache shared by matchers @@ -89,7 +87,7 @@ internal/testutil/ Test helpers (fake binary builder) **`bomly explain`** is implemented by `newExplainCmd` in `internal/cli/explain_cmd.go`. -**Scan pipeline order**: `runtimePreparation → subprojectDiscovery → preResolveHooks → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment) → analyze (reachability, when --analyze is set) → audit → postResolveHooks → format` +**Scan pipeline order**: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment) → analyze (reachability, when --analyze is set) → audit → format` Runtime preparation is owned by `internal/engine` and is reached through CLI option helpers before pipeline execution. The CLI resolves raw targets and flags but must not discover subprojects with a separate registry. @@ -98,8 +96,8 @@ Runtime preparation is owned by `internal/engine` and is reached through CLI opt - `internal/detectors/*` and `internal/analyzers/*` must not import `internal/engine`, `internal/engine/*`, or `internal/registry`. Analyzers depend only on `sdk` and the vendored library that backs their runner. - `sdk` owns neutral identifiers that would otherwise create import cycles. - `internal/registry` owns package-manager discovery, support lookups, and built-in wiring in `builder.go`. Do not create a separate `registrybuilder` package. -- `internal/engine` (pipeline core) may import `internal/engine/consolidation`, `internal/engine/hooks`, `internal/engine/explain`, `internal/detectors`, and `internal/registry`. -- `internal/engine` subpackages (`consolidation`, `hooks`, `diff`, `explain`, `scan`) must not import `internal/cli`. +- `internal/engine` (pipeline core) may import `internal/engine/consolidation`, `internal/engine/explain`, `internal/detectors`, and `internal/registry`. +- `internal/engine` subpackages (`consolidation`, `diff`, `explain`, `scan`) must not import `internal/cli`. - `internal/config`, `internal/selector`, `internal/progress`, `internal/cli/render`, `internal/tui` must not import `internal/cli`. They are downstream of cobra wiring; cli consumes them, not the reverse. - `internal/tui` may import `internal/cli/render` (for ANSI primitives, text helpers, and shared sort/format helpers used by both the TUI and the text reports). - `cmd/bomly/main.go` is the only file outside `internal/cli` that imports `internal/cli`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6d6130f1..2ac6ab95 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -194,7 +194,7 @@ Cache failures are non-fatal. The command should warn and continue rather than f |-----------------------|-------------------------------------------------------------------------------------------------| | `cmd/bomly` | CLI entry point | | `internal/cli` | Commands, config loading, progress, and help output | -| `internal/engine` | Runtime preparation, orchestration, pipeline hooks, and consolidation | +| `internal/engine` | Runtime preparation, orchestration, and consolidation | | `internal/registry` | Support metadata, package-manager discovery, and built-in detector, matcher, and auditor wiring | | `internal/detectors` | Detector contracts and ecosystem implementations | | `internal/auditors` | Policy evaluators and finding creation | @@ -298,6 +298,6 @@ This keeps the scan engine recognizable while making it possible to migrate sele - Detector packages must not import `internal/engine` or `internal/registry`. - `sdk` owns shared neutral identifiers and support types. - `internal/registry` owns discovery, support-matrix data, and built-in registry wiring. -- `internal/engine` owns runtime planning, orchestration, hook execution, and detector-chain reuse. +- `internal/engine` owns runtime planning, orchestration, and detector-chain reuse. - `internal/plugin` owns managed plugin installation, verification, store state, and external runtime adapters. - The CLI resolves user input but should not perform its own independent discovery pass. diff --git a/internal/engine/diff/diff.go b/internal/engine/diff/diff.go index aa86ca1e..1896114e 100644 --- a/internal/engine/diff/diff.go +++ b/internal/engine/diff/diff.go @@ -84,8 +84,6 @@ func Run(ctx context.Context, req Request) (Result, error) { result.Audit = AuditSummary(result.Base.Findings, result.Head.Findings) result.Findings = append(append([]sdk.Finding{}, result.Head.Findings...), result.Base.Findings...) } - req.Base.Pipeline.RunPostResolveHooks(ctx, req.Base.Request, result.Base) - req.Head.Pipeline.RunPostResolveHooks(ctx, req.Head.Request, result.Head) return result, nil } diff --git a/internal/engine/hooks/hooks.go b/internal/engine/hooks/hooks.go deleted file mode 100644 index f330d556..00000000 --- a/internal/engine/hooks/hooks.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package hooks defines the pre-resolve and post-resolve hook contract used by the scan pipeline. -// Hook implementations come from the registry; this package only owns the interfaces, the -// per-hook context shapes, and the executor used to run a slice of hooks in priority order. -package hooks - -import ( - "context" - "errors" - "fmt" - "io" - - "github.com/bomly-dev/bomly-cli/sdk" - "go.uber.org/zap" -) - -// Descriptor describes a hook registration. -type Descriptor struct { - Name string - Priority int - Stage string // "pre-resolve" | "post-resolve" -} - -// PreResolveContext provides inputs available before detection. -type PreResolveContext struct { - ExecutionTarget sdk.ExecutionTarget - Subprojects []sdk.Subproject - ProjectPath string - Stderr io.Writer -} - -// PostResolveContext provides inputs available after full resolution. -type PostResolveContext struct { - Consolidated sdk.ConsolidatedGraph - Findings []sdk.Finding - ProjectPath string - Stderr io.Writer -} - -// PreResolveHook runs logic before detection. -type PreResolveHook interface { - Descriptor() Descriptor - Execute(context.Context, PreResolveContext) error -} - -// PostResolveHook runs logic after all resolutions and auditing. -type PostResolveHook interface { - Descriptor() Descriptor - Execute(context.Context, PostResolveContext) error -} - -// RunPre runs each pre-resolve hook in order. The first failure stops the chain. -func RunPre(ctx context.Context, logger *zap.Logger, hooks []PreResolveHook, hookCtx PreResolveContext) error { - if logger == nil { - logger = zap.NewNop() - } - for _, hook := range hooks { - desc := hook.Descriptor() - logger.Debug("pipeline: running pre-resolve hook", zap.String("name", desc.Name)) - if err := hook.Execute(ctx, hookCtx); err != nil { - return fmt.Errorf("hook %s: %w", desc.Name, err) - } - } - return nil -} - -// RunPost runs each post-resolve hook in order, collecting errors so one failing hook -// does not prevent the rest from running. -func RunPost(ctx context.Context, logger *zap.Logger, hooks []PostResolveHook, hookCtx PostResolveContext) error { - if logger == nil { - logger = zap.NewNop() - } - var errs []error - for _, hook := range hooks { - desc := hook.Descriptor() - logger.Debug("pipeline: running post-resolve hook", zap.String("name", desc.Name)) - if err := hook.Execute(ctx, hookCtx); err != nil { - errs = append(errs, fmt.Errorf("hook %s: %w", desc.Name, err)) - } - } - return errors.Join(errs...) -} diff --git a/internal/engine/pipeline.go b/internal/engine/pipeline.go index 27485693..7c38a1d0 100644 --- a/internal/engine/pipeline.go +++ b/internal/engine/pipeline.go @@ -6,13 +6,12 @@ import ( "strings" "github.com/bomly-dev/bomly-cli/internal/engine/consolidation" - "github.com/bomly-dev/bomly-cli/internal/engine/hooks" "github.com/bomly-dev/bomly-cli/sdk" "go.uber.org/zap" ) // Pipeline orchestrates a full scan through a sequence of typed stages: -// pre-resolve hooks -> detect -> consolidate -> match -> analyze -> audit -> post-resolve hooks. +// detect -> consolidate -> match -> analyze -> audit. type Pipeline struct { Registry *Registry Logger *zap.Logger @@ -41,17 +40,13 @@ func (p *Pipeline) Run(ctx context.Context, req PipelineRequest) (PipelineResult return result, err } p.runAudit(ctx, &result, req) - p.runPost(ctx, req, result) return result, nil } // RunPreAudit executes the pipeline through enrichment and analysis, stopping -// before policy evaluation and post-resolve hooks. +// before policy evaluation. func (p *Pipeline) RunPreAudit(ctx context.Context, req PipelineRequest) (PipelineResult, error) { result := PipelineResult{} - if err := p.runPre(ctx, req); err != nil { - return result, err - } if err := p.runResolve(ctx, &result, req); err != nil { return result, err } @@ -86,18 +81,6 @@ func (p *Pipeline) RunAuditGraph(ctx context.Context, graph *sdk.Graph, registry return auditResult, auditWarnings } -// RunPostResolveHooks executes post-resolve hooks with the supplied result. -func (p *Pipeline) RunPostResolveHooks(ctx context.Context, req PipelineRequest, result PipelineResult) { - p.runPost(ctx, req, result) -} - -func (p *Pipeline) runPre(ctx context.Context, req PipelineRequest) error { - if err := hooks.RunPre(ctx, p.Logger, p.Registry.PreResolveHooks(), preHookContext(req)); err != nil { - return fmt.Errorf("pre-resolve hook: %w", err) - } - return nil -} - func (p *Pipeline) runResolve(ctx context.Context, result *PipelineResult, req PipelineRequest) error { resolveResults, resolveErr := p.resolveAll(ctx, req) result.ResolveResults = resolveResults @@ -266,30 +249,6 @@ func (p *Pipeline) runAudit(ctx context.Context, result *PipelineResult, req Pip } } -func (p *Pipeline) runPost(ctx context.Context, req PipelineRequest, result PipelineResult) { - if err := hooks.RunPost(ctx, p.Logger, p.Registry.PostResolveHooks(), postHookContext(req, result)); err != nil { - p.Logger.Warn("pipeline: post-resolve hook error", zap.Error(err)) - } -} - -func preHookContext(req PipelineRequest) hooks.PreResolveContext { - return hooks.PreResolveContext{ - ExecutionTarget: req.ExecutionTarget, - Subprojects: req.Subprojects, - ProjectPath: req.ProjectPath, - Stderr: req.Stderr, - } -} - -func postHookContext(req PipelineRequest, result PipelineResult) hooks.PostResolveContext { - return hooks.PostResolveContext{ - Consolidated: result.Consolidated, - Findings: result.Findings, - ProjectPath: req.ProjectPath, - Stderr: req.Stderr, - } -} - func (p *Pipeline) match(ctx context.Context, result *PipelineResult, req PipelineRequest) { if result.Graph == nil { return diff --git a/internal/engine/pipeline_explain.go b/internal/engine/pipeline_explain.go index dfe07731..aa51f0ab 100644 --- a/internal/engine/pipeline_explain.go +++ b/internal/engine/pipeline_explain.go @@ -40,9 +40,6 @@ func (p *Pipeline) RunExplain(ctx context.Context, req ExplainRequest) (ExplainR pipeReq.AuditEnabled = false base := PipelineResult{} - if err := p.runPre(ctx, pipeReq); err != nil { - return ExplainResult{PipelineResult: base}, err - } if err := p.runResolve(ctx, &base, pipeReq); err != nil { return ExplainResult{PipelineResult: base}, err } @@ -123,15 +120,6 @@ func (p *Pipeline) RunExplain(ctx context.Context, req ExplainRequest) (ExplainR } result.FocusedGraph = focusedGraph - postResult := result.PipelineResult - postResult.Consolidated = focusedConsolidated - postResult.Graph = focusedGraph - postResult.Findings = result.Findings - postResult.AuditWarnings = result.AuditWarnings - postResult.AuditorRuns = result.AuditorRuns - postResult.AuditorFindings = result.AuditorFindings - p.runPost(ctx, pipeReq, postResult) - return result, nil } diff --git a/internal/engine/pipeline_test.go b/internal/engine/pipeline_test.go index 02ffd411..183219c0 100644 --- a/internal/engine/pipeline_test.go +++ b/internal/engine/pipeline_test.go @@ -460,119 +460,6 @@ func scopedTestGraph(t *testing.T) *sdk.Graph { return graph } -// --------------------------------------------------------------------------- -// Pipeline hook tests -// --------------------------------------------------------------------------- - -type fakePreResolveHook struct { - descriptor HookDescriptor - err error - executed bool -} - -func (h *fakePreResolveHook) Descriptor() HookDescriptor { return h.descriptor } - -func (h *fakePreResolveHook) Execute(_ context.Context, _ PreResolveContext) error { - h.executed = true - return h.err -} - -type fakePostResolveHook struct { - descriptor HookDescriptor - err error - executed bool -} - -func (h *fakePostResolveHook) Descriptor() HookDescriptor { return h.descriptor } - -func (h *fakePostResolveHook) Execute(_ context.Context, _ PostResolveContext) error { - h.executed = true - return h.err -} - -func TestPipeline_PreResolveHook_CalledBeforeDetectors(t *testing.T) { - registry := newTestRegistry() - hook := &fakePreResolveHook{ - descriptor: HookDescriptor{Name: "test-pre-hook", Priority: 0, Stage: "pre-resolve"}, - } - registry.RegisterPreResolveHook(hook) - - registry.registerDetector(fakeDetector{ - descriptor: DetectorDescriptor{Name: "npm-detector", SupportedEcosystems: []Ecosystem{EcosystemNPM}, SupportedManagers: []PackageManager{PackageManagerNPM}}, - result: ResolveGraphResult{Graphs: SingleGraphContainer(sdk.New(), sdk.ManifestMetadata{Path: "package-lock.json", Kind: "package-lock.json"})}, - }) - - pipeline := NewPipeline(registry, zap.NewNop()) - _, err := pipeline.Run(context.Background(), PipelineRequest{ - Subprojects: []Subproject{{ - ExecutionTarget: ExecutionTarget{Kind: ExecutionTargetFilesystem, Location: "/repo"}, - RelativePath: ".", - PrimaryDetector: "npm-detector", - DetectedPackageManagers: []PackageManager{PackageManagerNPM}, - Ecosystem: EcosystemNPM, - }}, - }) - if err != nil { - t.Fatalf("Run() error = %v", err) - } - if !hook.executed { - t.Fatal("expected pre-resolve hook to be executed") - } -} - -func TestPipeline_PostResolveHook_CalledAfterAudit(t *testing.T) { - registry := newTestRegistry() - postHook := &fakePostResolveHook{ - descriptor: HookDescriptor{Name: "test-post-hook", Priority: 0, Stage: "post-resolve"}, - } - registry.RegisterPostResolveHook(postHook) - - registry.registerDetector(fakeDetector{ - descriptor: DetectorDescriptor{Name: "npm-detector", SupportedEcosystems: []Ecosystem{EcosystemNPM}, SupportedManagers: []PackageManager{PackageManagerNPM}}, - result: ResolveGraphResult{Graphs: SingleGraphContainer(sdk.New(), sdk.ManifestMetadata{Path: "package-lock.json", Kind: "package-lock.json"})}, - }) - - pipeline := NewPipeline(registry, zap.NewNop()) - _, err := pipeline.Run(context.Background(), PipelineRequest{ - Subprojects: []Subproject{{ - ExecutionTarget: ExecutionTarget{Kind: ExecutionTargetFilesystem, Location: "/repo"}, - RelativePath: ".", - PrimaryDetector: "npm-detector", - DetectedPackageManagers: []PackageManager{PackageManagerNPM}, - Ecosystem: EcosystemNPM, - }}, - }) - if err != nil { - t.Fatalf("Run() error = %v", err) - } - if !postHook.executed { - t.Fatal("expected post-resolve hook to be executed") - } -} - -func TestPipeline_PreResolveHookError_AbortsPipeline(t *testing.T) { - registry := newTestRegistry() - hook := &fakePreResolveHook{ - descriptor: HookDescriptor{Name: "failing-hook", Priority: 0, Stage: "pre-resolve"}, - err: errors.New("pre-hook failed"), - } - registry.RegisterPreResolveHook(hook) - - pipeline := NewPipeline(registry, zap.NewNop()) - _, err := pipeline.Run(context.Background(), PipelineRequest{ - Subprojects: []Subproject{{ - ExecutionTarget: ExecutionTarget{Kind: ExecutionTargetFilesystem, Location: "/repo"}, - RelativePath: ".", - PrimaryDetector: "npm-detector", - DetectedPackageManagers: []PackageManager{PackageManagerNPM}, - Ecosystem: EcosystemNPM, - }}, - }) - if err == nil { - t.Fatal("expected error from pre-resolve hook") - } -} - // --------------------------------------------------------------------------- // Pipeline full run test // --------------------------------------------------------------------------- @@ -932,25 +819,6 @@ func TestRegistry_Detectors_RespectsFilter(t *testing.T) { } } -func TestRegistry_HooksSortedByPriority(t *testing.T) { - registry := newTestRegistry() - hookA := &fakePreResolveHook{descriptor: HookDescriptor{Name: "hook-b", Priority: 10}} - hookB := &fakePreResolveHook{descriptor: HookDescriptor{Name: "hook-a", Priority: 5}} - registry.RegisterPreResolveHook(hookA) - registry.RegisterPreResolveHook(hookB) - - hooks := registry.PreResolveHooks() - if len(hooks) != 2 { - t.Fatalf("expected 2 hooks, got %d", len(hooks)) - } - if hooks[0].Descriptor().Name != "hook-a" { - t.Fatalf("expected hook-a first (lower priority), got %q", hooks[0].Descriptor().Name) - } - if hooks[1].Descriptor().Name != "hook-b" { - t.Fatalf("expected hook-b second (higher priority), got %q", hooks[1].Descriptor().Name) - } -} - // --------------------------------------------------------------------------- // PipelineWarningsFromError / parseWarningSource // --------------------------------------------------------------------------- diff --git a/internal/engine/registry.go b/internal/engine/registry.go index 5eb8eb07..63cf2f20 100644 --- a/internal/engine/registry.go +++ b/internal/engine/registry.go @@ -1,8 +1,6 @@ package engine import ( - "sort" - "github.com/bomly-dev/bomly-cli/internal/registry" "github.com/bomly-dev/bomly-cli/sdk" "go.uber.org/zap" @@ -20,11 +18,9 @@ type DetectorDiscoveryPlan = registry.DetectorDiscoveryPlan // ComponentOptions records Bomly-owned registry behavior for a component. type ComponentOptions = registry.ComponentOptions -// Registry wraps the shared registry with scan-specific hook registration. +// Registry wraps the shared registry with scan-specific registration helpers. type Registry struct { *registry.Registry - preHooks []PreResolveHook - postHooks []PostResolveHook } // NewRegistry creates an empty scan registry. @@ -60,58 +56,13 @@ func (r *Registry) registerDetectorDiscoveryPlan(detectorName string, plan Detec r.RegisterDetectorDiscoveryPlan(detectorName, plan) } -// RegisterPreResolveHook adds a pre-resolve hook to the registry. -func (r *Registry) RegisterPreResolveHook(hook PreResolveHook) { - if hook == nil { - return - } - r.preHooks = append(r.preHooks, hook) -} - -// RegisterPostResolveHook adds a post-resolve hook to the registry. -func (r *Registry) RegisterPostResolveHook(hook PostResolveHook) { - if hook == nil { - return - } - r.postHooks = append(r.postHooks, hook) -} - -// PreResolveHooks returns registered pre-resolve hooks sorted by priority (ascending). -func (r *Registry) PreResolveHooks() []PreResolveHook { - hooks := make([]PreResolveHook, len(r.preHooks)) - copy(hooks, r.preHooks) - sortHooks(hooks) - return hooks -} - -// PostResolveHooks returns registered post-resolve hooks sorted by priority (ascending). -func (r *Registry) PostResolveHooks() []PostResolveHook { - hooks := make([]PostResolveHook, len(r.postHooks)) - copy(hooks, r.postHooks) - sortHooks(hooks) - return hooks -} - // Filter returns a copy of the registry filtered by the supplied detector, auditor, // matcher, and ecosystem selections. func (r *Registry) Filter(filter RegistryFilter) *Registry { if r == nil { return nil } - filtered := &Registry{ + return &Registry{ Registry: r.Registry.Filter(filter), } - filtered.preHooks = append(filtered.preHooks, r.preHooks...) - filtered.postHooks = append(filtered.postHooks, r.postHooks...) - return filtered -} - -type hookDescriptor interface { - Descriptor() HookDescriptor -} - -func sortHooks[T hookDescriptor](hooks []T) { - sort.Slice(hooks, func(i, j int) bool { - return hooks[i].Descriptor().Priority < hooks[j].Descriptor().Priority - }) } diff --git a/internal/engine/types.go b/internal/engine/types.go index da7d28cf..c93e7be2 100644 --- a/internal/engine/types.go +++ b/internal/engine/types.go @@ -3,21 +3,9 @@ package engine import ( "io" - "github.com/bomly-dev/bomly-cli/internal/engine/hooks" "github.com/bomly-dev/bomly-cli/sdk" ) -// Hook types are re-exported from internal/engine/hooks for existing references -// inside the engine package and as the registry's hook surface. Plugins should depend on -// internal/engine/hooks directly. -type ( - HookDescriptor = hooks.Descriptor - PreResolveContext = hooks.PreResolveContext - PostResolveContext = hooks.PostResolveContext - PreResolveHook = hooks.PreResolveHook - PostResolveHook = hooks.PostResolveHook -) - // PipelineRequest defines input for a full pipeline run. type PipelineRequest struct { ProjectPath string From 40a185c5413b1e212885de6834f5e356cc3e4008 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 19:35:58 -0700 Subject: [PATCH 02/14] docs: prepare repository for public open-source release Scrub "currently private" caveats from docs/CI.md and the release workflow now that the repository is public. Add community health files (issue templates, pull request template, CODEOWNERS), a Bomly CLI SVG wordmark, and a README badge row (CI, release, license, Go Reference, Go Report Card). Co-Authored-By: Claude Opus 4.8 --- .github/CODEOWNERS | 3 ++ .github/ISSUE_TEMPLATE/bug_report.yml | 47 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 +++++ .github/ISSUE_TEMPLATE/feature_request.yml | 25 ++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 32 +++++++++++++++ .github/workflows/release.yml | 2 +- README.md | 18 +++++++-- assets/bomly-cli-wordmark.svg | 35 ++++++++++++++++ docs/CI.md | 7 ++-- 9 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 assets/bomly-cli-wordmark.svg diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..fcb856cc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Default owner for everything in the repo. +# Order matters: the last matching pattern takes precedence. +* @bomly-guy diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..c346b2f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,47 @@ +name: Bug report +description: Report incorrect behavior in the Bomly CLI +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug. Please fill out the sections below so we can reproduce and fix it quickly. + + Do **not** report security vulnerabilities here — see the repository's Security tab for private reporting. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug and what you expected instead. + placeholder: Running `bomly scan` on a Go module reported no dependencies, but go.mod has several. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: The exact command(s) and, if possible, a minimal project or public repo + ref that triggers it. + placeholder: | + 1. `git clone https://github.com/example/repo && cd repo` + 2. `bomly scan --json` + 3. Observe ... + validations: + required: true + - type: textarea + id: version + attributes: + label: Bomly version and platform + description: Output of `bomly version` plus your OS/arch. + placeholder: | + bomly v0.2.3 + macOS 14.5 arm64 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant output + description: Paste relevant output. Re-run with `-vv` for debug logs if helpful. Scrub anything sensitive. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0d099b8b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Question or discussion + url: https://github.com/bomly-dev/bomly-cli/discussions + about: Ask questions, share ideas, and discuss usage with the community. + - name: Report a security vulnerability + url: https://github.com/bomly-dev/bomly-cli/security/advisories/new + about: Please report security issues privately, not as a public issue. + - name: Contact the maintainers + url: mailto:contact@bomly.dev + about: Reach the Bomly team for anything that doesn't fit an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..6a00bc95 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,25 @@ +name: Feature request +description: Suggest a new capability or improvement for Bomly +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + description: Describe the use case or pain point, not just the proposed solution. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What would you like Bomly to do? Include example commands, flags, or output if you have them in mind. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you've tried or considered, including existing workarounds. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..a1235676 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ + + +## Summary + + + +## Changes + + + +- + +## Checklist + +- [ ] `make test` passes. +- [ ] `make lint` passes. +- [ ] `make generate` run and committed if I changed `internal/config/config.go`, `internal/output/*`, `sdk/catalog.go`, `sdk/support_matrix.go`, or `internal/registry/support.go`. +- [ ] Smoke goldens regenerated (`make smoke ARGS="-update"`) and committed if behavior changed. +- [ ] Docs updated (feature page, `docs/ARCHITECTURE.md`, `CLAUDE.md`/`AGENTS.md`) where relevant. + +### For a new user-visible feature (new flag / component / pipeline stage) + + + +- [ ] CLI flag, config field, and validation wired up. +- [ ] Reachable from the matching MCP tool. +- [ ] Surfaced in `bomly plugin` listing (for a new component kind). +- [ ] Smoke test added against a pinned public repo. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65155cf9..69743a5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: Each archive includes `LICENSE`, `NOTICE`, and a `licenses/` directory with the full license text for every bundled dependency. - GitHub-native artifact attestations are intentionally deferred while the repository remains private on a non-Enterprise plan. + GitHub-native artifact attestations are planned and will be added in a future release. EOF - name: Create draft prerelease diff --git a/README.md b/README.md index 8697752a..b2b68669 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ -# Bomly - -Dependency intelligence for modern software projects. +

+ Bomly CLI +

+ +

+ Dependency intelligence for modern software projects. +

+ +

+ CI + Latest release + License: Apache-2.0 + Go Reference + Go Report Card +

Bomly scans source trees, SBOMs, Git refs, and container images, generates SPDX and CycloneDX SBOMs, enriches packages with vulnerability and license data, evaluates policy, and diffs dependency state across refs. diff --git a/assets/bomly-cli-wordmark.svg b/assets/bomly-cli-wordmark.svg new file mode 100644 index 00000000..903ea61a --- /dev/null +++ b/assets/bomly-cli-wordmark.svg @@ -0,0 +1,35 @@ + + Bomly CLI + + + + + + + + + + + + + + + + + + + + + + + + + + Bomly + + + + + CLI + + diff --git a/docs/CI.md b/docs/CI.md index 82dfe192..73df78a5 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -122,14 +122,13 @@ For local parity, the Makefile exposes: The repository also ships a pre-commit hook in `.githooks/pre-commit`. Run `make install-hooks` once per clone to point Git at that hook directory. -## Private Repo Cost Controls +## Cost Controls -This repository is currently private, so the workflow set is intentionally lean: +The per-PR workflow set is intentionally lean so the common path stays fast: - `golangci-lint` runs inside the main `CI` workflow with the official action, pinned to the repository's lint version and using the action's built-in cache. - Standalone `go vet ./...` is not run separately in `CI` because `.golangci.yml` already enables `govet`. - `go test -race` is not enabled in CI because it adds runtime cost and is best reintroduced later if we need the extra concurrency diagnostics. -- CodeQL is disabled for now because it is not available for the current private-repo setup. It can be restored when the repository goes public or the GitHub plan changes. ## Build and Packaging Model @@ -200,4 +199,4 @@ sha256sum --check SHA256SUMS Get-FileHash .\bomly_v0.2.0_windows_amd64.zip -Algorithm SHA256 ``` -GitHub-native artifact attestations are intentionally deferred for now because the repository is private and the current plan assumes private-repo attestations are not available. The release workflow is structured, so provenance attestation steps can be added later after packaging and before release publication. +GitHub-native artifact attestations are planned. The release workflow is structured so provenance attestation steps can be added after packaging and before release publication. From 0638ef148b83772e2f4be06bc23d31c77aa31719 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 19:40:31 -0700 Subject: [PATCH 03/14] ci: add CodeQL, Scorecard, dependency-review, and CodeRabbit Now that the repository is public, enable the security and quality automation that was previously unavailable: - CodeQL static analysis for Go (PR, push to main, weekly). - OpenSSF Scorecard supply-chain checks with published results, plus a Scorecard badge in the README. - GitHub dependency-review on PRs, failing on high-severity additions. - CodeRabbit configuration that skips generated docs, schemas, and golden fixtures. Document the new workflows in docs/CI.md alongside the existing Bomly Review workflow. Co-Authored-By: Claude Opus 4.8 --- .coderabbit.yaml | 20 ++++++++++ .github/workflows/codeql.yml | 52 +++++++++++++++++++++++++ .github/workflows/dependency-review.yml | 27 +++++++++++++ .github/workflows/scorecard.yml | 48 +++++++++++++++++++++++ README.md | 1 + docs/CI.md | 5 ++- 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 .coderabbit.yaml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/scorecard.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..e946c73b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: en-US +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + auto_review: + enabled: true + drafts: false + # Skip generated artifacts and fixtures so reviews focus on hand-written code. + path_filters: + - "!docs/CONFIG_REFERENCE.md" + - "!docs/SUPPORT_MATRIX.md" + - "!docs/schemas/**" + - "!**/*.golden.json" + - "!**/testdata/**" +chat: + auto_reply: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..404c55eb --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "27 4 * * 1" + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (Go) + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: | + go.sum + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + queries: security-and-quality + + - name: Build + run: go build ./... + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:go" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..5b9f83b3 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +name: Dependency review + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + review: + name: Dependency review + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Review dependencies + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..d03a9529 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,48 @@ +name: Scorecard + +on: + push: + branches: + - main + schedule: + - cron: "18 5 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: scorecard-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@v2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: scorecard-results + path: results.sarif + retention-days: 5 + + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index b2b68669..a0f2c4a6 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@

CI + OpenSSF Scorecard Latest release License: Apache-2.0 Go Reference diff --git a/docs/CI.md b/docs/CI.md index 73df78a5..64bf1c9b 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -7,7 +7,10 @@ Bomly uses GitHub Actions for validation, security analysis, smoke coverage, and | Workflow | Trigger | Purpose | |------------------------|------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| | `CI` | Pull requests, pushes to `main` | Fast validation: `golangci-lint`, `gofmt` drift checks, tests, build sanity, module drift, and generated-doc drift | -| `Dependency Review` | Pull requests touching `go.mod` or `go.sum` | Review dependency changes before merge | +| `CodeQL` | Pull requests, pushes to `main`, weekly | Static security/quality analysis for Go; results surface in the Security tab | +| `Scorecard` | Pushes to `main`, weekly, manual dispatch | OpenSSF Scorecard supply-chain checks; publishes results and uploads SARIF | +| `Dependency review` | Pull requests | GitHub dependency-review of added/changed dependencies; fails on high-severity introductions | +| `Bomly Review` | Pull requests | Dogfoods the Bomly review action to diff and audit dependency changes on each PR | | `Smoke` | Merge queue, nightly schedule, manual dispatch | Slow end-to-end coverage against real repositories, SBOMs, and containers before merge, plus scheduled drift detection | | `Update Smoke Goldens` | Manual dispatch | Regenerate golden files on a chosen ref and open a PR when the changes are intentional | | `Auto Version` | Manual dispatch | Bump `cmd/bomly/main.go`, create a semver tag, and start the release workflow | From c752621fb32b3cf21871ffefc892abfdf194b728 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 19:55:47 -0700 Subject: [PATCH 04/14] docs: expand auditor, use-case, and extensibility documentation Generate per-auditor reference pages (vulnerability, license, package) by extending the component-docs generator with an auditors writer and hand-written prose, mirroring the existing matcher pages. Link them from the auditors overview and the docs index. Add docs/USE_CASES.md with task-oriented recipes (PR gate, SBOM, reachability triage, license policy, typosquats, offline scan, release diff). Foreground Bomly's extensibility in the README and docs/ARCHITECTURE.md with a plug-in pipeline diagram and the four typed extension points, and reference the Bomly review action from the README and docs/CI_INTEGRATION.md. Co-Authored-By: Claude Opus 4.8 --- README.md | 33 +++++ docs/ARCHITECTURE.md | 23 ++++ docs/AUDITORS.md | 11 ++ docs/CI_INTEGRATION.md | 30 ++++ docs/README.md | 2 + docs/USE_CASES.md | 113 +++++++++++++++ docs/auditors/README.md | 7 + docs/auditors/license.md | 58 ++++++++ docs/auditors/package.md | 59 ++++++++ docs/auditors/vulnerability.md | 52 +++++++ internal/support/component_docs.go | 129 ++++++++++++++++++ internal/support/generate_test.go | 2 + internal/support/prose/auditors/license.md | 39 ++++++ internal/support/prose/auditors/package.md | 40 ++++++ .../support/prose/auditors/vulnerability.md | 33 +++++ 15 files changed, 631 insertions(+) create mode 100644 docs/USE_CASES.md create mode 100644 docs/auditors/README.md create mode 100644 docs/auditors/license.md create mode 100644 docs/auditors/package.md create mode 100644 docs/auditors/vulnerability.md create mode 100644 internal/support/prose/auditors/license.md create mode 100644 internal/support/prose/auditors/package.md create mode 100644 internal/support/prose/auditors/vulnerability.md diff --git a/README.md b/README.md index a0f2c4a6..6a33f1f6 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,37 @@ flowchart LR H --> I[Render text, JSON, SARIF, or SBOM] ``` +### Extensible by design + +The pipeline is built around four typed extension points. Every built-in is just an +implementation of the same contract an external plugin uses, so you can add an +ecosystem, a vulnerability or license source, a reachability analyzer, or a policy +check without forking Bomly. + +```mermaid +flowchart LR + D[Detect] --> C[Consolidate] --> M[Match] --> An[Analyze] --> Au[Audit] --> O[Output] + + PD([Detector plugins]) -.-> D + PM([Matcher plugins]) -.-> M + PAn([Analyzer plugins]) -.-> An + PAu([Auditor plugins]) -.-> Au +``` + +| Extension point | Contract | Add support for | +| --- | --- | --- | +| Detector | turns evidence into a dependency graph | a new ecosystem or lockfile format | +| Matcher | enriches packages | a new vulnerability, license, or lifecycle source | +| Analyzer | annotates reachability | a new language's call-graph analysis | +| Auditor | evaluates policy, emits findings | a custom org policy or gate | + +Plugins ship as signed, versioned gRPC binaries (`v1` protocol), are disabled until +you explicitly enable them, and participate in the exact same planning flow as +built-ins. See [docs/PLUGINS.md](docs/PLUGINS.md) and the implementation guides for +[detectors](docs/plugins/how-to-implement-detector.md), +[matchers](docs/plugins/how-to-implement-matcher.md), and +[auditors](docs/plugins/how-to-implement-auditor.md). + More detail lives in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). ## Repository Layout @@ -263,6 +294,8 @@ Bomly uses GitHub Actions for: See [docs/CI.md](docs/CI.md) for workflow triggers, required checks, release packaging, checksum handling, and the planned future attestation step. +To gate your **own** repository's pull requests, drop in the [Bomly review action](https://github.com/bomly-dev/bomly-review-action) — it diffs and audits dependency changes on each PR and posts a summary comment. See [docs/CI_INTEGRATION.md](docs/CI_INTEGRATION.md) for that and direct-CLI recipes across GitHub Actions, GitLab, Jenkins, Azure, and CircleCI. + Contributor guidance lives in [CONTRIBUTING.md](CONTRIBUTING.md). ## License diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2ac6ab95..e5782caf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -75,6 +75,29 @@ Stage summary: `bomly explain` reuses the same scope-aware resolution, consolidation, and matching stages, then performs dependency path selection in its explain orchestration before optional component audit. +## Extensibility + +Extensibility is the core of Bomly's design. The pipeline exposes four typed extension points, and **every built-in is an implementation of the same contract an external plugin implements** — there is no privileged internal path. Adding an ecosystem, an enrichment source, a reachability analyzer, or a policy gate does not require forking the engine. + +```mermaid +flowchart LR + D[Detect] --> C[Consolidate] --> M[Match] --> An[Analyze] --> Au[Audit] --> O[Output] + + PD([Detector plugins]) -.-> D + PM([Matcher plugins]) -.-> M + PAn([Analyzer plugins]) -.-> An + PAu([Auditor plugins]) -.-> Au +``` + +| Extension point | Contract (`sdk`) | Responsibility | +| --- | --- | --- | +| Detector | `sdk.Detector` | Turn evidence (lockfile, manifest, SBOM) into a dependency graph | +| Matcher | `sdk.Matcher` | Enrich packages with vulnerability, license, or lifecycle data | +| Analyzer | `sdk.Analyzer` | Annotate `sdk.Vulnerability.Reachability` for a language | +| Auditor | `sdk.Auditor` | Evaluate policy and emit reference-style findings | + +External plugins run as versioned (`v1`) gRPC binaries and participate in the same runtime planning as built-ins: detector plugins declare evidence patterns and join subproject discovery; matcher, analyzer, and auditor plugins are selected with the same `--matchers` / `--analyzers` / `--auditors` selector grammar. Plugins are disabled until explicitly enabled. See [PLUGINS.md](PLUGINS.md) for the trust model and authoring guides. + ### Decision: YAML configuration is nested at the file boundary Bomly's YAML files use strict nested groups such as `target`, `analysis`, `policy`, `network.proxy`, and `matchers.osv`, while `config.Resolved` remains flat. Nesting keeps customer-authored files readable without spreading YAML organization through the CLI and engine. Each YAML leaf maps back to one flat runtime field, and layered files preserve explicit zero values, including empty lists. Unknown keys and the former flat YAML keys fail with migration guidance so typos cannot silently disable requested behavior. diff --git a/docs/AUDITORS.md b/docs/AUDITORS.md index 42db660e..b5213f3b 100644 --- a/docs/AUDITORS.md +++ b/docs/AUDITORS.md @@ -10,6 +10,16 @@ bomly scan --enrich --audit --fail-on high `--audit` alone is useful when you have ingested an SBOM that already carries vulnerability data, or when a matcher ran in a previous step. +## Built-in auditors + +| Auditor | Checks | Policy flags | +| --- | --- | --- | +| [`vulnerability`](auditors/vulnerability.md) | Enriched advisories vs. severity / allowlist policy | `--fail-on`, `--allow-vulnerability-id` | +| [`license`](auditors/license.md) | Package licenses vs. allow/deny SPDX policy | `--allow-license`, `--deny-license`, `--license-exempt-package` | +| [`package`](auditors/package.md) | Denied packages and typosquatted names | `--deny-package`, `--deny-group`, `--protected-package`, `--typosquat-threshold`, `--typosquat-mode` | + +Select a subset with the `--auditors` selector (e.g. `--auditors license`). See the [per-auditor reference](auditors/) for options, examples, and limitations. + ## When auditors run - `bomly scan --audit` evaluates the full graph. @@ -97,6 +107,7 @@ bomly diff --base main --head HEAD --enrich --audit --fail-on high ## See also +- [Per-auditor reference](auditors/) — options, examples, and limitations for each built-in auditor - [Exit codes](EXIT_CODES.md) — full table of process exit values - [Reachability](REACHABILITY.md) — narrowing findings to symbols actually called - [Output formats](OUTPUT_FORMATS.md) — text, JSON, SARIF rendering details diff --git a/docs/CI_INTEGRATION.md b/docs/CI_INTEGRATION.md index d2afcf03..6bdcd9fa 100644 --- a/docs/CI_INTEGRATION.md +++ b/docs/CI_INTEGRATION.md @@ -79,6 +79,36 @@ This fails only when the PR **introduces** a new high finding, ignoring pre-exis Cuts cold-start enrichment time from minutes to seconds. Cache TTLs are listed in [Matchers](MATCHERS.md#cache); a 24h TTL means cache hits stay fresh for a day even if the cache key changes. +### Turnkey PR reviews with the Bomly review action + +The recipes above call the CLI directly. For GitHub pull requests, the [**Bomly review action**](https://github.com/bomly-dev/bomly-review-action) wraps the same `bomly diff --enrich --audit` flow into a single step: it installs the CLI, diffs the PR against its base, posts a Markdown summary comment, and uploads SARIF to the Security tab. + +```yaml +name: Bomly Review + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + security-events: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history so the base and head refs resolve + - uses: bomly-dev/bomly-review-action@v1 + with: + fail-on-severity: high + deny-licenses: GPL-3.0-only +``` + +The action's inputs map onto the CLI policy flags (`fail-on-severity` → `--fail-on`, `deny-licenses` → `--deny-license`, `protected-packages` → `--protected-package`, and so on), so the policy you enforce locally is the policy it enforces on PRs. See the [action README](https://github.com/bomly-dev/bomly-review-action#readme) for the full input list. Bomly dogfoods this action — see the `Bomly Review` workflow in [docs/CI.md](CI.md). + ## GitLab CI ```yaml diff --git a/docs/README.md b/docs/README.md index f0c7ead4..aae1eb9a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ Task-oriented walkthroughs. - [Installation](INSTALLATION.md) — install methods, `bomly` vs `bomly-lite`, checksum verification, uninstall - [Getting Started](GETTING_STARTED.md) — first scan, enrich, audit, diff +- [Use Cases](USE_CASES.md) — recipes for PR gates, SBOMs, triage, license and offline scans - [Scan Targets](SCAN_TARGETS.md) — directories, Git repos, containers, SBOMs - [Output Formats](OUTPUT_FORMATS.md) — text, JSON, SARIF, SBOM - [SBOM Formats](SBOM.md) — SPDX vs. CycloneDX, write and ingest @@ -38,6 +39,7 @@ Generated from code. Treat as authoritative. - [Exit Codes](EXIT_CODES.md) — what each process exit value means - [Detector Ecosystem Guides](detectors/ecosystems/) — per-ecosystem detector chains - [Matcher Reference](matchers/) — per-matcher behavior, cache, output +- [Auditor Reference](auditors/) — per-auditor options, examples, limitations - [JSON Schemas](schemas/scan.md) — scan, explain, diff output shapes ## Project diff --git a/docs/USE_CASES.md b/docs/USE_CASES.md new file mode 100644 index 00000000..18ac530f --- /dev/null +++ b/docs/USE_CASES.md @@ -0,0 +1,113 @@ +# Use Cases + +Recipes for the jobs people actually use Bomly for. Each one is a goal, the command that does it, what you get back, and where to go deeper. New to Bomly? Start with [Getting Started](GETTING_STARTED.md) first. + +## Gate a pull request on new vulnerabilities + +**Goal:** fail a PR when it *introduces* a high-severity vulnerability, without nagging about pre-existing ones. + +```bash +bomly diff --base main --head HEAD --enrich --audit --fail-on high +``` + +`diff` classifies findings as introduced / resolved / persisted and only `--fail-on` matches against the introduced set, so a clean PR passes even on a repo with known debt. Exit code `2` means a new high finding; see [Exit codes](EXIT_CODES.md). + +→ Turnkey version for GitHub PRs: the [Bomly review action](https://github.com/bomly-dev/bomly-review-action) ([setup](CI_INTEGRATION.md#turnkey-pr-reviews-with-the-bomly-review-action)). + +## Generate and publish an SBOM + +**Goal:** produce SPDX and CycloneDX SBOMs as build artifacts. + +```bash +bomly scan -o spdx=sbom.spdx.json -o cyclonedx=sbom.cdx.json +``` + +Both files are written in one pass from the same resolved graph. Use `--format spdx` (or `cyclonedx`) to stream a single SBOM to stdout instead. Details: [SBOM formats](SBOM.md). + +## Triage vulnerabilities by reachability + +**Goal:** cut a long advisory list down to the ones your code actually calls. + +```bash +bomly scan --enrich --audit --analyze --fail-on high --fail-on reachable +``` + +`--analyze` annotates each advisory with a reachability status; combining `--fail-on high --fail-on reachable` fails only on advisories that are both high severity *and* reachable. Reachability is experimental and tier-dependent — an `unknown` status is not "safe." Read [Reachability](REACHABILITY.md) before relying on this gate. + +## Enforce a license policy + +**Goal:** block dependencies under licenses you can't ship. + +```bash +# Allowlist: permit only these, fail on anything else +bomly scan --enrich --audit \ + --allow-license MIT --allow-license Apache-2.0 --allow-license BSD-3-Clause \ + --fail-on any +``` + +Licenses are matched as SPDX expressions. Use `--deny-license` to block specific licenses instead of allowlisting, and `--license-exempt-package` to waive one package. See the [license auditor](auditors/license.md). + +## Catch typosquats and banned packages + +**Goal:** flag dependency names that impersonate packages you trust, or that you've banned outright. + +```bash +bomly scan --audit \ + --protected-package react --protected-package lodash \ + --typosquat-threshold 0.85 \ + --deny-package event-stream \ + --fail-on any +``` + +This check is name-based and needs no enrichment, so it runs fully offline. See the [package auditor](auditors/package.md). + +## Scan offline / air-gapped + +**Goal:** analyze dependencies with zero outbound network calls. + +```bash +bomly scan # native detectors + lockfiles, no network +bomly scan --sbom --path sbom.json # audit an SBOM you already have +``` + +Without `--enrich`, matchers make **zero** outbound HTTP calls. Note that some build-tool detectors (Go, Maven, Gradle) may fetch packages during normal resolution — pre-warm the local cache or commit a lockfile to stay fully offline. See [Detectors → Network behavior](DETECTORS.md#network-behavior). + +## Diff two releases + +**Goal:** see what changed in your dependency tree between two versions. + +```bash +# Between Git refs +bomly diff --base v1.2.0 --head v1.3.0 --enrich --audit + +# Between two SBOM files (no checkout needed) +bomly diff --sbom --base old.spdx.json --head new.spdx.json +``` + +You get added, removed, and updated dependencies, plus introduced/resolved findings when `--audit` is set. Great for release notes and upgrade reviews. + +## Understand why a dependency is there + +**Goal:** find the path that pulled a transitive package into your build. + +```bash +bomly explain lodash +``` + +`explain` prints the dependency path(s) that introduced the package. Add `--audit` to see findings in that path's context. + +## Explore results interactively + +**Goal:** browse a scan without parsing JSON. + +```bash +bomly scan --enrich --interactive +``` + +Opens the terminal UI to navigate packages, findings, and dependency paths. Keybindings: [TUI](TUI.md). + +## See also + +- [CI Integration](CI_INTEGRATION.md) — drop-in recipes for GitHub Actions, GitLab, Jenkins, Azure, CircleCI +- [Output Formats](OUTPUT_FORMATS.md) — text, JSON, SARIF, SBOM +- [Config Reference](CONFIG_REFERENCE.md) — every flag, env var, and config key diff --git a/docs/auditors/README.md b/docs/auditors/README.md new file mode 100644 index 00000000..4014dfd4 --- /dev/null +++ b/docs/auditors/README.md @@ -0,0 +1,7 @@ +# Auditor Guides + +These generated pages explain Bomly's built-in policy auditors. See [AUDITORS.md](../AUDITORS.md) for the shared finding model, severity grammar, and `--fail-on` behavior. + +- [License](license.md) +- [Package](package.md) +- [Vulnerability](vulnerability.md) diff --git a/docs/auditors/license.md b/docs/auditors/license.md new file mode 100644 index 00000000..b81ffe29 --- /dev/null +++ b/docs/auditors/license.md @@ -0,0 +1,58 @@ +# License auditor + + + +Evaluates package licenses against allow/deny SPDX policy. + +| Property | Value | +| --- | --- | +| Auditor name | `license` | +| Finding kind | license | +| Runs with `--audit` | Yes | +| Needs enrichment data | No | +| Policy flags | `--allow-license`, `--deny-license`, `--license-exempt-package` | +| Finding reasons | denied license, license not in allowlist, missing license | + +## User notes + +Works on licenses already on the graph; run `--enrich` first when native detection did not resolve a license. `--license-exempt-package` waives the policy for specific packages. License values are matched as SPDX expressions. + +## What the `license` auditor does + +It reads each package's license, parses it as an SPDX expression, and checks it against your allow/deny policy. A package fails policy when its license is explicitly denied, or — when an allowlist is set — when its license is not on the allowlist. Packages with no resolved license can be flagged so license gaps don't pass silently. + +License data comes from native detection or from a license matcher (deps.dev, ClearlyDefined). Run `--enrich` first if your graph is missing licenses. + +## Options + +| Flag | Effect | +| --- | --- | +| `--allow-license ` | Permit only these SPDX licenses; anything else fails. Repeatable. | +| `--deny-license ` | Fail on these SPDX licenses regardless of the allowlist. Repeatable. | +| `--license-exempt-package ` | Waive license policy for a specific package. Repeatable. | + +`--allow-license` and `--deny-license` accept SPDX identifiers (e.g. `MIT`, `Apache-2.0`, `GPL-3.0-only`). + +## Examples + +```bash +# Permit only permissive licenses +bomly scan --enrich --audit \ + --allow-license MIT --allow-license Apache-2.0 --allow-license BSD-3-Clause \ + --fail-on any + +# Block copyleft outright, allow everything else +bomly scan --enrich --audit --deny-license GPL-3.0-only --fail-on any + +# Allowlist, but exempt one internal package shipped under a custom license +bomly scan --enrich --audit \ + --allow-license MIT \ + --license-exempt-package my-internal-lib \ + --fail-on any +``` + +## Limitations + +- **SPDX expressions only.** A non-SPDX license string (a raw URL, a custom name) won't match an allow/deny SPDX identifier and may surface as a missing/unknown license. +- **One license source wins.** When detection and a matcher disagree, the value on the package is what gets evaluated; run with `--enrich` for the most complete coverage. +- **Exemptions are by package name**, so a name that appears in multiple ecosystems is exempted in all of them. diff --git a/docs/auditors/package.md b/docs/auditors/package.md new file mode 100644 index 00000000..bdfbe694 --- /dev/null +++ b/docs/auditors/package.md @@ -0,0 +1,59 @@ +# Package auditor + + + +Protects against denied packages and suspiciously similar (typosquatted) package names. + +| Property | Value | +| --- | --- | +| Auditor name | `package` | +| Finding kind | package | +| Runs with `--audit` | Yes | +| Needs enrichment data | No | +| Policy flags | `--deny-package`, `--deny-group`, `--protected-package`, `--typosquat-threshold`, `--typosquat-mode` | +| Finding reasons | denied package, denied group, typosquat of protected package | + +## User notes + +Name-based, so it needs no enrichment and runs fully offline. Declare the packages you trust with `--protected-package`; Bomly flags lookalikes within `--typosquat-threshold`. `--typosquat-mode` controls how aggressively names are compared. + +## What the `package` auditor does + +It guards the *identity* of your dependencies rather than their contents. Two checks: + +1. **Denylist** — flag any package (or any package in a denied group/namespace) you have decided to ban outright. +2. **Typosquat** — flag packages whose names are suspiciously close to a package you trust, catching `reqeusts`, `loadsh`, or `cross-env`-style lookalikes. + +Both checks are name-based, so this auditor needs no enrichment and runs fully offline. + +## Options + +| Flag | Effect | +| --- | --- | +| `--deny-package ` | Fail when this package is present. Repeatable. | +| `--deny-group ` | Fail on any package in this group/namespace (e.g. a Maven groupId). Repeatable. | +| `--protected-package ` | A trusted name; lookalikes within the threshold are flagged as possible typosquats. Repeatable. | +| `--typosquat-threshold <0..1>` | How close a name must be to a protected package to be flagged. Higher = stricter. | +| `--typosquat-mode ` | How names are compared when scoring similarity. | + +## Examples + +```bash +# Ban a known-bad package +bomly scan --audit --deny-package event-stream --fail-on any + +# Ban an entire namespace +bomly scan --audit --deny-group com.evil --fail-on any + +# Catch typosquats of the packages you actually depend on +bomly scan --audit \ + --protected-package react --protected-package lodash \ + --typosquat-threshold 0.85 \ + --fail-on any +``` + +## Limitations + +- **Names, not behavior.** This auditor cannot tell whether a package is malicious — only whether its name is denied or resembles a protected one. Pair it with the vulnerability auditor for content risk. +- **Typosquat tuning is a trade-off.** A low threshold catches more lookalikes but raises false positives on legitimately similar names; tune `--typosquat-threshold` per project. +- **Protected lists are explicit.** Bomly only checks lookalikes against names you pass with `--protected-package`; it does not infer a baseline of "popular" packages. diff --git a/docs/auditors/vulnerability.md b/docs/auditors/vulnerability.md new file mode 100644 index 00000000..91f20cae --- /dev/null +++ b/docs/auditors/vulnerability.md @@ -0,0 +1,52 @@ +# Vulnerability auditor + + + +Evaluates enriched vulnerability records against severity and allowlist policy. + +| Property | Value | +| --- | --- | +| Auditor name | `vulnerability` | +| Finding kind | vulnerability | +| Runs with `--audit` | Yes | +| Needs enrichment data | Yes | +| Policy flags | `--fail-on`, `--allow-vulnerability-id` | +| Finding reasons | severity threshold, reachable symbol, KEV listing | + +## User notes + +Needs vulnerability data on packages, so pair it with `--enrich` (or ingest an SBOM that already carries advisories). `--allow-vulnerability-id` suppresses specific CVE/GHSA IDs you have triaged. + +## What the `vulnerability` auditor does + +It walks every package in the resolved graph, reads the vulnerability records that matchers attached during enrichment, and turns each one into a finding when it matches policy. It does not fetch anything itself — it evaluates data that is already on the graph. + +A finding is produced for every advisory on a package unless its ID is in the allowlist. Whether a finding *fails the build* is decided separately by `--fail-on` (see [AUDITORS.md](../AUDITORS.md#--fail-on)). + +## Options + +| Flag | Effect | +| --- | --- | +| `--fail-on ` | Turns matching findings into a non-zero exit code. Repeat to AND constraints. | +| `--allow-vulnerability-id ` | Suppress a specific advisory (CVE or GHSA) you have already triaged. Repeatable. | + +## Examples + +```bash +# Fail CI on any high-or-critical vulnerability +bomly scan --enrich --audit --fail-on high + +# Same gate, but accept one advisory you've assessed as not exploitable +bomly scan --enrich --audit --fail-on high \ + --allow-vulnerability-id GHSA-xxxx-yyyy-zzzz + +# Only fail when a high-severity advisory is also reachable +bomly scan --enrich --audit --analyze \ + --fail-on high --fail-on reachable +``` + +## Limitations + +- **Garbage in, garbage out.** The auditor is only as good as the matcher data. Run `--enrich` (or ingest an SBOM with advisories) or it will find nothing. +- **Allowlisting is by ID, not by package.** `--allow-vulnerability-id` suppresses an advisory everywhere it appears, across every package. +- **Reachability is experimental.** `--fail-on reachable` depends on `--analyze`; an `unknown` reachability status does not mean "safe." See [REACHABILITY.md](../REACHABILITY.md). diff --git a/internal/support/component_docs.go b/internal/support/component_docs.go index 067044a2..3388974b 100644 --- a/internal/support/component_docs.go +++ b/internal/support/component_docs.go @@ -329,6 +329,16 @@ bomly scan --enrich --audit --fail-on high `+"`--audit`"+` alone is useful when you have ingested an SBOM that already carries vulnerability data, or when a matcher ran in a previous step. +## Built-in auditors + +| Auditor | Checks | Policy flags | +| --- | --- | --- | +| [`+"`vulnerability`"+`](auditors/vulnerability.md) | Enriched advisories vs. severity / allowlist policy | `+"`--fail-on`"+`, `+"`--allow-vulnerability-id`"+` | +| [`+"`license`"+`](auditors/license.md) | Package licenses vs. allow/deny SPDX policy | `+"`--allow-license`"+`, `+"`--deny-license`"+`, `+"`--license-exempt-package`"+` | +| [`+"`package`"+`](auditors/package.md) | Denied packages and typosquatted names | `+"`--deny-package`"+`, `+"`--deny-group`"+`, `+"`--protected-package`"+`, `+"`--typosquat-threshold`"+`, `+"`--typosquat-mode`"+` | + +Select a subset with the `+"`--auditors`"+` selector (e.g. `+"`--auditors license`"+`). See the [per-auditor reference](auditors/) for options, examples, and limitations. + ## When auditors run - `+"`bomly scan --audit`"+` evaluates the full graph. @@ -416,6 +426,7 @@ bomly diff --base main --head HEAD --enrich --audit --fail-on high ## See also +- [Per-auditor reference](auditors/) — options, examples, and limitations for each built-in auditor - [Exit codes](EXIT_CODES.md) — full table of process exit values - [Reachability](REACHABILITY.md) — narrowing findings to symbols actually called - [Output formats](OUTPUT_FORMATS.md) — text, JSON, SARIF rendering details @@ -441,6 +452,9 @@ func WriteComponentDocs(docsDir string) error { if err := writeMatcherDocs(filepath.Join(docsDir, "matchers")); err != nil { return err } + if err := writeAuditorDocs(filepath.Join(docsDir, "auditors")); err != nil { + return err + } return nil } @@ -610,6 +624,121 @@ func renderMatcherMarkdown(descriptor sdk.MatcherDescriptor) string { return b.String() } +func writeAuditorDocs(outputDir string) error { + if err := os.RemoveAll(outputDir); err != nil { + return fmt.Errorf("clean %s: %w", outputDir, err) + } + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return fmt.Errorf("create %s: %w", outputDir, err) + } + reg := registry.NewRegistry(registry.Configs{}, *zap.NewNop()) + reg.Build() + descriptors := reg.AuditorDescriptors() + sort.Slice(descriptors, func(i, j int) bool { return descriptors[i].Name < descriptors[j].Name }) + for _, descriptor := range descriptors { + if err := writeMarkdown(filepath.Join(outputDir, descriptor.Name+".md"), renderAuditorMarkdown(descriptor)); err != nil { + return err + } + } + return writeMarkdown(filepath.Join(outputDir, "README.md"), renderAuditorIndex(descriptors)) +} + +func renderAuditorIndex(descriptors []sdk.AuditorDescriptor) string { + var b strings.Builder + b.WriteString("# Auditor Guides\n\n") + b.WriteString("These generated pages explain Bomly's built-in policy auditors. See [AUDITORS.md](../AUDITORS.md) for the shared finding model, severity grammar, and `--fail-on` behavior.\n\n") + for _, descriptor := range descriptors { + _, _ = fmt.Fprintf(&b, "- [%s](%s.md)\n", humanAuditorTitle(descriptor.Name), descriptor.Name) + } + return b.String() +} + +func renderAuditorMarkdown(descriptor sdk.AuditorDescriptor) string { + behavior := auditorBehavior(descriptor.Name) + var b strings.Builder + _, _ = fmt.Fprintf(&b, "# %s auditor\n\n", humanAuditorTitle(descriptor.Name)) + b.WriteString(generatedBanner + "\n\n") + _, _ = fmt.Fprintf(&b, "%s\n\n", behavior.Summary) + b.WriteString("| Property | Value |\n") + b.WriteString("| --- | --- |\n") + _, _ = fmt.Fprintf(&b, "| Auditor name | `%s` |\n", descriptor.Name) + _, _ = fmt.Fprintf(&b, "| Finding kind | %s |\n", behavior.FindingKind) + _, _ = fmt.Fprintf(&b, "| Runs with `--audit` | Yes |\n") + _, _ = fmt.Fprintf(&b, "| Needs enrichment data | %s |\n", yesNo(behavior.RequiresEnrich)) + _, _ = fmt.Fprintf(&b, "| Policy flags | %s |\n", codeListOrDash(behavior.PolicyFlags)) + _, _ = fmt.Fprintf(&b, "| Finding reasons | %s |\n", strings.Join(behavior.Reasons, ", ")) + if len(descriptor.Tags) > 0 { + _, _ = fmt.Fprintf(&b, "| Tags | %s |\n", codeList(descriptor.Tags)) + } + b.WriteString("\n## User notes\n\n") + b.WriteString(behavior.Notes) + b.WriteString("\n") + if prose := loadProse("auditors", descriptor.Name); prose != "" { + b.WriteString("\n") + b.WriteString(prose) + } + return b.String() +} + +type auditorDocBehavior struct { + Summary string + FindingKind string + RequiresEnrich bool + PolicyFlags []string + Reasons []string + Notes string +} + +func auditorBehavior(name string) auditorDocBehavior { + switch name { + case "vulnerability": + return auditorDocBehavior{ + Summary: "Evaluates enriched vulnerability records against severity and allowlist policy.", + FindingKind: "vulnerability", + RequiresEnrich: true, + PolicyFlags: []string{"--fail-on", "--allow-vulnerability-id"}, + Reasons: []string{"severity threshold", "reachable symbol", "KEV listing"}, + Notes: "Needs vulnerability data on packages, so pair it with `--enrich` (or ingest an SBOM that already carries advisories). `--allow-vulnerability-id` suppresses specific CVE/GHSA IDs you have triaged.", + } + case "license": + return auditorDocBehavior{ + Summary: "Evaluates package licenses against allow/deny SPDX policy.", + FindingKind: "license", + RequiresEnrich: false, + PolicyFlags: []string{"--allow-license", "--deny-license", "--license-exempt-package"}, + Reasons: []string{"denied license", "license not in allowlist", "missing license"}, + Notes: "Works on licenses already on the graph; run `--enrich` first when native detection did not resolve a license. `--license-exempt-package` waives the policy for specific packages. License values are matched as SPDX expressions.", + } + case "package": + return auditorDocBehavior{ + Summary: "Protects against denied packages and suspiciously similar (typosquatted) package names.", + FindingKind: "package", + RequiresEnrich: false, + PolicyFlags: []string{"--deny-package", "--deny-group", "--protected-package", "--typosquat-threshold", "--typosquat-mode"}, + Reasons: []string{"denied package", "denied group", "typosquat of protected package"}, + Notes: "Name-based, so it needs no enrichment and runs fully offline. Declare the packages you trust with `--protected-package`; Bomly flags lookalikes within `--typosquat-threshold`. `--typosquat-mode` controls how aggressively names are compared.", + } + default: + return auditorDocBehavior{ + Summary: "Evaluates the dependency graph against policy and produces findings.", + FindingKind: "policy", + RequiresEnrich: false, + PolicyFlags: nil, + Reasons: []string{"policy match"}, + Notes: "Runs when `--audit` is set.", + } + } +} + +func humanAuditorTitle(name string) string { + switch name { + case "osv": + return "OSV" + default: + return titleWords(strings.ReplaceAll(name, "-", " ")) + } +} + func matcherRunsByDefault(name string) bool { switch name { case "grype", "depsdev-license-matcher": diff --git a/internal/support/generate_test.go b/internal/support/generate_test.go index f94a9259..4be1e73c 100644 --- a/internal/support/generate_test.go +++ b/internal/support/generate_test.go @@ -102,6 +102,8 @@ func TestRenderComponentDocs_IncludeGeneratedGuides(t *testing.T) { filepath.Join(tmp, "detectors", "ecosystems", "npm", "yarn.md"), filepath.Join(tmp, "detectors", "ecosystems", "python", "pip.md"), filepath.Join(tmp, "matchers", "osv.md"), + filepath.Join(tmp, "auditors", "README.md"), + filepath.Join(tmp, "auditors", "vulnerability.md"), } { if _, err := os.Stat(path); err != nil { t.Fatalf("expected generated doc %s: %v", path, err) diff --git a/internal/support/prose/auditors/license.md b/internal/support/prose/auditors/license.md new file mode 100644 index 00000000..f7071757 --- /dev/null +++ b/internal/support/prose/auditors/license.md @@ -0,0 +1,39 @@ +## What the `license` auditor does + +It reads each package's license, parses it as an SPDX expression, and checks it against your allow/deny policy. A package fails policy when its license is explicitly denied, or — when an allowlist is set — when its license is not on the allowlist. Packages with no resolved license can be flagged so license gaps don't pass silently. + +License data comes from native detection or from a license matcher (deps.dev, ClearlyDefined). Run `--enrich` first if your graph is missing licenses. + +## Options + +| Flag | Effect | +| --- | --- | +| `--allow-license ` | Permit only these SPDX licenses; anything else fails. Repeatable. | +| `--deny-license ` | Fail on these SPDX licenses regardless of the allowlist. Repeatable. | +| `--license-exempt-package ` | Waive license policy for a specific package. Repeatable. | + +`--allow-license` and `--deny-license` accept SPDX identifiers (e.g. `MIT`, `Apache-2.0`, `GPL-3.0-only`). + +## Examples + +```bash +# Permit only permissive licenses +bomly scan --enrich --audit \ + --allow-license MIT --allow-license Apache-2.0 --allow-license BSD-3-Clause \ + --fail-on any + +# Block copyleft outright, allow everything else +bomly scan --enrich --audit --deny-license GPL-3.0-only --fail-on any + +# Allowlist, but exempt one internal package shipped under a custom license +bomly scan --enrich --audit \ + --allow-license MIT \ + --license-exempt-package my-internal-lib \ + --fail-on any +``` + +## Limitations + +- **SPDX expressions only.** A non-SPDX license string (a raw URL, a custom name) won't match an allow/deny SPDX identifier and may surface as a missing/unknown license. +- **One license source wins.** When detection and a matcher disagree, the value on the package is what gets evaluated; run with `--enrich` for the most complete coverage. +- **Exemptions are by package name**, so a name that appears in multiple ecosystems is exempted in all of them. diff --git a/internal/support/prose/auditors/package.md b/internal/support/prose/auditors/package.md new file mode 100644 index 00000000..2ab2902d --- /dev/null +++ b/internal/support/prose/auditors/package.md @@ -0,0 +1,40 @@ +## What the `package` auditor does + +It guards the *identity* of your dependencies rather than their contents. Two checks: + +1. **Denylist** — flag any package (or any package in a denied group/namespace) you have decided to ban outright. +2. **Typosquat** — flag packages whose names are suspiciously close to a package you trust, catching `reqeusts`, `loadsh`, or `cross-env`-style lookalikes. + +Both checks are name-based, so this auditor needs no enrichment and runs fully offline. + +## Options + +| Flag | Effect | +| --- | --- | +| `--deny-package ` | Fail when this package is present. Repeatable. | +| `--deny-group ` | Fail on any package in this group/namespace (e.g. a Maven groupId). Repeatable. | +| `--protected-package ` | A trusted name; lookalikes within the threshold are flagged as possible typosquats. Repeatable. | +| `--typosquat-threshold <0..1>` | How close a name must be to a protected package to be flagged. Higher = stricter. | +| `--typosquat-mode ` | How names are compared when scoring similarity. | + +## Examples + +```bash +# Ban a known-bad package +bomly scan --audit --deny-package event-stream --fail-on any + +# Ban an entire namespace +bomly scan --audit --deny-group com.evil --fail-on any + +# Catch typosquats of the packages you actually depend on +bomly scan --audit \ + --protected-package react --protected-package lodash \ + --typosquat-threshold 0.85 \ + --fail-on any +``` + +## Limitations + +- **Names, not behavior.** This auditor cannot tell whether a package is malicious — only whether its name is denied or resembles a protected one. Pair it with the vulnerability auditor for content risk. +- **Typosquat tuning is a trade-off.** A low threshold catches more lookalikes but raises false positives on legitimately similar names; tune `--typosquat-threshold` per project. +- **Protected lists are explicit.** Bomly only checks lookalikes against names you pass with `--protected-package`; it does not infer a baseline of "popular" packages. diff --git a/internal/support/prose/auditors/vulnerability.md b/internal/support/prose/auditors/vulnerability.md new file mode 100644 index 00000000..98e3ea4b --- /dev/null +++ b/internal/support/prose/auditors/vulnerability.md @@ -0,0 +1,33 @@ +## What the `vulnerability` auditor does + +It walks every package in the resolved graph, reads the vulnerability records that matchers attached during enrichment, and turns each one into a finding when it matches policy. It does not fetch anything itself — it evaluates data that is already on the graph. + +A finding is produced for every advisory on a package unless its ID is in the allowlist. Whether a finding *fails the build* is decided separately by `--fail-on` (see [AUDITORS.md](../AUDITORS.md#--fail-on)). + +## Options + +| Flag | Effect | +| --- | --- | +| `--fail-on ` | Turns matching findings into a non-zero exit code. Repeat to AND constraints. | +| `--allow-vulnerability-id ` | Suppress a specific advisory (CVE or GHSA) you have already triaged. Repeatable. | + +## Examples + +```bash +# Fail CI on any high-or-critical vulnerability +bomly scan --enrich --audit --fail-on high + +# Same gate, but accept one advisory you've assessed as not exploitable +bomly scan --enrich --audit --fail-on high \ + --allow-vulnerability-id GHSA-xxxx-yyyy-zzzz + +# Only fail when a high-severity advisory is also reachable +bomly scan --enrich --audit --analyze \ + --fail-on high --fail-on reachable +``` + +## Limitations + +- **Garbage in, garbage out.** The auditor is only as good as the matcher data. Run `--enrich` (or ingest an SBOM with advisories) or it will find nothing. +- **Allowlisting is by ID, not by package.** `--allow-vulnerability-id` suppresses an advisory everywhere it appears, across every package. +- **Reachability is experimental.** `--fail-on reachable` depends on `--analyze`; an `unknown` reachability status does not mean "safe." See [REACHABILITY.md](../REACHABILITY.md). From c7599ce2da4c0f7e4303629e5e20c0abb954a5ce Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 20:08:13 -0700 Subject: [PATCH 05/14] chore: ignore local notes/ directory Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index cd8bf039..a70e1ddc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ coverage # Qodana files qodana.yaml + +# Local scratch notes (not tracked) +/notes From 6f899756e045584a4e9cd13120dfc0147555b469 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 20:08:13 -0700 Subject: [PATCH 06/14] docs: redesign logo to match brand Replace the off-brand cyan cube wordmark with a lockup built from the Bomly favicon: the amber (#E89B5C) terminal-prompt tile plus "Bomly CLI" set in JetBrains Mono, the landing page's mono typeface. The wordmark ink adapts to light/dark via prefers-color-scheme so it reads on both GitHub themes. Adjust the README logo width for the new aspect ratio. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- assets/bomly-cli-wordmark.svg | 40 ++++++++++------------------------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 6a33f1f6..a27cb8d0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Bomly CLI + Bomly CLI

diff --git a/assets/bomly-cli-wordmark.svg b/assets/bomly-cli-wordmark.svg index 903ea61a..fe1a4470 100644 --- a/assets/bomly-cli-wordmark.svg +++ b/assets/bomly-cli-wordmark.svg @@ -1,35 +1,17 @@ - + Bomly CLI - - - - - - - - - - - - - - + - - - - - - - + + + + + - Bomly - - - - - CLI - + Bomly CLI From aabd9d109438576bc1afcbfa8058bd0145925b31 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 20:36:21 -0700 Subject: [PATCH 07/14] Adding SECURITY.md --- SECURITY.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..7e07487e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,50 @@ +# Security Policy + +Bomly is a security-sensitive tool: it scans dependency trees, evaluates policy, and +generates SBOMs that downstream systems rely on. We take vulnerabilities in Bomly +itself seriously and appreciate reports from the community. + +## Reporting a Vulnerability + +**Please do not open a public issue for security vulnerabilities.** + +Report privately through GitHub's +[Private Vulnerability Reporting](https://github.com/bomly-dev/bomly-cli/security/advisories/new) +("Report a vulnerability" under the repository's **Security** tab). If you cannot use +that channel, email instead. This keeps the report confidential +while we investigate and prepare a fix. + +When reporting, please include: + +- A description of the vulnerability and its impact. +- Steps to reproduce (a minimal repro, command line, and affected input if possible). +- The Bomly version (`bomly version`) and your platform. +- Any suggested remediation, if you have one. + +## What to Expect + +- **Acknowledgement:** within 3 business days. +- **Initial assessment:** within 7 business days, including severity and a planned path. +- **Fix and disclosure:** we coordinate a release and a published advisory; we will + credit reporters who wish to be named. + +## Scope + +In scope: + +- The `bomly` CLI and its libraries in this repository. +- The release artifacts published from this repository. + +Out of scope: + +- Vulnerabilities in third-party dependencies (report those upstream; Bomly itself + helps you find them). +- Findings produced *about your dependencies* by a Bomly scan — those describe your + software, not a flaw in Bomly. +- Issues that require a compromised local machine or a malicious build of Bomly. + +## Supported Versions + +Bomly is pre-1.0 and ships frequently. Security fixes target the **latest released +version**. Please upgrade to the most recent release before reporting, in case the +issue is already fixed. From 15e8a52d08a6a7c585b2c543ee862ea0c43071ef Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Wed, 10 Jun 2026 23:53:02 -0700 Subject: [PATCH 08/14] docs: add Contributor Covenant 3.0 code of conduct Add CODE_OF_CONDUCT.md (Contributor Covenant 3.0, verbatim) with the reporting channel set to contact@bomly.dev, and link the Code of Conduct and Security Policy from CONTRIBUTING.md. Co-Authored-By: Claude Opus 4.8 --- CODE_OF_CONDUCT.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 ++ 2 files changed, 91 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..a2349725 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,89 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We pledge to make our community welcoming, safe, and equitable for all. + +We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. + + +## Encouraged Behaviors + +While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. + +With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: + +1. Respecting the **purpose of our community**, our activities, and our ways of gathering. +2. Engaging **kindly and honestly** with others. +3. Respecting **different viewpoints** and experiences. +4. **Taking responsibility** for our actions and contributions. +5. Gracefully giving and accepting **constructive feedback**. +6. Committing to **repairing harm** when it occurs. +7. Behaving in other ways that promote and sustain the **well-being of our community**. + + +## Restricted Behaviors + +We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. + +1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. +2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. +3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis of immutable identities or traits. +4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. +5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. +6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. +7. Behaving in other ways that **threaten the well-being** of our community. + +### Other Restrictions + +1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. +2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. +3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. +4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. + + +## Reporting an Issue + +Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. + +When an incident does occur, it is important to report it promptly. To report a possible violation, email the maintainers at . Reports are handled confidentially. + +Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. + + +## Addressing and Repairing Harm + +If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. + +1) Warning + 1) Event: A violation involving a single incident or series of incidents. + 2) Consequence: A private, written warning from the Community Moderators. + 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. +2) Temporarily Limited Activities + 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. + 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. + 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. +3) Temporary Suspension + 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. + 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. + 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. +4) Permanent Ban + 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. + 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. + 3) Repair: There is no possible repair in cases of this severity. + +This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. + + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). + +Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) + +For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla's code of conduct team](https://github.com/mozilla/inclusion). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 103f7c5d..7f5167f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Contributing to Bomly +By participating in this project you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). To report a security vulnerability, follow the [Security Policy](SECURITY.md) rather than opening a public issue. + ## Development Setup ### Prerequisites From 8c15e8fc39372f1143b02f220fa59b979e80dc9a Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Thu, 11 Jun 2026 00:11:53 -0700 Subject: [PATCH 09/14] Update AGENTS.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f7511b87..bf62ebfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,7 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full detail. Component ma | `internal/testutil` | Test helpers (fake binary builder) | | `internal/system` | OS-level helpers | -Scan pipeline: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment on the consolidated graph) → audit → format`. +Scan pipeline: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment on the consolidated graph) → analyze (reachability, when --analyze is set) → audit → format`. Runtime preparation is owned by `internal/engine`: build the filtered registry once, index the execution target with that same registry, and reuse the prepared runtime for `scan`, `diff`, `explain`, license enrichment, and auditing. The CLI resolves raw execution targets and flags, but it must not discover subprojects with a separate registry. From d38b95a3a88c3e9ce928a6f15b7391d26fa5f5e6 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Thu, 11 Jun 2026 00:51:14 -0700 Subject: [PATCH 10/14] fix: restore canonical Apache-2.0 license text The LICENSE was a reworded paraphrase of Apache-2.0 (the definitions of "Work", "Contribution", and "Contributor" and the patent-termination clause were all altered). GitHub's licensee therefore classified it as NOASSERTION ("Other") and pkg.go.dev withheld documentation under "license restrictions". Replace it with the verbatim Apache License 2.0 text, with only the appendix copyright filled in (Copyright 2026 bomly.dev), so the license is correctly detected as Apache-2.0. Co-Authored-By: Claude Opus 4.8 --- LICENSE | 143 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/LICENSE b/LICENSE index fd59171e..626cb738 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -32,33 +33,36 @@ not limited to compiled object code, generated documentation, and conversions to other media types. - "Work" shall mean the work of authorship made available under - the License, as indicated by a copyright notice that is included in - or attached to the work (an example is provided in the Appendix below). + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other transformations + editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - "Contribution" shall mean, as submitted to the Licensor for inclusion - in the Work by the copyright owner or by an individual or Legal Entity - authorized to submit on behalf of the copyright owner. For the purposes - of this definition, "submitted" means any form of electronic, verbal, - or written communication sent to the Licensor or its representatives, - including but not limited to communication on electronic mailing lists, - source code control systems, and issue tracking systems that are managed - by, or on behalf of, the Licensor for the purpose of developing and - coordinating such Work, but excluding communication that is - conspicuously marked or designated in writing by the copyright owner - as "Not a Contribution." - - "Contributor" shall mean Licensor and any Legal Entity on behalf of - whom a Contribution has been received by the Licensor and included - within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, @@ -74,22 +78,22 @@ use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their - Contribution(s) alone or by the combined contribution of their - Contribution(s) with the Work to which such Contribution(s) was submitted. - If You institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work or a - Contribution incorporated within the Work constitutes direct or - contributory patent infringement, then any patent licenses granted to - You under this License for that Work shall terminate as of the date - such litigation is filed. + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or Derivative - Works a copy of this License; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and @@ -101,28 +105,28 @@ the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its - distribution, You must include a readable copy of the - attribution notices contained within such NOTICE file, in - at least one of the following places: within a NOTICE text - file distributed as part of the Derivative Works; within - the Source form or documentation, if provided along with the - Derivative Works; or, within a display generated by the - Derivative Works, if and wherever such third-party notices - normally appear. The contents of the NOTICE file are for - informational purposes only and do not modify the License. - You may add Your own attribution notices within Derivative - Works that You distribute, alongside or in addition to the - NOTICE text from the Work, provided that such additional - attribution notices cannot be construed as modifying the License. - - You may add Your own license statement for Your modifications and - may provide additional grant of rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the - Derivative Works, as Your separate written agreement with recipients - provided that, for any modifications made, (i) you include a prominent - notice in each modified file stating that you have modified the file, - and (ii) such modifications are governed by your separate written agreement - rather than this License. + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work @@ -141,34 +145,34 @@ agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any conditions of TITLE, - MERCHANTIBILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely - responsible for determining the appropriateness of using or - redistributing the Work and assume any risks associated with Your - exercise of permissions under this License. + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, - incidental, or exemplary damages of any character arising as a + incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or all other - commercial damages or losses), even if such Contributor has been - advised of the possibility of such damages. + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may offer such - obligations only on Your own behalf and on Your sole responsibility, - not on behalf of any other Contributor, and only if You agree to - indemnify, defend, and hold each Contributor harmless for any - liability incurred by, or claims asserted against, such Contributor - by reason of your accepting any warranty or additional liability. + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS @@ -178,9 +182,10 @@ boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format in question. It may also be - included in the separate file called the NOTICE file in the - distribution alongside the Code file. + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright 2026 bomly.dev From 29997d7a30d041dda49e55325eccef265f353b7b Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Thu, 11 Jun 2026 00:55:33 -0700 Subject: [PATCH 11/14] refactor(engine): fold graph consolidation into the detection stage Consolidation is the tail of detection, not a separate pipeline stage: it merges the per-subproject graphs the detectors produced into the single graph and package registry the rest of the pipeline consumes, and it has no progress stage of its own. Introduce runDetect (= runResolve + runConsolidate) and call it from the scan and explain pipelines instead of invoking the two steps separately. Update the pipeline-order descriptions in CLAUDE.md and AGENTS.md and the Scan Pipeline diagram/stage list in docs/ARCHITECTURE.md to present detection as resolve + consolidate. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- CLAUDE.md | 2 +- docs/ARCHITECTURE.md | 22 ++++++++++------------ internal/engine/pipeline.go | 17 ++++++++++++----- internal/engine/pipeline_explain.go | 5 +---- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf62ebfe..220e783f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,7 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full detail. Component ma | `internal/testutil` | Test helpers (fake binary builder) | | `internal/system` | OS-level helpers | -Scan pipeline: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment on the consolidated graph) → analyze (reachability, when --analyze is set) → audit → format`. +Scan pipeline: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains; resolve + consolidate into one graph) → scopeFilter → match (license enrichment on the consolidated graph) → audit → format`. Consolidation is the tail of the detect stage, not a separate stage. Runtime preparation is owned by `internal/engine`: build the filtered registry once, index the execution target with that same registry, and reuse the prepared runtime for `scan`, `diff`, `explain`, license enrichment, and auditing. The CLI resolves raw execution targets and flags, but it must not discover subprojects with a separate registry. diff --git a/CLAUDE.md b/CLAUDE.md index 155a6828..7001f672 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,7 @@ internal/testutil/ Test helpers (fake binary builder) **`bomly explain`** is implemented by `newExplainCmd` in `internal/cli/explain_cmd.go`. -**Scan pipeline order**: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains) → scopeFilter → consolidate → match (license enrichment) → analyze (reachability, when --analyze is set) → audit → format` +**Scan pipeline order**: `runtimePreparation → subprojectDiscovery → detect (per-package-manager chains; resolve + consolidate into one graph) → scopeFilter → match (license enrichment) → analyze (reachability, when --analyze is set) → audit → format`. Consolidation is the tail of the detect stage (`runDetect` = `runResolve` + `runConsolidate`), not a separate stage. Runtime preparation is owned by `internal/engine` and is reached through CLI option helpers before pipeline execution. The CLI resolves raw targets and flags but must not discover subprojects with a separate registry. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e5782caf..ee5f37fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,29 +51,27 @@ The scan engine is responsible for orchestration, not the CLI command handlers. flowchart LR A[Runtime preparation] B[Subproject discovery] - C[Detector chains with requested scope] - E[Graph consolidation] + C[Detection: detector chains + graph consolidation] F[Matchers] F2[Analyzers] G[Auditors] H[Output rendering] - A --> B --> C --> E --> F --> F2 --> G --> H + A --> B --> C --> F --> F2 --> G --> H ``` Stage summary: 1. Runtime preparation builds the filtered registry and execution plan. 2. Subproject discovery finds supported package-manager roots for the target. -3. Detector chains resolve dependency graphs per package manager. When `--scope` is set, the requested scope is part of the detector request so build-tool detectors can narrow command execution where the package manager supports it. All detector results then pass through the shared SDK scope filter before consolidation. -4. Consolidation merges subproject graphs into a unified view. -5. Matchers enrich packages with additional metadata such as licenses, EOL status, and vulnerability records. -6. Analyzers run when `--analyze` is set. They consume the matched graph and annotate `sdk.Vulnerability.Reachability` (on the PURL-keyed registry package) with status (reachable/unreachable/unknown), tier (symbol/module/package/none), and call paths. Failures degrade to `Status=unknown` rather than aborting the pipeline. See `docs/REACHABILITY.md` for ecosystem coverage and tier semantics. -7. Auditors evaluate policy against the enriched graph + registry pair and create reference-style findings (`PackageRef` + `VulnerabilityID`) when `--audit` is enabled. The built-in `vulnerability`, `license`, and `package` auditors cover advisory thresholds, SPDX policy, and denied or suspicious packages respectively. -8. Users combine `--enrich --audit` when they want external matcher data to feed policy evaluation in the same run. -9. Output rendering emits text, JSON, SARIF, or SBOM documents. - -`bomly explain` reuses the same scope-aware resolution, consolidation, and matching stages, then performs dependency path selection in its explain orchestration before optional component audit. +3. Detection resolves a dependency graph per package manager and then consolidates the per-subproject graphs into the single graph and package registry the rest of the pipeline uses. When `--scope` is set, the requested scope is part of the detector request so build-tool detectors can narrow command execution where the package manager supports it; all detector results pass through the shared SDK scope filter, and consolidation is the tail of this stage rather than a separate step. +4. Matchers enrich packages with additional metadata such as licenses, EOL status, and vulnerability records. +5. Analyzers run when `--analyze` is set. They consume the matched graph and annotate `sdk.Vulnerability.Reachability` (on the PURL-keyed registry package) with status (reachable/unreachable/unknown), tier (symbol/module/package/none), and call paths. Failures degrade to `Status=unknown` rather than aborting the pipeline. See `docs/REACHABILITY.md` for ecosystem coverage and tier semantics. +6. Auditors evaluate policy against the enriched graph + registry pair and create reference-style findings (`PackageRef` + `VulnerabilityID`) when `--audit` is enabled. The built-in `vulnerability`, `license`, and `package` auditors cover advisory thresholds, SPDX policy, and denied or suspicious packages respectively. +7. Users combine `--enrich --audit` when they want external matcher data to feed policy evaluation in the same run. +8. Output rendering emits text, JSON, SARIF, or SBOM documents. + +`bomly explain` reuses the same detection (resolution + consolidation) and matching stages, then performs dependency path selection in its explain orchestration before optional component audit. ## Extensibility diff --git a/internal/engine/pipeline.go b/internal/engine/pipeline.go index 7c38a1d0..fa5c57ff 100644 --- a/internal/engine/pipeline.go +++ b/internal/engine/pipeline.go @@ -11,7 +11,7 @@ import ( ) // Pipeline orchestrates a full scan through a sequence of typed stages: -// detect -> consolidate -> match -> analyze -> audit. +// detect (resolve + consolidate) -> match -> analyze -> audit. type Pipeline struct { Registry *Registry Logger *zap.Logger @@ -47,10 +47,7 @@ func (p *Pipeline) Run(ctx context.Context, req PipelineRequest) (PipelineResult // before policy evaluation. func (p *Pipeline) RunPreAudit(ctx context.Context, req PipelineRequest) (PipelineResult, error) { result := PipelineResult{} - if err := p.runResolve(ctx, &result, req); err != nil { - return result, err - } - if err := p.runConsolidate(&result); err != nil { + if err := p.runDetect(ctx, &result, req); err != nil { return result, err } p.runMatch(ctx, &result, req) @@ -81,6 +78,16 @@ func (p *Pipeline) RunAuditGraph(ctx context.Context, graph *sdk.Graph, registry return auditResult, auditWarnings } +// runDetect is the detection stage: it resolves each subproject's graph and then +// consolidates them into the single graph and package registry the rest of the +// pipeline operates on. Consolidation is the tail of detection, not a separate stage. +func (p *Pipeline) runDetect(ctx context.Context, result *PipelineResult, req PipelineRequest) error { + if err := p.runResolve(ctx, result, req); err != nil { + return err + } + return p.runConsolidate(result) +} + func (p *Pipeline) runResolve(ctx context.Context, result *PipelineResult, req PipelineRequest) error { resolveResults, resolveErr := p.resolveAll(ctx, req) result.ResolveResults = resolveResults diff --git a/internal/engine/pipeline_explain.go b/internal/engine/pipeline_explain.go index aa51f0ab..fcd406c3 100644 --- a/internal/engine/pipeline_explain.go +++ b/internal/engine/pipeline_explain.go @@ -40,10 +40,7 @@ func (p *Pipeline) RunExplain(ctx context.Context, req ExplainRequest) (ExplainR pipeReq.AuditEnabled = false base := PipelineResult{} - if err := p.runResolve(ctx, &base, pipeReq); err != nil { - return ExplainResult{PipelineResult: base}, err - } - if err := p.runConsolidate(&base); err != nil { + if err := p.runDetect(ctx, &base, pipeReq); err != nil { return ExplainResult{PipelineResult: base}, err } p.runMatch(ctx, &base, pipeReq) From f877054f7e9a93670784169ff333ee8bbc2e65dd Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Thu, 11 Jun 2026 00:58:58 -0700 Subject: [PATCH 12/14] docs: auditor YAML config, typosquat details, and plugin links - Document --typosquat-mode values (warn default, fail) and the 0.90 threshold default in the package auditor, correcting the earlier "comparison mode" description, and add a Diff & baselines section explaining how the base graph seeds the protected-name set. - Add a "Configure with a YAML file" section to the auditors overview showing the policy.* block, file locations, and load-order precedence, and add the YAML key beside each flag in every per-auditor page. - Link the public plugin example repos from the matcher and auditor overviews (eol, clearlydefined, meme). Regenerated docs/AUDITORS.md, docs/MATCHERS.md, and docs/auditors/*. Co-Authored-By: Claude Opus 4.8 --- docs/AUDITORS.md | 31 +++++++++++++++- docs/MATCHERS.md | 4 ++- docs/auditors/license.md | 10 +++--- docs/auditors/package.md | 28 +++++++++------ docs/auditors/vulnerability.md | 8 ++--- internal/support/component_docs.go | 35 +++++++++++++++++-- internal/support/prose/auditors/license.md | 10 +++--- internal/support/prose/auditors/package.md | 28 +++++++++------ .../support/prose/auditors/vulnerability.md | 8 ++--- 9 files changed, 118 insertions(+), 44 deletions(-) diff --git a/docs/AUDITORS.md b/docs/AUDITORS.md index b5213f3b..67fde63d 100644 --- a/docs/AUDITORS.md +++ b/docs/AUDITORS.md @@ -18,7 +18,7 @@ bomly scan --enrich --audit --fail-on high | [`license`](auditors/license.md) | Package licenses vs. allow/deny SPDX policy | `--allow-license`, `--deny-license`, `--license-exempt-package` | | [`package`](auditors/package.md) | Denied packages and typosquatted names | `--deny-package`, `--deny-group`, `--protected-package`, `--typosquat-threshold`, `--typosquat-mode` | -Select a subset with the `--auditors` selector (e.g. `--auditors license`). See the [per-auditor reference](auditors/) for options, examples, and limitations. +Select a subset with the `--auditors` selector (e.g. `--auditors license`). See the [per-auditor reference](auditors/) for options, examples, and limitations. Auditors are also a plugin extension point — for a worked example of an external auditor, see the [Meme Dependency Auditor](https://github.com/bomly-dev/bomly-plugin-meme-auditor). ## When auditors run @@ -105,6 +105,35 @@ Combine with `--fail-on` to fail PRs that introduce new high-severity findings w bomly diff --base main --head HEAD --enrich --audit --fail-on high ``` +## Configure with a YAML file + +Compliance policy is usually the same across every scan, so set it once in a config file instead of repeating CLI flags. All auditor settings live under the `policy` key: + +```yaml +policy: + fail_on: [high, reachable] # severity / reachability gates + allow_vulnerability_ids: [GHSA-xxxx-yyyy-zzzz] + allow_licenses: [MIT, Apache-2.0, BSD-3-Clause] + deny_licenses: [GPL-3.0-only] + license_exempt_packages: [my-internal-lib] + deny_packages: [event-stream] + deny_groups: [com.evil] + protected_packages: [react, lodash] + typosquat_threshold: "0.90" + typosquat_mode: warn # warn | fail + warn_only: false +``` + +Bomly merges configuration from these sources, in increasing precedence: + +1. User-level `~/.bomly/config.yaml` — your defaults across every project. +2. Repo-level `/.bomly/config.yaml` — committed policy shared by the team (the usual home for a compliance policy). +3. `--config ` — an explicit file. +4. `BOMLY_*` environment variables. +5. CLI flags. + +So a checked-in `.bomly/config.yaml` defines the team policy, and a CLI flag still overrides it for a one-off run. Every key is listed in [CONFIG_REFERENCE.md](CONFIG_REFERENCE.md). + ## See also - [Per-auditor reference](auditors/) — options, examples, and limitations for each built-in auditor diff --git a/docs/MATCHERS.md b/docs/MATCHERS.md index 31f7df81..2f87aa40 100644 --- a/docs/MATCHERS.md +++ b/docs/MATCHERS.md @@ -10,7 +10,7 @@ Bomly is **offline-safe by default**. Matchers that use the network only run whe | --- | --- | --- | | Vulnerability | `osv`, `grype` | CVE / GHSA / OSV IDs, severity, CVSS, aliases, fixed versions, references, KEV signal | | License | `depsdev-license-matcher` | SPDX expression, declared/discovered split, license source | -| Lifecycle | External plugin | End-of-life status from a plugin such as `eol-lifecycle-matcher` | +| Lifecycle | External plugin | End-of-life status from a plugin such as the [eol-lifecycle-matcher](https://github.com/bomly-dev/bomly-plugin-eol-matcher) | The full live list lives in the CLI: @@ -40,6 +40,8 @@ bomly scan --enrich --matchers -depsdev-license-matcher bomly scan --enrich --matchers +clearlydefined-license-matcher ``` +External matchers are published as plugins — see the [ClearlyDefined License Matcher](https://github.com/bomly-dev/bomly-plugin-clearlydefined-matcher) and [EOL Lifecycle Matcher](https://github.com/bomly-dev/bomly-plugin-eol-matcher) for worked examples, and [PLUGINS.md](PLUGINS.md) to install and enable them. + ## Network endpoints When `--enrich` is set, Bomly may call: diff --git a/docs/auditors/license.md b/docs/auditors/license.md index b81ffe29..51248c71 100644 --- a/docs/auditors/license.md +++ b/docs/auditors/license.md @@ -25,11 +25,11 @@ License data comes from native detection or from a license matcher (deps.dev, Cl ## Options -| Flag | Effect | -| --- | --- | -| `--allow-license ` | Permit only these SPDX licenses; anything else fails. Repeatable. | -| `--deny-license ` | Fail on these SPDX licenses regardless of the allowlist. Repeatable. | -| `--license-exempt-package ` | Waive license policy for a specific package. Repeatable. | +| Flag | YAML key | Effect | +| --- | --- | --- | +| `--allow-license ` | `policy.allow_licenses` | Permit only these SPDX licenses; anything else fails. Repeatable. | +| `--deny-license ` | `policy.deny_licenses` | Fail on these SPDX licenses regardless of the allowlist. Repeatable. | +| `--license-exempt-package ` | `policy.license_exempt_packages` | Waive license policy for a specific package. Repeatable. | `--allow-license` and `--deny-license` accept SPDX identifiers (e.g. `MIT`, `Apache-2.0`, `GPL-3.0-only`). diff --git a/docs/auditors/package.md b/docs/auditors/package.md index bdfbe694..5189a95d 100644 --- a/docs/auditors/package.md +++ b/docs/auditors/package.md @@ -28,13 +28,15 @@ Both checks are name-based, so this auditor needs no enrichment and runs fully o ## Options -| Flag | Effect | -| --- | --- | -| `--deny-package ` | Fail when this package is present. Repeatable. | -| `--deny-group ` | Fail on any package in this group/namespace (e.g. a Maven groupId). Repeatable. | -| `--protected-package ` | A trusted name; lookalikes within the threshold are flagged as possible typosquats. Repeatable. | -| `--typosquat-threshold <0..1>` | How close a name must be to a protected package to be flagged. Higher = stricter. | -| `--typosquat-mode ` | How names are compared when scoring similarity. | +| Flag | YAML key | Effect | +| --- | --- | --- | +| `--deny-package ` | `policy.deny_packages` | Fail when this package is present. Repeatable. | +| `--deny-group ` | `policy.deny_groups` | Fail on any package in this group/namespace (e.g. a Maven groupId). Repeatable. | +| `--protected-package ` | `policy.protected_packages` | A trusted name; lookalikes within the threshold are flagged as possible typosquats. Repeatable. | +| `--typosquat-threshold <0..1>` | `policy.typosquat_threshold` | Similarity score above which a name is treated as a lookalike. Default `0.90`. Higher = stricter (fewer matches). | +| `--typosquat-mode ` | `policy.typosquat_mode` | Disposition for a typosquat finding. `warn` (default) records a warning; `fail` makes it count toward the exit code. | + +`--typosquat-mode` controls only the *disposition* of typosquat findings, not how names are compared. The two accepted values are `warn` (the default) and `fail`. Note that even in `fail` mode a finding still has to match `--fail-on` to change the exit code, so a typical strict gate is `--typosquat-mode fail --fail-on any`. ## Examples @@ -45,15 +47,19 @@ bomly scan --audit --deny-package event-stream --fail-on any # Ban an entire namespace bomly scan --audit --deny-group com.evil --fail-on any -# Catch typosquats of the packages you actually depend on +# Catch typosquats of the packages you actually depend on, and fail on them bomly scan --audit \ --protected-package react --protected-package lodash \ - --typosquat-threshold 0.85 \ + --typosquat-threshold 0.85 --typosquat-mode fail \ --fail-on any ``` +## Diff and baselines + +Under `bomly diff`, the base side of the comparison acts as a trusted baseline for the typosquat check. Bomly seeds the protected-name set with the package names already present in the base graph, and any package whose ID *or* display name already existed in the base is skipped. The practical effect: only **newly introduced** names are typosquat-checked — against both your `--protected-package` list and everything that was already in the tree — so a name that has lived in the project for releases is never flagged, while a freshly added lookalike is. Findings are then classified introduced / resolved / persisted like any other auditor (see [AUDITORS.md](../AUDITORS.md#diff-and-auditing)). + ## Limitations - **Names, not behavior.** This auditor cannot tell whether a package is malicious — only whether its name is denied or resembles a protected one. Pair it with the vulnerability auditor for content risk. -- **Typosquat tuning is a trade-off.** A low threshold catches more lookalikes but raises false positives on legitimately similar names; tune `--typosquat-threshold` per project. -- **Protected lists are explicit.** Bomly only checks lookalikes against names you pass with `--protected-package`; it does not infer a baseline of "popular" packages. +- **Typosquat tuning is a trade-off.** A lower threshold catches more lookalikes but raises false positives on legitimately similar names; tune `--typosquat-threshold` per project. +- **Protected lists are explicit.** Outside of `diff`, Bomly only checks lookalikes against names you pass with `--protected-package`; it does not infer a baseline of "popular" packages. diff --git a/docs/auditors/vulnerability.md b/docs/auditors/vulnerability.md index 91f20cae..c1e5b23b 100644 --- a/docs/auditors/vulnerability.md +++ b/docs/auditors/vulnerability.md @@ -25,10 +25,10 @@ A finding is produced for every advisory on a package unless its ID is in the al ## Options -| Flag | Effect | -| --- | --- | -| `--fail-on ` | Turns matching findings into a non-zero exit code. Repeat to AND constraints. | -| `--allow-vulnerability-id ` | Suppress a specific advisory (CVE or GHSA) you have already triaged. Repeatable. | +| Flag | YAML key | Effect | +| --- | --- | --- | +| `--fail-on ` | `policy.fail_on` | Turns matching findings into a non-zero exit code. Repeat to AND constraints. | +| `--allow-vulnerability-id ` | `policy.allow_vulnerability_ids` | Suppress a specific advisory (CVE or GHSA) you have already triaged. Repeatable. | ## Examples diff --git a/internal/support/component_docs.go b/internal/support/component_docs.go index 3388974b..93206938 100644 --- a/internal/support/component_docs.go +++ b/internal/support/component_docs.go @@ -229,7 +229,7 @@ Bomly is **offline-safe by default**. Matchers that use the network only run whe | --- | --- | --- | | Vulnerability | `+"`osv`"+`, `+"`grype`"+` | CVE / GHSA / OSV IDs, severity, CVSS, aliases, fixed versions, references, KEV signal | | License | `+"`depsdev-license-matcher`"+` | SPDX expression, declared/discovered split, license source | -| Lifecycle | External plugin | End-of-life status from a plugin such as `+"`eol-lifecycle-matcher`"+` | +| Lifecycle | External plugin | End-of-life status from a plugin such as the [eol-lifecycle-matcher](https://github.com/bomly-dev/bomly-plugin-eol-matcher) | The full live list lives in the CLI: @@ -259,6 +259,8 @@ bomly scan --enrich --matchers -depsdev-license-matcher bomly scan --enrich --matchers +clearlydefined-license-matcher `+"```"+` +External matchers are published as plugins — see the [ClearlyDefined License Matcher](https://github.com/bomly-dev/bomly-plugin-clearlydefined-matcher) and [EOL Lifecycle Matcher](https://github.com/bomly-dev/bomly-plugin-eol-matcher) for worked examples, and [PLUGINS.md](PLUGINS.md) to install and enable them. + ## Network endpoints When `+"`--enrich`"+` is set, Bomly may call: @@ -337,7 +339,7 @@ bomly scan --enrich --audit --fail-on high | [`+"`license`"+`](auditors/license.md) | Package licenses vs. allow/deny SPDX policy | `+"`--allow-license`"+`, `+"`--deny-license`"+`, `+"`--license-exempt-package`"+` | | [`+"`package`"+`](auditors/package.md) | Denied packages and typosquatted names | `+"`--deny-package`"+`, `+"`--deny-group`"+`, `+"`--protected-package`"+`, `+"`--typosquat-threshold`"+`, `+"`--typosquat-mode`"+` | -Select a subset with the `+"`--auditors`"+` selector (e.g. `+"`--auditors license`"+`). See the [per-auditor reference](auditors/) for options, examples, and limitations. +Select a subset with the `+"`--auditors`"+` selector (e.g. `+"`--auditors license`"+`). See the [per-auditor reference](auditors/) for options, examples, and limitations. Auditors are also a plugin extension point — for a worked example of an external auditor, see the [Meme Dependency Auditor](https://github.com/bomly-dev/bomly-plugin-meme-auditor). ## When auditors run @@ -424,6 +426,35 @@ Combine with `+"`--fail-on`"+` to fail PRs that introduce new high-severity find bomly diff --base main --head HEAD --enrich --audit --fail-on high `+"```"+` +## Configure with a YAML file + +Compliance policy is usually the same across every scan, so set it once in a config file instead of repeating CLI flags. All auditor settings live under the `+"`policy`"+` key: + +`+"```yaml"+` +policy: + fail_on: [high, reachable] # severity / reachability gates + allow_vulnerability_ids: [GHSA-xxxx-yyyy-zzzz] + allow_licenses: [MIT, Apache-2.0, BSD-3-Clause] + deny_licenses: [GPL-3.0-only] + license_exempt_packages: [my-internal-lib] + deny_packages: [event-stream] + deny_groups: [com.evil] + protected_packages: [react, lodash] + typosquat_threshold: "0.90" + typosquat_mode: warn # warn | fail + warn_only: false +`+"```"+` + +Bomly merges configuration from these sources, in increasing precedence: + +1. User-level `+"`~/.bomly/config.yaml`"+` — your defaults across every project. +2. Repo-level `+"`/.bomly/config.yaml`"+` — committed policy shared by the team (the usual home for a compliance policy). +3. `+"`--config `"+` — an explicit file. +4. `+"`BOMLY_*`"+` environment variables. +5. CLI flags. + +So a checked-in `+"`.bomly/config.yaml`"+` defines the team policy, and a CLI flag still overrides it for a one-off run. Every key is listed in [CONFIG_REFERENCE.md](CONFIG_REFERENCE.md). + ## See also - [Per-auditor reference](auditors/) — options, examples, and limitations for each built-in auditor diff --git a/internal/support/prose/auditors/license.md b/internal/support/prose/auditors/license.md index f7071757..92f9ffa1 100644 --- a/internal/support/prose/auditors/license.md +++ b/internal/support/prose/auditors/license.md @@ -6,11 +6,11 @@ License data comes from native detection or from a license matcher (deps.dev, Cl ## Options -| Flag | Effect | -| --- | --- | -| `--allow-license ` | Permit only these SPDX licenses; anything else fails. Repeatable. | -| `--deny-license ` | Fail on these SPDX licenses regardless of the allowlist. Repeatable. | -| `--license-exempt-package ` | Waive license policy for a specific package. Repeatable. | +| Flag | YAML key | Effect | +| --- | --- | --- | +| `--allow-license ` | `policy.allow_licenses` | Permit only these SPDX licenses; anything else fails. Repeatable. | +| `--deny-license ` | `policy.deny_licenses` | Fail on these SPDX licenses regardless of the allowlist. Repeatable. | +| `--license-exempt-package ` | `policy.license_exempt_packages` | Waive license policy for a specific package. Repeatable. | `--allow-license` and `--deny-license` accept SPDX identifiers (e.g. `MIT`, `Apache-2.0`, `GPL-3.0-only`). diff --git a/internal/support/prose/auditors/package.md b/internal/support/prose/auditors/package.md index 2ab2902d..09df1e40 100644 --- a/internal/support/prose/auditors/package.md +++ b/internal/support/prose/auditors/package.md @@ -9,13 +9,15 @@ Both checks are name-based, so this auditor needs no enrichment and runs fully o ## Options -| Flag | Effect | -| --- | --- | -| `--deny-package ` | Fail when this package is present. Repeatable. | -| `--deny-group ` | Fail on any package in this group/namespace (e.g. a Maven groupId). Repeatable. | -| `--protected-package ` | A trusted name; lookalikes within the threshold are flagged as possible typosquats. Repeatable. | -| `--typosquat-threshold <0..1>` | How close a name must be to a protected package to be flagged. Higher = stricter. | -| `--typosquat-mode ` | How names are compared when scoring similarity. | +| Flag | YAML key | Effect | +| --- | --- | --- | +| `--deny-package ` | `policy.deny_packages` | Fail when this package is present. Repeatable. | +| `--deny-group ` | `policy.deny_groups` | Fail on any package in this group/namespace (e.g. a Maven groupId). Repeatable. | +| `--protected-package ` | `policy.protected_packages` | A trusted name; lookalikes within the threshold are flagged as possible typosquats. Repeatable. | +| `--typosquat-threshold <0..1>` | `policy.typosquat_threshold` | Similarity score above which a name is treated as a lookalike. Default `0.90`. Higher = stricter (fewer matches). | +| `--typosquat-mode ` | `policy.typosquat_mode` | Disposition for a typosquat finding. `warn` (default) records a warning; `fail` makes it count toward the exit code. | + +`--typosquat-mode` controls only the *disposition* of typosquat findings, not how names are compared. The two accepted values are `warn` (the default) and `fail`. Note that even in `fail` mode a finding still has to match `--fail-on` to change the exit code, so a typical strict gate is `--typosquat-mode fail --fail-on any`. ## Examples @@ -26,15 +28,19 @@ bomly scan --audit --deny-package event-stream --fail-on any # Ban an entire namespace bomly scan --audit --deny-group com.evil --fail-on any -# Catch typosquats of the packages you actually depend on +# Catch typosquats of the packages you actually depend on, and fail on them bomly scan --audit \ --protected-package react --protected-package lodash \ - --typosquat-threshold 0.85 \ + --typosquat-threshold 0.85 --typosquat-mode fail \ --fail-on any ``` +## Diff and baselines + +Under `bomly diff`, the base side of the comparison acts as a trusted baseline for the typosquat check. Bomly seeds the protected-name set with the package names already present in the base graph, and any package whose ID *or* display name already existed in the base is skipped. The practical effect: only **newly introduced** names are typosquat-checked — against both your `--protected-package` list and everything that was already in the tree — so a name that has lived in the project for releases is never flagged, while a freshly added lookalike is. Findings are then classified introduced / resolved / persisted like any other auditor (see [AUDITORS.md](../AUDITORS.md#diff-and-auditing)). + ## Limitations - **Names, not behavior.** This auditor cannot tell whether a package is malicious — only whether its name is denied or resembles a protected one. Pair it with the vulnerability auditor for content risk. -- **Typosquat tuning is a trade-off.** A low threshold catches more lookalikes but raises false positives on legitimately similar names; tune `--typosquat-threshold` per project. -- **Protected lists are explicit.** Bomly only checks lookalikes against names you pass with `--protected-package`; it does not infer a baseline of "popular" packages. +- **Typosquat tuning is a trade-off.** A lower threshold catches more lookalikes but raises false positives on legitimately similar names; tune `--typosquat-threshold` per project. +- **Protected lists are explicit.** Outside of `diff`, Bomly only checks lookalikes against names you pass with `--protected-package`; it does not infer a baseline of "popular" packages. diff --git a/internal/support/prose/auditors/vulnerability.md b/internal/support/prose/auditors/vulnerability.md index 98e3ea4b..662ca003 100644 --- a/internal/support/prose/auditors/vulnerability.md +++ b/internal/support/prose/auditors/vulnerability.md @@ -6,10 +6,10 @@ A finding is produced for every advisory on a package unless its ID is in the al ## Options -| Flag | Effect | -| --- | --- | -| `--fail-on ` | Turns matching findings into a non-zero exit code. Repeat to AND constraints. | -| `--allow-vulnerability-id ` | Suppress a specific advisory (CVE or GHSA) you have already triaged. Repeatable. | +| Flag | YAML key | Effect | +| --- | --- | --- | +| `--fail-on ` | `policy.fail_on` | Turns matching findings into a non-zero exit code. Repeat to AND constraints. | +| `--allow-vulnerability-id ` | `policy.allow_vulnerability_ids` | Suppress a specific advisory (CVE or GHSA) you have already triaged. Repeatable. | ## Examples From 8598a5a38641459a214589a5450c83270dc774d0 Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Thu, 11 Jun 2026 01:03:44 -0700 Subject: [PATCH 13/14] docs: unify slogan, fix analyzer framing, expand containers, move CI.md - Use the single slogan "Analyze Your Software DNA." in the README, matching the CLI startup logo. - Correct the extensibility story: detector, matcher, and auditor are pluggable today; analyzers are built-in only and external analyzer plugins are planned. Update the README "Extensible by design" section and docs/ARCHITECTURE.md, reusing one extensibility diagram (now including configure-runtime and index-subprojects nodes, with consolidation folded into detect). - Expand docs/USE_CASES.md container coverage: scan/audit an image, generate an SBOM from an image, a CI base-image gate, and diffing two image tags. - Move docs/CI.md to docs/development/CI.md (contributor docs) and update all inbound links. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- CLAUDE.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 28 +++++++++++++++------------- docs/ARCHITECTURE.md | 26 +++++++++++++++----------- docs/CI_INTEGRATION.md | 6 +++--- docs/README.md | 2 +- docs/USE_CASES.md | 36 +++++++++++++++++++++++++++++++++++- docs/{ => development}/CI.md | 0 9 files changed, 72 insertions(+), 32 deletions(-) rename docs/{ => development}/CI.md (100%) diff --git a/AGENTS.md b/AGENTS.md index 220e783f..c015d117 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -228,7 +228,7 @@ If a new analyzer / matcher / detector produces deterministic output for a fixed |--------------------------------------------------------|-----------------------------------------------------------------------------------------| | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Full architecture: pipeline, detectors, auditors, plugins, trust model, decision log | | [`docs/MODELS.md`](docs/MODELS.md) | Domain model reference: Dependency, Package, Vulnerability, Finding, PackageRegistry | -| [`docs/CI.md`](docs/CI.md) | CI setup and workflow (GitHub Actions) | +| [`docs/development/CI.md`](docs/development/CI.md) | CI setup and workflow (GitHub Actions) | | [`docs/CONFIG_REFERENCE.md`](docs/CONFIG_REFERENCE.md) | Generated config reference (all keys, env vars, defaults) | | [`docs/SUPPORT_MATRIX.md`](docs/SUPPORT_MATRIX.md) | Ecosystem detector coverage | | `docs/schemas/*.json`, `docs/schemas/*.md` | Generated JSON schemas and human-readable output docs for `scan`, `diff`, and `explain` | diff --git a/CLAUDE.md b/CLAUDE.md index 7001f672..d88e1e60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -229,7 +229,7 @@ For squash-merges, the squash commit title/body determines the version bump. | --- | --- | | `docs/ARCHITECTURE.md` | Full pipeline, detector model, decision log | | `docs/MODELS.md` | Domain model: Dependency / Package / Vulnerability / Finding / PackageRegistry | -| `docs/CI.md` | GitHub Actions, release workflow | +| `docs/development/CI.md` | GitHub Actions, release workflow | | `docs/CONFIG_REFERENCE.md` | All config keys, env vars, defaults (generated) | | `docs/SUPPORT_MATRIX.md` | Ecosystem detector coverage (generated) | | `docs/schemas/` | JSON schemas + human-readable output docs (generated) | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f5167f9..c74ef588 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ logger.Warn("cache miss", zap.Error(err)) - Add integration tests for new commands and user-visible flows. - Run `make test` before considering work complete. - Run `make smoke` if you touched a detector chain, matcher, auditor, or analyzer. -- If you change GitHub Actions workflows or release behavior, update [docs/CI.md](docs/CI.md) and any affected install guidance in [README.md](README.md). +- If you change GitHub Actions workflows or release behavior, update [docs/development/CI.md](docs/development/CI.md) and any affected install guidance in [README.md](README.md). ### Helpers diff --git a/README.md b/README.md index a27cb8d0..e8a42636 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Dependency intelligence for modern software projects. + Analyze Your Software DNA.

@@ -219,27 +219,29 @@ flowchart LR ### Extensible by design -The pipeline is built around four typed extension points. Every built-in is just an +The pipeline is built around typed extension points. Every built-in is just an implementation of the same contract an external plugin uses, so you can add an -ecosystem, a vulnerability or license source, a reachability analyzer, or a policy -check without forking Bomly. +ecosystem, or a vulnerability or license source, or a policy check without forking +Bomly. Three of these are pluggable today — **detectors, matchers, and auditors**; +external **analyzer** plugins are planned (the built-in reachability analyzers are +not yet an extension point). ```mermaid flowchart LR - D[Detect] --> C[Consolidate] --> M[Match] --> An[Analyze] --> Au[Audit] --> O[Output] + R[Configure runtime] --> X[Index subprojects] --> D[Detect: resolve + consolidate] --> M[Match] --> An[Analyze] --> Au[Audit] --> O[Output] PD([Detector plugins]) -.-> D PM([Matcher plugins]) -.-> M - PAn([Analyzer plugins]) -.-> An PAu([Auditor plugins]) -.-> Au + PAn([Analyzer plugins: planned]) -. planned .-> An ``` -| Extension point | Contract | Add support for | -| --- | --- | --- | -| Detector | turns evidence into a dependency graph | a new ecosystem or lockfile format | -| Matcher | enriches packages | a new vulnerability, license, or lifecycle source | -| Analyzer | annotates reachability | a new language's call-graph analysis | -| Auditor | evaluates policy, emits findings | a custom org policy or gate | +| Extension point | Status | Contract | Add support for | +| --- | --- | --- | --- | +| Detector | Available | turns evidence into a dependency graph | a new ecosystem or lockfile format | +| Matcher | Available | enriches packages | a new vulnerability, license, or lifecycle source | +| Auditor | Available | evaluates policy, emits findings | a custom org policy or gate | +| Analyzer | Planned | annotates reachability | a new language's call-graph analysis | Plugins ship as signed, versioned gRPC binaries (`v1` protocol), are disabled until you explicitly enable them, and participate in the exact same planning flow as @@ -292,7 +294,7 @@ Bomly uses GitHub Actions for: - automatic semantic version tagging on `main` - draft prerelease packaging to GitHub Releases -See [docs/CI.md](docs/CI.md) for workflow triggers, required checks, release packaging, checksum handling, and the planned future attestation step. +See [docs/development/CI.md](docs/development/CI.md) for workflow triggers, required checks, release packaging, checksum handling, and the planned future attestation step. To gate your **own** repository's pull requests, drop in the [Bomly review action](https://github.com/bomly-dev/bomly-review-action) — it diffs and audits dependency changes on each PR and posts a summary comment. See [docs/CI_INTEGRATION.md](docs/CI_INTEGRATION.md) for that and direct-CLI recipes across GitHub Actions, GitLab, Jenkins, Azure, and CircleCI. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ee5f37fd..6f47104e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -75,26 +75,30 @@ Stage summary: ## Extensibility -Extensibility is the core of Bomly's design. The pipeline exposes four typed extension points, and **every built-in is an implementation of the same contract an external plugin implements** — there is no privileged internal path. Adding an ecosystem, an enrichment source, a reachability analyzer, or a policy gate does not require forking the engine. +Extensibility is the core of Bomly's design. **Every built-in is an implementation of the same contract an external plugin implements** — there is no privileged internal path. Adding an ecosystem, an enrichment source, or a policy gate does not require forking the engine. Three extension points are pluggable today (detector, matcher, auditor); external **analyzer** plugins are planned — the built-in reachability analyzers are not yet loadable as plugins. + +The diagram shows where plugins hook into the run, after the runtime is configured and subprojects are indexed: ```mermaid flowchart LR - D[Detect] --> C[Consolidate] --> M[Match] --> An[Analyze] --> Au[Audit] --> O[Output] + R[Configure runtime] --> X[Index subprojects] --> D[Detect: resolve + consolidate] --> M[Match] --> An[Analyze] --> Au[Audit] --> O[Output] PD([Detector plugins]) -.-> D PM([Matcher plugins]) -.-> M - PAn([Analyzer plugins]) -.-> An PAu([Auditor plugins]) -.-> Au + PAn([Analyzer plugins: planned]) -. planned .-> An ``` -| Extension point | Contract (`sdk`) | Responsibility | -| --- | --- | --- | -| Detector | `sdk.Detector` | Turn evidence (lockfile, manifest, SBOM) into a dependency graph | -| Matcher | `sdk.Matcher` | Enrich packages with vulnerability, license, or lifecycle data | -| Analyzer | `sdk.Analyzer` | Annotate `sdk.Vulnerability.Reachability` for a language | -| Auditor | `sdk.Auditor` | Evaluate policy and emit reference-style findings | +| Extension point | Status | Contract (`sdk`) | Responsibility | +| --- | --- | --- | --- | +| Detector | Available | `sdk.Detector` | Turn evidence (lockfile, manifest, SBOM) into a dependency graph | +| Matcher | Available | `sdk.Matcher` | Enrich packages with vulnerability, license, or lifecycle data | +| Auditor | Available | `sdk.Auditor` | Evaluate policy and emit reference-style findings | +| Analyzer | Planned | `sdk.Analyzer` | Annotate `sdk.Vulnerability.Reachability` for a language | + +External plugins run as versioned (`v1`) gRPC binaries and participate in the same runtime planning as built-ins: detector plugins declare evidence patterns and join subproject discovery; matcher and auditor plugins are selected with the same `--matchers` / `--auditors` selector grammar. Plugins are disabled until explicitly enabled. See [PLUGINS.md](PLUGINS.md) for the trust model and authoring guides. -External plugins run as versioned (`v1`) gRPC binaries and participate in the same runtime planning as built-ins: detector plugins declare evidence patterns and join subproject discovery; matcher, analyzer, and auditor plugins are selected with the same `--matchers` / `--analyzers` / `--auditors` selector grammar. Plugins are disabled until explicitly enabled. See [PLUGINS.md](PLUGINS.md) for the trust model and authoring guides. +Analyzers exist as a contract (`sdk.Analyzer`) and ship four built-in implementations (govulncheck, jsreach, pyreach, jvmreach), but the plugin runtime does not yet accept an analyzer kind, so they cannot be supplied by an external plugin today. Making analyzers a first-class plugin extension point is planned. ### Decision: YAML configuration is nested at the file boundary @@ -190,7 +194,7 @@ GitHub Actions handles validation, security analysis, smoke coverage, and releas - Pushes to `main` run deeper quality checks and scheduled smoke coverage. - Semver tags publish draft prereleases to GitHub Releases with cross-platform archives and `SHA256SUMS`. -See [CI and Release Pipeline](CI.md) for workflow details and release mechanics. +See [CI and Release Pipeline](development/CI.md) for workflow details and release mechanics. ## Network Behavior diff --git a/docs/CI_INTEGRATION.md b/docs/CI_INTEGRATION.md index 6bdcd9fa..a18e82be 100644 --- a/docs/CI_INTEGRATION.md +++ b/docs/CI_INTEGRATION.md @@ -1,6 +1,6 @@ # CI Integration -Drop-in recipes for running Bomly in CI. For Bomly's own CI configuration see [docs/CI.md](CI.md). +Drop-in recipes for running Bomly in CI. For Bomly's own CI configuration see [docs/development/CI.md](development/CI.md). The pattern is the same everywhere: install Bomly, run `bomly scan` with `--audit --fail-on `, upload the SBOM and SARIF artifacts. Exit code 2 fails the build on policy violation; see [Exit codes](EXIT_CODES.md). @@ -107,7 +107,7 @@ jobs: deny-licenses: GPL-3.0-only ``` -The action's inputs map onto the CLI policy flags (`fail-on-severity` → `--fail-on`, `deny-licenses` → `--deny-license`, `protected-packages` → `--protected-package`, and so on), so the policy you enforce locally is the policy it enforces on PRs. See the [action README](https://github.com/bomly-dev/bomly-review-action#readme) for the full input list. Bomly dogfoods this action — see the `Bomly Review` workflow in [docs/CI.md](CI.md). +The action's inputs map onto the CLI policy flags (`fail-on-severity` → `--fail-on`, `deny-licenses` → `--deny-license`, `protected-packages` → `--protected-package`, and so on), so the policy you enforce locally is the policy it enforces on PRs. See the [action README](https://github.com/bomly-dev/bomly-review-action#readme) for the full input list. Bomly dogfoods this action — see the `Bomly Review` workflow in [docs/development/CI.md](development/CI.md). ## GitLab CI @@ -251,4 +251,4 @@ Tune `--fail-on` to taste. `pre-push` keeps commits fast and only runs on push. - [Exit codes](EXIT_CODES.md) — what each CI exit means - [Output formats](OUTPUT_FORMATS.md) — SARIF, JSON, SBOM details - [Auditors](AUDITORS.md) — `--fail-on` -- [docs/CI.md](CI.md) — Bomly's own internal CI configuration (not for end users) +- [docs/development/CI.md](development/CI.md) — Bomly's own internal CI configuration (not for end users) diff --git a/docs/README.md b/docs/README.md index aae1eb9a..612d9536 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,5 +46,5 @@ Generated from code. Treat as authoritative. For contributors and release engineers. -- [CI](CI.md) — Bomly's own internal CI configuration +- [CI](development/CI.md) — Bomly's own internal CI configuration - [Contributing](../CONTRIBUTING.md) — build setup, code conventions, release process diff --git a/docs/USE_CASES.md b/docs/USE_CASES.md index 18ac530f..4491f9f6 100644 --- a/docs/USE_CASES.md +++ b/docs/USE_CASES.md @@ -72,6 +72,37 @@ bomly scan --sbom --path sbom.json # audit an SBOM you already have Without `--enrich`, matchers make **zero** outbound HTTP calls. Note that some build-tool detectors (Go, Maven, Gradle) may fetch packages during normal resolution — pre-warm the local cache or commit a lockfile to stay fully offline. See [Detectors → Network behavior](DETECTORS.md#network-behavior). +## Scan and audit a container image + +**Goal:** find what's inside an image and gate on it. + +```bash +# Inventory an image (native lockfile detectors in layers + Syft for OS packages) +bomly scan --container ghcr.io/example/app:latest + +# Audit an image and fail on high-severity vulnerabilities +bomly scan --container ghcr.io/example/app:latest --enrich --audit --fail-on high + +# Generate an SBOM from an image +bomly scan --container ghcr.io/example/app:latest -o spdx=image.spdx.json + +# Pin by digest for a reproducible scan +bomly scan --container ghcr.io/example/app@sha256: --enrich --audit +``` + +Bomly pulls the image using your host's registry credentials — the same ones `docker`/`podman` use — so private images work once you've authenticated (`docker login ghcr.io`). Native detectors still parse lockfiles found in layers; everything else falls through to Syft. See [Scan targets](SCAN_TARGETS.md) for the full container behavior and exit codes. + +### Gate a base-image upgrade in CI + +```yaml + - name: Install Bomly + run: curl -sSfL https://github.com/bomly-dev/bomly-cli/releases/latest/download/bomly_linux_amd64.tar.gz | tar -xz -C /usr/local/bin bomly + - name: Audit the built image + run: bomly scan --container ${{ env.IMAGE }}:${{ github.sha }} --enrich --audit --fail-on high --format sarif > image.sarif +``` + +Exit code `2` fails the job on a policy violation; upload `image.sarif` to the Security tab as in [CI Integration](CI_INTEGRATION.md). + ## Diff two releases **Goal:** see what changed in your dependency tree between two versions. @@ -82,9 +113,12 @@ bomly diff --base v1.2.0 --head v1.3.0 --enrich --audit # Between two SBOM files (no checkout needed) bomly diff --sbom --base old.spdx.json --head new.spdx.json + +# Between two tags (or digests) of the same container image +bomly diff --container ghcr.io/example/app --base 1.4.0 --head 1.5.0 --enrich --audit --fail-on high ``` -You get added, removed, and updated dependencies, plus introduced/resolved findings when `--audit` is set. Great for release notes and upgrade reviews. +You get added, removed, and updated dependencies, plus introduced/resolved findings when `--audit` is set. Great for release notes, upgrade reviews, and catching what a base-image bump dragged in. ## Understand why a dependency is there diff --git a/docs/CI.md b/docs/development/CI.md similarity index 100% rename from docs/CI.md rename to docs/development/CI.md From 1004110085e4291d6d10849efbbbb95b3dbbd21f Mon Sep 17 00:00:00 2001 From: Ahmed ElMallah Date: Thu, 11 Jun 2026 01:05:52 -0700 Subject: [PATCH 14/14] ci: rename CI workflow to "Build & Test" and parallelize jobs Split the single validate job into independent, parallel jobs (lint, test, build, format, modules, generated-docs) so checks run concurrently and report separately, and rename the workflow from "CI" to "Build & Test". Update docs/development/CI.md to match. Note: the required status-check names change from a single "CI" to the per-job names, so branch protection on main must be updated accordingly. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 73 +++++++++++++++++++++++++++++++++++----- docs/development/CI.md | 16 +++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5060ae48..3305518c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: Build & Test on: pull_request: @@ -10,41 +10,86 @@ permissions: contents: read concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + group: build-test-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - validate: + lint: + name: Lint runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v5 - - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - cache-dependency-path: | - go.sum - + cache-dependency-path: go.sum - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: version: v2.12.0 + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: go.sum - name: Run unit and integration tests run: make test + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: go.sum - name: Build default Bomly binary run: go build -o /tmp/bomly ./cmd/bomly - - name: Build lite Bomly binary run: go build -tags "bomly_external_syft,bomly_external_grype" -o /tmp/bomly-lite ./cmd/bomly + format: + name: Format + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: go.sum - name: Check Go formatting run: make fmt-check + modules: + name: Module drift + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: go.sum - name: Check module metadata drift shell: bash run: | @@ -52,6 +97,18 @@ jobs: go mod tidy git diff --exit-code -- go.mod go.sum + generated-docs: + name: Generated docs drift + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: go.sum - name: Check generated documentation drift shell: bash run: | diff --git a/docs/development/CI.md b/docs/development/CI.md index 64bf1c9b..15786836 100644 --- a/docs/development/CI.md +++ b/docs/development/CI.md @@ -6,7 +6,7 @@ Bomly uses GitHub Actions for validation, security analysis, smoke coverage, and | Workflow | Trigger | Purpose | |------------------------|------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| `CI` | Pull requests, pushes to `main` | Fast validation: `golangci-lint`, `gofmt` drift checks, tests, build sanity, module drift, and generated-doc drift | +| `Build & Test` | Pull requests, pushes to `main` | Fast validation split into parallel jobs: `lint`, `test`, `build`, `format`, `modules` (go.mod drift), and `generated-docs` (generated-doc drift) | | `CodeQL` | Pull requests, pushes to `main`, weekly | Static security/quality analysis for Go; results surface in the Security tab | | `Scorecard` | Pushes to `main`, weekly, manual dispatch | OpenSSF Scorecard supply-chain checks; publishes results and uploads SARIF | | `Dependency review` | Pull requests | GitHub dependency-review of added/changed dependencies; fails on high-severity introductions | @@ -20,17 +20,19 @@ Bomly uses GitHub Actions for validation, security analysis, smoke coverage, and For protected branches, require at least: -- `CI` -- `Dependency Review` when dependency metadata changes +- The `Build & Test` jobs (`Lint`, `Test`, `Build`, `Format`, `Module drift`, `Generated docs drift`) +- `Dependency review` when dependency metadata changes - `Smoke` on merge queue entries +`Build & Test` runs each check as its own job so they execute in parallel and report independently. Because the checks are now per-job, update the required status checks in branch protection to the individual job names above (the previous single `CI` check no longer exists). + `Smoke` is intentionally not a per-PR check; it is the slower pre-merge gate for merge queue entries and a nightly health monitor for upstream drift. ## Merge Queue Strategy Bomly uses a layered merge policy: -- `CI` provides fast feedback on every pull request. +- `Build & Test` provides fast feedback on every pull request. - `Smoke` runs when a change enters the merge queue, which catches end-to-end regressions before merge without slowing every PR update. - The nightly `Smoke` run remains in place to catch ecosystem drift, container changes, and upstream repository changes that are unrelated to a new pull request. @@ -129,9 +131,9 @@ The repository also ships a pre-commit hook in `.githooks/pre-commit`. Run `make The per-PR workflow set is intentionally lean so the common path stays fast: -- `golangci-lint` runs inside the main `CI` workflow with the official action, pinned to the repository's lint version and using the action's built-in cache. -- Standalone `go vet ./...` is not run separately in `CI` because `.golangci.yml` already enables `govet`. -- `go test -race` is not enabled in CI because it adds runtime cost and is best reintroduced later if we need the extra concurrency diagnostics. +- `golangci-lint` runs as the `lint` job in the `Build & Test` workflow with the official action, pinned to the repository's lint version and using the action's built-in cache. +- Standalone `go vet ./...` is not run separately because `.golangci.yml` already enables `govet`. +- `go test -race` is not enabled because it adds runtime cost and is best reintroduced later if we need the extra concurrency diagnostics. ## Build and Packaging Model