Skip to content

feat(rules): corpus shakedown rule pass — coverage for Gitea, Ghost, Cal.com, OpenMRS, Zulip#50

Merged
boorad merged 9 commits intomainfrom
corpus-rules-pass
May 4, 2026
Merged

feat(rules): corpus shakedown rule pass — coverage for Gitea, Ghost, Cal.com, OpenMRS, Zulip#50
boorad merged 9 commits intomainfrom
corpus-rules-pass

Conversation

@boorad
Copy link
Copy Markdown
Contributor

@boorad boorad commented May 4, 2026

Summary

Rule-coverage pass driven by plans/todo/05-corpus-shakedown-followups.md (P0 #1#4, P1 #5#7, P2 #9). One item per commit so any can be dropped in review without blocking the rest.

Net: 8 rule changes; rule count 53 → 59; rule tests 225 → 288 (all passing). Clippy clean, fmt clean.

New rules

  • go-permission-predicate-call — Gitea-shape IsAdmin(), IsOwner(), CanRead(unit), HasAnyUnitAccess() (no string-literal arg).
  • ts-chained-permission-call — Ghost/CASL/Pundit-style canThis(user).edit.post(post).
  • py-check-helper-call — Zulip-shape check_can_*, check_basic_*_access, check_*_permission.
  • java-authorized-annotation — OpenMRS @Authorized({"PRIV"}).
  • py-require-decorator — Zulip @require_realm_admin, @require_member_or_admin, ... (excludes @require_GET/@require_POST).
  • ts-trpc-procedure — tRPC protectedProcedure/adminProcedure chained builders.

Widened rules

  • ts-permission-check-call — adds belongsTo, isTeamAdmin, isOrgOwner, hasMembership, userIs<Role>.
  • ts-role-check-conditional — accepts !=/!== (Cal.com guard pattern) and roleName.

Breaking

  • ts-jwt-token-check is split into:

    • ts-jwt-verify-decode (middleware, medium) — drop-in for the authz signal.
    • ts-jwt-sign-issue (custom, low) — outbound token issuance, kept rather than dropped so users can disable a single rule by ID.

    Marked as feat!: to bump 0.1.x → 0.2.0 per the version policy in CLAUDE.md.

Sequencing

Scanner/output items from the same plan doc (P2 #10, P3 #11a, #11, #12) are deferred to a follow-up PR — different code area, different review surface.

Test plan

  • cargo build
  • cargo test (325 unit + 30 integration + doc tests)
  • cargo run -- rules validate (59 rules)
  • cargo run -- rules test (288 / 288 passing)
  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • Spot-check: cargo run -- scan <gitea-clone> to confirm go-permission-predicate-call lights up the Gitea perm/access tree (target: ~200+ findings vs 18 before).
  • Spot-check: scan Ghost to confirm ts-jwt-sign-issue separates cleanly from ts-jwt-verify-decode and ts-chained-permission-call lights up core/server/.
  • Spot-check: scan Cal.com to confirm ts-role-check-conditional matches session.user.role !== "ADMIN" cases and ts-trpc-procedure matches the procedure-builder endpoints.
  • Spot-check: scan OpenMRS to confirm java-authorized-annotation matches the 24 @Authorized files.
  • Spot-check: scan Zulip to confirm py-check-helper-call and py-require-decorator light up zerver/views/ and zerver/lib/.

Summary by CodeRabbit

  • New Features

    • Go: detect predicate-style permission calls (IsAdmin/CanRead-style)
    • Java: analyze @Authorized annotations for privilege strings
    • Python: detect require_* decorators and check_* helper authorization calls
    • TypeScript/JS: detect chained permission DSLs, membership helper calls, and tRPC procedure builders
    • TypeScript/JS: added JWT verify/decode and JWT sign issuance detection
  • Changes

    • Broadened role-comparison patterns to include !=/!== and roleName variants
  • Removed

    • Deprecated older JWT token-check rule replaced by new JWT checks

boorad added 8 commits May 4, 2026 18:35
…cates

Sister rule to go-has-role-call/go-permission-check-call. Those require a
string-literal argument; this rule covers the very common Go shape where
the call is a boolean predicate (IsAdmin(), IsOwner(), CanRead(unit),
HasAnyUnitAccess()). Surfaced by the corpus shakedown on Gitea, where
the entire permission model is hand-rolled around predicates of this
shape and none matched any existing rule.

No rego template — the role/permission isn't recoverable from the AST,
so any stub would mislead. Predicate names are explicit alternations
rather than a broad Has[A-Z]... to avoid duplicate findings with the
existing string-literal rules. Confidence is medium.
…ip helpers

Two changes from the corpus shakedown on Ghost and Cal.com:

- New ts-chained-permission-call: matches the shape
  `<callee>(...).<verb>.<resource>(...)` used by Ghost's permission DSL
  (canThis(user).edit.post(post)) and by CASL/Pundit ports. Predicate is
  a prefix match on can/may/allow/check/ability. Medium confidence; no
  rego template (verb/resource are identifiers, not literals).

- Widen ts-permission-check-call predicate to include belongsTo,
  isTeamAdmin, isOrgOwner, hasMembership, and the userIs<Role> family.
  Cal.com uses these as first-class authz checks but they were
  previously unmatched.
Cal.com Next.js handlers use `if (session.user.role !== "ADMIN")` as a
forbidden-guard pattern. The existing query already supported nested
member access on the LHS (the rightmost property is captured), but
restricted the operator to `==`/`===`. Extend operators to include `!=`
and `!==`, capture the operator as @op for future use, and add
`roleName` to the predicate alternation (common in NextAuth-style
session shapes).

Note: the rego template still emits `==`. For `!==` matches the
generated stub will be operator-inverted from the original intent — the
policy decision point itself is the value here; operator polarity is a
small manual fix at extract time. Documented inline.
Surfaced by the corpus shakedown on Zulip, where the actual policy
decision points are spelled `check_can_*`, `check_basic_*`,
`check_message_*access`, `check_stream_*access`, `check_*_access`,
`check_*_permission`, `check_*_authz` — bare module functions invoked
from views/lib code. Sister rule to py-permission-check-call (which
only matches attribute calls like user.can("delete")).

No rego template: the policy intent is in the function name itself.
Anchored alternation requires a permission/access keyword in the
suffix to avoid validation helpers like check_message_format. Medium
confidence.
…ized

OpenMRS's primary policy surface is `@Authorized({"Manage Users"})`
applied to service methods. Surfaced by the corpus shakedown — 24
files use it; we matched zero. Two query patterns cover the single-
and array-string forms; both feed the same role_value capture so the
rego template emits a single privilege-check shape against
input.user.privileges.
Surfaced by the corpus shakedown on Zulip, where view functions are
gated by @require_realm_admin, @require_organization_member,
@require_member_or_admin, etc. Sister rule to py-login-required-decorator
(which matches the exact name login_required); this matches the
open-ended require_<role> family.

Accepts both decorator forms (marker/call) as bare identifier or
attribute reference, mirroring py-login-required-decorator's shape.
Predicate uses a lowercase suffix to exclude HTTP method gates like
@require_GET / @require_POST from django.views.decorators.http.
tRPC's idiomatic authz pattern is a procedure builder
(protectedProcedure, adminProcedure, authedProcedure, ...) chained
into each endpoint declaration. Each chain is a distinct policy
decision point — the builder injects the authz middleware into
everything built from it. Surfaced by the corpus shakedown on
Cal.com.

Match shape fires exactly once per chain (leftmost member access
whose object is a bare identifier), so multi-method chains yield a
single finding. Predicate is [a-z][A-Za-z]*Procedure so well-known
and project-specific builders both qualify. No rego template — the
policy lives in the builder's middleware, not the call site.
Surfaced by the corpus shakedown on Ghost: the report was dominated by
jwt.sign(...) calls — token issuance for outbound integrations, not
authz decisions. The original ts-jwt-token-check rule conflated:

  - verify/decode: extract claims a request will be authorized against
    (real authz decision point)
  - sign: issue a token to send outbound (crypto, not authz)

Split into two rules:

  - ts-jwt-verify-decode: middleware category, medium confidence,
    drop-in replacement for the authz signal of the old rule.
  - ts-jwt-sign-issue: custom category, low confidence, kept (rather
    than dropped) because key handling / audience scoping are still
    relevant during a security review and users can disable a single
    rule by ID.

BREAKING CHANGE: rule ID `ts-jwt-token-check` is removed. Users
referencing it in config/filters should switch to the verify-decode
or sign-issue replacements.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 095b98de-2db5-4e65-91af-e39b5d695b1f

📥 Commits

Reviewing files that changed from the base of the PR and between f4a570b and 8938d6f.

📒 Files selected for processing (4)
  • rules/typescript/chained-permission-call.toml
  • rules/typescript/membership-check-call.toml
  • rules/typescript/permission-check-call.toml
  • src/rules/embedded.rs
✅ Files skipped from review due to trivial changes (2)
  • rules/typescript/permission-check-call.toml
  • src/rules/embedded.rs

📝 Walkthrough

Walkthrough

This PR adds multiple new authorization detection rules across Go, Java, Python, and TypeScript; splits TypeScript JWT checks into verify/decode and sign-issue rules; updates two TypeScript permission-role rules; and registers all rule TOML files in the embedded rules list.

Changes

Authorization rule additions & registry wiring

Layer / File(s) Summary
New TOML rules
rules/go/permission-predicate-call.toml, rules/java/authorized-annotation.toml, rules/python/check-helper-call.toml, rules/python/require-decorator.toml, rules/typescript/chained-permission-call.toml, rules/typescript/trpc-procedure.toml, rules/typescript/membership-check-call.toml, rules/typescript/jwt-sign-issue.toml, rules/typescript/jwt-verify-decode.toml
Adds new Tree-sitter–backed rule configurations matching various authz patterns (Go predicate calls, Java @Authorized, Python check_* helpers and @require_* decorators, TypeScript chained permission DSL, tRPC procedure builders, membership helpers, and JWT sign/verify-decode distinctions). Each includes predicates, optional Rego templates, and test fixtures.
Rule modifications (queries/predicates/tests)
rules/typescript/permission-check-call.toml, rules/typescript/role-check-conditional.toml
Adds clarifying comments in permission-check-call; expands role-check-conditional query to capture !=/!== and include roleName in property predicate; updates tests accordingly.
Removed rule config
rules/typescript/jwt-token-check.toml (deleted)
Removes the previous monolithic JWT rule; its responsibilities are split into ts-jwt-verify-decode and ts-jwt-sign-issue.
Embedded rules registry update
src/rules/embedded.rs
Registers all new TOML rule entries for TypeScript, Java, Python, and Go; removes the old jwt-token-check and replaces go-permission-check-call with go-permission-predicate-call.
Tests / Fixtures
rules/*/*.toml (each [[rule.tests]])
Each new/updated rule file includes positive and negative test cases validating the Tree-sitter query/predicate behavior and non-overlap with related rules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • EnforceAuth/zift#32: Prior PR added go-permission-check-call and related Go/TypeScript rules; this PR replaces that Go rule with go-permission-predicate-call and updates embedded rules.
  • EnforceAuth/zift#3: Both PRs add Java rule support and update the embedded rules registry (related code-level changes).
  • EnforceAuth/zift#29: Both PRs add Python Tree-sitter TOML rules and register them in embedded rules (closely related).

"I nibble logs of TOML bright,
Rules sprout up in morning light,
JWT split, decorators found,
Chained calls hop all around,
Hooray — the rabbit guards the site!"

🚥 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 title accurately summarizes the main change: adding multiple new authorization rules targeting specific code bases (Gitea, Ghost, Cal.com, OpenMRS, Zulip) as part of a rule-coverage pass, which is the primary focus of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

Copy link
Copy Markdown

@amazon-q-developer amazon-q-developer Bot left a comment

Choose a reason for hiding this comment

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

This PR successfully adds authorization rule coverage for Gitea, Ghost, Cal.com, OpenMRS, and Zulip. The implementation is solid with 6 new rules, 2 widened rules, and a well-justified breaking change to split JWT token handling. All 288 rule tests pass, and the code follows established patterns. The changes are ready to merge.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@rules/typescript/chained-permission-call.toml`:
- Around line 13-19: The TOML rule lacks a Rego/OPA template stub so the
`extract` output has no policy artifact; add a minimal conservative Rego
template in the same "template" section of
rules/typescript/chained-permission-call.toml that defines a simple package
(e.g., package rules.chained_permission_call) and a single stub rule (e.g., deny
or allow) that accepts inputs and returns a decision, referencing the captured
identifiers (the verb/resource pair such as canThis/mayDo/allowFor/checkAbility)
as input.action/input.resource or as metadata so the generated policy compiles;
keep it intentionally imprecise (no enforcement logic), include a short comment
explaining it's a stub for manual review, and ensure the template key name
matches the rule’s existing template reference so `extract` will emit the policy
artifact.

In `@rules/typescript/permission-check-call.toml`:
- Around line 7-14: The ts-permission-check-call rule currently mixes
role/membership helper method families (belongsTo, isTeamAdmin, isOrgOwner,
hasMembership, userIs*) into its permission-extraction regex; remove those names
from the permission-oriented regex and related capture handling in
ts-permission-check-call and instead create a separate rule (e.g.,
ts-role-membership-check or similar) that contains a regex and extraction logic
specifically for belongsTo|isTeamAdmin|isOrgOwner|hasMembership|userIs*; update
the original permission rule to only match permission-oriented verbs (can,
hasPermission, checkPermission, isAllowed, allows, hasAccess, checkAccess,
hasClaim and deny-style variants) and ensure the new role rule validates that
the captured argument is a resource/role identifier rather than a permission
value.
🪄 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: CHILL

Plan: Pro

Run ID: bfb08787-5f0a-48e7-beb5-a9f3ec2ff96c

📥 Commits

Reviewing files that changed from the base of the PR and between c95965d and f4a570b.

📒 Files selected for processing (12)
  • rules/go/permission-predicate-call.toml
  • rules/java/authorized-annotation.toml
  • rules/python/check-helper-call.toml
  • rules/python/require-decorator.toml
  • rules/typescript/chained-permission-call.toml
  • rules/typescript/jwt-sign-issue.toml
  • rules/typescript/jwt-token-check.toml
  • rules/typescript/jwt-verify-decode.toml
  • rules/typescript/permission-check-call.toml
  • rules/typescript/role-check-conditional.toml
  • rules/typescript/trpc-procedure.toml
  • src/rules/embedded.rs
💤 Files with no reviewable changes (1)
  • rules/typescript/jwt-token-check.toml

Comment thread rules/typescript/chained-permission-call.toml Outdated
Comment thread rules/typescript/permission-check-call.toml Outdated
@boorad boorad changed the title feat(rules)!: corpus shakedown rule pass — coverage for Gitea, Ghost, Cal.com, OpenMRS, Zulip feat(rules): corpus shakedown rule pass — coverage for Gitea, Ghost, Cal.com, OpenMRS, Zulip May 4, 2026
@boorad boorad self-assigned this May 4, 2026
Two Major findings, both fixed:

1. ts-chained-permission-call lacked a rego template. Added a stub that
   substitutes the captured @verb / @resource identifiers, with a TODO
   comment noting the captures are identifiers (not runtime values) and
   manual review is required. Mirrors the convention already used by
   java-spring-preauthorize.

2. ts-permission-check-call mixed role/membership helpers (belongsTo,
   isTeamAdmin, isOrgOwner, hasMembership, userIs<Role>) with permission
   verbs. The membership shape's literal arg is a resource identifier
   (e.g. "team-42"), not a permission name, so the existing template
   `input.action == "{{permission}}"` would emit misleading rego.

   Reverted the predicate widening on ts-permission-check-call and
   moved the membership family to a new ts-membership-check-call rule
   with category=rbac and a membership-shaped stub template
   (`"{{resource_id}}" in input.user.memberships`). Preserves the
   project's role-vs-permission separation.
@boorad boorad merged commit 53d45ab into main May 4, 2026
2 checks passed
@boorad boorad deleted the corpus-rules-pass branch May 4, 2026 23:37
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.

1 participant