From 2f0d8f9f24ea1edebf42d52ef7d33778a4b736d6 Mon Sep 17 00:00:00 2001 From: Munish Chouhan Date: Sat, 2 Nov 2024 03:11:44 +0530 Subject: [PATCH] Cap number of scan vulnerabilities reported (#728) Signed-off-by: munishchouhan Signed-off-by: Paolo Di Tommaso Co-authored-by: Paolo Di Tommaso --- .../wave/configuration/ScanConfig.groovy | 5 +- .../scan/ContainerScanServiceImpl.groovy | 6 +- .../service/scan/ScanVulnerability.groovy | 2 +- .../service/scan/TrivyResultProcessor.groovy | 21 ++++- .../scan/ContainerScanServiceImplTest.groovy | 2 +- .../scan/TrivyResultProcessorTest.groovy | 89 ++++++++++++++++++- 6 files changed, 114 insertions(+), 11 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/configuration/ScanConfig.groovy b/src/main/groovy/io/seqera/wave/configuration/ScanConfig.groovy index 1223d137e..a3232f300 100644 --- a/src/main/groovy/io/seqera/wave/configuration/ScanConfig.groovy +++ b/src/main/groovy/io/seqera/wave/configuration/ScanConfig.groovy @@ -84,6 +84,9 @@ class ScanConfig { @Value('${wave.scan.environment}') List environment + @Value('${wave.scan.vulnerability.limit:100}') + Integer vulnerabilityLimit + String getScanImage() { return scanImage } @@ -133,6 +136,6 @@ class ScanConfig { @PostConstruct private void init() { - log.info("Scan config: docker image name: ${scanImage}; cache directory: ${cacheDirectory}; timeout=${timeout}; cpus: ${requestsCpu}; mem: ${requestsMemory}; severity: $severity; retry-attempts: $retryAttempts; env=${environment}") + log.info("Scan config: docker image name: ${scanImage}; cache directory: ${cacheDirectory}; timeout=${timeout}; cpus: ${requestsCpu}; mem: ${requestsMemory}; severity: $severity; vulnerability-limit: $vulnerabilityLimit; retry-attempts: $retryAttempts; env=${environment}") } } diff --git a/src/main/groovy/io/seqera/wave/service/scan/ContainerScanServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/scan/ContainerScanServiceImpl.groovy index c87020d60..1571bb50d 100644 --- a/src/main/groovy/io/seqera/wave/service/scan/ContainerScanServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/scan/ContainerScanServiceImpl.groovy @@ -269,12 +269,14 @@ class ContainerScanServiceImpl implements ContainerScanService, JobHandler { - static final Map ORDER = Map.of( + static final private Map ORDER = Map.of( 'LOW', 0, 'MEDIUM', 1, 'HIGH', 2, diff --git a/src/main/groovy/io/seqera/wave/service/scan/TrivyResultProcessor.groovy b/src/main/groovy/io/seqera/wave/service/scan/TrivyResultProcessor.groovy index a1055037b..a4bd14788 100644 --- a/src/main/groovy/io/seqera/wave/service/scan/TrivyResultProcessor.groovy +++ b/src/main/groovy/io/seqera/wave/service/scan/TrivyResultProcessor.groovy @@ -21,6 +21,8 @@ package io.seqera.wave.service.scan import java.nio.file.Path import groovy.json.JsonSlurper +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import io.seqera.wave.exception.ScanRuntimeException /** @@ -30,16 +32,23 @@ import io.seqera.wave.exception.ScanRuntimeException * @author Paolo Di Tommaso */ @Slf4j +@CompileStatic class TrivyResultProcessor { - static List process(Path reportFile) { - process(reportFile.getText()) + static List parse(Path scanFile) { + return parse(scanFile.getText()) } - static List process(String trivyResult) { + static List parse(Path scanFile, int maxEntries) { + final result = parse(scanFile) + return filter(result, maxEntries) + } + + @CompileDynamic + static List parse(String scanJson) { final slurper = new JsonSlurper() try{ - final jsonMap = slurper.parseText(trivyResult) as Map + final jsonMap = slurper.parseText(scanJson) as Map return jsonMap.Results.collect { result -> result.Vulnerabilities.collect { vulnerability -> new ScanVulnerability( @@ -57,4 +66,8 @@ class TrivyResultProcessor { throw new ScanRuntimeException("Failed to parse the trivy result", e) } } + + static protected List filter(List vulnerabilities, int limit){ + vulnerabilities.toSorted((v,w) -> w.compareTo(v)).take(limit) + } } diff --git a/src/test/groovy/io/seqera/wave/service/scan/ContainerScanServiceImplTest.groovy b/src/test/groovy/io/seqera/wave/service/scan/ContainerScanServiceImplTest.groovy index 2410142c1..4ab8ca50b 100644 --- a/src/test/groovy/io/seqera/wave/service/scan/ContainerScanServiceImplTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/scan/ContainerScanServiceImplTest.groovy @@ -151,7 +151,7 @@ class ContainerScanServiceImplTest extends Specification { and: def KEY = 'scan-20' def jobService = Mock(JobService) - def service = new ContainerScanServiceImpl(scanStore: scanStore, persistenceService: persistenceService, jobService: jobService) + def service = new ContainerScanServiceImpl(scanStore: scanStore, persistenceService: persistenceService, jobService: jobService, config: new ScanConfig(vulnerabilityLimit: 100)) def job = JobSpec.scan(KEY, 'ubuntu:latest', Instant.now(), Duration.ofMinutes(1), workDir) def scan = ScanEntry.of(scanId: KEY, buildId: 'build-20', containerImage: 'ubuntu:latest', startTime: Instant.now()) diff --git a/src/test/groovy/io/seqera/wave/service/scan/TrivyResultProcessorTest.groovy b/src/test/groovy/io/seqera/wave/service/scan/TrivyResultProcessorTest.groovy index 0563f8842..8bb0186b6 100644 --- a/src/test/groovy/io/seqera/wave/service/scan/TrivyResultProcessorTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/scan/TrivyResultProcessorTest.groovy @@ -91,7 +91,7 @@ class TrivyResultProcessorTest extends Specification { """ when: - def result = TrivyResultProcessor.process(trivyDockerResulJson) + def result = TrivyResultProcessor.parse(trivyDockerResulJson) then: def vulnerability = result[0] @@ -105,9 +105,94 @@ class TrivyResultProcessorTest extends Specification { } + def "should return a sorted map of vulnerabilities"() { + given: + def trivyDockerResulJson = """ + { "Results": [ + { + "Target": "sample-application", + "Class": "os-pkgs", + "Type": "linux", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2023-0001", + "PkgID": "example-lib@1.0.0", + "PkgName": "example-lib", + "InstalledVersion": "1.0.0", + "FixedVersion": "1.0.1", + "Severity": "LOW", + "Description": "A minor vulnerability with low impact.", + "PrimaryURL": "https://example.com/CVE-2023-0001" + }, + { + "VulnerabilityID": "CVE-2023-0002", + "PkgID": "example-lib@1.2.3", + "PkgName": "example-lib", + "InstalledVersion": "1.2.3", + "FixedVersion": "1.2.4", + "Severity": "MEDIUM", + "Description": "A vulnerability that allows unauthorized access.", + "PrimaryURL": "https://example.com/CVE-2023-0002" + }, + { + "VulnerabilityID": "CVE-2023-0003", + "PkgID": "example-lib@2.3.4", + "PkgName": "example-lib", + "InstalledVersion": "2.3.4", + "FixedVersion": "2.3.5", + "Severity": "HIGH", + "Description": "A vulnerability that could lead to remote code execution.", + "PrimaryURL": "https://example.com/CVE-2023-0003" + }, + { + "VulnerabilityID": "CVE-2023-0004", + "PkgID": "example-lib@3.0.0", + "PkgName": "example-lib", + "InstalledVersion": "3.0.0", + "FixedVersion": "3.0.1", + "Severity": "HIGH", + "Description": "A random test vulnerability with unspecified impact.", + "PrimaryURL": "https://example.com/CVE-2023-0004" + }, + { + "VulnerabilityID": "CVE-2023-0005", + "PkgID": "example-lib@3.1.0", + "PkgName": "example-lib", + "InstalledVersion": "3.1.0", + "FixedVersion": "3.1.1", + "Severity": "CRITICAL", + "Description": "Another random test vulnerability for testing purposes.", + "PrimaryURL": "https://example.com/CVE-2023-0005" + } + ] + } + ] + }""".stripIndent() + + when: + def result = TrivyResultProcessor.parse(trivyDockerResulJson) + result = TrivyResultProcessor.filter(result, 4) + + then: + result.size() == 4 + result[0].severity == "CRITICAL" + result[0].id == "CVE-2023-0005" + result[1].severity == "HIGH" + result[1].id == "CVE-2023-0003" + result[2].severity == "HIGH" + result[2].id == "CVE-2023-0004" + result[3].severity == "MEDIUM" + result[3].id == "CVE-2023-0002" + } + + def 'should not fail with empty list' () { + expect: + TrivyResultProcessor.filter([], 10) == [] + } + def "process should throw exception if json is not correct"() { when: - TrivyResultProcessor.process("invalid json") + TrivyResultProcessor.parse("invalid json") then: thrown ScanRuntimeException }