From 33b20b4784076158b69f478772b2c6fe928acb81 Mon Sep 17 00:00:00 2001 From: Alejandro Munoz Date: Fri, 24 Jul 2026 14:27:19 -0400 Subject: [PATCH 1/2] feat: quarantine based on scanner malicious verdict Some security providers return an explicit isMalicious verdict rather than relying solely on rule/finding matches. Add support for scanners to report this verdict; quarantine is enforced when malicious is true, and findings are recorded as warnings when malicious is false. --- .../ExtensionScanPersistenceService.java | 21 ++- .../scanning/HttpResponseExtractor.java | 37 ++++++ .../openvsx/scanning/RemoteScanner.java | 40 +++--- .../scanning/RemoteScannerProperties.java | 13 ++ .../org/eclipse/openvsx/scanning/Scanner.java | 47 ++++++- .../ExtensionScanPersistenceServiceTest.java | 121 ++++++++++++++++++ .../scanning/RemoteScannerPropertiesTest.java | 2 + .../openvsx/scanning/RemoteScannerTest.java | 107 ++++++++++++++++ .../scanning/ResponseExtractorTest.java | 19 +++ .../eclipse/openvsx/scanning/ScannerTest.java | 18 +++ 10 files changed, 397 insertions(+), 28 deletions(-) create mode 100644 server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceService.java b/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceService.java index d3c2e2ea0..09ba97d62 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceService.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceService.java @@ -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; diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/HttpResponseExtractor.java b/server/src/main/java/org/eclipse/openvsx/scanning/HttpResponseExtractor.java index f56a66b7b..cce4004d0 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/HttpResponseExtractor.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/HttpResponseExtractor.java @@ -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. */ @@ -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> extractJsonList(String json, String jsonPath) throws ScannerException { JsonNode root = parseJson(json); List nodes = evaluateJsonPath(root, jsonPath); diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScanner.java b/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScanner.java index f65cf86b4..dd4038085 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScanner.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScanner.java @@ -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; @@ -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 threats = Collections.emptyList(); + if (threatsPath != null) { + List> threatObjects = responseExtractor.extractList( + response, + responseConfig.getFormat(), + threatsPath); + threats = mapThreats(threatObjects, responseConfig); } - List> threatObjects = responseExtractor.extractList( - response, - responseConfig.getFormat(), - threatsPath); - - // Map threats - List 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); } /** diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScannerProperties.java b/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScannerProperties.java index 85de9e7bf..128f07d13 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScannerProperties.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/RemoteScannerProperties.java @@ -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 @@ -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; } diff --git a/server/src/main/java/org/eclipse/openvsx/scanning/Scanner.java b/server/src/main/java/org/eclipse/openvsx/scanning/Scanner.java index d78707ff5..0a2623a00 100644 --- a/server/src/main/java/org/eclipse/openvsx/scanning/Scanner.java +++ b/server/src/main/java/org/eclipse/openvsx/scanning/Scanner.java @@ -67,33 +67,51 @@ class Result { private final boolean clean; private final List threats; private final String summary; + private final Boolean maliciousVerdict; - private Result(boolean clean, List threats, String summary) { + private Result(boolean clean, List 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 threats) { - return new Result(false, threats, null); + return new Result(false, threats, null, null); } @NonNull public static Result withThreats(@NonNull List 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 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; } @@ -108,6 +126,25 @@ public List 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); + } } /** diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java index 2ebc7ab65..74c4aae66 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java @@ -13,6 +13,7 @@ package org.eclipse.openvsx.scanning; import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,8 +23,11 @@ import tools.jackson.databind.json.JsonMapper; import org.eclipse.openvsx.entities.ExtensionScan; +import org.eclipse.openvsx.entities.FileDecision; +import org.eclipse.openvsx.entities.ScanCheckResult; import org.eclipse.openvsx.entities.ScanStatus; import org.eclipse.openvsx.entities.ScannerJob; +import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.repositories.FileDecisionRepository; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.repositories.ScanCheckResultRepository; @@ -31,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ExtensionScanPersistenceServiceTest { @@ -101,4 +106,120 @@ void resetJobForRetry_resetsJobFieldsAndFlipsScanToScanning() { verify(scannerJobRepository).save(job); verify(repositories).saveExtensionScan(scan); } + + // ========== processCompletedScan: isMalicious verdict vs scanner "enforced" setting ========== + + private ScannerJob jobFor(ExtensionScan scan) { + var job = new ScannerJob(); + job.setId(1L); + job.setScanId(String.valueOf(scan.getId())); + job.setScannerType("ARGUS"); + return job; + } + + @Test + void processCompletedScan_quarantines_whenEnforcedAndFindingsPresent_noVerdict() { + var scan = new ExtensionScan(); + scan.setId(1L); + when(repositories.findExtensionScan(1L)).thenReturn(scan); + var job = jobFor(scan); + var result = Scanner.Result.withThreats(List.of(new Scanner.Threat("rule", "desc", "HIGH"))); + + var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.QUARANTINE); + } + + @Test + void processCompletedScan_passes_whenBenignVerdict_evenWithFindings_andScannerEnforced() { + var scan = new ExtensionScan(); + scan.setId(2L); + when(repositories.findExtensionScan(2L)).thenReturn(scan); + var job = jobFor(scan); + var result = Scanner.Result.of( + List.of(new Scanner.Threat("rule", "desc", "HIGH")), null, false); + + var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.PASSED); + } + + @Test + void processCompletedScan_quarantines_whenMaliciousVerdict_withZeroFindings_andScannerEnforced() { + var scan = new ExtensionScan(); + scan.setId(3L); + when(repositories.findExtensionScan(3L)).thenReturn(scan); + var job = jobFor(scan); + var result = Scanner.Result.of(List.of(), null, true); + + var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.QUARANTINE); + } + + @Test + void processCompletedScan_doesNotQuarantine_whenMaliciousVerdict_butScannerNotEnforced() { + // A malicious verdict must never override a scanner explicitly configured as enforced=false: + // that setting is documented to always leave the extension activated. + var scan = new ExtensionScan(); + scan.setId(4L); + when(repositories.findExtensionScan(4L)).thenReturn(scan); + var job = jobFor(scan); + var result = Scanner.Result.of( + List.of(new Scanner.Threat("rule", "desc", "HIGH")), null, true); + + var outcome = svc.processCompletedScan(job, result, false, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.PASSED); + } + + @Test + void processCompletedScan_doesNotQuarantine_whenMaliciousVerdict_zeroFindings_andScannerNotEnforced() { + var scan = new ExtensionScan(); + scan.setId(5L); + when(repositories.findExtensionScan(5L)).thenReturn(scan); + var job = jobFor(scan); + var result = Scanner.Result.of(List.of(), null, true); + + var outcome = svc.processCompletedScan(job, result, false, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.PASSED); + } + + @Test + void processCompletedScan_doesNotQuarantine_whenMaliciousVerdict_butAllFindingsAllowlisted() { + // The admin-managed allowlist takes precedence over a repeat verdict on already-vetted files. + var scan = new ExtensionScan(); + scan.setId(6L); + when(repositories.findExtensionScan(6L)).thenReturn(scan); + var job = jobFor(scan); + when(fileDecisionRepository.findByFileHash("hash1")) + .thenReturn(FileDecision.allowed("hash1", new UserData())); + var result = Scanner.Result.of( + List.of(new Scanner.Threat("rule", "desc", "HIGH", "file1", "hash1")), null, true); + + var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.PASSED); + } + + @Test + void processCompletedScan_quarantines_whenMaliciousVerdict_andOnlySomeFindingsAllowlisted() { + var scan = new ExtensionScan(); + scan.setId(7L); + when(repositories.findExtensionScan(7L)).thenReturn(scan); + var job = jobFor(scan); + when(fileDecisionRepository.findByFileHash("hash1")) + .thenReturn(FileDecision.allowed("hash1", new UserData())); + var result = Scanner.Result.of( + List.of( + new Scanner.Threat("rule", "desc", "HIGH", "file1", "hash1"), + new Scanner.Threat("rule", "desc", "HIGH", "file2", "hash2")), + null, + true); + + var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); + + assertThat(outcome.checkResult()).isEqualTo(ScanCheckResult.CheckResult.QUARANTINE); + } } diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerPropertiesTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerPropertiesTest.java index 52906db1a..8f6a8ba36 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerPropertiesTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerPropertiesTest.java @@ -145,6 +145,7 @@ void responseConfig_settersWork() { response.setErrorPath("$.error"); response.setErrorCondition("$.status == 'error'"); response.setSummaryPath("$.verdictData.summary"); + response.setMaliciousPath("$.verdictData.isMalicious"); assertEquals("xml", response.getFormat()); assertEquals("$.scan.id", response.getJobIdPath()); @@ -154,6 +155,7 @@ void responseConfig_settersWork() { assertEquals("$.error", response.getErrorPath()); assertEquals("$.status == 'error'", response.getErrorCondition()); assertEquals("$.verdictData.summary", response.getSummaryPath()); + assertEquals("$.verdictData.isMalicious", response.getMaliciousPath()); } // === ThreatMapping tests === diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java new file mode 100644 index 000000000..626ea53c7 --- /dev/null +++ b/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java @@ -0,0 +1,107 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.openvsx.scanning; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link RemoteScanner}'s response parsing, focused on the fail-closed + * behavior of the isMalicious verdict extraction. + */ +class RemoteScannerTest { + + private RemoteScanner newScanner() { + var config = new RemoteScannerProperties.ScannerConfig(); + config.setType("ARGUS"); + return new RemoteScanner( + "argus", + config, + null, + null, + new HttpResponseExtractor(new JsonMapper()), + null); + } + + private RemoteScannerProperties.HttpOperation operationWithMaliciousPath(String maliciousPath) { + var response = new RemoteScannerProperties.ResponseConfig(); + response.setMaliciousPath(maliciousPath); + + var operation = new RemoteScannerProperties.HttpOperation(); + operation.setResponse(response); + return operation; + } + + private Scanner.Result parseResult( + RemoteScanner scanner, + String response, + RemoteScannerProperties.HttpOperation operation + ) throws Exception { + Method method = RemoteScanner.class.getDeclaredMethod( + "parseResult", String.class, RemoteScannerProperties.HttpOperation.class); + method.setAccessible(true); + try { + return (Scanner.Result) method.invoke(scanner, response, operation); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof ScannerException scannerException) { + throw scannerException; + } + throw e; + } + } + + @Test + void parseResult_failsClosed_whenMaliciousPathConfigured_butMissingFromResponse() { + var scanner = newScanner(); + var operation = operationWithMaliciousPath("$.verdictData.isMalicious"); + String response = "{\"verdictData\": {\"summary\": \"ok\"}}"; // no isMalicious field + + assertThrows(ScannerException.class, () -> parseResult(scanner, response, operation)); + } + + @Test + void parseResult_failsClosed_whenMaliciousPathConfigured_butNotABoolean() { + var scanner = newScanner(); + var operation = operationWithMaliciousPath("$.verdictData.isMalicious"); + String response = "{\"verdictData\": {\"isMalicious\": \"unknown\"}}"; // not true/false + + assertThrows(ScannerException.class, () -> parseResult(scanner, response, operation)); + } + + @Test + void parseResult_succeeds_whenMaliciousPathConfigured_andResolvesToBoolean() throws Exception { + var scanner = newScanner(); + var operation = operationWithMaliciousPath("$.verdictData.isMalicious"); + String response = "{\"verdictData\": {\"isMalicious\": true}}"; + + Scanner.Result result = parseResult(scanner, response, operation); + + assertTrue(result.hasMaliciousVerdict()); + } + + @Test + void parseResult_doesNotThrow_whenNoMaliciousPathConfigured() throws Exception { + var scanner = newScanner(); + var operation = operationWithMaliciousPath(null); + String response = "{}"; + + Scanner.Result result = parseResult(scanner, response, operation); + + assertNull(result.isMalicious()); + } +} diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/ResponseExtractorTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/ResponseExtractorTest.java index bbb3ba934..0690cb564 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/ResponseExtractorTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/ResponseExtractorTest.java @@ -72,6 +72,25 @@ void extractString_returnsNullForNullInput() throws ScannerException { assertNull(extractor.extractString(null, "json", "$.field")); } + @Test + void extractBoolean_readsJsonBoolean() throws ScannerException { + String json = """ + {"verdictData": {"isMalicious": true}} + """; + + assertEquals(Boolean.TRUE, extractor.extractBoolean(json, "json", "$.verdictData.isMalicious")); + } + + @Test + void extractBoolean_readsFalseAndMissing() throws ScannerException { + String json = """ + {"verdictData": {"isMalicious": false}} + """; + + assertEquals(Boolean.FALSE, extractor.extractBoolean(json, "json", "$.verdictData.isMalicious")); + assertNull(extractor.extractBoolean(json, "json", "$.verdictData.missing")); + } + @Test void extractString_handlesArrayIndex() throws ScannerException { String json = """ diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java index 0bb36138f..d71609d62 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/ScannerTest.java @@ -115,6 +115,24 @@ void result_withThreatsAndSummaryStoresBoth() { assertFalse(result.isClean()); assertEquals(1, result.getThreats().size()); assertEquals("Verdict: malicious", result.getSummary()); + assertNull(result.isMalicious()); + } + + @Test + void result_maliciousVerdictHelpers() { + var benign = Scanner.Result.of( + List.of(new Scanner.Threat("rule", "details", "LOW")), + "Looks fine", + false); + assertTrue(benign.hasBenignVerdict()); + assertFalse(benign.hasMaliciousVerdict()); + assertEquals(Boolean.FALSE, benign.isMalicious()); + + var malicious = Scanner.Result.of(List.of(), "Malicious package", true); + assertTrue(malicious.hasMaliciousVerdict()); + assertFalse(malicious.hasBenignVerdict()); + assertTrue(malicious.isClean()); // no findings, but verdict is malicious + assertEquals(Boolean.TRUE, malicious.isMalicious()); } // === Scanner default methods === From 4eeebba96db3b2bd332194eab15a252f2896c141 Mon Sep 17 00:00:00 2001 From: Alejandro Munoz Date: Fri, 24 Jul 2026 20:27:18 -0400 Subject: [PATCH 2/2] style: apply eclipse formatter line wrapping to scanning tests Reformat multi-argument calls in ExtensionScanPersistenceServiceTest and RemoteScannerTest to match the project's eclipse formatter output. --- .../ExtensionScanPersistenceServiceTest.java | 12 +++++++++--- .../eclipse/openvsx/scanning/RemoteScannerTest.java | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java index 74c4aae66..7f0ada727 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/ExtensionScanPersistenceServiceTest.java @@ -137,7 +137,9 @@ void processCompletedScan_passes_whenBenignVerdict_evenWithFindings_andScannerEn when(repositories.findExtensionScan(2L)).thenReturn(scan); var job = jobFor(scan); var result = Scanner.Result.of( - List.of(new Scanner.Threat("rule", "desc", "HIGH")), null, false); + List.of(new Scanner.Threat("rule", "desc", "HIGH")), + null, + false); var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); @@ -166,7 +168,9 @@ void processCompletedScan_doesNotQuarantine_whenMaliciousVerdict_butScannerNotEn when(repositories.findExtensionScan(4L)).thenReturn(scan); var job = jobFor(scan); var result = Scanner.Result.of( - List.of(new Scanner.Threat("rule", "desc", "HIGH")), null, true); + List.of(new Scanner.Threat("rule", "desc", "HIGH")), + null, + true); var outcome = svc.processCompletedScan(job, result, false, LocalDateTime.now()); @@ -196,7 +200,9 @@ void processCompletedScan_doesNotQuarantine_whenMaliciousVerdict_butAllFindingsA when(fileDecisionRepository.findByFileHash("hash1")) .thenReturn(FileDecision.allowed("hash1", new UserData())); var result = Scanner.Result.of( - List.of(new Scanner.Threat("rule", "desc", "HIGH", "file1", "hash1")), null, true); + List.of(new Scanner.Threat("rule", "desc", "HIGH", "file1", "hash1")), + null, + true); var outcome = svc.processCompletedScan(job, result, true, LocalDateTime.now()); diff --git a/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java b/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java index 626ea53c7..18d4e2a89 100644 --- a/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/scanning/RemoteScannerTest.java @@ -53,7 +53,9 @@ private Scanner.Result parseResult( RemoteScannerProperties.HttpOperation operation ) throws Exception { Method method = RemoteScanner.class.getDeclaredMethod( - "parseResult", String.class, RemoteScannerProperties.HttpOperation.class); + "parseResult", + String.class, + RemoteScannerProperties.HttpOperation.class); method.setAccessible(true); try { return (Scanner.Result) method.invoke(scanner, response, operation);