Skip to content

Commit

Permalink
pip, _cli: Add --path argument to mirror pip list (#148)
Browse files Browse the repository at this point in the history
* pip, _cli: Add `--path` argument to mirror `pip list`

* _dependency_source/pip: Switch to interface that uses `os.PathLike`

* _cli: Avoid `None` checks by using an empty list as default

* _cli: Use mutually exclusive group for -r and --path args

* _dependency_source, setup: Bump `pip-api` version and fix typing

* README: Update help text

* _dependency_source/pip: Add doc for paths arg

* _cli: rename group

* CHANGELOG: record changes

Co-authored-by: William Woodruff <[email protected]>
  • Loading branch information
tetsuo-cpp and woodruffw authored Dec 3, 2021
1 parent 9b0afe4 commit a0fe40b
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ All versions prior to 0.0.9 are untracked.

### Added

* CLI: The `--path <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
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: [])
```
<!-- @end-pip-audit-help@ -->

Expand Down
14 changes: 12 additions & 2 deletions pip_audit/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@ def audit() -> None:
description="audit the Python environment for dependencies with known vulnerabilities",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
dep_source_args = parser.add_mutually_exclusive_group()
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument(
"-l",
"--local",
action="store_true",
help="show only results for dependencies in the local environment",
)
parser.add_argument(
dep_source_args.add_argument(
"-r",
"--requirement",
type=argparse.FileType("r"),
Expand Down Expand Up @@ -216,6 +217,15 @@ def audit() -> None:
parser.add_argument(
"--timeout", type=int, default=15, help="set the socket timeout" # Match the `pip` default
)
dep_source_args.add_argument(
"--path",
type=Path,
action="append",
dest="paths",
default=[],
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}")
Expand All @@ -232,7 +242,7 @@ def audit() -> None:
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)

auditor = Auditor(service, options=AuditOptions(dry_run=args.dry_run))

Expand Down
16 changes: 13 additions & 3 deletions pip_audit/_dependency_source/pip.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"""

import logging
from typing import Iterator, Optional
from pathlib import Path
from typing import Iterator, Optional, Sequence

import pip_api
from packaging.version import InvalidVersion, Version
Expand Down Expand Up @@ -32,16 +33,23 @@ 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: Sequence[Path] = [], state: Optional[AuditState] = None
) -> None:
"""
Create a new `PipSource`.
`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
self._paths = paths
self.state = state

if _PIP_VERSION < _MINIMUM_RELIABLE_PIP_VERSION:
Expand All @@ -61,7 +69,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=list(self._paths)
).items():
dep: Dependency
try:
dep = ResolvedDependency(name=dist.name, version=Version(str(dist.version)))
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
Expand Down
7 changes: 5 additions & 2 deletions test/dependency_source/test_pip.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"),
Expand Down

0 comments on commit a0fe40b

Please sign in to comment.