Coverage for lintro / parsers / osv_scanner / suppression_parser.py: 94%
49 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Parser for .osv-scanner.toml vulnerability suppression entries.
3Reads [[IgnoredVulns]] entries from the OSV-Scanner configuration file
4and classifies them as ACTIVE, STALE, or EXPIRED based on probe scan
5results and expiry dates.
6"""
8from __future__ import annotations
10import tomllib
11from datetime import date, datetime
12from pathlib import Path
13from typing import Any
15from loguru import logger
17from lintro.parsers.osv_scanner.suppression_models import (
18 ClassifiedSuppression,
19 SuppressionEntry,
20)
21from lintro.parsers.osv_scanner.suppression_status import SuppressionStatus
24def parse_suppressions(toml_path: Path) -> list[SuppressionEntry]:
25 """Parse [[IgnoredVulns]] entries from an .osv-scanner.toml file.
27 Args:
28 toml_path: Path to the .osv-scanner.toml file.
30 Returns:
31 List of suppression entries. Returns empty list if the file
32 doesn't exist, can't be parsed, or has no IgnoredVulns.
33 """
34 if not toml_path.is_file():
35 return []
37 try:
38 with toml_path.open("rb") as f:
39 data: dict[str, Any] = tomllib.load(f)
40 except (tomllib.TOMLDecodeError, OSError) as e:
41 logger.warning("Failed to parse {}: {}", toml_path, e)
42 return []
44 ignored_vulns = data.get("IgnoredVulns", [])
45 if not isinstance(ignored_vulns, list):
46 return []
48 entries: list[SuppressionEntry] = []
49 for item in ignored_vulns:
50 if not isinstance(item, dict):
51 continue
53 vuln_id = item.get("id")
54 if not isinstance(vuln_id, str) or not vuln_id:
55 logger.debug("Skipping IgnoredVulns entry with missing id")
56 continue
58 ignore_until = item.get("ignoreUntil")
59 if not isinstance(ignore_until, date) or isinstance(ignore_until, datetime):
60 logger.debug(
61 "Skipping IgnoredVulns entry '{}': missing or invalid ignoreUntil",
62 vuln_id,
63 )
64 continue
66 reason = item.get("reason", "")
67 if not isinstance(reason, str):
68 reason = ""
70 entries.append(
71 SuppressionEntry(
72 id=vuln_id,
73 ignore_until=ignore_until,
74 reason=reason,
75 ),
76 )
78 return entries
81def classify_suppressions(
82 entries: list[SuppressionEntry],
83 probe_vuln_ids: set[str],
84 today: date | None = None,
85) -> list[ClassifiedSuppression]:
86 """Classify suppression entries as ACTIVE, STALE, or EXPIRED.
88 Compares each suppression against the probe scan results (a scan
89 run without suppressions) to determine if the suppressed
90 vulnerability is still present in the dependency tree.
92 Args:
93 entries: Suppression entries parsed from .osv-scanner.toml.
94 probe_vuln_ids: Set of vulnerability IDs found by the probe
95 scan (run with --config /dev/null to disable suppressions).
96 today: Override for the current date (for testing).
97 Defaults to date.today().
99 Returns:
100 List of classified suppressions with their status.
101 """
102 if today is None:
103 today = date.today()
105 classified: list[ClassifiedSuppression] = []
106 for entry in entries:
107 if today > entry.ignore_until:
108 status = SuppressionStatus.EXPIRED
109 elif entry.id in probe_vuln_ids:
110 status = SuppressionStatus.ACTIVE
111 else:
112 status = SuppressionStatus.STALE
114 classified.append(ClassifiedSuppression(entry=entry, status=status))
116 return classified