Know what your code demands.
High-performance authorization pattern scanner. Extracts RBAC/ACL/ABAC/Others permission checks from source code, diffs against your catalog, fails CI if unregistered. Model-agnostic via regex config.
It reads your source code, extracts every permission check -- RBAC, ACL, ABAC, or custom -- and tells you what's missing from your catalog. No more guessing which permissions your app needs.
aegis scanLEVEL RESOURCE ACTION FILE LINE RULE
api api:packages read backend/handler/packages.go 15 go-check-any
api api:vouchers create backend/handler/vouchers.go 67 go-check-any
api api:documents delete backend/services/documents.py 42 python-guard
ui api:transactions read frontend/pages/Transactions.tsx 28 react-can
ui api:packages delete frontend/pages/Packages.tsx 12 react-can
api cms:articles publish backend/app/Gates/ArticlePolicy.php 33 laravel-gate
6 permissions found -- 3 backend, 2 frontend, 1 PHP.
Permissions drift. Developers add permission checks across every layer -- React components, Go handlers, Python services, Laravel policies. Ops can't assign what they don't know exists. Aegis closes the loop:
you write code aegis scans it you register in catalog ops assigns to roles
│ │ │ │
▼ ▼ ▼ ▼
CheckAny(ctx, roles, → UNREGISTERED: → rbac_catalog.go → ☑ api:webhooks
"api:webhooks", api:webhooks:read +{LevelKey: "api", :read granted
"read") Key: "webhooks",
Actions: ["read"]}
Gate::allows( → UNREGISTERED: → {LevelKey: "api", → ☑ cms:articles
'publish-article') cms:articles:publish Key: "articles", :publish granted
Actions: ["publish"]}
Any language. Any pattern. One config file.
- Language-agnostic. Config-driven via
.aegis.yaml. Works with any language, any framework. - Multi-permission aware. Detects multi-permission components (e.g.,
<Can all={[...]}>,<Can any={[...]}>) -- extracts every permission from compound checks. - CI-ready.
aegis lintexits 1 if code references permissions not in your catalog. Block merges with missing permissions. - Blazing fast. Rust + rayon parallel scanning. Handles monorepos with thousands of files.
- Multiple output formats. Table, CSV, JSON, catalog-json (ready to POST to your authorization API).
Pre-built binary (Linux, macOS, Windows):
# macOS arm64
curl -fsSL https://github.com/mocha-bot/aegis/releases/latest/download/aegis-macos-arm64 -o aegis
# Linux amd64
curl -fsSL https://github.com/mocha-bot/aegis/releases/latest/download/aegis-linux-amd64 -o aegis
# Windows
curl -fsSL https://github.com/mocha-bot/aegis/releases/latest/download/aegis-windows-amd64.exe -o aegis.exe
chmod +x aegis && sudo mv aegis /usr/local/bin/Cargo:
cargo install aegis-policycargo install --git https://github.com/mocha-bot/aegisOr build from source:
git clone https://github.com/mocha-bot/aegis.git
cd aegis
cargo build --release
./target/release/aegis --versionDrop a .aegis.yaml in your repo root:
version: 1
rules:
- id: react-can
files: ["**/*.tsx", "**/*.jsx"]
patterns:
- regex: '<Can\s+object="(?P<object>[^"]+)"\s+action="(?P<action>[^"]+)"'
level: ui
- id: go-check-any
files: ["**/*.go"]
patterns:
- regex: 'CheckAny\(.*?,\s*"(?P<object>[^"]+)",\s*"(?P<action>[^"]+)"\)'
level: api
- id: annotation
files: ["**/*"]
patterns:
- regex: '@rbac\s+(?P<level>\w+):(?P<object>[\w.-]+):(?P<action>\w+)'
level: "$level"Then:
aegis scan # See what your code asks for
aegis diff --api https://... # Find unregistered permissions (against an API)
aegis lint # CI gate -- fails if anything missing (baseline file)| Command | What it does |
|---|---|
aegis scan |
Scan source code for permission usage |
aegis diff |
Show permissions found in code but missing from the catalog |
aegis lint |
CI gate -- exit 1 if any permission is unregistered |
diff and lint resolve the catalog from one of two sources:
--api <url>— fetch the live catalog from your authorization API--baseline <file>— read a committed catalog file (for projects with no API)
If you pass neither, Aegis falls back to a .aegis.catalog.json baseline file in the
scan root. Passing both is an error.
Generate the baseline from your own scan output, commit it, and lint against it — no authorization service needed:
aegis scan --save # write .aegis.catalog.json + commit
aegis scan --save path/to/catalog.json # explicit path
aegis scan --format catalog-json > .aegis.catalog.json # equivalent, via redirect
aegis lint # default source: the baseline file
aegis lint --baseline path/to/catalog.json # explicit path--save writes sorted catalog-json (stable output — clean git diffs). Bare --save
defaults to .aegis.catalog.json, the path lint/diff auto-read.
The baseline accepts either aegis catalog-json output
({"permissions":[{"level_key","resource_key","action_key"}]}) or the API catalog shape
({"data":{"permissions":[{"key":"level:resource:action"}]}}), so an API response saved
to disk works as a baseline too. Runnable example: examples/baseline.
| Flag | Description |
|---|---|
--api <url> |
Authorization API base URL (diff/lint; mutually exclusive with --baseline) |
--baseline <file> |
Local catalog file (diff/lint; mutually exclusive with --api) |
--config <path> |
Path to .aegis.yaml (auto-discovered if omitted) |
--root <path> |
Root directory to scan (default: cwd) |
--format table|csv|json|catalog-json |
Output format |
--ignore-rule <id> |
Skip a specific rule (repeatable) |
Benchmarked against a synthetic monorepo -- 272 files, 33,697 lines, scattered permission checks:
| Metric | Cold start | Warm (OS cache) |
|---|---|---|
| Scan time | ~700ms | ~10ms |
| Permissions found | 6,865 | 6,865 |
| Files/sec | ~380 | ~27,200 |
| Lines/sec | ~47,000 | ~3,370,000 |
Release build, Apple M-series. Warm runs saturate at ~10ms -- the bottleneck becomes regex compilation, not I/O.
Aegis is model-agnostic. It doesn't care whether you use RBAC, ACL, ABAC, ReBAC, or a custom policy engine. It just matches patterns.
| Model | Example pattern | Config |
|---|---|---|
| RBAC | <Can object="api:packages" action="delete"> |
Built-in rules |
| ACL | $acl->check($user, 'resource', 'write') |
Add regex in .aegis.yaml |
| ABAC | @rbac api:documents:read (annotation) |
Built-in annotation rule |
| Laravel Gate | Gate::allows('update-post', $post) |
Add regex |
| Django Guard | user.has_perm('app.delete_model') |
Add regex |
| Custom | Anything your codebase uses | Write a regex |
The scanner is a dumb pattern matcher -- smart enough to extract what you tell it to, dumb enough to work with anything. Add rules to .aegis.yaml and Aegis handles the rest.
Aegis walks your file tree, matches configured regex patterns against each file, and extracts (object, action) pairs using named capture groups. For multi-permission components (like <Can all={[...]}>), it uses a two-pass strategy: an outer regex captures the permission block, and a sub_pattern extracts each inner pair.
Every match becomes a row: (level, resource, action, file, line, rule_id).
version: 1
catalog: config/permissions.json # optional: remembered catalog path (alias)
rules:
- id: my-rule # unique identifier
files: # glob patterns to match
- "**/*.tsx"
- "**/*.ts"
patterns:
- regex: '...' # regex with (?P<object>) and (?P<action>) named captures
level: ui # static level, or "$level" to use captured group
capture_mode: repeated # optional: "single" (default) or "repeated"
sub_pattern: '...' # required for repeated mode: inner extraction regexSet catalog: to alias your baseline file once, and scan --save, lint, and diff
all use it automatically — no per-command flag:
aegis scan --save # writes to config/permissions.json
aegis lint # reads config/permissions.json
aegis diff # reads config/permissions.jsonResolution precedence: CLI flag (--save / --baseline) → config catalog →
default .aegis.catalog.json.
capture_mode: repeated is for components that contain multiple permissions in a single block:
- id: react-can-any
files: ["**/*.tsx"]
patterns:
- regex: '<Can\s+any=\{'
capture_mode: repeated
sub_pattern: '\{object:\s*"(?P<object>[^"]+)",\s*action:\s*"(?P<action>[^"]+)"\}'
level: uiThis turns <Can any={[{object: "a:b", action: "c"}, {object: "d:e", action: "f"}]}> into two scan results -- one for each permission pair.
Against a live authorization API:
# .github/workflows/rbac-lint.yml
- name: Check permissions registered
run: |
aegis lint --api ${{ vars.AEGIS_API_URL }}Against a committed baseline file (no API — works in any repo):
# .github/workflows/rbac-lint.yml
- name: Check permissions registered
run: |
aegis lint --baseline .aegis.catalog.jsonFails the build if any permission referenced in code is missing from the catalog. Forces developers to register permissions before merging.
MIT
