Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -869,11 +869,14 @@ public final File generateRapidJsonFile(NameVersion projectNameVersion, List<Dev
);
}

public final File generateFullRapidJsonFile(List<Response> scanResults) throws OperationException {
// Accepts a pre-read content string rather than List<Response> 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())
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<DeveloperScansScanView> (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<DeveloperScansScanView> rapidResults = operationRunner.waitForRapidResults(blackDuckRunData, parsedUrls, mode);
List<Response> rapidFullResults = operationRunner.waitForRapidFullResults(blackDuckRunData, parsedUrls, mode);
List<String> fullResultContents = extractContentStrings(rapidFullResults);
List<DeveloperScansScanView> rapidResults = convertContentsToScanViews(fullResultContents);

// List<DeveloperScansScanView> rapidResultsOld = operationRunner.waitForRapidResults(blackDuckRunData, parsedUrls, mode);

if (operationRunner.shouldAttemptQuackPatchFullResults()) {
logger.info("Quack Patch is enabled, attempting to retrieve full Rapid scan results.");
List<Response> 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);
}
}
Expand Down Expand Up @@ -224,4 +242,48 @@ private List<HttpUrl> parseScanUrls(String scanMode, SignatureScanOuputResult si
}
return parsedUrls;
}

// Reads each Response body string upfront and returns them as a plain List<String>.
// 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<String> extractContentStrings(List<Response> responses) throws OperationException {
try {
List<String> 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<DeveloperScansScanView> convertContentsToScanViews(List<String> contents) {
List<DeveloperScansScanView> 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;
}
}