From 6f4e2684e5695b87947b9836e6384592e47cb75d Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Wed, 1 Dec 2021 18:55:44 +1100 Subject: [PATCH 1/9] pip, _cli: Add `--path` argument to mirror `pip list` --- pip_audit/_cli.py | 13 ++++++++++++- pip_audit/_dependency_source/pip.py | 12 +++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 17781df0..b7e9237e 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -214,6 +214,14 @@ def audit() -> None: parser.add_argument( "--timeout", type=int, default=15, help="set the socket timeout" # Match the `pip` default ) + parser.add_argument( + "--path", + type=Path, + action="append", + dest="paths", + help="restrict to the specified installation path for auditing packages; " + "this option can be used multiple times", + ) args = parser.parse_args() logger.debug(f"parsed arguments: {args}") @@ -227,10 +235,13 @@ def audit() -> None: source: DependencySource if args.requirements is not None: + if args.paths is not None: + print("--requirement (-r) and --path arguments cannot be used together") + sys.exit(1) req_files: List[Path] = [Path(req.name) for req in args.requirements] source = RequirementSource(req_files, ResolveLibResolver(args.timeout, state), state) else: - source = PipSource(local=args.local) + source = PipSource(local=args.local, paths=args.paths if args.path is not None else []) auditor = Auditor(service, options=AuditOptions(dry_run=args.dry_run)) diff --git a/pip_audit/_dependency_source/pip.py b/pip_audit/_dependency_source/pip.py index e7aea739..404f7614 100644 --- a/pip_audit/_dependency_source/pip.py +++ b/pip_audit/_dependency_source/pip.py @@ -4,7 +4,8 @@ """ import logging -from typing import Iterator, Optional +from pathlib import Path +from typing import Iterator, List, Optional import pip_api from packaging.version import InvalidVersion, Version @@ -32,7 +33,9 @@ class PipSource(DependencySource): Wraps `pip` (specifically `pip list`) as a dependency source. """ - def __init__(self, *, local: bool = False, state: Optional[AuditState] = None) -> None: + def __init__( + self, *, local: bool = False, paths: List[Path], state: Optional[AuditState] = None + ) -> None: """ Create a new `PipSource`. @@ -42,6 +45,7 @@ def __init__(self, *, local: bool = False, state: Optional[AuditState] = None) - `state` is an optional `AuditState` to use for state callbacks. """ self._local = local + self._paths: List[str] = [str(path) for path in paths] self.state = state if _PIP_VERSION < _MINIMUM_RELIABLE_PIP_VERSION: @@ -61,7 +65,9 @@ def collect(self) -> Iterator[Dependency]: # The `pip list` call that underlies `pip_api` could fail for myriad reasons. # We collect them all into a single well-defined error. try: - for (_, dist) in pip_api.installed_distributions(local=self._local).items(): + for (_, dist) in pip_api.installed_distributions( + local=self._local, paths=self._paths + ).items(): dep: Dependency try: dep = ResolvedDependency(name=dist.name, version=Version(str(dist.version))) From 582a489d9cb151de1f40ff7de0dbfa6f70aa7cca Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Wed, 1 Dec 2021 19:39:21 +1100 Subject: [PATCH 2/9] _dependency_source/pip: Switch to interface that uses `os.PathLike` --- pip_audit/_dependency_source/pip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_audit/_dependency_source/pip.py b/pip_audit/_dependency_source/pip.py index 404f7614..f7ad34cc 100644 --- a/pip_audit/_dependency_source/pip.py +++ b/pip_audit/_dependency_source/pip.py @@ -45,7 +45,7 @@ def __init__( `state` is an optional `AuditState` to use for state callbacks. """ self._local = local - self._paths: List[str] = [str(path) for path in paths] + self._paths = paths self.state = state if _PIP_VERSION < _MINIMUM_RELIABLE_PIP_VERSION: From 8b09cd858c1d65c4af4ed78a1a6e140477d83981 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Wed, 1 Dec 2021 19:42:47 +1100 Subject: [PATCH 3/9] _cli: Avoid `None` checks by using an empty list as default --- pip_audit/_cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index b7e9237e..66a10b16 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -219,6 +219,7 @@ def audit() -> None: type=Path, action="append", dest="paths", + default=[], help="restrict to the specified installation path for auditing packages; " "this option can be used multiple times", ) @@ -235,13 +236,13 @@ def audit() -> None: source: DependencySource if args.requirements is not None: - if args.paths is not None: + if args.paths: print("--requirement (-r) and --path arguments cannot be used together") sys.exit(1) req_files: List[Path] = [Path(req.name) for req in args.requirements] source = RequirementSource(req_files, ResolveLibResolver(args.timeout, state), state) else: - source = PipSource(local=args.local, paths=args.paths if args.path is not None else []) + source = PipSource(local=args.local, paths=args.paths) auditor = Auditor(service, options=AuditOptions(dry_run=args.dry_run)) From d1ecd3dcfb1ae1089024dabb6821e7c5de8ee7f0 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Thu, 2 Dec 2021 16:27:48 +1100 Subject: [PATCH 4/9] _cli: Use mutually exclusive group for -r and --path args --- pip_audit/_cli.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 66a10b16..c0fead06 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -143,6 +143,7 @@ def audit() -> None: description="audit the Python environment for dependencies with known vulnerabilities", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + req_source_args = parser.add_mutually_exclusive_group() parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") parser.add_argument( "-l", @@ -150,7 +151,7 @@ def audit() -> None: action="store_true", help="show only results for dependencies in the local environment", ) - parser.add_argument( + req_source_args.add_argument( "-r", "--requirement", type=argparse.FileType("r"), @@ -214,7 +215,7 @@ def audit() -> None: parser.add_argument( "--timeout", type=int, default=15, help="set the socket timeout" # Match the `pip` default ) - parser.add_argument( + req_source_args.add_argument( "--path", type=Path, action="append", @@ -236,9 +237,6 @@ def audit() -> None: source: DependencySource if args.requirements is not None: - if args.paths: - print("--requirement (-r) and --path arguments cannot be used together") - sys.exit(1) req_files: List[Path] = [Path(req.name) for req in args.requirements] source = RequirementSource(req_files, ResolveLibResolver(args.timeout, state), state) else: From 041cd7c06c756913fd39ed5dee29e40e40362918 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 3 Dec 2021 14:23:48 +1100 Subject: [PATCH 5/9] _dependency_source, setup: Bump `pip-api` version and fix typing --- pip_audit/_dependency_source/pip.py | 6 +++--- setup.py | 2 +- test/dependency_source/test_pip.py | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pip_audit/_dependency_source/pip.py b/pip_audit/_dependency_source/pip.py index f7ad34cc..d75fc07d 100644 --- a/pip_audit/_dependency_source/pip.py +++ b/pip_audit/_dependency_source/pip.py @@ -5,7 +5,7 @@ import logging from pathlib import Path -from typing import Iterator, List, Optional +from typing import Iterator, Optional, Sequence import pip_api from packaging.version import InvalidVersion, Version @@ -34,7 +34,7 @@ class PipSource(DependencySource): """ def __init__( - self, *, local: bool = False, paths: List[Path], state: Optional[AuditState] = None + self, *, local: bool = False, paths: Sequence[Path] = [], state: Optional[AuditState] = None ) -> None: """ Create a new `PipSource`. @@ -66,7 +66,7 @@ def collect(self) -> Iterator[Dependency]: # We collect them all into a single well-defined error. try: for (_, dist) in pip_api.installed_distributions( - local=self._local, paths=self._paths + local=self._local, paths=list(self._paths) ).items(): dep: Dependency try: diff --git a/setup.py b/setup.py index a8944458..f1625504 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ platforms="any", python_requires=">=3.6", install_requires=[ - "pip-api>=0.0.23", + "pip-api>=0.0.25", "packaging>=21.0.0", # TODO: Remove this once 3.7 is our minimally supported version. "dataclasses>=0.6; python_version < '3.7'", diff --git a/test/dependency_source/test_pip.py b/test/dependency_source/test_pip.py index 172c0373..5f1e12de 100644 --- a/test/dependency_source/test_pip.py +++ b/test/dependency_source/test_pip.py @@ -1,5 +1,6 @@ +import os from dataclasses import dataclass -from typing import Dict +from typing import Dict, List import pip_api import pretend # type: ignore @@ -61,7 +62,9 @@ class MockDistribution: # Return a distribution with a version that doesn't conform to PEP 440. # We should log a debug message and skip it. - def mock_installed_distributions(local: bool) -> Dict[str, MockDistribution]: + def mock_installed_distributions( + local: bool, paths: List[os.PathLike] + ) -> Dict[str, MockDistribution]: return { "pytest": MockDistribution("pytest", "0.1"), "pip-audit": MockDistribution("pip-audit", "1.0-ubuntu0.21.04.1"), From 8e0cef7e1f4fd382f198050168790f10b8a95c77 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 3 Dec 2021 14:28:28 +1100 Subject: [PATCH 6/9] README: Update help text --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3f9c0cae..df9207da 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ python -m pip install pip-audit usage: pip-audit [-h] [-V] [-l] [-r REQUIREMENTS] [-f FORMAT] [-s SERVICE] [-d] [-S] [--desc [{on,off,auto}]] [--cache-dir CACHE_DIR] [--progress-spinner {on,off}] [--timeout TIMEOUT] + [--path PATHS] audit the Python environment for dependencies with known vulnerabilities @@ -62,6 +63,9 @@ optional arguments: --progress-spinner {on,off} display a progress spinner (default: on) --timeout TIMEOUT set the socket timeout (default: 15) + --path PATHS restrict to the specified installation path for + auditing packages; this option can be used multiple + times (default: []) ``` From 849aee728233d27fab54dfdb3103865a45859b47 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Fri, 3 Dec 2021 15:03:41 +1100 Subject: [PATCH 7/9] _dependency_source/pip: Add doc for paths arg --- pip_audit/_dependency_source/pip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pip_audit/_dependency_source/pip.py b/pip_audit/_dependency_source/pip.py index d75fc07d..051654a2 100644 --- a/pip_audit/_dependency_source/pip.py +++ b/pip_audit/_dependency_source/pip.py @@ -42,6 +42,10 @@ def __init__( `local` determines whether to do a "local-only" list. If `True`, the `DependencySource` does not expose globally installed packages. + `paths` is a list of locations to look for installed packages. If the + list is empty, the `DependencySource` will query the current Python + environment. + `state` is an optional `AuditState` to use for state callbacks. """ self._local = local From e94fd85993d5bd6891269f24e0707d0e06aeec15 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 3 Dec 2021 11:58:18 -0500 Subject: [PATCH 8/9] _cli: rename group --- pip_audit/_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 99ea7097..a54b18a8 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -143,7 +143,7 @@ def audit() -> None: description="audit the Python environment for dependencies with known vulnerabilities", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - req_source_args = parser.add_mutually_exclusive_group() + dep_source_args = parser.add_mutually_exclusive_group() parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") parser.add_argument( "-l", @@ -151,7 +151,7 @@ def audit() -> None: action="store_true", help="show only results for dependencies in the local environment", ) - req_source_args.add_argument( + dep_source_args.add_argument( "-r", "--requirement", type=argparse.FileType("r"), @@ -217,7 +217,7 @@ def audit() -> None: parser.add_argument( "--timeout", type=int, default=15, help="set the socket timeout" # Match the `pip` default ) - req_source_args.add_argument( + dep_source_args.add_argument( "--path", type=Path, action="append", From 20950309301a2a6663db233912675bbf7919d057 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 3 Dec 2021 12:00:09 -0500 Subject: [PATCH 9/9] CHANGELOG: record changes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b547bf9..9f93ecb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ All versions prior to 0.0.9 are untracked. ### Added +* CLI: The `--path ` flag has been added, allowing users to limit + dependency discovery to one or more paths (specified separately) + when `pip-audit` is invoked in environment mode + ([#148](https://github.com/trailofbits/pip-audit/pull/148)) + ### Changed ### Fixed