Skip to content

fix(providers): analyze all FROM lines in multi-stage Dockerfiles via batch analysis#577

Open
a-oren wants to merge 1 commit into
guacsec:mainfrom
a-oren:TC-5070
Open

fix(providers): analyze all FROM lines in multi-stage Dockerfiles via batch analysis#577
a-oren wants to merge 1 commit into
guacsec:mainfrom
a-oren:TC-5070

Conversation

@a-oren

@a-oren a-oren commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix the Dockerfile/Containerfile provider to analyze all FROM lines in multi-stage Dockerfiles instead of only the last one
  • Each FROM image gets its own SBOM in a purl-keyed batch map routed to /api/v5/batch-analysis
  • Resolve ARG substitutions using declared defaults (matching VS Code extension behavior)
  • FROM lines with unresolvable ARGs (no default) are silently skipped

Changes

  • src/providers/oci_dockerfile.js — rename parseFromImage()parseAllFromImages(), build sbomByPurl map, add batch: true flag
  • src/providers/containerfile_parser.js — add getArgQuery() for ARG instruction parsing
  • src/analysis.js — detect batch provider results and route to requestStackBatch()
  • test/providers/oci_dockerfile.test.js — update tests for array return, add ARG resolution and batch output tests

Implements TC-5070

Test plan

  • Unit tests: parseAllFromImages returns array of all image refs
  • Unit tests: ARG substitution resolved using declared defaults
  • Unit tests: Unresolvable ARGs (no default) skipped
  • Unit tests: Batch output format with correct purl keys
  • Integration tests: Dockerfile hardened recommendations scenario passes
  • Integration tests: Dockerfile recommendations-disabled scenario passes
  • npm test — 496 passing, 0 new failures
  • npm run lint — 0 errors

🤖 Generated with Claude Code

Summary by Sourcery

Analyze all FROM instructions in Dockerfiles for OCI image SBOM generation and route batch SBOM results through stack/component analysis.

New Features:

  • Return SBOMs for each FROM image in multi-stage Dockerfiles as a batch map keyed by image purl.
  • Support ARG instruction parsing and default-based substitution in Dockerfile FROM lines.
  • Handle batch SBOM results in stack and component analysis flows via batch-analysis routing.

Enhancements:

  • Rename and extend Dockerfile FROM parsing to return all resolvable base images and skip unresolvable ARG-based stages.
  • Add batch flag and content handling in the Dockerfile provider and analysis pipeline to support multi-image SBOMs.

Tests:

  • Expand Dockerfile provider tests to cover multi-stage parsing, ARG resolution behavior, error handling, and batch SBOM output for provideStack and provideComponent.

… batch analysis

Change the Dockerfile/Containerfile provider to parse every FROM instruction
instead of only the last one. Each image gets its own SBOM in a purl-keyed map
that is routed to the /api/v5/batch-analysis endpoint via requestStackBatch().

- Rename parseFromImage() to parseAllFromImages() returning an array
- Build sbomByPurl map in getImageSBOM() following requestImages() pattern
- Add batch flag to provider result for routing in analysis.js
- Resolve ARG substitutions using declared defaults (matching VS Code extension)
- FROM lines with unresolvable ARGs (no default) are skipped without failing

Implements TC-5070

Assisted-by: Claude Code
@sourcery-ai

sourcery-ai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the Dockerfile/Containerfile OCI provider to parse and analyze all FROM stages in multi-stage Dockerfiles, resolve ARG-based image references using defaults, and emit batch SBOM results that are routed through new batch-handling paths in the analysis pipeline.

Sequence diagram for batch Dockerfile stack analysis

sequenceDiagram
    actor User
    participant CLI as requestStack
    participant Provider as oci_dockerfile_provider
    participant Batch as requestStackBatch

    User->>CLI: invoke requestStack(manifest, url, opts)
    CLI->>Provider: provideStack(manifest, opts)
    Provider-->>CLI: { content, contentType, ecosystem, batch:true }
    CLI->>Batch: requestStackBatch(JSON.parse(content), url, html, opts)
    Batch-->>User: batch analysis result
Loading

Flow diagram for parsing all FROM images with ARG resolution

flowchart TD
    A[parseAllFromImages]
    A-->B[getParser]
    A-->C[getFromQuery]
    A-->D[getArgQuery]
    B & C & D -->E[parser.parse]
    E-->F[collectArgs]
    F-->G[iterate FROM matches]
    G-->H{containsExpansion}
    H-->|no|I[add imageSpec.text to images]
    H-->|yes|J[resolveArgs]
    J-->K{resolved != null}
    K-->|yes|L[add resolved to images]
    K-->|no|G
    I-->M{images.length > 0}
    L-->M
    M-->|yes|N[return images]
    M-->|no|O[throw error]
Loading

File-Level Changes

Change Details Files
Parse all resolvable FROM instructions in Dockerfiles, including ARG-based image specs, and generate batch SBOM outputs keyed by image purls.
  • Renamed single-image parser to parseAllFromImages and changed it to return all FROM image references instead of only the last one.
  • Added collectArgs and resolveArgs helpers to gather ARG defaults from the Dockerfile AST and substitute them into image specs, skipping FROM lines with unresolvable ARGs.
  • Updated getImageSBOM to iterate over all parsed images, generate one SBOM per image, and build a purl-keyed sbomByPurl map with a batch: true flag in the provider result.
src/providers/oci_dockerfile.js
test/providers/oci_dockerfile.test.js
Extend the containerfile parser utilities and analysis pipeline to support ARG queries and batch analysis routing.
  • Added getArgQuery to containerfile_parser.js to provide a tree-sitter query for arg_instruction nodes with name/default captures.
  • Updated analysis.js requestStack and requestComponent to detect provider results with batch: true and forward the parsed SBOM map to requestStackBatch instead of the single-SBOM path.
  • Augmented tests to cover parseAllFromImages behavior, ARG resolution semantics, skipping of unresolved FROM lines, and the batch output shape of provideStack/provideComponent using esmock-based mocking.
src/providers/containerfile_parser.js
src/analysis.js
test/providers/oci_dockerfile.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@a-oren a-oren requested review from Strum355 and ruromero and removed request for ruromero July 7, 2026 07:53

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In getImageSBOM, the same image could appear in multiple FROM stages and will trigger multiple generateImageSBOM calls even though the sbomByPurl entry is overwritten; consider de‑duplicating by purl or image ref to avoid redundant SBOM generation work.
  • When parseAllFromImages ends up skipping all FROM lines due to unresolved ARGs, the thrown error is quite generic; including the list of unresolved variables or at least the problematic FROM lines would make it easier to diagnose why the Dockerfile could not be resolved.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `getImageSBOM`, the same image could appear in multiple FROM stages and will trigger multiple `generateImageSBOM` calls even though the `sbomByPurl` entry is overwritten; consider de‑duplicating by purl or image ref to avoid redundant SBOM generation work.
- When `parseAllFromImages` ends up skipping all FROM lines due to unresolved ARGs, the thrown error is quite generic; including the list of unresolved variables or at least the problematic FROM lines would make it easier to diagnose why the Dockerfile could not be resolved.

## Individual Comments

### Comment 1
<location path="src/providers/containerfile_parser.js" line_range="25" />
<code_context>
+
+export async function getArgQuery() {
+	const language = await init();
+	return new Query(language, '(arg_instruction (arg_pair name: (unquoted_string) @name default: (unquoted_string) @default))');
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** ARG query only captures unquoted defaults, missing valid quoted ARG defaults.

By restricting both `name` and `default` to `unquoted_string`, this misses valid cases like `ARG FOO="bar"` (or other representations tree-sitter may use). Those ARGs will be invisible to `collectArgs`, causing FROM lines that depend on them to be treated as unresolvable and skipped. Please broaden the matched node types for `default` (and possibly `name`) so quoted and alternative forms are included.

Suggested implementation:

```javascript
export async function getArgQuery() {
	const language = await init();
	return new Query(
		language,
		'(arg_instruction (arg_pair name: (_) @name default: (_) @default))',
	);
}

```

If downstream code assumes `@name` and `@default` are always `unquoted_string` nodes, you may need to adjust it to handle other node types (e.g., string literals) by inspecting the captured nodes and normalizing their text (stripping quotes where needed).
</issue_to_address>

### Comment 2
<location path="test/providers/oci_dockerfile.test.js" line_range="65-66" />
<code_context>
+			expect(result).to.deep.equal(['node:18', 'nginx:alpine'])
+		})
+
+		/** Verifies that FROM lines with ARG substitution are resolved using declared defaults. */
+		test('resolves ARG substitution in FROM lines using declared defaults', async () => {
+			const content = [
+				'ARG BASE_IMAGE=ubuntu:22.04',
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case covering `$VAR` (non-braced) substitutions in FROM to exercise both resolveArgs patterns

Currently only the `${VAR}` path in `resolveArgs` is covered by tests. To also exercise the `$VAR` branch of the regex, please add a similar test using `$BASE_IMAGE` in a FROM line, e.g.:

```js
test('resolves $VAR substitution in FROM lines using declared defaults', async () => {
  const content = [
    'ARG BASE_IMAGE=ubuntu:22.04',
    'FROM $BASE_IMAGE AS base',
    'FROM alpine:3.18',
  ].join('\n')
  const result = await parseAllFromImages(content)
  expect(result).to.deep.equal(['ubuntu:22.04', 'alpine:3.18'])
})
```

This ensures the `$VAR` pattern remains covered and prevents regressions on that path.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


export async function getArgQuery() {
const language = await init();
return new Query(language, '(arg_instruction (arg_pair name: (unquoted_string) @name default: (unquoted_string) @default))');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): ARG query only captures unquoted defaults, missing valid quoted ARG defaults.

By restricting both name and default to unquoted_string, this misses valid cases like ARG FOO="bar" (or other representations tree-sitter may use). Those ARGs will be invisible to collectArgs, causing FROM lines that depend on them to be treated as unresolvable and skipped. Please broaden the matched node types for default (and possibly name) so quoted and alternative forms are included.

Suggested implementation:

export async function getArgQuery() {
	const language = await init();
	return new Query(
		language,
		'(arg_instruction (arg_pair name: (_) @name default: (_) @default))',
	);
}

If downstream code assumes @name and @default are always unquoted_string nodes, you may need to adjust it to handle other node types (e.g., string literals) by inspecting the captured nodes and normalizing their text (stripping quotes where needed).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sdlc-workflow/verify-pr] Classified as suggestion — the current query correctly captures only ARGs with unquoted defaults, which is sufficient for static resolution. Broadening to wildcard node types would also require downstream normalization (quote stripping). No sub-task created.

@Strum355 Strum355 Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although Ive never seen quoted ones in the wild (I was thinking of something else), we should ideally handle them. Removing the quotes will be necessary

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, handling them would make this more robust, and the extra normalization is pretty contained.

Concretely, I’d suggest two small tweaks:

  1. Broaden the query to accept any node type for name/default:
export async function getArgQuery() {
	const language = await init();
	return new Query(
		language,
		'(arg_instruction (arg_pair name: (_) @name default: (_) @default))',
	);
}
  1. Normalize in collectArgs so we strip surrounding quotes from defaults (and optionally names, though they’re usually bare identifiers):
function stripQuotes(text) {
	if (!text) return text;
	// Handle both single and double quoted strings
	if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith('\'') && text.endsWith('\''))) {
		return text.slice(1, -1);
	}
	return text;
}

function collectArgs(tree, argQuery) {
	const args = new Map();
	for (const match of argQuery.matches(tree.rootNode)) {
		const nameNode = match.captures.find(c => c.name === 'name')?.node;
		const defaultNode = match.captures.find(c => c.name === 'default')?.node;
		if (!nameNode || !defaultNode) continue;

		const name = nameNode.text; // typically unquoted identifier
		const defaultValue = stripQuotes(defaultNode.text);

		if (name && defaultValue) {
			args.set(name, defaultValue);
		}
	}
	return args;
}

That keeps the rest of the pipeline unchanged while covering ARG FOO="bar", ARG FOO='bar', and the existing unquoted cases.

If you’d like, I can also suggest a follow-up test case for a Dockerfile with quoted ARG defaults to ensure this path is exercised.

Comment thread test/providers/oci_dockerfile.test.js
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.99%. Comparing base (ab5949f) to head (df1dfe6).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #577      +/-   ##
==========================================
+ Coverage   90.75%   90.99%   +0.24%     
==========================================
  Files          36       38       +2     
  Lines        7766     7986     +220     
  Branches     1353     1391      +38     
==========================================
+ Hits         7048     7267     +219     
- Misses        718      719       +1     
Flag Coverage Δ
unit-tests 90.99% <100.00%> (+0.24%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/providers/containerfile_parser.js 100.00% <100.00%> (ø)
src/providers/oci_dockerfile.js 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@a-oren

a-oren commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @sourcery-ai[bot] review — Classified as suggestion (2 items):

  1. De-duplicate SBOM generation for repeated images — optimization proposal, not backed by CONVENTIONS.md. No sub-task created.
  2. Include unresolved variables in error message — reasonable improvement but current message is descriptive per convention. No sub-task created.

@a-oren

a-oren commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-5070 (commit df1dfe6)

Check Result Details
Review Feedback PASS 4 suggestions from sourcery-ai (no code change requests)
Root-Cause Investigation N/A No sub-tasks created
Scope Containment WARN All 3 task-specified files present; containerfile_parser.js modified (out-of-scope but necessary for ARG query)
Diff Size PASS 308 lines across 4 files — proportionate to task scope
Commit Traceability PASS Commit df1dfe6 references TC-5070
Sensitive Patterns PASS No secrets or credentials detected
CI Status PASS All 5 CI checks pass (lint/test Node 22/24, Sourcery, PR title, commit messages)
Acceptance Criteria PASS All 6 criteria satisfied — multi-stage FROM parsing, batch SBOM map, batch routing, single-FROM, ARG resolution, Dockerfile/Containerfile support
Test Quality WARN Test Documentation: PASS (all tests have JSDoc); Repetitive Test Detection: WARN (some parseAllFromImages tests share identical structure); Eval Quality: N/A
Test Change Classification ADDITIVE 8 net-new tests added, 0 removed; coverage increased from 90.75% to 90.99%
Verification Commands PASS All 33 Dockerfile/Containerfile tests pass; lint has 0 errors

Overall: WARN

Scope Containment details:

src/providers/containerfile_parser.js was modified (+5 lines) to add getArgQuery() for ARG instruction parsing. This file is not listed in the task spec's "Files to Modify" but is a tightly-coupled dependency required for the ARG substitution feature described in the Implementation Notes.

Repetitive Test Detection details:

Several parseAllFromImages tests (single FROM, platform flag, digest, case-insensitive FROM, single FROM with ARG) share identical structure: define content string → call parseAllFromImages(content) → assert deep.equal on result array. These could potentially be parameterized using a data-driven pattern, though individually they test distinct parsing edge cases.


This comment was AI-generated by sdlc-workflow/verify-pr v0.12.2.

const sbomByPurl = {}
for (const image of images) {
const imageRef = parseImageRef(image, opts)
sbomByPurl[imageRef.getPackageURL().toString()] = generateImageSBOM(imageRef, opts)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a containerfile lists the same image multiple times, this is rerun unnecessarily

@Strum355 Strum355 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small changes but LGTM otherwise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants