Skip to content

fix(dockerfile): analyze all FROM lines via batch analysis#546

Open
a-oren wants to merge 5 commits into
guacsec:mainfrom
a-oren:TC-5071
Open

fix(dockerfile): analyze all FROM lines via batch analysis#546
a-oren wants to merge 5 commits into
guacsec:mainfrom
a-oren:TC-5071

Conversation

@a-oren

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

Copy link
Copy Markdown
Contributor

Summary

  • Change DockerfileProvider from analyzing only the last FROM line to analyzing all FROM lines in multi-stage Dockerfiles
  • Add batch flag to Provider.Content to route between /api/v5/analysis and /api/v5/batch-analysis endpoints
  • Resolve ARG substitutions (both ${VAR} and $VAR syntax) using default values; skip FROM scratch and unresolvable ARGs
  • Route batch content through ExhortApi for component, stack, and license analysis flows
  • Fix case-insensitive scratch comparison so FROM SCRATCH, FROM Scratch, etc. are properly skipped (TC-5098)

Test plan

  • 26 unit tests pass (parsing, batch content, ecosystem integration, provider properties, case-insensitive scratch)
  • Spotless formatting check passes
  • Full test suite passes (485 tests, 0 failures)
  • Integration tests pass against live backend (syft scenarios: hardened recommendations + recommendations disabled)

Fixes: TC-5071
Fixes: TC-5098

🤖 Generated with Claude Code

a-oren and others added 3 commits July 1, 2026 14:09
…ysis

Add DockerfileProvider that parses FROM instructions to extract base
image references and generates CycloneDX SBOMs via syft. Supports
multi-stage builds (uses final FROM), suffixed filenames (Dockerfile.dev),
multiple --flag tokens, and rejects ARG substitution and FROM scratch.

Also normalize Docker Hub image references in ImageRef.getPackageURL()
so bare names (node) and library-prefixed names (docker.io/library/node)
produce the same PURL (docker.io/node), aligning with the JS client.

Implements: TC-4938

Assisted-by: Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Aligns with the JavaScript client by allowing users to set
TRUSTIFY_DA_RECOMMEND=false to append ?recommend=false to analysis URLs,
disabling Trusted Content recommendations in responses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change the DockerfileProvider from analyzing only the last FROM line to
analyzing all FROM lines in multi-stage Dockerfiles. Each image gets its
own SBOM via syft, and results are sent as a batch request to
/api/v5/batch-analysis.

- Add batch flag to Provider.Content to route between single and batch endpoints
- Resolve ARG substitutions (both ${VAR} and $VAR syntax) with default values
- Skip FROM scratch and unresolvable ARG references
- Route batch content through ExhortApi to the batch-analysis endpoint

Fixes: TC-5071

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a DockerfileProvider that parses all FROM lines for multi-stage Dockerfiles and produces batch SBOM content, wires Dockerfile manifests into the Ecosystem provider/ExhortApi batch-analysis flow (including recommend flag handling), and normalizes Docker Hub image references in ImageRef PURLs, with comprehensive tests for Dockerfile parsing, batch behavior, and URI construction.

Sequence diagram for Dockerfile DockerfileProvider batch analysis flow

sequenceDiagram
  participant User
  participant ExhortApi
  participant Ecosystem
  participant DockerfileProvider
  participant Backend as ExhortBackend

  User->>ExhortApi: componentAnalysis(manifestFile)
  ExhortApi->>Ecosystem: getProvider(manifestPath)
  Ecosystem-->>ExhortApi: DockerfileProvider
  ExhortApi->>DockerfileProvider: provideComponent()
  DockerfileProvider->>DockerfileProvider: generateBatchSbomContent()
  DockerfileProvider-->>ExhortApi: Content(batch=true)
  ExhortApi->>ExhortApi: resolveAnalysisUri(content)
  ExhortApi->>ExhortApi: buildAnalysisUri(S_API_V_5_BATCH_ANALYSIS, endpoint)
  ExhortApi->>Backend: sendAsync(request)
  Backend-->>ExhortApi: batch response
  ExhortApi->>ExhortApi: getBatchStackAnalysisReports(response)
  ExhortApi-->>User: AnalysisReport (first from batch)
Loading

Sequence diagram for resolveAnalysisUri and recommend flag handling

sequenceDiagram
  participant ExhortApi
  participant Environment
  participant Backend as ExhortBackend

  ExhortApi->>ExhortApi: resolveAnalysisUri(content)
  alt content.batch == true
    ExhortApi->>ExhortApi: buildAnalysisUri(S_API_V_5_BATCH_ANALYSIS, endpoint)
  else content.batch == false
    ExhortApi->>ExhortApi: buildAnalysisUri(S_API_V_5_ANALYSIS, endpoint)
  end

  ExhortApi->>Environment: getBoolean(TRUSTIFY_DA_RECOMMEND, true)
  alt TRUSTIFY_DA_RECOMMEND == false
    ExhortApi->>ExhortApi: append ?recommend=false
  else TRUSTIFY_DA_RECOMMEND == true
    ExhortApi->>ExhortApi: use base URI
  end

  ExhortApi->>Backend: sendAsync(request with URI)
  Backend-->>ExhortApi: response
Loading

File-Level Changes

Change Details Files
Introduce DockerfileProvider to parse all FROM instructions, resolve ARGs, and generate batch SBOM content for each base image using syft.
  • Implement DockerfileProvider that scans Dockerfile/Containerfile manifests, collects ARG defaults, resolves ${VAR}/$VAR substitutions, skips scratch/unresolvable FROMs, and throws when no analyzable FROM is found
  • Generate a JSON batch map of purl→SBOM using ImageUtils.parseImageRef and ImageUtils.generateImageSBOM, returning Provider.Content with batch=true and CycloneDX media type
  • Add extensive unit tests and Dockerfile fixtures covering single/multi-stage, platform flags, digests, ARG substitution edge cases, scratch handling, and batch behavior including partial failures
src/main/java/io/github/guacsec/trustifyda/providers/DockerfileProvider.java
src/test/java/io/github/guacsec/trustifyda/providers/Dockerfile_Provider_Test.java
src/test/resources/tst_manifests/dockerfile/...
Wire DockerfileProvider into Ecosystem and Provider.Content, enabling Dockerfile manifests to be treated as an OCI ecosystem with batch-capable content.
  • Extend Ecosystem.Type with DOCKERFILE mapped to syft and update provider resolution to return DockerfileProvider for Dockerfile/Containerfile (including suffixed names) while rejecting non-Dockerfile prefixes
  • Augment Provider.Content with a batch flag and constructor overload to mark batch responses without breaking existing call sites
src/main/java/io/github/guacsec/trustifyda/tools/Ecosystem.java
src/main/java/io/github/guacsec/trustifyda/Provider.java
Update ExhortApi to route batch content to /api/v5/batch-analysis, reuse request-building logic, and support a recommend=false query parameter controlled by environment.
  • Introduce TRUSTIFY_DA_RECOMMEND env flag and buildAnalysisUri helper to append ?recommend=false when disabled, and apply it to both analysis and batch-analysis URIs
  • Refactor stackAnalysis and stackAnalysisHtml to use resolveStackContent/resolveAnalysisUri and handle batch vs non-batch flows, returning the first report from the batch map
  • Update componentAnalysis and componentAnalysisWithLicense to route batch content through batch-analysis, short-circuit license checks for batch flows, and share a new getBatchAnalysisReportForComponent helper
  • Reuse buildStackRequest to construct stack analysis requests based on resolved content/URI and apply buildAnalysisUri in performBatchAnalysis
src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java
Normalize Docker Hub image references in ImageRef so different notations yield consistent PURLs.
  • Adjust getPackageURL to normalize bare Docker Hub names and docker.io/library/ prefixes into a canonical docker.io/ repository_url while preserving non-Docker Hub registries and user namespaces
  • Add tests to verify repository_url and name normalization for bare, library-prefixed, user, and non-Docker Hub images
src/main/java/io/github/guacsec/trustifyda/image/ImageRef.java
src/test/java/io/github/guacsec/trustifyda/image/ImageRefTest.java
Add ExhortApi tests to validate recommend flag behavior in stackAnalysis URI construction.
  • Add tests to ensure TRUSTIFY_DA_RECOMMEND=false appends ?recommend=false to /api/v5/analysis stackAnalysis requests
  • Add tests to ensure default recommend behavior omits the recommend query parameter and uses the plain analysis URI
src/test/java/io/github/guacsec/trustifyda/impl/Exhort_Api_Test.java

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

@codecov-commenter

codecov-commenter commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.24138% with 9 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@2db003f). Learn more about missing BASE report.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #546   +/-   ##
=======================================
  Coverage        ?   69.30%           
  Complexity      ?     1035           
=======================================
  Files           ?       66           
  Lines           ?     4365           
  Branches        ?      770           
=======================================
  Hits            ?     3025           
  Misses          ?      994           
  Partials        ?      346           
Flag Coverage Δ
integration-tests 69.30% <92.24%> (?)

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • In DockerfileProvider.parseAllFromImages, the special-case handling for scratch is case-sensitive; consider normalizing the image token (e.g., to lower-case) before comparing so that FROM SCRATCH or From Scratch are also skipped consistently.
  • buildAnalysisUri currently appends ?recommend=false directly to the template URL; if these endpoints ever gain their own query parameters this will produce invalid URIs, so it may be safer to build the URI via a dedicated builder that can merge query parameters correctly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In DockerfileProvider.parseAllFromImages, the special-case handling for `scratch` is case-sensitive; consider normalizing the image token (e.g., to lower-case) before comparing so that `FROM SCRATCH` or `From Scratch` are also skipped consistently.
- buildAnalysisUri currently appends `?recommend=false` directly to the template URL; if these endpoints ever gain their own query parameters this will produce invalid URIs, so it may be safer to build the URI via a dedicated builder that can merge query parameters correctly.

## Individual Comments

### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/providers/DockerfileProvider.java" line_range="140" />
<code_context>
+              continue;
+            }
+          }
+          if ("scratch".equals(image)) {
+            LOG.info(String.format("Skipping FROM scratch in %s", dockerfile));
+            continue;
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle `scratch` base image in a case-insensitive way.

`FROM` is already handled case-insensitively, but this comparison to `"scratch"` is not. As a result, `FROM Scratch` or `FROM SCRATCH` would still be analyzed. Please make this comparison case-insensitive (e.g., `equalsIgnoreCase("scratch")` or by normalizing `image` to lowercase) so all variants are skipped consistently.
</issue_to_address>

### Comment 2
<location path="src/main/java/io/github/guacsec/trustifyda/image/ImageRef.java" line_range="155" />
<code_context>
+    //   node                    → docker.io/node
+    //   docker.io/library/node  → docker.io/node
+    if (repositoryUrl != null) {
+      var lower = repositoryUrl.toLowerCase();
+      if (lower.equals(simpleName.toLowerCase())) {
+        repositoryUrl = "docker.io/" + simpleName;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Use a locale-independent lowercase operation when normalizing repository URLs.

`toLowerCase()` without a locale can behave differently on systems with non-English defaults (e.g., Turkish), causing incorrect normalization. Please use `toLowerCase(Locale.ROOT)` for both `repositoryUrl` and `simpleName` to ensure consistent behavior across environments.

Suggested implementation:

```java
    if (repositoryUrl != null) {
      var lower = repositoryUrl.toLowerCase(java.util.Locale.ROOT);
      if (lower.equals(simpleName.toLowerCase(java.util.Locale.ROOT))) {
        repositoryUrl = "docker.io/" + simpleName;
      } else if (lower.startsWith(DOCKER_HUB_LIBRARY_PREFIX)) {
        repositoryUrl = "docker.io/" + lower.substring(DOCKER_HUB_LIBRARY_PREFIX.length());
      }
    }

    if (repositoryUrl != null && !repositoryUrl.equalsIgnoreCase(simpleName)) {
      qualifiers.put(REPOSITORY_QUALIFIER, repositoryUrl.toLowerCase(java.util.Locale.ROOT));
    }

```

If you prefer using a direct import instead of qualifying `Locale`, add:
- `import java.util.Locale;`

and then replace `java.util.Locale.ROOT` with `Locale.ROOT` in the edited code.
</issue_to_address>

### Comment 3
<location path="src/test/java/io/github/guacsec/trustifyda/providers/Dockerfile_Provider_Test.java" line_range="326-288" />
<code_context>
+     /** Verifies that batch content skips images that fail SBOM generation and includes the rest. */
</code_context>
<issue_to_address>
**suggestion (testing):** Cover the case where all images fail SBOM generation to assert the IOException from generateBatchSbomContent

`provide_stack_skips_failing_images_in_batch` already covers mixed success/failure, but there’s no coverage for the branch where *all* SBOM generations fail and `generateBatchSbomContent` throws.

Please add a test that:
- Uses a multi-stage Dockerfile (e.g., `multi_stage/Dockerfile`).
- Mocks `ImageUtils.generateImageSBOM(..)` to throw for every image.
- Asserts `provider.provideStack()` (and optionally `provider.provideComponent()`) throws an `IOException` whose message contains `"No analyzable FROM images found"`.

This will exercise the error path when SBOM generation fails for all images and protect against regressions.

Suggested implementation:

```java
    /** Verifies that batch content skips images that fail SBOM generation and includes the rest. */
    @Test
    void provide_stack_skips_failing_images_in_batch() throws Exception {
      // Given a multi-stage Dockerfile where one image fails SBOM generation
      var manifestPath = TEST_MANIFESTS.resolve("multi_stage/Dockerfile");
      var provider = new DockerfileProvider(manifestPath);

      ImageRef nodeRef = Mockito.mock(ImageRef.class);
      JsonNode nginxSbom = MAPPER.createObjectNode().put("bomFormat", "CycloneDX");
      ImageRef nginxRef = Mockito.mock(ImageRef.class);
      PackageURL nginxPurl = new PackageURL("pkg:oci/nginx@alpine");
    }

    /**
     * Verifies that when all images fail SBOM generation, provideStack throws an IOException
     * with a helpful message instead of returning an empty batch.
     */
    @Test
    void provide_stack_throws_when_all_images_fail_sbom_generation() throws Exception {
      // Given a multi-stage Dockerfile where every FROM image fails SBOM generation
      var manifestPath = TEST_MANIFESTS.resolve("multi_stage/Dockerfile");
      var provider = new DockerfileProvider(manifestPath);

      try (MockedStatic<ImageUtils> imageUtilsMock = Mockito.mockStatic(ImageUtils.class)) {
        imageUtilsMock
            .when(() -> ImageUtils.generateImageSBOM(Mockito.any(ImageRef.class)))
            .thenThrow(new IOException("SBOM generation failed"));

        assertThatThrownBy(provider::provideStack)
            .isInstanceOf(IOException.class)
            .hasMessageContaining("No analyzable FROM images found");

        // Optionally also cover provideComponent to ensure it propagates the same error
        assertThatThrownBy(provider::provideComponent)
            .isInstanceOf(IOException.class)
            .hasMessageContaining("No analyzable FROM images found");
      }

```

I could only see part of the existing `provide_stack_skips_failing_images_in_batch` test body, so in the replacement block above I preserved the visible portion verbatim and closed the method before adding the new test.

To integrate this cleanly with the rest of your file, please:
1. Ensure the class already imports `ImageUtils`, `ImageRef`, `MockedStatic`, `Mockito`, `IOException`, and AssertJ's `assertThatThrownBy`. If not, add:
   - `import java.io.IOException;`
   - `import org.mockito.MockedStatic;`
   - `import org.mockito.Mockito;`
   - `import static org.assertj.core.api.Assertions.assertThatThrownBy;`
   - `import io.github.guacsec.trustifyda.util.ImageUtils;` (or the correct package for `ImageUtils` in your project)
2. If `ImageUtils.generateImageSBOM(..)` has a different signature (e.g., additional parameters), adjust the `when` stub accordingly:
   - Replace `Mockito.any(ImageRef.class)` with the appropriate argument matchers.
3. If `provider.provideComponent()` is not expected to propagate the same exception, you can remove the second `assertThatThrownBy` block or adapt it to the desired behavior.
4. If your test class uses a different static mocking mechanism (e.g., PowerMockito or a JUnit 4 runner), adapt the `try (MockedStatic<...>)` block to match the established pattern.
</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.

Comment thread src/main/java/io/github/guacsec/trustifyda/providers/DockerfileProvider.java Outdated
Comment thread src/main/java/io/github/guacsec/trustifyda/image/ImageRef.java
assertThat(batchJson.isObject()).isTrue();
assertThat(batchJson.size()).isEqualTo(2);
assertThat(batchJson.has(nodePurl.toString())).isTrue();
assertThat(batchJson.has(nginxPurl.toString())).isTrue();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Cover the case where all images fail SBOM generation to assert the IOException from generateBatchSbomContent

provide_stack_skips_failing_images_in_batch already covers mixed success/failure, but there’s no coverage for the branch where all SBOM generations fail and generateBatchSbomContent throws.

Please add a test that:

  • Uses a multi-stage Dockerfile (e.g., multi_stage/Dockerfile).
  • Mocks ImageUtils.generateImageSBOM(..) to throw for every image.
  • Asserts provider.provideStack() (and optionally provider.provideComponent()) throws an IOException whose message contains "No analyzable FROM images found".

This will exercise the error path when SBOM generation fails for all images and protect against regressions.

Suggested implementation:

    /** Verifies that batch content skips images that fail SBOM generation and includes the rest. */
    @Test
    void provide_stack_skips_failing_images_in_batch() throws Exception {
      // Given a multi-stage Dockerfile where one image fails SBOM generation
      var manifestPath = TEST_MANIFESTS.resolve("multi_stage/Dockerfile");
      var provider = new DockerfileProvider(manifestPath);

      ImageRef nodeRef = Mockito.mock(ImageRef.class);
      JsonNode nginxSbom = MAPPER.createObjectNode().put("bomFormat", "CycloneDX");
      ImageRef nginxRef = Mockito.mock(ImageRef.class);
      PackageURL nginxPurl = new PackageURL("pkg:oci/nginx@alpine");
    }

    /**
     * Verifies that when all images fail SBOM generation, provideStack throws an IOException
     * with a helpful message instead of returning an empty batch.
     */
    @Test
    void provide_stack_throws_when_all_images_fail_sbom_generation() throws Exception {
      // Given a multi-stage Dockerfile where every FROM image fails SBOM generation
      var manifestPath = TEST_MANIFESTS.resolve("multi_stage/Dockerfile");
      var provider = new DockerfileProvider(manifestPath);

      try (MockedStatic<ImageUtils> imageUtilsMock = Mockito.mockStatic(ImageUtils.class)) {
        imageUtilsMock
            .when(() -> ImageUtils.generateImageSBOM(Mockito.any(ImageRef.class)))
            .thenThrow(new IOException("SBOM generation failed"));

        assertThatThrownBy(provider::provideStack)
            .isInstanceOf(IOException.class)
            .hasMessageContaining("No analyzable FROM images found");

        // Optionally also cover provideComponent to ensure it propagates the same error
        assertThatThrownBy(provider::provideComponent)
            .isInstanceOf(IOException.class)
            .hasMessageContaining("No analyzable FROM images found");
      }

I could only see part of the existing provide_stack_skips_failing_images_in_batch test body, so in the replacement block above I preserved the visible portion verbatim and closed the method before adding the new test.

To integrate this cleanly with the rest of your file, please:

  1. Ensure the class already imports ImageUtils, ImageRef, MockedStatic, Mockito, IOException, and AssertJ's assertThatThrownBy. If not, add:
    • import java.io.IOException;
    • import org.mockito.MockedStatic;
    • import org.mockito.Mockito;
    • import static org.assertj.core.api.Assertions.assertThatThrownBy;
    • import io.github.guacsec.trustifyda.util.ImageUtils; (or the correct package for ImageUtils in your project)
  2. If ImageUtils.generateImageSBOM(..) has a different signature (e.g., additional parameters), adjust the when stub accordingly:
    • Replace Mockito.any(ImageRef.class) with the appropriate argument matchers.
  3. If provider.provideComponent() is not expected to propagate the same exception, you can remove the second assertThatThrownBy block or adapt it to the desired behavior.
  4. If your test class uses a different static mocking mechanism (e.g., PowerMockito or a JUnit 4 runner), adapt the try (MockedStatic<...>) block to match the established pattern.

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 — this proposes additional test coverage for the all-images-fail SBOM generation path. While reasonable, CONVENTIONS.md does not require exhaustive error-path coverage, and the existing test suite already covers the error path in parseAllFromImages (all_invalid scenario). No sub-task created.

@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 — (1) scratch case-sensitivity: addressed via sub-task TC-5098 created from the inline comment. (2) buildAnalysisUri URI building: this proposes using a URI builder instead of string concatenation, which is a valid robustness suggestion but has no established codebase pattern or CONVENTIONS.md backing. No sub-task created for item (2).

@a-oren

a-oren commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-5071 (commit 3b43c23)

Check Result Details
Review Feedback WARN 1 code change request → sub-task TC-5098 created (scratch case-insensitive comparison)
Root-Cause Investigation DONE Convention gap identified → TC-5099 created (document case-sensitivity consistency convention)
Scope Containment WARN All 3 task-specified files present; 18 additional files (test fixtures, supporting classes, bundled TRUSTIFY_DA_RECOMMEND feature)
Diff Size WARN 935+/29- across 23 files; includes bundled second feature commit beyond task scope
Commit Traceability WARN 1/3 commits references TC-5071; others reference TC-4938 or no task ID
Sensitive Patterns PASS No secrets or credentials detected
CI Status PASS All 44 CI checks pass
Acceptance Criteria PASS All 6 acceptance criteria satisfied
Test Quality WARN Repetitive Test Detection: 2 recommend tests share ~90% boilerplate; Test Documentation: inconsistent JavaDoc (Dockerfile_Provider_Test has docs, ImageRefTest/Exhort_Api_Test do not); Eval Quality: N/A
Test Change Classification ADDITIVE 30 new test methods added, 0 removed
Verification Commands PASS ./mvnw test passes (488 tests, 6 pre-existing Python env errors on main); ./mvnw spotless:check passes

Overall: WARN

Issues requiring attention:

  • Code fix needed: "scratch".equals(image) should be "scratch".equalsIgnoreCase(image) in DockerfileProvider.java:140 — sub-task TC-5098 tracks this fix
  • Scope note: PR bundles commit 63309e25 (TRUSTIFY_DA_RECOMMEND env var feature) which is unrelated to TC-5071. Consider splitting into a separate PR for clean traceability
  • Commit traceability: Only 1/3 commits references TC-5071 — commit bf0257f5 references TC-4938, commit 63309e25 has no task reference

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

Change "scratch".equals(image) to "scratch".equalsIgnoreCase(image)
so FROM SCRATCH, FROM Scratch, and other case variants are properly
skipped during Dockerfile parsing, matching the case-insensitive
handling of the FROM keyword itself.

Fixes: TC-5098
Assisted-by: Claude Code
@a-oren a-oren requested review from Strum355 and ruromero July 8, 2026 09:53

@ruromero ruromero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Summary

The PR adds Dockerfile/Containerfile support by parsing FROM lines, generating per-image SBOMs via Syft, and sending them to the batch-analysis endpoint. The Dockerfile parsing logic (ARG resolution, scratch skipping, multi-stage support) is solid and well-tested. However, the analysis routing has an architectural issue that breaks parity with how both IDE extensions currently consume image results.


Critical: Dockerfile analysis must route through imageAnalysis(), not stackAnalysis()

Both IDE extensions currently handle Dockerfiles by calling imageAnalysis(Set<ImageRef>), which returns Map<ImageRef, AnalysisReport> — one report per FROM image:

  • IntelliJ (image/ApiService.java:69): exhortApi.imageAnalysis(imageRefs)
  • VS Code (rhda.ts:116-117): fileType === 'docker'executeDockerImageAnalysis()exhort.imageAnalysis()

This PR routes Dockerfile content through stackAnalysis()/componentAnalysis() instead, which return CompletableFuture<AnalysisReport> (single report). The batch results are collapsed with:

reports.values().iterator().next(); // takes only the FIRST image's report

For a multi-FROM Dockerfile, all image reports after the first are silently dropped.

Suggestion: Keep stack Dockerfile as a valid CLI command, but internally detect that the manifest is a Dockerfile and delegate to the existing imageAnalysis(Set<ImageRef>) path. The DockerfileProvider parsing logic (FROM extraction, ARG resolution) is valuable — it should produce Set<ImageRef> that feeds into imageAnalysis(), preserving the Map<ImageRef, AnalysisReport> return type.

This also eliminates the duplicated batch handling code in stackAnalysis (inline) and getBatchAnalysisReportForComponent — the existing imageAnalysis already does all of this correctly.

Additionally, stackAnalysisHtml() was not updated for batch content at all (still uses buildStackRequest), so generating an HTML report for a Dockerfile via this path would not use the batch endpoint.


Bug: Docker Hub normalization casing inconsistency in ImageRef.getPackageURL()

The library-prefix branch uses lower.substring() which lowercases the image name, while the bare-name branch preserves original casing via simpleName:

if (lower.equals(simpleName.toLowerCase())) {
    repositoryUrl = "docker.io/" + simpleName;           // preserves casing
} else if (lower.startsWith(DOCKER_HUB_LIBRARY_PREFIX)) {
    repositoryUrl = "docker.io/" + lower.substring(...);  // lowercased
}

docker.io/library/MyImagedocker.io/myimage (lowercased)
MyImagedocker.io/MyImage (preserved)

Both should behave the same. The library-prefix branch should use repositoryUrl.substring(DOCKER_HUB_LIBRARY_PREFIX.length()) to preserve original casing, consistent with the bare-name branch. (Note: repositoryUrl is later lowercased when put into qualifiers anyway, but the PURL name field should be consistent.)


Low: Recommend env var naming mismatch across clients

Java uses TRUSTIFY_DA_RECOMMEND, JavaScript uses TRUSTIFY_DA_RECOMMENDATIONS_ENABLED. These should be aligned so users don't need different env vars per client. The JS client's name is already established, so consider adopting TRUSTIFY_DA_RECOMMENDATIONS_ENABLED in the Java client as well.

Also note the semantic inversion: Java defaults to true (recommend enabled unless explicitly set to false), while JS checks for the string 'false'. They happen to have the same default behavior, but the var names should match.


Low: Silent license check skip for batch content

if (!isLicenseCheckEnabled() || content.batch) {
    return CompletableFuture.completedFuture(new ComponentAnalysisResult(report, null));
}

License checking is silently skipped when content.batch is true, with no logging. This is likely intentional (license check doesn't apply to images), but a debug log would help troubleshooting.


Positive

  • Dockerfile parsing is well-structured: correct handling of multi-stage builds, ARG substitution, scratch skipping, and case-insensitive FROM matching
  • Excellent test coverage: 26 tests for DockerfileProvider with 13 fixture Dockerfiles covering edge cases
  • The Ecosystem.isDockerfile() correctly requires a dot separator, avoiding false matches on files like Dockerfileignore

…mage results

Dockerfile/Containerfile manifests now use imageAnalysis() instead of
stackAnalysis(), preserving per-image Map<ImageRef, AnalysisReport> output.
This prevents multi-FROM results from being collapsed to a single report.

- Add DockerfileProvider.parseImageRefs() to convert FROM lines to ImageRefs
- Detect Dockerfile in CLI before command routing, redirect to image analysis
- Use image full names as JSON keys in formatImageAnalysisResult()
- Block sbom/license commands for Dockerfiles with clear error message
- Make Ecosystem.isDockerfile() public for CLI detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@a-oren a-oren requested a review from ruromero July 9, 2026 13:05
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