Nest Engineering Docs
Handbook

Python security scanning

Local Bandit workflow, report consumption, and dismissal policy.

Bandit is the local static security scanner for first-party Python code in this monorepo. The setup is intentionally local-only for now; there is no CI or workflow automation attached to it.

What Bandit scans

Run Bandit from the repository root. The root .bandit profile points Bandit at the Python code we own:

  • services
  • jobs
  • packages/python/common/src
  • db/scripts
  • scripts
  • apps/raven/tools/release

The shared pyproject.toml Bandit config excludes generated code, tests, virtualenvs, package caches, and build output. In particular, packages/python/nest_protos is generated protobuf/Connect code and should not be reviewed through Bandit.

Local commands

Use this command for the normal local review queue:

uvx bandit --ini .bandit --severity-level low --confidence-level high

That keeps low-severity findings visible while filtering to high-confidence results.

For a reusable report, generate Bandit's JSON and then render the local reports:

uvx bandit --ini .bandit -f json -o .bandit-results.json --exit-zero
uv run python scripts/summarize_bandit.py .bandit-results.json

The parser writes all supported local report formats next to the JSON file:

FilePurpose
.bandit-results.txtCompact terminal-style review summary
.bandit-results.mdMarkdown report for notes or comments
.bandit-results.htmlOffline visual report with grouped summaries and findings

To print the text report while generating all files:

uv run python scripts/summarize_bandit.py .bandit-results.json --print-text

To review lower-confidence findings as a secondary sweep:

uv run python scripts/summarize_bandit.py .bandit-results.json --min-confidence medium

Use --limit 0 when you want every matching finding in the generated reports.

How to triage findings

Treat Bandit findings as prompts for review, not as automatic proof of a vulnerability.

  1. Confirm whether the flagged line is reachable production/runtime code, developer tooling, one-shot migration code, or test-only code.
  2. Check whether the issue is a real vulnerability, a reliability concern, an accepted operational tradeoff, or a static-analysis false positive.
  3. Prefer a code change when it makes the intent safer or clearer.
  4. Use a targeted dismissal only after the finding has been reviewed.

Preferred fixes

Apply a code fix when the finding points at behavior that can be made safer without obscuring intent.

For B324 on MD5 used as a non-security checksum, make the non-security intent explicit:

digest = hashlib.md5(value.encode("utf-8"), usedforsecurity=False).hexdigest()

For B101 on runtime assert, use an explicit check and exception. Assertions can be removed by optimized Python execution.

if self._statsig is None:
    raise RuntimeError("Statsig client is not initialized")

For B310 on urllib.request.urlopen, validate the URL scheme and expected host before making the request. Bandit may still flag the call because the rule is call-based; if the guard is correct, a targeted dismissal can be appropriate.

Dismissal policy

Bandit supports native inline suppressions with # nosec. Use the specific rule ID and include a short reason.

with urllib.request.urlopen(  # nosec B310: validated HTTPS issuer URL
    discovery_url,
    timeout=5,
) as resp:
    payload = resp.read()

Use this policy for dismissals:

  • Prefer a code fix over a suppression.
  • Suppress only the specific rule ID, for example # nosec B101.
  • Add a short reason after the suppression.
  • Keep suppressions on the line that Bandit flags.
  • Do not use plain # nosec unless every Bandit finding on that line is intentionally accepted.
  • Do not suppress generated files; exclude generated paths in config instead.
  • Do not suppress test fixtures unless tests are intentionally included in a separate advisory scan.

Config-level skips and excludes

Bandit also supports config-level rule skips and path excludes.

Use path excludes for directories that should never be scanned, such as generated stubs, virtualenvs, caches, and build output.

Avoid global rule skips unless a rule is clearly not useful anywhere in this monorepo. A global skip such as this hides future legitimate findings:

[tool.bandit]
skips = ["B101"]

Prefer a targeted line-level # nosec BXXX: reason for reviewed false positives or accepted risks.

Baselines

Bandit supports JSON baselines:

uvx bandit --ini .bandit -f json -o bandit-baseline.json --exit-zero
uvx bandit --ini .bandit --baseline bandit-baseline.json

Use baselines only as a temporary migration tool when existing findings are being burned down. Do not treat a baseline as the long-term dismissal record. Inline suppressions are clearer because they keep the review decision next to the code.

What to commit

Commit source fixes, targeted # nosec BXXX: reason comments, and configuration changes when they are intentional.

Do not commit local report outputs:

  • .bandit-results.json
  • .bandit-results.txt
  • .bandit-results.md
  • .bandit-results.html

These files are ignored by git and should stay local review artifacts.

Last updated on

On this page