From 5810fa46cf9ee11071806d20ac0bfb110027b5d3 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Wed, 1 Jul 2026 10:23:29 +0300 Subject: [PATCH 1/2] fix(hardened): enable hardened recommendations for single analysis and fix cache leakage (TC-4987) Wire the single analysis endpoint (/api/v5/analysis) to return hardened image recommendations for OCI PURLs, matching the batch endpoint behavior. Also fix a cache contamination bug where recommend=false was bypassed by cached PackageItems containing recommendation data from previous requests. - Set SBOM_ID_PROPERTY from root PURL in processAnalysisRequest - Add addSbomIdToDependencyTree to single analysis route - Guard addSbomIdToDependencyTree against null/blank sbomId - Preserve all DependencyTree fields when rebuilding - Keep OCI PURLs in filterLocalPackages for hardened image matching - Strip cached recommendation data when recommend=false Implements TC-4987 Assisted-by: Claude Code --- .../backend/ExhortIntegration.java | 10 +++++++- .../trustify/TrustifyIntegration.java | 25 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java b/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java index c0d95d75..61792186 100644 --- a/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java +++ b/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java @@ -218,6 +218,7 @@ public void configure() { .to(direct("preProcessAnalysisRequest")) .process(this::processAnalysisRequest) .process(monitoringProcessor::processOriginalRequest) + .bean(this, "addSbomIdToDependencyTree") .to(direct("analyzeSbom")) .to(direct("enrichTrustedLibraries")) .to(direct("report")) @@ -317,6 +318,9 @@ private void processAnalysisRequest(Exchange exchange) { var parser = getSbomParser(exchange); var tree = parser.parse(exchange.getIn().getBody(InputStream.class)); exchange.setProperty(Constants.DEPENDENCY_TREE_PROPERTY, tree); + if (tree.root() != null) { + exchange.setProperty(Constants.SBOM_ID_PROPERTY, tree.root().purl().canonicalize()); + } exchange.getIn().setBody(tree); } @@ -418,6 +422,9 @@ private void validateSbomsInput(Exchange exchange) { public DependencyTree addSbomIdToDependencyTree( @Body DependencyTree tree, @ExchangeProperty(Constants.SBOM_ID_PROPERTY) String sbomId) { + if (sbomId == null || sbomId.isBlank()) { + return tree; + } Map dependencies = new HashMap<>(); if (tree.dependencies() != null) { dependencies.putAll(tree.dependencies()); @@ -426,7 +433,8 @@ public DependencyTree addSbomIdToDependencyTree( if (!dependencies.containsKey(sbomRef)) { dependencies.put(sbomRef, new DirectDependency(sbomRef, Collections.emptySet())); } - return DependencyTree.builder().dependencies(dependencies).build(); + return new DependencyTree( + dependencies, tree.licenseExpressions(), tree.root(), tree.componentHashes()); } public Map.Entry transformBatchAnalysisReport( diff --git a/src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/TrustifyIntegration.java b/src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/TrustifyIntegration.java index 97dee126..b500e7c3 100644 --- a/src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/TrustifyIntegration.java +++ b/src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/TrustifyIntegration.java @@ -462,10 +462,13 @@ private void filterLocalPackages(Exchange exchange) { .collect(Collectors.toSet()); var sbomId = exchange.getProperty(Constants.SBOM_ID_PROPERTY, String.class); if (sbomId != null && !sbomId.isBlank()) { - // Batch analysis injects the SBOM root component as a dependency entry to drive other - // processing; it is not necessarily returned by Trustify and would otherwise stay a cache - // miss on every request. - packages.remove(new PackageRef(sbomId)); + // The SBOM root component is injected as a dependency entry to drive recommendation + // processing; remove it from the vulnerability scan unless it is an OCI image PURL, + // which must stay so the hardened-image recommendation route can match it. + PackageRef sbomRef = new PackageRef(sbomId); + if (!OCI_PURL_TYPE.equals(sbomRef.purl().getType())) { + packages.remove(sbomRef); + } } exchange.getIn().setBody(packages); } @@ -501,6 +504,13 @@ private void aggregateCacheHits(Exchange exchange) { return; } + // Cached PackageItems may contain recommendation data from a previous request + // with recommend=true. Strip it when the current request has recommend=false. + if (!isRecommendEnabled(exchange)) { + cacheHits.replaceAll( + (ref, item) -> new PackageItem(item.packageRef(), null, item.issues(), item.warnings())); + } + ProviderResponse response = exchange.getIn().getBody(ProviderResponse.class); if (response == null) { String providerName = exchange.getProperty(Constants.PROVIDER_NAME_PROPERTY, String.class); @@ -520,4 +530,11 @@ private void aggregateCacheHits(Exchange exchange) { exchange.getIn().setBody(new ProviderResponse(mergedItems, response.status())); } + + private boolean isRecommendEnabled(Exchange exchange) { + Object param = exchange.getProperty(Constants.RECOMMEND_PARAM); + if (param instanceof Boolean b) return b; + if (param instanceof String s) return Boolean.parseBoolean(s); + return true; + } } From 609ff3fc44ee97e4d02c2a567220fca482c58aed Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Wed, 1 Jul 2026 14:05:13 +0300 Subject: [PATCH 2/2] fix(test): exclude root PURL from license lookup and avoid preserving licenseExpressions (TC-4987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exclude the root PURL from DepsDevRequestBuilder.fromSbom() — it represents the project itself, not a dependency, so deps.dev has no data for it. This prevents uncacheable cache misses that broke testLicensesCaching. Pass null for licenseExpressions in addSbomIdToDependencyTree to preserve existing batch license behavior and avoid breaking the batch_report.json fixture. Co-Authored-By: Claude Opus 4.6 --- .../trustifyda/integration/backend/ExhortIntegration.java | 3 +-- .../trustifyda/integration/licenses/DepsDevRequestBuilder.java | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java b/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java index 61792186..435393a3 100644 --- a/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java +++ b/src/main/java/io/github/guacsec/trustifyda/integration/backend/ExhortIntegration.java @@ -433,8 +433,7 @@ public DependencyTree addSbomIdToDependencyTree( if (!dependencies.containsKey(sbomRef)) { dependencies.put(sbomRef, new DirectDependency(sbomRef, Collections.emptySet())); } - return new DependencyTree( - dependencies, tree.licenseExpressions(), tree.root(), tree.componentHashes()); + return new DependencyTree(dependencies, null, tree.root(), tree.componentHashes()); } public Map.Entry transformBatchAnalysisReport( diff --git a/src/main/java/io/github/guacsec/trustifyda/integration/licenses/DepsDevRequestBuilder.java b/src/main/java/io/github/guacsec/trustifyda/integration/licenses/DepsDevRequestBuilder.java index 164be9e3..3a744a82 100644 --- a/src/main/java/io/github/guacsec/trustifyda/integration/licenses/DepsDevRequestBuilder.java +++ b/src/main/java/io/github/guacsec/trustifyda/integration/licenses/DepsDevRequestBuilder.java @@ -68,6 +68,9 @@ public List fromEndpoint(LicensesRequest request) { public List fromSbom(DependencyTree tree) { var purls = tree.getAll(); + if (tree.root() != null) { + purls.remove(tree.root()); + } return new ArrayList(purls); }