From b713904dea3e978da769cce91cfa3eeaf3cbbbe7 Mon Sep 17 00:00:00 2001 From: azizelbelaychy Date: Mon, 21 Sep 2026 14:42:39 +0000 Subject: [PATCH 1/3] fix: upgrade osv-scanner to v2 and improve validation checks in OSVAgent --- Dockerfile | 4 ++-- agent/osv_agent.py | 8 +++----- tests/osv_agent_test.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index c13fada..b230783 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM golang:1.22-alpine as go_stage -RUN go install github.com/google/osv-scanner/cmd/osv-scanner@v1 +FROM golang:alpine as go_stage +RUN go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2 FROM python:3.14-alpine as base FROM base as builder diff --git a/agent/osv_agent.py b/agent/osv_agent.py index 83a5fcb..188cd0d 100644 --- a/agent/osv_agent.py +++ b/agent/osv_agent.py @@ -750,14 +750,12 @@ def _process_fingerprint_file(self, message: m.Message) -> None: def _is_valid_osv_result(results: str | None) -> bool: """Check if the results are valid.""" - if results is None: - return False - - if results == "": + if results is None or results == "": return False try: - if json.loads(results) == {"results": []}: + parsed = json.loads(results) + if not parsed.get("results"): return False except json.JSONDecodeError: return False diff --git a/tests/osv_agent_test.py b/tests/osv_agent_test.py index 3dbdae2..719f01e 100644 --- a/tests/osv_agent_test.py +++ b/tests/osv_agent_test.py @@ -1585,3 +1585,33 @@ def testAgentOSV_whenContentUrlBasenameIsSupportedFileName_shouldScanOnlyThatFor "package-lock.json" in call.args[0] for call in subprocess_mock.call_args_list ) assert len(agent_mock) == 1 + + +def testIsValidOsvResult_whenResultsNoneOrEmpty_returnsFalse() -> None: + """Test _is_valid_osv_result returns False for None or empty string.""" + assert osv_agent._is_valid_osv_result(None) is False + assert osv_agent._is_valid_osv_result("") is False + + +def testIsValidOsvResult_whenInvalidJson_returnsFalse() -> None: + """Test _is_valid_osv_result returns False for malformed JSON.""" + assert osv_agent._is_valid_osv_result("not-a-json") is False + + +def testIsValidOsvResult_whenEmptyResultsArray_returnsFalse() -> None: + """Test _is_valid_osv_result returns False for empty results (v1 format).""" + assert osv_agent._is_valid_osv_result('{"results": []}') is False + + +def testIsValidOsvResult_whenEmptyResultsWithExperimentalConfig_returnsFalse() -> None: + """Test _is_valid_osv_result returns False for empty results with config (v2 format).""" + v2_empty_output = ( + '{"results": [], "experimental_config": {"licenses": {"summary": false}}}' + ) + assert osv_agent._is_valid_osv_result(v2_empty_output) is False + + +def testIsValidOsvResult_whenResultsPresent_returnsTrue() -> None: + """Test _is_valid_osv_result returns True when results contain entries.""" + valid_output = '{"results": [{"packages": [{"package": {"name": "foo"}}]}]}' + assert osv_agent._is_valid_osv_result(valid_output) is True From f4dc9bfb34cdc87ca869dc66a9271fb63c501490 Mon Sep 17 00:00:00 2001 From: azizelbelaychy Date: Mon, 21 Sep 2026 15:28:52 +0000 Subject: [PATCH 2/3] fix: update osv-scanner version and enhance validation in OSVAgent tests --- Dockerfile | 4 +- agent/osv_agent.py | 5 +- tests/osv_agent_test.py | 135 ++++++++++++++++++++++++++++++++++------ 3 files changed, 122 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index b230783..f9114c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM golang:alpine as go_stage -RUN go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2 +FROM golang:1.27-alpine as go_stage +RUN go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2.6.0 FROM python:3.14-alpine as base FROM base as builder diff --git a/agent/osv_agent.py b/agent/osv_agent.py index 188cd0d..c1cd234 100644 --- a/agent/osv_agent.py +++ b/agent/osv_agent.py @@ -755,7 +755,10 @@ def _is_valid_osv_result(results: str | None) -> bool: try: parsed = json.loads(results) - if not parsed.get("results"): + if not isinstance(parsed, dict): + return False + results_list = parsed.get("results") + if not isinstance(results_list, list) or len(results_list) == 0: return False except json.JSONDecodeError: return False diff --git a/tests/osv_agent_test.py b/tests/osv_agent_test.py index 719f01e..5d2870e 100644 --- a/tests/osv_agent_test.py +++ b/tests/osv_agent_test.py @@ -1587,31 +1587,128 @@ def testAgentOSV_whenContentUrlBasenameIsSupportedFileName_shouldScanOnlyThatFor assert len(agent_mock) == 1 -def testIsValidOsvResult_whenResultsNoneOrEmpty_returnsFalse() -> None: - """Test _is_valid_osv_result returns False for None or empty string.""" - assert osv_agent._is_valid_osv_result(None) is False - assert osv_agent._is_valid_osv_result("") is False +def testAgentOSV_whenV2OutputHasEmptyResultsWithExperimentalConfig_shouldNotEmitVulnerability( + test_agent: osv_agent.OSVAgent, + agent_mock: list[message.Message], + agent_persist_mock: dict[str | bytes, str | bytes], + scan_message_file: message.Message, + mocker: plugin.MockerFixture, +) -> None: + """When OSV v2 returns empty results with experimental_config, no vulnerabilities should be emitted.""" + v2_empty_output = ( + '{"results": [], "experimental_config": {"licenses": {"summary": false}}}' + ) + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess([], 0, v2_empty_output, None), + ) + test_agent.process(scan_message_file) -def testIsValidOsvResult_whenInvalidJson_returnsFalse() -> None: - """Test _is_valid_osv_result returns False for malformed JSON.""" - assert osv_agent._is_valid_osv_result("not-a-json") is False + assert len(agent_mock) == 0 -def testIsValidOsvResult_whenEmptyResultsArray_returnsFalse() -> None: - """Test _is_valid_osv_result returns False for empty results (v1 format).""" - assert osv_agent._is_valid_osv_result('{"results": []}') is False +def testAgentOSV_whenV1OutputHasEmptyResults_shouldNotEmitVulnerability( + test_agent: osv_agent.OSVAgent, + agent_mock: list[message.Message], + agent_persist_mock: dict[str | bytes, str | bytes], + scan_message_file: message.Message, + mocker: plugin.MockerFixture, +) -> None: + """When OSV v1 returns empty results, no vulnerabilities should be emitted.""" + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess([], 0, '{"results": []}', None), + ) + test_agent.process(scan_message_file) -def testIsValidOsvResult_whenEmptyResultsWithExperimentalConfig_returnsFalse() -> None: - """Test _is_valid_osv_result returns False for empty results with config (v2 format).""" - v2_empty_output = ( - '{"results": [], "experimental_config": {"licenses": {"summary": false}}}' + assert len(agent_mock) == 0 + + +def testAgentOSV_whenOutputIsMalformedJson_shouldNotEmitVulnerability( + test_agent: osv_agent.OSVAgent, + agent_mock: list[message.Message], + agent_persist_mock: dict[str | bytes, str | bytes], + scan_message_file: message.Message, + mocker: plugin.MockerFixture, +) -> None: + """When OSV scanner returns malformed JSON, no vulnerabilities should be emitted.""" + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess([], 0, "not-a-json", None), ) - assert osv_agent._is_valid_osv_result(v2_empty_output) is False + test_agent.process(scan_message_file) + + assert len(agent_mock) == 0 -def testIsValidOsvResult_whenResultsPresent_returnsTrue() -> None: - """Test _is_valid_osv_result returns True when results contain entries.""" - valid_output = '{"results": [{"packages": [{"package": {"name": "foo"}}]}]}' - assert osv_agent._is_valid_osv_result(valid_output) is True + +def testAgentOSV_whenV2OutputHasVulnerabilities_shouldEmitVulnerability( + test_agent: osv_agent.OSVAgent, + agent_mock: list[message.Message], + agent_persist_mock: dict[str | bytes, str | bytes], + scan_message_file: message.Message, + mocker: plugin.MockerFixture, + fake_osv_output: str, + osv_output_as_dict: dict[str, Any], +) -> None: + """When OSV v2 returns populated results alongside experimental_config, vulnerabilities should be emitted.""" + import json + + parsed = json.loads(fake_osv_output) + parsed["experimental_config"] = {"licenses": {"summary": False}} + v2_vuln_output = json.dumps(parsed) + + cve_data = cve_service_api.CVE( + risk="HIGH", + description="description", + fixed_version="2", + cvss_v3_vector=None, + ) + mocker.patch("agent.cve_service_api.get_cve_data_from_api", return_value=cve_data) + mocker.patch("agent.osv_output_handler.calculate_risk_rating", return_value="HIGH") + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess([], 0, v2_vuln_output, None), + ) + + test_agent.process(scan_message_file) + + assert len(agent_mock) > 0 + + +def testAgentOSV_whenOutputIsNullJson_shouldNotEmitVulnerability( + test_agent: osv_agent.OSVAgent, + agent_mock: list[message.Message], + agent_persist_mock: dict[str | bytes, str | bytes], + scan_message_file: message.Message, + mocker: plugin.MockerFixture, +) -> None: + """When OSV scanner returns 'null' JSON, no vulnerabilities should be emitted.""" + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess([], 0, "null", None), + ) + + test_agent.process(scan_message_file) + + assert len(agent_mock) == 0 + + +def testAgentOSV_whenOutputIsJsonArray_shouldNotEmitVulnerability( + test_agent: osv_agent.OSVAgent, + agent_mock: list[message.Message], + agent_persist_mock: dict[str | bytes, str | bytes], + scan_message_file: message.Message, + mocker: plugin.MockerFixture, +) -> None: + """When OSV scanner returns a JSON array '[]', no vulnerabilities should be emitted.""" + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess([], 0, "[]", None), + ) + + test_agent.process(scan_message_file) + + assert len(agent_mock) == 0 From c00267de8435963f4b43149a025ce32756cccf0e Mon Sep 17 00:00:00 2001 From: azizelbelaychy Date: Mon, 21 Sep 2026 15:38:30 +0000 Subject: [PATCH 3/3] fix: enhance JSON validation in OSVAgent and add missing import in tests --- agent/osv_agent.py | 4 ++-- tests/osv_agent_test.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/agent/osv_agent.py b/agent/osv_agent.py index c1cd234..94a5132 100644 --- a/agent/osv_agent.py +++ b/agent/osv_agent.py @@ -755,10 +755,10 @@ def _is_valid_osv_result(results: str | None) -> bool: try: parsed = json.loads(results) - if not isinstance(parsed, dict): + if isinstance(parsed, dict) is False: return False results_list = parsed.get("results") - if not isinstance(results_list, list) or len(results_list) == 0: + if isinstance(results_list, list) is False or len(results_list) == 0: return False except json.JSONDecodeError: return False diff --git a/tests/osv_agent_test.py b/tests/osv_agent_test.py index 5d2870e..63f18af 100644 --- a/tests/osv_agent_test.py +++ b/tests/osv_agent_test.py @@ -1,5 +1,6 @@ """Unittests for OSV agent.""" +import json import subprocess from collections.abc import Callable from typing import Any @@ -1654,8 +1655,6 @@ def testAgentOSV_whenV2OutputHasVulnerabilities_shouldEmitVulnerability( osv_output_as_dict: dict[str, Any], ) -> None: """When OSV v2 returns populated results alongside experimental_config, vulnerabilities should be emitted.""" - import json - parsed = json.loads(fake_osv_output) parsed["experimental_config"] = {"licenses": {"summary": False}} v2_vuln_output = json.dumps(parsed)