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

1"""Parser for .osv-scanner.toml vulnerability suppression entries. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10import tomllib 

11from datetime import date, datetime 

12from pathlib import Path 

13from typing import Any 

14 

15from loguru import logger 

16 

17from lintro.parsers.osv_scanner.suppression_models import ( 

18 ClassifiedSuppression, 

19 SuppressionEntry, 

20) 

21from lintro.parsers.osv_scanner.suppression_status import SuppressionStatus 

22 

23 

24def parse_suppressions(toml_path: Path) -> list[SuppressionEntry]: 

25 """Parse [[IgnoredVulns]] entries from an .osv-scanner.toml file. 

26 

27 Args: 

28 toml_path: Path to the .osv-scanner.toml file. 

29 

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 [] 

36 

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 [] 

43 

44 ignored_vulns = data.get("IgnoredVulns", []) 

45 if not isinstance(ignored_vulns, list): 

46 return [] 

47 

48 entries: list[SuppressionEntry] = [] 

49 for item in ignored_vulns: 

50 if not isinstance(item, dict): 

51 continue 

52 

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 

57 

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 

65 

66 reason = item.get("reason", "") 

67 if not isinstance(reason, str): 

68 reason = "" 

69 

70 entries.append( 

71 SuppressionEntry( 

72 id=vuln_id, 

73 ignore_until=ignore_until, 

74 reason=reason, 

75 ), 

76 ) 

77 

78 return entries 

79 

80 

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. 

87 

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. 

91 

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(). 

98 

99 Returns: 

100 List of classified suppressions with their status. 

101 """ 

102 if today is None: 

103 today = date.today() 

104 

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 

113 

114 classified.append(ClassifiedSuppression(entry=entry, status=status)) 

115 

116 return classified