|
12 | 12 | from typing import Iterable |
13 | 13 | from typing import List |
14 | 14 | from typing import Optional |
| 15 | +from typing import Tuple |
15 | 16 |
|
16 | 17 | import dateparser |
17 | 18 | from cvss.exceptions import CVSS3MalformedError |
@@ -84,8 +85,8 @@ def parse_advisory_data( |
84 | 85 | ) |
85 | 86 |
|
86 | 87 | for fixed_range in affected_pkg.get("ranges") or []: |
87 | | - fixed_version = get_fixed_versions( |
88 | | - fixed_range=fixed_range, raw_id=raw_id, supported_ecosystem=purl.type |
| 88 | + fixed_version, _ = get_fixed_versions_and_commits( |
| 89 | + ranges=fixed_range, raw_id=raw_id, supported_ecosystem=purl.type |
89 | 90 | ) |
90 | 91 |
|
91 | 92 | for version in fixed_version: |
@@ -150,12 +151,11 @@ def parse_advisory_data_v2( |
150 | 151 | fixed_versions = [] |
151 | 152 | fixed_version_range = None |
152 | 153 | for fixed_range in affected_pkg.get("ranges") or []: |
153 | | - fixed_version = get_fixed_versions( |
154 | | - fixed_range=fixed_range, raw_id=advisory_id, supported_ecosystem=purl.type |
| 154 | + fixed_version, (introduced_commits, fixed_commits) = get_fixed_versions_and_commits( |
| 155 | + ranges=fixed_range, raw_id=advisory_id, supported_ecosystem=purl.type |
155 | 156 | ) |
156 | 157 | fixed_versions.extend([v.string for v in fixed_version]) |
157 | 158 |
|
158 | | - introduced_commits, fixed_commits = get_code_commit(fixed_range, raw_id=advisory_id) |
159 | 159 | fixed_by_commits.extend(fixed_commits) |
160 | 160 | affected_by_commits.extend(introduced_commits) |
161 | 161 |
|
@@ -195,32 +195,23 @@ def parse_advisory_data_v2( |
195 | 195 | ) |
196 | 196 |
|
197 | 197 |
|
198 | | -def extract_fixed_versions(fixed_range) -> Iterable[str]: |
| 198 | +def extract_introduced_and_fixed(ranges) -> Tuple[List[str], List[str]]: |
199 | 199 | """ |
200 | | - Return a list of fixed version strings given a ``fixed_range`` mapping of |
201 | | - OSV data. |
| 200 | + Return pairs of introduced and fixed versions or commit hashes given a ``ranges`` |
| 201 | + mapping of OSV data. |
202 | 202 |
|
203 | | - >>> list(extract_fixed_versions( |
204 | | - ... {"type": "SEMVER", "events": [{"introduced": "0"},{"fixed": "1.6.0"}]})) |
205 | | - ['1.6.0'] |
206 | | -
|
207 | | - >>> list(extract_fixed_versions( |
208 | | - ... {"type": "ECOSYSTEM","events":[{"introduced": "0"}, |
209 | | - ... {"fixed": "1.0.0"},{"fixed": "9.0.0"}]})) |
210 | | - ['1.0.0', '9.0.0'] |
211 | | - """ |
212 | | - for event in fixed_range.get("events") or []: |
213 | | - fixed = event.get("fixed") |
214 | | - if fixed: |
215 | | - yield fixed |
| 203 | + Both introduced and fixed fields may represent semantic versions or commit hashes. |
216 | 204 |
|
| 205 | + >>> list(extract_introduced_and_fixed( |
| 206 | + ... {"type": "SEMVER", "events": [{"introduced": "0"}, {"fixed": "1.6.0"}]})) |
| 207 | + [('0', None), (None, '1.6.0')] |
217 | 208 |
|
218 | | -def extract_commits(introduced_range) -> Iterable[str]: |
| 209 | + >>> list(extract_introduced_and_fixed( |
| 210 | + ... {"type": "GIT", "events": [{"introduced": "abc123"}, |
| 211 | + ... {"fixed": "def456"}]})) |
| 212 | + [('abc123', None), (None, 'def456')] |
219 | 213 | """ |
220 | | - Return a list of fixed version strings given a ``fixed_range`` mapping of |
221 | | - OSV data. |
222 | | - """ |
223 | | - for event in introduced_range.get("events") or []: |
| 214 | + for event in ranges.get("events") or []: |
224 | 215 | introduced = event.get("introduced") |
225 | 216 | fixed = event.get("fixed") |
226 | 217 | yield introduced, fixed |
@@ -369,91 +360,81 @@ def get_fixed_version_range(versions, ecosystem): |
369 | 360 | logger.error(f"Failed to create VersionRange from: {versions}: error:{e!r}") |
370 | 361 |
|
371 | 362 |
|
372 | | -def get_fixed_versions(fixed_range, raw_id, supported_ecosystem) -> List[Version]: |
| 363 | +def get_fixed_versions_and_commits( |
| 364 | + ranges, raw_id, supported_ecosystem=None |
| 365 | +) -> Tuple[List[Version], Tuple]: |
373 | 366 | """ |
374 | | - Return a list of unique fixed univers Versions given a ``fixed_range`` |
375 | | - univers VersionRange and a ``raw_id``. |
| 367 | + Extract and return all unique fixed versions and related commit data |
| 368 | + from a given OSV vulnerability range. |
| 369 | +
|
376 | 370 | For example:: |
377 | | - >>> get_fixed_versions(fixed_range={}, raw_id="GHSA-j3f7-7rmc-6wqj", supported_ecosystem="pypi",) |
378 | | - [] |
379 | | - >>> get_fixed_versions( |
380 | | - ... fixed_range={"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}], }, |
| 371 | + >>> get_fixed_versions_and_commits(ranges={}, raw_id="GHSA-j3f7-7rmc-6wqj", supported_ecosystem="pypi",) |
| 372 | + ([], ([], [])) |
| 373 | + >>> get_fixed_versions_and_commits( |
| 374 | + ... ranges={"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}], }, |
381 | 375 | ... raw_id="GHSA-j3f7-7rmc-6wqj", |
382 | 376 | ... supported_ecosystem="pypi", |
383 | 377 | ... ) |
384 | | - [PypiVersion(string='1.7.0')] |
| 378 | + ([PypiVersion(string='1.7.0')], ([], [])) |
385 | 379 | """ |
386 | 380 | fixed_versions = [] |
387 | | - if "type" not in fixed_range: |
388 | | - logger.error(f"Invalid fixed_range type for: {fixed_range} for OSV id: {raw_id!r}") |
389 | | - return [] |
| 381 | + introduced_commits = [] |
| 382 | + fixed_commits = [] |
390 | 383 |
|
391 | | - fixed_range_type = fixed_range["type"] |
| 384 | + if "type" not in ranges: |
| 385 | + logger.error(f"Invalid range type for: {ranges} for OSV id: {raw_id!r}") |
| 386 | + return [], ([], []) |
| 387 | + |
| 388 | + fixed_range_type = ranges["type"] |
392 | 389 |
|
393 | 390 | version_range_class = RANGE_CLASS_BY_SCHEMES.get(supported_ecosystem) |
394 | 391 | version_class = version_range_class.version_class if version_range_class else None |
395 | 392 |
|
396 | | - for version in extract_fixed_versions(fixed_range): |
397 | | - if fixed_range_type == "ECOSYSTEM": |
| 393 | + for introduced, fixed in extract_introduced_and_fixed(ranges): |
| 394 | + if fixed_range_type == "ECOSYSTEM" and fixed: |
398 | 395 | try: |
399 | 396 | if not version_class: |
400 | 397 | raise InvalidVersion( |
401 | 398 | f"Unsupported version for ecosystem: {supported_ecosystem}" |
402 | 399 | ) |
403 | | - fixed_versions.append(version_class(version)) |
| 400 | + fixed_versions.append(version_class(fixed)) |
404 | 401 | except InvalidVersion: |
405 | 402 | logger.error( |
406 | | - f"Invalid version class: {version_class} - {version!r} for OSV id: {raw_id!r}" |
| 403 | + f"Invalid version class: {version_class} - {fixed!r} for OSV id: {raw_id!r}" |
407 | 404 | ) |
408 | 405 |
|
409 | | - elif fixed_range_type == "SEMVER": |
| 406 | + elif fixed_range_type == "SEMVER" and fixed: |
410 | 407 | try: |
411 | | - fixed_versions.append(SemverVersion(version)) |
| 408 | + fixed_versions.append(SemverVersion(fixed)) |
412 | 409 | except InvalidVersion: |
413 | | - logger.error(f"Invalid SemverVersion: {version!r} for OSV id: {raw_id!r}") |
414 | | - |
415 | | - if fixed_range_type == "GIT": |
416 | | - # We process this in the get_code_commit function. |
417 | | - continue |
418 | | - else: |
419 | | - logger.error(f"Unsupported fixed version type: {version!r} for OSV id: {raw_id!r}") |
| 410 | + logger.error(f"Invalid SemverVersion: {fixed!r} for OSV id: {raw_id!r}") |
420 | 411 |
|
421 | | - return dedupe(fixed_versions) |
| 412 | + elif fixed_range_type == "GIT" and (fixed or introduced): |
| 413 | + repo = ranges.get("repo") |
| 414 | + if not repo: |
| 415 | + logger.error(f"Missing 'repo' field in ranges: {ranges} (OSV id: {raw_id!r})") |
| 416 | + continue |
422 | 417 |
|
| 418 | + # Git uses this magic hash for the empty tree |
| 419 | + if introduced == "0": |
| 420 | + introduced = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" |
423 | 421 |
|
424 | | -def get_code_commit(ranges, raw_id): |
425 | | - """ |
426 | | - Return two lists of unique code commits (introduced and fixed) extracted from a |
427 | | - given vulnerability `ranges` dictionary. |
428 | | - """ |
429 | | - if ranges.get("type") != "GIT": |
430 | | - logger.debug(f"Skipping non-GIT range for OSV id: {raw_id!r}") |
431 | | - return [], [] |
432 | | - |
433 | | - repo = ranges.get("repo") |
434 | | - if not repo: |
435 | | - logger.error(f"Missing 'repo' field in range: {ranges} (OSV id: {raw_id!r})") |
436 | | - return [], [] |
437 | | - |
438 | | - repo = ranges.get("repo") |
439 | | - introduced_commits, fixed_commits = [], [] |
440 | | - for introduced, fixed in extract_commits(ranges): |
441 | | - # Git uses this magic hash for the empty tree |
442 | | - if introduced == "0": |
443 | | - introduced = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" |
444 | | - |
445 | | - try: |
446 | 422 | if introduced: |
447 | | - introduced_commit = CodeCommitData(commit_hash=introduced, vcs_url=repo) |
448 | | - introduced_commits.append(introduced_commit) |
449 | | - except ValueError as e: |
450 | | - logger.error(f"Failed to extract introduced commits: {e!r}") |
| 423 | + try: |
| 424 | + introduced_commit = CodeCommitData(commit_hash=introduced, vcs_url=repo) |
| 425 | + introduced_commits.append(introduced_commit) |
| 426 | + except ValueError as e: |
| 427 | + logger.error(f"Failed to extract introduced commits: {e!r}") |
451 | 428 |
|
452 | | - try: |
453 | 429 | if fixed: |
454 | | - fixed_commit = CodeCommitData(commit_hash=fixed, vcs_url=repo) |
455 | | - fixed_commits.append(fixed_commit) |
456 | | - except ValueError as e: |
457 | | - logger.error(f"Failed to extract fixed commits: {e!r}") |
| 430 | + try: |
| 431 | + fixed_commit = CodeCommitData(commit_hash=fixed, vcs_url=repo) |
| 432 | + fixed_commits.append(fixed_commit) |
| 433 | + except ValueError as e: |
| 434 | + logger.error(f"Failed to extract fixed commits: {e!r}") |
| 435 | + |
| 436 | + else: |
| 437 | + if fixed: |
| 438 | + logger.error(f"Unsupported fixed version type: {ranges!r} for OSV id: {raw_id!r}") |
458 | 439 |
|
459 | | - return introduced_commits, fixed_commits |
| 440 | + return dedupe(fixed_versions), (introduced_commits, fixed_commits) |
0 commit comments