# PLAYBOOK — Remediating Python Findings

## Per-toolchain remediation patterns

### Plain requirements.txt + pip

```bash
# Edit requirements.txt to bump the pinned version
sed -i 's/requests==2.20.0/requests==2.31.0/' requirements.txt
pip install -r requirements.txt --upgrade
pytest                           # confirm no behavior regressions
git add requirements.txt && git commit -m "fix: bump requests to 2.31.0 (CVE-XXXX)"
```

If you have a separate `requirements-lock.txt` generated by
`pip-compile`:

```bash
pip-compile --upgrade-package requests requirements.in
```

### Poetry

```bash
poetry add requests@^2.31.0
poetry lock --no-update          # refresh just the affected entry
poetry install
pytest
git add pyproject.toml poetry.lock
```

For a transitive bump (a dep not declared directly):

```bash
poetry add requests@^2.31.0 --lock   # forces resolution even if not direct
```

Or use the `dependencies` table to pin transitive deps without
adding them as your project's direct deps.

### Pipenv

```bash
pipenv update requests
pipenv lock
pipenv install --deploy --ignore-pipfile     # smoke test
```

### uv (modern fast resolver)

```bash
uv pip install --upgrade requests
uv pip freeze > requirements.txt        # if you track frozen requirements
```

### conda environments

`pip-audit` does not natively audit conda packages, only pip
packages installed inside a conda environment. If a vulnerability
affects a conda-installed package, fix via `conda update <pkg>`
and document the manual remediation; pip-audit will not see the
finding go away on its own.

## Transitive dependency overrides

Python has no first-class `overrides` equivalent like npm 8.3+.
The patterns:

### Pin in requirements (works in pip)

If `boto3` pulls in `urllib3<2.0` and you need `urllib3>=2.0.2`:

```
urllib3>=2.0.2
boto3==1.34.0
```

pip's resolver prefers the more specific version. This sometimes
causes conflicts the resolver flags ("ResolutionImpossible"); when
that happens, the parent itself needs to be bumped.

### Poetry constraints

```toml
[tool.poetry.dependencies]
boto3 = "^1.34.0"
urllib3 = "^2.0.2"           # add as a direct dep to force the floor
```

This makes the transitive dep your direct dep, which is messy but
effective.

### Reset and re-resolve

When a transitive override produces conflicts, sometimes the cleanest
fix is to delete the lock file, bump everything, and re-resolve.
Reserve for major version refreshes or when you have a release window
to validate the full diff.

## Monorepo / workspace scanning

For monorepos that have multiple Python projects (often `services/*`
or `apps/*` layout):

```bash
for project in services/*/; do
    if [ -f "$project/pyproject.toml" ] || [ -f "$project/requirements.txt" ]; then
        python3 plugins/security/penetration-tester/skills/auditing-python-dependencies/scripts/audit_python.py \
            "$project" --format json --output "audit-$(basename $project).json"
    fi
done
```

The skill scans one project at a time by design — keeps the
per-project blast radius clear and lets CI parallelize.

## GitHub Dependabot ecosystem mapping

Dependabot's "pip" ecosystem auto-PRs requirements.txt and
pyproject.toml bumps. It does NOT audit poetry.lock — for that,
use the "poetry" ecosystem entry:

```yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: pip
    directory: /
    schedule:
      interval: weekly
  - package-ecosystem: pip            # poetry is reported as "pip" too in newer versions
    directory: /services/api
    schedule:
      interval: weekly
```

For uv, Dependabot support was added in 2025; check Dependabot's
docs for the current ecosystem name.

## SOC2 evidence retention

For Trust Service Category CC7 (System Operations):

```bash
mkdir -p evidence/CC7/
python3 ./scripts/audit_python.py . --include-dev \
    --format json \
    --output evidence/CC7/py-audit-$(date +%Y%m%d).json
```

Retain at minimum one audit per quarter, plus the audit run that
preceded each release. Auditors look for evidence that:

1. The audit ran on a defined cadence.
2. Findings were triaged within the window your vuln-response
   policy commits to (typically 7 days for HIGH, 30 days for
   MEDIUM, 90 days for LOW).
3. Exceptions are documented with re-evaluation dates.

## Common false positives

Three patterns produce findings that aren't actionable:

1. **Dev-only test fixtures pinning old versions** — e.g. tests
   that intentionally exercise an old `requests` to verify backward
   compat. Mark the source file as dev-only via `--include-dev`
   filtering or split test requirements into a separate file.

2. **Yanked releases on PyPI** — a release pulled by the maintainer
   may still have an open advisory while no longer installable.
   pip-audit handles yanked correctly in recent versions; older
   versions may still flag.

3. **CVEs that don't apply in your usage** — a vulnerability in the
   FTP transport of `urllib3` is irrelevant if you only use HTTPS.
   Don't blanket-suppress; document the per-finding rationale in
   your security register.

## When to vendor + patch

If a package has an unpatched CRITICAL/HIGH CVE and replacement
isn't feasible:

1. Fork the upstream source into a private vendored copy in your
   repo (e.g. `vendor/<package>/`).
2. Apply the upstream patch (cherry-pick from the maintainer's PR
   if one exists).
3. Install from your vendored path: `pip install ./vendor/<package>`.
4. Set a calendar reminder to re-evaluate quarterly; switch back to
   upstream once a published fix is available.

Vendor + patch is a maintenance burden; track it in your security
register the same way you'd track any other exception.
