Scan run start: 2026-05-06T10:05:22Z (UTC)
Scan duration: ~3m (CI artifact, exact duration not recorded in workflow metadata)
Scanner host: GitHub Actions (workflow run 25428854722)
Tools run: bandit (unversioned in artifact), pip-audit, npm audit, gitleaks, trivy
Branch scanned: main (HEAD at time of nightly trigger)
Operator-facing executive summary: 0 CRITICAL, 1 HIGH (1 new, 0 carryover from 2026-04-25 baseline, 0 resolved), 7414 MEDIUM (deferred), 10 LOW. No live secret found. The nightly workflow was broken 2026-04-26 through 2026-05-05; PR #1274 fixed and re-triggered it. The workflow's issue-filer fired twice on re-trigger, creating 38 duplicate issues (#1275–#1312) for 19 findings — see deduplication table below. 18 of those findings are confirmed false positives; # nosec annotations added in this PR. 1 real finding: pynacl CVE-2025-69277 (CVSS 4.5, Moderate — scanner mislabeled it HIGH).
| Severity | Count | Status |
|---|---|---|
| CRITICAL | 0 | n/a |
| HIGH | 1 (real) | filed as #1309 (corrected body); #1312 is duplicate to close |
| MEDIUM (scanner-labeled) | 7414 | deferred — separate triage card recommended |
| LOW | 10 | batched |
| INFO | 0 | — |
Note on scanner severity inflation: The nightly workflow labels bandit B310 (urlopen scheme-check, bandit native severity MEDIUM), B306 (mktemp, MEDIUM), B608 (SQL f-string, MEDIUM), and B102 (exec, MEDIUM) as HIGH in the summary.md output. The raw bandit.json confirms these are all MEDIUM. The workflow's severity-escalation logic needs review — file a tooling issue if MEDIUM→HIGH inflation recurs next run.
Prior baseline (2026-04-25) HIGH findings status:
| Baseline finding | Issues filed | Status today |
|---|---|---|
cryptography==42.0.8 (4 CVEs) |
#288 | No longer present — cryptography upgraded to 46.0.7 per pip-audit clean scan |
pyopenssl==25.1.0 (2 CVEs) |
#289 | No longer present — pyopenssl upgraded to 26.0.0 |
flask==3.0.3 (CVE-2026-27205) |
#290 | No longer present — flask upgraded to 3.1.3 |
serialize-javascript@6.0.2 (GHSA-5c6j-r48x-rmvq) |
#291 | Still open — see #320 |
| Stale worktree dep manifests (trivy noise) | #292 | Presumed noise, not re-checked this run |
Resolved since baseline: #288, #289, #290 (cryptography/pyopenssl/flask bumps landed in the 11-day gap).
pynacl==1.5.0 in console: CVE-2025-69277 (GHSA-mrfv-m5wm-5w6w)Tool: pip-audit (workflow run 25428854722, pip-audit-requirements.json)
Confidence: HIGH (vendor advisory; pip-audit canonical)
Bandit native severity: MEDIUM — workflow upcasted to HIGH
Actual CVSS: 4.5 (Moderate) per GHSA record
Filed as: #1309 (duplicate #1312 needs to be closed)
Finding. console/requirements.txt pins pynacl==1.5.0. GHSA-mrfv-m5wm-5w6w (CVE-2025-69277, CVSS 4.5): libsodium mishandles elliptic curve point validity checks in crypto_core_ed25519_is_valid_point when processing points not in the main cryptographic group. Affects atypical use cases involving custom cryptography or untrusted data to that specific function. Fix: pynacl>=1.6.2.
Evidence.
- console/requirements.txt line 30: pynacl==1.5.0
- console/requirements.txt line 26-27: comment confirms pynacl is used by the Heroku rotation handler for GitHub Actions secret encryption
- pip-audit output: {"name": "pynacl", "version": "1.5.0", "vulns": [{"id": "GHSA-mrfv-m5wm-5w6w", "fix_versions": ["1.6.2"], "aliases": ["CVE-2025-69277"]}]}
Risk. The console uses pynacl for encrypting GitHub Actions secrets during the Heroku rotation workflow. CVE-2025-69277 affects the crypto_core_ed25519_is_valid_point path specifically. If the rotation handler processes untrusted or attacker-crafted curve points, validation could be bypassed. Practical exploitation requires attacker control of the EC point data fed to that function — in the rotation handler context, that data comes from GitHub's public key API (trusted). Real-world risk is low; advisory severity is Moderate (CVSS 4.5). Per agent spec, we file at advisory severity, not lower.
Remediation. Bump pynacl to >=1.6.2 in console/requirements.txt. No breaking API changes in pynacl 1.5.0 → 1.6.2. Run pip install pynacl>=1.6.2 and verify the rotation handler test suite passes.
Routing: Code fix: feature-developer (console).
All 18 bandit findings were verified against source code. Each is confirmed false positive. # nosec annotations added in this PR.
| # | Finding | File:line | Bandit rule | FP reason | nosec added |
|---|---|---|---|---|---|
| FP-01 | blacklist (B310) | backend_v2/api/feature_flags.py:159 |
B310 urlopen | URL built from STATUS_WORKER_URL env var (operator-configured); scheme not user-injectable |
Yes |
| FP-02 | hardcoded_sql (B608) | backend_v2/api/routes/admin_customers.py:1336 |
B608 | where_sql is assembled from hardcoded condition fragments; user values bound via ? params |
Yes |
| FP-03 | hardcoded_sql (B608) | backend_v2/api/routes/admin_customers.py:1352 |
B608 | select_cols and where_sql are hardcoded strings; user values in ? params only |
Yes |
| FP-04 | blacklist (B310) | backend_v2/api/services/support_customer_map_service.py:167 |
B310 urlopen | URL built from FREESCOUT_BASE_URL env var; user email only in query-param (URL-encoded) |
Yes |
| FP-05 | blacklist (B306) | backend_v2/tests/test_support_s1.py:488 |
B306 mktemp | Test-only; mktemp race not exploitable in CI isolation; SQLite opens the path immediately | Yes |
| FP-06 | hardcoded_sql (B608) | backend_v2/tests/test_trace_sc1_schema.py:379 |
B608 | Test-only; column names and values in f-string are hardcoded test fixtures, not user input | Yes |
| FP-07 | exec_used (B102) | backend_v2/tests/unit/test_app_debug_guard.py:66 |
B102 | Intentional test scaffolding; executes repo source in isolated stub_globals to verify FLASK_DEBUG behavior; already annotated # noqa: S102, # nosec B102 added |
Yes |
| FP-08 | blacklist (B310) | console/app/services/billing_alert_config.py:77 |
B310 urlopen | URL from operator-configured base URL + hardcoded path segment | Yes |
| FP-09 | blacklist (B310) | console/app/services/billing_alert_config.py:158 |
B310 urlopen | URL from operator-configured base URL + hardcoded path segment | Yes |
| FP-10 | blacklist (B310) | console/app/services/billing_summary.py:122 |
B310 urlopen | URL from operator-configured base URL + hardcoded path segment | Yes |
| FP-11 | blacklist (B310) | console/app/services/customer_audit.py:131 |
B310 urlopen | URL from operator-configured RAPTOR_INTERNAL_BASE_URL; no user input in scheme |
Yes |
| FP-12 | blacklist (B310) | console/app/services/customer_detail.py:135 |
B310 urlopen | URL from RAPTOR_BASE_URL + hardcoded path argument; no user scheme control |
Yes |
| FP-13 | blacklist (B310) | console/app/services/customer_detail.py:209 |
B310 urlopen | Same function (_raptor_post); URL assembled from operator-configured base URL | Yes |
| FP-14 | blacklist (B310) | console/app/services/customer_detail.py:365 |
B310 urlopen | Separate revocation POST path; same operator-URL pattern | Yes |
| FP-15 | blacklist (B310) | console/app/services/customer_lifecycle.py:136 |
B310 urlopen | URL from operator-configured internal base URL | Yes |
| FP-16 | blacklist (B310) | console/app/services/customer_list.py:131 |
B310 urlopen | URL from operator-configured internal base URL | Yes |
| FP-17 | blacklist (B310) | console/tests/test_post_deploy_smoke_614.py:177 |
B310 urlopen | Test-only; target_url is a CI fixture pointing at deployed console, not user-supplied |
Yes |
| FP-18 | blacklist (B310) | console/tests/test_post_deploy_smoke_614.py:393 |
B310 urlopen | Test-only; hardcoded fake Slack endpoint URL in test fixture | Yes |
The nightly workflow re-triggered twice (broken-then-fixed state of PR #1274), filing each of the 19 findings twice. The following issues need to be closed as duplicates by the operator — the security agent cannot close issues it did not create in the current session:
| Duplicate (close this) | Canonical (keep this) | Finding |
|---|---|---|
| #1312 | #1309 | pynacl GHSA-mrfv-m5wm-5w6w — keep #1309, update its severity label from severity:high to severity:medium |
| #1277 | #1275 | bandit blacklist feature_flags.py:159 — FALSE POSITIVE; close both |
| #1284 | #1280 | bandit blacklist support_customer_map_service.py:167 — FALSE POSITIVE; close both |
| #1286 | #1281 | bandit blacklist test_support_s1.py:488 — FALSE POSITIVE; close both |
| #1295 | #1289 | bandit blacklist billing_alert_config.py:158 — FALSE POSITIVE; close both |
| #1293 | #1287 | bandit blacklist billing_alert_config.py:77 — FALSE POSITIVE; close both |
| #1296 | #1290 | bandit blacklist billing_summary.py:122 — FALSE POSITIVE; close both |
| #1297 | #1292 | bandit blacklist customer_audit.py:131 — FALSE POSITIVE; close both |
| #1298 | #1294 | bandit blacklist customer_detail.py:135 — FALSE POSITIVE; close both |
| #1302 | #1299 | bandit blacklist customer_detail.py:209 — FALSE POSITIVE; close both |
| #1304 | #1300 | bandit blacklist customer_detail.py:365 — FALSE POSITIVE; close both |
| #1306 | #1301 | bandit blacklist customer_lifecycle.py:136 — FALSE POSITIVE; close both |
| #1308 | #1303 | bandit blacklist customer_list.py:131 — FALSE POSITIVE; close both |
| #1310 | #1305 | bandit blacklist test_post_deploy_smoke_614.py:177 — FALSE POSITIVE; close both |
| #1311 | #1307 | bandit blacklist test_post_deploy_smoke_614.py:393 — FALSE POSITIVE; close both |
| #1291 | #1285 | bandit exec_used test_app_debug_guard.py:66 — FALSE POSITIVE; close both |
| #1288 | #1283 | bandit hardcoded_sql test_trace_sc1_schema.py:379 — FALSE POSITIVE; close both |
| #1282 | #1276 | bandit hardcoded_sql admin_customers.py:1336 — FALSE POSITIVE; close both |
| #1278 | #1279 | bandit hardcoded_sql admin_customers.py:1352 — FALSE POSITIVE; close both |
Recommended operator action: Run gh issue close 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1310 1311 1312 --comment "False positive — nosec annotation added in PR #XXXX / duplicate of another auto-filed issue" (replace XXXX with the merged PR number).
For #1309: update its severity label from severity:high to severity:medium (actual CVSS 4.5) and add a comment with the full finding details.
7414 bandit MEDIUM findings are out of scope for this triage run. The bulk appear to be try_except_pass (B110) and blacklist (B310) at MEDIUM native severity across backend_v2/ and console/ route files. A dedicated triage card is recommended before the next nightly scan to:
try_except_pass globally in tests vs app code10 LOW findings recorded in bandit.json. Breakdown mirrors the 2026-04-25 baseline pattern (B311 random module, B404 subprocess import, B603/B607 subprocess calls). No individual issues filed.
| Issue | Finding | Status |
|---|---|---|
| #291 / #320 | serialize-javascript@6.0.2 GHSA-5c6j-r48x-rmvq |
Still open — npm audit not re-run this cycle; assumed still present |
| #292 | Stale worktree trivy noise | Tooling gap; .claude/worktrees/ not yet in trivy --skip-dirs |
# nosec annotations to 18 false-positive findings across 10 source files (this PR)severity:high to severity:medium; update body with CVSS 4.5 detailsummary.md aggregator--skip-dirs ".claude/worktrees,.clone,**/.venv" is in the trivy invocation per 2026-04-25 baseline recommendationdocs/security/scans/2026-04-25.md