Skip to content

Commit c01f4d5

Browse files
authored
README, _cli: Add a -S, --strict mode (#146)
* README, _cli: Add a `-S`, `--strict` mode * README: fix `--help` * _cli: Use logger.error for _fatal * _cli: Fix logger.error call * _cli: Drop the "Fatal: " prefix for logged errors The logger already provides a reasonable prefix. * README, _cli: Fix a small typo * README: fix `--help` * pip_audit, test: use debug for skipped dep messages * README, _cli: judicious use of `--help` metavars * README: fix `--help`
1 parent bfe393e commit c01f4d5

File tree

6 files changed

+54
-22
lines changed

6 files changed

+54
-22
lines changed

README.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@ python -m pip install pip-audit
2626

2727
<!-- @begin-pip-audit-help@ -->
2828
```
29-
usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS]
30-
[-f {columns,json,cyclonedx-json,cyclonedx-xml}]
31-
[-s {osv,pypi}] [-d] [--desc {on,off,auto}]
32-
[--cache-dir CACHE_DIR] [--progress-spinner {on,off}]
33-
[--timeout TIMEOUT]
29+
usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f FORMAT] [-s SERVICE]
30+
[-d] [-S] [--desc {on,off,auto}] [--cache-dir CACHE_DIR]
31+
[--progress-spinner {on,off}] [--timeout TIMEOUT]
3432
3533
audit the Python environment for dependencies with known vulnerabilities
3634
@@ -42,13 +40,17 @@ optional arguments:
4240
-r REQUIREMENTS, --requirement REQUIREMENTS
4341
audit the given requirements file; this option can be
4442
used multiple times (default: None)
45-
-f {columns,json,cyclonedx-json,cyclonedx-xml}, --format {columns,json,cyclonedx-json,cyclonedx-xml}
46-
the format to emit audit results in (default: columns)
47-
-s {osv,pypi}, --vulnerability-service {osv,pypi}
43+
-f FORMAT, --format FORMAT
44+
the format to emit audit results in (choices: columns,
45+
json, cyclonedx-json, cyclonedx-xml) (default:
46+
columns)
47+
-s SERVICE, --vulnerability-service SERVICE
4848
the vulnerability service to audit dependencies
49-
against (default: pypi)
49+
against (choices: osv, pypi) (default: pypi)
5050
-d, --dry-run collect all dependencies but do not perform the
5151
auditing step (default: False)
52+
-S, --strict fail the entire audit if dependency collection fails
53+
on any dependency (default: False)
5254
--desc {on,off,auto} include a description for each vulnerability; `auto`
5355
defaults to `on` for the `json` format. This flag has
5456
no effect on the `cyclonedx-json` or `cyclonedx-xml`

pip_audit/_cli.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
from contextlib import ExitStack
1111
from pathlib import Path
12-
from typing import List, Optional, cast
12+
from typing import List, NoReturn, Optional, Type, cast
1313

1414
from pip_audit import __version__
1515
from pip_audit._audit import AuditOptions, Auditor
@@ -117,6 +117,23 @@ def __str__(self):
117117
return self.value
118118

119119

120+
def _enum_help(msg: str, e: Type[enum.Enum]) -> str:
121+
"""
122+
Render a `--help`-style string for the given enumeration.
123+
"""
124+
return f"{msg} (choices: {', '.join(str(v) for v in e)})"
125+
126+
127+
def _fatal(msg: str) -> NoReturn:
128+
"""
129+
Log a fatal error to the standard error stream and exit.
130+
"""
131+
# NOTE: We buffer the logger when the progress spinner is active,
132+
# ensuring that the fatal message is formatted on its own line.
133+
logger.error(msg)
134+
sys.exit(1)
135+
136+
120137
def audit() -> None:
121138
"""
122139
The primary entrypoint for `pip-audit`.
@@ -147,22 +164,32 @@ def audit() -> None:
147164
type=OutputFormatChoice,
148165
choices=OutputFormatChoice,
149166
default=OutputFormatChoice.Columns,
150-
help="the format to emit audit results in",
167+
metavar="FORMAT",
168+
help=_enum_help("the format to emit audit results in", OutputFormatChoice),
151169
)
152170
parser.add_argument(
153171
"-s",
154172
"--vulnerability-service",
155173
type=VulnerabilityServiceChoice,
156174
choices=VulnerabilityServiceChoice,
157175
default=VulnerabilityServiceChoice.Pypi,
158-
help="the vulnerability service to audit dependencies against",
176+
metavar="SERVICE",
177+
help=_enum_help(
178+
"the vulnerability service to audit dependencies against", VulnerabilityServiceChoice
179+
),
159180
)
160181
parser.add_argument(
161182
"-d",
162183
"--dry-run",
163184
action="store_true",
164185
help="collect all dependencies but do not perform the auditing step",
165186
)
187+
parser.add_argument(
188+
"-S",
189+
"--strict",
190+
action="store_true",
191+
help="fail the entire audit if dependency collection fails on any dependency",
192+
)
166193
parser.add_argument(
167194
"--desc",
168195
type=VulnerabilityDescriptionChoice,
@@ -214,7 +241,10 @@ def audit() -> None:
214241
if state is not None:
215242
if spec.is_skipped():
216243
spec = cast(SkippedDependency, spec)
217-
state.update_state(f"Skipping {spec.name}: {spec.skip_reason}")
244+
if args.strict:
245+
_fatal(f"{spec.name}: {spec.skip_reason}")
246+
else:
247+
state.update_state(f"Skipping {spec.name}: {spec.skip_reason}")
218248
else:
219249
spec = cast(ResolvedDependency, spec)
220250
state.update_state(f"Auditing {spec.name} ({spec.version})")

pip_audit/_dependency_source/pip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def collect(self) -> Iterator[Dependency]:
7474
"Package has invalid version and could not be audited: "
7575
f"{dist.name} ({dist.version})"
7676
)
77-
logger.warning(f"Warning: {skip_reason}")
77+
logger.debug(skip_reason)
7878
dep = SkippedDependency(name=dist.name, skip_reason=skip_reason)
7979
yield dep
8080
except Exception as e:

pip_audit/_service/pypi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def query(self, spec: Dependency) -> Tuple[Dependency, List[VulnerabilityResult]
168168
"Dependency not found on PyPI and could not be audited: "
169169
f"{spec.canonical_name} ({spec.version})"
170170
)
171-
logger.warning(f"Warning: {skip_reason}")
171+
logger.debug(skip_reason)
172172
return SkippedDependency(name=spec.name, skip_reason=skip_reason), []
173173
raise ServiceError from http_error
174174

test/dependency_source/test_pip.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def explode():
4949

5050

5151
def test_pip_source_invalid_version(monkeypatch):
52-
logger = pretend.stub(warning=pretend.call_recorder(lambda s: None))
52+
logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
5353
monkeypatch.setattr(pip, "logger", logger)
5454

5555
source = pip.PipSource()
@@ -60,7 +60,7 @@ class MockDistribution:
6060
version: str
6161

6262
# Return a distribution with a version that doesn't conform to PEP 440.
63-
# We should log a warning and skip it.
63+
# We should log a debug message and skip it.
6464
def mock_installed_distributions(local: bool) -> Dict[str, MockDistribution]:
6565
return {
6666
"pytest": MockDistribution("pytest", "0.1"),
@@ -71,7 +71,7 @@ def mock_installed_distributions(local: bool) -> Dict[str, MockDistribution]:
7171
monkeypatch.setattr(pip_api, "installed_distributions", mock_installed_distributions)
7272

7373
specs = list(source.collect())
74-
assert len(logger.warning.calls) == 1
74+
assert len(logger.debug.calls) == 1
7575
assert len(specs) == 3
7676
assert ResolvedDependency(name="pytest", version=Version("0.1")) in specs
7777
assert (

test/service/test_pypi.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def test_pypi_multiple_pkg(cache_dir):
4949

5050
def test_pypi_http_notfound(monkeypatch, cache_dir):
5151
# If we get a "not found" response, that means that we're querying a package or version that
52-
# isn't known to PyPI. If that's the case, we should just log a warning and continue on with
53-
# the audit.
52+
# isn't known to PyPI. If that's the case, we should just log a debug message and continue on
53+
# with the audit.
5454
def get_error_response():
5555
class MockResponse:
5656
# 404: Not Found
@@ -64,7 +64,7 @@ def raise_for_status(self):
6464
monkeypatch.setattr(
6565
service.pypi, "_get_cached_session", lambda _: get_mock_session(get_error_response)
6666
)
67-
logger = pretend.stub(warning=pretend.call_recorder(lambda s: None))
67+
logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
6868
monkeypatch.setattr(service.pypi, "logger", logger)
6969

7070
pypi = service.PyPIService(cache_dir)
@@ -80,7 +80,7 @@ def raise_for_status(self):
8080
assert skipped_dep in results
8181
assert dep not in results
8282
assert len(results[skipped_dep]) == 0
83-
assert len(logger.warning.calls) == 1
83+
assert len(logger.debug.calls) == 1
8484

8585

8686
def test_pypi_http_error(monkeypatch, cache_dir):

0 commit comments

Comments
 (0)