Check Catalog
The complete native check set, generated directly from checks.json — the source of truth shared by the web app and CLI native engine. The catalog currently holds 59 checks across nine agent domains, weighted to 100 points.
Every check below is read straight from checks.json by scripts/generate-checks-docs.py. Nothing here is hand-written or aspirational — if a check is not in checks.json, it does not run, and it is not listed here. For planned checks that are not yet implemented, see the roadmap section of the examination spec.
The CLI can also run the separately scored ALIGNMENT analyzer and optional Gitleaks, OSV-Scanner, Zizmor, OpenSSF Scorecard, and actionlint feeders. Their rules are not part of this catalog and never alter native points. See the CLI Scanner and Reading Reports pages.
Check states
Every check resolves to exactly one of four states when a scan runs:
Agent index
| Agent | Domain | Weight | Checks |
|---|---|---|---|
| SENTINEL | Code Analysis | 14 | 11 |
| GATEKEEPER | Access Control | 12 | 6 |
| VAULT | Data Security | 13 | 8 |
| CONDUIT | Network & API | 11 | 5 |
| WATCHTOWER | Application Config | 11 | 8 |
| LIBRARIAN | Dependencies | 12 | 6 |
| SHIELD | Client Security | 11 | 6 |
| AUDITOR | Logging & Monitoring | 8 | 4 |
| ARCHITECT | Infrastructure | 8 | 5 |
| Total | 100 | 59 | |
SENTINEL11 checks
Hardcoded credential assignment
critical CWE-798A secret-looking variable is assigned a long literal string directly in source.
Anyone who can read the repository — including every fork, every CI log, and every future clone — holds a working credential. Git history keeps it even after the line is deleted.
Move the value into an environment variable or a secrets manager, rotate the exposed credential immediately, and purge it from git history with `git filter-repo`.
- API_KEY = "sk_live_9f2b1c8e4a7d0553"
+ import os
+ API_KEY = os.environ["API_KEY"] # set in your secrets manager / .env (gitignored)
AWS access key ID in source
critical CWE-798A literal matching the AWS access key ID format (AKIA/ASIA + 16 chars) appears in the repository.
AWS key IDs are harvested from public GitHub within minutes by automated scrapers. Paired with a secret key this grants direct access to your cloud account.
Deactivate the key in IAM right now, then re-issue via IAM roles or AWS SSO instead of long-lived keys.
aws iam update-access-key --access-key-id AKIA... --status Inactive
aws iam delete-access-key --access-key-id AKIA...
Private key material committed
critical CWE-321A PEM private key block is embedded in a tracked file.
A leaked private key lets an attacker impersonate your service, decrypt intercepted traffic, or sign artifacts as you.
Revoke and reissue the key pair, then load keys at runtime from a mounted secret or KMS — never from the repo.
SQL built by string concatenation
critical CWE-89A SQL statement is assembled with f-strings, `+`, `%`, or `.format()` rather than bound parameters.
Any user-controlled value reaching this query can rewrite it — reading other tenants' rows, dumping the user table, or dropping it.
Use parameter binding for every dynamic value. Only table/column names may be interpolated, and only from a fixed allow-list.
- cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
+ cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
Shell execution with interpolated input
critical CWE-78An OS command is run through a shell with a dynamically built string, or with `shell=True`.
A `;`, backtick, or `$()` in any interpolated value becomes arbitrary code execution on the host running the process.
Pass arguments as a list with `shell=False` (the default) and validate any value that reaches the argv.
- subprocess.run(f"git clone {repo_url}", shell=True)
+ subprocess.run(["git", "clone", repo_url], shell=False, check=True)
Dynamic code evaluation
high CWE-95`eval`, `exec`, `new Function`, or `setTimeout(string)` is called on a non-literal value.
If the evaluated string is influenced by input, request data, or config fetched at runtime, it is remote code execution.
Replace with an explicit parser (`JSON.parse`, `ast.literal_eval`) or a dispatch table keyed by known-safe names.
Python exec() on a dynamic value
high CWE-95Python's builtin `exec()` is called on a non-literal value.
`exec` compiles and runs whatever string it is handed. If that string is influenced by input or remote config, it is remote code execution.
Use `ast.literal_eval` for data, or a dispatch dict keyed by known-safe names for behaviour.
Unsafe deserialization
high CWE-502Untrusted bytes are loaded through pickle, `yaml.load` without a safe loader, or Java/PHP native deserialization.
These formats can instantiate arbitrary classes on load — a crafted payload runs code before your first line of validation.
Use `yaml.safe_load`, JSON, or a schema-validated format. Never unpickle data that crossed a trust boundary.
- config = yaml.load(untrusted_bytes)
+ config = yaml.safe_load(untrusted_bytes)
Non-cryptographic randomness for security values
medium CWE-338`Math.random()` or `random.random()` is used near a token, password, nonce, or ID.
These generators are predictable from a handful of outputs, so an attacker can forecast reset tokens or session identifiers.
Use `crypto.randomUUID()` / `crypto.randomBytes()` in Node, `secrets.token_urlsafe()` in Python.
- const token = Math.random().toString(36).slice(2);
+ const token = crypto.randomUUID();
Path traversal in file access
high CWE-22A filesystem path is built from request/user input without normalisation.
`../../etc/passwd` style input reads or overwrites files outside the intended directory.
Resolve the path and assert it stays within a base directory before opening it.
base = Path("/srv/uploads").resolve()
target = (base / user_path).resolve()
if not target.is_relative_to(base):
raise PermissionError("path escapes upload root")
Server-side request forgery risk
high CWE-918An outbound HTTP request targets a URL taken from request input.
An attacker can point the request at internal services or the cloud metadata endpoint (169.254.169.254) and read credentials.
Validate the destination against an allow-list of hosts and block link-local, loopback, and private ranges.
GATEKEEPER6 checks
Signature verification disabled
critical CWE-347JWT or token verification is switched off, or the `none` algorithm is accepted.
Anyone can mint a token claiming to be any user, including an administrator. Authentication is effectively absent.
Always verify with an explicit algorithm allow-list (`algorithms=["RS256"]`) and never accept `none`.
- jwt.decode(token, options={"verify_signature": False})
+ jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"], audience=API_AUDIENCE)
Weak password length policy
high CWE-521Password validation accepts fewer than 8 characters.
Short passwords fall to offline cracking in seconds regardless of how well you hash them.
Require at least 12 characters and screen against a breached-password list (e.g. Have I Been Pwned range API).
Endpoint opted out of authentication
medium CWE-306A route is explicitly marked as public via `AllowAny`, `@csrf_exempt`, `authenticate: false`, or similar.
Each opt-out is an unauthenticated entry point. They accumulate silently and are rarely re-reviewed.
Default to deny. Keep an audited list of intentionally public routes and assert it in a test.
Default or hardcoded admin credentials
critical CWE-1392An admin/root username is paired with a literal password in source or config.
Default credentials are the single most reliable way into a self-hosted deployment; scanners try them first.
Generate the initial admin password at install time, force rotation on first login, and never ship a fallback.
Session cookie missing security flags
high CWE-1004A cookie is set with `httpOnly` or `secure` explicitly false, or a session cookie config omits both.
Without `httpOnly` any XSS reads the session; without `secure` it leaks over plain HTTP on a hostile network.
Set `httpOnly: true`, `secure: true`, and `sameSite: 'lax'` (or `'strict'`) on every session cookie.
res.cookie('sid', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 8
});
Authorization decided on the client
medium CWE-602A role or admin flag is read from local/session storage or a decoded token without server verification.
Anything the browser stores, the user edits. Client-side role checks are UI hints, not access control.
Re-check the caller's role server-side on every privileged request; treat client state as untrusted display data.
VAULT8 checks
Broken hash used for passwords
high CWE-916MD5 or SHA-1 is applied to a password or credential value.
Commodity GPUs test billions of MD5/SHA-1 candidates per second; a stolen table is cracked, not merely exposed.
Use Argon2id (preferred), bcrypt, or scrypt with tuned work factors, and re-hash on next successful login.
- digest = hashlib.md5(password.encode()).hexdigest()
+ from argon2 import PasswordHasher
+ digest = PasswordHasher().hash(password)
Weak cipher mode or static IV
high CWE-327ECB mode, DES/RC4, or a hardcoded initialisation vector is used for encryption.
ECB leaks structure in ciphertext and a reused IV in CBC/CTR lets an attacker recover plaintext across messages.
Use AES-GCM or ChaCha20-Poly1305 with a fresh random nonce per message.
Sensitive value written to stdout
medium CWE-532A print/console statement includes a password, token, SSN, or card field.
Container stdout is shipped to log aggregators, retained for months, and readable by anyone with dashboard access.
Remove the statement or mask the value; add a redaction filter in the logging pipeline as a backstop.
Environment file committed to the repository
critical CWE-538A real `.env` file (not `.env.example`) is tracked in git.
Env files are where production credentials live. Committed once, they stay in history and in every clone forever.
`git rm --cached .env`, add it to `.gitignore`, rotate every value it contained, then scrub history.
git rm --cached .env
echo '.env' >> .gitignore
git commit -m 'chore: stop tracking .env'
# then rotate every credential it held
Key or certificate file committed
critical CWE-312A `.pem`, `.key`, `.p12`, `.pfx`, `.keystore`, or SSH private key file is tracked.
Committed key material must be treated as compromised the moment it is pushed.
Revoke and reissue, then deliver keys through a secret mount or KMS at deploy time.
Database dump or datastore committed
medium CWE-538A `.sqlite`, `.db`, or `.sql` dump file is tracked in the repository.
Development databases routinely contain copies of real user records, and often password hashes.
Remove the file, gitignore the pattern, and seed local databases from a fixtures script instead.
.gitignore does not cover secret files
medium CWE-1230The repository has a `.gitignore` but it does not exclude `.env` or key material.
Without the ignore rule, the next `git add .` commits whatever credentials happen to be on disk.
Add the standard secret patterns to `.gitignore` before they get committed.
# secrets
.env
.env.*
!.env.example
*.pem
*.key
*.p12
credentials.json
Bearer or provider token literal
critical CWE-798A recognisable provider token prefix (GitHub, Slack, Stripe, Google, OpenAI, Anthropic) appears as a literal.
These are directly usable credentials with a known issuer, so exploitation needs no guesswork at all.
Revoke at the provider immediately, then load from the environment at runtime.
CONDUIT5 checks
Wildcard CORS origin
high CWE-942`Access-Control-Allow-Origin` is set to `*`, or the CORS middleware allows all origins.
Any website can call your API from a visitor's browser. On a cookie-authenticated API this is cross-origin data theft.
Enumerate trusted origins explicitly and reject everything else.
- app.use(cors({ origin: '*' }))
+ app.use(cors({ origin: ['https://app.example.com'], credentials: true }))
Wildcard CORS combined with credentials
critical CWE-942Credentialed CORS is enabled alongside a permissive origin policy.
Attacker-controlled pages can issue authenticated requests as the logged-in victim and read the responses.
Never combine `credentials: true` with a reflected or wildcard origin — pin to an explicit host list.
TLS certificate validation disabled
high CWE-295An HTTP client is configured with `verify=False`, `rejectUnauthorized: false`, or `InsecureSkipVerify`.
TLS without certificate validation stops any active attacker on the path from being detected — HTTPS becomes decoration.
Keep validation on. For internal CAs, install the CA bundle rather than disabling the check.
- requests.get(url, verify=False)
+ requests.get(url, verify="/etc/ssl/certs/internal-ca.pem")
Cleartext HTTP endpoint
medium CWE-319A non-local `http://` URL is used for an API or asset.
Requests and any tokens they carry are readable and modifiable by anyone on the network path.
Switch to `https://` and add HSTS so downgrades are refused.
Service bound to all interfaces
medium CWE-1327A server listens on `0.0.0.0` outside of a container entrypoint.
Services intended for localhost become reachable from the network, and from the internet on a misconfigured host.
Bind to `127.0.0.1` and place a reverse proxy in front of anything that must be public.
WATCHTOWER8 checks
Debug mode enabled
high CWE-489`DEBUG = True`, `app.run(debug=True)`, or an equivalent development flag is set in committed config.
Debug handlers expose stack traces, settings, and — in Flask/Django — an interactive console that executes code.
Drive the flag from the environment and default it to off.
- DEBUG = True
+ DEBUG = os.environ.get("DEBUG", "").lower() == "true" # off unless explicitly enabled
Container runs as root
high CWE-250A Dockerfile never drops privileges — no `USER` instruction, or it explicitly sets `USER root`.
A process escape or a mounted host path gives the attacker root on the node instead of an unprivileged account.
Create a non-root user and switch to it before the entrypoint.
RUN adduser --system --uid 10001 appuser
USER appuser
CMD ["node", "server.js"]
Unpinned base image tag
medium CWE-1104A Dockerfile uses `:latest` or omits the tag entirely.
Builds are not reproducible and a compromised or breaking upstream image ships straight to production.
Pin to a digest: `FROM node:20.11-alpine@sha256:...`.
Privileged container or host namespace
high CWE-250A compose or Kubernetes manifest requests `privileged: true`, `hostNetwork`, `hostPID`, or docker socket access.
A privileged container is functionally root on the host; the docker socket is a full container escape.
Drop the privilege, add only the specific capabilities needed, and never mount `/var/run/docker.sock`.
Plaintext secret in CI workflow
high CWE-798A CI workflow sets a token or password to a literal value instead of referencing the secrets store.
Workflow files are public on public repos, and the value is echoed into build logs on failure.
Reference `${{ secrets.NAME }}` (GitHub) or the equivalent masked variable in your CI provider.
- env:
- NPM_TOKEN: npm_9f2b1c8e4a7d0553aa11
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
No security disclosure policy
medium CWE-1059The repository has no `SECURITY.md`.
Researchers who find a flaw have no private channel, so issues get filed publicly — or sold.
Add `SECURITY.md` with a contact address, supported versions, and expected response time.
# Security Policy
## Reporting a Vulnerability
Email security@example.com. We acknowledge within 2 business days
and aim to ship a fix within 30 days.
## Supported Versions
| Version | Supported |
|---------|-----------|
| 2.x | yes |
| < 2.0 | no |
No license file
lowThe repository does not declare a license.
Without a license the code is all-rights-reserved by default, which blocks legitimate reuse and complicates audits.
Add a `LICENSE` file, or state the proprietary terms explicitly if the code is closed.
Security headers not configured
medium CWE-693No Content-Security-Policy, HSTS, or X-Frame-Options configuration found anywhere in the repository.
Missing CSP removes the main mitigation for XSS; missing frame protection allows clickjacking of authenticated views.
Add a security-headers middleware (helmet, django-csp, secure.py) or set them at the edge/CDN.
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: { directives: { defaultSrc: ["'self'"] } },
hsts: { maxAge: 31536000, includeSubDomains: true }
}));
LIBRARIAN6 checks
Dependency lockfile missing
medium CWE-1104A manifest declares dependencies but no lockfile pins the resolved versions.
Every install can pull different transitive code. A hijacked patch release lands in production without a diff.
Commit the lockfile your package manager produces and install with `npm ci` / `pip install -r requirements.txt --require-hashes`.
Known-vulnerable dependency version
high CWE-1395A manifest pins a package version with a published CVE.
Public advisories come with public exploits; these are the first things scanned for on an exposed service.
Upgrade to the patched release and enable automated dependency updates so this does not recur.
Dependency sourced from a URL or git ref
medium CWE-829A dependency is installed from a git URL or tarball rather than a registry release.
A moving branch reference means the code can change under you with no version bump and no audit trail.
Publish an internal registry package, or at minimum pin to an immutable commit SHA.
No automated dependency updates
low CWE-1104No Dependabot or Renovate configuration is present.
Patch lag is the dominant cause of exploited dependency CVEs; manual upgrades slip.
Add `.github/dependabot.yml` or a Renovate config so upgrade PRs open automatically.
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule: { interval: weekly }
open-pull-requests-limit: 10
Remote script piped to a shell
high CWE-494A build or CI step pipes `curl`/`wget` output directly into `bash` or `sh`.
The remote server decides what code runs on your builder, and can serve different content to you than to reviewers.
Download to a file, verify a pinned checksum or signature, then execute.
curl -fsSL -o install.sh https://example.com/install.sh
echo "<known-sha256> install.sh" | sha256sum -c -
sh install.sh
Unpinned third-party GitHub Action
medium CWE-829A workflow references a third-party action by branch or floating tag instead of a commit SHA.
The action author — or anyone who compromises their account — can retroactively change what runs with your repo token.
Pin third-party actions to a full commit SHA and let Dependabot bump them.
- uses: some-org/deploy-action@main
+ uses: some-org/deploy-action@a1b2c3d4e5f60718293a4b5c6d7e8f9012345678 # v3.1.0
SHIELD6 checks
Auth token stored in web storage
high CWE-922A JWT, session, or auth token is written to `localStorage` or `sessionStorage`.
Web storage is readable by any script on the page, so a single XSS or a compromised npm package exfiltrates every session.
Keep the session in an `HttpOnly; Secure; SameSite` cookie so script cannot read it.
- localStorage.setItem('token', res.data.token);
+ // server sets: Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax
+ // client sends credentials automatically:
+ fetch('/api/me', { credentials: 'include' });
Unsanitised HTML injection sink
high CWE-79`dangerouslySetInnerHTML`, `v-html`, `[innerHTML]`, or a direct `innerHTML =` assignment is used.
If any part of that string is user-controlled it becomes stored XSS — session theft, keylogging, or account takeover.
Render as text, or sanitise with DOMPurify immediately before insertion.
- <div dangerouslySetInnerHTML={{ __html: comment.body }} />
+ import DOMPurify from 'dompurify';
+ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.body) }} />
document.write or legacy DOM sink
medium CWE-79`document.write` is used, which parses its argument as HTML.
It is an XSS sink, blocks the parser, and is ignored entirely in async script contexts.
Build nodes with `createElement`/`textContent` and append them.
postMessage without origin validation
medium CWE-346A `message` listener does not check `event.origin`, or `postMessage` targets `*`.
Any framing or opened window can send messages your handler trusts, or read messages you broadcast.
Compare `event.origin` against an exact expected origin and pass a specific target origin when sending.
window.addEventListener('message', (e) => {
if (e.origin !== 'https://trusted.example.com') return;
handle(e.data);
});
target="_blank" without rel protection
low CWE-1022An anchor opens a new tab without `rel="noopener"`.
In older browsers the opened page can redirect the original tab via `window.opener` — a credible phishing pivot.
Add `rel="noopener noreferrer"` to every `target="_blank"` link.
Secret embedded in client-side code
critical CWE-798A credential literal appears in a file that ships to the browser.
Anything in the bundle is public — view-source is all the attacker needs, regardless of build-time obfuscation.
Proxy the call through your backend and keep the credential server-side.
AUDITOR4 checks
Credentials passed to the logger
high CWE-532A structured log call includes a password, key, or secret field.
Log aggregators have far broader access than production databases, and retain data long after rotation.
Redact sensitive keys in a log processor and pass identifiers, not credentials.
- logger.info("login attempt", { email, password });
+ logger.info("login attempt", { email, hasPassword: Boolean(password) });
Stack trace returned to the client
medium CWE-209An error handler sends `err.stack`, `traceback`, or the raw exception in the HTTP response.
Traces reveal file paths, framework versions, and query structure — the reconnaissance step of a real attack.
Return a generic message plus a correlation ID, and log the detail server-side.
- res.status(500).json({ error: err.stack });
+ const ref = crypto.randomUUID();
+ logger.error({ ref, err });
+ res.status(500).json({ error: 'Internal error', ref });
Debug output left in source
low CWE-489`debugger` statements or `console.debug`/`console.trace` calls remain in non-test source.
`debugger` halts execution in any browser with devtools open, and verbose debug calls bury real signals.
Route through a level-aware logger and enforce `no-console` / `no-debugger` in lint.
No continuous integration pipeline
mediumNo CI configuration was found in the repository.
Without automated checks, security linting and dependency audits depend on whoever remembers to run them.
Add a CI workflow that runs tests, a linter, and a dependency audit on every pull request.
name: ci
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- run: npm audit --audit-level=high
ARCHITECT5 checks
Hardcoded IP address
medium CWE-1327A routable IPv4 literal is embedded in source or configuration.
Infrastructure changes silently break the deployment, and the address discloses internal topology.
Resolve endpoints through DNS or service discovery and supply them as configuration.
Security group open to the internet
critical CWE-284An infrastructure definition allows ingress from `0.0.0.0/0`.
Databases and admin ports exposed this way are found by internet-wide scanners within hours of going live.
Restrict ingress to known CIDRs or a bastion/VPN security group; expose only 443 publicly.
ingress {
from_port = 5432
to_port = 5432
- cidr_blocks = ["0.0.0.0/0"]
+ security_groups = [aws_security_group.app.id]
}
Publicly readable object storage
high CWE-732A bucket or blob container is configured with a public-read ACL.
Public buckets are the most common source of large-scale data exposure; they are indexed and enumerated continuously.
Block public access at the account level and serve objects through signed URLs or a CDN origin identity.
No automated tests
mediumNo test directory or test files were found in the repository.
Security fixes regress silently when nothing verifies the behaviour they depend on.
Add a test suite and wire it into CI, starting with the authentication and authorization paths.
Terraform state committed
critical CWE-538A `.tfstate` file is tracked in the repository.
Terraform state stores resource attributes in plaintext, routinely including database passwords and generated keys.
Move state to an encrypted remote backend (S3 + DynamoDB lock, Terraform Cloud) and gitignore `*.tfstate*`.
terraform {
backend "s3" {
bucket = "tf-state-prod"
key = "app/terraform.tfstate"
encrypt = true
dynamodb_table = "tf-locks"
}
}
See Severity & Scoring for how per-check deductions, hit caps, and agent weights combine into the final 0–100 score.