From 0a57df4a7a3438d511bcba3704b6e5da7c5fd516 Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Fri, 15 Sep 2023 10:57:20 +0200 Subject: [PATCH] Support Trivy JSON files as source for the security warnings metric. Closes #6927. --- .../src/example-reports/example-report.json | 10 ++ components/collector/.vulture_ignore_list.py | 141 +++++++++++++++++- .../src/source_collectors/__init__.py | 1 + .../src/source_collectors/trivy/__init__.py | 0 .../trivy/security_warnings.py | 62 ++++++++ .../tests/source_collectors/trivy/__init__.py | 0 .../trivy/test_security_warnings.py | 85 +++++++++++ .../shared_data_model/logos/trivy_json.png | Bin 0 -> 40154 bytes .../src/shared_data_model/metrics.py | 123 +++++++-------- .../src/shared_data_model/sources/__init__.py | 2 + .../shared_data_model/sources/quality_time.py | 4 +- .../src/shared_data_model/sources/sarif.py | 2 +- .../src/shared_data_model/sources/trivy.py | 40 +++++ components/testdata/reports/trivy/trivy.json | 101 +++++++++++++ docs/src/changelog.md | 4 + 15 files changed, 507 insertions(+), 68 deletions(-) create mode 100644 components/collector/src/source_collectors/trivy/__init__.py create mode 100644 components/collector/src/source_collectors/trivy/security_warnings.py create mode 100644 components/collector/tests/source_collectors/trivy/__init__.py create mode 100644 components/collector/tests/source_collectors/trivy/test_security_warnings.py create mode 100644 components/shared_code/src/shared_data_model/logos/trivy_json.png create mode 100644 components/shared_code/src/shared_data_model/sources/trivy.py create mode 100644 components/testdata/reports/trivy/trivy.json diff --git a/components/api_server/src/example-reports/example-report.json b/components/api_server/src/example-reports/example-report.json index 75833c64e6..05ca2b92b0 100644 --- a/components/api_server/src/example-reports/example-report.json +++ b/components/api_server/src/example-reports/example-report.json @@ -472,6 +472,16 @@ "password": "", "private_token": "" } + }, + "84eee1c2-53e5-5260-75cb-2b444a5ba336": { + "type": "trivy_json", + "parameters": { + "url": "http://testdata:8000/reports/trivy_json/trivy.json", + "landing_url": "http://localhost:8000/reports/trivy_json/trivy.json", + "username": "", + "password": "", + "private_token": "" + } } }, "name": null, diff --git a/components/collector/.vulture_ignore_list.py b/components/collector/.vulture_ignore_list.py index 78febacf3e..c8157ebbeb 100644 --- a/components/collector/.vulture_ignore_list.py +++ b/components/collector/.vulture_ignore_list.py @@ -1,5 +1,136 @@ -__new__ # unused function (src/source_collectors/azure_devops/source_up_to_dateness.py:60) -__new__ # unused function (src/source_collectors/gitlab/source_up_to_dateness.py:136) -scan_status # unused variable (src/source_collectors/harbor/security_warnings.py:43) -id # unused variable (src/source_collectors/jira/velocity.py:17) -id # unused variable (src/source_collectors/jira/velocity.py:31) +AnchoreSecurityWarnings # unused class (src/source_collectors/anchore/security_warnings.py:9) +AnchoreSourceUpToDateness # unused class (src/source_collectors/anchore/source_up_to_dateness.py:13) +AnchoreJenkinsPluginSecurityWarnings # unused class (src/source_collectors/anchore_jenkins_plugin/security_warnings.py:9) +AnchoreJenkinsPluginSourceUpToDateness # unused class (src/source_collectors/anchore_jenkins_plugin/source_up_to_dateness.py:6) +AxeCoreAccessibility # unused class (src/source_collectors/axe_core/accessibility.py:53) +AxeCoreSourceUpToDateness # unused class (src/source_collectors/axe_core/source_up_to_dateness.py:10) +AxeCoreSourceVersion # unused class (src/source_collectors/axe_core/source_version.py:9) +AxeCSVAccessibility # unused class (src/source_collectors/axe_csv/accessibility.py:12) +AxeHTMLReporterAccessibility # unused class (src/source_collectors/axe_html_reporter/accessibility.py:14) +AzureDevopsAverageIssueLeadTime # unused class (src/source_collectors/azure_devops/average_issue_lead_time.py:13) +AzureDevopsFailedJobs # unused class (src/source_collectors/azure_devops/failed_jobs.py:8) +AzureDevopsJobRunsWithinTimePeriod # unused class (src/source_collectors/azure_devops/job_runs_within_time_period.py:11) +AzureDevopsSourceUpToDateness # unused class (src/source_collectors/azure_devops/source_up_to_dateness.py:53) +__new__ # unused function (src/source_collectors/azure_devops/source_up_to_dateness.py:56) +AzureDevopsUnmergedBranches # unused class (src/source_collectors/azure_devops/unmerged_branches.py:15) +AzureDevopsUnusedJobs # unused class (src/source_collectors/azure_devops/unused_jobs.py:11) +AzureDevopsUserStoryPoints # unused class (src/source_collectors/azure_devops/user_story_points.py:12) +BanditSecurityWarnings # unused class (src/source_collectors/bandit/security_warnings.py:10) +BanditSourceUpToDateness # unused class (src/source_collectors/bandit/source_up_to_dateness.py:10) +CalendarSourceUpToDateness # unused class (src/source_collectors/calendar/source_up_to_dateness.py:13) +CalendarTimeRemaining # unused class (src/source_collectors/calendar/time_remaining.py:13) +CargoAuditSecurityWarnings # unused class (src/source_collectors/cargo_audit/security_warnings.py:10) +ClocLOC # unused class (src/source_collectors/cloc/loc.py:10) +ClocSourceVersion # unused class (src/source_collectors/cloc/source_version.py:9) +CoberturaSourceUpToDateness # unused class (src/source_collectors/cobertura/source_up_to_dateness.py:11) +CoberturaSourceVersion # unused class (src/source_collectors/cobertura/source_version.py:10) +CoberturaUncoveredBranches # unused class (src/source_collectors/cobertura/uncovered_branches.py:6) +CoberturaUncoveredLines # unused class (src/source_collectors/cobertura/uncovered_lines.py:6) +CoberturaJenkinsPluginSourceUpToDateness # unused class (src/source_collectors/cobertura_jenkins_plugin/source_up_to_dateness.py:8) +CoberturaJenkinsPluginUncoveredBranches # unused class (src/source_collectors/cobertura_jenkins_plugin/uncovered_branches.py:6) +CoberturaJenkinsPluginUncoveredLines # unused class (src/source_collectors/cobertura_jenkins_plugin/uncovered_lines.py:6) +ComposerDependencies # unused class (src/source_collectors/composer/dependencies.py:7) +CxSASTSecurityWarnings # unused class (src/source_collectors/cxsast/security_warnings.py:10) +CxSASTSourceUpToDateness # unused class (src/source_collectors/cxsast/source_up_to_dateness.py:10) +CxSASTSourceVersion # unused class (src/source_collectors/cxsast/source_version.py:12) +GatlingPerformanceTestDuration # unused class (src/source_collectors/gatling/performancetest_duration.py:9) +GatlingSlowTransactions # unused class (src/source_collectors/gatling/slow_transactions.py:12) +GatlingSourceUpToDateness # unused class (src/source_collectors/gatling/source_up_to_dateness.py:15) +GatlingSourceVersion # unused class (src/source_collectors/gatling/source_version.py:11) +GenericJSONSecurityWarnings # unused class (src/source_collectors/generic_json/security_warnings.py:11) +GitLabFailedJobs # unused class (src/source_collectors/gitlab/failed_jobs.py:8) +GitLabJobRunsWithinTimePeriod # unused class (src/source_collectors/gitlab/job_runs_within_time_period.py:12) +GitLabMergeRequests # unused class (src/source_collectors/gitlab/merge_requests.py:70) +GitLabSourceUpToDateness # unused class (src/source_collectors/gitlab/source_up_to_dateness.py:130) +__new__ # unused function (src/source_collectors/gitlab/source_up_to_dateness.py:133) +GitLabSourceVersion # unused class (src/source_collectors/gitlab/source_version.py:11) +GitLabUnmergedBranches # unused class (src/source_collectors/gitlab/unmerged_branches.py:15) +GitLabUnusedJobs # unused class (src/source_collectors/gitlab/unused_jobs.py:11) +scan_status # unused variable (src/source_collectors/harbor/security_warnings.py:58) +HarborSecurityWarnings # unused class (src/source_collectors/harbor/security_warnings.py:62) +JacocoSourceUpToDateness # unused class (src/source_collectors/jacoco/source_up_to_dateness.py:11) +JacocoUncoveredBranches # unused class (src/source_collectors/jacoco/uncovered_branches.py:6) +JacocoUncoveredLines # unused class (src/source_collectors/jacoco/uncovered_lines.py:6) +JacocoJenkinsPluginSourceUpToDateness # unused class (src/source_collectors/jacoco_jenkins_plugin/source_up_to_dateness.py:8) +JacocoJenkinsPluginUncoveredBranches # unused class (src/source_collectors/jacoco_jenkins_plugin/uncovered_branches.py:6) +JacocoJenkinsPluginUncoveredLines # unused class (src/source_collectors/jacoco_jenkins_plugin/uncovered_lines.py:6) +JenkinsFailedJobs # unused class (src/source_collectors/jenkins/failed_jobs.py:8) +JenkinsJobRunsWithinTimePeriod # unused class (src/source_collectors/jenkins/job_runs_within_time_period.py:12) +JenkinsSourceUpToDateness # unused class (src/source_collectors/jenkins/source_up_to_dateness.py:9) +JenkinsSourceVersion # unused class (src/source_collectors/jenkins/source_version.py:9) +JenkinsUnusedJobs # unused class (src/source_collectors/jenkins/unused_jobs.py:11) +JenkinsTestReportSourceUpToDateness # unused class (src/source_collectors/jenkins_test_report/source_up_to_dateness.py:9) +JiraAverageIssueLeadTime # unused class (src/source_collectors/jira/average_issue_lead_time.py:13) +JiraIssueStatus # unused class (src/source_collectors/jira/issue_status.py:11) +JiraManualTestDuration # unused class (src/source_collectors/jira/manual_test_duration.py:6) +JiraManualTestExecution # unused class (src/source_collectors/jira/manual_test_execution.py:14) +JiraSourceVersion # unused class (src/source_collectors/jira/source_version.py:11) +JiraUserStoryPoints # unused class (src/source_collectors/jira/user_story_points.py:6) +id # unused variable (src/source_collectors/jira/velocity.py:16) +id # unused variable (src/source_collectors/jira/velocity.py:30) +JiraVelocity # unused class (src/source_collectors/jira/velocity.py:38) +JMeterCSVPerformanceTestDuration # unused class (src/source_collectors/jmeter_csv/performancetest_duration.py:9) +JMeterCSVSlowTransactions # unused class (src/source_collectors/jmeter_csv/slow_transactions.py:11) +JMeterCSVSourceUpToDateness # unused class (src/source_collectors/jmeter_csv/source_up_to_dateness.py:15) +JMeterJSONSlowTransactions # unused class (src/source_collectors/jmeter_json/slow_transactions.py:10) +JUnitSourceUpToDateness # unused class (src/source_collectors/junit/source_up_to_dateness.py:13) +ManualNumber # unused class (src/source_collectors/manual_number/all_metrics.py:8) +NCoverSourceUpToDateness # unused class (src/source_collectors/ncover/source_up_to_dateness.py:13) +NCoverUncoveredBranches # unused class (src/source_collectors/ncover/uncovered_branches.py:6) +NCoverUncoveredLines # unused class (src/source_collectors/ncover/uncovered_lines.py:6) +NpmDependencies # unused class (src/source_collectors/npm/dependencies.py:10) +OJAuditViolations # unused class (src/source_collectors/ojaudit/violations.py:22) +OpenVASSecurityWarnings # unused class (src/source_collectors/openvas/security_warnings.py:11) +OpenVASSourceUpToDateness # unused class (src/source_collectors/openvas/source_up_to_dateness.py:11) +OpenVASSourceVersion # unused class (src/source_collectors/openvas/source_version.py:10) +OWASPDependencyCheckSecurityWarnings # unused class (src/source_collectors/owasp_dependency_check/security_warnings.py:11) +OWASPDependencyCheckSourceUpToDateness # unused class (src/source_collectors/owasp_dependency_check/source_up_to_dateness.py:13) +OWASPDependencyCheckSourceVersion # unused class (src/source_collectors/owasp_dependency_check/source_version.py:12) +OWASPZAPSecurityWarnings # unused class (src/source_collectors/owasp_zap/security_warnings.py:13) +OWASPZAPSourceUpToDateness # unused class (src/source_collectors/owasp_zap/source_up_to_dateness.py:11) +OWASPZAPSourceVersion # unused class (src/source_collectors/owasp_zap/source_version.py:10) +PerformanceTestRunnerPerformanceTestDuration # unused class (src/source_collectors/performancetest_runner/performancetest_duration.py:13) +PerformanceTestRunnerScalability # unused class (src/source_collectors/performancetest_runner/performancetest_scalability.py:13) +PerformanceTestRunnerPerformanceTestStability # unused class (src/source_collectors/performancetest_runner/performancetest_stability.py:13) +PerformanceTestRunnerSlowTransactions # unused class (src/source_collectors/performancetest_runner/slow_transactions.py:11) +PerformanceTestRunnerSoftwareVersion # unused class (src/source_collectors/performancetest_runner/software_version.py:14) +PerformanceTestRunnerSourceUpToDateness # unused class (src/source_collectors/performancetest_runner/source_up_to_dateness.py:15) +PipDependencies # unused class (src/source_collectors/pip/dependencies.py:10) +PyupioSafetySecurityWarnings # unused class (src/source_collectors/pyupio_safety/security_warnings.py:12) +QualityTimeMetrics # unused class (src/source_collectors/quality_time/metrics.py:19) +QualityTimeMissingMetrics # unused class (src/source_collectors/quality_time/missing_metrics.py:13) +QualityTimeSourceUpToDateness # unused class (src/source_collectors/quality_time/source_up_to_dateness.py:13) +QualityTimeSourceVersion # unused class (src/source_collectors/quality_time/source_version.py:9) +RobotFrameworkSourceUpToDateness # unused class (src/source_collectors/robot_framework/source_up_to_dateness.py:13) +RobotFrameworkSourceVersion # unused class (src/source_collectors/robot_framework/source_version.py:12) +RobotFrameworkJenkinsPluginSourceUpToDateness # unused class (src/source_collectors/robot_framework_jenkins_plugin/source_up_to_dateness.py:8) +SARIFJSONSecurityWarnings # unused class (src/source_collectors/sarif/security_warnings.py:6) +SARIFJSONViolations # unused class (src/source_collectors/sarif/violations.py:6) +SnykSecurityWarnings # unused class (src/source_collectors/snyk/security_warnings.py:13) +SonarQubeCommentedOutCode # unused class (src/source_collectors/sonarqube/commented_out_code.py:6) +SonarQubeComplexUnits # unused class (src/source_collectors/sonarqube/complex_units.py:6) +SonarQubeDuplicatedLines # unused class (src/source_collectors/sonarqube/duplicated_lines.py:6) +SonarQubeLOC # unused class (src/source_collectors/sonarqube/loc.py:11) +SonarQubeLongUnits # unused class (src/source_collectors/sonarqube/long_units.py:6) +SonarQubeManyParameters # unused class (src/source_collectors/sonarqube/many_parameters.py:6) +SonarQubeRemediationEffort # unused class (src/source_collectors/sonarqube/remediation_effort.py:12) +SonarQubeSecurityWarnings # unused class (src/source_collectors/sonarqube/security_warnings.py:11) +SonarQubeSoftwareVersion # unused class (src/source_collectors/sonarqube/software_version.py:11) +SonarQubeSourceUpToDateness # unused class (src/source_collectors/sonarqube/source_up_to_dateness.py:12) +SonarQubeSourceVersion # unused class (src/source_collectors/sonarqube/source_version.py:9) +SonarQubeSuppressedViolations # unused class (src/source_collectors/sonarqube/suppressed_violations.py:10) +SonarQubeUncoveredBranches # unused class (src/source_collectors/sonarqube/uncovered_branches.py:6) +SonarQubeUncoveredLines # unused class (src/source_collectors/sonarqube/uncovered_lines.py:6) +TrelloIssues # unused class (src/source_collectors/trello/issues.py:13) +TrelloSourceUpToDateness # unused class (src/source_collectors/trello/source_up_to_dateness.py:12) +VulnerabilityID # unused variable (src/source_collectors/trivy/security_warnings.py:13) +Title # unused variable (src/source_collectors/trivy/security_warnings.py:14) +Description # unused variable (src/source_collectors/trivy/security_warnings.py:15) +PkgName # unused variable (src/source_collectors/trivy/security_warnings.py:17) +InstalledVersion # unused variable (src/source_collectors/trivy/security_warnings.py:18) +FixedVersion # unused variable (src/source_collectors/trivy/security_warnings.py:19) +References # unused variable (src/source_collectors/trivy/security_warnings.py:20) +Target # unused variable (src/source_collectors/trivy/security_warnings.py:26) +Vulnerabilities # unused variable (src/source_collectors/trivy/security_warnings.py:27) +TrivyJSONSecurityWarnings # unused class (src/source_collectors/trivy/security_warnings.py:32) +RobotFrameworkSourceVersion # unused class (tests/source_collectors/robot_framework/test_source_version.py:6) diff --git a/components/collector/src/source_collectors/__init__.py b/components/collector/src/source_collectors/__init__.py index eb32cb38b3..6a2cec01f5 100644 --- a/components/collector/src/source_collectors/__init__.py +++ b/components/collector/src/source_collectors/__init__.py @@ -142,3 +142,4 @@ from .testng.tests import TestNGTests from .trello.issues import TrelloIssues from .trello.source_up_to_dateness import TrelloSourceUpToDateness +from .trivy.security_warnings import TrivyJSONSecurityWarnings diff --git a/components/collector/src/source_collectors/trivy/__init__.py b/components/collector/src/source_collectors/trivy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/collector/src/source_collectors/trivy/security_warnings.py b/components/collector/src/source_collectors/trivy/security_warnings.py new file mode 100644 index 0000000000..b6942dd962 --- /dev/null +++ b/components/collector/src/source_collectors/trivy/security_warnings.py @@ -0,0 +1,62 @@ +"""Trivy JSON collector.""" + +from typing import TypedDict, cast + +from base_collectors import JSONFileSourceCollector +from collector_utilities.type import JSON +from model import Entities, Entity + + +class TrivyJSONVulnerability(TypedDict): + """Trivy JSON for one vulnerability.""" + + VulnerabilityID: str + Title: str + Description: str + Severity: str + PkgName: str + InstalledVersion: str + FixedVersion: str + References: list[str] + + +class TrivyJSONDependencyRepository(TypedDict): + """Trivy JSON for one dependency repository.""" + + Target: str + Vulnerabilities: list[TrivyJSONVulnerability] | None + + +TrivyJSON = list[TrivyJSONDependencyRepository] + + +class TrivyJSONSecurityWarnings(JSONFileSourceCollector): + """Trivy JSON collector for security warnings.""" + + def _parse_json(self, json: JSON, filename: str) -> Entities: + """Override to parse the analysis results from the Trivy JSON.""" + entities = Entities() + for dependency_repository in cast(TrivyJSON, json): + target = dependency_repository["Target"] + for vulnerability in dependency_repository.get("Vulnerabilities") or []: + vulnerability_id = vulnerability["VulnerabilityID"] + package_name = vulnerability["PkgName"] + entities.append( + Entity( + key=f"{vulnerability_id}@{package_name}@{target}", + vulnerability_id=vulnerability_id, + title=vulnerability["Title"], + description=vulnerability["Description"], + level=vulnerability["Severity"], + package_name=package_name, + installed_version=vulnerability["InstalledVersion"], + fixed_version=vulnerability.get("FixedVersion", "none"), + url=vulnerability["References"][0], + ), + ) + return entities + + def _include_entity(self, entity: Entity) -> bool: + """Return whether to include the entity in the measurement.""" + levels = self._parameter("levels") + return entity["level"].lower() in levels diff --git a/components/collector/tests/source_collectors/trivy/__init__.py b/components/collector/tests/source_collectors/trivy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/collector/tests/source_collectors/trivy/test_security_warnings.py b/components/collector/tests/source_collectors/trivy/test_security_warnings.py new file mode 100644 index 0000000000..cd3f96bf67 --- /dev/null +++ b/components/collector/tests/source_collectors/trivy/test_security_warnings.py @@ -0,0 +1,85 @@ +"""Unit tests for the Trivy JSON security warnings collector.""" + +from typing import ClassVar + +from source_collectors.trivy.security_warnings import TrivyJSON + +from tests.source_collectors.source_collector_test_case import SourceCollectorTestCase + + +class TrivyJSONSecurityWarningsTest(SourceCollectorTestCase): + """Unit tests for the security warning metric.""" + + SOURCE_TYPE = "trivy_json" + METRIC_TYPE = "security_warnings" + VULNERABILITIES_JSON: ClassVar[TrivyJSON] = [ + { + "Target": "php-app/composer.lock", + "Vulnerabilities": None, + }, + { + "Target": "trivy-ci-test (alpine 3.7.1)", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2018-16840", + "PkgName": "curl", + "InstalledVersion": "7.61.0-r0", + "FixedVersion": "7.61.1-r1", + "Title": 'curl: Use-after-free when closing "easy" handle in Curl_close()', + "Description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through ...", + "Severity": "HIGH", + "References": [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840", + ], + }, + { + "VulnerabilityID": "CVE-2019-3822", + "PkgName": "curl", + "InstalledVersion": "7.61.1-r0", + "FixedVersion": "7.61.2-r2", + "Title": "curl: NTLMv2 type-3 header stack buffer overflow", + "Description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to ...", + "Severity": "MEDIUM", + "References": [ + "https://curl.haxx.se/docs/CVE-2019-3822.html", + "https://lists.apache.org/thread.html", + ], + }, + ], + }, + ] + EXPECTED_ENTITIES: ClassVar[list[dict[str, str]]] = [ + { + "key": "CVE-2018-16840@curl@trivy-ci-test (alpine 3_7_1)", + "vulnerability_id": "CVE-2018-16840", + "title": 'curl: Use-after-free when closing "easy" handle in Curl_close()', + "description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through ...", + "level": "HIGH", + "package_name": "curl", + "installed_version": "7.61.0-r0", + "fixed_version": "7.61.1-r1", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840", + }, + { + "key": "CVE-2019-3822@curl@trivy-ci-test (alpine 3_7_1)", + "vulnerability_id": "CVE-2019-3822", + "title": "curl: NTLMv2 type-3 header stack buffer overflow", + "description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to ...", + "level": "MEDIUM", + "package_name": "curl", + "installed_version": "7.61.1-r0", + "fixed_version": "7.61.2-r2", + "url": "https://curl.haxx.se/docs/CVE-2019-3822.html", + }, + ] + + async def test_warnings(self): + """Test the number of security warnings.""" + response = await self.collect(get_request_json_return_value=self.VULNERABILITIES_JSON) + self.assert_measurement(response, value="2", entities=self.EXPECTED_ENTITIES) + + async def test_warning_levels(self): + """Test the number of security warnings when specifying a level.""" + self.set_source_parameter("levels", ["high", "critical"]) + response = await self.collect(get_request_json_return_value=self.VULNERABILITIES_JSON) + self.assert_measurement(response, value="1", entities=[self.EXPECTED_ENTITIES[0]]) diff --git a/components/shared_code/src/shared_data_model/logos/trivy_json.png b/components/shared_code/src/shared_data_model/logos/trivy_json.png new file mode 100644 index 0000000000000000000000000000000000000000..de307e505793517f25bae1622f48abdee1bd5c9d GIT binary patch literal 40154 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 literal 0 HcmV?d00001 diff --git a/components/shared_code/src/shared_data_model/metrics.py b/components/shared_code/src/shared_data_model/metrics.py index 652f93fe5b..a3ce9451cf 100644 --- a/components/shared_code/src/shared_data_model/metrics.py +++ b/components/shared_code/src/shared_data_model/metrics.py @@ -327,6 +327,50 @@ sources=["manual_number", "performancetest_runner"], tags=[Tag.PERFORMANCE], ), + "security_warnings": Metric( + name="Security warnings", + description="The number of security warnings about the software.", + rationale="Monitor security warnings about the software, its source code, dependencies, or " + "infrastructure so vulnerabilities can be fixed before they end up in production.", + unit=Unit.SECURITY_WARNINGS, + near_target="5", + sources=[ + "anchore", + "anchore_jenkins_plugin", + "bandit", + "cargo_audit", + "cxsast", + "generic_json", + "harbor", + "manual_number", + "openvas", + "owasp_dependency_check", + "owasp_zap", + "pyupio_safety", + "sarif_json", + "snyk", + "sonarqube", + "trivy_json", + ], + tags=[Tag.SECURITY], + ), + "sentiment": Metric( + name="Sentiment", + description="How are the team members feeling?", + rationale="Satisfaction is how fulfilled developers feel with their work, team, tools, or culture; " + "well-being is how healthy and happy they are, and how their work impacts it. Measuring satisfaction " + "and well-being can be beneficial for understanding productivity and perhaps even for predicting it. " + "For example, productivity and satisfaction are correlated, and it is possible that satisfaction could " + "serve as a leading indicator for productivity; a decline in satisfaction and engagement could signal " + "upcoming burnout and reduced productivity.", + rationale_urls=["https://queue.acm.org/detail.cfm?id=3454124"], + unit=Unit.NONE, + addition=Addition.MIN, + direction=Direction.MORE_IS_BETTER, + target="10", + near_target="8", + sources=["manual_number"], + ), "slow_transactions": Metric( name="Slow transactions", description="The number of transactions slower than their target response time.", @@ -337,6 +381,24 @@ sources=["gatling", "manual_number", "jmeter_csv", "jmeter_json", "performancetest_runner"], tags=[Tag.PERFORMANCE], ), + "software_version": Metric( + name="Software version", + description="The version number of the software as analyzed by the source.", + rationale="Monitor that the version of the software is at least a specific version or get notified when " + "the software version becomes higher than a specific version.", + explanation=VERSION_NUMBER_EXPLANATION, + explanation_urls=VERSION_NUMBER_EXPLANATION_URLS, + scales=["version_number"], + addition=Addition.MIN, + direction=Direction.MORE_IS_BETTER, + target="1.0", + near_target="0.9", + sources=[ + "performancetest_runner", + "sonarqube", + ], + tags=[Tag.CI], + ), "source_up_to_dateness": Metric( name="Source up-to-dateness", description="The number of days since the source was last updated.", @@ -378,24 +440,6 @@ ], tags=[Tag.CI], ), - "software_version": Metric( - name="Software version", - description="The version number of the software as analyzed by the source.", - rationale="Monitor that the version of the software is at least a specific version or get notified when " - "the software version becomes higher than a specific version.", - explanation=VERSION_NUMBER_EXPLANATION, - explanation_urls=VERSION_NUMBER_EXPLANATION_URLS, - scales=["version_number"], - addition=Addition.MIN, - direction=Direction.MORE_IS_BETTER, - target="1.0", - near_target="0.9", - sources=[ - "performancetest_runner", - "sonarqube", - ], - tags=[Tag.CI], - ), "source_version": Metric( name="Source version", description="The version number of the source.", @@ -438,49 +482,6 @@ ], tags=[Tag.CI], ), - "security_warnings": Metric( - name="Security warnings", - description="The number of security warnings about the software.", - rationale="Monitor security warnings about the software, its source code, dependencies, or " - "infrastructure so vulnerabilities can be fixed before they end up in production.", - unit=Unit.SECURITY_WARNINGS, - near_target="5", - sources=[ - "anchore", - "anchore_jenkins_plugin", - "bandit", - "cargo_audit", - "cxsast", - "generic_json", - "harbor", - "manual_number", - "openvas", - "owasp_dependency_check", - "owasp_zap", - "pyupio_safety", - "sarif_json", - "snyk", - "sonarqube", - ], - tags=[Tag.SECURITY], - ), - "sentiment": Metric( - name="Sentiment", - description="How are the team members feeling?", - rationale="Satisfaction is how fulfilled developers feel with their work, team, tools, or culture; " - "well-being is how healthy and happy they are, and how their work impacts it. Measuring satisfaction " - "and well-being can be beneficial for understanding productivity and perhaps even for predicting it. " - "For example, productivity and satisfaction are correlated, and it is possible that satisfaction could " - "serve as a leading indicator for productivity; a decline in satisfaction and engagement could signal " - "upcoming burnout and reduced productivity.", - rationale_urls=["https://queue.acm.org/detail.cfm?id=3454124"], - unit=Unit.NONE, - addition=Addition.MIN, - direction=Direction.MORE_IS_BETTER, - target="10", - near_target="8", - sources=["manual_number"], - ), "suppressed_violations": Metric( name="Suppressed violations", description="The number of violations suppressed in the source.", diff --git a/components/shared_code/src/shared_data_model/sources/__init__.py b/components/shared_code/src/shared_data_model/sources/__init__.py index bba6a0a870..4036a6ef67 100644 --- a/components/shared_code/src/shared_data_model/sources/__init__.py +++ b/components/shared_code/src/shared_data_model/sources/__init__.py @@ -36,6 +36,7 @@ from .sonarqube import SONARQUBE from .testng import TESTNG from .trello import TRELLO +from .trivy import TRIVY_JSON SOURCES = { "anchore": ANCHORE, @@ -81,5 +82,6 @@ "snyk": SNYK, "sonarqube": SONARQUBE, "testng": TESTNG, + "trivy_json": TRIVY_JSON, "trello": TRELLO, } diff --git a/components/shared_code/src/shared_data_model/sources/quality_time.py b/components/shared_code/src/shared_data_model/sources/quality_time.py index f88a225f98..14e6fb5554 100644 --- a/components/shared_code/src/shared_data_model/sources/quality_time.py +++ b/components/shared_code/src/shared_data_model/sources/quality_time.py @@ -183,6 +183,7 @@ "Jira", "JMeter CSV", "JMeter JSON", + "JSON file with security warnings", "JUnit XML report", "Manual number", "NCover", @@ -199,10 +200,10 @@ "Robot Framework Jenkins plugin", "SARIF", "Snyk", - "JSON file with security warnings", "SonarQube", "TestNG", "Trello", + "Trivy JSON", ], api_values={ "Anchore": "anchore", @@ -249,6 +250,7 @@ "SonarQube": "sonarqube", "TestNG": "testng", "Trello": "trello", + "Trivy JSON": "trivy_json", }, metrics=["metrics"], ), diff --git a/components/shared_code/src/shared_data_model/sources/sarif.py b/components/shared_code/src/shared_data_model/sources/sarif.py index 6e95a16ccb..2612758d16 100644 --- a/components/shared_code/src/shared_data_model/sources/sarif.py +++ b/components/shared_code/src/shared_data_model/sources/sarif.py @@ -1,4 +1,4 @@ -"""SARIF JSON for security warnings source.""" +"""SARIF JSON source.""" from pydantic import HttpUrl diff --git a/components/shared_code/src/shared_data_model/sources/trivy.py b/components/shared_code/src/shared_data_model/sources/trivy.py new file mode 100644 index 0000000000..9f19bcabcf --- /dev/null +++ b/components/shared_code/src/shared_data_model/sources/trivy.py @@ -0,0 +1,40 @@ +"""Trivy JSON source.""" + +from pydantic import HttpUrl + +from shared_data_model.meta.entity import Color, Entity, EntityAttribute +from shared_data_model.meta.source import Source +from shared_data_model.parameters import Severities, access_parameters + +TRIVY_JSON = Source( + name="Trivy JSON", + description="A Trivy vulnerability report in JSON format.", + url=HttpUrl("https://aquasecurity.github.io/trivy/v0.45/docs/configuration/reporting/#json"), + parameters={ + "levels": Severities( + name="Levels", + placeholder="all levels", + help="If provided, only count security warnings with the selected levels.", + values=["unknown", "low", "medium", "high", "critical"], + metrics=["security_warnings"], + ), + **access_parameters(["security_warnings"], source_type="Trivy vulnerability report", source_type_format="JSON"), + }, + entities={ + "security_warnings": Entity( + name="security warning", + attributes=[ + EntityAttribute(name="Vulnerability ID"), + EntityAttribute(name="Title", url="url"), + EntityAttribute(name="Description"), + EntityAttribute( + name="Level", + color={"critical": Color.NEGATIVE, "high": Color.WARNING, "unknown": Color.ACTIVE}, + ), + EntityAttribute(name="Package name"), + EntityAttribute(name="Installed version"), + EntityAttribute(name="Fixed version"), + ], + ), + }, +) diff --git a/components/testdata/reports/trivy/trivy.json b/components/testdata/reports/trivy/trivy.json new file mode 100644 index 0000000000..a9bfec0465 --- /dev/null +++ b/components/testdata/reports/trivy/trivy.json @@ -0,0 +1,101 @@ +[ + { + "Target": "php-app/composer.lock", + "Vulnerabilities": null + }, + { + "Target": "node-app/package-lock.json", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2018-16487", + "PkgName": "lodash", + "InstalledVersion": "4.17.4", + "FixedVersion": "\u003e=4.17.11", + "Title": "lodash: Prototype pollution in utilities function", + "Description": "A prototype pollution vulnerability was found in lodash \u003c4.17.11 where the functions merge, mergeWith, and defaultsDeep can be tricked into adding or modifying properties of Object.prototype.", + "Severity": "HIGH", + "References": [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16487" + ] + } + ] + }, + { + "Target": "trivy-ci-test (alpine 3.7.1)", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2018-16840", + "PkgName": "curl", + "InstalledVersion": "7.61.0-r0", + "FixedVersion": "7.61.1-r1", + "Title": "curl: Use-after-free when closing \"easy\" handle in Curl_close()", + "Description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through 7.61.1 in the code related to closing an easy handle. ", + "Severity": "HIGH", + "References": [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840" + ] + }, + { + "VulnerabilityID": "CVE-2019-3822", + "PkgName": "curl", + "InstalledVersion": "7.61.0-r0", + "FixedVersion": "7.61.1-r2", + "Title": "curl: NTLMv2 type-3 header stack buffer overflow", + "Description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to a stack-based buffer overflow. ", + "Severity": "HIGH", + "References": [ + "https://curl.haxx.se/docs/CVE-2019-3822.html", + "https://lists.apache.org/thread.html/8338a0f605bdbb3a6098bb76f666a95fc2b2f53f37fa1ecc89f1146f@%3Cdevnull.infra.apache.org%3E" + ] + }, + { + "VulnerabilityID": "CVE-2018-16839", + "PkgName": "curl", + "InstalledVersion": "7.61.0-r0", + "FixedVersion": "7.61.1-r1", + "Title": "curl: Integer overflow leading to heap-based buffer overflow in Curl_sasl_create_plain_message()", + "Description": "Curl versions 7.33.0 through 7.61.1 are vulnerable to a buffer overrun in the SASL authentication code that may lead to denial of service.", + "Severity": "HIGH", + "References": [ + "https://github.com/curl/curl/commit/f3a24d7916b9173c69a3e0ee790102993833d6c5" + ] + }, + { + "VulnerabilityID": "CVE-2018-19486", + "PkgName": "git", + "InstalledVersion": "2.15.2-r0", + "FixedVersion": "2.15.3-r0", + "Title": "git: Improper handling of PATH allows for commands to be executed from the current directory", + "Description": "Git before 2.19.2 on Linux and UNIX executes commands from the current working directory (as if '.' were at the end of $PATH) in certain cases involving the run_command() API and run-command.c, because there was a dangerous change from execvp to execv during 2017.", + "Severity": "HIGH", + "References": [ + "https://usn.ubuntu.com/3829-1/" + ] + }, + { + "VulnerabilityID": "CVE-2018-17456", + "PkgName": "git", + "InstalledVersion": "2.15.2-r0", + "FixedVersion": "2.15.3-r0", + "Title": "git: arbitrary code execution via .gitmodules", + "Description": "Git before 2.14.5, 2.15.x before 2.15.3, 2.16.x before 2.16.5, 2.17.x before 2.17.2, 2.18.x before 2.18.1, and 2.19.x before 2.19.1 allows remote code execution during processing of a recursive \"git clone\" of a superproject if a .gitmodules file has a URL field beginning with a '-' character.", + "Severity": "HIGH", + "References": [ + "http://www.securitytracker.com/id/1041811" + ] + } + ] + }, + { + "Target": "python-app/Pipfile.lock", + "Vulnerabilities": null + }, + { + "Target": "ruby-app/Gemfile.lock", + "Vulnerabilities": null + }, + { + "Target": "rust-app/Cargo.lock", + "Vulnerabilities": null + } +] diff --git a/docs/src/changelog.md b/docs/src/changelog.md index d540a0d012..838cbf6c6c 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - The "Reset all settings" button did not reset filtered tags. Fixes [#6947](https://github.com/ICTU/quality-time/issues/6947). +### Added + +- Support Trivy JSON files as source for the security warnings metric. Closes [#6927](https://github.com/ICTU/quality-time/issues/6927). + ## v5.1.0 - 2023-09-05 ### Deployment notes