Coverage for src / mafw / devtools / cli / dependencies / audit.py: 98%

70 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-06-28 13:34 +0000

1# Copyright 2026 European Union 

2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu) 

3# SPDX-License-Identifier: EUPL-1.2 

4"""Dependency vulnerability auditing command.""" 

5 

6from __future__ import annotations 

7 

8import datetime 

9import itertools 

10from pathlib import Path 

11 

12import click 

13 

14from mafw.devtools.dependencies.audit import run_pip_audit 

15from mafw.devtools.dependencies.compile import ( 

16 DEFAULT_FREEZE_EXTRAS, 

17 compile_dependency_lockfile, 

18 ensure_mafw_project_root, 

19 python_versions_between, 

20) 

21 

22 

23@click.command( 

24 context_settings={'help_option_names': ['-h', '--help']}, 

25 help='Audit dependency vulnerabilities using pip-audit.', 

26) 

27@click.option( 

28 '--min-python-ver', 

29 default=None, 

30 type=click.STRING, 

31 help='The oldest version of python to be used for the audit. Default: lowest supported in pyproject.toml', 

32) 

33@click.option( 

34 '--max-python-ver', 

35 default=None, 

36 type=click.STRING, 

37 help='The newest version of python to be used for the audit. Default: highest supported in pyproject.toml', 

38) 

39@click.option( 

40 '-r', 

41 '--resolution', 

42 type=click.Choice(['highest', 'lowest', 'both']), 

43 default='both', 

44 show_default=True, 

45 help='The uv resolution strategy to audit.', 

46) 

47@click.option( 

48 '--output-dir', 

49 type=click.Path(path_type=Path), 

50 default=Path('./audit'), 

51 show_default=True, 

52 help='Directory where to store the audit artifact files.', 

53) 

54def audit( 

55 min_python_ver: str | None, 

56 max_python_ver: str | None, 

57 resolution: str, 

58 output_dir: Path, 

59) -> None: 

60 """Audit dependency vulnerabilities using pip-audit.""" 

61 supported_python_versions = ensure_mafw_project_root() 

62 if min_python_ver is None: 

63 min_python_ver = supported_python_versions[0] 

64 if max_python_ver is None: 

65 max_python_ver = supported_python_versions[-1] 

66 

67 resolutions: list[str] 

68 if resolution == 'highest': 

69 resolutions = ['highest'] 

70 elif resolution == 'lowest': 

71 resolutions = ['lowest-direct'] 

72 else: 

73 resolutions = ['highest', 'lowest-direct'] 

74 

75 python_versions = python_versions_between(min_python_ver, max_python_ver, supported_python_versions) 

76 

77 output_dir.mkdir(parents=True, exist_ok=True) 

78 

79 has_vulnerabilities = False 

80 

81 for res, python_ver in itertools.product(resolutions, python_versions): 

82 file_res = 'lowest' if res == 'lowest-direct' else 'highest' 

83 req_file = output_dir / f'requirements_{file_res}_{python_ver}.txt' 

84 audit_md = output_dir / f'audit_{file_res}_{python_ver}.md' 

85 audit_json = output_dir / f'audit_{file_res}_{python_ver}.json' 

86 

87 if req_file.exists(): 

88 req_file.unlink() 

89 

90 compile_dependency_lockfile( 

91 python_version=python_ver, 

92 pylock_file=req_file, 

93 extras=list(DEFAULT_FREEZE_EXTRAS), 

94 resolution=res, 

95 output_format='requirements.txt', 

96 with_hashes=True, 

97 ) 

98 

99 res_md = run_pip_audit(req_file, audit_md, 'markdown') 

100 if res_md.returncode != 0: 

101 has_vulnerabilities = True 

102 elif not audit_md.exists(): 

103 audit_md.write_text('No known vulnerabilities found.\n', encoding='utf-8') 

104 

105 res_json = run_pip_audit(req_file, audit_json, 'json') 

106 if res_json.returncode != 0: 

107 has_vulnerabilities = True 

108 

109 # Prepare overall auditing report 

110 report_file = output_dir / 'audit_report.md' 

111 timestamp = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') 

112 

113 report_lines = [ 

114 '# Dependency vulnerabilities audit', 

115 '', 

116 f'Performed on {timestamp}', 

117 '', 

118 ] 

119 

120 lowest_files_exist = False 

121 for python_ver in python_versions: 

122 if (output_dir / f'audit_lowest_{python_ver}.md').exists(): 

123 lowest_files_exist = True 

124 break 

125 

126 if 'lowest-direct' in resolutions and lowest_files_exist: 

127 report_lines.extend( 

128 [ 

129 '## Oldest supported ecosystem', 

130 '', 

131 ] 

132 ) 

133 for python_ver in python_versions: 

134 md_file = output_dir / f'audit_lowest_{python_ver}.md' 

135 if md_file.exists(): 135 ↛ 133line 135 didn't jump to line 133 because the condition on line 135 was always true

136 report_lines.extend( 

137 [ 

138 f'### Python version {python_ver}', 

139 '', 

140 md_file.read_text(encoding='utf-8'), 

141 '', 

142 ] 

143 ) 

144 

145 highest_files_exist = False 

146 for python_ver in python_versions: 

147 if (output_dir / f'audit_highest_{python_ver}.md').exists(): 

148 highest_files_exist = True 

149 break 

150 

151 if 'highest' in resolutions and highest_files_exist: 

152 report_lines.extend( 

153 [ 

154 '## Newest supported ecosystem', 

155 '', 

156 ] 

157 ) 

158 for python_ver in python_versions: 

159 md_file = output_dir / f'audit_highest_{python_ver}.md' 

160 if md_file.exists(): 160 ↛ 158line 160 didn't jump to line 158 because the condition on line 160 was always true

161 report_lines.extend( 

162 [ 

163 f'### Python version {python_ver}', 

164 '', 

165 md_file.read_text(encoding='utf-8'), 

166 '', 

167 ] 

168 ) 

169 

170 report_file.write_text('\n'.join(report_lines), encoding='utf-8') 

171 

172 if has_vulnerabilities: 

173 raise click.ClickException('Vulnerabilities were found during the audit.')