Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ bomly scan
bomly scan --path ./services/api

# Scan a container image
bomly scan --container ghcr.io/example/app:latest
bomly scan --image ghcr.io/example/app:latest

# Scan a remote Git ref
bomly scan --url https://github.com/owner/repo --ref v1.2.3
Expand Down
5 changes: 3 additions & 2 deletions docs/CONFIG_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ YAML files use the nested keys documented below. Unknown keys and the former fla
| YAML Key | Environment Variable | Type | Default | Description |
|----------|---------------------|------|---------|-------------|
| `target.path` | `BOMLY_PATH` | `string` | - | Filesystem path to scan |
| `target.container` | `BOMLY_CONTAINER` | `string` | - | Container image to scan (e.g. alpine:latest) |
| `target.image` | `BOMLY_IMAGE` | `string` | - | Container image to scan (e.g. alpine:latest) |
| `target.url` | `BOMLY_URL` | `string` | - | Remote Git URL to clone and scan |
| `target.ref` | `BOMLY_REF` | `string` | - | Git ref to checkout when scanning a URL |
| `target.sbom` | `BOMLY_SBOM` | `bool` | - | Treat the selected filesystem target as an SBOM file |
Expand Down Expand Up @@ -113,6 +113,7 @@ Flat YAML keys are no longer accepted. Move each existing key to its nested repl
| `http_proxy_port` | `network.proxy.port` |
| `http_proxy_type` | `network.proxy.type` |
| `http_proxy_username` | `network.proxy.username` |
| `image` | `target.image` |
| `install_args` | `pipeline.install_args` |
| `install_first` | `pipeline.install_first` |
| `interactive` | `output.interactive` |
Expand Down Expand Up @@ -147,7 +148,7 @@ Flat YAML keys are no longer accepted. Move each existing key to its nested repl
# Filesystem path to scan
# path: ""
# Container image to scan (e.g. alpine:latest)
# container: ""
# image: ""
# Remote Git URL to clone and scan
# url: ""
# Git ref to checkout when scanning a URL
Expand Down
2 changes: 1 addition & 1 deletion docs/DETECTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ This is fast and offline. See [SBOM formats](SBOM.md) for the format comparison.
Bomly resolves container references via the host's registry credentials. Native detectors that work on lockfile contents inside layers still run; everything else falls through to Syft:

```bash
bomly scan --container ghcr.io/example/app:latest
bomly scan --image ghcr.io/example/app:latest
```

## See also
Expand Down
4 changes: 2 additions & 2 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ Need structured output for automation? `--json` is the shortcut for `--format js
bomly scan --json
```

Pass `--container` to scan a container image:
Pass `--image` to scan a container image:

```bash
bomly scan --container ghcr.io/example/app:latest
bomly scan --image ghcr.io/example/app:latest
```

Pass `--url` (with optional `--ref`) to scan a Git repository without cloning by hand:
Expand Down
20 changes: 11 additions & 9 deletions docs/SCAN_TARGETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ Bomly resolves dependencies from four kinds of input. Each subcommand (`scan`, `
| --- | --- | --- |
| Local directory | `--path <dir>` | Current working directory |
| Git repository | `--url <repo>` (with optional `--ref`) | — |
| Container image | `--container <ref>` | — |
| Container image | `--image <ref>` | — |
| Existing SBOM | `--sbom --path <file>` | — |

Exactly one target type per run. Combining `--container` with `--url`, or passing `--ref` without `--url`, is rejected with exit 4.
Exactly one target type per run. Combining `--image` with `--url`, or passing `--ref` without `--url`, is rejected with exit 4.

> `--container` is a deprecated alias for `--image`. It still works but is hidden from `--help` and prints a deprecation notice; prefer `--image`.

## Local directory — `--path`

Expand Down Expand Up @@ -39,14 +41,14 @@ The clone goes to a temporary directory and is removed after the scan. Credentia

`--ref` accepts any value `git checkout` accepts: branch, tag, commit SHA.

## Container image — `--container`
## Container image — `--image`

Pulls and scans an image by reference. Native detectors that work on lockfile contents inside layers still run; everything else falls through to Syft.

```bash
bomly scan --container ghcr.io/example/app:latest
bomly scan --container alpine:3.20
bomly scan --container <digest>
bomly scan --image ghcr.io/example/app:latest
bomly scan --image alpine:3.20
bomly scan --image <digest>
```

Registry credentials come from your host: `~/.docker/config.json`, the Docker credential helpers, and `DOCKER_CONFIG` are all honored.
Expand Down Expand Up @@ -74,12 +76,12 @@ See [SBOM formats](SBOM.md) for the format comparison.
| --- | --- | --- |
| `--path` alone | Yes | Default; scans the directory |
| `--url` + `--ref` | Yes | Checks out `ref` after clone |
| `--container` alone | Yes | Pulls and scans the image |
| `--image` alone | Yes | Pulls and scans the image |
| `--sbom` + `--path` | Yes | Ingests the SBOM file |
| `--sbom` + `--container` | No | Exit 4 |
| `--sbom` + `--image` | No | Exit 4 |
| `--sbom` + `--url` | No | Exit 4 |
| `--ref` without `--url` | No | Exit 4 |
| `--container` + `--url` | No | Exit 4 |
| `--image` + `--url` | No | Exit 4 |

## What runs after target resolution

Expand Down
2 changes: 1 addition & 1 deletion docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Bomly uses your host's registry credentials. Confirm with:
docker pull <image-ref>
```

If `docker pull` works and `bomly scan --container <image-ref>` doesn't, file a bug with the credential helper you use (`docker-credential-*`).
If `docker pull` works and `bomly scan --image <image-ref>` doesn't, file a bug with the credential helper you use (`docker-credential-*`).

## `bomly-lite` says "syft: command not found"

Expand Down
12 changes: 6 additions & 6 deletions docs/USE_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ Without `--enrich`, matchers make **zero** outbound HTTP calls. Note that some b

```bash
# Inventory an image (native lockfile detectors in layers + Syft for OS packages)
bomly scan --container ghcr.io/example/app:latest
bomly scan --image 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
bomly scan --image 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
bomly scan --image 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:<digest> --enrich --audit
bomly scan --image ghcr.io/example/app@sha256:<digest> --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.
Expand All @@ -98,7 +98,7 @@ Bomly pulls the image using your host's registry credentials — the same ones `
- 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
run: bomly scan --image ${{ 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).
Expand All @@ -115,7 +115,7 @@ bomly diff --base v1.2.0 --head v1.3.0 --enrich --audit
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
bomly diff --image 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, upgrade reviews, and catching what a base-image bump dragged in.
Expand Down
2 changes: 1 addition & 1 deletion docs/detectors/ecosystems/sbom/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ bomly diff --sbom --base ./v1.0.cdx.json --head ./v1.1.cdx.json

- **Relationship fidelity depends on the source SBOM.** If the SBOM was produced by a tool that emits a flat package list (no `DEPENDS_ON` / `dependencies` edges), Bomly's graph is also flat. `bomly explain` cannot show paths that aren't recorded.
- **Vendor-specific extensions** (custom properties, non-standard package types) are passed through to the JSON output but are not used for policy decisions.
- **SBOM ingest is exclusive** — combining `--sbom` with `--container` or `--url` is rejected with exit 4.
- **SBOM ingest is exclusive** — combining `--sbom` with `--image` or `--url` is rejected with exit 4.
- **Format versions other than SPDX 2.3 JSON and CycloneDX 1.6 JSON** are rejected. SPDX 3.0 and CycloneDX 1.5 ingest are tracked for follow-up.
- **Tag-Value SPDX** and **XML CycloneDX** are not currently ingested.
2 changes: 1 addition & 1 deletion internal/cli/cmd_progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func inputResolutionLabels(cfg opts.Options) (string, string, bool) {
return "Reading SBOM", "Read SBOM", true
case resolved.URL != "":
return "Cloning repository", "Cloned repository", true
case resolved.Container != "":
case resolved.Image != "":
return "Resolving container reference", "Resolved container reference", true
default:
// Local filesystem — resolution is instant; skip the step.
Expand Down
8 changes: 4 additions & 4 deletions internal/cli/diff_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func newDiffCmd() *cobra.Command {
Short: "Compare dependency states",
Long: "Compare dependency states between two git refs, two SBOM files, or two container image tags/digests.",
Example: " bomly diff --url https://github.com/bomly-dev/bomly-cli --base main --head feature\n" +
" bomly diff --container alpine --base 3.19 --head 3.20 --enrich\n" +
" bomly diff --image alpine --base 3.19 --head 3.20 --enrich\n" +
" bomly diff --sbom --base ./before.cdx.json --head ./after.cdx.json --json",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -65,8 +65,8 @@ func newDiffCmd() *cobra.Command {
if headRef == "" {
return exit.InvalidInputError("--head is required when --sbom is set")
}
if current.Container != "" {
return exit.InvalidInputError("--sbom cannot be combined with --container")
if current.Image != "" {
return exit.InvalidInputError("--sbom cannot be combined with --image")
}
} else {
if baseRef == "" {
Expand Down Expand Up @@ -113,7 +113,7 @@ func newDiffCmd() *cobra.Command {
switch {
case current.SBOM:
baseTarget, headTarget, projectIdentifier, resolutionWarnings, err = resolveSBOMDiffGraphs(cmd.Context(), options, prog, logger, baseRef, headRef)
case current.Container != "":
case current.Image != "":
baseTarget, headTarget, projectIdentifier, resolutionWarnings, err = resolveContainerDiffGraphs(cmd.Context(), options, prog, logger, baseRef, headRef)
default:
var gitChangedLines map[string][]git.LineRange
Expand Down
8 changes: 4 additions & 4 deletions internal/cli/diff_resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ func resolveDiffResultsForRef(ctx context.Context, options *opts.Options, logger
func resolveContainerDiffGraphs(ctx context.Context, options *opts.Options, prog *progress.Progress, logger *zap.Logger, baseRef, headRef string) (diffResolvedTarget, diffResolvedTarget, string, []engine.PipelineWarning, error) {
current := options.GetConfig()
refStep := prog.StartWithDoneLabel("input", "Resolving container references", "Resolved container references")
baseTarget, err := resolveContainerDiffTarget(current.Container, baseRef)
baseTarget, err := resolveContainerDiffTarget(current.Image, baseRef)
if err != nil {
refStep.Fail("Resolving container references failed")
return diffResolvedTarget{}, diffResolvedTarget{}, "", nil, exit.InvalidInputError("resolve --base %q: %v", baseRef, err)
}
headTarget, err := resolveContainerDiffTarget(current.Container, headRef)
headTarget, err := resolveContainerDiffTarget(current.Image, headRef)
if err != nil {
refStep.Fail("Resolving container references failed")
return diffResolvedTarget{}, diffResolvedTarget{}, "", nil, exit.InvalidInputError("resolve --head %q: %v", headRef, err)
Expand All @@ -133,7 +133,7 @@ func resolveContainerDiffGraphs(ctx context.Context, options *opts.Options, prog
}
indexStep.Complete("Indexed subprojects", combinedSubprojectChildren(baseResolved.Context.Subprojects(), headResolved.Context.Subprojects()))

return baseResolved, headResolved, current.Container, collectPipelineWarnings(baseResolved.Warnings, headResolved.Warnings), nil
return baseResolved, headResolved, current.Image, collectPipelineWarnings(baseResolved.Warnings, headResolved.Warnings), nil
}

// executionTargetForResolved returns a filesystem target when the resolved location
Expand Down Expand Up @@ -179,7 +179,7 @@ func resolveContainerDiffTarget(container, selector string) (string, error) {
}
container = strings.TrimSpace(container)
if container == "" {
return "", fmt.Errorf("--container is empty")
return "", fmt.Errorf("--image is empty")
}
if strings.HasPrefix(selector, "sha256:") {
return container + "@" + selector, nil
Expand Down
10 changes: 5 additions & 5 deletions internal/cli/mcp_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ type mcpOptionsAdapter struct {
// churn at every callsite.
type mcpOverrides struct {
Path string
Container string
Image string
URL string
Ref string
Enrich bool
Expand All @@ -135,7 +135,7 @@ func (a *mcpOptionsAdapter) cloneWithOverrides(o mcpOverrides) *opts.Options {

resolved := clone.GetConfig()
applyStringOverride(&clone.ResolvedConfig.Path, o.Path)
applyStringOverride(&clone.ResolvedConfig.Container, o.Container)
applyStringOverride(&clone.ResolvedConfig.Image, o.Image)
applyStringOverride(&clone.ResolvedConfig.URL, o.URL)
applyStringOverride(&clone.ResolvedConfig.Ref, o.Ref)
applyFailOnOverride(&clone.ResolvedConfig.FailOn, o.FailOn)
Expand Down Expand Up @@ -164,7 +164,7 @@ func (a *mcpOptionsAdapter) cloneWithOverrides(o mcpOverrides) *opts.Options {
clone.ResolvedConfig.Interactive = false

applyStringOverride(&resolved.Path, o.Path)
applyStringOverride(&resolved.Container, o.Container)
applyStringOverride(&resolved.Image, o.Image)
applyStringOverride(&resolved.URL, o.URL)
applyStringOverride(&resolved.Ref, o.Ref)
applyFailOnOverride(&resolved.FailOn, o.FailOn)
Expand Down Expand Up @@ -229,7 +229,7 @@ func (a *mcpOptionsAdapter) RunScan(ctx context.Context, req mcp.ScanRequest) (o
started := time.Now()
o := a.cloneWithOverrides(mcpOverrides{
Path: req.Path,
Container: req.Container,
Image: req.Image,
URL: req.URL,
Ref: req.Ref,
Enrich: req.Enrich,
Expand Down Expand Up @@ -315,7 +315,7 @@ func (a *mcpOptionsAdapter) RunDiff(ctx context.Context, req mcp.DiffRequest) (o
started := time.Now()
o := a.cloneWithOverrides(mcpOverrides{
Path: req.Path,
Container: req.Container,
Image: req.Image,
Enrich: req.Enrich,
Audit: req.Audit,
Analyze: req.Analyze,
Expand Down
11 changes: 8 additions & 3 deletions internal/cli/opts/flag_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ func BindCommandFlagGroups(cmd *cobra.Command, cfg *config.Resolved, groups ...F

func bindTargetFlags(flags *pflag.FlagSet, cfg *config.Resolved) {
flags.StringVar(&cfg.Path, "path", "", "Execution target path")
flags.StringVar(&cfg.Container, "container", "", "Container image reference to scan with Syft")
flags.StringVar(&cfg.Image, "image", "", "Container image reference to scan with Syft")
// --container is a backwards-compatible alias for --image. It binds to the
// same field and is hidden from help; a deprecation notice is emitted to
// stderr (not stdout) when used, so machine-readable output stays clean.
flags.StringVar(&cfg.Image, "container", "", "Deprecated alias for --image")
_ = flags.MarkHidden("container")
flags.StringVar(&cfg.URL, "url", "", "Git repository URL to clone and scan")
flags.StringVar(&cfg.Ref, "ref", "", "Git reference to scan when using --url")
flags.BoolVar(&cfg.SBOM, "sbom", false, "Treat the selected filesystem target as an SBOM file")
Expand Down Expand Up @@ -127,8 +132,8 @@ func applyFlagOverrides(dst *config.Resolved, flags config.Resolved, cmd *cobra.
if flagChanged(cmd, "path") {
dst.Path = flags.Path
}
if flagChanged(cmd, "container") {
dst.Container = flags.Container
if flagChanged(cmd, "image") || flagChanged(cmd, "container") {
dst.Image = flags.Image
}
if flagChanged(cmd, "url") {
dst.URL = flags.URL
Expand Down
16 changes: 8 additions & 8 deletions internal/cli/opts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ func (o *Options) configLoadPaths() ([]string, error) {
}

func (o *Options) projectConfigPathForLoading() (string, error) {
if strings.TrimSpace(o.ResolvedConfig.URL) != "" || strings.TrimSpace(o.ResolvedConfig.Container) != "" {
if strings.TrimSpace(o.ResolvedConfig.URL) != "" || strings.TrimSpace(o.ResolvedConfig.Image) != "" {
return "", nil
}

Expand All @@ -551,8 +551,8 @@ func (o *Options) projectConfigPathForLoading() (string, error) {
func (o *Options) resolveExecutionTarget(logger *zap.Logger) (sdk.ExecutionTarget, string, func() error, error) {
resolved := o.ResolvedConfig
if resolved.SBOM {
if resolved.Container != "" || resolved.URL != "" || resolved.Ref != "" {
return sdk.ExecutionTarget{}, "", nil, exit.InvalidInputError("--sbom cannot be combined with --container, --url, or --ref")
if resolved.Image != "" || resolved.URL != "" || resolved.Ref != "" {
return sdk.ExecutionTarget{}, "", nil, exit.InvalidInputError("--sbom cannot be combined with --image, --url, or --ref")
}
sbomPath, err := system.ResolveExistingFile(resolved.Path)
if err != nil {
Expand All @@ -567,11 +567,11 @@ func (o *Options) resolveExecutionTarget(logger *zap.Logger) (sdk.ExecutionTarge
if resolved.URL != "" {
targetCount++
}
if resolved.Container != "" {
if resolved.Image != "" {
targetCount++
}
if targetCount > 1 {
return sdk.ExecutionTarget{}, "", nil, exit.InvalidInputError("--path, --url, and --container cannot be used together")
return sdk.ExecutionTarget{}, "", nil, exit.InvalidInputError("--path, --url, and --image cannot be used together")
}
if resolved.URL != "" {
projectPath, err := git.CloneTemp(logger, resolved.URL, resolved.Ref)
Expand All @@ -588,14 +588,14 @@ func (o *Options) resolveExecutionTarget(logger *zap.Logger) (sdk.ExecutionTarge
Ref: resolved.Ref,
}, projectPath, cleanup, nil
}
if resolved.Container != "" {
if resolved.Image != "" {
if resolved.Ref != "" {
return sdk.ExecutionTarget{}, "", nil, exit.InvalidInputError("--ref can only be used with --url")
}
return sdk.ExecutionTarget{
Kind: sdk.ExecutionTargetContainerImage,
Location: strings.TrimSpace(resolved.Container),
}, resolved.Container, nil, nil
Location: strings.TrimSpace(resolved.Image),
}, resolved.Image, nil, nil
}
projectPath, err := o.ResolveProjectPath()
if err != nil {
Expand Down
Loading