Skip to content

fix(security): v0.40.0 — path-boundary scope, more secret redaction, ingestion bounds, run-executor truth, provider errors#126

Merged
hxddh merged 1 commit into
mainfrom
claude/architecture-code-docs-review-k41dn6
Jul 21, 2026
Merged

fix(security): v0.40.0 — path-boundary scope, more secret redaction, ingestion bounds, run-executor truth, provider errors#126
hxddh merged 1 commit into
mainfrom
claude/architecture-code-docs-review-k41dn6

Conversation

@hxddh

@hxddh hxddh commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Adversarial mining-round hardening across the security floor, ingestion, run executors, and provider-credential handling.

Security

  • Path-boundary prefix scopecheck_scope matched prefixes with a raw startswith, so allowed_prefixes=["logs"] admitted logs-private/secret (a different top-level path). It now matches only an exact equal or a /-delimited child, and drops empty-string prefix entries so a stray "" can't silently unrestrict the bucket.
  • Three more secret shapes redacted in logs/reports/traces/UI: Azure Blob SAS sig= HMAC params, temporary-credential session tokens (FQoG/FwoG/IQoJ…), and private_key = <value> assignments — labels kept, values masked.
  • The agent's appended provider block (name + endpoint URL, added after build_session_context and thus outside assert_no_secrets_in_context) now routes both fields through redact_text.

Ingestion bounds

  • _nonempty_lines reads via a bounded readline(_MAX_LINE_CHARS) loop with a cumulative-bytes ceiling — a single multi-GiB line can no longer be slurped whole into one Python string.
  • Free-text group-by labels clipped to _LABEL_LEN; finding cells HTML-escaped in both report writers; future-dated objects land in unknown age (not 0-7d).
  • Per-run DuckDB gets a best-effort memory_limit/threads ceiling; a managed evidence combine enforces a cumulative decompressed-output budget across all parts (decompression-bomb defense in depth).

Run executors

  • recent_run_ids_for_provider joins on runs.status = 'completed' so a crashed/partial survey isn't read back as newest.
  • Per-bucket persistence rolls back on failure with a warning finding instead of aborting the survey.
  • Report generation in diagnostic / config-review / account-discovery wrapped in require_success.
  • Region-mismatch HeadBucket (301/PermanentRedirect) surfaces a distinct region_mismatch status.

Provider credentials

  • Vault .tmp is fchmod'd to 0600 before ciphertext is written (a leftover .tmp can't be briefly group/world-readable).
  • Malformed credential refs raise CredentialResolutionError instead of a raw 500.

How to run

  • cd sidecar && pytest -q
  • cd frontend && npm run build && npx tsc --noEmit && npx vitest run

Checks run

  • sidecar: 726 passed, ruff check app clean (new tests/test_v0400_fixes.py, 16 cases).
  • frontend: tsc --noEmit clean, npm run build clean, vitest 17 passed.

Security invariants

No generic shell, no plaintext secrets, no destructive/mutating S3 op; secrets resolved only server-side and never placed in an LLM prompt; migration SQL append-only. This PR tightens the redaction and scope floor rather than relaxing it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M3YojFPzjkfMj6YYSwvzdx


Generated by Claude Code

…ingestion bounds, run-executor truth, provider errors

Adversarial mining round hardening across the security floor, ingestion,
run executors, and provider-credential handling.

Security:
- check_scope matches prefixes at a path boundary (logs never admits
  logs-private/…) and drops empty-string prefix entries.
- Redact Azure SAS sig=, temporary session tokens (FQoG/FwoG/IQoJ), and
  private_key = <value> assignments.
- Route the agent's appended provider name/endpoint through redact_text.

Ingestion:
- Bounded readline loop + cumulative-bytes ceiling in _nonempty_lines.
- Clip free-text group-by labels; HTML-escape finding cells in both
  report writers; future-dated objects → unknown age bucket.
- DuckDB memory_limit/threads ceiling; cumulative decompression-output
  budget across all combine parts.

Run executors:
- recent_run_ids_for_provider returns only completed surveys; per-bucket
  persistence rolls back on failure with a warning finding; report
  generation wrapped in require_success; region-mismatch HeadBucket
  surfaces a distinct region_mismatch status.

Provider credentials:
- fchmod the vault .tmp to 0600 before writing ciphertext; malformed
  credential refs raise CredentialResolutionError instead of a raw 500.

Tests: tests/test_v0400_fixes.py (16 cases). Full suite 726 passed,
ruff clean, frontend tsc/build/vitest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M3YojFPzjkfMj6YYSwvzdx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 721f85c405

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# degrades to bounded junk rows (the parsed-fraction truth guard flags them),
# never an OOM.
_MAX_LINE_CHARS = 1 << 20 # 1 MiB per line
_MAX_TOTAL_CHARS = 3 << 30 # ~3 GiB total (above the 2 GiB upload cap, below OOM)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep access-log byte caps below resident memory

Because _nonempty_lines() still returns a materialized list[str], this 3 GiB ceiling allows a max-size access-log upload to be accumulated in RAM before parsing; for example, a 2 GiB single-line file becomes ~2048 one-MiB chunks appended to out in import_access_logs, so the sidecar can still OOM despite the new bounded readline() loop. The cap needs to be a real in-memory budget or the parser needs to stream chunks instead of collecting them all.

Useful? React with 👍 / 👎.

Comment on lines +268 to +272
except Exception as exc: # noqa: BLE001 - per-bucket persistence isolation
conn.rollback()
bus.publish(run_id, {"type": "finding", "severity": "warning",
"title": f"Bucket {name}: persistence error",
"detail": redact_text(str(exc))})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not complete surveys after bucket persistence fails

When one of these inserts fails, the rollback removes that bucket from the persisted snapshot but the run continues, later marks the survey completed, and builds the summary/report from per_bucket as if the bucket were saved. In that scenario recent_run_ids_for_provider() will treat this completed but partially persisted survey as authoritative, so query_account_profile and diffs can report missing buckets as removed or omit posture findings; either the snapshot counts should reflect only successfully persisted buckets or the run should not complete as a valid survey.

Useful? React with 👍 / 👎.

-- A future last_modified (clock skew, or a garbage 9999 date) yields a NEGATIVE
-- age; without this guard it satisfies '<= 7' and is mis-bucketed as freshly
-- modified ('0-7d'). Treat future-dated objects as 'unknown', not recent.
WHEN datediff('day', try_cast(last_modified AS TIMESTAMP), current_timestamp) < 0 THEN 'unknown'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include future timestamps in the unknown-age ratio

This branch now classifies future last_modified values as the unknown age bucket, but the unknown_age_ratio query below still counts only unparseable timestamps. For inventories where many rows have future dates, the distribution shows unknown while the warning threshold sees 0%, so the report can say age analysis is fine even though those rows were explicitly treated as unknown; include the same future-date predicate in the unknown_age count.

Useful? React with 👍 / 👎.

@hxddh
hxddh merged commit 156ac35 into main Jul 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants