Skip to content

feat(risk): granular per-rule toggles within policy categories#2994

Open
mfbx9da4 wants to merge 2 commits into
mainfrom
da/granular-rule-toggles
Open

feat(risk): granular per-rule toggles within policy categories#2994
mfbx9da4 wants to merge 2 commits into
mainfrom
da/granular-rule-toggles

Conversation

@mfbx9da4
Copy link
Copy Markdown
Contributor

@mfbx9da4 mfbx9da4 commented May 21, 2026

Closes AGE-2461.

Demo

https://github.com/speakeasy-api/gram/releases/download/_gh-attach-assets/granular-rule-toggles-2994.mp4

Why

Risk policies expose category-level toggles only — "Secrets" covers all 221 underlying gitleaks rules, "PII" covers every Presidio entity. Authors couldn't disable a noisy rule (e.g. a wildcard generic-api-key match) without giving up the whole category. This adds per-rule toggles inside each expandable category so the policy editor matches the granularity the scanner already supports server-side.

The categories.go ↔ rule_id mapping (secret.*, pii.*) and the existing dashboard rule catalog already had the data; the API and UI just didn't expose it.

What changed

Before

  • Category checkbox is the only control. Rule rows in the expanded panel rendered, but their checkboxes were disabled and tracked the parent. Unchecking a single rule was impossible.

After

  • Each rule in an expanded category has its own enabled/disabled checkbox.
  • Category header shows an indeterminate state when only some rules are enabled, plus a N/M on badge.
  • "Enable all" / "Disable all" buttons inside each expanded panel for fast bulk edits.
  • New optional disabled_rules: []string field on RiskPolicy (canonical ids like secret.aws_access_token, pii.credit_card).
  • Scanner pipeline drops findings matching the disabled set on both the real-time enforcement path (server/internal/risk/scanner.go) and the batch analyzer (server/internal/background/activities/risk_analysis/analyze_batch.go). Filter runs after dedup so an enabled secret finding still wins overlap against a disabled presidio finding.
  • Internal scanner names (gitleaks, presidio) are NOT surfaced in any new UI string or API field.

Drive-by

  • "Get Started" empty state now creates the first policy with autoName: true instead of hardcoding the name "Risk Scanner", so the LLM-generated name applies to first policies like every other create path.

Migration

Single PR with two commits:

  1. feat(risk): add disabled_rules column to risk_policies — expand-only ADD COLUMN ... NULL, fully backward-compatible.
  2. feat(risk): per-rule toggles within policy categories — Goa, scanner, dashboard.

Happy to split into two PRs if preferred.

Test plan

  • mise lint:server clean
  • go test ./server/internal/risk/... ./server/internal/background/activities/risk_analysis/... passes (verified locally)
  • Manual: create a policy, expand Secrets, uncheck a rule, save. POST /rpc/hooks.claude with content matching that rule and another enabled rule. Confirm only the enabled rule produces a finding/block.
  • Manual: re-enable the disabled rule; same payload now produces a finding for both.
  • Manual: empty state "Get Started" creates a policy with an auto-generated name (not "Risk Scanner").

mfbx9da4 added 2 commits May 21, 2026 23:18
Adds a nullable TEXT[] column for canonical rule_ids the policy author
has unchecked within an otherwise-enabled category. Empty/NULL means
every rule in the selected categories runs.

This is an expand-only migration: existing rows get NULL, which the
scanner treats as the no-op default.
Policy categories like Secrets and PII each cover many underlying
detection rules. Authors couldn't pick a subset without giving up the
whole category. Now each rule within a category is independently
toggleable from the policy editor.

Backend
- Goa: new optional disabled_rules ([]string) on RiskPolicy / create /
  update payloads. Canonical rule_ids (e.g. secret.aws_access_token,
  pii.credit_card) the user has unchecked within otherwise-enabled
  categories.
- Scanner: a shared DisabledRuleSet drops matching findings from
  gitleaks, presidio, and prompt-injection scanners on both the
  real-time enforcement path (server/internal/risk/scanner.go) and the
  batch analyzer (analyze_batch.go). Detection still runs all rules;
  the filter applies post-scan so dedup decisions aren't affected.
- Persistence: SQLc create/update threaded through, and disabled_rules
  participates in policy.version bumps so a per-rule change re-drains.

Dashboard
- PolicyCenter: expanded category panel now renders one checkbox per
  rule. Unchecking a rule adds its canonical id to disabled_rules.
  Category header checkbox becomes indeterminate when only some rules
  in the category are enabled; "Enable all" / "Disable all" affordances
  inside the panel for fast bulk edits.
- Internal scanner names (gitleaks, presidio) are not surfaced in any
  new UI strings; only canonical rule_ids and user-facing labels.

Drive-by
- "Get Started" empty-state now creates a policy with autoName=true
  instead of hardcoding "Risk Scanner", so first policies pick up a
  generated name like every other create path.
@mfbx9da4 mfbx9da4 requested review from a team as code owners May 21, 2026 22:19
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

⚠️ No Changeset found

Latest commit: 51aef9e

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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gram-docs-redirect Ready Ready Preview, Comment May 21, 2026 10:19pm

Request Review

Copy link
Copy Markdown
Member

@adaam2 adaam2 left a comment

Choose a reason for hiding this comment

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

All goodo except the migration in here

Copy link
Copy Markdown
Member

@vishalg0wda vishalg0wda left a comment

Choose a reason for hiding this comment

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

LGTM overall! Approved with comments.

}

// Drop findings whose canonical rule_id has been unchecked by the policy
// author. Done after the dedup pass so an enabled secret finding still
Copy link
Copy Markdown
Member

@vishalg0wda vishalg0wda May 22, 2026

Choose a reason for hiding this comment

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

A disabled presidio rule will mask overlapping findings from other sources that feature later in the priority order.

nit: let's make sure to add a test for this case if we agree to adapt dedup logic.

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 22, 2026

AGE-2461

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.

3 participants