test_osv_check.py
1 """Tests for OSV malware check on MCP extension packages.""" 2 3 import json 4 import pytest 5 from unittest.mock import patch, MagicMock 6 7 from tools.osv_check import ( 8 check_package_for_malware, 9 _infer_ecosystem, 10 _parse_package_from_args, 11 _parse_npm_package, 12 _parse_pypi_package, 13 _query_osv, 14 ) 15 16 17 class TestInferEcosystem: 18 def test_npx(self): 19 assert _infer_ecosystem("npx") == "npm" 20 assert _infer_ecosystem("/usr/bin/npx") == "npm" 21 22 def test_uvx(self): 23 assert _infer_ecosystem("uvx") == "PyPI" 24 assert _infer_ecosystem("/home/user/.local/bin/uvx") == "PyPI" 25 26 def test_pipx(self): 27 assert _infer_ecosystem("pipx") == "PyPI" 28 29 def test_unknown(self): 30 assert _infer_ecosystem("node") is None 31 assert _infer_ecosystem("python") is None 32 assert _infer_ecosystem("/bin/bash") is None 33 34 35 class TestParseNpmPackage: 36 def test_simple(self): 37 assert _parse_npm_package("react") == ("react", None) 38 39 def test_with_version(self): 40 assert _parse_npm_package("react@18.3.1") == ("react", "18.3.1") 41 42 def test_scoped(self): 43 assert _parse_npm_package("@modelcontextprotocol/server-filesystem") == ( 44 "@modelcontextprotocol/server-filesystem", None 45 ) 46 47 def test_scoped_with_version(self): 48 assert _parse_npm_package("@scope/pkg@1.2.3") == ("@scope/pkg", "1.2.3") 49 50 def test_latest_ignored(self): 51 assert _parse_npm_package("react@latest") == ("react", None) 52 53 54 class TestParsePypiPackage: 55 def test_simple(self): 56 assert _parse_pypi_package("requests") == ("requests", None) 57 58 def test_with_version(self): 59 assert _parse_pypi_package("requests==2.32.3") == ("requests", "2.32.3") 60 61 def test_with_extras(self): 62 assert _parse_pypi_package("mcp[cli]==1.2.3") == ("mcp", "1.2.3") 63 64 def test_extras_no_version(self): 65 assert _parse_pypi_package("mcp[cli]") == ("mcp", None) 66 67 68 class TestParsePackageFromArgs: 69 def test_npm_skips_flags(self): 70 name, ver = _parse_package_from_args(["-y", "@scope/pkg@1.0"], "npm") 71 assert name == "@scope/pkg" 72 assert ver == "1.0" 73 74 def test_pypi_skips_flags(self): 75 name, ver = _parse_package_from_args(["--from", "mcp[cli]"], "PyPI") 76 # --from is a flag, mcp[cli] is the package 77 # Actually --from is a flag so it gets skipped, mcp[cli] is found 78 assert name == "mcp" 79 80 def test_empty_args(self): 81 assert _parse_package_from_args([], "npm") == (None, None) 82 83 def test_only_flags(self): 84 assert _parse_package_from_args(["-y", "--yes"], "npm") == (None, None) 85 86 87 class TestCheckPackageForMalware: 88 def test_clean_package(self): 89 """Clean package returns None (allow).""" 90 mock_response = MagicMock() 91 mock_response.read.return_value = json.dumps({"vulns": []}).encode() 92 mock_response.__enter__ = lambda s: s 93 mock_response.__exit__ = MagicMock(return_value=False) 94 95 with patch("tools.osv_check.urllib.request.urlopen", return_value=mock_response): 96 result = check_package_for_malware("npx", ["-y", "@modelcontextprotocol/server-filesystem"]) 97 assert result is None 98 99 def test_malware_blocked(self): 100 """Known malware package returns error string.""" 101 mock_response = MagicMock() 102 mock_response.read.return_value = json.dumps({ 103 "vulns": [ 104 {"id": "MAL-2023-7938", "summary": "Malicious code in evil-pkg"}, 105 {"id": "CVE-2023-1234", "summary": "Regular vulnerability"}, # should be filtered 106 ] 107 }).encode() 108 mock_response.__enter__ = lambda s: s 109 mock_response.__exit__ = MagicMock(return_value=False) 110 111 with patch("tools.osv_check.urllib.request.urlopen", return_value=mock_response): 112 result = check_package_for_malware("npx", ["evil-pkg"]) 113 assert result is not None 114 assert "BLOCKED" in result 115 assert "MAL-2023-7938" in result 116 assert "CVE-2023-1234" not in result # regular CVEs filtered 117 118 def test_network_error_fails_open(self): 119 """Network errors allow the package (fail-open).""" 120 with patch("tools.osv_check.urllib.request.urlopen", side_effect=ConnectionError("timeout")): 121 result = check_package_for_malware("npx", ["some-package"]) 122 assert result is None 123 124 def test_non_npx_skipped(self): 125 """Non-npx/uvx commands are skipped entirely.""" 126 result = check_package_for_malware("node", ["server.js"]) 127 assert result is None 128 129 def test_uvx_pypi(self): 130 """uvx commands check PyPI ecosystem.""" 131 mock_response = MagicMock() 132 mock_response.read.return_value = json.dumps({"vulns": []}).encode() 133 mock_response.__enter__ = lambda s: s 134 mock_response.__exit__ = MagicMock(return_value=False) 135 136 with patch("tools.osv_check.urllib.request.urlopen", return_value=mock_response) as mock_url: 137 check_package_for_malware("uvx", ["mcp-server-fetch"]) 138 # Verify PyPI ecosystem was sent 139 call_data = json.loads(mock_url.call_args[0][0].data) 140 assert call_data["package"]["ecosystem"] == "PyPI" 141 assert call_data["package"]["name"] == "mcp-server-fetch" 142 143 144 class TestLiveOsvQuery: 145 """Live integration test against the real OSV API. Skipped if offline.""" 146 147 @pytest.mark.skipif( 148 not pytest.importorskip("urllib.request", reason="no network"), 149 reason="network required", 150 ) 151 def test_known_malware_package(self): 152 """node-hide-console-windows has a real MAL- advisory.""" 153 try: 154 result = _query_osv("node-hide-console-windows", "npm") 155 assert len(result) >= 1 156 assert result[0]["id"].startswith("MAL-") 157 except Exception: 158 pytest.skip("OSV API unreachable") 159 160 @pytest.mark.skipif( 161 not pytest.importorskip("urllib.request", reason="no network"), 162 reason="network required", 163 ) 164 def test_clean_package(self): 165 """react should have zero MAL- advisories.""" 166 try: 167 result = _query_osv("react", "npm") 168 assert len(result) == 0 169 except Exception: 170 pytest.skip("OSV API unreachable")