Harden the audit trail: origin-only destination targets, atomic SSO provisioning row - #353
Merged
icebergai-review-bot[bot] merged 2 commits intoSep 2, 2026
Merged
Conversation
…rovisioning row Two findings from the review of #351, both against invariants that PR set. Destination create/update/delete recorded the raw target. A Slack or Teams incoming-webhook URL is a capability token — the path is the credential — and the trail is readable by auditors and retained forever, so it now records the target's origin (scheme + host) via alerts._audit_target and never the path or query. Non-URL targets (email recipients) are kept. JIT SSO provisioning committed the new user, then staged and committed its user.provision row separately; a crash between the two left a durable account with no trail entry, and the returning-user path never records it. The row is now staged right after the insert is flushed (so it carries the id) and lands in the same commit, on both the first attempt and the username-collision retry — the unique-violation rollback discards both, so exactly one row exists naming the final username. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XY2M1RbUVgATMDceLubuBz
Contributor
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Completed bounded review across 1 immutable scope(s). One security issue found in the new audit-target redaction.
Scope health
Convergence: healthy. Review mode: initial.
Recommended action: CONTINUE_INCREMENTAL.
- No escalation signals.
Prior findings
| Finding | Status |
|---|---|
| — | No prior finding state |
New findings
Root cause: Using netloc, which contains userinfo, instead of constructing the origin from hostname and optional port.
- BLOCKER · high: Audit redaction retains URL userinfo credentials —
app/routes/alerts.py:43
Status: NEW. Attribution: new_in_scope.
urlsplit().netlocincludes URL userinfo, so this is not an origin-only representation. A destination such ashttps://api-user:api-password@alerts.example.test/webhookis recorded ashttps://api-user:api-password@alerts.example.test. The credential is then retained in the audit trail and exposed to audit readers.
Invariant: Audit details must never include destination credentials and must record only scheme, host, and optional port.
Ownership: Alert destination audit-detail sanitization.. Behaviour: Audit entries for URL-based alert destinations..
Evidence:_audit_targetreturnsf"{parts.scheme}://{parts.netloc}"; create, update, and delete audit records store that result astarget.
Independent assessment:urlsplit(...).netlocretains URL userinfo._audit_targetwrites that netloc into audit records on create, update, and delete, sohttps://user:password@host/...exposes credentials in the retained audit trail.
Fix-induced regressions
- None evidenced.
Uncertainty
- No material uncertainty recorded.
Validation
- Reviewed the supplied immutable diff and associated tests.
- No prior findings were supplied; no rechecks are applicable.
Residual risks
- None identified.
Review finding on #353: urlsplit().netloc keeps a user:pass@ prefix, so _audit_target could still record a basic-auth credential embedded in a destination URL. The origin is now built from hostname and optional port (IPv6 brackets restored, a non-numeric port dropped rather than echoed), never from netloc. Covered by unit cases for userinfo, IPv6 and port handling plus an end-to-end route test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XY2M1RbUVgATMDceLubuBz
Contributor
There was a problem hiding this comment.
Verdict
APPROVE
Completed bounded review across 1 immutable scope(s). No remaining actionable findings. The supplied change removes the prior audit credential disclosure.
Scope health
Convergence: healthy. Review mode: delta.
Recommended action: CONTINUE_INCREMENTAL.
- No escalation signals.
Prior findings
| Finding | Status |
|---|---|
| F-5D83B294435B | Resolved |
New findings
No new findings.
Fix-induced regressions
- None evidenced.
Uncertainty
- No material uncertainty recorded.
Validation
- Prior blocker F-5D83B294435B resolved: The supplied tests assert userinfo-free output for hostname, port, and IPv6 URL forms, plus that credentials do not reach route-created audit rows.
- Reviewed the supplied immutable diff.
- Exact-head CI was reported as passed.
- The new
_audit_targetconstructs URL audit targets from hostname and optional port, excluding userinfo; tests cover userinfo, ports, IPv6, and route-level audit behavior.
Residual risks
- None identified.
icebergai-review-bot
Bot
deleted the
claude/dependabot-prs-review-raggzc
branch
September 2, 2026 03:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The review of #351 approved and merged it with two medium findings against invariants that PR itself set. Both are real; this fixes them. No linked issue — the parent issues (#33, #34) closed with #351.
1. Destination targets were recorded verbatim in audit detail (
alerts.py, FYI · medium). A Slack or Teams incoming-webhook URL is a capability token — the path is the credential — and the trail is readable by auditors and retained forever, including after the destination is deleted.destination.create/update/deletenow record the target's origin (scheme + host) via a new_audit_target()and never the path or query. Non-URL targets (email recipients) are not credentials and are kept as-is, so the trail still answers "where did alerts go".2.
user.provisionwas not atomic with the JIT insert (oidc/service.py, follow-up · medium). The insert committed, then the audit row was staged and committed separately — the one place #351 broke its own "row commits with the change" rule, done to survive the username-collision retry. A crash between the two commits would leave a durable account with no trail entry, and the returning-user path never records it. The row is now staged after the insert is flushed (so it carries the id) and lands in the same commit, on both the first attempt and the retry: the unique-violation rollback discards the user and the staged row together, so the retry starts clean and exactly one row exists naming the final username.Test evidence
Two new/extended tests, one per finding:
test_destination_audit_records_origin_not_capability_url— creates a Slack destination with targethttps://hooks.slack.com/services/T000/B000/SECRETTOKENxyz, renames and deletes it, then asserts all three rows recordtarget == "https://hooks.slack.com"and that neitherSECRETTOKENnor/services/appears anywhere in the stored detail. Also pins_audit_targeton an email-recipient list (unchanged) and a Jira URL with path + query (origin only).test_recovery_widens_username_suffix_on_collision(extended) — the existing retry test, which forces the first insert to collide on username, now also asserts that exactly oneuser.provisionrow exists and that it names the final (width-32) username and id — i.e. the first attempt's staged row was rolled back with the failed insert, not leaked.The existing
test_every_mutating_route_is_audited_or_allowlistedandtest_audit_detail_never_carries_a_secret_shaped_keyguards still pass; the second one doesn't catch atargetkey by design (it's not credential-shaped), which is why the origin reduction lives in code rather than a key rename.Checklist
CHANGELOG.md— the Audit log of mutating actions #34 entry's "detail never carries secrets" line now names the origin-only rule for destinations.CLAUDE.md— the audit-log detail-hygiene note records_audit_targetand why.app/templates/help.html— the audit-log section's secrets bullet mentions destinations.uv lock— n/a, no dependency change.🤖 Generated with Claude Code
https://claude.ai/code/session_01XY2M1RbUVgATMDceLubuBz
Generated by Claude Code