Repository navigation
fix(security): classify redaction by what a value can do (awaiting design approval) - #22887
bolichen97 wants to merge 1 commit into
Conversation
Sensitive Change ReviewSensitive files changed:
How to clear this check:
Only the author's comment counts this way. A reviewer can instead |
First Principles Review (Opus 5.5) — 🔴 BLOCKPremise-level review of First-Principles-Verdict: BLOCK Product-shape change without accepted RFC: AWS key IDs now stay visible in chat, channels, logs and links, and the entropy guess is removed. The only redaction RFC on base, a Notes
What this change shipsInventory (8 items) — 6 justifiedIntent: let investigators read logs and transcripts while every real secret is still redacted. This is a FIX that also changes defaults.
Blockersproduct-shape change without accepted RFC — key IDs and entropy (items 1, 2) [FIRST-PRINCIPLES-REVIEWED] a5358d7 Description read[DESCRIPTION-READ] 8dc2246e52ce11038832e29a3e7ecbe36611298e46a4af3c3e0f7994885fbdd6 That sha256 names the PR description this verdict read. Recompute it with |
Opus 5.5 Review — 🔴 changes requested (blocking)Reviewed The streaming redactor this PR adds leaks labelled secrets that batch BLOCKING — src/kiro_crew/security/text_redaction.py:941 BLOCKING — src/kiro_crew/security/text_redaction.py:625 BLOCKING — src/kiro_crew/security/text_redaction.py:250 BLOCKING — src/kiro_crew/credential_values.py:486 [BLOCK-MERGE] a5358d7 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 6.1 Review — 🔴 changes requested (blocking)GPT 6.1 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/credential_values.py:740 -- Later quoted fields expose a folded YAML password
BLOCKING -- src/kiro_crew/credential_patterns.py:245 -- Triple-quoted passwords starting with a newline bypass redaction
[BLOCK-MERGE] a5358d7 Adjudication (Opus 5.5) — is blocking on each finding proportionate?F1 — UNBOUNDED harm (the credential reaches the dashboard and logs, and nothing notices). Reproduced through F2 — UNBOUNDED harm. Reproduced: False positive or not applicable? A repository writer can comment: |
Security Scope Review -- 🔴 could not complete (fails closed)No scope verdict for |
aadfe69 to
ca60662
Compare
|
|
|
|
|
|
|
|
ca60662 to
6a1aab9
Compare
|
|
|
GPT 6.1 findings on F1: plain words behind a strong label were exempt. In
A plain word is redacted at any length or case ( F3: config values stopped at
Behind a strong label only an exact
Tests updated to the decided design:
Battery: 27,130 passed. The remaining failures were the line budget (updated to 34,318) and those two fixtures. Web: 853 passed. |
|
F1 and F3 on F1: a plain word behind a strong label. Behind a strong secret label (
A plain word ( F3: config-line values stopped at Batch, log and stream agree. All three use Tests (red on the previous heads, green here):
Tests updated to the design:
Battery: 27,142 passed. Web: 853. |
|
The GPT 6.1 finding on
FP passed on |
|
GPT 6.1 findings on
Opus on |
|
The GPT 6.1 and Opus 5.5 findings on
Battery: 27,187 passed. Web: 856 passed. |
|
GPT 6.1 findings on
The regex-linearity shape test now reads |
|
GPT 6.1 findings on
Opus on |
|
[operator: bolichen97] Disposition for GPT 6.1 on 67f7051, fixed on 5a79d39. F1 (credential_patterns.py:186), punctuation-prefixed config passwords: fixed.
Verification: backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass; full battery 27,237 passed. F1/F3 tests remain on the head: |
|
[operator: bolichen97] Disposition for GPT 6.1 and Opus 5.5 on 5a79d39, fixed on 132a1f1. GPT F1 (redaction_labels.py:942), block scalars in JSON-escaped strings: fixed. GPT F2 (credential_values.py:327), GPT F3 (scan.py:340), inline images: fixed without dropping the canonical-mode bare-secret scan. Removing that scan is the guard removal Opus flagged earlier. Instead, a run is exempt only when it follows a Opus, credential_patterns.py:133:
Opus, stream_aws_anchor.py:47: fixed.
Opus note, Opus, deploy/scan.py:186 (label-only records are hard and non-overridable): not changed in this push. It changes a deploy gate's severity and is referred to the design decision. First Principles BLOCK (product-shape, no accepted RFC): the decision is recorded in #22577 and approved on #22552. The owner-authorized override will be posted on the final head. Verification: backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. Full battery: 27,285 passed, plus one facade-export test, now fixed: the packaging suite passes 782. The F1/F3 tests remain on the head. |
|
[operator: bolichen97] Disposition for GPT 6.1 and Opus 5.5 on 132a1f1, plus Opus's q1/q2 asks from 5a79d39. Fixed on 0bae467. (89e0dd1 was pushed with one facade-manifest entry missing; it is replaced by this head.) GPT F1 (credential_values.py:399), padded INI keys: fixed. Inside an INI section, GPT F2 (scan.py:378), public digests: fixed without removing the canonical-mode bare-secret scan. That scan is a guard, as Opus showed earlier. A run that is exactly the shape of a base64 SHA-256 (43 characters + GPT F3 (credential_values.py:508), YAML null and boolean values: fixed. In the YAML form only ( Opus (redaction_decode.py:249), binary decodes in Opus q1, plain words behind a strong label: changed in part.
Opus q2 (deploy/scan.py:186), severity: severity is unchanged. The deploy scan already reads the shared predicate ( Verification: backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. Full battery: 27,319 passed. The F1/F3 tests remain on the head. |
|
[operator: bolichen97] Disposition for GPT 6.1 on 0bae467 and Opus 5.5 on 89e0dd1. Fixed on eb690bf. GPT F1 (redaction_labels.py:180), Python string prefixes: fixed. GPT F2 (redaction_decode.py:49), short URL-safe base64: fixed. GPT F3 (text_redaction.py:939), continued config values past the hold: fixed. A config value past its hold reach drops as GPT F4 (scan.py:324), bare-secret snippet: fixed. The finding shows only GPT F5 (text_redaction.py:1028), clamped block indent: fixed. A Opus 1 (text_redaction.py:1175), a long base64 run's tail: fixed. The stream records how far into its last 4-character group a commit left a base64 run ( Opus 2 (redaction_patterns.py:63), AKIA key IDs and bare 40-character secrets: by design, not changed. That is the tier model recorded in #22577 and approved on #22552. A key ID is an Identifier and stays visible. Every Secret-tier value is still redacted: one with a key ID beside it, a label or a prefix. The entropy guess for a value with none of these is removed. First Principles' product-shape item covers this, and the owner's override is posted on the final head. Opus 3 (secret_catalog.py:152), cost on large output: fixed.
Opus 4 (deploy/scan.py:219), severity: not changed. Downgrading a deploy gate is outside this PR. The deploy scan reads the shared predicate, so code shapes ( Opus note, Verification: the backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. The full battery passed 27,349, and its one failure was the package line budget, now raised to the measured 34,677 and passing. The F1/F3 tests remain on the head. |
|
[operator: bolichen97] Disposition for CI, CodeQL, GPT 6.1 and Opus 5.5 on eb690bf. Fixed on ec63898. CI Backend Tests (3.12 shards 1, 3, 5, 8; Windows shards 1, 3, 5, 8): fixed. All six failing tests pass on fresh main and fail on eb690bf, so they are regressions from this branch.
CodeQL: 7 new high alerts (
GPT F1 (redaction_normalise.py:73), GPT F2 (text_redaction.py:116), long CSI sequences: fixed. Opus (redaction_labels.py:91), Verification: the backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. The local battery passed 27,365. The six CI-failing modules plus agent_capabilities passed 1,007. A local CodeQL run was not available; the next CodeQL run on this head confirms the alerts. |
|
[operator: bolichen97] #22887 was CONFLICTING on ec63898, so I rebased it onto fresh main as f4cb752. The content is the same as ec63898 (disposition 6389764613). Two test files conflicted. main had folded tests into parametrized forms: After the rebase, the conflicted files, the CI-failing modules, the line budget and the facade guards passed 896; the local battery passed 27,396; mypy, black and the line cap pass. |
|
[operator: bolichen97] Disposition for GPT 6.1 on f4cb752. Fixed on 11fbc2d. F1 (credential_values.py:119), CDATA values: fixed.
F2 (credential_values.py:89), multi-line quoted YAML: fixed.
Other lanes on f4cb752:
Verification: the backend lint gates pass. The local battery passed 27,409. The modules that failed in CI earlier passed 626. |
|
[operator: bolichen97] Disposition for GPT 6.1 on 11fbc2d. Fixed on 9adca1b. F1 (credential_values.py:123), blank lines in quoted YAML: fixed. F2 (credential_values.py:160), XML values over lines: fixed.
F3 (credential_patterns.py:236), triple-quoted literals: fixed. The separator reads F4 (credential_values.py:568), encoded config lines: fixed.
Other lanes on 11fbc2d:
Verification: the backend lint gates pass. The local battery passed 27,431, and the earlier CI-failing modules passed 626. |
|
[operator: bolichen97] Disposition for GPT 6.1 on 9adca1b. Fixed on 2550983. F1 (credential_patterns.py:138), next-line values containing
F2 (credential_values.py:657), folded plain YAML values: fixed.
F3 (scan.py:297), standalone media bypass: fixed. In standalone mode, a base64 run that decodes to binary is no longer skipped. Its printable runs (a PNG Other lanes on 9adca1b:
Verification: the backend lint gates pass. The local battery passed 27,442, the earlier CI-failing modules plus the prefilter oracle passed 1,035, and the line budget was re-measured at 34,771. |
|
[operator: bolichen97] Disposition for GPT 6.1 on 2550983. Fixed on a7238fa, which is also rebased onto fresh main: #22887 had gone CONFLICTING. F1 (text_redaction.py:225), long values past the stream's hold: fixed. When a value runs past the hold, the stream drops it through the value's own close:
Rebase:
Lanes on 2550983:
Verification: the backend lint gates pass. The local battery passed 27,480. The earlier CI-failing modules, the prefilter oracle and the two files main touched passed 1,226. |
|
[operator: bolichen97] Disposition for GPT 6.1 on a7238fa. Fixed on 5d8d178. F1 (credential_values.py:756), quoted config values behind weak labels: fixed.
Lanes on a7238fa:
Verification: the backend lint gates pass. The local battery passed 27,487; its one failure was the case moved above, which now passes. The earlier CI-failing modules and the prefilter oracle passed 1,035. The line budget was re-measured at 34,807. |
|
[operator: bolichen97] Disposition for GPT 6.1 on 5d8d178. Fixed on 903e7a2. F1 (credential_values.py:786), short quoted passwords behind weak labels: fixed. A quoted value behind a weak label ( Tests: Lanes on 5d8d178:
Verification: the backend lint gates pass. The local battery passed 27,495. The earlier CI-failing modules and the prefilter oracle passed 1,035. |
|
[operator: bolichen97] Disposition for GPT 6.1 on 903e7a2. Fixed on c51a175. F1 (redaction_labels.py:1034, credential_values.py:704), long folded YAML values: fixed.
F2 (credential_values.py:357), vendor Identifier tier behind a generic label: fixed.
Lanes on 903e7a2:
Verification: the backend lint gates pass. The local battery passed 27,501 and the earlier CI-failing modules passed 1,035. The line budget was re-measured at 34,847. |
|
[operator: bolichen97] Disposition for GPT 6.1 on c51a175. Fixed on dea0017. F1 (credential_patterns.py:152), backtick-quoted values: fixed.
F2 (redaction_labels.py:121), name/value fields in any order: fixed.
Lanes on c51a175:
Verification: backend lint gates pass. Local battery 27,512 passed; earlier CI-failing modules 1,035 passed. Line budget re-measured at 34,897. |
|
[operator: bolichen97] Disposition for GPT 6.1 on dea0017. Fixed on 1274e13. F1 (text_redaction.py:500), a value streamed before its secret name: fixed. The stream's F2 (redaction_labels.py:149), braces inside quoted values: fixed. F3 (redaction_labels.py:146), upper-case fields: fixed. The pre-check is now case-insensitive ( Lanes on dea0017: First Principles is the product-shape BLOCK that the owner override covers on the final head. Verification: the backend lint gates pass. The local battery passed 27,534, and the earlier CI-failing modules passed 1,035. The line budget was re-measured at 34,965. |
|
[operator: bolichen97] Disposition for CI, CodeQL and GPT 6.1 on 1274e13. Fixed on 1e2b431. CI Backend Tests (3.12, 3) and (Windows, 3): fixed. CodeQL (1 new high): fixed. GPT F1 (redaction_labels.py:622), sibling fields far apart in the stream: fixed.
GPT F2 (text_redaction.py:1230), long backtick values: fixed. A backtick value past the hold now drops as Verification:
|
|
[operator: bolichen97] Disposition for GPT 6.1 on 1e2b431. Fixed on a5358d7. F1 (credential_values.py:235), YAML list-item values swallowing sibling fields: fixed.
Verification:
|
Every redaction rule now carries a tier (Secret, Bearer link, Identifier), decided once by what a value can do, not guessed from its shape. - AWS access key IDs (AKIA/ASIA) are Identifier tier and stay visible on every surface; a signed link loses its signature and session token, not its key ID. - Labelled values keep their label and lose only the value (`api_key = [REDACTED: credential]`), so structured output still parses. Labels match case-insensitively with the usual word separators, cover generic secret fields and Authorization Bearer/Basic, and are shared with the log filter, which now redacts the Secret tier too. - A 40-character secret-shaped value next to an AWS key ID is the secret half of the pair; the key ID stays. - Every rule also runs over a normalised view (percent-decoded, JSON-unescaped, control and invisible characters dropped), with spans mapped back to the original text. Base64 decoding covers URL-safe and wrapped runs. - The entropy guess for unlabelled values is removed, with every exemption that existed only for it (document and diagram links, commit permalinks, macOS temp-dir ids, the code-text scope). - The stream redactor holds a value with any secret label while it may still be arriving. - A vendor token catalog is generated at build time from the Betterleaks rule set (MIT) by scripts/gen_secret_catalog.py into src/kiro_crew/data/secret_catalog.generated.json; a finding whose value is an identifier shape is dropped. No new runtime dependency. Closes #22552
|
[operator: bolichen97] Disposition for GPT on a5358d7, fixed on f4b2c33:
Local gate (black, isort, flake8, mypy, repo checks, comment-history, semgrep) and the 294-module redaction battery (42,017 passed) are green. |
Do not merge: waiting for design approval on #22552.
Problem / Motivation
Goal: Redact every secret and never an identifier, by deciding once, per rule, what a matched value can do.
Today each redaction rule decides sensitivity on its own. AWS access key IDs are removed from every log row and CloudTrail record, so an investigator loses the value they join on. A JSON field loses its label along with its value and stops parsing. A secret behind an upper-case or camel-case label, or in an escaped form, is missed. A guess by entropy for unlabelled values removes digests, request IDs and inline images, and needs a new exemption for every false positive.
Why it matters
People investigating incidents cannot read the logs and transcripts Kiro Crew writes, because identifiers are replaced. Secrets in common spellings still reach the screen, the log and chat channels. Every new false positive costs another one-off exemption, and those exemptions have been a source of leaks themselves.
Not a goal
What changed (motivation → approach → change)
The design is in #22552 (tier model, section 3; detection, section 4); the tier decision is recorded in
docs/decisions/2026-10-20-redaction-classifies-by-what-a-value-can-do.md.{"SecretAccessKey": "[REDACTED: credential]"}still parses. Labels match in any case and spelling, after=,:,=>, an XML tag or a siblingname/valuepair; generic secret fields andAuthorizationBasic/Bearer are covered. The log filter now applies the same Secret tier.scripts/gen_secret_catalog.pygeneratessrc/kiro_crew/data/secret_catalog.generated.jsonfrom the Betterleaks rule set (MIT, licence kept in the file) at build time. Rules that need a filter program, a file path, or are not linear-time in Python'sreare skipped; a finding whose value is an identifier shape is dropped. No new runtime dependency.get_credential_patterns()read the same records and labels; a deploy finding names its rule and length, never a character of the value.Where. Shared spellings live in
src/kiro_crew/credential_patterns.pyand feedsrc/kiro_crew/log_redaction.pyandsrc/kiro_crew/deploy/scan.py. The key-ID copies leavesrc/kiro_crew/sel.py,src/kiro_crew/metrics/schema.py,src/kiro_crew/decisions/gate.py,src/kiro_crew/security_posture.py,src/kiro_crew/platform/, the built-in apps undersrc/kiro_crew/apps/,src/kiro_crew/computer_use/,src/kiro_crew/testing/fake_computer_use.py, and the frontendwebsite/src/utils/sanitize.ts,website/src/utils/errorReport.tsandwebsite/src/apps/mochi/src/shared/utils.ts(signed links keyed on the signature inwebsite/src/utils/credentialSignature.test.ts; fixtures inwebsite/src/test/).setup.cfgpackages the catalog;.github/drops the removed module from a path list.This carries the work of #22757, #22736 and #22737 in one commit.
Backwards compatibility
Breaking: AWS access key IDs are no longer redacted or blocked, an unlabelled value with no prefix, label or key ID beside it is no longer redacted, and the redacted text now keeps a value's label. Writer sweep on origin/main: every in-repo caller of the removed helpers is updated in this diff (listed below); stored transcripts are unchanged, and credential records naming the old rule id still resolve.
Reader: src/kiro_crew/security/exfil.py:_oauth_url_payload_diagnostic call site -- chat -- scans with
_contains_fixed_credentialonly;test_security.pyReader: src/kiro_crew/security/exfil_oauth.py:_safe_oauth_parameter_name -- chat -- same;
test_security.pyReader: src/kiro_crew/security/display_safety.py:sanitize -- chat -- same;
test_display_redaction_source_guard.pyReader: src/kiro_crew/dashboard/chat_utils_display.py:_display_scope_changes_battery -- crew page -- reads only the owner-view scope;
test_display_time_redaction.pyReader: src/kiro_crew/dashboard/handlers/member_dashboard.py:_template_text_safe -- crew page -- script and style text get the same pass;
test_member_dashboard_routes.pyReader: src/kiro_crew/dashboard/file_api/project_dirs.py:_redact_project_path -- crew page -- canonical redactor;
test_project_git.pyReader: src/kiro_crew/dashboard/display_verdicts.py:_rules_fingerprint -- crew page -- new rule files are in the digest, so stored verdicts are recomputed;
test_display_time_redaction.pyReader: src/kiro_crew/security/credential_sources.py:_AWS_TEMPLATE_KEYS -- chat -- keeps the old rule id for existing records;
test_credential_sources.pyReader: src/kiro_crew/security/stream_aws_anchor.py:names_a_key -- chat -- reads the shared label table;
test_stream_redactor_aws_keyed.pyTests
test/test_redaction_context_rules.py: everyday inputs (CloudTrail row, assume-role output, env dump,~/.aws/credentials, escaped JSON, percent-encoded values, CSV pair, SigV4 header, presigned S3 and CloudFront links, base64 INI, Basic header) lose every secret and keep every identifier; label shapes beyondkey=value; fields that name no secret stay; stream splits mid-label, mid-URL, after each separator, across invisible characters and beside a key ID.test/test_secret_catalog.py: one test per ported vendor rule with a sample generated at test time from the rule itself, the Go-to-Python translation cases, the identifier veto, and a zero-noise check over the repository's own docs.test/test_redaction_tiers.py,test/test_log_redaction.py,test/test_redaction_key_id_identifier.py: tiers for the new rules, the log Secret tier, and key IDs kept.Manual verification
N/A — unit coverage sufficient: the change is text-in, text-out, and every surface is covered by the tests above. An independent read of the diff looked for secrets that survive; the shapes it found are fixed and pinned in
test_redaction_context_rules.py.Screenshots / video
Why no screenshot: the frontend change only swaps which strings the shared sanitizer replaces (key IDs stay, signing values go), so no panel, layout or component renders differently.
Related Issues
Closes #22552
Pattern harvest
Rule candidate: review-prompt
Pattern: a redaction rule that decides sensitivity from a value's shape instead of reading its tier from the shared table
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)