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
« 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."""
6from __future__ import annotations
8import datetime
9import itertools
10from pathlib import Path
12import click
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)
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]
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']
75 python_versions = python_versions_between(min_python_ver, max_python_ver, supported_python_versions)
77 output_dir.mkdir(parents=True, exist_ok=True)
79 has_vulnerabilities = False
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'
87 if req_file.exists():
88 req_file.unlink()
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 )
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')
105 res_json = run_pip_audit(req_file, audit_json, 'json')
106 if res_json.returncode != 0:
107 has_vulnerabilities = True
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')
113 report_lines = [
114 '# Dependency vulnerabilities audit',
115 '',
116 f'Performed on {timestamp}',
117 '',
118 ]
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
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 )
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
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 )
170 report_file.write_text('\n'.join(report_lines), encoding='utf-8')
172 if has_vulnerabilities:
173 raise click.ClickException('Vulnerabilities were found during the audit.')