First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# PolicyEnforced
|
||||
|
||||
Check that a stated policy is enforced in the code, not only written down.
|
||||
|
||||
PolicyEnforced is a small policy gate engine. A rule is data: which paths it
|
||||
covers, what content makes a file subject to it, what content satisfies it,
|
||||
and how an author documents an exemption. The engine is pure. You pass in the
|
||||
files (usually the ones a change touched) and it returns what was checked,
|
||||
what passed, what was exempted and what failed.
|
||||
|
||||
## What it checks and why
|
||||
|
||||
Policy gates written as shell scripts tend to get copied from app to app and
|
||||
edited in place. Some copies drift for no reason. Others have to differ: an
|
||||
org-scoping check for an app that filters by an `org_id` column would flag
|
||||
every handler in an app that has no tenant columns and authorises through
|
||||
RBAC capability checks. One shared script cannot serve both, and a separate
|
||||
copy per app is the wrong fix. So the engine is shared and the patterns are
|
||||
parameters.
|
||||
|
||||
```ts
|
||||
// An app that scopes rows by tenant column
|
||||
orgScopingRule({
|
||||
guards: /requireOrg|resolveOrg/,
|
||||
inlineTenantFilter: /\.eq\((['"])(organization_id|org_id|owner_id)\1/,
|
||||
})
|
||||
|
||||
// An app with no tenant columns that uses RBAC checks
|
||||
orgScopingRule({
|
||||
serviceClients: /createServiceClient|createClient\(/,
|
||||
guards: /requireAuth|requireProjectCap|requireAdminCap/,
|
||||
})
|
||||
```
|
||||
|
||||
The engine is built against three failure modes.
|
||||
|
||||
1. A vacuous run is not a pass. `passed` and `vacuous` are separate fields,
|
||||
and a rule that no file was subject to is reported as NOT ASSESSED. "No
|
||||
changed file was covered by this rule" is not evidence that the policy
|
||||
holds. A gate that prints a check mark for both is indistinguishable from
|
||||
one that does nothing, which is the same defect as a test that cannot fail.
|
||||
2. Regex state can skip files. A rule built with a `/g` pattern carries
|
||||
`lastIndex` from one file to the next, so later files can pass unexamined
|
||||
and the gate goes green while covering nothing. Every pattern is tested
|
||||
through a fresh non-global copy.
|
||||
3. A gate with no escape hatch gets switched off. The first legitimate
|
||||
cross-tenant cron job would force someone to disable it. Exemptions are a
|
||||
documented comment marker, and they are counted and cited in the report so
|
||||
they stay visible.
|
||||
|
||||
A pass names the files it checked, and a violation cites the file and line
|
||||
where the rule's trigger matched. A result of "3 files checked" that does not
|
||||
say which files is an asserted result, which is the kind of claim this library
|
||||
exists to catch.
|
||||
|
||||
One limit to keep in mind: the org-scoping rule confirms that a guard is
|
||||
present. It does not prove the guard filters to the caller's own rows. That
|
||||
still needs tests and review.
|
||||
|
||||
## Rules included
|
||||
|
||||
- `orgScopingRule` flags a handler that uses a service-role client (row-level
|
||||
security bypassed) with no authorization guard, tenant filter or exemption.
|
||||
- `inOverflowRule` flags a dynamic `.in('col', ids)` filter with no chunking
|
||||
helper. With supabase-js, a long id list goes into the query string, the
|
||||
request fails, and `data || []` turns that into an empty result with no
|
||||
error. A literal array is bounded and is not flagged.
|
||||
- `claimRule` flags a public claim (for example "99.9% uptime SLA" on a
|
||||
pricing page) when nothing in the code enforces it.
|
||||
|
||||
You can also write a `PolicyRule` object directly.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { evaluatePolicy, formatReport, orgScopingRule, inOverflowRule } from '@extant2000/policy-enforced'
|
||||
|
||||
const rules = [
|
||||
orgScopingRule({ guards: /requireOrg|resolveOrg/, inlineTenantFilter: /\.eq\((['"])(org_id|owner_id)\1/ }),
|
||||
inOverflowRule({ chunkHelpers: /selectInChunks/ }),
|
||||
]
|
||||
|
||||
const files = [
|
||||
{ path: 'server/api/invoices.get.ts', content: "const db = createServiceClient()\nreturn db.from('invoices').select()" },
|
||||
{ path: 'server/api/contacts.get.ts', content: "await requireOrg(event)\nconst db = createServiceClient()" },
|
||||
]
|
||||
|
||||
const report = evaluatePolicy(rules, files)
|
||||
console.log({ passed: report.passed, vacuous: report.vacuous })
|
||||
for (const v of report.violations) console.log(`${v.ruleId} ${v.path}:${v.line} ${v.excerpt}`)
|
||||
|
||||
// The full report for a CI log, with remedies under each violation
|
||||
console.log(formatReport(report))
|
||||
```
|
||||
|
||||
`toReport(report)` returns the same result as a `CapabilityReport` from
|
||||
`@extant2000/evidence-record`. `evaluateRule(rule, files)` runs a single rule.
|
||||
|
||||
## Sample output
|
||||
|
||||
Produced by running the example above (first two lines):
|
||||
|
||||
```
|
||||
{ passed: false, vacuous: false }
|
||||
org-scoping server/api/invoices.get.ts:1 const db = createServiceClient()
|
||||
```
|
||||
|
||||
In the full report, `in-overflow` shows as NOT ASSESSED, since neither file
|
||||
used a dynamic `.in()` filter.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @extant2000/policy-enforced
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. Copyright (c) 2026 Extant 2000 LLC. See [LICENSE](LICENSE).
|
||||
Reference in New Issue
Block a user