Skip to content

feat(risk): pick a ready-made exclusion rule instead of writing DSL - #5089

Open
svadrutk wants to merge 6 commits into
mainfrom
simplify-exclusion-rule-creation
Open

feat(risk): pick a ready-made exclusion rule instead of writing DSL#5089
svadrutk wants to merge 6 commits into
mainfrom
simplify-exclusion-rule-creation

Conversation

@svadrutk

@svadrutk svadrutk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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.suggestExclusion before 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

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:

Option Offered when
Just this value — <match> one finding, and we hold its plaintext — or can reveal it
Any <rule title> finding every selected row shares a ruleId
Anything detected by <source> every selected row shares a source
Write it myself always, last

Each option carries its own ExclusionFields, so saving one skips the serialize → parse round trip entirely — handleSave hands the mutation the same shape it already produced. Only custom has 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, the Promise.race, the requestIdRef token, 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. useSetupExclusionRule goes 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 policyId while 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 match at 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-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. Re-selecting after switching away reuses the cached value, so one reveal is one audit entry.

useUnmaskedMatch and hasRevealableEvent move out of risk-ui.tsx into a new unmask.ts — exporting non-components from a component module trips react/only-export-components. No behaviour change to MaskedMatch or EventMatchDialog.

Unchanged

Edit mode and the no-finding create paths (Exclusions tab, Policy Center) render exactly as before — exclusionOptions([]) returns just custom, 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 — plus exactCandidate's precedence and its scope gate.
  • pnpm -F dashboard type-check, build, lint, and vitest run src/pages/security/ (14 files, 105 tests) all green.
  • Manually walked the transcript, Risk Events bulk, Risk Overview category bulk, Exclusions tab, Policy Center, and edit paths against the local seed. Confirmed on Risk Events that selecting the exact option fires exactly one risk.unmaskResult, that switching away and back fires no second one, and that no request goes out when the operator picks rule/source instead.

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.
@svadrutk svadrutk added enhancement New feature or request javascript Pull requests that update javascript code labels Aug 8, 2026
@svadrutk
svadrutk requested review from a team as code owners August 8, 2026 22:26
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 128ef16

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 9 files

Re-trigger cubic

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread client/dashboard/src/pages/security/exclusion-sheet.tsx Outdated
Comment thread client/dashboard/src/pages/security/exclusion-sheet.tsx Outdated
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants