diff --git a/src/main/java/com/blackduck/integration/detect/lifecycle/run/operation/OperationRunner.java b/src/main/java/com/blackduck/integration/detect/lifecycle/run/operation/OperationRunner.java index f0b1447628..4adcf3379b 100644 --- a/src/main/java/com/blackduck/integration/detect/lifecycle/run/operation/OperationRunner.java +++ b/src/main/java/com/blackduck/integration/detect/lifecycle/run/operation/OperationRunner.java @@ -869,11 +869,14 @@ public final File generateRapidJsonFile(NameVersion projectNameVersion, List scanResults) throws OperationException { + // Accepts a pre-read content string rather than List because the caller + // (RapidModeStepRunner) must read and cache the response body upfront to reuse it + // for both the V6-to-V5 conversion and this QuackPatch file write. + public final File generateFullRapidJsonFile(String contentString) throws OperationException { return auditLog.namedPublic( "Generate Rapid Full Json File", "RapidScan", - () -> new RapidModeGenerateJsonOperation(htmlEscapeDisabledGson, directoryManager).generateJsonFileFromString(scanResults.get(0).getContentString(), detectConfigurationFactory.getDetectPropertyConfiguration().getValue(DetectProperties.DETECT_QUACK_PATCH_OUTPUT).trim()) + () -> new RapidModeGenerateJsonOperation(htmlEscapeDisabledGson, directoryManager).generateJsonFileFromString(contentString, detectConfigurationFactory.getDetectPropertyConfiguration().getValue(DetectProperties.DETECT_QUACK_PATCH_OUTPUT).trim()) ); } diff --git a/src/main/java/com/blackduck/integration/detect/lifecycle/run/step/RapidModeStepRunner.java b/src/main/java/com/blackduck/integration/detect/lifecycle/run/step/RapidModeStepRunner.java index 6547aaa385..afbe1d0f63 100644 --- a/src/main/java/com/blackduck/integration/detect/lifecycle/run/step/RapidModeStepRunner.java +++ b/src/main/java/com/blackduck/integration/detect/lifecycle/run/step/RapidModeStepRunner.java @@ -17,6 +17,9 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.blackduck.integration.blackduck.api.generated.view.DeveloperScansScanView; import com.blackduck.integration.blackduck.codelocation.Result; import com.blackduck.integration.blackduck.codelocation.signaturescanner.command.ScanCommandOutput; @@ -118,18 +121,33 @@ public void runOnline(BlackDuckRunData blackDuckRunData, NameVersion projectVers } }); - // Get info about any scans that were done + // Fetch scan results using the V6 full-result endpoint (scan-6+json) for all rapid scans. + // Previously, a compact V5 call (scan-5+json) was made unconditionally here, followed by a + // separate V6 call only for QuackPatch. Since V6 is a superset of V5 for every field that + // Detect actually reads internally, the V5 call is redundant and replaced here entirely. + // + // The V6 response is converted back into List (the existing V5 type) + // via Gson so all downstream operations (aggregation, JSON output, component location analysis) + // remain unchanged. V6-only fields are ignored by Gson; V5-only fields (originId, policyStatuses) + // will be null — this is the documented breaking change for users parsing the output JSON. + // + // Content strings are read from each Response immediately and cached: Response.getContentString() + // reads the underlying HTTP entity stream which can only be consumed once. The cached strings + // are reused by both convertContentsToScanViews() below and generateFullRapidJsonFile() in the + // QuackPatch block. BlackduckScanMode mode = blackDuckRunData.getScanMode(); - List rapidResults = operationRunner.waitForRapidResults(blackDuckRunData, parsedUrls, mode); + List rapidFullResults = operationRunner.waitForRapidFullResults(blackDuckRunData, parsedUrls, mode); + List fullResultContents = extractContentStrings(rapidFullResults); + List rapidResults = convertContentsToScanViews(fullResultContents); +// List rapidResultsOld = operationRunner.waitForRapidResults(blackDuckRunData, parsedUrls, mode); if (operationRunner.shouldAttemptQuackPatchFullResults()) { - logger.info("Quack Patch is enabled, attempting to retrieve full Rapid scan results."); - List rapidFullResults = operationRunner.waitForRapidFullResults(blackDuckRunData, parsedUrls, mode); - if (rapidFullResults.isEmpty()) { + logger.info("Quack Patch is enabled, using full Rapid scan results."); + if (fullResultContents.isEmpty()) { logger.info("Quack Patch requires non-empty Rapid Scan results. Skipping Quack Patch."); } else { - File jsonFileFULL = operationRunner.generateFullRapidJsonFile(rapidFullResults); + File jsonFileFULL = operationRunner.generateFullRapidJsonFile(fullResultContents.get(0)); operationRunner.runQuackPatch(jsonFileFULL); } } @@ -224,4 +242,48 @@ private List parseScanUrls(String scanMode, SignatureScanOuputResult si } return parsedUrls; } + + // Reads each Response body string upfront and returns them as a plain List. + // This must be done before any other consumer touches the responses: the underlying + // Apache HttpClient entity stream can only be read once per Response object. + private List extractContentStrings(List responses) throws OperationException { + try { + List contents = new ArrayList<>(); + for (Response response : responses) { + contents.add(response.getContentString()); + } + return contents; + } catch (IntegrationException e) { + throw new OperationException(e); + } + } + + // Converts V6 full-result page content strings into the V5 DeveloperScansScanView type. + // Each content string is a paged API response: { "items": [ ... ], "totalCount": N, ... }. + // Gson deserializes each item by field name — fields present in both V5 and V6 map correctly, + // V6-only fields (matchTypes, allVulnerabilities, etc.) are silently ignored, and V5-only + // fields (originId, policyStatuses) are left null since they do not exist in the V6 schema. + // + // Items without violating policies are filtered out here to match V5 volume in downstream + // outputs: V5 was server-filtered to policy-violating components only, while V6 also returns + // vulnerable-but-not-policy-violating components. Without this filter, generateRapidJsonFile, + // generateComponentLocationAnalysisIfEnabled, and logRapidReport would all see a larger + // component set than they did under V5. QuackPatch is unaffected because it consumes the + // unfiltered raw page string (fullResultContents.get(0)) before this conversion runs. + private List convertContentsToScanViews(List contents) { + List scanViews = new ArrayList<>(); + for (String content : contents) { + JsonObject page = gson.fromJson(content, JsonObject.class); + JsonArray items = page.getAsJsonArray("items"); + if (items != null) { + for (JsonElement item : items) { + DeveloperScansScanView view = gson.fromJson(item, DeveloperScansScanView.class); + if (view.getViolatingPolicies() != null && !view.getViolatingPolicies().isEmpty()) { + scanViews.add(view); + } + } + } + } + return scanViews; + } }