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