What each part of Aegis does, what it needs from you, and what it will not tell you.
Generated from the same registry the application reads, which is checked against the repository by a test. A capability that lost its implementation would fail that test rather than quietly keep its page here.
Who can see and change what, across single sign-on, roles and tenancy.
Scoped tokens that can never exceed the permissions of the person who created them, with rotation and revocation.
Implemented in: src/lib/credentials.ts, src/lib/access.ts
Tested by: tests/unit/credentials.test.ts, tests/rls/public-api.test.ts
Append-only record of security-relevant actions, exportable as CSV.
Implemented in: src/lib/identity.functions.ts
Tested by: tests/rls/identity.test.ts
Six built-in roles plus organisation-defined custom roles, with every permission checked in the database.
Implemented in: src/lib/access.ts, src/lib/identity.functions.ts
Tested by: tests/unit/access.test.ts, tests/rls/rbac.test.ts
SAML/OIDC configuration and SCIM 2.0 user provisioning and deprovisioning, including session revocation on deprovisioning.
Configure an identity provider under Settings. SCIM is off until an admin enables it and issues a scoped token.
Implemented in: src/lib/identity.functions.ts
Tested by: tests/rls/scim.test.ts
Team objects with membership, used to route ownership of assets and findings.
Implemented in: src/lib/identity.functions.ts
Tested by: tests/rls/identity.test.ts
Every tenant-scoped table is protected by Postgres row-level security, so isolation is enforced by the database rather than by application code remembering to filter.
Implemented in: src/lib/access.ts
Tested by: tests/rls/tenant-isolation.test.ts, tests/rls/rbac.test.ts
Reading repositories, and writing back to them as pull requests and checks.
Device-code sign-in, GitHub App installation or personal access token, with repository inventory and file-content scanning. Each repository has its own page listing every open finding -- dependencies, code, secrets, infrastructure, supply chain -- grouped into the upgrades and rules they are, each with the fix Aegis can write or the reason it cannot, and the pull request can be created from there. The Findings and Fix sections start from a repository: a picker above their tabs, remembered per organisation, and every tab and its badge then shows only that repository's findings, fixes, scans, plans and pull requests. The page also shows what the repository is made of, read from its file paths: languages with the SAST coverage each gets, package managers, CI, containers, infrastructure, entry points, and how many files are application code rather than tests, fixtures or docs. An architecture and data-flow view is drawn from the files the scan reads: the routes each module declares, the data stores, queues, services and authentication its files import, and the configuration it reads from the environment, each node naming the file that proves it; a data flow is shown only where one module both declares the route and holds the store's client, and open findings are laid over the module and route that contain them. It states the branch and commit it was read from.
Implemented in: src/lib/github.server.ts, src/lib/github-app.server.ts, src/lib/github.functions.ts, src/lib/github-device-flow.ts, src/lib/autofix-eligibility.ts, src/lib/scan-lifecycle.ts, src/lib/nav-counts.ts, src/components/repo-scope.ts, src/components/repo-scope-gate.tsx, src/lib/diff-view.ts, src/lib/scan-budget.ts, src/lib/durable-dispatch.server.ts, src/lib/scan-continuation.ts, src/lib/subrequest-tally.ts, src/lib/scan-walk-position.server.ts, src/lib/repo-archive.server.ts, src/lib/tar-stream.ts, src/lib/repo-issues.ts, src/lib/repository.functions.ts, src/lib/repository-profile.ts, src/lib/repository-architecture.ts
Tested by: tests/unit/repo-issues.test.ts, tests/unit/repository-profile.test.ts, tests/unit/repository-architecture.test.ts, tests/unit/github-app.test.ts, tests/unit/github-auth-paths.test.ts, tests/unit/github-device-flow.test.ts, tests/unit/autofix-eligibility.test.ts, tests/unit/findings-reach-the-ui.test.ts, tests/unit/scan-lifecycle.test.ts, tests/unit/scans-finish.test.ts, tests/unit/nav-counts.test.ts, tests/unit/repo-scope.test.ts, tests/unit/diff-view.test.ts, tests/unit/scan-budget.test.ts, tests/unit/scan-continuation.test.ts, tests/unit/subrequest-tally.test.ts, tests/unit/scan-budget-handler-reserve.test.ts, tests/unit/scan-walk-position.test.ts, tests/unit/tar-stream.test.ts, tests/unit/repo-archive.test.ts, tests/unit/the-product-says-what-it-knows.test.ts, tests/unit/a-sweep-runs-one-scan.test.ts, tests/unit/scan-button-runs-in-an-object.test.ts, tests/unit/a-daily-round-resumes.test.ts
Repository inventory and pull/merge request review across GitLab, Bitbucket Cloud and Server, and Azure DevOps. GitLab and Bitbucket Cloud push webhooks: a per-webhook secret checked in constant time (GitLab's `X-Gitlab-Token`, Bitbucket's HMAC-SHA256 `X-Hub-Signature` over the raw body), push and merge/pull request events parsed from the providers' documented payloads, the webhook bound to one repository, and every delivery recorded with its provider and verdict on the Webhooks page. The hook is registered through the connected account when the webhook is created and removed when it or the connection is deleted. A self-managed instance behind Cloudflare Access is reached with an Access service token stored beside the provider token and sent on every call to that instance (connect, repository list, commit status, hook registration and removal); a refusal by Access is reported as Access, not as a bad provider token, and an Access login redirect is not followed -- measured against the owner's gitlab.aegis.sarl: no token and a made-up token refused at the edge, the real token admitted.
Add a token for the provider under Integrations. Bitbucket Server cannot enforce a merge gate, and says so rather than appearing to. A verified GitLab or Bitbucket push is recorded but does not yet queue a scan: the repository scan reads code through GitHub only, and the delivery row says so rather than queuing a job that cannot run. Bitbucket Server webhooks are refused at registration, since the receiver reads Cloud payloads only.
Implemented in: src/lib/scm.ts, src/lib/scm-webhooks.ts, src/lib/scm-webhook-receiver.server.ts, src/lib/scm-webhook-registration.server.ts, src/lib/scm-request.server.ts
Tested by: tests/unit/scm.test.ts, tests/unit/scm-webhooks.test.ts, tests/unit/scm-webhook-receiver.test.ts, tests/unit/scm-request.test.ts, tests/integration/scm-cloudflare-access.test.ts
Per-PR delta scanning with line-level review comments and an `aegis/security` commit status. A push to a connected repository triggers a scan through the webhook receiver. A reviewer who replies `intentional`, `accepted`, `false positive`, `wontfix` or `aegis: ignore` under an Aegis comment suppresses that finding on the next pass: it is recorded as a 90-day exception quoting the reviewer and reply, is not re-commented, and does not fail the status. The acceptance follows the code, not the line: if the line the comment was about changes, the finding is reported again with a note that the earlier acceptance no longer applies. Replies from bots or from the account that posted the comment do not count, and an acceptance an admin revoked in the product is not re-applied.
Implemented in: src/lib/webhook-receiver.server.ts, src/lib/pr-comments.ts, src/lib/pr-accept-replies.ts, src/lib/github.server.ts
Tested by: tests/unit/webhook-receiver.test.ts, tests/unit/pr-comments.test.ts, tests/unit/pr-accept-replies.test.ts, tests/unit/commit-status.test.ts, tests/unit/branch-protection.test.ts
What is wrong in the code and its dependencies, and how far it is actually reachable.
Dependency version bumps, deterministic configuration fixes and AI-generated source fixes, raised as a branch and pull request after the repository's own install, typecheck, lint and test scripts have been run against the change in an isolated copy. A dependency bump installs the lowest release OSV reports no advisory against, not merely the advisory's own fix, and a rescan of the default branch closes the findings it resolved -- proven by scanning, fixing, merging and rescanning a real repository, for a dependency and for a Terraform finding, and by delivering the merge's push through the signed webhook. With autonomous autofix switched on in Policies, the scheduler opens these pull requests itself -- one upgrade per package and repository, highest risk first, a daily cap, never the same upgrade twice, with the organisation's branch prefix, commit prefix and notes -- and a scanner node advertising autofix-validate runs the repository's own checks on the branch before the pull request opens. The node ships as a container (scanner-node/Dockerfile, docs/SCANNER_NODE_HOSTING.md) with git, Node, npm, pnpm and yarn pinned, runs as a non-root user with the workspace on tmpfs, and makes only outbound calls, so it runs on a NAS, a VPS or any Docker host; `AEGIS_RUN_ONCE=1` drains the queue and exits for a scheduled runner, exiting 0 on an empty queue and non-zero when the control plane refuses it, with a GitHub Actions template to copy (ci-templates/aegis-scanner-node.yml) for anyone without an always-on host. Proven on production: with no node on the developer's machine, the container claimed the job and its report opened a verified pull request; on 27 September the compose service (restart policy unless-stopped: a crashing container on the same image was restarted seven times in ten seconds; a host reboot has not been observed) validated a sandbox lodash fix whose single commit carried package.json and package-lock.json, and `npm ci` and the test passed on the branch; and, with that service stopped, the same template's steps and environment run in a clean node:22 container (not on GitHub's runners) drained a fix queued from the fix window, exited 0, and the pull request opened verified; and the same image, loaded into the Docker of a UGREEN DXP4800 Plus NAS itself (not a VM on it) and started with the compose file, claimed a production fix and opened it verified with npm ci and the test passing. The repository's install and tests run on the node without access to its credentials: the node re-executes itself with neither token in its environment and is non-dumpable, scripts run with credential-shaped variables removed, git authenticates per command rather than through a URL written to .git/config, and a job whose branch git could read as an option is refused -- proven by a hostile npm on a real node finding nothing in its environment or any ancestor's /proc entry, and on production through the hardened DXP node. The node needs no GitHub credential at all: each repository job is handed a codeload archive address the control plane mints with the organisation's own credential -- measured to serve one repository at one ref, with no credential, and to expire within five minutes -- and the node refuses an archive from any other host. Proven by a real node with no GITHUB_TOKEN and git replaced by a trap validating the private sandbox repository. The fix window's button takes the same route whenever such a node is registered, naming it and saying when it last polled, so a node that runs on a schedule or is restarting is waited for rather than silently skipped; the unattended run uses only a node polling at that moment, because its daily cap would otherwise be spent on pull requests that never open: the branch is pushed, the node runs the install, typecheck, lint, test and build, regenerates a lockfile the editor left to npm, and the pull request opens with its checks -- measured on production with a node registered from the Scanner Nodes page: cookie-parser via its parent and diff via an override, each opened as verified with `npm ci` and the repository's test passing, the override's package-lock.json regenerated by npm on the node. What GitHub does with each fix pull request is followed back every half hour: one closed without merging returns its findings to open, and a merged one is verified by scanning the files it changed at the default branch, closing what is gone and reopening what is still there -- proven end to end from the repository page against a real repository. An npm fix is one commit carrying package.json and package-lock.json together, the lockfile edited without running npm: each moved package's registry entry, integrity and flags, the dependencies the new release needs moved to their lowest accepted release, and every dependant in the tree checked to still be satisfied. Where that cannot be done exactly -- a package installed more than once, one with nested dependencies of its own, one from outside the public registry, one the manifest overrides, or a lockfile older than version 2 -- the lockfile is left alone and the pull request says why. A transitive package whose single direct parent has released a version depending only on fixed releases of it is fixed by upgrading that parent, within its major, before an override is considered: the fix its maintainers made, and the only fix where the override would be a breaking upgrade (cookie-parser 1.4.6 -> 1.4.7 for cookie 0.4.1, whose fix 0.7.0 is a new minor at 0.x) -- measured end to end on production: the scan found cookie 0.4.1 in the sandbox, the fix window opened one commit with package.json and a package-lock.json byte-identical to npm's own, npm ci installed the branch, and after the merge the rescan closed the finding. The parent route is offered from the fix window and the Autofix page, not by autonomous autofix, because whether it exists is only known from the lockfile and a refusal spends the daily cap. The parent is read from whichever lockfile the project has -- package-lock.json, pnpm-lock.yaml (lockfile 6.0 and 9.0), yarn.lock (1 and Berry) or bun.lock (lockfile-parents.ts, tested on lockfiles written by each tool) -- and for those other than npm's the scanner node regenerates the lockfile with the repository's own package manager: measured on production with a pnpm project and a yarn 1 project, where cookie-parser was raised via pnpm-lock.yaml and yarn.lock and each pull request opened verified with the frozen install and the test passing. Yarn Berry and bun are tested on lockfiles those tools wrote, not yet on production. A transitive package whose every dependant already declares a range admitting the fix is fixed in package-lock.json alone -- no manifest change, no override, byte-identical to `npm update <pkg>` -- measured on production: js-yaml 4.1.0 under cosmiconfig's ^4.1.0 moved to 4.3.2, the pull request opened verified, and the merge closed five findings. It applies only where package-lock.json is what installs the project -- not beside a pnpm, yarn or bun lockfile, nor under another `packageManager`. How often each route applies, measured two ways on production's 1,684 open transitive npm findings: a read-only pass over all of them with each finding's own fixed version gives 105 parent upgrades (61 npm, 39 pnpm, 5 yarn), 158 lockfile-only and 1,421 overrides or refusals (1,102 in pnpm projects, where most packages arrive through a longer chain); the real plan, run read-only on a random 60 -- it first raises the version to the lowest release OSV clears -- gave 2 parent, 5 lockfile-only, 42 overrides and 11 refusals, each refusal naming why. The parent and lockfile routes are the minority case; the override remains the common fix. An override whose jump would be breaking is refused, and the refusal names why the parent could not be upgraded instead (no lockfile, a longer chain, several direct parents, no release yet, or only in a new major). Measured on 7 real projects: 21 lockfiles npm ci installed, 1 byte-identical to npm's own, 15 refused with a reason, none npm ci rejected. Every fix pull request is written in the format Aikido uses on the owner's own repositories (iconicLive #6, #7, #23, #24 and #26): an '[Aegis] Fix … via … version upgrade from X to Y' or '[Aegis] AI Fix for …' title, a fix/aegis-security-… branch, a fix(security): commit, and for a dependency the same three collapsible blocks -- breaking changes, the CVEs it resolves with their severity, and remediation details with a version-changes table. Where Aikido says the code is not affected by breaking changes, Aegis says so only when the repository's own tests passed on a bump that is not a major; a deterministic configuration fix is not described as AI.
A fix whose checks fail opens as a draft rather than being hidden, and a repository with no test script yields a fix marked unvalidated rather than verified. Validation runs the repository's own scripts, which means executing its code, so it belongs on a scanner node the customer controls. Autofix never pushes to a protected branch and never merges. Configuration fixes cover 10 of the 22 IaC scanner rules -- including a floating base image, pinned to the digest its tag resolves to now in the registry (with the organisation's stored credential for that host, or anonymously for public images), and a third-party GitHub Action pinned to the commit its tag resolves to now; the other 12 need a decision the code does not contain (which CIDR to allow, which IAM actions a role needs, where a secret should live) and say so instead of guessing. Dependency bumps edit npm manifests and Python requirements files: an exact == pin, with a pip-compile entry's --hash lines replaced by the target release's digests from PyPI (proven by pip accepting the pull request's file and refusing the stale hashes). Rust dependencies are moved in Cargo.lock -- version, checksum from the crates.io index, and references to that version -- only when the fix is inside the manifest's range and the fixed release depends on exactly what the installed one did; the result is byte-identical to `cargo update --precise` and accepted by `cargo fetch --locked` (measured on iconicbeen/weft), and anything the resolver would have to settle is refused with the cargo command to run. pyproject.toml, Poetry and uv lockfiles, and Go modules are reported with the reason and not patched.
Implemented in: src/lib/ai-fix.ts, src/lib/autofix.functions.ts, src/lib/fix-validation.ts, src/lib/fix-validation.server.ts, src/lib/config-fixes.ts, src/lib/clean-upgrade.ts, src/lib/npm-manifest.ts, src/lib/pypi-requirements.ts, src/lib/autofix-pypi.ts, src/lib/autofix-cargo.ts, src/lib/cargo-lock.ts, src/lib/npm-lockfile.ts, src/lib/npm-parent-upgrade.ts, src/lib/lockfile-parents.ts, src/lib/registry-credentials.server.ts, src/lib/fix-checks.ts, src/components/fix-dialog.tsx, src/lib/autofix-autopilot.ts, src/lib/autofix-autopilot.server.ts, src/lib/autofix-node.ts, src/lib/node-checkout.server.ts, src/lib/fix-pulls.ts, src/lib/fix-pulls.server.ts, src/lib/ai-triage.server.ts, src/lib/pull-request-format.ts
Tested by: tests/unit/pull-request-format.test.ts, tests/unit/autofix-pypi.test.ts, tests/unit/autofix-cargo.test.ts, tests/unit/fix-checks.test.ts, tests/integration/autofix-browser-github-e2e.test.ts, tests/integration/autofix-pypi-github-e2e.test.ts, tests/unit/fix-pulls.test.ts, tests/unit/ai-triage-unattended.test.ts, tests/rls/code-context.test.ts, tests/unit/deferred-findings-stay-open.test.ts, tests/integration/repository-page-github-e2e.test.ts, tests/integration/fix-pull-idempotent-github-e2e.test.ts, tests/unit/autofix-autopilot.test.ts, tests/unit/autofix-node-job.test.ts, tests/rls/autofix-settings.test.ts, tests/integration/autofix-autopilot-github-e2e.test.ts, tests/integration/autofix-node-validated-e2e.test.ts, tests/unit/clean-upgrade.test.ts, tests/unit/npm-manifest.test.ts, tests/unit/npm-lockfile.test.ts, tests/unit/npm-parent-upgrade.test.ts, tests/unit/lockfile-parents.test.ts, tests/unit/autofix-node-online.test.ts, tests/unit/scanner-node-scheduled.test.ts, tests/integration/scanner-node-run-once.test.ts, tests/integration/scanner-node-credentials.test.ts, tests/unit/scanner-node-credentials.test.ts, tests/unit/node-checkout.test.ts, tests/integration/scanner-node-archive.test.ts, tests/unit/autofix-npm-lockfile-plan.test.ts, tests/integration/fix-loop-github-e2e.test.ts, tests/integration/merge-webhook-closes-findings-e2e.test.ts, tests/integration/autofix-browser-github-e2e.test.ts, tests/unit/ai-fix.test.ts, tests/unit/fix-validation.test.ts, tests/unit/config-fixes.test.ts, tests/integration/fix-validation-e2e.test.ts, tests/integration/config-fixes-e2e.test.ts
A catalogue of code quality checks by category, severity and language, each switched on or off per organisation, run on every repository scan and kept out of security counts, grades and the pull request gate. Thirteen deterministic checks -- debugging leftovers, obfuscated code, dangerous SKILL.md instructions, weak hashes on credentials, sensitive data in logs, empty catch blocks, catastrophic regexes, SELECT *, TODOs, long functions, large files, deep nesting, long lines -- each tested on the look-alikes measured in real code. Every finding opens the same fix window as a security finding. Twelve more checks need judgement -- simpler equivalent code, commenting the why, single responsibility, recursion without a depth limit, division without a zero check -- and are read by the organisation's own model, three application files per scheduler run, largest first, each file again only when its content changes. A reply is kept only where the code it quotes is on the line it names, so an answer about code that is not in the file is dropped and counted.
Model-judged checks need a model route (the Anthropic sign-in or a key under Model providers) and cannot be switched on without one. Source leaves the product for them, under the organisation's AI policy and audited per call; a file the secrets scanner flags is never sent. Their findings are a reader's judgement, stored at medium confidence, and only a later model review of the same file closes them. The deterministic checks are line and brace based, not AST based, so function length and nesting depth are measured on the source as written.
Implemented in: src/lib/scanners/quality.ts, src/lib/code-quality.functions.ts, src/routes/_authenticated/code-quality.tsx, src/lib/quality-model.ts, src/lib/quality-model.server.ts
Tested by: tests/unit/code-quality.test.ts, tests/unit/quality-model.test.ts, tests/integration/quality-model-review.test.ts, tests/integration/quality-fix-merge.test.ts
Chains the checks an analyst would otherwise do by hand across five screens — known exploitation, reachability, internet exposure, data classification, fix availability, blast radius — and returns a trail of steps, a verdict where each claim cites the step that produced it, and the gaps it could not close. Also runs unattended from the scheduler on findings nobody has opened, selecting only those where the answer would change the work and notifying only when the picture actually changed.
Read-only by construction: acting tools are declared so they can be proposed, and are refused execution by the loop, because an agent that can act unattended is an agent that can be prompt-injected into acting. Tool selection is deterministic rather than model-driven, so the same finding investigated twice takes the same path. The step budget is bounded, and exhausting it is reported as a partial investigation rather than summarised into a confident answer. No model is called: the reasoning is code, which is why it can be tested. Unattended runs are off unless AEGIS_AUTO_TRIAGE=1, since running an agent over a customer's findings is a decision they should make rather than inherit from a deploy; conclusions are recorded as finding events and never applied to a finding's severity or status.
Implemented in: src/lib/agent.ts, src/lib/agent.functions.ts, src/lib/auto-triage.ts, src/lib/auto-triage.server.ts, src/lib/finding-context.server.ts
Tested by: tests/unit/agent.test.ts, tests/unit/auto-triage.test.ts, tests/unit/finding-context.test.ts, tests/integration/agent-e2e.test.ts, tests/integration/auto-triage-e2e.test.ts, tests/integration/handler-schema-e2e.test.ts
Compares a scan against one taken on the base branch and reports only the findings this change added, so a pull request against a repository with four hundred existing findings shows the one its author can still fix cheaply. Findings are matched on the same identity the queue uses, which excludes the line number, so a finding that moved because a line was inserted above it is not reported as new.
Run `aegis scan --format json` on the base branch, keep the output, and pass it as `aegis scan --baseline <file>`. Findings present on the base are filtered out before the report, the chosen output format and the `--fail-on` gate, so all three agree. A baseline that cannot be read reports the full list and says so rather than treating a missing file as a clean diff. A renamed file produces one fixed and one introduced finding rather than a guessed match.
Implemented in: src/lib/finding-baseline.ts, cli/aegis.ts
Tested by: tests/unit/finding-baseline.test.ts, tests/unit/cli-baseline-flag.test.ts
Checks whether a detected credential still authenticates, using the most harmless read-only call each provider offers: GitHub, GitLab, Slack, Stripe, OpenAI, Anthropic, Hugging Face, npm, SendGrid and Discord webhooks. With it on, the scheduler checks a few secret findings per run and re-checks each verdict weekly; a single finding can be checked from its details. A live credential becomes critical, a revoked one low, and the verdict survives rescans until the credential on that line changes.
Off until an admin turns it on on the Secrets page, because it transmits the credential to its own provider. The credential is never stored: it is re-read from the repository for one call, and only if it is still the value that was reported. Only the server can record a verdict; a member writing one directly is refused by the database. JWTs, database URLs, private keys, AWS keys and generic high-entropy strings have no read-only check and are never sent.
Implemented in: src/lib/secret-liveness.ts, src/lib/secret-liveness.functions.ts, src/lib/secret-validation.server.ts, src/components/secret-liveness.tsx
Tested by: tests/unit/secret-liveness.test.ts, tests/unit/secret-validation.test.ts, tests/rls/secret-liveness.test.ts
Provider-specific rules and entropy analysis over the current file tree, plus full commit-history scanning with Gitleaks on a scanner node. Each hit carries a keyed fingerprint of the value -- never the value -- so the same credential in several files is one issue to rotate; a hit in test, fixture, example or documentation code is rated low with the reason stated, unless its counterpart sits beside it. The commit history is also read on the control plane through the organisation's own GitHub connection, with no node and no clone: every commit's added and removed lines are scanned, stored as masked events, and folded into one lifecycle per credential -- first committed, still present or the commit that removed the last copy -- across a walk that resumes over many passes and then follows each push. A credential removed from the code but still in history is one finding keyed on its fingerprint; one the tree scan still reports is left to that finding. Every hit also reads the value and its line for the signs that it was typed rather than issued -- a run of consecutive characters, a repeated block, a handful of distinct symbols, a provider's documentation example, a name like fake_token -- and a hit with such a sign is rated low with each reason stated, unless a production name or a paired counterpart says otherwise; the reasons are shown on the finding as 'Why it is rated this way'. Measured against 20,000 random tokens per provider shape, fewer than 1 in 5,000 real-shaped values is called typed. History findings store the sign ids with each event and are rated by the same function.
Gitleaks history scanning needs a scanner node. The API-based history walk needs only the GitHub connection and reads up to 1000 commits per repository, twenty per pass; a longer history is recorded as depth-limited rather than complete. Whether a detected credential still works is the separate secret-liveness capability, off until an admin turns it on.
Implemented in: src/lib/scanners/gitleaks.ts, src/lib/scanners/secrets-scan.ts, src/lib/scanners/secret-plausibility.ts, src/lib/secret-history.ts, src/lib/secret-history.server.ts
Tested by: tests/unit/gitleaks.test.ts, tests/integration/gitleaks-history-e2e.test.ts, tests/unit/database-url-secrets.test.ts, tests/unit/private-key-body.test.ts, tests/unit/secret-correlation.test.ts, tests/unit/secret-history.test.ts, tests/integration/secret-history-persist.test.ts, tests/unit/secret-plausibility.test.ts
A built-in pattern engine covering JavaScript, TypeScript, Python, Go and others, plus source-to-sink dataflow for JavaScript, TypeScript, Python, Go and Java that traces an untrusted value from where it enters to where it is dangerous and reports the path step by step, across function and module boundaries for all but Java. Semgrep, including taint-mode rules, runs on a scanner node.
Dataflow follows values by reading assignments line by line, and a second pass crosses function boundaries and imports: in JavaScript and TypeScript by relative or package import; in Python by `from x import y` (with `as`), `import x` called as `x.y`, and relative imports, following f-strings, % and .format(); in Go by same-package resolution (any .go file in the same directory), following := and =, fmt.Sprintf and concatenation. In Python and Go a value the callee returns is tainted in the caller, so a sink after the call is found, and a flow whose every hop resolved to exactly one definition is reported at high confidence; a hop through a decorated Python function, a `*args` parameter list or a Go method (whose receiver type is not resolved) keeps the file-local confidence. Not modelled in either: dynamic dispatch, class methods and `self.` calls, calls into another Go package, interface method resolution, goroutines, channels and closures; the language matrix on the repository page states this per language. A source used directly in a sink, with no variable in between, is reported at high confidence because nothing about the propagation is inferred. Not tracked: values passed as call arguments from a handler parameter, and values crossing a callback boundary other than the element of a collection the request sent (`items.map(item => ...)`). A price the buyer's request sets -- a Stripe `unit_amount` inside `price_data`, or a PaymentIntent or Charge `amount` -- is followed with the reverse rules: converting or rounding does not clean it, while using it as a key (an index into a price table, the argument to a lookup) or comparing it with a server amount does. A buyer's quantity times a server price, a catalogue price an operator defines, and a donation, tip, top-up or deposit named near the charge are not reported. Measured on 70 real Stripe handlers from public repositories and 17810 files of the owner's own projects: 35 checkouts at a price the request chose, and no false positives after the exclusions above; the exclusions are name and shape based, so a donation page that never says so is still reported. A Stripe webhook that takes the request body as the event with no signature check anywhere in the file is reported as critical; measured on 750 public webhook files it reported 22, each read and confirmed, and nothing on the owner's projects. A file that verifies anywhere is not reported, even when that verification only runs if a secret is configured, because a file cannot show what production sets. An AI SDK constructed with `dangerouslyAllowBrowser: true` and a key from a client-bundled variable is reported when the variable's name does not already give it away (`VITE_API_KEY`); where it does, the key is reported once on its own line. On 286 public files using the flag that is 9 constructors, each read and confirmed. Every finding states its own limit. A finding whose code only moved (lines inserted above it) keeps its row, first detection, triage and tickets, when the scanner, file, rule and recorded code match; a move to another file, a rename, or an engine that records no code (secrets) is not followed and still reads as one fixed and one new. Semgrep needs a registered scanner node. An organisation can write its own line rules on the Policies page -- a regular expression, the file types it applies to, a severity -- tried against a pasted snippet before saving; every repository scan, pull-request gate and merge verification runs them with the built-in rules' comment skipping, test-code downgrade and inline suppression, and closes their findings when the line is gone. Patterns that can backtrack catastrophically (a repeated group that itself repeats, backreferences, more than three unbounded repeats) are refused at save time, and a custom rule reads at most 500 characters of a line; a slow but bounded pattern is still possible. Custom rules are single-line patterns, not dataflow, and cannot follow a value between lines. CodeQL queries cannot be run: there is no CodeQL engine on the node or in the Worker. Measured against labelled benchmarks nobody here wrote (docs/execution/DETECTION_BENCHMARKS.md): on the half of the OWASP Benchmark for Python never inspected while changing the engine, the score (TPR minus FPR) is 0.932, up from 0.158, finding all 229 real vulnerabilities with 13 of 386 safe cases flagged; DVNA 6 of 6 and NodeGoat 6 of 6 documented defects. A Python shell call or deserialisation whose input is provably fixed text (literals, constant f-strings and concatenation on every path reaching it, including arms a constant condition decides) is not reported; anything the proof cannot establish still is. A guard the engine recognises -- a host allowlist with an https check before a redirect, a '..' or apostrophe refusal, a string-literal check before eval -- is recognised only in the forms written in docs/execution/DETECTION_BENCHMARKS.md; a guard written differently still reports. The deterministic code audit (B03) runs on every repository, pull-request and merge-verification scan alongside the pattern engine: a handler that passes a request-supplied organisation id into a service-role read with no membership check is written as a `sast` finding at high severity (scanner `aegis-code-audit`, rule `audit:unguarded-privileged-read`) and blocks the merge gate under the default policy like any other high finding. The audit's `possible` findings -- a verifier that only tests presence, a limit checked outside a transaction -- are a count on the engine summary, never a row, and its whole-tree rule (a control written and never read) runs only from `aegis audit`, because a scan holds a page and a setting read on the next page is not dead.
Implemented in: src/lib/scanners/semgrep.ts, src/lib/scanners/dataflow.ts, src/lib/scanners/interprocedural.ts, src/lib/scanners/language-coverage.ts, src/lib/scanners/sast-scan.ts, src/lib/scanners/custom-sast.ts, src/components/custom-sast-rules-panel.tsx, src/lib/finding-location.ts, src/lib/finding-moves.ts, src/lib/code-audit.ts, src/lib/scanners/code-audit-findings.ts, src/lib/pr-gate.ts
Tested by: tests/unit/custom-sast.test.ts, tests/unit/code-audit.test.ts, tests/unit/code-audit-in-gate.test.ts, tests/unit/python-detection.test.ts, tests/unit/js-detection.test.ts, tests/unit/custom-sast-in-scan.test.ts, tests/rls/custom-sast-rules.test.ts, tests/unit/finding-moves.test.ts, tests/integration/finding-moves.test.ts, tests/unit/semgrep.test.ts, tests/unit/dataflow.test.ts, tests/unit/detection-benchmark.test.ts, tests/unit/weak-hash-helper.test.ts, tests/unit/finding-location.test.ts, tests/unit/interprocedural-local-source.test.ts, tests/unit/interprocedural-go-python.test.ts, tests/unit/sast-client-env-secret.test.ts, tests/unit/dataflow-client-price.test.ts, tests/unit/sast-stripe-webhook.test.ts, tests/unit/sast-browser-ai-key.test.ts
Line, function and branch coverage per repository and file, from the LCOV report CI already writes (Jest, Vitest, c8, pytest-cov, cargo-llvm-cov, Go via gcov2lcov), uploaded with a token carrying only coverage:write. Each code finding says whether a test executes its line, from the latest report for its repository. A file the report does not mention is shown as no data rather than untested, a file with nothing to cover as unknown rather than 0% or 100%, a report naming no file inside the repository is refused rather than stored as zero coverage, and one over 10,000 files is refused whole rather than truncated.
Implemented in: src/lib/coverage.ts, src/lib/coverage.functions.ts, src/routes/api/public/v1/coverage.ts, src/routes/_authenticated/coverage.tsx, src/components/finding-coverage.tsx
Tested by: tests/unit/coverage-lcov.test.ts, tests/integration/coverage-upload-e2e.test.ts
Findings in the Problems panel while the file is still open, and package vetting before an install. Shells out to the same CLI the pipeline runs, so the editor cannot disagree with CI, and reports a scanner that did not run rather than showing an empty panel a developer would read as clean. Editor-side severity filtering never suppresses anything in CI.
Install the Aegis CLI and put it on PATH, or set aegis.cliPath. Scanners are separate installs; whichever are present run, and whichever are not are reported as not run. Only VS Code is implemented — there is no JetBrains or Neovim client.
Implemented in: ide/vscode/src/extension.ts, cli/aegis.ts
Tested by: tests/integration/vscode-extension-e2e.test.ts
What you are installing and where it came from, before an advisory exists to warn you.
For a vulnerability whose fix landed in a version you cannot take, this finds the upstream fix commit, separates the source change from the upstream's own regression test, and checks whether it applies to the version you are pinned to. The test is the point: it is evidence written by the people who understood the bug, so a patch that carries one gives you something that fails before and passes after.
Aegis produces the patch and never the package. Publishing a patched dependency means owning its security, its correctness and its next CVE for as long as anyone runs it, and an abandoned backport library is worse than none because the customer is pinned to a fork nobody maintains. You apply it yourself, through patch-package or a vendored fork, knowingly. Where the fix was written against later code and does not apply to your version, that is reported as the headline with the missing lines named, rather than offered anyway.
Implemented in: src/lib/backport-patch.ts, src/lib/backport-apply.ts
Tested by: tests/unit/backport-patch.test.ts, tests/unit/backport-apply.test.ts
Says how likely a version bump is to break something before a reviewer approves it: a patch bump is the maintainer promising nothing observable changed, a major bump is a statement that something did, and an automated fix that presents both identically is how the mechanism gets switched off after an outage.
Implemented in: src/lib/upgrade-breakage.ts
Tested by: tests/unit/upgrade-breakage.test.ts
OCI manifests and config blobs are read live over the registry API. Layer CVEs run through Trivy and Grype on a scanner node, with hardened base image recommendations.
Layer scanning needs a scanner node. Private registries are supported: store a credential per host and it is used at pull time. Base-image hardening advice compares your own scans of the current and suggested images and reports the difference; where either has not been scanned it says the saving is unmeasured rather than showing a count. Maintaining pre-patched base images, as a vendor-supplied catalogue, is not something Aegis does: that is a continuous commitment to rebuild other people's software, not a feature.
Implemented in: src/lib/registry.server.ts, src/lib/scanners/trivy.ts, src/lib/scanners/grype.ts, src/lib/base-images.ts, src/lib/base-image-compare.functions.ts
Tested by: tests/unit/registry.test.ts, tests/unit/trivy.test.ts, tests/unit/grype.test.ts, tests/unit/base-images.test.ts, tests/rls/base-image-compare.test.ts, tests/integration/trivy-image-e2e.test.ts
Assesses a repository's dependencies for typosquatting, dependency confusion against the organisation's private scopes, install scripts that read credentials or download code, maintainer takeover and dormant revival, scored into a reputation with the evidence attached.
Reads the lockfile so packages are assessed at the version actually installed: bun.lock and package-lock.json give the full tree with each finding naming the direct dependency that pulls it in, poetry.lock gives versions and edges but cannot say which packages the project asked for directly, and go.sum gives exact versions with no edges at all because Go resolves those from each module's own go.mod. Where a path cannot be known it is not invented. Bounded at 500 packages per scan with the number omitted reported. Maven, NuGet, RubyGems and crates read metadata but have no lockfile walk. The name-based checks -- typosquatting and dependency confusion -- also run inside every repository scan with no registry traffic, deriving the organisation's own scopes from its root manifest rather than waiting for a policy nobody fills in; the registry-backed signals (maintainer changes, publication history, install scripts) still require the full scan.
Implemented in: src/lib/supply-chain/reputation.ts, src/lib/supply-chain/dependency-tree.ts, src/lib/supply-chain/scan.ts, src/lib/supply-chain/offline-signals.ts, src/lib/supply-chain.functions.ts
Tested by: tests/unit/dependency-tree.test.ts, tests/unit/dependency-tree-ecosystems.test.ts, tests/unit/supply-chain-reputation.test.ts, tests/unit/supply-chain-scan.test.ts, tests/unit/offline-supply-chain.test.ts
Authenticates to private registries by reading the registry's own challenge and exchanging a stored credential for a token scoped to pull on one repository. Recognises Docker Hub, GHCR, ECR, GAR, ACR, Harbor and generic v2 registries.
Credentials are stored and rotated per organisation from the containers page, encrypted in integration_secrets and never in the connector config, and a credential is verified against a repository the operator names before it is stored: a scoped token that authenticates but cannot pull is refused rather than saved. A host that answers without credentials is recorded as reachable-only, because a public repository proves nothing about the credential. Rotation leaves the working credential in place when the new one fails.
Implemented in: src/lib/registry-auth.ts, src/lib/registry-connections.functions.ts, src/lib/registry.server.ts
Tested by: tests/unit/registry-connections.test.ts, tests/unit/registry-auth.test.ts, tests/integration/registry-auth-e2e.test.ts
CycloneDX 1.5 and SPDX 2.3 export, with licence policy evaluation from deps.dev, and Syft inventory on a scanner node. VEX statements persisted per organisation with an audit trail: recorded from the finding drawer or imported as an OpenVEX document (validated statement by statement, refused ones named, source document hashed), listed and revoked on the Supply Chain page. A not_affected statement closes the findings it covers, keeps them closed on later scans, takes them out of the merge gate with the reason in the pull-request comment, and is shown on the Dependencies page; fixed leaves the gate pending the next scan; under_investigation annotates and blocks. Revoking reopens what the statement closed. OpenVEX export carries the recorded decisions.
Implemented in: src/lib/sbom.ts, src/lib/scanners/license.ts, src/lib/scanners/syft.ts, src/lib/vex.ts, src/lib/vex.functions.ts, supabase/migrations/20260929210000_vex_import_and_revocation.sql
Tested by: tests/unit/license.test.ts, tests/unit/syft.test.ts, tests/unit/vex-output.test.ts, tests/unit/vex-persisted.test.ts, tests/integration/syft-sbom-e2e.test.ts, tests/rls/vex-statements.test.ts, tests/rls/vex-persistence.test.ts, tests/integration/vex-statement-browser.test.ts
Dependency vulnerabilities from OSV including transitive paths, enriched with CISA KEV and EPSS, with reachability analysis and end-of-life detection. The installed version is read from the lockfile, not from the floor of a package.json range. Every repository's whole dependency set -- every manifest, in one request -- is scanned daily, and every half hour while a fix pull request is open, so a merged upgrade closes its findings even in a monorepo. Advisory detail is stored once for every tenant and filled in for findings a scan had no request left to enrich.
Every vulnerability here comes from a published advisory. Pre-CVE and zero-day intelligence is deliberately not offered: producing it means analysts, paid-for sources and a disclosure pipeline, which is a threat-intelligence operation rather than a feature, and a product that implied it had one would be claiming knowledge it does not have. A vulnerability nobody has published is therefore absent rather than predicted, and the gap is stated instead of filled with a guess. Measured on 27 September for the case that prompted it: Aikido opened a fix for postal-mime 2.7.5 under its own identifier AIKIDO-2026-297362; four public sources -- OSV, the GitHub advisory database (queried for reviewed, unreviewed and malware entries), npm's audit endpoint and deps.dev -- return nothing for that version, and the upstream 2.7.6 release notes call the change a bug fix (unbounded message/rfc822 recursion). Aegis does not report it. Were an advisory published naming 2.7.6 as the fix, the parent route would choose resend ^6.28.1 from the live registry (checked with the same function; the pull request itself was not exercised for this package).
Implemented in: src/lib/sca.functions.ts, src/lib/reachability.ts, src/lib/eol.ts, src/lib/dependency-inventory.ts, src/lib/dependency-inventory.server.ts, src/lib/advisory-detail.server.ts
Tested by: tests/unit/advisory-detail.test.ts, tests/integration/dependency-inventory-github-e2e.test.ts, tests/rls/reopen-redetected.test.ts, tests/unit/reachability.test.ts, tests/unit/eol.test.ts, tests/integration/reachability-e2e.test.ts, tests/unit/recorded-refusals.test.ts
Configuration, identity and data exposure across cloud accounts.
Snapshot-based scanning of EC2 instances and EBS snapshots, extracting installed packages and matching them against known vulnerabilities without running anything inside the VM. Idle snapshots are swept and costed on AWS, Azure and GCP; AWS is the only provider Aegis creates or deletes snapshots on.
Runs on a scanner node with snapshot read permission on the connected cloud account. The Azure and GCP sweeps are read-only: Aegis does not delete their snapshots, including its own, because the decision to let it destroy resources was made about AWS and is the owner's to extend.
Implemented in: src/lib/scanners/vm.ts, src/lib/snapshot-lifecycle.ts, src/lib/cloud-snapshots.server.ts
Tested by: tests/unit/vm-scanner.test.ts, tests/integration/vm-scan-e2e.test.ts, tests/unit/snapshot-lifecycle.test.ts, tests/unit/cloud-snapshots.test.ts
Read-only posture scanning of S3, EC2, RDS and IAM using signed SigV4 calls, against CIS rule packs. The sweep also reads what the shadow-asset engine judges on: every load balancer with its healthy-target count (DescribeTargetGroups and DescribeTargetHealth per balancer), every instance with its state and, for a stopped one, when it stopped, and every elastic IP with whether anything is attached. All within the SecurityAudit policy; a reader that fails is a named read error, never an empty fleet.
Connect an AWS account with read-only credentials under Integrations.
Implemented in: src/lib/aws.server.ts, src/lib/cloud-posture.ts, src/lib/cloud-scaling.ts
Tested by: tests/unit/aws-cspm.test.ts, tests/unit/cloud-posture.test.ts, tests/unit/cloud-scaling.test.ts, tests/unit/describe-scaling-groups.test.ts, tests/unit/shadow-assets-live.test.ts
Azure and Google Cloud posture scanning against CIS rule packs, alongside AWS.
Connect an Azure or GCP account under Integrations. Posture is read per connected account. Discovery on the Cloud page lists every subscription the Azure credential can see and every project the GCP credential can search (with its parent folder), across all pages, and names those with no credentials in Aegis; it does not connect or scan them, and a credential that cannot list says so instead of reporting an empty tenant.
Implemented in: src/lib/azure.server.ts, src/lib/gcp.server.ts, src/lib/multi-cloud.functions.ts, src/lib/scanners/scaling-findings.ts
Tested by: tests/unit/multi-cloud.test.ts, tests/unit/cloud-scaling-readers.test.ts, tests/unit/scaling-findings.test.ts, tests/unit/cloud-discovery.test.ts
Enumerates the accounts in an AWS organisation and reports which are active but unconnected, so an account nobody remembered to connect stops being invisible. Also flags recorded resources that look unowned: idle, untagged, unattached or referenced by no connected repository.
Needs organisation-level credentials to enumerate accounts. Shadow assets are read from stored metadata, which the AWS sweep now fills with what the judgement needs: healthy targets behind each balancer, whether each elastic IP is attached, and when a stopped instance stopped. A resource the live readers do not cover (buckets, databases) still carries only last_seen, and one whose provider was never recorded is reported as unknown rather than assumed: a guessed provider reaches the finding, the dedupe key and every cross-cloud rule, where it is both wrongly matched and wrongly missed. Azure and GCP sweeps do not yet read attachment or activity, so their resources are judged on tags alone. No AWS account is connected to this deployment, so the live readers are proven against documented response shapes only (O4).
Implemented in: src/lib/cloud-discovery.ts, src/lib/cloud-discovery.functions.ts
Tested by: tests/unit/cross-cloud-rules.test.ts, tests/unit/cloud-discovery.test.ts
Classifies what a datastore holds from its column, table and object names, then weights its exposures by that: a public bucket of payroll data is critical and a public bucket of build logs is not. Covers PII, financial, health and credential data. Code-first as well (`aegis data`): reads migrations, models and handlers for sensitive field names and the lines that log, return or export them, and follows a field across files through the import graph, so a row read in one module and sent by another is reported with both ends and every hop between. A route returning only a derived scalar or a masked copy is not reported, and a partial chain is not reported. The cross-file pass also runs in every hosted repository scan with SAST selected, writing each exposure as a `sast` finding in category `data-exposure` (CWE-359, medium) keyed on its sink line, over the same page of files the dataflow pass reads -- so a carrier and its caller on different pages are not joined.
Metadata only by default. Contents are never read by Aegis, because copying customer data into it to classify it would make this database a second copy of everything sensitive they own. Where a store's names say nothing, an admin can consent -- per store, per column, with the authority it rests on, expiring within a year, withdrawable -- to a scanner node reading a bounded sample (at most 1000 rows) where the data lives. The node reads the database configured on it (AEGIS_DSPM_DSN, sealed from the code it runs); Aegis never holds or sends a connection string. The consent is read again when the node claims the job, so one withdrawn while the job waited stops it, and only counts per category come back: a submission carrying anything else is refused. Proven with the shipped node against a real Postgres whose role could read only the consented column, and on production on 29 September: the deployed claim route handed a temporary node the consented column and a 20-row bound, the submission produced one finding (20 of 20 values matched) with no value from the table in any production row, and after the consent was withdrawn the next claim failed the queued job with the reason. The consent form on the Cloud page has not been clicked on production: it is shown only for a datastore metadata could not classify, and production has none. The code-first analysis runs from the CLI only; it is not yet part of the hosted repository scan.
Implemented in: src/lib/dspm.ts, src/lib/dspm.functions.ts, src/lib/dspm-sampling.ts, src/lib/dspm-sampling.functions.ts, src/lib/dspm-code.ts, src/lib/dspm-cross-file.ts, src/components/sampling-consents.tsx
Tested by: tests/unit/dspm.test.ts, tests/unit/dspm-sampling.test.ts, tests/unit/dspm-code.test.ts, tests/unit/dspm-cross-file.test.ts, tests/rls/dspm-sampling-consents.test.ts, tests/integration/dspm-claim.test.ts, tests/integration/dspm-node-sample.test.ts, tests/integration/dspm-consent-functions.test.ts, tests/integration/cli-e2e.test.ts
A built-in rule set runs in-runtime across Terraform, CloudFormation, Kubernetes manifests, Helm, ARM, Bicep and Dockerfiles, with Checkov and Trivy config mode on a scanner node. GitHub Actions workflows and composite actions are read for script injection from attacker-controlled event fields, privileged workflows that check out a fork's code, third-party actions on a movable tag, write-all tokens and re-enabled insecure commands -- measured against zizmor's own labelled corpus, where it reports every high-severity line zizmor does for those checks and nothing more. Supabase migrations are replayed as one history to find public tables no migration protects with row-level security, tables whose RLS a later migration switched off, and write policies whose condition is `true`; a history the scan could not read in full is reported as not judged rather than clean.
Checkov and Trivy config scanning need a scanner node.
Implemented in: src/lib/scanners/checkov.ts, src/lib/scanners/iac-scan.ts, src/lib/scanners/sarif.ts, src/lib/scanners/workflow-scan.ts, src/lib/scanners/supabase-rls.ts
Tested by: tests/unit/checkov.test.ts, tests/unit/supabase-rls.test.ts, tests/integration/public-tables-behind-rls.test.ts, tests/unit/workflow-scan.test.ts, tests/unit/workflow-scan-benchmark.test.ts, tests/unit/trivy-config.test.ts, tests/unit/sarif.test.ts, tests/integration/checkov-iac-e2e.test.ts
Workload and RBAC posture evaluated from manifests, covering privileged containers, host mounts, missing security contexts and over-broad roles.
Analyses manifests supplied by a connected repository. Running workloads are read from a cluster by the separate live cluster connector (kubernetes-live), over the Kubernetes API with a read-only service-account token; it has been exercised against k3s, not against EKS, AKS or GKE specifically, and there is no managed-provider sign-in that discovers clusters for you.
Implemented in: src/lib/kubernetes.ts
Tested by: tests/unit/kubernetes.test.ts
Reads pods, RBAC, services, network policies and Secret metadata from a running cluster over its API, and runs the existing posture rules against what is actually deployed rather than what a manifest says. Also collects running image references, which is the link from a workload back to the repository that built it.
Apply the generated read-only RBAC manifest and supply a service-account token. The connector can only get and list: it cannot create, modify or delete anything, and it reads Secret metadata without ever fetching Secret contents.
Implemented in: src/lib/kubernetes-cluster.ts, src/lib/kubernetes-cluster.functions.ts, src/lib/kubernetes.ts
Tested by: tests/unit/kubernetes-cluster.test.ts, tests/integration/kubernetes-cluster-e2e.test.ts
Broad CIS and compliance coverage across AWS, Azure, GCP, Kubernetes and infrastructure-as-code, parsed from Prowler's OCSF output by the server.
Runs on a scanner node with Docker. Prowler's own severities are never trusted directly: the server recalculates severity from its own rules.
Implemented in: src/lib/scanners/prowler.ts
Tested by: tests/unit/prowler.test.ts, tests/integration/prowler-e2e.test.ts
What is happening in production, as opposed to what is in the repository.
Judges automated traffic by behaviour rather than by user agent: request rate, whether assets a browser would load are requested, cookie and Referer handling, network type, and whether one address presents several identities. Responses are graded from observe through throttle and challenge to block, and a verified crawler is exempt.
Challenges are proof of work, not CAPTCHA: no third party sees the visitor and nothing is tracked. Supply a challengeSecret to the SDK to enable them; without one a client over the rate limit is refused outright, because a challenge nobody can verify is worse than none. Default difficulty is 12 bits, about 30ms here, chosen by measurement after 16 bits came back at 289ms median and 1.7s worst case. It raises the cost of volume; it does not detect humanity and does not stop a determined attacker solving one per session. Reputation feeds are not integrated.
Implemented in: src/lib/runtime-rules.ts, src/lib/bot-defence.ts, src/lib/bot-challenge.ts
Tested by: tests/unit/runtime-extended.test.ts, tests/unit/bot-defence.test.ts, tests/unit/bot-challenge.test.ts, tests/integration/bot-challenge-sdk.test.ts
An optional Coraza-based reverse proxy enforcing Aegis rules in front of an application, with monitor and blocking modes and per-route exceptions. Virtual patches turn a confirmed finding into a temporary, expiring rule scoped to the affected route and parameter, generated as a Coraza rule so the proxy and the in-app SDK enforce the same definition.
Deploy the WAF binary in front of the application: `go build` in waf/, then run it with --upstream. Verified as a deployment rather than in-process — the compiled binary proxies to a real application, blocks SQL injection and path traversal with 403 in block mode, passes both through in the default detect mode, and refuses to start when a --rules directory is missing or empty. A virtual patch is never a fix: the vulnerable code is unchanged, the finding stays open, and every patch expires so it cannot become permanent by accident.
Implemented in: waf/main.go, src/lib/virtual-patch.ts, src/lib/virtual-patch-feed.ts
Tested by: waf/main_test.go, tests/integration/waf-deploy-e2e.test.ts, tests/unit/virtual-patch.test.ts, tests/integration/virtual-patch-waf-e2e.test.ts, tests/unit/virtual-patch-feed.test.ts
In-app SDKs for Node, Python, Java, PHP, .NET, Go and Ruby (Rack) reporting attacks and enforcing monitor or block mode, with per-endpoint policy. Each carries the manifest its ecosystem installs from, and the Node SDK is verified by packing it, installing the tarball into an empty directory, and driving the installed code.
Install an SDK in the protected application and issue an agent token. Until an agent reports, the page shows no data rather than an empty success. None of the seven are published to a registry yet, so installation today means building from this repository. Protection is decided locally and reporting is best-effort, so an unreachable control plane changes neither what is blocked nor what is allowed.
Implemented in: src/lib/runtime-protection.ts, src/lib/runtime-rules.ts, src/lib/runtime.functions.ts, sdk/node/package.json, sdk/java/pom.xml, sdk/php/composer.json, sdk/ruby/aegis-security.gemspec
Tested by: tests/unit/runtime-protection.test.ts, tests/unit/runtime-extended.test.ts, tests/integration/sdk-parity-e2e.test.ts, tests/integration/sdk-package-e2e.test.ts, tests/integration/sdk-ruby-e2e.test.ts
Fresh traffic to an endpoint raises the priority of findings on that endpoint, with the observation named in the explanation. Silence never lowers one: a collector that has stopped reporting, an endpoint nobody visited this week, and an asset the collector cannot speak for all leave the score exactly where it was.
Runtime events record HTTP requests, so they are evidence about a web application and nothing else. A dependency, container image or cloud account gets no runtime verdict at all, because deriving "this package was observed at runtime" from a request to a path would be an invention dressed as telemetry. Dependency-level runtime evidence needs a collector that reports it, and none is integrated. The verdict is never negative: `computeRisk` has a branch for telemetry that looked and saw nothing, and this never reaches it, because absence over a window is weak evidence and a wrong negative is the one answer that makes a finding look safer than it is.
Implemented in: src/lib/runtime-evidence.ts, src/lib/persist.server.ts
Tested by: tests/unit/runtime-evidence.test.ts
What is reachable from the internet, including what nobody meant to publish.
Builds an API inventory from observed traffic and compares it against the specification, naming shadow endpoints that serve traffic and appear in no spec, and zombie endpoints that are specified and serve nothing.
Report observations from the runtime SDK or a proxy; nothing is discovered without traffic. Endpoint shapes only are stored -- method, normalised path, status codes, and parameter names with inferred types -- because recording real requests would make Aegis a copy of every request its customers serve, which is a worse liability than the undocumented endpoint it found.
Implemented in: src/lib/api-discovery.ts, src/lib/api-discovery.functions.ts
Tested by: tests/unit/api-discovery.test.ts
OpenAPI inventory and static analysis, GraphQL schema analysis covering introspection exposure and destructive mutations, and Schemathesis contract testing on a scanner node.
Schemathesis needs a scanner node and a verified host. OpenAPI documents are read as JSON.
Implemented in: src/lib/api-schema.ts, src/lib/graphql-scan.ts, src/lib/scanners/schemathesis.ts
Tested by: tests/unit/graphql-scan.test.ts, tests/unit/schemathesis.test.ts, tests/integration/schemathesis-api-e2e.test.ts
Derives relationships between assets from evidence the scanners already collected — an image label naming its source repository, a pod naming its image, a domain resolving to an address a resource holds — and walks them to answer which repository produced the container running behind a given domain. Attack paths are deterministic and every step cites its evidence.
Implemented in: src/lib/attack-path.ts, src/lib/asset-graph.ts, src/lib/asset-edges.ts, src/lib/asset-types.ts
Tested by: tests/unit/asset-graph.test.ts, tests/unit/asset-edges.test.ts, tests/unit/asset-types.test.ts, tests/unit/graph-relation-vocabulary.test.ts
Certificate transparency enumeration, DNS resolution, HTTP fingerprinting and dangling CNAME detection, against DNS-verified domains only. A separate posture check reads the apex's SPF, DMARC, CAA and DNSSEC from public DNS, notes DKIM keys at the common selectors, and fetches its security.txt; a lookup that does not answer produces no finding, and a DNSSEC failure is read from inside the zone, where dnssec-failed.org shows it, rather than from the parent's DS record, which still validates. A web-surface pass reads up to twelve same-origin pages, robots.txt and the sitemap, the site's own scripts and their source maps, and the conventional OpenAPI paths; it records the technologies the site states, the third parties whose scripts run on it, CDN-loaded libraries, and the API, GraphQL and WebSocket endpoints its client code names, links them in the asset graph, and grades CSP, HSTS, framing, Permissions-Policy, CORS, redirects, forms, Subresource Integrity, public source maps and public API descriptions. It runs daily for the least recently crawled verified domain and on demand, inside a Durable Object, and never fetches outside the verified apex. After each pass the application's endpoints are matched against the routes every repository declares: when one repository explains at least two distinct endpoints, one of them more than a single segment deep, and no other explains as many, the application is linked built_from that repository with the matched routes as evidence, and every open code finding in a file that declares a matched route is marked internet-exposed with the URLs that reach it and its risk rescored. Domain intelligence reads each verified domain's registration from RDAP (registrar, expiry, name servers, transfer lock), the network each address sits in (reverse DNS and the Team Cymru ASN service), every certificate certificate transparency holds, and whether look-alike domains resolve; it reports an expiring registration, a missing transfer lock, an expiring newest certificate, a valid certificate from a CA the domain's CAA does not allow, and resolving look-alikes. Each observer keeps its latest snapshot and writes one row per change against the previous one -- a new address, name server, certificate or network, a lost header, a new third party or endpoint -- and the Attack Surface page shows them as Latest changes. A source that does not answer produces no finding, closes nothing and records no change. A TLS check on a scanner node pins one OpenSSL handshake to each of TLS 1.0, 1.1, 1.2 and 1.3 and reads the certificate and chain OpenSSL verifies against the system trust store; it reports accepted TLS 1.0 or 1.1, no modern version, an expired or expiring certificate, a self-signed one, a name mismatch, a missing intermediate and an RSA key under 2048 bits. It needs a verified domain but no active-test approval, because a handshake carries no payload, and a host whose certificate could not be read is a failed scan rather than a clean one.
Verify domain ownership before a host can be scanned. Port discovery is a TCP connect scan that additionally needs a recorded approval covering active_safe testing, and it runs on a scanner node: Workers has no outbound TCP API, and a scan from the control plane would report what Cloudflare can reach rather than what your network exposes.
Implemented in: src/lib/domains.functions.ts, src/lib/dns.server.ts, src/lib/domain-posture.ts, src/lib/web-surface.ts, src/lib/web-surface.server.ts, src/lib/endpoint-correlation.ts, src/lib/endpoint-correlation.server.ts, src/lib/domain-intel.ts, src/lib/domain-intel.server.ts, src/lib/surface-changes.ts, src/lib/scanners/port-scan.ts, src/lib/scanners/tls-scan.ts, src/lib/scanners/tls-session.ts, src/lib/scanners/zone-transfer.ts
Tested by: tests/rls/active-scan-auth.test.ts, tests/unit/domain-posture.test.ts, tests/unit/web-surface.test.ts, tests/integration/web-surface-persist.test.ts, tests/unit/endpoint-correlation.test.ts, tests/integration/endpoint-correlation-persist.test.ts, tests/unit/domain-intel.test.ts, tests/integration/domain-intel-persist.test.ts, tests/unit/port-scan.test.ts, tests/integration/port-scan-sockets.test.ts, tests/unit/tls-scan.test.ts, tests/unit/tls-session.test.ts, tests/integration/tls-scan-node.test.ts, tests/unit/zone-transfer.test.ts, tests/integration/zone-transfer-node.test.ts
Re-testing of open findings under a written authorisation that an admin approves, scoped to verified domains and bounded by an expiry. Discovery pentests probe a fixed read-only set, cross-role and role-reach comparisons, and in whitebox mode every parameter-free GET route the connected repository declares. A scanner-node validation stage now confirms reflected XSS, boolean SQLi, traversal and controlled open redirects with bounded evidence and high-confidence findings.
The implemented validation classes run only on a registered scanner node after active_injection approval and persist bounded evidence. SSRF OOB, IDOR/cross-role evidence, scheduled retest execution and explicit 429/backoff degradation are absent, and expired authorisation stops the run. Whitebox reads Express-style, Flask, FastAPI and Spring declarations, Django urlconfs with DRF routers, Laravel and Rails route files, and Next.js file routes; declarations it cannot follow are counted, so the probed surface is a lower bound.
Implemented in: src/lib/test-approval.functions.ts, src/lib/validation.functions.ts, src/lib/pentest.functions.ts, src/lib/pentest-probe.ts, src/lib/source-routes.ts, src/lib/pentest-validation.ts, src/lib/scanners/catalog.ts, src/lib/nodes.ts, src/routes/api/public/nodes/jobs.ts, scanner-node/index.ts
Tested by: tests/unit/test-approval.test.ts, tests/rls/test-approvals.test.ts, tests/unit/source-routes.test.ts, tests/unit/django-routes.test.ts, tests/unit/django-drf-routes.test.ts, tests/unit/laravel-routes.test.ts, tests/unit/rails-routes.test.ts, tests/unit/rails-minimal-routes.test.ts, tests/integration/pentest-pipeline.test.ts, tests/integration/pentest-node-e2e.test.ts, tests/integration/pentest-validate.test.ts, tests/integration/whitebox-pentest-github-e2e.test.ts
Safe header, cookie, banner and exposed-path checks in-runtime, plus Nuclei templates and ZAP baseline crawling on a scanner node. Authenticated crawling resolves a stored credential only at node claim time.
Runs only against DNS-verified domains with recorded authorisation, on a daily schedule as well as on demand -- the in-runtime header and TLS checks previously ran only when somebody pressed a button, and a configuration change nobody thought to re-check went unnoticed until they did. Parameter fuzzing (payload injection into query and path) needs a URL carrying a parameter and a recorded active_injection approval, and is refused rather than downgraded when either is missing: measured against a page reflecting a query parameter unescaped, the template corpus sent 10,042 requests and missed it while the fuzzing run found it.
Implemented in: src/lib/scanners/nuclei.ts, src/lib/scanners/zap.ts, src/lib/auth-crawl.server.ts, src/lib/scheduled-dast.server.ts
Tested by: tests/unit/scheduled-dast.test.ts, tests/unit/nuclei.test.ts, tests/unit/nuclei-fuzzing.test.ts, tests/unit/zap.test.ts, tests/unit/auth-crawl-dispatch.test.ts, tests/integration/zap-dast-e2e.test.ts
Devices and virtual machines, and what is installed on them.
An opt-in agent reporting global packages, IDE extensions, AI tooling and MCP servers, analysed against OSV, with per-category consent. MCP servers are judged on one narrow combination -- broad filesystem reach together with a network path -- and a server whose reach cannot be read is surfaced for review rather than scored, because the agent reads the invocation and never connects to the server.
Enrol a device and run the agent from a clone with `bun run device-agent/index.ts`, granting consent per category. It refuses to start without AEGIS_URL and AEGIS_DEVICE_TOKEN, and reports a failed upload rather than exiting as though it had succeeded. Posture checks such as disk encryption need a privileged system agent and are not implemented.
Implemented in: src/lib/devices.server.ts, src/lib/devices.functions.ts, src/lib/scanners/mcp-posture.ts, device-agent/index.ts
Tested by: tests/unit/device-agent.test.ts, tests/unit/mcp-posture.test.ts, tests/integration/self-hosted-agents-e2e.test.ts
Quarantines MCP servers an approved policy blocks, so the client no longer launches them. This is the only developer tooling this product can genuinely prevent rather than report: an MCP server is an entry in a configuration file read at startup, whereas a browser extension cannot be refused from outside the browser and a deleted editor extension is reinstalled in seconds. Nothing is removed -- the definition is kept verbatim in the same file with the reason and the policy version, and the developer can restore it by hand.
Needs all three: an `editor_policies` row at status `active`, which requires a second person's approval; `enforcement_enabled` set on the device by whoever owns it; and the agent started with `--enforce`. Any one absent and the agent reports what would have happened and changes nothing. A developer with local admin can undo the quarantine, which is the honest limit: this is friction and an audit trail, not a seal.
Implemented in: src/lib/mcp-enforcement.ts, src/lib/mcp-quarantine.ts, device-agent/index.ts
Tested by: tests/unit/mcp-enforcement.test.ts, tests/unit/mcp-quarantine.test.ts, tests/unit/device-report-carries-policy.test.ts
A CLI wrapper that checks package provenance and known advisories before a dependency is installed.
Implemented in: safe-install/index.ts
Tested by: tests/unit/safe-install.test.ts, tests/integration/safe-install-live.test.ts
Turning findings into the evidence an auditor asks for, and recording the controls no scanner can see.
Technical controls evaluated from real scan, finding and audit evidence across multiple frameworks, with hash-verifiable evidence packs, Ed25519-signed where the deployment holds a signing key so an auditor can verify authorship with openssl and the published public key, and a trust centre. Ten organisational controls that no scanner can see are recorded separately as dated attestations by named people, with review intervals, an undeletable history of superseded claims, and wording that never presents a claim as a check.
Aegis evidences technical controls and records organisational ones; it does not verify the latter and says so wherever they appear. The auditor request workflow is implemented -- request, submit, review, with separation of duties enforced in the database so a member cannot insert a submission directly -- and GRC platform sync is its own capability, grc-connectors.
Implemented in: src/lib/compliance.ts, src/lib/compliance.functions.ts, src/lib/evidence.ts, src/lib/attestations.ts, src/lib/attestations.functions.ts, src/lib/evidence-signing.ts, src/lib/compliance-ops.functions.ts
Tested by: tests/unit/evidence.test.ts, tests/rls/compliance-ops.test.ts, tests/rls/evidence-requests.test.ts, tests/unit/attestations.test.ts, tests/rls/attestations.test.ts, tests/unit/evidence-signing.test.ts, tests/rls/evidence-pack-signatures.test.ts, tests/integration/evidence-signature-browser.test.ts
Pushes Aegis observations into Drata, Vanta or Secureframe so an audit reads from the tool the auditor already uses. Every mapping between an Aegis control and an external one is declared with the rationale that defends it and what it does not cover, and both travel with the evidence. Controls Aegis has no evidence for are pushed as such rather than omitted, since an omitted control reads as one nobody checked.
Store the platform's API token, which only an owner or admin can do. Previewing and sending are separate actions, and the send button appears only after a preview, so the button offering to show what would be written cannot write it. Aegis never marks a control passed or compliant — it reports what it observed and the platform and auditor draw the conclusion; a push that would assert a verdict is refused before it reaches the network. Six mappings ship, deliberately few: each is a claim that two differently-worded controls are about the same thing, and a wrong one is invisible until an audit depends on it. Both handlers run against a real Postgres with RLS in force, delivery is verified against a local HTTP server that records what arrives, and the built server refuses a cross-site request to every endpoint. Not verified against a real Drata, Vanta or Secureframe account.
Implemented in: src/lib/grc.ts, src/lib/grc.server.ts, src/lib/grc.functions.ts, src/components/grc-panel.tsx
Tested by: tests/unit/grc.test.ts, tests/integration/grc-push-e2e.test.ts, tests/rls/grc-push.test.ts, tests/integration/production-build-e2e.test.ts
Routing of findings to Slack, Microsoft Teams, Jira, Linear, GitHub Issues and ServiceNow, plus signed webhooks and ECS-format SIEM export. Sent without anyone pressing a button: every new critical or high finding in application code, and what GitHub did with an Aegis fix pull request (merged, merged but still present, closed without merging), once per channel. Jira Cloud, Linear and GitHub Issues call back when a ticket changes: a signed delivery to the channel's callback URL closes the finding the moment the ticket is done, through the same status rule the on-demand sync applies. Every callback, including a refused one, is on the page with its reason.
Add a channel with its credential under Notifications. Nothing is delivered until a route exists, and nothing detected before the first active rule is announced. For a tracker callback, point the provider's webhook at the URL and secret issued when the channel is created; ServiceNow and the other trackers have no callback and are read on demand only.
Implemented in: src/lib/notifications.ts, src/lib/workflow.functions.ts, src/lib/notify-unattended.server.ts, src/lib/channel-config.ts, src/lib/ticket-webhooks.ts, src/lib/ticket-webhook-receiver.server.ts, src/lib/ticket-status-apply.ts
Tested by: tests/unit/notifications.test.ts, tests/rls/workflow.test.ts, tests/unit/notify-unattended.test.ts, tests/unit/channel-config.test.ts, tests/integration/fix-merge-recorded.test.ts, tests/unit/ticket-webhooks.test.ts, tests/rls/ticket-webhooks.test.ts
SLA targets, merge-gate policy, time-bounded exceptions requiring a second admin to approve the riskiest kinds, a licence policy the repository scan enforces (and the merge gate can block on), and a minimum package age: a dependency version published more recently than the organisation allows becomes a supply-chain finding.
The licence policy has defaults; the package-age policy is off until an owner or admin sets a number of days on the Policies page.
Implemented in: src/lib/policy.ts, src/lib/workflow.ts, src/lib/policies.functions.ts, src/lib/supply-chain/reputation.ts
Tested by: tests/unit/policy.test.ts, tests/unit/workflow.test.ts, tests/rls/sla-anchor.test.ts, tests/rls/license-policy.test.ts, tests/rls/package-age-policy.test.ts, tests/unit/supply-chain-reputation.test.ts
MTTR, SLA, backlog and coverage metrics, with Markdown, CSV and PDF exports generated from stored scanner evidence. Reports carry the organisation's company name, footer and a handling marking stamped on every PDF page, and PDFs its uploaded logo and heading colour.
Scheduled delivery is implemented. PDF reports carry a detached Ed25519 signature over their exact bytes when EVIDENCE_SIGNING_KEY is set; production has one since 24 September (a PDF downloaded there verifies with `openssl pkeyutl -verify -rawin` against /api/public/evidence-signing-key, and one appended byte is refused); a deployment without one downloads unsigned and says so. PDF reports carry the organisation's uploaded logo on the first page and its colour on headings -- checked by rendering with poppler, not by reading the file -- for PNG (8-bit greyscale or RGB, not interlaced) and JPEG (greyscale or RGB); a transparent, palette, 16-bit, interlaced or CMYK image is refused at upload with what to export instead. A logo is uploaded, never fetched from an address. The markdown and CSV exports carry neither, and the CSV export is unmarked.
Implemented in: src/lib/metrics.ts, src/lib/pdf.ts, src/lib/report-pdf.functions.ts, src/lib/report-branding.ts, src/lib/report-branding.functions.ts, src/lib/pdf-image.ts, src/lib/read-all.ts, src/lib/in-chunks.ts, src/lib/page-window.ts, src/lib/evidence-signing.ts, src/lib/overview-metrics.ts
Tested by: tests/unit/overview-metrics.test.ts, tests/unit/read-all.test.ts, tests/unit/in-chunks.test.ts, tests/unit/reads-respect-the-row-cap.test.ts, tests/integration/postgrest-row-cap.test.ts, tests/unit/metrics.test.ts, tests/unit/pdf.test.ts, tests/rls/reports.test.ts, tests/unit/report-branding.test.ts, tests/rls/report-branding.test.ts, tests/unit/report-logo-pdf.test.ts, tests/rls/scheduler.test.ts, tests/unit/evidence-signing.test.ts, tests/unit/page-window.test.ts, tests/integration/pagination-keyboard.test.ts
The parts that make the rest usable: limits, jobs, delivery and setup.
A CLI producing SARIF, line-delimited diagnostics for editors, and CI-appropriate exit codes, with ready-made templates for GitHub Actions, GitLab, Azure Pipelines and Jenkins, plus a pre-commit hook. Packaged as @aegis/cli: a single bundled file with a linked `aegis` binary, verified by installing the tarball into an empty directory and running it.
The package builds, packs and installs, but is not published to npm, so `bun x @aegis/cli` in the CI templates will not resolve until it is. Scanners are separate installs; whichever are present run, and whichever are not are reported as not run.
Implemented in: cli/aegis.ts, cli-package/package.json
Tested by: tests/integration/cli-e2e.test.ts, tests/integration/cli-package-e2e.test.ts
Fires a deliberately safe canary at a control and records what it did, so evidence is an observation rather than a claim. A WAF whose rule no longer matches still looks configured; a drill is what notices. Response times are computed from recorded instants rather than stored, so any number in a report traces back to two timestamps that each name an observer, and a phase that never happened is null rather than zero because zero would present the worst outcome as the best. A drill cannot pass without a canary, without detection, with detection preceding the canary, or with the canary left behind. Every kind carries a stated scope, safety rationale and cleanup plan, and a planned-but-never-run drill leaves its control reported as unvalidated.
Implemented in: src/lib/control-drills.ts, src/lib/control-drills.functions.ts, src/routes/_authenticated/drills.tsx
Tested by: tests/unit/control-drills.test.ts, tests/rls/control-drills.test.ts
Shares indicators between organisations under recorded consent, redacting on the way out and trusting nothing on the way in: a received signal carries its provenance and is weighed rather than believed. A partner pushes Ed25519-signed envelopes to a per-consent callback URL; the signature, not the payload, establishes who sent it, replays are refused by envelope id inside the age window, and deliveries are rate limited per partner. Consents created before transport existed still verify with their shared secret and are told that path is deprecated.
Both organisations must record a consent covering the specific signal class, with the partner's Ed25519 public key. Nothing is shared by default, nothing is shared unredacted, and nothing received is trusted on the strength of having arrived. Sending to a partner is still by hand: the outbound client needs an agreement that does not exist.
Implemented in: src/lib/federation.ts, src/lib/federation.functions.ts, src/lib/federation-receiver.server.ts, src/lib/federation-keys.server.ts
Tested by: tests/unit/federation.test.ts, tests/unit/federation-transport.test.ts, tests/rls/federation.test.ts
A sample organisation with deterministic example findings and assets, so the product can be explored without connecting a repository. Every record is flagged in the database and labelled in its title, so it stays identifiable in exports and API responses.
Only available in an organisation created as a demo. Seeding a real organisation is refused, because fabricated findings alongside genuine ones would be indistinguishable until somebody chased one down.
Implemented in: src/lib/demo.ts, src/lib/demo.functions.ts
Tested by: tests/unit/demo.test.ts
A declared environment schema with startup validation, a migration runner that applies each file once under an advisory lock, and liveness, readiness and version endpoints. The packaged build is verified by building it, serving it, and driving it in a real browser rather than only compiling it.
The deployment preset is cloudflare-module. The production-build test runs the same application under the node-server preset instead, because workerd accepts connections but never responds in some sandboxes — confirmed with a three-line hello-world Worker — and an untestable build is one nobody checks.
Implemented in: src/lib/environment.ts, src/lib/health.ts, src/lib/build-info.ts, src/routes/api/public/health.ts, src/routes/api/public/version.ts
Tested by: tests/unit/environment.test.ts, tests/unit/health.test.ts, tests/integration/migrate-e2e.test.ts, tests/integration/production-build-e2e.test.ts
Combines rules, dataflow, runtime and model signals without letting any of them rule. Weights come from measured precision and recall on a labelled corpus built from repository fixtures rather than customer data, keyed per detector and version, because a rule set change is exactly what invalidates a measurement. A detector that never fired has undefined precision rather than perfect precision, and a corpus too small to calibrate is reported as uncalibrated instead of as a score. Where reproducible detectors disagree the disagreement is the output and goes to a human. A model agreeing raises confidence within a cap, a model disagreeing is recorded and subtracts nothing, and a model alone can raise something for review but never auto-triage it.
Implemented in: src/lib/ensemble.ts, src/lib/ensemble.functions.ts, src/routes/_authenticated/ensemble.tsx, src/lib/finding-correlation.ts, src/lib/finding-noise.ts, src/lib/scanners/precision.ts, src/lib/finding-trend.ts
Tested by: tests/unit/ensemble.test.ts, tests/rls/ensemble.test.ts, tests/unit/finding-correlation.test.ts, tests/unit/finding-noise.test.ts, tests/unit/scanner-precision.test.ts, tests/unit/finding-trend.test.ts
Leased jobs with retry, backoff and dead-lettering, so a scanner node that dies mid-job does not strand the work.
Implemented in: src/lib/jobs.server.ts, src/lib/nodes.functions.ts
Tested by: tests/unit/platform.test.ts, tests/rls/platform-jobs.test.ts
Every claim the product makes is linked to the observations behind it, with the source, its version, the commit or digest examined, and when it was seen. Freshness is the worst of the supporting observations rather than the best, so a claim resting on one current reading and one stale one reports as stale. Disagreement between sources is surfaced rather than resolved into a single verdict. Investigation bundles carry provenance and never raw scanner output, because that output routinely contains the secret that caused the finding. A wrong observation is corrected rather than deleted: any member proposes a retraction with a reason, an administrator other than the proposer decides it, and the claims that rested on it are marked as standing on withdrawn support instead of silently changing.
Implemented in: src/lib/evidence-graph.ts, src/lib/evidence-graph.functions.ts, src/lib/correction-notice.ts, src/components/evidence-corrections-panel.tsx, src/routes/_authenticated/evidence.tsx
Tested by: tests/unit/evidence-graph.test.ts, tests/unit/correction-notice.test.ts, tests/rls/evidence-graph.test.ts, tests/rls/evidence-retractions.test.ts
A dashboard checklist showing the shortest path from an empty account to a useful one, with each step explaining what it gets you rather than only what to click. State is derived from what exists rather than from a stored flag, so disconnecting an integration makes its step incomplete again, and steps that do not apply say why. Disappears once setup is done.
Implemented in: src/lib/onboarding.ts, src/lib/onboarding.functions.ts
Tested by: tests/unit/onboarding.test.ts, tests/integration/onboarding-e2e.test.ts
Connect Anthropic or OpenAI by OAuth sign-in or a pasted API key, and choose which model powers triage and fixes. The router prefers an OAuth login, then a per-org API key, then a deployment-wide AI_API_KEY, and the chosen model drives whichever credential wins.
The API key is verified against the provider before it is stored, so a saved connection is one that works. The chosen model is validated against a catalog on the way in, so a stored preference can only be a model the provider serves. OAuth borrows the provider CLI's public client id by default -- the consent screen names that CLI -- and a deployment that registers its own OAuth app sets AEGIS_<PROVIDER>_CLIENT_ID so the screen names it instead.
Implemented in: src/lib/model-call.server.ts, src/lib/model-api-key.server.ts, src/lib/model-choice.server.ts, src/lib/provider-oauth.ts, src/lib/provider-oauth.server.ts, src/lib/provider-oauth.functions.ts
Tested by: tests/unit/model-api-key-routing.test.ts, tests/unit/model-choice.test.ts, tests/unit/provider-oauth.test.ts, tests/unit/provider-oauth-refresh.test.ts, tests/unit/provider-oauth-pending-store.test.ts
Detects sub-projects from their manifests and routes a finding to the team that owns the directory it is in, so a repository holding forty services does not send every finding to whoever is listed first.
Add a sub-project prefix to a notification rule; rules without one are unaffected and keep matching exactly as before. A finding with no file path never matches a sub-project rule, because a cloud misconfiguration belongs to no directory and routing it to the first-named service would be a guess.
Implemented in: src/lib/monorepo.ts, src/lib/workflow.ts
Tested by: tests/unit/monorepo.test.ts, tests/unit/workflow.test.ts
Install Aegis as an application on GitHub, Bitbucket, Slack and Jira, or authorise it where a provider has no installable app (GitLab, Google Workspace). Full operational access: it reads code and configuration and writes back -- branches, pull requests, workflow files, code scanning alerts, tickets and their transitions, and messages. Nothing is pasted, tokens are exchanged server-side with PKCE, and they refresh before they expire.
Register an application with each provider and set its client id, secret and (where it installs) its public app slug. A connector without credentials is shown as unavailable with the variables named, and prints the exact redirect URI to register.
Implemented in: src/lib/connectors.ts, src/lib/integration-auth.ts, src/lib/connectors.server.ts, src/lib/connectors.functions.ts
Tested by: tests/unit/connectors.test.ts, tests/unit/connector-flows.test.ts, tests/unit/integration-auth.test.ts, tests/unit/connector-grants.test.ts, tests/unit/connectors-server.test.ts, tests/unit/connector-callback.test.ts, tests/unit/channel-connector-credentials.test.ts
Extensions declare a kind, an API version, capabilities and resource limits, and the host grants no more than it was asked for. A plugin cannot choose its tenant, set severity or risk score, or close and suppress findings: those fields are refused by name rather than stripped, because a silently sanitised field looks to the author like it worked and to an attacker like a control worth probing. Capabilities are checked at the point of use, not only at install, since an install-time check stops applying the moment policy changes underneath a loaded plugin. Output is bounded by count and bytes, a flood is refused entirely rather than truncated, and a hanging plugin is stopped at its budget. An admitted plugin runs as a plugin-run scan on the customer's scanner node inside a bubblewrap sandbox: own user, PID, network, IPC and UTS namespaces, read-only runtime, only its directory writable, empty environment, killed at its deadline. Measured from inside: the host filesystem is absent, sockets are refused, a /tmp write never reaches the host. The node submits the plugin's raw output and the sandbox's refusals; the control plane judges the output against the SDK contract again and refuses any run that reports no sandbox.
Plugin execution needs a registered scanner node with bubblewrap installed (the node image ships it). Not enforced: a per-host network allow-list, since the sandbox has no network at all, so network:outbound stays refused at admission; memory limits; anything a kernel escape would reach.
Implemented in: src/lib/plugin-sdk.ts, src/lib/plugin-registry.functions.ts, src/lib/plugin-run.ts, src/routes/_authenticated/plugins.tsx
Tested by: tests/unit/plugin-sdk.test.ts, tests/unit/plugin-run.test.ts, tests/integration/plugin-sandbox.test.ts
Four plans with derived feature entitlements and usage limits, a grace period before a failed payment changes anything, and a downgrade preview that states what would stop working before the change is made. A codified list of capabilities no plan may withhold — seeing your own findings, exporting your data, receiving a critical alert, revoking a credential — is enforced by a test rather than by convention.
No payment provider is connected, so plans are recorded rather than sold: the subscription table is written only by the service role reacting to a provider webhook, and that webhook handler is not built. Until then every organisation is on the free plan.
Implemented in: src/lib/entitlements.ts, src/lib/entitlements.functions.ts
Tested by: tests/unit/entitlements.test.ts, tests/rls/subscriptions.test.ts
A complete export of an organisation's findings, assets, scans, exceptions, policies, audit log and evidence graph, as JSON with a SHA-256 manifest per section so the file can be checked for damage. Available on every plan including a lapsed one, because holding an audit history against a billing status is what the never-gated list forbids. Credentials are withheld and the manifest says so, since an export is routinely written to a laptop or a ticket attachment. The same file restores into an organisation the caller administers, behind a typed confirmation of the destination: the manifest is verified first, every row gets a new identity, references outside the export are left empty and counted, and all nine sections are written back including exceptions and policies.
Implemented in: src/lib/tenant-export.ts, src/lib/tenant-export.functions.ts, src/components/export-panel.tsx, src/components/import-panel.tsx
Tested by: tests/unit/tenant-export.test.ts, tests/unit/export-readers.test.ts, tests/rls/tenant-export.test.ts, tests/rls/tenant-import-roundtrip.test.ts
One page ranking every organisation a person can act for by what needs attention first, so a provider looking after ten customers does not have to open ten dashboards to find the one that needs them.
Reads each organisation separately and authorises each one through assertOrgMemberOrDelegate, the same guard fleetHealth uses: is_org_member is untouched, because every RLS policy calls it and widening it would change every table at once. The organisation list comes from the caller's own memberships and live delegations and never from input, so it cannot be used to probe which organisation ids exist. A customer whose findings could not be read is ranked above every known problem and shows why, rather than showing zeroes; there is deliberately no portfolio-wide severity total, because ten criticals at one customer and ten across nine others are different situations that one number cannot distinguish.
Implemented in: src/lib/portfolio.ts, src/lib/portfolio.functions.ts
Tested by: tests/unit/portfolio.test.ts
An unauthenticated page describing every capability, generated from the same registry the application reads rather than written by hand. Lists what is not built before what is, carries each capability's stated limit verbatim, and publishes the modules and tests behind every claim so a reader can check rather than believe.
Implemented in: src/lib/docs.ts, src/routes/docs.tsx
Tested by: tests/unit/docs.test.ts, tests/integration/browser-hydration-e2e.test.ts
A documented, scope-authenticated REST API with an OpenAPI 3.1 description, per-token rate limiting and RFC 9457 problem responses.
Implemented in: src/routes/api/public/v1/findings.ts, src/routes/api/public/v1/openapi.ts, src/lib/asset-import.ts
Tested by: tests/unit/openapi.test.ts, tests/rls/public-api.test.ts, tests/unit/asset-import.test.ts
Scans applications that never face the internet, through a node running inside the customer network that dials out and is never dialled into, with an explicit per-node allowlist of what it may reach.
Run a scanner node and set AEGIS_ALLOWED_TARGETS to the internal hosts it may reach. A scan of a private address is queued only when a node in the organisation advertises the scanner: node jobs are claimed through claim_scan_job, scoped to one node's organisation and granted to the service role alone, so no hosted path ever picks one up. With no such node the hosted runtime's refusal applies unchanged -- a service that fetches http://10.0.0.5/ on request is an SSRF proxy with a login page. Reachability is not authorisation: an internal host must still be covered by a verified domain and an unexpired in-scope approval. A target outside the node's own allowlist is refused by the node rather than silently skipped.
Implemented in: src/lib/broker.ts, src/lib/nodes.functions.ts
Tested by: tests/unit/broker.test.ts
Records a fix as a chain of separate claims rather than a status, so a change that got as far as a built artefact reports that instead of being rounded up to fixed. A contract cannot be verified until runtime verification passed, enforced by a database function so the word means the same thing whichever caller writes it. The rollback plan and the previous known-good reference are captured when the contract is created, because a plan written during an incident is written by somebody who may no longer have the digest. Diffs that close a finding by adding a suppression or editing a scanner ignore file are reported as symptom fixes, and a suppression alongside real code changes is reported as suspected rather than condemned. Unmeasured quantities are null rather than zero, and a repository with no tests reports unknown regression risk rather than low.
Implemented in: src/lib/fix-contract.ts, src/lib/fix-contract.functions.ts, src/routes/_authenticated/fixes.tsx
Tested by: tests/unit/fix-contract.test.ts, tests/rls/fix-contract.test.ts
Ranks units of work rather than findings: one dependency upgrade that closes twelve findings is one item, ordered by weighted risk removed per engineering hour. Business weightings are inputs a team can disagree with rather than constants, and every recommendation carries the assumptions behind its estimate, so a guess is never presented as a measurement. Accepted risk and findings with no available fix are excluded from the plan and reported separately, so what the plan leaves out stays visible. A simulation shows which attack paths a remediation would close before it is done, including the case where it removes real risk and closes nothing.
Implemented in: src/lib/risk-optimizer.ts, src/lib/risk-optimizer.functions.ts, src/routes/_authenticated/plan.tsx
Tested by: tests/unit/risk-optimizer.test.ts, tests/rls/risk-optimizer.test.ts
A profile (repository, web surface, or both) queues the scans that apply to a chosen repository and verified host as ordinary jobs, and records a reason for every step it did not queue: no verified domain, no online node advertising the scanner, or a node gate that refused. Node steps go through the same function and checks as the per-scanner button; hosted steps share the daily triggers' idempotency keys, so a profile joins a pending daily scan instead of doubling it. Each web-surface and intelligence job re-reads domain verification when it runs. The Engines table lists every hosted engine and every node scanner with where it runs, whether it is installed, the version that last ran, its last success and last failure with the error, and what it needs, aggregated in the database from the scans table.
Implemented in: src/lib/scan-profiles.ts, src/lib/scan-profiles.server.ts, src/lib/scan-profiles.functions.ts
Tested by: tests/unit/scan-profiles.test.ts, tests/integration/scan-profiles-persist.test.ts
A node run on customer infrastructure that executes heavyweight tools and uploads raw output. The server parses it and decides severity, so a node cannot inject findings or dictate their severity.
Register a node and run it from a clone with `bun run scanner-node/index.ts`, where the tools are installed. Capabilities are fixed at registration, so a node cannot claim work it was not approved for. It refuses to start without AEGIS_URL and AEGIS_NODE_TOKEN, announces which scanners it actually found, and reports an unreachable control plane rather than polling silently.
Implemented in: src/lib/nodes.server.ts, src/routes/api/public/nodes/jobs.ts, scanner-node/index.ts
Tested by: tests/rls/scanner-nodes.test.ts, tests/integration/scanner-node-e2e.test.ts, tests/integration/self-hosted-agents-e2e.test.ts
One page answering what is connected and what is broken, across source control, cloud, registries, notifications, compliance, runtime and model providers. Every connection reports in the same vocabulary: connected, needs attention with a reason, not connected, or unavailable with the missing variables named.
Implemented in: src/lib/connections.ts, src/lib/connections.functions.ts
Tested by: tests/unit/connections.test.ts, tests/unit/connections-status.test.ts
Lists every organisation a person can act for, by membership or by live delegation, and switches between them: what a managed service provider needs to reach the second of their ten customers.
The chosen organisation is a display preference stored client-side, and the server checks membership on every request regardless of what is stored: a request for an organisation the caller does not belong to returns no row and falls back to their own, which refuses without confirming whether that organisation exists. Delegated organisations are shown as delegated and drop off the list when the grant expires.
Implemented in: src/lib/org.functions.ts, src/lib/delegations.functions.ts
Tested by: tests/rls/org-switching.test.ts, tests/unit/org-guard-delegation.test.ts