Coverage for tests / unit / parsers / test_osv_scanner_parser.py: 100%

85 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Unit tests for OSV-Scanner parser.""" 

2 

3from __future__ import annotations 

4 

5import json 

6 

7import pytest 

8from assertpy import assert_that 

9 

10from lintro.parsers.osv_scanner.osv_scanner_parser import parse_osv_scanner_output 

11 

12 

13@pytest.mark.parametrize( 

14 ("output", "expected_count"), 

15 [ 

16 pytest.param(None, 0, id="none_input"), 

17 pytest.param("", 0, id="empty_string"), 

18 pytest.param(" \n\n ", 0, id="whitespace_only"), 

19 ], 

20) 

21def test_parse_empty_cases(output: str | None, expected_count: int) -> None: 

22 """Parser returns empty list for empty/None input.""" 

23 result = parse_osv_scanner_output(output) 

24 assert_that(result).is_length(expected_count) 

25 

26 

27def test_parse_empty_results() -> None: 

28 """Empty results list returns no issues.""" 

29 issues = parse_osv_scanner_output(json.dumps({"results": []})) 

30 assert_that(issues).is_equal_to([]) 

31 

32 

33def test_parse_single_vulnerability() -> None: 

34 """Parser extracts single vulnerability correctly.""" 

35 output = json.dumps( 

36 { 

37 "results": [ 

38 { 

39 "source": {"path": "requirements.txt"}, 

40 "packages": [ 

41 { 

42 "package": { 

43 "name": "flask", 

44 "version": "2.0.0", 

45 "ecosystem": "PyPI", 

46 }, 

47 "groups": [ 

48 { 

49 "ids": ["GHSA-abcd-1234-efgh"], 

50 "max_severity": "HIGH", 

51 }, 

52 ], 

53 "vulnerabilities": [ 

54 { 

55 "id": "GHSA-abcd-1234-efgh", 

56 "summary": "XSS in Flask", 

57 }, 

58 ], 

59 }, 

60 ], 

61 }, 

62 ], 

63 }, 

64 ) 

65 issues = parse_osv_scanner_output(output) 

66 

67 assert_that(issues).is_length(1) 

68 assert_that(issues[0].file).is_equal_to("requirements.txt") 

69 assert_that(issues[0].vuln_id).is_equal_to("GHSA-abcd-1234-efgh") 

70 assert_that(issues[0].severity).is_equal_to("HIGH") 

71 assert_that(issues[0].package_name).is_equal_to("flask") 

72 assert_that(issues[0].package_version).is_equal_to("2.0.0") 

73 assert_that(issues[0].package_ecosystem).is_equal_to("PyPI") 

74 

75 

76def test_parse_multiple_vulnerabilities() -> None: 

77 """Parser handles multiple vulnerabilities across packages.""" 

78 output = json.dumps( 

79 { 

80 "results": [ 

81 { 

82 "source": {"path": "requirements.txt"}, 

83 "packages": [ 

84 { 

85 "package": { 

86 "name": "flask", 

87 "version": "2.0.0", 

88 "ecosystem": "PyPI", 

89 }, 

90 "groups": [ 

91 { 

92 "ids": ["GHSA-1111-aaaa-bbbb"], 

93 "max_severity": "HIGH", 

94 }, 

95 ], 

96 "vulnerabilities": [ 

97 { 

98 "id": "GHSA-1111-aaaa-bbbb", 

99 "summary": "Issue 1", 

100 }, 

101 ], 

102 }, 

103 { 

104 "package": { 

105 "name": "django", 

106 "version": "3.0.0", 

107 "ecosystem": "PyPI", 

108 }, 

109 "groups": [ 

110 { 

111 "ids": ["GHSA-2222-cccc-dddd"], 

112 "max_severity": "CRITICAL", 

113 }, 

114 ], 

115 "vulnerabilities": [ 

116 { 

117 "id": "GHSA-2222-cccc-dddd", 

118 "summary": "Issue 2", 

119 }, 

120 ], 

121 }, 

122 ], 

123 }, 

124 ], 

125 }, 

126 ) 

127 issues = parse_osv_scanner_output(output) 

128 

129 assert_that(issues).is_length(2) 

130 assert_that(issues[0].package_name).is_equal_to("flask") 

131 assert_that(issues[0].severity).is_equal_to("HIGH") 

132 assert_that(issues[1].package_name).is_equal_to("django") 

133 assert_that(issues[1].severity).is_equal_to("CRITICAL") 

134 

135 

136def test_parse_vulnerability_with_multiple_ids() -> None: 

137 """Parser uses first ID as primary when group has multiple IDs.""" 

138 output = json.dumps( 

139 { 

140 "results": [ 

141 { 

142 "source": {"path": "package-lock.json"}, 

143 "packages": [ 

144 { 

145 "package": { 

146 "name": "lodash", 

147 "version": "4.17.15", 

148 "ecosystem": "npm", 

149 }, 

150 "groups": [ 

151 { 

152 "ids": [ 

153 "GHSA-xxxx-yyyy-zzzz", 

154 "CVE-2021-23337", 

155 ], 

156 "max_severity": "CRITICAL", 

157 }, 

158 ], 

159 "vulnerabilities": [ 

160 { 

161 "id": "GHSA-xxxx-yyyy-zzzz", 

162 "summary": "Prototype pollution", 

163 }, 

164 ], 

165 }, 

166 ], 

167 }, 

168 ], 

169 }, 

170 ) 

171 issues = parse_osv_scanner_output(output) 

172 

173 assert_that(issues).is_length(1) 

174 assert_that(issues[0].vuln_id).is_equal_to("GHSA-xxxx-yyyy-zzzz") 

175 

176 

177def test_parse_vulnerability_with_fixed_version() -> None: 

178 """Parser extracts fixed version from affected data.""" 

179 output = json.dumps( 

180 { 

181 "results": [ 

182 { 

183 "source": {"path": "requirements.txt"}, 

184 "packages": [ 

185 { 

186 "package": { 

187 "name": "requests", 

188 "version": "2.25.0", 

189 "ecosystem": "PyPI", 

190 }, 

191 "groups": [ 

192 { 

193 "ids": ["GHSA-test-1234-abcd"], 

194 "max_severity": "MEDIUM", 

195 }, 

196 ], 

197 "vulnerabilities": [ 

198 { 

199 "id": "GHSA-test-1234-abcd", 

200 "summary": "Test vuln", 

201 "affected": [ 

202 { 

203 "package": { 

204 "name": "requests", 

205 "ecosystem": "PyPI", 

206 }, 

207 "ranges": [ 

208 { 

209 "type": "ECOSYSTEM", 

210 "events": [ 

211 {"introduced": "0"}, 

212 {"fixed": "2.32.0"}, 

213 ], 

214 }, 

215 ], 

216 }, 

217 ], 

218 }, 

219 ], 

220 }, 

221 ], 

222 }, 

223 ], 

224 }, 

225 ) 

226 issues = parse_osv_scanner_output(output) 

227 

228 assert_that(issues).is_length(1) 

229 assert_that(issues[0].fixed_version).is_equal_to("2.32.0") 

230 assert_that(issues[0].message).contains("fix: 2.32.0") 

231 

232 

233def test_parse_invalid_json() -> None: 

234 """Invalid JSON returns empty list without crashing.""" 

235 issues = parse_osv_scanner_output("not valid json") 

236 assert_that(issues).is_equal_to([]) 

237 

238 

239def test_parse_non_object_json() -> None: 

240 """Non-object JSON returns empty list.""" 

241 issues = parse_osv_scanner_output(json.dumps([1, 2, 3])) 

242 assert_that(issues).is_equal_to([]) 

243 

244 

245def test_parse_non_list_results() -> None: 

246 """Non-list results returns empty list.""" 

247 issues = parse_osv_scanner_output(json.dumps({"results": "not a list"})) 

248 assert_that(issues).is_equal_to([]) 

249 

250 

251def test_parse_missing_results_key() -> None: 

252 """Missing results key returns empty list.""" 

253 issues = parse_osv_scanner_output(json.dumps({})) 

254 assert_that(issues).is_equal_to([]) 

255 

256 

257def test_parse_malformed_package_entry() -> None: 

258 """Malformed package entries are skipped gracefully.""" 

259 output = json.dumps( 

260 { 

261 "results": [ 

262 { 

263 "source": {"path": "requirements.txt"}, 

264 "packages": [ 

265 None, 

266 42, 

267 {"package": "not a dict"}, 

268 {"package": {"name": ""}}, 

269 ], 

270 }, 

271 ], 

272 }, 

273 ) 

274 issues = parse_osv_scanner_output(output) 

275 assert_that(issues).is_equal_to([]) 

276 

277 

278def test_issue_display_row() -> None: 

279 """OsvScannerIssue.to_display_row returns correct values.""" 

280 from lintro.parsers.osv_scanner.osv_scanner_issue import OsvScannerIssue 

281 

282 issue = OsvScannerIssue( 

283 file="requirements.txt", 

284 line=0, 

285 column=0, 

286 vuln_id="GHSA-test-1234", 

287 severity="HIGH", 

288 package_name="requests", 

289 package_version="2.25.0", 

290 package_ecosystem="PyPI", 

291 ) 

292 row = issue.to_display_row() 

293 assert_that(row["file"]).is_equal_to("requirements.txt") 

294 assert_that(row["code"]).is_equal_to("GHSA-test-1234") 

295 # "HIGH" is normalized to "ERROR" by SeverityLevel 

296 assert_that(row["severity"]).is_equal_to("ERROR") 

297 assert_that(row["message"]).contains("GHSA-test-1234") 

298 assert_that(row["message"]).contains("requests@2.25.0") 

299 

300 

301def test_issue_message_format() -> None: 

302 """Issue message includes vuln ID, package info, and fix version.""" 

303 from lintro.parsers.osv_scanner.osv_scanner_issue import OsvScannerIssue 

304 

305 issue = OsvScannerIssue( 

306 vuln_id="GHSA-abcd-1234", 

307 package_name="flask", 

308 package_version="2.0.0", 

309 fixed_version="2.3.0", 

310 ) 

311 assert_that(issue.message).is_equal_to( 

312 "[GHSA-abcd-1234] flask@2.0.0 (fix: 2.3.0)", 

313 ) 

314 

315 

316def test_issue_message_format_no_fix() -> None: 

317 """Issue message omits fix when no fixed version available.""" 

318 from lintro.parsers.osv_scanner.osv_scanner_issue import OsvScannerIssue 

319 

320 issue = OsvScannerIssue( 

321 vuln_id="GHSA-abcd-1234", 

322 package_name="flask", 

323 package_version="2.0.0", 

324 ) 

325 assert_that(issue.message).is_equal_to("[GHSA-abcd-1234] flask@2.0.0") 

326 

327 

328def test_parse_fallthrough_vuln_id_lookup() -> None: 

329 """Parser finds fixed version even when primary ID is not in vulnerabilities. 

330 

331 When the first group ID (e.g. a CVE alias) doesn't have a matching entry 

332 in the vulnerabilities array, the parser should fall through to subsequent 

333 IDs to find the vulnerability details and fixed version. 

334 """ 

335 output = json.dumps( 

336 { 

337 "results": [ 

338 { 

339 "source": {"path": "requirements.txt"}, 

340 "packages": [ 

341 { 

342 "package": { 

343 "name": "requests", 

344 "version": "2.25.0", 

345 "ecosystem": "PyPI", 

346 }, 

347 "groups": [ 

348 { 

349 "ids": [ 

350 "CVE-2024-99999", 

351 "GHSA-abcd-1234-efgh", 

352 ], 

353 "max_severity": "HIGH", 

354 }, 

355 ], 

356 "vulnerabilities": [ 

357 { 

358 "id": "GHSA-abcd-1234-efgh", 

359 "summary": "Session bypass", 

360 "affected": [ 

361 { 

362 "package": { 

363 "name": "requests", 

364 "ecosystem": "PyPI", 

365 }, 

366 "ranges": [ 

367 { 

368 "type": "ECOSYSTEM", 

369 "events": [ 

370 {"introduced": "0"}, 

371 {"fixed": "2.32.0"}, 

372 ], 

373 }, 

374 ], 

375 }, 

376 ], 

377 }, 

378 ], 

379 }, 

380 ], 

381 }, 

382 ], 

383 }, 

384 ) 

385 issues = parse_osv_scanner_output(output) 

386 

387 assert_that(issues).is_length(1) 

388 assert_that(issues[0].vuln_id).is_equal_to("CVE-2024-99999") 

389 assert_that(issues[0].fixed_version).is_equal_to("2.32.0") 

390 

391 

392def test_default_severity() -> None: 

393 """Default severity is MEDIUM when not specified in groups.""" 

394 output = json.dumps( 

395 { 

396 "results": [ 

397 { 

398 "source": {"path": "requirements.txt"}, 

399 "packages": [ 

400 { 

401 "package": { 

402 "name": "foo", 

403 "version": "1.0.0", 

404 "ecosystem": "PyPI", 

405 }, 

406 "groups": [ 

407 { 

408 "ids": ["GHSA-no-severity"], 

409 }, 

410 ], 

411 "vulnerabilities": [ 

412 { 

413 "id": "GHSA-no-severity", 

414 "summary": "No severity", 

415 }, 

416 ], 

417 }, 

418 ], 

419 }, 

420 ], 

421 }, 

422 ) 

423 issues = parse_osv_scanner_output(output) 

424 

425 assert_that(issues).is_length(1) 

426 assert_that(issues[0].severity).is_equal_to("MEDIUM")