chore(ci): add security-audit tooling scripts (workflow files pushed separately) - #414
Conversation
- scripts/prove_security.py, audit_plugins.py, generate_report.py -- automated checks (dangerous eval()/exec() calls, dependency scanning, report generation) for plugins. - .github/workflows/security-audit.yml + bandit.yaml -- CI wiring for the above plus gitleaks secret scanning and bandit static analysis. - .github/workflows/tests.yml -- pytest matrix across Python 3.10-3.12. Also fixes two Codacy findings while these files are freshly landing: - prove_security.py: dropped a pointless f-string prefix with no placeholders. - security-audit.yml: pinned gitleaks/gitleaks-action to a full commit SHA (matching this repo's existing pinning convention in test.yml) instead of the floating v2 tag. Split out of the original chore/dead-code-removal commit, which had accidentally bundled this in alongside unrelated dead-code deletions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
|
Warning Review limit reached
Next review available in: 44 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdded Bandit configuration, plugin security auditing, repository security proof checks, and a report generator that combines scan artifacts into a Markdown security report with aggregated critical findings. ChangesSecurity tooling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CI
participant prove_security
participant audit_plugins
participant ArtifactDirectory
participant generate_report
CI->>prove_security: Run repository security proofs
prove_security->>ArtifactDirectory: Write security-proofs-results.json
CI->>audit_plugins: Audit plugin directories
audit_plugins->>ArtifactDirectory: Write plugin-audit-results.json
CI->>generate_report: Generate report from scan artifacts
generate_report->>ArtifactDirectory: Load tool JSON results
generate_report-->>CI: Write combined Markdown report
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 251 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@bandit.yaml`:
- Around line 13-22: Remove the repo-wide B607 exclusion from the Bandit
configuration and narrow it to the reviewed subprocess call sites in api_v3.py,
wifi_manager.py, and the affected scripts, or replace the sensitive bare-name
invocations of systemctl, sudo, and git with absolute executable paths. Keep
B603 suppression unchanged.
- Around line 30-33: Remove scripts/prove_security.py from the file-level
exclusion list in bandit.yaml so Bandit scans it normally. Add targeted # nosec
annotations only to the specific detection-pattern lines that produce false
positives, leaving all other findings enabled.
In `@scripts/audit_plugins.py`:
- Around line 169-186: Update the exception handling in scripts/audit_plugins.py
at lines 169-186 to classify SyntaxError and OSError findings as blocking scan
errors rather than WARNING or INFO results. Update the handling in
scripts/prove_security.py at lines 114-124 to preserve and propagate these
failures as CRITICAL or infrastructure-error results instead of discarding them;
use the existing Finding and security-result symbols and retain the captured
exception details.
- Around line 216-235: Update the plugin traversal around audit_plugin and
plugins_scanned to track whether the requested args.plugin was found; after all
PLUGIN_BASE_DIRS have been scanned, reject an unmatched plugin name with a clear
error and non-success exit status, while preserving normal behavior when no
plugin filter is provided or the plugin exists.
- Around line 70-107: Update the audit visitor’s import tracking and call-target
normalization so dangerous API checks cover aliased imports and from-imports,
including subprocess.run, os.system, and builtins.eval forms. Add alias handling
in visit_Import and visit_ImportFrom, then reuse the normalized targets in
visit_Call while preserving the existing PLUGIN-001 through PLUGIN-005
classifications and shell=True validation.
In `@scripts/generate_report.py`:
- Around line 58-59: Update _md_table_row() to sanitize each cell before joining
the row: escape pipe characters and normalize newline characters so
scanner-controlled content cannot alter Markdown table structure or finding
boundaries. Preserve the existing row formatting and support all cell values
handled by the current str conversion.
- Around line 50-55: Update _load and the report aggregation flow to distinguish
missing or malformed scan artifacts from valid empty results. Track each
unavailable tool, report INCOMPLETE instead of PASSED, return a non-zero exit
code, and provide a clear error message identifying the unavailable or invalid
artifact while preserving normal results for valid scans.
- Around line 136-143: Update the finding filter in the report-generation flow
to remove broad substring checks against _GITLEAKS_SUPPRESS. Suppress only exact
known placeholder values or findings whose paths match explicitly approved
template paths, while preserving genuine credentials in real_findings and the
critical count.
In `@scripts/prove_security.py`:
- Around line 417-424: Update the base-image validation loop in the FROM parsing
logic to require an immutable `@sha256`: digest in every image reference. Continue
reporting missing or latest tags as unpinned, and reject tag-only references
such as python:3.12 while allowing valid digest-pinned images.
- Around line 146-185: Update test_t2a_api_surface_inventory so unauthenticated
access with disabled CSRF is not reported as INFO by default. Add
environment-aware validation of the deployment boundary, using the project’s
configuration mechanism to recognize when local-only access is explicitly
enforced; return at least WARNING when that enforcement is absent, while
preserving the existing route inventory and documentation checks.
- Around line 74-89: Replace the file-wide substring checks in
scripts/prove_security.py at lines 74-89 with validation that confirms the
is_relative_to() guard and “Zip-slip detected” handling directly protect the
relevant zipfile.extractall() operation. Update the extractall() inspection at
lines 324-344 to evaluate each extraction call independently and establish that
its preceding validation path enforces the guard; preserve the existing
TestResult outcomes and severity semantics.
- Around line 228-251: Update the violation construction in the secret-scanning
loop over _SECRET_PATTERNS so result details never include line_content or the
matched credential. Report only the relative file, line number, pattern
type/severity, and a redacted fingerprint derived from the match, while
preserving the existing critical and warning aggregation in the T3a result
handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fc66f736-6213-4364-bfd0-3a8152d99a10
📒 Files selected for processing (4)
bandit.yamlscripts/audit_plugins.pyscripts/generate_report.pyscripts/prove_security.py
… audit_plugins.py, generate_report.py, prove_security.py bandit.yaml: - Removed scripts/prove_security.py's file-level exclusion. Ran bandit directly to get ground truth: the real false positive is B105 (dict key "PASS" misread as password-like), not the eval/exec pattern the old comment claimed. Added a targeted # nosec B105 there, and found+fixed the identical pattern already present in generate_report.py. - Left the repo-wide B607 skip as-is: confirmed via AST scan that properly narrowing it touches 100+ bare-name subprocess call sites across wifi_manager.py, store_manager.py, permission_utils.py, app.py, and start.py -- none of which are part of this PR. Out of proportion to fix here; flagged as a dedicated follow-up. scripts/audit_plugins.py: - SyntaxError/OSError while scanning a plugin file now report CRITICAL (blocking) instead of WARNING/INFO -- a file that couldn't be parsed or read was never actually checked for danger, so it must not silently pass the audit. - --plugin <name> now tracks whether the requested plugin was found across all PLUGIN_BASE_DIRS and exits 1 with a clear error if not, instead of silently scanning zero plugins and reporting success. - The AST visitor now tracks import aliases (import subprocess as sp; from builtins import eval as e) and resolves them before checking against dangerous APIs, closing a straightforward evasion of every PLUGIN-001 through PLUGIN-005 check. Verified against both aliased and unaliased evasion patterns. scripts/generate_report.py: - _md_table_row now escapes pipe characters and normalizes newlines in every cell, so scanner-controlled content (a matched secret, a bandit issue_text) can't corrupt the Markdown table structure. - _load now distinguishes "artifact missing/malformed" from "valid empty result": each summarizer returns an availability flag, and main() now reports INCOMPLETE (not PASSED) with exit code 1 when any artifact is unavailable, instead of silently folding it in as 0 findings. - Gitleaks suppression now uses exact-match placeholder values (pulled from the actual config_secrets.template.json) plus a template-path allowlist, replacing broad substring checks that could hide a real secret containing something like "example.com" as part of its value. scripts/prove_security.py: - T1b (dangerous plugin calls): a file that fails to parse/read now reports CRITICAL with the exception details instead of being silently swallowed by `except (SyntaxError, OSError): pass`. - T6 (Docker hardening): base images must now be pinned to an @sha256 digest; a specific tag like python:3.12 is mutable and is now correctly flagged as unpinned, not just missing tags or :latest. - T2a (API surface): no config mechanism for enforcing local-only access exists in this codebase today (app.py hardcodes host='0.0.0.0'), so the "environment-aware" check as described isn't buildable without adding new config infrastructure -- out of scope here. Applied the achievable part: upgraded from INFO to WARNING, since enforcement can never currently be confirmed. - T1a (zip-slip): replaced the whole-file substring check with an AST walk that finds every extract()/extractall() call and confirms an is_relative_to() guard + "Zip-slip detected" log precede it in the same function. Verified it still passes on the real store_manager.py (both the per-member and validate-then-bulk-extract call sites) and correctly flags a synthetic unguarded extractall(). - T3a (hardcoded secrets): violation details no longer include the matched credential text -- only file, line, pattern type, and a redacted SHA-256 fingerprint, so a real finding doesn't get published into CI logs/artifacts/PR comments with wider exposure than the original leak. Verified with a synthetic secret that no raw content reaches the output. Validated: all four files compile; bandit scans all three scripts clean (2 legitimate targeted suppressions, 0 unaddressed findings); each new/ changed code path exercised directly (alias evasion, unmatched --plugin, missing/malformed/valid-empty artifacts, digest-pinning, zip-slip guard/no-guard, secret redaction); full audit_plugins.py -> prove_security.py -> generate_report.py pipeline run end-to-end producing a correct report. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/audit_plugins.py (1)
80-117: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winSupport
from-importsforsubprocessandos.system.The current implementation only correctly handles direct names for
eval/execand simple aliased imports (likeimport subprocess as sp) forsubprocessandos. It continues to missfrom-importstatements (e.g.,from subprocess import run) because the target becomes anast.Name, which skips theisinstance(node.func, ast.Attribute)block entirely.Consolidate the call path resolution to uniformly check both imported names and attribute accesses against all target rules.
🛠️ Proposed fix to unify target resolution
- # eval() / exec() / compile() — arbitrary code execution, including - # aliased or from-imported forms (from builtins import eval as e; e(...)) - if isinstance(node.func, ast.Name): - target = self._resolve(node.func.id).rsplit(".", 1)[-1] - if target == "eval": - self._add(node, "CRITICAL", "PLUGIN-001", - "eval() call — arbitrary code execution risk") - elif target == "exec": - self._add(node, "CRITICAL", "PLUGIN-002", - "exec() call — arbitrary code execution risk") - elif target == "compile": - self._add(node, "WARNING", "PLUGIN-003", - "compile() call — dynamic code compilation") - - # subprocess.*(shell=True) / os.system(), including aliased imports - # (import subprocess as sp; import os as o) - if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name): - base = self._resolve(node.func.value.id) - - is_subprocess = ( - base == "subprocess" and - node.func.attr in ("run", "call", "Popen", "check_call", "check_output") - ) - if is_subprocess: - for kw in node.keywords: - if (kw.arg == "shell" and - isinstance(kw.value, ast.Constant) and - kw.value.value is True): - self._add(node, "WARNING", "PLUGIN-004", - f"subprocess.{node.func.attr}(shell=True) — " - f"shell injection risk if args include user input") - - # os.system() — shell execution - is_os_system = base == "os" and node.func.attr == "system" - if is_os_system: - self._add(node, "WARNING", "PLUGIN-005", - "os.system() call — prefer subprocess with list args") + target_path = "" + if isinstance(node.func, ast.Name): + target_path = self._resolve(node.func.id) + elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name): + target_path = f"{self._resolve(node.func.value.id)}.{node.func.attr}" + + func_name = target_path.rsplit(".", 1)[-1] + + # eval() / exec() / compile() — arbitrary code execution + if func_name == "eval": + self._add(node, "CRITICAL", "PLUGIN-001", + "eval() call — arbitrary code execution risk") + elif func_name == "exec": + self._add(node, "CRITICAL", "PLUGIN-002", + "exec() call — arbitrary code execution risk") + elif func_name == "compile": + self._add(node, "WARNING", "PLUGIN-003", + "compile() call — dynamic code compilation") + + # subprocess.*(shell=True) / os.system() + is_subprocess = target_path.startswith("subprocess.") and func_name in ( + "run", "call", "Popen", "check_call", "check_output" + ) + if is_subprocess: + for kw in node.keywords: + if (kw.arg == "shell" and + isinstance(kw.value, ast.Constant) and + kw.value.value is True): + self._add(node, "WARNING", "PLUGIN-004", + f"{target_path}(shell=True) — " + f"shell injection risk if args include user input") + + if target_path == "os.system": + self._add(node, "WARNING", "PLUGIN-005", + "os.system() call — prefer subprocess with list args")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/audit_plugins.py` around lines 80 - 117, Update visit_Call to resolve both ast.Name and ast.Attribute callees through the existing import-resolution mechanism, so from-imported subprocess functions and os.system aliases are evaluated alongside direct and module-attribute calls. Consolidate rule matching around the resolved target while preserving the existing PLUGIN-001 through PLUGIN-005 classifications and shell=True requirement.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/generate_report.py`:
- Line 74: Update the value parameter annotation in _md_sanitize_cell to specify
the expected input type, while preserving its existing return type and
sanitization behavior.
In `@scripts/prove_security.py`:
- Around line 503-509: Update the FROM-line parsing around from_lines to rename
the comprehension variable l to line, then resolve the image token after any
optional --platform= flag before checking for `@sha256`:. Preserve the existing
issue reporting and ensure platform flags do not trigger false positives.
---
Outside diff comments:
In `@scripts/audit_plugins.py`:
- Around line 80-117: Update visit_Call to resolve both ast.Name and
ast.Attribute callees through the existing import-resolution mechanism, so
from-imported subprocess functions and os.system aliases are evaluated alongside
direct and module-attribute calls. Consolidate rule matching around the resolved
target while preserving the existing PLUGIN-001 through PLUGIN-005
classifications and shell=True requirement.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7a6d04b6-e6b8-48c1-9a30-f52a03e8f929
📒 Files selected for processing (4)
bandit.yamlscripts/audit_plugins.pyscripts/generate_report.pyscripts/prove_security.py
💤 Files with no reviewable changes (1)
- bandit.yaml
scripts/generate_report.py: - Added an explicit `object` type annotation to _md_sanitize_cell's value parameter -- it deliberately accepts any stringifiable value (calls str(value) unconditionally), so `object` reflects its actual contract more accurately than leaving it untyped. scripts/prove_security.py: - Dockerfile FROM-line parsing: renamed the comprehension variable `l` to `line` (ambiguous single-letter name). More importantly, fixed a real false-positive: `FROM --platform=<platform> <image>` was reading the --platform= flag itself as the image token, so a properly digest-pinned image behind a platform flag was incorrectly reported as unpinned. Verified against platform+digest, platform+tag-only, and digest+AS-alias Dockerfiles. scripts/audit_plugins.py: - Consolidated visit_Call's dangerous-API detection: previously, alias resolution only covered ast.Name calls for eval/exec/compile and ast.Attribute calls for subprocess/os.system, missing from-imported subprocess/os functions called as bare names (from subprocess import run as prun; prun(cmd, shell=True) or from os import system as s; s(cmd)). Added _resolve_call_target() to resolve both call shapes to a single fully-qualified target, then run all five PLUGIN-00x checks against that one resolved value. Verified against 10 evasion combinations (from-import aliases, direct/attribute calls, aliased module imports) and confirmed zero false positives on benign os/ subprocess usage without shell=True. Validated: all three files compile, bandit scans clean (same 2 legitimate suppressions as before, 0 new findings), audit_plugins.py/prove_security.py re-run against the real repo with no regressions from the prior fix pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Summary
Split out of PR #412 (
chore/dead-code-removal), which had accidentally bundled this in alongside unrelated dead-code deletions.scripts/prove_security.py,audit_plugins.py,generate_report.py-- automated checks (dangerous eval()/exec() calls, dependency scanning, report generation) for plugins.bandit.yaml-- bandit static-analysis config..github/workflows/security-audit.ymland.github/workflows/tests.yml-- my git credentials here only havereposcope, notworkflow, which GitHub requires to push changes under.github/workflows/. Those two files still need to be added (I have them ready if you want the diff, or can push once scope is granted).Fixes 1 Codacy finding while these files are freshly landing:
prove_security.py: dropped a pointless f-string prefix (no placeholders).security-audit.yml, not yet pushed -- still needs applying when that file lands.)Test plan
python3 -m py_compile scripts/*.pypassesworkflowscope is available🤖 Generated with Claude Code
https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Summary by CodeRabbit