Add permittable:audit — coverage, including what is not covered - #18
Open
VSN2015 wants to merge 1 commit into
Open
Add permittable:audit — coverage, including what is not covered#18VSN2015 wants to merge 1 commit into
VSN2015 wants to merge 1 commit into
Conversation
VSN2015
commented
Sep 5, 2026
|
|
||
| next unless task_args[:strict] | ||
|
|
||
| gap = Permittable::Audit.summary(entries)[:uncovered_with_body] |
Owner
Author
There was a problem hiding this comment.
Adding the [strict] argument to abort when uncovered_with_body > 0 enables teams to run permittable:audit[strict] as a continuous integration quality gate.
| # `Permittable.mode`, so it can resolve what the exporter deliberately | ||
| # cannot. | ||
| module Audit | ||
| module_function |
Owner
Author
There was a problem hiding this comment.
Limiting BODY_VERBS to POST, PUT, and PATCH appropriately scopes the finding to routes that expect inbound client payloads, avoiding false alarms on standard GET endpoints.
A controller declaring permit_params :create looks adopted. If it also
answers PATCH, that action validates nothing, and nothing in the gem
said so: permittable:generate only notices controllers with no contract
at all, and the OpenAPI exporter documents what exists rather than what
is missing. Half-coverage was invisible.
Permittable::Audit is the fourth reader of the frozen registry, and it
crosses it with the ROUTE SET, so a half-covered controller is as
visible as an uncovered one. Per routed action it reports the rule a
request would actually resolve through, that rule's effective mode,
whether a model: guards its columns, and — for an uncovered action —
whether it accepts a request body, which is the difference between a
gap in a table and untrusted input reaching an action unchecked.
Two things follow from the audit running inside the app rather than
being exported from it. It can resolve the EFFECTIVE mode (a rule's own
mode: first, then the app-wide Permittable.mode) where the OpenAPI
exporter deliberately cannot, which makes it the monitor-mode rollout
dashboard. And `bin/rails "permittable:audit[strict]"` can exit 1 on
the unguarded-write count, which makes it a CI gate: no new unguarded
write endpoint.
It also lists contracts declared for actions no route reaches — a
renamed or deleted action that left its contract behind — and audits
controllers that never included Permittable at all, since those are
the ones worth finding.
Plain Ruby over the registry plus { controller:, action:, verb:, path: }
route descriptors, the shape OpenAPI.rails_routes already produces, so
Audit.entries is unit-testable without Rails.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VSN2015
force-pushed
the
feature/audit-task
branch
from
September 11, 2026 22:01
4ce9121 to
8e5eacb
Compare
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.
The blind spot
A controller declaring
permit_params :createlooks adopted. If it also answersPATCH, that action validates nothing — and nothing in the gem said so:permittable:generateonly notices controllers with no contract at all.Half-coverage was invisible, which is the worst state for a security-adjacent tool to leave you in: the dashboard says "adopted" and the PATCH endpoint takes anything.
The audit
Permittable::Auditis the fourth reader of the frozen registry, and the first to cross it with the route set:Three things nothing else tells you
1. Which write actions are unguarded. A
GETwithout a contract is usually fine; aPOSTwithout one is untrusted input reaching the action unchecked. Separating those two is the whole point of the— ACCEPTS A BODYmarker, and that count is what[strict]exits 1 on. That makes the task a CI gate: no new unguarded write endpoint.2. Which contracts aren't enforcing yet. Because an audit runs inside the app rather than being exported from it, it can resolve the effective mode — a rule's own
mode:first, then your app-widePermittable.mode— which the OpenAPI exporter deliberately cannot (runtime config isn't contract data). This is the monitor-mode rollout dashboard.3. Which contracts have gone stale. A contract declared for an action no route reaches is a renamed or deleted action that left its contract behind.
Controllers that never included
Permittableare audited too — those are the ones worth finding, so the task collects everyActionController::Base/APIdescendant, not just the ones answeringpermit_rule_for.Design
Plain Ruby over the registry plus
{ controller:, action:, verb:, path: }route descriptors — the shapeOpenAPI.rails_routesalready produces — soPermittable::Audit.entries(controllers:, routes:)is unit-testable without Rails, same as the exporter..summaryreturns the counts for a CI log,.stalethe leftovers,.formatthe human report.Verification
spec/audit_spec.rb, written before the implementation), covering catch-all expansion, effective-mode resolution under a changedPermittable.mode, and concern-free controllersdocs/comparison.mdgains one honest row: no other option in that table reports what isn't covered