diff --git a/README.md b/README.md
index 090fdd15..c6676096 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/docs/CONFIG_REFERENCE.md b/docs/CONFIG_REFERENCE.md
index 90c346a1..78d60b67 100644
--- a/docs/CONFIG_REFERENCE.md
+++ b/docs/CONFIG_REFERENCE.md
@@ -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 |
@@ -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` |
@@ -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
diff --git a/docs/DETECTORS.md b/docs/DETECTORS.md
index 81153549..c80e68b9 100644
--- a/docs/DETECTORS.md
+++ b/docs/DETECTORS.md
@@ -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
diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md
index f5978720..2a0039dd 100644
--- a/docs/GETTING_STARTED.md
+++ b/docs/GETTING_STARTED.md
@@ -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:
diff --git a/docs/SCAN_TARGETS.md b/docs/SCAN_TARGETS.md
index a6c588e1..8b03c3c1 100644
--- a/docs/SCAN_TARGETS.md
+++ b/docs/SCAN_TARGETS.md
@@ -6,10 +6,12 @@ Bomly resolves dependencies from four kinds of input. Each subcommand (`scan`, `
| --- | --- | --- |
| Local directory | `--path
` | Current working directory |
| Git repository | `--url ` (with optional `--ref`) | — |
-| Container image | `--container [` | — |
+| Container image | `--image ][` | — |
| Existing SBOM | `--sbom --path ` | — |
-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`
@@ -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
+bomly scan --image ghcr.io/example/app:latest
+bomly scan --image alpine:3.20
+bomly scan --image
```
Registry credentials come from your host: `~/.docker/config.json`, the Docker credential helpers, and `DOCKER_CONFIG` are all honored.
@@ -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
diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
index e2baeb54..c841a9d7 100644
--- a/docs/TROUBLESHOOTING.md
+++ b/docs/TROUBLESHOOTING.md
@@ -86,7 +86,7 @@ Bomly uses your host's registry credentials. Confirm with:
docker pull
```
-If `docker pull` works and `bomly scan --container ` doesn't, file a bug with the credential helper you use (`docker-credential-*`).
+If `docker pull` works and `bomly scan --image ` doesn't, file a bug with the credential helper you use (`docker-credential-*`).
## `bomly-lite` says "syft: command not found"
diff --git a/docs/USE_CASES.md b/docs/USE_CASES.md
index 21c60cdf..fc580a2c 100644
--- a/docs/USE_CASES.md
+++ b/docs/USE_CASES.md
@@ -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: --enrich --audit
+bomly scan --image 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.
@@ -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).
@@ -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.
diff --git a/docs/detectors/ecosystems/sbom/sbom.md b/docs/detectors/ecosystems/sbom/sbom.md
index e46a86c6..06cccc1b 100644
--- a/docs/detectors/ecosystems/sbom/sbom.md
+++ b/docs/detectors/ecosystems/sbom/sbom.md
@@ -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.
diff --git a/internal/cli/cmd_progress.go b/internal/cli/cmd_progress.go
index fff0fe2e..440ec9e5 100644
--- a/internal/cli/cmd_progress.go
+++ b/internal/cli/cmd_progress.go
@@ -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.
diff --git a/internal/cli/diff_cmd.go b/internal/cli/diff_cmd.go
index 5e95b305..bf2ea3c1 100644
--- a/internal/cli/diff_cmd.go
+++ b/internal/cli/diff_cmd.go
@@ -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 {
@@ -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 == "" {
@@ -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
diff --git a/internal/cli/diff_resolve.go b/internal/cli/diff_resolve.go
index cce4d3a8..885660f5 100644
--- a/internal/cli/diff_resolve.go
+++ b/internal/cli/diff_resolve.go
@@ -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)
@@ -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
@@ -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
diff --git a/internal/cli/mcp_cmd.go b/internal/cli/mcp_cmd.go
index 1b08a437..6952ccbe 100644
--- a/internal/cli/mcp_cmd.go
+++ b/internal/cli/mcp_cmd.go
@@ -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
@@ -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)
@@ -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)
@@ -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,
@@ -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,
diff --git a/internal/cli/opts/flag_options.go b/internal/cli/opts/flag_options.go
index 02cc69be..85968d6c 100644
--- a/internal/cli/opts/flag_options.go
+++ b/internal/cli/opts/flag_options.go
@@ -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")
@@ -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
diff --git a/internal/cli/opts/options.go b/internal/cli/opts/options.go
index 34405b24..5d3f585b 100644
--- a/internal/cli/opts/options.go
+++ b/internal/cli/opts/options.go
@@ -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
}
@@ -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 {
@@ -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)
@@ -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 {
diff --git a/internal/cli/opts/options_test.go b/internal/cli/opts/options_test.go
index 0a0e3891..c938020d 100644
--- a/internal/cli/opts/options_test.go
+++ b/internal/cli/opts/options_test.go
@@ -27,21 +27,21 @@ func TestCommandContextRoundTripsThroughContext(t *testing.T) {
}
}
-func TestCommandContextResolveExecutionTarget_Container(t *testing.T) {
- options := Options{ResolvedConfig: config.Resolved{Container: "alpine:3.20"}}
+func TestCommandContextResolveExecutionTarget_Image(t *testing.T) {
+ options := Options{ResolvedConfig: config.Resolved{Image: "alpine:3.20"}}
target, location, cleanup, err := options.resolveExecutionTarget(nil)
if err != nil {
t.Fatalf("resolveExecutionTarget() error = %v", err)
}
if cleanup != nil {
- t.Fatal("expected no cleanup for container target")
+ t.Fatal("expected no cleanup for image target")
}
if target.Kind != sdk.ExecutionTargetContainerImage {
t.Fatalf("expected container execution target, got %#v", target)
}
if target.Location != "alpine:3.20" || location != "alpine:3.20" {
- t.Fatalf("unexpected container target values: %#v %q", target, location)
+ t.Fatalf("unexpected image target values: %#v %q", target, location)
}
}
@@ -60,13 +60,13 @@ func TestProjectDescriptor_UsesUserFacingTargetLabels(t *testing.T) {
}
func TestCommandContextResolveExecutionTarget_RejectsMultipleTargets(t *testing.T) {
- options := Options{ResolvedConfig: config.Resolved{Path: ".", Container: "alpine:3.20"}}
+ options := Options{ResolvedConfig: config.Resolved{Path: ".", Image: "alpine:3.20"}}
_, _, _, err := options.resolveExecutionTarget(nil)
if err == nil {
t.Fatal("expected multiple target error")
}
- if !strings.Contains(err.Error(), "--path, --url, and --container cannot be used together") {
+ if !strings.Contains(err.Error(), "--path, --url, and --image cannot be used together") {
t.Fatalf("unexpected error: %v", err)
}
}
diff --git a/internal/cli/root_cmd.go b/internal/cli/root_cmd.go
index 8a0e909f..799631ca 100644
--- a/internal/cli/root_cmd.go
+++ b/internal/cli/root_cmd.go
@@ -2,6 +2,7 @@ package cli
import (
"context"
+ "fmt"
"strings"
"github.com/bomly-dev/bomly-cli/internal/cli/exit"
@@ -52,6 +53,7 @@ func newRootCmd(version string) (*cobra.Command, error) {
if cmd.Name() == "benchmark" {
return nil
}
+ warnDeprecatedContainerFlag(cmd)
options, err := commandOptions(cmd)
if err != nil {
return err
@@ -157,6 +159,20 @@ func rootHasCommandRequiredFlags(cmd *cobra.Command) bool {
return hasRequiredFlags
}
+// warnDeprecatedContainerFlag emits a one-line deprecation notice to stderr
+// when the hidden --container alias is used, steering users toward --image.
+// The notice goes to ErrOrStderr (never stdout) so machine-readable output
+// from --json/--format remains uncorrupted for existing --container callers.
+func warnDeprecatedContainerFlag(cmd *cobra.Command) {
+ if cmd == nil || cmd.Flags().Lookup("container") == nil {
+ return
+ }
+ if !cmd.Flags().Changed("container") {
+ return
+ }
+ _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Warning: --container is deprecated; use --image instead.")
+}
+
func logResolvedOptions(cmd *cobra.Command) {
if cmd == nil {
return
@@ -169,7 +185,7 @@ func logResolvedOptions(cmd *cobra.Command) {
resolved := options.GetConfig()
logger.Debug("Resolved options",
zap.String("path", resolved.Path),
- zap.String("container", resolved.Container),
+ zap.String("image", resolved.Image),
zap.String("url", resolved.URL),
zap.String("ref", resolved.Ref),
zap.Bool("sbom", resolved.SBOM),
diff --git a/internal/cli/root_cmd_test.go b/internal/cli/root_cmd_test.go
index 35963fa8..abc3ffcb 100644
--- a/internal/cli/root_cmd_test.go
+++ b/internal/cli/root_cmd_test.go
@@ -422,7 +422,7 @@ func TestRootHelp_CommandExamplesRender(t *testing.T) {
"bomly scan --enrich --audit",
"bomly scan -o spdx=bomly.spdx.json",
"bomly scan --url https://github.com/bomly-dev/bomly-cli --ref main --json",
- "bomly scan --container alpine:3.20",
+ "bomly scan --image alpine:3.20",
"Explore available detectors, matchers, and auditors with `bomly plugins list`.",
},
notInText: []string{"Exit Codes:"},
diff --git a/internal/cli/scan_cmd.go b/internal/cli/scan_cmd.go
index c55b2ee9..f5e2a9db 100644
--- a/internal/cli/scan_cmd.go
+++ b/internal/cli/scan_cmd.go
@@ -25,7 +25,7 @@ func newScanCmd() *cobra.Command {
Example: " bomly scan --enrich --audit\n" +
" bomly scan -o spdx=bomly.spdx.json\n" +
" bomly scan --url https://github.com/bomly-dev/bomly-cli --ref main --json\n" +
- " bomly scan --container alpine:3.20",
+ " bomly scan --image alpine:3.20",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
started := time.Now()
diff --git a/internal/config/config.go b/internal/config/config.go
index 11b9d817..d062689a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -21,7 +21,7 @@ package config
// the YAML config file, then env vars, then explicit flags.
type Resolved struct {
Path string `doc:"Filesystem path to scan" env:"BOMLY_PATH"`
- Container string `doc:"Container image to scan (e.g. alpine:latest)" env:"BOMLY_CONTAINER"`
+ Image string `doc:"Container image to scan (e.g. alpine:latest)" env:"BOMLY_IMAGE" envalias:"BOMLY_CONTAINER"`
URL string `doc:"Remote Git URL to clone and scan" env:"BOMLY_URL"`
Ref string `doc:"Git ref to checkout when scanning a URL" env:"BOMLY_REF"`
SBOM bool `doc:"Treat the selected filesystem target as an SBOM file" env:"BOMLY_SBOM"`
@@ -98,7 +98,8 @@ type File struct {
// TargetFile configures the execution target selected for a scan.
type TargetFile struct {
Path *string `yaml:"path,omitempty" resolved:"Path" legacy:"path"`
- Container *string `yaml:"container,omitempty" resolved:"Container" legacy:"container"`
+ Container *string `yaml:"container,omitempty" resolved:"Image" legacy:"container"` // deprecated alias for image
+ Image *string `yaml:"image,omitempty" resolved:"Image" legacy:"image"`
URL *string `yaml:"url,omitempty" resolved:"URL" legacy:"url"`
Ref *string `yaml:"ref,omitempty" resolved:"Ref" legacy:"ref"`
SBOM *bool `yaml:"sbom,omitempty" resolved:"SBOM" legacy:"sbom"`
diff --git a/internal/config/load.go b/internal/config/load.go
index 229ae38a..c19b7e2f 100644
--- a/internal/config/load.go
+++ b/internal/config/load.go
@@ -246,6 +246,11 @@ func ApplyEnvOverrides(dst *Resolved) {
continue
}
val, ok := os.LookupEnv(key)
+ if !ok {
+ if alias := t.Field(i).Tag.Get("envalias"); alias != "" {
+ val, ok = os.LookupEnv(alias)
+ }
+ }
if !ok {
continue
}
diff --git a/internal/config/load_test.go b/internal/config/load_test.go
index 71756798..bc59544f 100644
--- a/internal/config/load_test.go
+++ b/internal/config/load_test.go
@@ -77,7 +77,7 @@ func TestLoadFileNestedConfig(t *testing.T) {
if err := os.WriteFile(path, []byte(`
target:
path: fixture
- container: alpine:3.20
+ image: alpine:3.20
url: https://example.com/acme/repo.git
ref: main
sbom: true
@@ -138,7 +138,7 @@ matchers:
if want := filepath.Join(filepath.Dir(path), "fixture"); resolved.Path != want {
t.Fatalf("Path = %q, want %q", resolved.Path, want)
}
- if resolved.Container != "alpine:3.20" || resolved.URL == "" || resolved.Ref != "main" || !resolved.SBOM {
+ if resolved.Image != "alpine:3.20" || resolved.URL == "" || resolved.Ref != "main" || !resolved.SBOM {
t.Fatalf("target config = %#v", resolved)
}
if !resolved.Enrich || !resolved.Audit || !resolved.Analyze || !resolved.InstallFirst {
@@ -164,6 +164,75 @@ matchers:
}
}
+// TestLoadFileDeprecatedContainerKey verifies the deprecated target.container
+// YAML key still resolves to the canonical Image field, and that target.image
+// wins when both are set.
+func TestLoadFileDeprecatedContainerKey(t *testing.T) {
+ t.Run("alias resolves to Image", func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "config.yaml")
+ if err := os.WriteFile(path, []byte("target:\n container: alpine:3.20\n"), 0o644); err != nil {
+ t.Fatalf("write config: %v", err)
+ }
+ fileCfg, err := LoadFile(path)
+ if err != nil {
+ t.Fatalf("LoadFile() error = %v", err)
+ }
+ var resolved Resolved
+ ApplyFileConfig(&resolved, *fileCfg)
+ if resolved.Image != "alpine:3.20" {
+ t.Fatalf("Image = %q, want alpine:3.20", resolved.Image)
+ }
+ })
+
+ t.Run("image wins over container", func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "config.yaml")
+ if err := os.WriteFile(path, []byte("target:\n container: alpine:3.20\n image: alpine:3.21\n"), 0o644); err != nil {
+ t.Fatalf("write config: %v", err)
+ }
+ fileCfg, err := LoadFile(path)
+ if err != nil {
+ t.Fatalf("LoadFile() error = %v", err)
+ }
+ var resolved Resolved
+ ApplyFileConfig(&resolved, *fileCfg)
+ if resolved.Image != "alpine:3.21" {
+ t.Fatalf("Image = %q, want alpine:3.21 (canonical key should win)", resolved.Image)
+ }
+ })
+}
+
+// TestApplyEnvOverridesImageAlias verifies BOMLY_IMAGE is the primary env var
+// and the deprecated BOMLY_CONTAINER still applies as a fallback.
+func TestApplyEnvOverridesImageAlias(t *testing.T) {
+ t.Run("primary BOMLY_IMAGE", func(t *testing.T) {
+ t.Setenv("BOMLY_IMAGE", "alpine:3.21")
+ var resolved Resolved
+ ApplyEnvOverrides(&resolved)
+ if resolved.Image != "alpine:3.21" {
+ t.Fatalf("Image = %q, want alpine:3.21", resolved.Image)
+ }
+ })
+
+ t.Run("deprecated BOMLY_CONTAINER fallback", func(t *testing.T) {
+ t.Setenv("BOMLY_CONTAINER", "alpine:3.20")
+ var resolved Resolved
+ ApplyEnvOverrides(&resolved)
+ if resolved.Image != "alpine:3.20" {
+ t.Fatalf("Image = %q, want alpine:3.20 (BOMLY_CONTAINER alias)", resolved.Image)
+ }
+ })
+
+ t.Run("BOMLY_IMAGE wins over BOMLY_CONTAINER", func(t *testing.T) {
+ t.Setenv("BOMLY_IMAGE", "alpine:3.21")
+ t.Setenv("BOMLY_CONTAINER", "alpine:3.20")
+ var resolved Resolved
+ ApplyEnvOverrides(&resolved)
+ if resolved.Image != "alpine:3.21" {
+ t.Fatalf("Image = %q, want alpine:3.21 (primary env should win)", resolved.Image)
+ }
+ })
+}
+
func TestApplyFileConfigClearsInheritedLists(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte(`
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
index 576de1d8..11ec4802 100644
--- a/internal/mcp/server.go
+++ b/internal/mcp/server.go
@@ -14,7 +14,7 @@ import (
// ScanRequest holds per-call overrides for the bomly_scan tool.
type ScanRequest struct {
Path string `json:"path"`
- Container string `json:"container"`
+ Image string `json:"image"`
URL string `json:"url"`
Ref string `json:"ref"`
Enrich bool `json:"enrich"`
@@ -39,7 +39,7 @@ type DiffRequest struct {
Base string `json:"base"`
Head string `json:"head"`
Path string `json:"path"`
- Container string `json:"container"`
+ Image string `json:"image"`
Enrich bool `json:"enrich"`
Audit bool `json:"audit"`
Analyze bool `json:"analyze"`
@@ -118,6 +118,17 @@ func NewServer(mcpCtx Context) *server.MCPServer {
}
// jsonResult marshals v to JSON and returns it as a text tool result.
+// firstNonEmpty returns the first non-empty string, used to prefer a primary
+// argument over a deprecated alias.
+func firstNonEmpty(values ...string) string {
+ for _, v := range values {
+ if v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
func jsonResult(v any) (*mcplib.CallToolResult, error) {
data, err := json.Marshal(v)
if err != nil {
diff --git a/internal/mcp/tool_diff.go b/internal/mcp/tool_diff.go
index ef665c37..d1871e9a 100644
--- a/internal/mcp/tool_diff.go
+++ b/internal/mcp/tool_diff.go
@@ -19,6 +19,8 @@ func registerDiffTool(s *server.MCPServer, mcpCtx Context) {
mcplib.Description("Head Git ref to compare (e.g. HEAD, a branch name, a commit SHA)"),
),
mcplib.WithString("path", mcplib.Description("Local repository path (defaults to cwd)")),
+ mcplib.WithString("image", mcplib.Description("Container image reference to diff; base and head are treated as tags/digests (e.g. alpine)")),
+ mcplib.WithString("container", mcplib.Description("Deprecated alias for image")),
mcplib.WithBoolean("enrich", mcplib.Description("Enrich packages with vulnerability and license data")),
mcplib.WithBoolean("audit", mcplib.Description("Include audit delta (introduced, resolved, and persisted findings) (requires enrich)")),
mcplib.WithBoolean("analyze", mcplib.Description("Run code analysis on each side and include reachability annotations on the audit delta (requires enrich)")),
@@ -47,6 +49,7 @@ func registerDiffTool(s *server.MCPServer, mcpCtx Context) {
Base: base,
Head: head,
Path: req.GetString("path", ""),
+ Image: firstNonEmpty(req.GetString("image", ""), req.GetString("container", "")),
Enrich: req.GetBool("enrich", false),
Audit: req.GetBool("audit", false),
Analyze: req.GetBool("analyze", false),
diff --git a/internal/mcp/tool_scan.go b/internal/mcp/tool_scan.go
index ae62adc6..f2b509f4 100644
--- a/internal/mcp/tool_scan.go
+++ b/internal/mcp/tool_scan.go
@@ -11,7 +11,8 @@ func registerScanTool(s *server.MCPServer, mcpCtx Context) {
tool := mcplib.NewTool("bomly_scan",
mcplib.WithDescription("Scan a project for dependencies, vulnerabilities, and policy findings. Returns structured JSON with all packages, manifests, and optional audit results."),
mcplib.WithString("path", mcplib.Description("Filesystem path to scan (defaults to cwd)")),
- mcplib.WithString("container", mcplib.Description("Container image reference to scan (e.g. alpine:latest)")),
+ mcplib.WithString("image", mcplib.Description("Container image reference to scan (e.g. alpine:latest)")),
+ mcplib.WithString("container", mcplib.Description("Deprecated alias for image")),
mcplib.WithString("url", mcplib.Description("Git repository URL to clone and scan")),
mcplib.WithString("ref", mcplib.Description("Git ref to checkout when using url")),
mcplib.WithBoolean("enrich", mcplib.Description("Enrich packages with vulnerability and license data from external sources")),
@@ -24,7 +25,7 @@ func registerScanTool(s *server.MCPServer, mcpCtx Context) {
s.AddTool(tool, func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
scanReq := ScanRequest{
Path: req.GetString("path", ""),
- Container: req.GetString("container", ""),
+ Image: firstNonEmpty(req.GetString("image", ""), req.GetString("container", "")),
URL: req.GetString("url", ""),
Ref: req.GetString("ref", ""),
Enrich: req.GetBool("enrich", false),
diff --git a/internal/support/component_docs.go b/internal/support/component_docs.go
index 43e06b76..554df3bb 100644
--- a/internal/support/component_docs.go
+++ b/internal/support/component_docs.go
@@ -204,7 +204,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
diff --git a/internal/support/prose/detectors/sbom.md b/internal/support/prose/detectors/sbom.md
index e7341a0d..77daa862 100644
--- a/internal/support/prose/detectors/sbom.md
+++ b/internal/support/prose/detectors/sbom.md
@@ -57,6 +57,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.
diff --git a/test/smoke/audit_test.go b/test/smoke/audit_test.go
index acc3c767..3c997330 100644
--- a/test/smoke/audit_test.go
+++ b/test/smoke/audit_test.go
@@ -143,7 +143,7 @@ func TestContainerAuditScan(t *testing.T) {
}{
{
name: "container-scan-alpine-audit",
- args: []string{"scan", "--container", alpineImage, "--format", "json", "--enrich", "--audit", "--matchers", "osv", "--auditors", "vulnerability"},
+ args: []string{"scan", "--image", alpineImage, "--format", "json", "--enrich", "--audit", "--matchers", "osv", "--auditors", "vulnerability"},
},
}
diff --git a/test/smoke/container_test.go b/test/smoke/container_test.go
index ca97b326..2fee062f 100644
--- a/test/smoke/container_test.go
+++ b/test/smoke/container_test.go
@@ -24,9 +24,10 @@ func TestContainerScan(t *testing.T) {
}{
{
name: "container-scan-alpine",
- args: []string{"scan", "--container", alpineImage, "--format", "json"},
+ args: []string{"scan", "--image", alpineImage, "--format", "json"},
},
{
+ // Exercises the deprecated --container alias for backwards compatibility.
name: "container-scan-debian",
args: []string{"scan", "--container", debianImage, "--format", "json"},
},
@@ -62,7 +63,7 @@ func TestContainerDiff(t *testing.T) {
}{
{
name: "container-diff-alpine",
- args: []string{"diff", "--container", "alpine", "--base", "3.19", "--head", "3.20", "--format", "json"},
+ args: []string{"diff", "--image", "alpine", "--base", "3.19", "--head", "3.20", "--format", "json"},
},
}
@@ -96,7 +97,7 @@ func TestContainerExplain(t *testing.T) {
}{
{
name: "container-explain-alpine",
- args: []string{"explain", "musl", "--container", alpineImage, "--format", "json"},
+ args: []string{"explain", "musl", "--image", alpineImage, "--format", "json"},
},
}
]