Coverage for .github / scripts / check_pip_audit_ignore.py: 100.00%

62 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Validate the .pip-audit-ignore suppression file. 

2 

3Each entry must: 

4 * have a non-comment, non-blank line whose first whitespace-delimited 

5 token is the vulnerability ID (e.g. ``PYSEC-2025-183``, 

6 ``CVE-2025-45768``, ``GHSA-xxxx-xxxx-xxxx``); and 

7 * include an ``exp:YYYY-MM-DD`` marker somewhere on the same line. 

8 

9The check fails the workflow when: 

10 * any entry's ``exp:`` date is on-or-before the reference date 

11 (today by default; configurable via ``--today`` for tests); or 

12 * any entry is missing the ``exp:`` marker entirely or has a 

13 malformed date. 

14 

15Inclusive expiration is intentional — once the listed date arrives the 

16suppression is considered expired, no bonus day. That mirrors how 

17``.trivyignore`` is treated by the rest of the project. 

18 

19Usage:: 

20 

21 python3 .github/scripts/check_pip_audit_ignore.py .pip-audit-ignore 

22 python3 .github/scripts/check_pip_audit_ignore.py .pip-audit-ignore --today 2026-08-19 

23 

24Exit codes:: 

25 

26 0 OK (or file does not exist — an absent ignore file is not an error) 

27 1 one or more entries failed validation 

28 2 unexpected I/O / argument error 

29 

30The module is importable from the test suite — call ``check_file()`` 

31directly to exercise the logic against fixtures. 

32""" 

33 

34from __future__ import annotations 

35 

36import argparse 

37import datetime 

38import re 

39import sys 

40from pathlib import Path 

41 

42EXP_RE = re.compile(r"\bexp:(\d{4})-(\d{2})-(\d{2})\b") 

43 

44 

45def check_file( 

46 path: Path, today: datetime.date | None = None 

47) -> tuple[list[tuple[int, str]], list[tuple[int, str, datetime.date]]]: 

48 """Return (missing, expired) entry lists for the given ignore file. 

49 

50 ``missing`` lists ``(line_number, vuln_id)`` for entries without a 

51 valid ``exp:YYYY-MM-DD`` marker. ``expired`` lists 

52 ``(line_number, vuln_id, exp_date)`` for entries whose date is 

53 on-or-before ``today``. 

54 

55 ``today`` defaults to ``datetime.date.today()`` and exists as a 

56 parameter so the test suite can pin a deterministic reference date. 

57 

58 A non-existent file returns two empty lists — the absence of an 

59 ignore file is not an error. 

60 """ 

61 today = today or datetime.date.today() 

62 missing: list[tuple[int, str]] = [] 

63 expired: list[tuple[int, str, datetime.date]] = [] 

64 

65 if not path.exists(): 

66 return missing, expired 

67 

68 for lineno, raw in enumerate(path.read_text().splitlines(), start=1): 

69 stripped = raw.strip() 

70 # Blank lines and full-line comments are skipped. 

71 if not stripped or stripped.startswith("#"): 

72 continue 

73 # First whitespace token is the vuln ID; rest is rationale + exp:. 

74 tokens = stripped.split(None, 1) 

75 vuln_id = tokens[0] 

76 rest = tokens[1] if len(tokens) > 1 else "" 

77 match = EXP_RE.search(rest) 

78 if not match: 

79 missing.append((lineno, vuln_id)) 

80 continue 

81 year, month, day = (int(part) for part in match.groups()) 

82 try: 

83 exp_date = datetime.date(year, month, day) 

84 except ValueError: 

85 # Catches e.g. exp:2026-13-40 — invalid date components. 

86 missing.append((lineno, vuln_id)) 

87 continue 

88 if exp_date <= today: 

89 expired.append((lineno, vuln_id, exp_date)) 

90 

91 return missing, expired 

92 

93 

94def _format_report( 

95 today: datetime.date, 

96 missing: list[tuple[int, str]], 

97 expired: list[tuple[int, str, datetime.date]], 

98) -> str: 

99 """Render the human-facing error report. Empty string when both clean.""" 

100 lines: list[str] = [] 

101 if missing: 

102 lines.append("ERROR: .pip-audit-ignore entries missing a valid exp:YYYY-MM-DD marker:") 

103 for lineno, vuln_id in missing: 

104 lines.append(f" line {lineno}: {vuln_id}") 

105 if expired: 

106 lines.append( 

107 f"ERROR: .pip-audit-ignore entries past their expiration date " 

108 f"(today is {today.isoformat()}):" 

109 ) 

110 for lineno, vuln_id, exp_date in expired: 

111 lines.append(f" line {lineno}: {vuln_id} expired on {exp_date.isoformat()}") 

112 lines.append( 

113 "Re-evaluate each entry: remove it if the CVE is fixed, or extend " 

114 "the date with fresh rationale." 

115 ) 

116 return "\n".join(lines) 

117 

118 

119def _parse_today(value: str) -> datetime.date: 

120 try: 

121 return datetime.date.fromisoformat(value) 

122 except ValueError as exc: 

123 raise argparse.ArgumentTypeError(f"--today must be YYYY-MM-DD, got {value!r}") from exc 

124 

125 

126def main(argv: list[str] | None = None) -> int: 

127 parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) 

128 parser.add_argument( 

129 "path", 

130 type=Path, 

131 help="Path to the .pip-audit-ignore file to validate.", 

132 ) 

133 parser.add_argument( 

134 "--today", 

135 type=_parse_today, 

136 default=None, 

137 help=( 

138 "Reference date in YYYY-MM-DD format. Defaults to today's " 

139 "UTC-naive date. Provided for deterministic testing." 

140 ), 

141 ) 

142 args = parser.parse_args(argv) 

143 

144 today = args.today or datetime.date.today() 

145 missing, expired = check_file(args.path, today=today) 

146 report = _format_report(today, missing, expired) 

147 if report: 

148 print(report) 

149 return 1 

150 return 0 

151 

152 

153if __name__ == "__main__": 

154 sys.exit(main())