1+ from __future__ import annotations
2+
3+ import os
4+ import json
5+ from unittest .mock import MagicMock , patch , mock_open
6+
7+ import pytest
8+ from vunnel import result , workspace
9+ from vunnel .providers .rootio import Config , Provider , parser
10+
11+
12+ class TestRootIoProvider :
13+ @pytest .fixture ()
14+ def mock_vulnerability_data (self ):
15+ """Returns sample vulnerability data that would be fetched from Root.io API"""
16+ return {
17+ "CVE-2023-1234" : {
18+ "cve_id" : "CVE-2023-1234" ,
19+ "packages" : [
20+ {
21+ "package" : "curl" ,
22+ "distro" : "alpine" ,
23+ "distro_version" : "3.17" ,
24+ "fixed_version" : "7.88.1-r1" ,
25+ "has_rootio_fix" : True
26+ },
27+ {
28+ "package" : "openssl" ,
29+ "distro" : "debian" ,
30+ "distro_version" : "11" ,
31+ "fixed_version" : None ,
32+ "has_rootio_fix" : True
33+ }
34+ ],
35+ "severity" : "HIGH" ,
36+ "description" : "Test vulnerability description" ,
37+ "references" : ["https://nvd.nist.gov/vuln/detail/CVE-2023-1234" ]
38+ },
39+ "CVE-2023-5678" : {
40+ "cve_id" : "CVE-2023-5678" ,
41+ "packages" : [
42+ {
43+ "package" : "requests" ,
44+ "language" : "python" ,
45+ "fixed_version" : "2.31.0" ,
46+ "has_rootio_fix" : True
47+ }
48+ ],
49+ "severity" : "MEDIUM" ,
50+ "description" : "Another test vulnerability" ,
51+ "references" : ["https://nvd.nist.gov/vuln/detail/CVE-2023-5678" ]
52+ }
53+ }
54+
55+ @pytest .fixture ()
56+ def workspace_dir (self , tmp_path ):
57+ ws = workspace .Workspace (root = str (tmp_path / "rootio" ), name = "rootio" )
58+ ws .create ()
59+ return ws
60+
61+ def test_parser_emit_unaffected_for_os_packages (self , workspace_dir , mock_vulnerability_data ):
62+ """Test that parser emits ROOTIO_UNAFFECTED markers for OS packages"""
63+ p = parser .Parser (workspace = workspace_dir )
64+
65+ # Mock the API response
66+ with patch .object (parser .Parser , "_fetch_vulnerabilities" , return_value = mock_vulnerability_data ):
67+ p ._process_vulnerabilities (mock_vulnerability_data )
68+
69+ # Check that results were written
70+ results_path = workspace_dir .results_path
71+ assert os .path .exists (results_path )
72+
73+ # Verify the output contains ROOTIO_UNAFFECTED markers
74+ vuln_files = list (results_path .glob ("*.json" ))
75+ assert len (vuln_files ) > 0
76+
77+ # Check for CVE-2023-1234 (OS package vulnerability)
78+ found_unaffected = False
79+ for vuln_file in vuln_files :
80+ with open (vuln_file ) as f :
81+ data = json .load (f )
82+ if data .get ("Vulnerability" , {}).get ("Name" ) == "CVE-2023-1234" :
83+ for fixed_in in data ["Vulnerability" ].get ("FixedIn" , []):
84+ if fixed_in .get ("Version" ) == "ROOTIO_UNAFFECTED" :
85+ found_unaffected = True
86+ assert fixed_in .get ("VulnerableRange" ) == "NOT version_contains .root.io"
87+
88+ assert found_unaffected , "Should have ROOTIO_UNAFFECTED marker for OS packages"
89+
90+ def test_parser_emit_unaffected_for_language_packages (self , workspace_dir , mock_vulnerability_data ):
91+ """Test that parser emits ROOTIO_UNAFFECTED markers for language packages"""
92+ p = parser .Parser (workspace = workspace_dir )
93+
94+ # Mock the API response
95+ with patch .object (parser .Parser , "_fetch_vulnerabilities" , return_value = mock_vulnerability_data ):
96+ p ._process_vulnerabilities (mock_vulnerability_data )
97+
98+ # Check for CVE-2023-5678 (Python package vulnerability)
99+ results_path = workspace_dir .results_path
100+ vuln_files = list (results_path .glob ("*.json" ))
101+
102+ found_python_unaffected = False
103+ for vuln_file in vuln_files :
104+ with open (vuln_file ) as f :
105+ data = json .load (f )
106+ if data .get ("Vulnerability" , {}).get ("Name" ) == "CVE-2023-5678" :
107+ namespace = data ["Vulnerability" ].get ("NamespaceName" , "" )
108+ if namespace == "rootio:language:python" :
109+ for fixed_in in data ["Vulnerability" ].get ("FixedIn" , []):
110+ if fixed_in .get ("Version" ) == "ROOTIO_UNAFFECTED" :
111+ found_python_unaffected = True
112+
113+ assert found_python_unaffected , "Should have ROOTIO_UNAFFECTED marker for language packages"
114+
115+ def test_parser_namespace_format (self , workspace_dir ):
116+ """Test that parser generates correct namespace formats"""
117+ p = parser .Parser (workspace = workspace_dir )
118+
119+ # Test OS namespace
120+ os_namespace = p ._get_namespace ("alpine" , "3.17" , None )
121+ assert os_namespace == "rootio:distro:alpine:3.17"
122+
123+ # Test language namespace
124+ lang_namespace = p ._get_namespace (None , None , "python" )
125+ assert lang_namespace == "rootio:language:python"
126+
127+ def test_parser_version_format_mapping (self , workspace_dir ):
128+ """Test that parser maps to correct version formats"""
129+ p = parser .Parser (workspace = workspace_dir )
130+
131+ # Test OS mappings
132+ assert p ._get_version_format ("alpine" ) == "apk"
133+ assert p ._get_version_format ("debian" ) == "dpkg"
134+ assert p ._get_version_format ("ubuntu" ) == "dpkg"
135+ assert p ._get_version_format ("centos" ) == "rpm"
136+
137+ # Test language mappings
138+ assert p ._get_version_format (None , "python" ) == "python"
139+ assert p ._get_version_format (None , "javascript" ) == "semver"
140+ assert p ._get_version_format (None , "java" ) == "maven"
141+
142+ def test_provider_name (self ):
143+ """Test that provider returns correct name"""
144+ p = Provider (root = "/tmp" , config = Config ())
145+ assert p .name == "rootio"
146+
147+ def test_provider_update (self , workspace_dir , mock_vulnerability_data ):
148+ """Test the provider update process"""
149+ config = Config (runtime = Config .RuntimeConfig (existing_results = "keep" ))
150+ p = Provider (root = str (workspace_dir .path ), config = config )
151+
152+ # Mock the API fetch
153+ with patch .object (parser .Parser , "_fetch_vulnerabilities" , return_value = mock_vulnerability_data ):
154+ # Run update
155+ update_count , urls = p .update ()
156+
157+ # Verify results
158+ assert isinstance (update_count , int )
159+ assert update_count > 0
160+
161+ # Check that metadata was written
162+ metadata_path = workspace_dir .metadata_path
163+ assert metadata_path .exists ()
164+
165+ with open (metadata_path ) as f :
166+ metadata = json .load (f )
167+ assert metadata ["provider" ] == "rootio"
168+ assert metadata ["listing" ]["digest" ]
169+ assert metadata ["listing" ]["algorithm" ]
170+
171+ def test_parser_handles_empty_response (self , workspace_dir ):
172+ """Test that parser handles empty API responses gracefully"""
173+ p = parser .Parser (workspace = workspace_dir )
174+
175+ # Mock empty API response
176+ with patch .object (parser .Parser , "_fetch_vulnerabilities" , return_value = {}):
177+ p ._process_vulnerabilities ({})
178+
179+ # Should complete without errors
180+ results_path = workspace_dir .results_path
181+ assert results_path .exists ()
182+
183+ def test_parser_vulnerable_range_constraint (self , workspace_dir , mock_vulnerability_data ):
184+ """Test that vulnerable range constraints are properly set"""
185+ p = parser .Parser (workspace = workspace_dir )
186+
187+ with patch .object (parser .Parser , "_fetch_vulnerabilities" , return_value = mock_vulnerability_data ):
188+ p ._process_vulnerabilities (mock_vulnerability_data )
189+
190+ # Read results and check constraints
191+ results_path = workspace_dir .results_path
192+ vuln_files = list (results_path .glob ("*.json" ))
193+
194+ for vuln_file in vuln_files :
195+ with open (vuln_file ) as f :
196+ data = json .load (f )
197+ for fixed_in in data .get ("Vulnerability" , {}).get ("FixedIn" , []):
198+ if fixed_in .get ("Version" ) == "ROOTIO_UNAFFECTED" :
199+ # Should have the Root.io constraint
200+ assert fixed_in .get ("VulnerableRange" ) == "NOT version_contains .root.io"
201+
202+ def test_config_defaults (self ):
203+ """Test that Config has proper defaults"""
204+ config = Config ()
205+ assert config .api_url == "https://api.root.io/v1/vulnerabilities"
206+ assert config .runtime .existing_results == "delete"
207+
208+
209+ class TestRootIoParser :
210+ """Additional parser-specific tests"""
211+
212+ def test_normalize_severity (self ):
213+ """Test severity normalization"""
214+ p = parser .Parser (workspace = MagicMock ())
215+
216+ assert p ._normalize_severity ("CRITICAL" ) == "Critical"
217+ assert p ._normalize_severity ("HIGH" ) == "High"
218+ assert p ._normalize_severity ("MEDIUM" ) == "Medium"
219+ assert p ._normalize_severity ("LOW" ) == "Low"
220+ assert p ._normalize_severity ("unknown" ) == "Unknown"
221+ assert p ._normalize_severity (None ) == "Unknown"
0 commit comments