Files
policy-enforced/README.md
T
2026-09-28 14:49:02 -04:00

120 lines
4.7 KiB
Markdown

# 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).