Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: support --fix --dry-run #223

Merged
merged 4 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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