Skip to content

fix: treat dangerous flags inside permissions.deny/ask rules as prohibitions, not usages - #103

Merged
affaan-m merged 1 commit into
affaan-m:mainfrom
solvela-ai:fix/deny-rule-no-verify-fp
Sep 10, 2026
Merged

affaan-m merged 1 commit into
affaan-m:mainfrom
solvela-ai:fix/deny-rule-no-verify-fp

Conversation

@sky64

@sky64 sky64 commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #102.

Problem

The permissions-dangerous-skip rule string-matches dangerous flags anywhere in a file. A permissions.deny rule whose entire purpose is to block a flag — e.g. "Bash(git commit --no-verify:*)" — is reported as a CRITICAL "Git hook verification bypass" and zeroes the Permissions category. Since "add a deny list" is the scanner's own remediation for the permissions-no-deny-list HIGH finding, following the tool's advice makes the reported posture worse (full feedback loop documented in #102).

Fix

The rule already downgrades prohibitive context to INFO for negated markdown mentions ("NEVER use --no-verify" → "Prohibition of --no-verify (good practice)"). This PR extends that same treatment to matches whose character span falls inside a permissions.deny or permissions.ask rule value in settings JSON:

  • New helper prohibitivePermissionRuleSpans() parses the settings JSON and returns the content spans of every deny/ask rule value. Non-settings files and unparseable JSON return no spans, so matches fail closed (stay CRITICAL).
  • Matches inside those spans become INFO findings (Deny/ask rule blocking --no-verify (good practice)), consistent with the existing negation downgrade.
  • Matches inside permissions.allow values are unaffected and stay CRITICAL — allowing a dangerous flag is a real finding.

The span check runs before the 100-char negation-context check, so deny entries are classified by their structural position rather than by whatever text happens to precede them.

Tests

Six new cases in tests/rules/permissions.test.ts (95 total, all passing):

  • deny rule with --no-verify → INFO ×2, not CRITICAL
  • ask rule with --no-verify → INFO
  • allow rule with --no-verify → stays CRITICAL
  • deny rule with dangerously-skip-permissions → INFO
  • deny-rule-shaped text in invalid JSON → stays CRITICAL (fail-closed)

npm run typecheck and npm run lint clean.

Real-world verification

Against the repo that prompted #102 (a .claude/settings.json with deny rules blocking --no-verify), built from this branch:

  • Before: 2 CRITICAL findings at settings.json:8,9, Permissions 0/100
  • After: both downgraded to INFO; no CRITICAL findings from the deny list

Related: this is the same context-insensitivity family as #100 (string literals in hook scripts) — this PR deliberately fixes only the structurally-decidable JSON case and doesn't touch the markdown/script heuristics.

Summary by CodeRabbit

  • New Features

    • Permission rule scanning now intelligently detects when dangerous patterns are already blocked by deny or ask configurations, downgrading severity from critical to informational.
  • Tests

    • Added comprehensive test coverage for permission rule pattern handling across different configuration types and severity levels.

Greptile Summary

This PR fixes a false-positive where permissions-dangerous-skip would flag dangerous flags (e.g. --no-verify) found inside permissions.deny or permissions.ask entries as CRITICAL, even though those entries exist to block the flag. A new helper prohibitivePermissionRuleSpans() parses the settings JSON, collects the character spans of every deny/ask value, and downgrades any match that falls within those spans to INFO — consistent with the existing negation-word heuristic for markdown prose.

  • New helper prohibitivePermissionRuleSpans() locates deny/ask entry spans in the raw JSON by exact-string indexOf; non-settings files and unparseable JSON return no spans so the rule fails closed.
  • Span check runs before the negation-context check, giving structural position precedence over nearby text heuristics; allow-list matches are explicitly not included in spans and remain CRITICAL.
  • Six new test cases cover deny, ask, allow, dangerously-skip-permissions, and invalid-JSON (fail-closed) scenarios, bringing the suite to 95 tests.

Confidence Score: 3/5

The core logic change is sound for the common case, but the span-building strategy can silently suppress a CRITICAL finding when the same entry string appears in both deny and allow — the exact scenario the PR claims is safe.

The prohibitivePermissionRuleSpans helper searches the raw JSON text with indexOf for each deny/ask entry string without constraining the search to the deny/ask array regions. If an identical string also exists in the allow array, the second indexOf hit lands on the allow value and adds it to the prohibitive spans. A dangerous flag inside that allow entry then gets silently downgraded from CRITICAL to INFO — a false negative in a security scanner where the PR's stated invariant breaks. The test suite validates the allow case in isolation but not in the presence of a matching deny entry, leaving this gap undetected.

The span-building loop in src/rules/permissions.ts (lines 180–186) needs a position-aware approach — for example tracking the JSON-parsed offset of each entry rather than using raw content search — to guarantee that allow-list occurrences can never be captured in the prohibitive spans.

Important Files Changed

Filename Overview
src/rules/permissions.ts Adds prohibitivePermissionRuleSpans() to downgrade dangerous-flag findings inside deny/ask entries to INFO; the span-building loop uses verbatim indexOf which can misclassify allow-list occurrences as prohibitive when the same entry string also appears in deny/ask.
tests/rules/permissions.test.ts Adds six targeted test cases covering deny, ask, allow, dangerously-skip-permissions, and invalid-JSON scenarios; missing a case where the same entry string appears in both deny and allow simultaneously.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Dangerous flag match found\nin file content] --> B{File is\nsettings-json?}
    B -- No --> E[Check negation\ncontext 100 chars]
    B -- Yes --> C{JSON\nparseable?}
    C -- No --> E
    C -- Yes --> D[Build prohibitiveSpans\nfrom deny + ask entries\nvia indexOf]
    D --> F{match.index\ninside a span?}
    F -- Yes --> G[INFO finding\nDeny/ask rule blocking flag\ngood practice]
    F -- No --> E
    E --> H{Negation word\nin context?}
    H -- Yes --> I[INFO finding\nProhibition — good practice]
    H -- No --> J[CRITICAL finding\nDangerous flag]

    style G fill:#d4edda
    style I fill:#d4edda
    style J fill:#f8d7da
Loading

Reviews (1): Last reviewed commit: "fix: treat dangerous flags inside permis..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

…bitions

The permissions-dangerous-skip rule string-matches dangerous flags
anywhere in a file, so a deny rule that exists to BLOCK a flag (e.g.
"Bash(git commit --no-verify:*)") is reported as a CRITICAL usage and
zeroes the Permissions category — the scanner penalizes its own
recommended remediation of adding a deny list.

Extend the existing prohibition handling (negated markdown mentions ->
INFO) to matches whose index falls inside a permissions.deny or
permissions.ask rule value in settings JSON. allow-rule matches stay
CRITICAL, and unparseable JSON yields no spans so matches fail closed.

Fixes affaan-m#102
@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key: "tools"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

This PR fixes a logic error where dangerous-flag scanner rules string-matched against deny/ask permission rule values, incorrectly treating hardening configurations as unsafe. The changes add span detection to recognize when dangerous flags appear inside prohibitive contexts and downgrade severity to INFO, preventing false positives that contradicted the scanner's own remediation advice.

Changes

Permission deny/ask rule context detection

Layer / File(s) Summary
Prohibitive span detection and dangerous-skip integration
src/rules/permissions.ts
Added prohibitivePermissionRuleSpans() helper parsing settings-json to extract character ranges of all permissions.deny and permissions.ask string values. Updated permissions-dangerous-skip rule to compute these spans and emit INFO-level findings when dangerous pattern matches fall within those spans, skipping normal CRITICAL reporting.
Test coverage for context-aware severity downgrade
tests/rules/permissions.test.ts
Added test cases validating --no-verify downgraded to INFO inside permissions.deny and permissions.ask, remains CRITICAL inside permissions.allow, and dangerously-skip-permissions inside permissions.deny downgraded to INFO. Included fail-closed test ensuring invalid JSON containing deny-shaped text with --no-verify still surfaces the flag as CRITICAL.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main change: treating dangerous flags inside permissions.deny/ask rules as prohibitions rather than usages, directly addressing the core issue.
Linked Issues check ✅ Passed The PR fully addresses issue #102 by implementing prohibitive permission rule span detection that downgrades dangerous flags in deny/ask rules to INFO while keeping allow rules CRITICAL.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #102: prohibitive span detection, rule downgrading logic, and comprehensive test coverage with no unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed due to a network error.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/rules/permissions.ts
Comment on lines +180 to +186
for (const entry of entries) {
let from = 0;
let at: number;
while ((at = file.content.indexOf(entry, from)) !== -1) {
spans.push([at, at + entry.length]);
from = at + entry.length;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Allow-entry false-negative when the same string exists in deny and allow

file.content.indexOf(entry, from) is position-agnostic: it finds every verbatim occurrence of the deny/ask entry string in the raw JSON, including any occurrence that happens to land inside a permissions.allow value with identical text. Consider a config with deny: ["Bash(git commit --no-verify:*)"] and allow: ["Bash(git commit --no-verify:*)"]. The deny entry scan calls indexOf twice and adds both occurrences to spans. The dangerous-flag match inside the allow value then falls within the second span and is downgraded to INFO — silently suppressing what should be a CRITICAL finding.

The PR's stated invariant ("Matches inside permissions.allow values are unaffected and stay CRITICAL") holds only when no deny/ask entry has the same text as an allow entry. There is no test covering the case where identical strings co-exist in deny and allow.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/rules/permissions.ts`:
- Around line 172-186: The current span mapping uses file.content.indexOf(entry)
across the entire file which can misclassify matches outside the permissions
block; restrict the search to the textual region that corresponds to this
config.permissions object instead of the whole file. Locate the permissions
block in the file content (e.g., find the substring for JSON.stringify(perms)
or, preferably, use the AST/source range for the config node if available),
compute a base offset for that block, then run the existing while loop searching
only within that slice (use file.content.slice(baseOffset, baseOffset +
blockLength) and adjust pushed spans by adding baseOffset) so entries, spans,
perms, entries, and file.content.indexOf(entry) only match occurrences inside
the declared permissions block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 467e157f-c705-4477-9d9d-ae702452728f

📥 Commits

Reviewing files that changed from the base of the PR and between 25d91f0 and 4565623.

📒 Files selected for processing (2)
  • src/rules/permissions.ts
  • tests/rules/permissions.test.ts

Comment thread src/rules/permissions.ts
Comment on lines +172 to +186
const perms = (config as { permissions?: { deny?: unknown; ask?: unknown } })
?.permissions;
const entries = [
...(Array.isArray(perms?.deny) ? perms.deny : []),
...(Array.isArray(perms?.ask) ? perms.ask : []),
].filter((entry): entry is string => typeof entry === "string");

const spans: Array<readonly [number, number]> = [];
for (const entry of entries) {
let from = 0;
let at: number;
while ((at = file.content.indexOf(entry, from)) !== -1) {
spans.push([at, at + entry.length]);
from = at + entry.length;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Global indexOf span mapping can misclassify dangerous allow entries as prohibitions.

At Line 183, span detection matches every occurrence of a deny/ask value across the whole file content. If the same string appears in permissions.allow (or elsewhere), that occurrence is also treated as prohibitive, so the dangerous flag can be downgraded to info incorrectly instead of staying critical.

Suggested fix direction
-  const entries = [
-    ...(Array.isArray(perms?.deny) ? perms.deny : []),
-    ...(Array.isArray(perms?.ask) ? perms.ask : []),
-  ].filter((entry): entry is string => typeof entry === "string");
-
-  const spans: Array<readonly [number, number]> = [];
-  for (const entry of entries) {
-    let from = 0;
-    let at: number;
-    while ((at = file.content.indexOf(entry, from)) !== -1) {
-      spans.push([at, at + entry.length]);
-      from = at + entry.length;
-    }
-  }
+  // Compute spans from the deny/ask array regions in raw JSON content,
+  // then extract string-token ranges only within those regions.
+  // Do not map spans by searching value text globally.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rules/permissions.ts` around lines 172 - 186, The current span mapping
uses file.content.indexOf(entry) across the entire file which can misclassify
matches outside the permissions block; restrict the search to the textual region
that corresponds to this config.permissions object instead of the whole file.
Locate the permissions block in the file content (e.g., find the substring for
JSON.stringify(perms) or, preferably, use the AST/source range for the config
node if available), compute a base offset for that block, then run the existing
while loop searching only within that slice (use file.content.slice(baseOffset,
baseOffset + blockLength) and adjust pushed spans by adding baseOffset) so
entries, spans, perms, entries, and file.content.indexOf(entry) only match
occurrences inside the declared permissions block.

Copy link
Copy Markdown

Local review recommendation: request changes or add coverage before merge. The current fix for #102 may still miss the case where the same permission appears in both allow and deny, creating a false-negative risk. Please add a duplicate allow/deny regression and confirm the rule still flags contradictory permission entries.

@affaan-m
affaan-m merged commit 434963f into affaan-m:main Sep 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants