feat(risk): pick a ready-made exclusion rule instead of writing DSL - #5089
Open
svadrutk wants to merge 6 commits into
Open
feat(risk): pick a ready-made exclusion rule instead of writing DSL#5089svadrutk wants to merge 6 commits into
svadrutk wants to merge 6 commits into
Conversation
Pure helper that turns a selection of risk findings into savable exclusion rules stated in plain English: exact value when a single finding's plaintext is in hand, rule when every row shares a ruleId, source when every row shares a source, and custom last. Nothing renders it yet.
A finding-originated exclusion now opens on a radio list of complete rules stated in plain English. The criteria textarea and the AI suggestion box move behind a single "Write it myself" option, so the operator never sees two text inputs at once and never sees code unless they ask for it. The picker does not render without findings, so the Exclusions tab and Policy Center still open straight onto the criteria box. Dropping findingToExclusionState also drops its policy-scoped default, so a finding-originated create is now global-scoped.
…I call The batch path blocked on risk.suggestExclusion behind a 6s Promise.race, with a request token, a spinner on the bulk-actions trigger, and three fallback toasts that all dead-ended on an empty code box. The ready-made options are computed from the selection itself, so none of that has an output to produce any more. The suggestion endpoint survives inside "Write it myself", where it now carries the selected finding ids -- the one case local computation can't cover is a batch sharing neither rule nor source, which is exactly what the server generalizes.
|
Risk Events and Risk Overview null the raw match at the API boundary, so the picker could only offer rule/source there -- the entry point silently changed what the default rule meant. The exact option now appears behind the same audited, chat:read-gated risk.unmaskResult reveal the row itself uses. The reveal fires on selecting the option, not on opening the sheet: an operator who picks "any finding from this rule" should not leave an audit entry for a value they never looked at. Until it resolves the option is labelled with the redaction, is never preselected, and Create stays disabled -- there is no client-side stand-in for the plaintext. useUnmaskedMatch and hasRevealableEvent move out of risk-ui.tsx into unmask.ts; exporting non-components from a component module trips react/only-export-components.
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
adaam2
approved these changes
Aug 9, 2026
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.
Setting up an exclusion rule from a finding used to open on a monospace textarea expecting DSL, with an AI prompt box sitting right above it — two inputs, no indication that filling one meant skipping the other. Batch selections additionally blocked on a 6-second race against
risk.suggestExclusionbefore the sheet would open at all.The sheet now opens instantly on a radio list of ready-made rules stated as sentences. Every option is already a complete, savable rule; no code is visible unless you ask for it.
Demo
Single finding in a transcript → the picker opens preselected on the exact value → "Write it myself" is what reveals the DSL → back to the exact value → Create, and the session drops from 2 risks to 1.
What changed
A picker instead of a code box.
exclusionOptions()(new,exclusion-options.ts) takes the selection and returns only the rules that are valid for it:Just this value — <match>Any <rule title> findingruleIdAnything detected by <source>sourceWrite it myselfEach option carries its own
ExclusionFields, so saving one skips the serialize → parse round trip entirely —handleSavehands the mutation the same shape it already produced. Onlycustomhas no fields.The two text inputs can no longer co-occur. The criteria textarea and the "Suggest with AI" box are both behind
choice === "custom". They are removed from the tree rather than hidden, so the picker never leaves a stale expression behind it.The blocking AI race is gone.
SUGGEST_TIMEOUT_MS, thePromise.race, therequestIdReftoken,isSuggesting, the spinner on the bulk-actions trigger, and three fallback toasts that all dead-ended on an empty code box — deleted. The options are computed locally, so none of that has an output to produce any more.useSetupExclusionRulegoes 124 → 32 lines.The suggestion endpoint survives inside "Write it myself", where it now carries the selected
findingIds(capped at 50). That covers the one case local computation can't: a batch sharing neither rule nor source, which is exactly what the server generalizes well. On demand now, rather than on a blocking race.Scope defaults to global for any finding-originated create. Previously the chat transcript defaulted to the finding's
policyIdwhile the batch path defaulted to global — an undisclosed split. "Stop flagging this" means everywhere; the Scope select is there to narrow it.The exact-value option reaches the masked list pages too. Risk Events and Risk Overview null the raw
matchat the API boundary, so without this the entry point silently changed what the default rule meant. The option now appears there behind the same audited,chat:read-gatedrisk.unmaskResultreveal the row itself uses.The reveal fires on selecting the option, not on opening the sheet — an operator who picks "any finding from this rule" should not leave an audit entry for a value they never looked at. Until it resolves the option is labelled with the redaction, is never preselected, and Create stays disabled; there is no client-side stand-in for the plaintext. Re-selecting after switching away reuses the cached value, so one reveal is one audit entry.
useUnmaskedMatchandhasRevealableEventmove out ofrisk-ui.tsxinto a newunmask.ts— exporting non-components from a component module tripsreact/only-export-components. No behaviour change toMaskedMatchorEventMatchDialog.Unchanged
Edit mode and the no-finding create paths (Exclusions tab, Policy Center) render exactly as before —
exclusionOptions([])returns justcustom, so no picker appears and the form opens straight onto the criteria box. The expression parser and all 12 of its error strings are untouched.Not in this PR
No "this will suppress N findings" preview;
entity_type, regex, and combined rule+source filters stay DSL-only. The Exclusions tab and Policy Center still open on a code box — there is no finding there to build options from, and the picker simply never appears.Testing
exclusion-options.test.ts(new, pure, no mocks) covers the selection shapes — empty, single-with-match, single-with-revealed-value, pending reveal, shared-rule batch, batch sharing nothing — plusexactCandidate's precedence and its scope gate.pnpm -F dashboard type-check,build,lint, andvitest run src/pages/security/(14 files, 105 tests) all green.risk.unmaskResult, that switching away and back fires no second one, and that no request goes out when the operator picks rule/source instead.