Skip to content

Commit 3b43c23

Browse files
a-orenclaude
andcommitted
fix(dockerfile): analyze all FROM lines via batch analysis
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>
1 parent 63309e2 commit 3b43c23

8 files changed

Lines changed: 548 additions & 184 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/Provider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@ public abstract class Provider {
3737
public static class Content {
3838
public final byte[] buffer;
3939
public final String type;
40+
public final boolean batch;
4041

4142
public Content(byte[] buffer, String type) {
43+
this(buffer, type, false);
44+
}
45+
46+
public Content(byte[] buffer, String type, boolean batch) {
4247
this.buffer = buffer;
4348
this.type = type;
49+
this.batch = batch;
4450
}
4551
}
4652

src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java

Lines changed: 109 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,36 @@ public CompletableFuture<byte[]> stackAnalysisHtml(final String manifestFile) th
297297
public CompletableFuture<AnalysisReport> stackAnalysis(final String manifestFile)
298298
throws IOException {
299299
String exClientTraceId = commonHookBeginning(false);
300+
var content = resolveStackContent(manifestFile);
301+
var uri = resolveAnalysisUri(content);
302+
var request = buildRequest(content, uri, MediaType.APPLICATION_JSON, "Stack Analysis");
303+
if (content.batch) {
304+
return this.client
305+
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
306+
.thenApply(
307+
response -> {
308+
RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId);
309+
if (debugLoggingIsNeeded()) {
310+
logExhortRequestId(response);
311+
}
312+
Map<String, AnalysisReport> reports = getBatchStackAnalysisReports(response);
313+
commonHookAfterExhortResponse();
314+
return reports.isEmpty()
315+
? new AnalysisReport()
316+
: reports.values().iterator().next();
317+
})
318+
.exceptionally(
319+
exception -> {
320+
LOG.severe(
321+
String.format(
322+
"failed to invoke stackAnalysis (batch) for getting the json report,"
323+
+ " received message= %s ",
324+
exception.getMessage()));
325+
return new AnalysisReport();
326+
});
327+
}
300328
return this.client
301-
.sendAsync(
302-
this.buildStackRequest(manifestFile, MediaType.APPLICATION_JSON),
303-
HttpResponse.BodyHandlers.ofString())
329+
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
304330
.thenApply(
305331
response ->
306332
getAnalysisReportFromResponse(response, "StackAnalysis", "json", exClientTraceId))
@@ -376,9 +402,12 @@ public CompletableFuture<AnalysisReport> componentAnalysis(
376402
String exClientTraceId = commonHookBeginning(false);
377403
var manifestPath = Path.of(manifest);
378404
var provider = Ecosystem.getProvider(manifestPath);
379-
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
380405
var content = provider.provideComponent();
381406
commonHookAfterProviderCreatedSbomAndBeforeExhort();
407+
var uri = resolveAnalysisUri(content);
408+
if (content.batch) {
409+
return getBatchAnalysisReportForComponent(uri, content, exClientTraceId);
410+
}
382411
return getAnalysisReportForComponent(uri, content, exClientTraceId);
383412
}
384413

@@ -420,9 +449,12 @@ public CompletableFuture<AnalysisReport> componentAnalysis(String manifestFile)
420449
String exClientTraceId = commonHookBeginning(false);
421450
var manifestPath = Path.of(manifestFile);
422451
var provider = Ecosystem.getProvider(manifestPath);
423-
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
424452
var content = provider.provideComponent();
425453
commonHookAfterProviderCreatedSbomAndBeforeExhort();
454+
var uri = resolveAnalysisUri(content);
455+
if (content.batch) {
456+
return getBatchAnalysisReportForComponent(uri, content, exClientTraceId);
457+
}
426458
return getAnalysisReportForComponent(uri, content, exClientTraceId);
427459
}
428460

@@ -449,8 +481,48 @@ private CompletableFuture<AnalysisReport> getAnalysisReportForComponent(
449481
});
450482
}
451483

484+
/** Resolves the analysis URI based on whether the content is batch or single. */
485+
private URI resolveAnalysisUri(Provider.Content content) {
486+
if (content.batch) {
487+
return buildAnalysisUri(S_API_V_5_BATCH_ANALYSIS, getEndpoint());
488+
}
489+
return buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
490+
}
491+
452492
/**
453-
* Build an HTTP request wrapper for sending to the Backend API for Stack Analysis only.
493+
* Sends batch content to the batch-analysis endpoint and returns the first analysis report from
494+
* the batch response map.
495+
*/
496+
private CompletableFuture<AnalysisReport> getBatchAnalysisReportForComponent(
497+
URI uri, Provider.Content content, String exClientTraceId) {
498+
return this.client
499+
.sendAsync(
500+
this.buildRequest(content, uri, MediaType.APPLICATION_JSON, "Batch Component Analysis"),
501+
HttpResponse.BodyHandlers.ofString())
502+
.thenApply(
503+
response -> {
504+
RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId);
505+
if (debugLoggingIsNeeded()) {
506+
logExhortRequestId(response);
507+
}
508+
Map<String, AnalysisReport> reports = getBatchStackAnalysisReports(response);
509+
commonHookAfterExhortResponse();
510+
return reports.isEmpty() ? new AnalysisReport() : reports.values().iterator().next();
511+
})
512+
.exceptionally(
513+
exception -> {
514+
LOG.severe(
515+
String.format(
516+
"failed to invoke Batch Component Analysis for getting the json report,"
517+
+ " received message= %s ",
518+
exception.getMessage()));
519+
return new AnalysisReport();
520+
});
521+
}
522+
523+
/**
524+
* Build an HTTP request wrapper for sending to the Backend API for Stack Analysis only. Uses the
525+
* batch-analysis endpoint when the provider returns batch content.
454526
*
455527
* @param manifestFile the path for the manifest file
456528
* @param acceptType the type of requested content
@@ -459,13 +531,21 @@ private CompletableFuture<AnalysisReport> getAnalysisReportForComponent(
459531
*/
460532
private HttpRequest buildStackRequest(final String manifestFile, final MediaType acceptType)
461533
throws IOException {
534+
var content = resolveStackContent(manifestFile);
535+
var uri = resolveAnalysisUri(content);
536+
return buildRequest(content, uri, acceptType, "Stack Analysis");
537+
}
538+
539+
/**
540+
* Resolves the provider for the given manifest file and produces the stack content. Also tracks
541+
* timing via {@link #commonHookAfterProviderCreatedSbomAndBeforeExhort()}.
542+
*/
543+
private Provider.Content resolveStackContent(String manifestFile) throws IOException {
462544
var manifestPath = Path.of(manifestFile);
463545
var provider = Ecosystem.getProvider(manifestPath);
464-
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
465546
var content = provider.provideStack();
466547
commonHookAfterProviderCreatedSbomAndBeforeExhort();
467-
468-
return buildRequest(content, uri, acceptType, "Stack Analysis");
548+
return content;
469549
}
470550

471551
@Override
@@ -610,27 +690,29 @@ public CompletableFuture<ComponentAnalysisResult> componentAnalysisWithLicense(
610690
String exClientTraceId = commonHookBeginning(false);
611691
var manifestPath = Path.of(manifestFile);
612692
var provider = Ecosystem.getProvider(manifestPath);
613-
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
614693
var content = provider.provideComponent();
615694
String sbomJson = new String(content.buffer);
616695
commonHookAfterProviderCreatedSbomAndBeforeExhort();
617-
return getAnalysisReportForComponent(uri, content, exClientTraceId)
618-
.thenCompose(
619-
report -> {
620-
if (!isLicenseCheckEnabled()) {
621-
return CompletableFuture.completedFuture(new ComponentAnalysisResult(report, null));
622-
}
623-
return LicenseCheck.runLicenseCheck(this, provider, manifestPath, sbomJson, report)
624-
.thenApply(summary -> new ComponentAnalysisResult(report, summary))
625-
.exceptionally(
626-
ex -> {
627-
LOG.warning(
628-
String.format(
629-
"License check failed, continuing without it: %s",
630-
ex.getMessage()));
631-
return new ComponentAnalysisResult(report, null);
632-
});
633-
});
696+
var uri = resolveAnalysisUri(content);
697+
CompletableFuture<AnalysisReport> reportFuture =
698+
content.batch
699+
? getBatchAnalysisReportForComponent(uri, content, exClientTraceId)
700+
: getAnalysisReportForComponent(uri, content, exClientTraceId);
701+
return reportFuture.thenCompose(
702+
report -> {
703+
if (!isLicenseCheckEnabled() || content.batch) {
704+
return CompletableFuture.completedFuture(new ComponentAnalysisResult(report, null));
705+
}
706+
return LicenseCheck.runLicenseCheck(this, provider, manifestPath, sbomJson, report)
707+
.thenApply(summary -> new ComponentAnalysisResult(report, summary))
708+
.exceptionally(
709+
ex -> {
710+
LOG.warning(
711+
String.format(
712+
"License check failed, continuing without it: %s", ex.getMessage()));
713+
return new ComponentAnalysisResult(report, null);
714+
});
715+
});
634716
}
635717

636718
/**

0 commit comments

Comments
 (0)