diff --git a/dojo/tools/trivy_operator/checks_handler.py b/dojo/tools/trivy_operator/checks_handler.py index e6a1ccd8bb6..c42eef0fa8a 100644 --- a/dojo/tools/trivy_operator/checks_handler.py +++ b/dojo/tools/trivy_operator/checks_handler.py @@ -10,8 +10,15 @@ class TrivyChecksHandler: - def handle_checks(self, service, checks, test): + def handle_checks(self, labels, checks, test): findings = [] + resource_namespace = labels.get("trivy-operator.resource.namespace", "") + resource_kind = labels.get("trivy-operator.resource.kind", "") + resource_name = labels.get("trivy-operator.resource.name", "") + container_name = labels.get("trivy-operator.container.name", "") + service = f"{resource_namespace}/{resource_kind}/{resource_name}" + if container_name != "": + service = f"{service}/{container_name}" for check in checks: check_title = check.get("title") check_severity = TRIVY_SEVERITIES[check.get("severity")] @@ -23,6 +30,10 @@ def handle_checks(self, service, checks, test): + check_id.lower() ) check_description = check.get("description", "") + check_description += "\n**container.name:** " + container_name + check_description += "\n**resource.kind:** " + resource_kind + check_description += "\n**resource.name:** " + resource_name + check_description += "\n**resource.namespace:** " + resource_namespace title = f"{check_id} - {check_title}" finding = Finding( test=test, @@ -33,6 +44,7 @@ def handle_checks(self, service, checks, test): static_finding=True, dynamic_finding=False, service=service, + tags=[resource_namespace], ) if check_id: finding.unsaved_vulnerability_ids = [check_id] diff --git a/dojo/tools/trivy_operator/parser.py b/dojo/tools/trivy_operator/parser.py index 138cee17624..8be42e8e31e 100644 --- a/dojo/tools/trivy_operator/parser.py +++ b/dojo/tools/trivy_operator/parser.py @@ -25,7 +25,15 @@ def get_findings(self, scan_file, test): data = json.loads(str(scan_data, "utf-8")) except Exception: data = json.loads(scan_data) + findings = [] + if type(data) is list: + for listitems in data: + findings += self.output_findings(listitems, test) + else: + findings += self.output_findings(data, test) + return findings + def output_findings(self, data, test): if data is None: return [] metadata = data.get("metadata", None) @@ -40,24 +48,15 @@ def get_findings(self, scan_file, test): benchmarkreport = benchmark.get("detailReport", None) findings = [] if report is not None: - resource_namespace = labels.get( - "trivy-operator.resource.namespace", "", - ) - resource_kind = labels.get("trivy-operator.resource.kind", "") - resource_name = labels.get("trivy-operator.resource.name", "") - container_name = labels.get("trivy-operator.container.name", "") - service = f"{resource_namespace}/{resource_kind}/{resource_name}" - if container_name != "": - service = f"{service}/{container_name}" vulnerabilities = report.get("vulnerabilities", None) if vulnerabilities is not None: - findings += TrivyVulnerabilityHandler().handle_vulns(service, vulnerabilities, test) + findings += TrivyVulnerabilityHandler().handle_vulns(labels, vulnerabilities, test) checks = report.get("checks", None) if checks is not None: - findings += TrivyChecksHandler().handle_checks(service, checks, test) + findings += TrivyChecksHandler().handle_checks(labels, checks, test) secrets = report.get("secrets", None) if secrets is not None: - findings += TrivySecretsHandler().handle_secrets(service, secrets, test) + findings += TrivySecretsHandler().handle_secrets(labels, secrets, test) elif benchmarkreport is not None: findings += TrivyComplianceHandler().handle_compliance(benchmarkreport, test) return findings diff --git a/dojo/tools/trivy_operator/secrets_handler.py b/dojo/tools/trivy_operator/secrets_handler.py index c5e767a1bc5..a00c894a034 100644 --- a/dojo/tools/trivy_operator/secrets_handler.py +++ b/dojo/tools/trivy_operator/secrets_handler.py @@ -15,8 +15,15 @@ class TrivySecretsHandler: - def handle_secrets(self, service, secrets, test): + def handle_secrets(self, labels, secrets, test): findings = [] + resource_namespace = labels.get("trivy-operator.resource.namespace", "") + resource_kind = labels.get("trivy-operator.resource.kind", "") + resource_name = labels.get("trivy-operator.resource.name", "") + container_name = labels.get("trivy-operator.container.name", "") + service = f"{resource_namespace}/{resource_kind}/{resource_name}" + if container_name != "": + service = f"{service}/{container_name}" for secret in secrets: secret_title = secret.get("title") secret_category = secret.get("category") @@ -31,7 +38,10 @@ def handle_secrets(self, service, secrets, test): category=secret_category, match=secret_match, ) - + secret_description += "\n**container.name:** " + container_name + secret_description += "\n**resource.kind:** " + resource_kind + secret_description += "\n**resource.name:** " + resource_name + secret_description += "\n**resource.namespace:** " + resource_namespace finding = Finding( test=test, title=title, @@ -42,6 +52,7 @@ def handle_secrets(self, service, secrets, test): static_finding=True, dynamic_finding=False, service=service, + tags=[resource_namespace], ) if secret_rule_id: finding.unsaved_vulnerability_ids = [secret_rule_id] diff --git a/dojo/tools/trivy_operator/vulnerability_handler.py b/dojo/tools/trivy_operator/vulnerability_handler.py index 13be3e55a41..a5a26e1288a 100644 --- a/dojo/tools/trivy_operator/vulnerability_handler.py +++ b/dojo/tools/trivy_operator/vulnerability_handler.py @@ -14,8 +14,15 @@ class TrivyVulnerabilityHandler: - def handle_vulns(self, service, vulnerabilities, test): + def handle_vulns(self, labels, vulnerabilities, test): findings = [] + resource_namespace = labels.get("trivy-operator.resource.namespace", "") + resource_kind = labels.get("trivy-operator.resource.kind", "") + resource_name = labels.get("trivy-operator.resource.name", "") + container_name = labels.get("trivy-operator.container.name", "") + service = f"{resource_namespace}/{resource_kind}/{resource_name}" + if container_name != "": + service = f"{service}/{container_name}" for vulnerability in vulnerabilities: vuln_id = vulnerability.get("vulnerabilityID", "0") severity = TRIVY_SEVERITIES[vulnerability.get("severity")] @@ -24,8 +31,7 @@ def handle_vulns(self, service, vulnerabilities, test): package_name = vulnerability.get("resource") package_version = vulnerability.get("installedVersion") cvssv3_score = vulnerability.get("score") - - finding_tags = [] + finding_tags = [resource_namespace] target_target = None target_class = None package_path = None @@ -57,7 +63,10 @@ def handle_vulns(self, service, vulnerabilities, test): description = DESCRIPTION_TEMPLATE.format( title=vulnerability.get("title"), fixed_version=mitigation, ) - + description += "\n**container.name:** " + container_name + description += "\n**resource.kind:** " + resource_kind + description += "\n**resource.name:** " + resource_name + description += "\n**resource.namespace:** " + resource_namespace title = f"{vuln_id} {package_name} {package_version}" finding = Finding( test=test, diff --git a/unittests/scans/trivy_operator/findings_in_list.json b/unittests/scans/trivy_operator/findings_in_list.json new file mode 100644 index 00000000000..7a79600eb8f --- /dev/null +++ b/unittests/scans/trivy_operator/findings_in_list.json @@ -0,0 +1,399 @@ +[ + { + "apiVersion": "aquasecurity.github.io/v1alpha1", + "kind": "ConfigAuditReport", + "metadata": { + "annotations": { + "trivy-operator.aquasecurity.github.io/report-ttl": "24h0m0s" + }, + "creationTimestamp": "2023-03-23T16:22:54Z", + "generation": 1, + "labels": { + "plugin-config-hash": "659b7b9c46", + "resource-spec-hash": "fc85b485f", + "trivy-operator.resource.kind": "ReplicaSet", + "trivy-operator.resource.name": "nginx-deployment-965685897", + "trivy-operator.resource.namespace": "default" + }, + "name": "replicaset-nginx-deployment-965685897", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": false, + "controller": true, + "kind": "ReplicaSet", + "name": "nginx-deployment-965685897", + "uid": "d19c7f74-b4c3-429d-9a45-1b2f5efc3c88" + } + ], + "resourceVersion": "1268", + "uid": "a92e0951-e988-419d-8602-6852f920ce06" + }, + "report": { + "checks": [ + { + "category": "Kubernetes Security Check", + "checkID": "KSV014", + "description": "An immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'securityContext.readOnlyRootFilesystem' to true" + ], + "severity": "LOW", + "success": false, + "title": "Root file system is not read-only" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV016", + "description": "When containers have memory requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'resources.requests.memory'" + ], + "severity": "LOW", + "success": false, + "title": "Memory requests not specified" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV030", + "description": "The RuntimeDefault/Localhost seccomp profile must be required, or allow specific additional profiles.", + "messages": [ + "Either Pod or Container should set 'securityContext.seccompProfile.type' to 'RuntimeDefault'" + ], + "severity": "LOW", + "success": false, + "title": "Default Seccomp profile not set" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV015", + "description": "When containers have resource requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'resources.requests.cpu'" + ], + "severity": "LOW", + "success": false, + "title": "CPU requests not specified" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV011", + "description": "Enforcing CPU limits prevents DoS via resource exhaustion.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'resources.limits.cpu'" + ], + "severity": "LOW", + "success": false, + "title": "CPU not limited" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV012", + "description": "'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'securityContext.runAsNonRoot' to true" + ], + "severity": "MEDIUM", + "success": false, + "title": "Runs as root user" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV018", + "description": "Enforcing memory limits prevents DoS via resource exhaustion.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'resources.limits.memory'" + ], + "severity": "LOW", + "success": false, + "title": "Memory not limited" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV110", + "description": "ensure that default namespace should not be used", + "messages": [ + "ReplicaSet 'nginx-deployment-965685897' should not be set with 'default' namespace" + ], + "severity": "LOW", + "success": false, + "title": "The default namespace should not be used" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV003", + "description": "The container should drop all default capabilities and add only those that are needed for its execution.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should add 'ALL' to 'securityContext.capabilities.drop'" + ], + "severity": "LOW", + "success": false, + "title": "Default capabilities not dropped" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV021", + "description": "Force the container to run with group ID \u003e 10000 to avoid conflicts with the host’s user table.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'securityContext.runAsGroup' \u003e 10000" + ], + "severity": "LOW", + "success": false, + "title": "Runs with low group ID" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV001", + "description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'securityContext.allowPrivilegeEscalation' to false" + ], + "severity": "MEDIUM", + "success": false, + "title": "Process can elevate its own privileges" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV106", + "description": "Containers must drop ALL capabilities, and are only permitted to add back the NET_BIND_SERVICE capability.", + "messages": [ + "container should drop all" + ], + "severity": "LOW", + "success": false, + "title": "Container capabilities must only include NET_BIND_SERVICE" + }, + { + "category": "Kubernetes Security Check", + "checkID": "KSV020", + "description": "Force the container to run with user ID \u003e 10000 to avoid conflicts with the host’s user table.", + "messages": [ + "Container 'nginx' of ReplicaSet 'nginx-deployment-965685897' should set 'securityContext.runAsUser' \u003e 10000" + ], + "severity": "LOW", + "success": false, + "title": "Runs with low user ID" + } + ], + "scanner": { + "name": "Trivy", + "vendor": "Aqua Security", + "version": "dev" + }, + "summary": { + "criticalCount": 0, + "highCount": 0, + "lowCount": 11, + "mediumCount": 2 + }, + "updateTimestamp": "2023-03-23T16:22:54Z" + } + }, + { + "kind": "VulnerabilityReport", + "apiVersion": "aquasecurity.github.io/v1alpha1", + "metadata": { + "name": "pod-ubuntu-ubuntu", + "namespace": "lbc", + "uid": "e2c1fa59-051b-479d-ab47-f7bf6e7f858d", + "resourceVersion": "26700784781", + "generation": 1, + "creationTimestamp": "2024-01-23T13:43:55Z", + "labels": { + "resource-spec-hash": "666674544b", + "trivy-operator.container.name": "ubuntu", + "trivy-operator.resource.kind": "Pod", + "trivy-operator.resource.name": "ubuntu", + "trivy-operator.resource.namespace": "lbc" + }, + "annotations": { + "trivy-operator.aquasecurity.github.io/report-ttl": "24h0m0s" + }, + "ownerReferences": [ + { + "apiVersion": "v1", + "kind": "Pod", + "name": "ubuntu", + "uid": "aa8d6ec8-5417-4190-93e9-6d4d78dc8da9", + "controller": true, + "blockOwnerDeletion": false + } + ], + "managedFields": [ + { + "manager": "trivy-operator", + "operation": "Update", + "apiVersion": "aquasecurity.github.io/v1alpha1", + "time": "2024-01-23T13:43:55Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:trivy-operator.aquasecurity.github.io/report-ttl": {} + }, + "f:labels": { + ".": {}, + "f:resource-spec-hash": {}, + "f:trivy-operator.container.name": {}, + "f:trivy-operator.resource.kind": {}, + "f:trivy-operator.resource.name": {}, + "f:trivy-operator.resource.namespace": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"aa8d6ec8-5417-4190-93e9-6d4d78dc8da9\"}": {} + } + }, + "f:report": { + ".": {}, + "f:artifact": { + ".": {}, + "f:digest": {}, + "f:repository": {}, + "f:tag": {} + }, + "f:os": { + ".": {}, + "f:family": {}, + "f:name": {} + }, + "f:registry": { + ".": {}, + "f:server": {} + }, + "f:scanner": { + ".": {}, + "f:name": {}, + "f:vendor": {}, + "f:version": {} + }, + "f:summary": { + ".": {}, + "f:criticalCount": {}, + "f:highCount": {}, + "f:lowCount": {}, + "f:mediumCount": {}, + "f:noneCount": {}, + "f:unknownCount": {} + }, + "f:updateTimestamp": {}, + "f:vulnerabilities": {} + } + } + } + ] + }, + "report": { + "updateTimestamp": "2024-01-23T13:43:55Z", + "scanner": { + "name": "Trivy", + "vendor": "Aqua Security", + "version": "0.48.3" + }, + "registry": { + "server": "index.docker.io" + }, + "artifact": { + "repository": "library/ubuntu", + "digest": "sha256:f78909c2b360d866b3220655c0b079838258b8891a12ac25fc670f0cbb54229f", + "tag": "20.04" + }, + "os": { + "family": "ubuntu", + "name": "20.04" + }, + "summary": { + "criticalCount": 0, + "highCount": 0, + "mediumCount": 5, + "lowCount": 0, + "unknownCount": 0, + "noneCount": 0 + }, + "vulnerabilities": [ + { + "vulnerabilityID": "CVE-2024-0553", + "resource": "libgnutls30", + "installedVersion": "3.6.13-2ubuntu1.9", + "fixedVersion": "3.6.13-2ubuntu1.10", + "publishedDate": "2024-01-16T12:15:45Z", + "lastModifiedDate": "2024-01-19T21:15:08Z", + "severity": "MEDIUM", + "title": "gnutls: incomplete fix for CVE-2023-5981", + "primaryLink": "https://avd.aquasec.com/nvd/cve-2024-0553", + "links": [], + "score": 5.9, + "target": "ubuntu:20.04 (ubuntu 20.04)", + "class": "os-pkgs", + "packageType": "ubuntu" + }, + { + "vulnerabilityID": "CVE-2024-22365", + "resource": "libpam-modules", + "installedVersion": "1.3.1-5ubuntu4.6", + "fixedVersion": "1.3.1-5ubuntu4.7", + "publishedDate": "", + "lastModifiedDate": "", + "severity": "MEDIUM", + "title": "pam: allowing unpriledged user to block another user namespace", + "primaryLink": "https://avd.aquasec.com/nvd/cve-2024-22365", + "links": [], + "score": 5.5, + "target": "ubuntu:20.04 (ubuntu 20.04)", + "class": "os-pkgs", + "packageType": "ubuntu" + }, + { + "vulnerabilityID": "CVE-2024-22365", + "resource": "libpam-modules-bin", + "installedVersion": "1.3.1-5ubuntu4.6", + "fixedVersion": "1.3.1-5ubuntu4.7", + "publishedDate": "", + "lastModifiedDate": "", + "severity": "MEDIUM", + "title": "pam: allowing unpriledged user to block another user namespace", + "primaryLink": "https://avd.aquasec.com/nvd/cve-2024-22365", + "links": [], + "score": 5.5, + "target": "ubuntu:20.04 (ubuntu 20.04)", + "class": "os-pkgs", + "packageType": "ubuntu" + }, + { + "vulnerabilityID": "CVE-2024-22365", + "resource": "libpam-runtime", + "installedVersion": "1.3.1-5ubuntu4.6", + "fixedVersion": "1.3.1-5ubuntu4.7", + "publishedDate": "", + "lastModifiedDate": "", + "severity": "MEDIUM", + "title": "pam: allowing unpriledged user to block another user namespace", + "primaryLink": "https://avd.aquasec.com/nvd/cve-2024-22365", + "links": [], + "score": 5.5, + "target": "ubuntu:20.04 (ubuntu 20.04)", + "class": "os-pkgs", + "packageType": "ubuntu" + }, + { + "vulnerabilityID": "CVE-2024-22365", + "resource": "libpam0g", + "installedVersion": "1.3.1-5ubuntu4.6", + "fixedVersion": "1.3.1-5ubuntu4.7", + "publishedDate": "", + "lastModifiedDate": "", + "severity": "MEDIUM", + "title": "pam: allowing unpriledged user to block another user namespace", + "primaryLink": "https://avd.aquasec.com/nvd/cve-2024-22365", + "links": [], + "score": 5.5, + "target": "ubuntu:20.04 (ubuntu 20.04)", + "class": "os-pkgs", + "packageType": "ubuntu" + } + ] + } + } +] \ No newline at end of file diff --git a/unittests/tools/test_trivy_operator_parser.py b/unittests/tools/test_trivy_operator_parser.py index 8ac6b5f3189..5e4a71558da 100644 --- a/unittests/tools/test_trivy_operator_parser.py +++ b/unittests/tools/test_trivy_operator_parser.py @@ -135,7 +135,7 @@ def test_vulnerabilityreport_extended(self): self.assertEqual("3.6.13-2ubuntu1.10", finding.mitigation) self.assertEqual(5.9, finding.cvssv3_score) self.assertEqual("ubuntu:20.04 (ubuntu 20.04)", finding.file_path) - self.assertEqual("os-pkgs, ubuntu", str(finding.tags)) + self.assertEqual("lbc, os-pkgs, ubuntu", str(finding.tags)) def test_cis_benchmark(self): with open(sample_path("cis_benchmark.json"), encoding="utf-8") as test_file: @@ -157,3 +157,9 @@ def test_cis_benchmark(self): self.assertEqual("Medium", finding.severity) self.assertEqual(1, len(finding.unsaved_vulnerability_ids)) self.assertEqual("AVD-KSV-0012", finding.unsaved_vulnerability_ids[0]) + + def test_findings_in_list(self): + with open(sample_path("findings_in_list.json"), encoding="utf-8") as test_file: + parser = TrivyOperatorParser() + findings = parser.get_findings(test_file, Test()) + self.assertEqual(len(findings), 18)