Skip to content
Open
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 @@ -440,22 +440,29 @@ public CompletedScanResult processCompletedScan(
String summary;
ScanCheckResult.CheckResult checkResult;

if (result.isClean()) {
if (result.isClean() && !result.hasMaliciousVerdict()) {
checkResult = ScanCheckResult.CheckResult.PASSED;
summary = "No threats found";
} else {
threatCount = result.getThreats().size();

// A benign verdict may only downgrade enforcement (never quarantine on findings alone); a malicious
// verdict may never upgrade past the scanner's own enforced setting
boolean findingsEnforced = scannerEnforced && !result.hasBenignVerdict();

// Save threats and get enforcement statistics
var saveResult = saveThreats(job, result, scannerEnforced);
var saveResult = saveThreats(job, result, findingsEnforced);

// Determine check result based on actual enforcement (considers allowlist)
if (saveResult.hasEnforcedThreats()) {
boolean unmitigatedMaliciousVerdict = scannerEnforced && result.hasMaliciousVerdict() && threatCount == 0;
if (saveResult.hasEnforcedThreats() || unmitigatedMaliciousVerdict) {
checkResult = ScanCheckResult.CheckResult.QUARANTINE;
summary = String.format(
"Found %d threat(s) - %d enforced",
threatCount,
saveResult.enforcedCount());
summary = threatCount == 0
? "Marked malicious by scanner verdict (no specific findings)"
: String.format(
"Found %d threat(s) - %d enforced",
threatCount,
saveResult.enforcedCount());
} else {
// Threats found but none enforced (scanner not enforced OR all on allowlist)
checkResult = ScanCheckResult.CheckResult.PASSED;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ public String extractString(String response, String format, String path) throws
};
}

/**
* Extract a boolean value from a response. Accepts JSON booleans and string forms ("true"/"false"). Returns null
* when the path is missing or the value is not a boolean.
*/
public Boolean extractBoolean(String response, String format, String path) throws ScannerException {
if (response == null || path == null) {
return null;
}

return switch (format.toLowerCase()) {
case "json" -> extractJsonBoolean(response, path);
case "xml" -> throw new UnsupportedOperationException("XML extraction not yet implemented");
case "text" -> throw new UnsupportedOperationException("Text extraction not yet implemented");
default -> throw new IllegalArgumentException("Unsupported format: " + format);
};
}

/**
* Extract a list of objects from a response.
*/
Expand Down Expand Up @@ -79,6 +96,26 @@ private String extractJsonString(String json, String jsonPath) throws ScannerExc
return node != null && !node.isMissingNode() ? node.asString(null) : null;
}

private Boolean extractJsonBoolean(String json, String jsonPath) throws ScannerException {
JsonNode node = pickFirst(parseJson(json), jsonPath);
if (node == null || node.isMissingNode() || node.isNull()) {
return null;
}
if (node.isBoolean()) {
return node.asBoolean();
}
if (node.isString()) {
String value = node.asString(null);
if ("true".equalsIgnoreCase(value)) {
return true;
}
if ("false".equalsIgnoreCase(value)) {
return false;
}
}
return null;
}

private List<Map<String, Object>> extractJsonList(String json, String jsonPath) throws ScannerException {
JsonNode root = parseJson(json);
List<JsonNode> nodes = evaluateJsonPath(root, jsonPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -405,26 +406,33 @@ private Scanner.Result parseResult(
responseConfig.getSummaryPath());
}

// Extract explicit malicious verdict, if configured. A configured path that fails to resolve is treated as
// a scan failure
Boolean maliciousVerdict = null;
if (responseConfig.getMaliciousPath() != null) {
maliciousVerdict = responseExtractor.extractBoolean(
response,
responseConfig.getFormat(),
responseConfig.getMaliciousPath());
if (maliciousVerdict == null) {
throw new ScannerException(
"Scanner '" + scannerName + "' has malicious-path '" + responseConfig.getMaliciousPath()
+ "' configured, but it did not resolve to a boolean in the response");
}
}

// Extract threats
String threatsPath = responseConfig.getThreatsPath();
if (threatsPath == null) {
// No threats path - assume clean
return Scanner.Result.clean(summary);
List<Scanner.Threat> threats = Collections.emptyList();
if (threatsPath != null) {
List<Map<String, Object>> threatObjects = responseExtractor.extractList(
response,
responseConfig.getFormat(),
threatsPath);
threats = mapThreats(threatObjects, responseConfig);
}

List<Map<String, Object>> threatObjects = responseExtractor.extractList(
response,
responseConfig.getFormat(),
threatsPath);

// Map threats
List<Scanner.Threat> threats = mapThreats(threatObjects, responseConfig);

if (threats.isEmpty()) {
return Scanner.Result.clean(summary);
} else {
return Scanner.Result.withThreats(threats, summary);
}
return Scanner.Result.of(threats, summary, maliciousVerdict);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ public static class ResponseConfig {
// Surfaced verbatim in ScanCheckResult.summary when present
private String summaryPath;

/**
* JSONPath to a boolean malicious verdict on the result response (e.g. "$.verdictData.isMalicious"). When set,
* quarantine is driven by this verdict rather than "any findings present".
*/
private String maliciousPath;

// For error detection
private String errorPath; // Path to error message
private String errorCondition; // Expression to detect errors
Expand Down Expand Up @@ -527,6 +533,13 @@ public void setSummaryPath(String summaryPath) {
this.summaryPath = summaryPath;
}

public String getMaliciousPath() {
return maliciousPath;
}
public void setMaliciousPath(String maliciousPath) {
this.maliciousPath = maliciousPath;
}

public String getErrorPath() {
return errorPath;
}
Expand Down
47 changes: 42 additions & 5 deletions server/src/main/java/org/eclipse/openvsx/scanning/Scanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,33 +67,51 @@ class Result {
private final boolean clean;
private final List<Threat> threats;
private final String summary;
private final Boolean maliciousVerdict;

private Result(boolean clean, List<Threat> threats, String summary) {
private Result(boolean clean, List<Threat> threats, String summary, Boolean maliciousVerdict) {
this.clean = clean;
this.threats = new ArrayList<>(threats);
this.summary = summary;
this.maliciousVerdict = maliciousVerdict;
}

@NonNull
public static Result clean() {
return new Result(true, Collections.emptyList(), null);
return new Result(true, Collections.emptyList(), null, null);
}

@NonNull
public static Result clean(@Nullable String summary) {
return new Result(true, Collections.emptyList(), summary);
return new Result(true, Collections.emptyList(), summary, null);
}

@NonNull
public static Result withThreats(@NonNull List<Threat> threats) {
return new Result(false, threats, null);
return new Result(false, threats, null, null);
}

@NonNull
public static Result withThreats(@NonNull List<Threat> threats, @Nullable String summary) {
return new Result(false, threats, summary);
return new Result(false, threats, summary, null);
}

/**
* Builds a result directly from a scanner's findings and its explicit malicious verdict (e.g. Argus
* verdictData.isMalicious). Prefer this over clean()/withThreats() whenever a verdict is available: naming a
* malicious result via "clean" reads as contradictory, since findings and the safety verdict are independent
* signals here. "Clean" (no findings) is derived from the threats list itself.
*/
@NonNull
public static Result of(@NonNull List<Threat> threats, @Nullable String summary, @Nullable Boolean malicious) {
return new Result(threats.isEmpty(), threats, summary, malicious);
}

/**
* True when there are no findings recorded. This reflects the threats list only — it is not a safety verdict. A
* malicious verdict with zero findings is still isClean() == true; check {@link #hasMaliciousVerdict()} for the
* actual verdict.
*/
public boolean isClean() {
return clean;
}
Expand All @@ -108,6 +126,25 @@ public List<Threat> getThreats() {
public String getSummary() {
return summary;
}

/**
* Explicit malicious verdict from the scanner response, if configured. null means the scanner does not expose
* this signal.
*/
@Nullable
public Boolean isMalicious() {
return maliciousVerdict;
}

/** True when the scanner explicitly returned a malicious verdict. */
public boolean hasMaliciousVerdict() {
return Boolean.TRUE.equals(maliciousVerdict);
}

/** True when the scanner explicitly returned a non-malicious verdict. */
public boolean hasBenignVerdict() {
return Boolean.FALSE.equals(maliciousVerdict);
}
}

/**
Expand Down
Loading