Skip to content

Commit

Permalink
cli: support --fix --dry-run (#223)
Browse files Browse the repository at this point in the history
* cli: support `--fix --dry-run`

Closes #220.

* Makefile: remove redundant lint command

We don't need this diff check.

* README: update `pip-audit --help`, document `--dry-run` behavior

* CHANGELOG: record changes
  • Loading branch information
woodruffw authored Jan 18, 2022
1 parent 3b29e66 commit b5e2ac0
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ All versions prior to 0.0.9 are untracked.
available ([#212](https://github.com/trailofbits/pip-audit/pull/212),
[#222](https://github.com/trailofbits/pip-audit/pull/222))

* CLI: The combination of `--fix` and `--dry-run` is now supported, causing
`pip-audit` to perform the auditing step but not any resulting fix steps
([#223](https://github.com/trailofbits/pip-audit/pull/223))

### Changed

* CLI: The default output format is now correctly pluralized
Expand Down
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ lint: env/pyvenv.cfg
isort $(ALL_PY_SRCS) && \
flake8 $(ALL_PY_SRCS) && \
mypy $(PY_MODULE) test/ && \
interrogate -c pyproject.toml . && \
git diff --exit-code
interrogate -c pyproject.toml .

.PHONY: test tests
test tests: env/pyvenv.cfg
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ optional arguments:
-s SERVICE, --vulnerability-service SERVICE
the vulnerability service to audit dependencies
against (choices: osv, pypi) (default: pypi)
-d, --dry-run collect all dependencies but do not perform the
auditing step (default: False)
-d, --dry-run without `--fix`: collect all dependencies but do not
perform the auditing step; with `--fix`: perform the
auditing step but do not perform any fixes (default:
False)
-S, --strict fail the entire audit if dependency collection fails
on any dependency (default: False)
--desc [{on,off,auto}]
Expand Down Expand Up @@ -125,6 +127,17 @@ The current codes are:
* `0`: No known vulnerabilities were detected.
* `1`: One or more known vulnerabilities were found.

### Dry runs

`pip-audit` supports the `--dry-run` flag, which can be used to control whether
an audit (or fix) step is actually performed.

* On its own, `pip-audit --dry-run` skips the auditing step and prints
the number of dependencies that *would have been* audited.
* In fix mode, `pip-audit --fix --dry-run` performs the auditing step and prints
out the fix behavior (i.e., which dependencies would be upgraded or skipped)
that *would have been performed*.

## Examples

Audit dependencies for the current Python environment:
Expand All @@ -139,7 +152,7 @@ $ pip-audit -r ./requirements.txt
No known vulnerabilities found
```

Audit dependencies for the current Python environment excluding system packages:
Audit dependencies for a requirements file, excluding system packages:
```
$ pip-audit -r ./requirements.txt -l
No known vulnerabilities found
Expand Down
34 changes: 25 additions & 9 deletions pip_audit/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def _parser() -> argparse.ArgumentParser:
"-d",
"--dry-run",
action="store_true",
help="collect all dependencies but do not perform the auditing step",
help="without `--fix`: collect all dependencies but do not perform the auditing step; "
"with `--fix`: perform the auditing step but do not perform any fixes",
)
parser.add_argument(
"-S",
Expand Down Expand Up @@ -276,7 +277,10 @@ def audit() -> None:
else:
source = PipSource(local=args.local, paths=args.paths)

auditor = Auditor(service, options=AuditOptions(dry_run=args.dry_run))
# `--dry-run` only affects the auditor if `--fix` is also not supplied,
# since the combination of `--dry-run` and `--fix` implies that the user
# wants to dry-run the "fix" step instead of the "audit" step
auditor = Auditor(service, options=AuditOptions(dry_run=args.dry_run and not args.fix))

result = {}
pkg_count = 0
Expand All @@ -302,16 +306,28 @@ def audit() -> None:
fixed_pkg_count = 0
fixed_vuln_count = 0
if args.fix:
for fix_version in resolve_fix_versions(service, result):
if not fix_version.is_skipped():
fix_version = cast(ResolvedFixVersion, fix_version)
for fix in resolve_fix_versions(service, result):
if args.dry_run:
if fix.is_skipped():
fix = cast(SkippedFixVersion, fix)
logger.info(
f"Dry run: would have skipped {fix.dep.name} "
f"upgrade because {fix.skip_reason}"
)
else:
fix = cast(ResolvedFixVersion, fix)
logger.info(f"Dry run: would have upgraded {fix.dep.name} to " f"{fix.version}")
continue

if not fix.is_skipped():
fix = cast(ResolvedFixVersion, fix)
try:
source.fix(fix_version)
source.fix(fix)
fixed_pkg_count += 1
fixed_vuln_count += len(result[fix_version.dep])
fixed_vuln_count += len(result[fix.dep])
except DependencySourceError as dse:
fix_version = SkippedFixVersion(fix_version.dep, str(dse))
fixes.append(fix_version)
fix = SkippedFixVersion(fix.dep, str(dse))
fixes.append(fix)

# TODO(ww): Refine this: we should always output if our output format is an SBOM
# or other manifest format (like the default JSON format).
Expand Down

0 comments on commit b5e2ac0

Please sign in to comment.