MILLENNIUMS.AI documentation
Point MILLENNIUMS.AI at your AI application and it hacks it for you — in a private, throwaway sandbox — then hands back the vulnerabilities it proved, with a working exploit and a plain fix for each one.
You don't need a security team or a single line of attack code. Give it a target (a URL, a repository, or an API), and autonomous agents probe the AI attack surface the way a real attacker would: prompt injection, tool and agent abuse, data leakage, RAG flaws, runaway cost, and the rest of the OWASP LLM Top 10. Every finding you see is one it actually pulled off, with the exact steps to reproduce it.
This site has three parts. Core concepts explains what the tool does and why. Guides walk you through real tasks. The API reference documents every endpoint so you can drive scans from CI or your own scripts.
Quickstart — in the browser
The fastest way to see a result. No install, no card.
- Create your account. Go to scan.millenniums.ai/app and sign up with your email. A work email is best; a personal one works too. You get a free scan to start.
- Verify your email. Click the link we send you. This unlocks scanning. (Didn't arrive? Use "Resend" in the app.)
- Point it at a target. Paste your app's staging URL — the live address where it's running (not production). Optionally drag in your code (a
.zip) for a deeper scan. Press Start a scan. - Watch it work. The scan runs in an isolated sandbox and typically finishes in minutes. You'll see it probe each category live.
- Read the findings. Each proven vulnerability comes with its severity, impact, and a plain remediation. On a paid plan, every finding also includes a working proof of concept and a draft fix.
Quickstart — with the API
Prefer to drive it from a terminal or CI? Everything the app does is a REST call. Your access token is created at signup and shown in the app; it goes in an Authorization: Bearer header.
1. Check your account
Confirm your token works and see your plan and remaining quota.
curl https://scan.millenniums.ai/api/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{ "tenant": "acme-3f9a2c", "plan": "starter", "poc": true,
"scans_used": 1, "scans_limit": 5, "verified": true }
2. Start a scan
Give it a target. Optionally add source (a repo or path) for a much deeper white-box scan, and a budget spend cap.
curl -X POST https://scan.millenniums.ai/api/scan \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"https://staging.yourapp.com/chat","budget":10}'
{ "run_id": "20260731-abc123" }
3. Read the results
Poll the run until running is false, then read its findings.
curl https://scan.millenniums.ai/api/runs/20260731-abc123 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
That's the whole loop: me → scan → runs/<id>. The API reference documents every field.
How it works
Five stages, all automatic:
- Point. You register a target: a staging URL, a repository, or an API endpoint.
- Attack. Autonomous agents spin up in an isolated Docker sandbox and probe the AI attack surface — no scripts for you to write.
- Prove. When an agent finds a weakness, it tries to exploit it for real. Only exploits that actually fire become findings; unproven leads are held separately.
- Fix. Each finding comes with a plain remediation and, when you want it, a draft pull request your engineers review.
- Gate. On the CI plans, a scoped scan runs on each pull request and blocks the merge on a proven, net-new vulnerability.
When the scan ends, the sandbox — and everything in it — is destroyed.
Tools: autonomous AI agents driven by frontier LLMs, working in an isolated per-scan Docker sandbox, guided by our vulnerability skill pack (mapped to OWASP LLM Top 10 2025 + MITRE ATLAS). Findings are scored with CVSS 3.1; dependency CVEs use osv-scanner; fix PRs are generated for your GitHub repo.
What to point it at
The single most common question, answered up front: you give us a target, and optionally your source. They're not the same thing.
| Target (required) | Source (optional) | |
|---|---|---|
| What | Your live, running app — a URL or API endpoint that answers requests right now. | Your code — a git repo or an uploaded .zip. |
| Why | It's what the scanner actually attacks. | Lets the scanner read the code as it attacks (white-box) — deeper findings. |
| Example | https://staging.yourapp.com/chat | github.com/acme/app or my-app.zip |
Staging vs production — always aim at staging
- Production is the real app your customers use, with real data.
- Staging is a separate, running copy in a test environment — the same app, but with no real customers or data.
The scanner is a real attacker: it submits inputs, can create records, trigger actions your app exposes, and drive up model cost against your app. On staging that's harmless. On production it can mean junk data or real side effects for real users. Only scan production if you understand and accept that (a test account and a quiet time help).
How do I know my URL? A staging URL looks like…
It's just a web address where your app runs. Common shapes:
staging.yourapp.comorapp-staging.yourapp.com- A preview URL from Vercel or Netlify (they mint one per change)
- A
*-staging.onrender.comor*.herokuapp.comapp - A local address (
localhost:3000) exposed to the internet with a tunnel (ngrok, cloudflared)
Easiest: let the app find it for you
In the app, paste the one link you already know — your main website or app address — into "Not sure what to scan?" and press Find what to scan. We look at the page, spot the AI features and API endpoints, and check public records for your other environments (like staging. and app.). You get back a short list of recommended targets — click one to scan it. No hosting logins, no hunting.
Find it yourself — no developer needed
Prefer to do it by hand, or want to double-check? You don't need anyone technical. Two reliable ways:
- Open your own app and copy the link. Use your product the way a customer does — go to the page with the AI feature (the chat, the assistant) and copy the address straight from your browser's address bar. That address is your target.
- Follow the money to your hosting account. Your app runs somewhere, and whoever's card pays the hosting bill can see every URL. Check your statements for Vercel, Netlify, Render, Heroku, AWS, DigitalOcean, Google Cloud, log into that account, and the dashboard lists your app's addresses — production and any staging/preview ones.
Backup routes: your domain registrar / DNS (GoDaddy, Namecheap, Cloudflare — wherever you bought the domain) shows subdomains like staging., app., api.; or ask your hosting provider's support from inside your account.
I don't have a staging URL — how do I get one?
- Most hosts spin up a staging copy in one click from your account — Vercel, Netlify, and Render all offer free preview/staging environments, no code required.
- If your app is on GitHub, a preview URL is often generated automatically on each pull request.
- Only have production? You can still scan it — just treat it as a live attack (quiet time, a test account, expect some junk data). See Staging vs production above.
- No one technical at all? A freelancer can stand up staging in an afternoon — then you paste that URL here.
Once you have the URL, follow Run your first scan. Add your code (upload or repo) for a deeper white-box scan.
Black-box vs white-box
How much you give the scanner decides how much it finds.
- Black-box — you give only a live URL or API. The scanner attacks from the outside, like an anonymous attacker. Fast, zero setup, but it can only find what's reachable from the front door.
- White-box — you also give the application's source. The scanner reads the code as it attacks, so it understands the tools, prompts, and data paths behind the endpoint. This is a large uplift in both the number and the depth of findings.
Two ways to hand over the source — you don't need to know git:
- Upload a
.zipof your project. In the app, drop it into the scan box; over the API, upload it and pass the returnedupload_id. Your code is held only for that scan and deleted when it ends — never used to train. - Point at a git repo — GitHub, GitLab, Bitbucket, or Azure DevOps. Paste the repo URL in the
sourcefield, or in the app Connect GitHub once and pick a repo (including private ones) from a dropdown. For other hosts, paste the URL; private repos there use the.zipupload.
Complete scan — both passes, one click
Turn on Complete scan (the checkbox on the scan form) to run both passes for one target automatically: a black-box pass probes the live endpoints, then a white-box pass reads your source and re-tests the known surface. You get one merged findings list, each card tagged by the pass that found it. It needs your source (a repo or .zip) and uses about twice the budget of a single scan.
The white-box pass carries the black-box findings forward and re-tests each one: an issue that's still exploitable is shown as a ↻ re-confirmed regression (counted once, not twice), and anything only the source could reveal is added as new. Even the Free plan can run a Complete scan — capped at $20 total ($10 per pass).
What it tests
Every category on the OWASP LLM Top 10, mapped to MITRE ATLAS:
- Prompt injection
- Sensitive information disclosure
- Supply chain
- Data & model poisoning
- Improper output handling
- Excessive agency (tool & agent abuse)
- System prompt leakage
- Vector & embedding weaknesses (RAG)
- Misinformation
- Unbounded consumption (runaway cost)
The full matrix, with the mapping to MITRE ATLAS, is published on the Trust Center.
Tools: our per-category vulnerability skill pack, keyed to OWASP LLM Top 10 2025 and MITRE ATLAS v5.6.0 (technique ids verified against the source, not recalled). Supply chain has its own section.
Findings & proofs of concept
A finding is a vulnerability the scanner proved, not a guess. Each one carries:
| Field | What it is |
|---|---|
title | One-line summary of the vulnerability. |
severity | critical high medium low — impact-ranked. |
cwe / cvss | Standard weakness ID and CVSS score, when applicable. |
impact | What an attacker gains in plain terms. |
technical_analysis | Why it works, for an engineer. |
remediation | The concrete fix. |
poc | A working, re-runnable proof of concept. Paid plans only. |
endpoint / method / locations | Where it lives — the request and the code locations. |
poc) is held back. Upgrading to any paid plan unlocks it on all findings, including past ones.Dependency CVEs — how the list gets short
A dependency scan on a real app returns hundreds or thousands of known CVEs. Handing you that list is worse than useless: everything looks urgent, so nothing is. Each one is filtered through four questions, and only what survives is put in front of you.
| Stage | The question | What it removes |
|---|---|---|
| 1. Inventory | What versions do we actually depend on? | Everything you don't ship. The full list is kept as your dependency inventory (SBOM) — nothing is hidden, it's just not shouting. |
| 2. Exploitability | Is anyone exploiting this? CISA KEV, or EPSS ≥ 10%. | The large majority — high-severity CVEs with no observed or predicted exploitation. |
| 3. Reachability | Does our code call the vulnerable path? | Vulnerable libraries you use, but not the vulnerable part of. Checked automatically when a repo is linked. |
| 4. Determination | Have we already ruled on this? | Anything you marked not affected. The ruling is exported as OpenVEX (GET /api/runs/{run_id}/vex) and carries into future scans, so a settled CVE never re-nags. |
What's left is the actionable set: known-exploited or likely-to-be-exploited CVEs whose vulnerable code your app can actually reach, and which you haven't already ruled on. That's the number in the dashboard, the set in the compliance report, and the set ⬆ Update dependencies writes a PR for.
Reading a finding — the labels explained
Every finding is tagged with standard security labels so you can judge the risk and decide what to fix first. Here is what each label means, how to read it, and why it matters.
| Label | What it is & how to read it | Why it matters |
|---|---|---|
| Severity | critical high medium low — our impact ranking. | Your fix order. Address critical and high first — an attacker can cause real damage. Low is hardening. |
| CVE Common Vulnerabilities & Exposures | A specific, publicly-catalogued flaw in a specific software version — usually a third-party library your app depends on. The ID looks like CVE-2026-27962 (year + number); look it up at nvd.nist.gov for details. | The flaw is public — attackers already know it and often have ready-made exploits. The fix is almost always to upgrade the affected dependency to a patched version. |
| CWE Common Weakness Enumeration | The type of flaw, not a specific instance — e.g. CWE-89 (SQL injection), CWE-918 (SSRF), CWE-862 (missing authorization). | Tells you the kind of mistake, so the standard fix pattern is known. |
| CVSS Common Vulnerability Scoring System | A standardised 0–10 severity score. 9.0–10 critical · 7.0–8.9 high · 4.0–6.9 medium · 0.1–3.9 low. | An industry-standard number to compare and rank risk consistently. |
| OWASP LLM Top 10 | The security industry's standard list of the ten biggest risks specific to AI/LLM apps, coded LLM01–LLM10 (full list below). Every AI-specific finding maps to one. | Puts your risk in a framework auditors, engineers, and insurers recognise. |
| MITRE ATLAS | The attacker's playbook for AI systems — the AI counterpart of MITRE ATT&CK. Each finding maps to the technique an attacker would actually use. | Shows the real-world attack technique behind the finding. |
| EPSS Exploit Prediction Scoring System | The probability, 0–100%, that a given CVE will actually be exploited in the wild in the next 30 days. 0.5 means 50%. Applies to dependency CVEs, not app-logic findings. | Severity tells you how bad it would be; EPSS tells you how likely it is. Most high-severity CVEs are never exploited — this is what stops you fixing in the wrong order. |
| CISA KEV Known Exploited Vulnerabilities | The US government's catalogue of CVEs with confirmed, observed exploitation. A finding is either on it or it isn't. | Not a prediction — a fact. KEV means attackers are already using it. Fix these first, whatever the CVSS says. |
| Reachability | Whether your code actually calls the vulnerable path of a flagged dependency — confirmed reachable, not reachable, or not determined. Needs a linked repo. | A vulnerable library you never call the vulnerable part of is not an emergency. This is what turns a wall of CVEs into a short list. |
| PoC Proof of Concept | A working, re-runnable demonstration of the exploit — the exact steps and payload. | The finding is proven, not a guess. If it has a PoC, it is real and reproducible. |
CVE from our automated dependency scan) is a known flaw in a library you use — fix it by upgrading the library. An application finding is a flaw in your own app's logic — prompt injection, broken access control, and so on — fix it by changing your code or configuration.OWASP LLM Top 10 — what each risk means
| Code | Risk | In plain terms |
|---|---|---|
LLM01 | Prompt Injection | Attacker-supplied text overrides the model's instructions or hijacks its tools. |
LLM02 | Sensitive Information Disclosure | The app leaks secrets, personal data, or another user's data. |
LLM03 | Supply Chain | Vulnerable or untrusted dependencies, models, or plugins. |
LLM04 | Data & Model Poisoning | Attacker-controlled data corrupts training, fine-tuning, or the RAG corpus. |
LLM05 | Improper Output Handling | The app trusts model output unsafely — passing it into SQL, a shell, or HTML. |
LLM06 | Excessive Agency | The agent has too much power or access; a manipulated model can act on it. |
LLM07 | System Prompt Leakage | The hidden system prompt — and any secrets or logic in it — can be extracted. |
LLM08 | Vector & Embedding Weaknesses | RAG/vector-store flaws: cross-tenant retrieval, corpus poisoning, embedding leakage. |
LLM09 | Misinformation | False or fabricated output that causes real harm — unsafe reliance, hallucinated code. |
LLM10 | Unbounded Consumption | No limits on use — denial-of-wallet, resource exhaustion, or model extraction. |
Spend caps & safety
Autonomous agents call a language model, which costs money. Two independent limits make sure a scan can never surprise you:
- Per-scan budget cap. Every scan takes a
budget(US dollars, default10). The engine stops when it hits that ceiling. - Token watchdog. A separate backstop kills any scan that blows past a hard token count, even for a self-hosted model the pricing layer can't see. So a runaway loop can't drain your account.
Every scan also runs in its own isolated sandbox, and concurrency is capped per plan so you can't accidentally launch a hundred scans at once.
Authorization & domain verification
Because a scan is a real attack, you may only scan an app you own or are authorized to test. To make that more than a promise, the first time you scan a new public domain we ask you to prove you own it — one time per domain. This stops anyone from pointing us at a competitor.
Verify by either method (pick whichever you can reach — you only need one):
- Meta tag (easiest). Add a line to your homepage's
<head>. Most site builders — Wix, Squarespace, Webflow, Shopify — have a "header code" or "site verification" box for exactly this:<meta name="millenniums-verification" content="YOUR-TOKEN"> - DNS TXT record. Add a TXT record at your domain registrar:
millenniums-verification=YOUR-TOKEN
Verifying the root domain covers all its subdomains — verify acme.com once and you can scan staging.acme.com, api.acme.com, and the rest. You'll also confirm a short authorization certification (that you own the app, or are authorized to test it) before the first scan runs.
localhost, a private IP, host.docker.internal — need no verification. That's your own machine.Verify in the app when prompted, or over the API at POST /api/domains/verify. Discovery (reading a public page) needs no verification — only scanning does.
Your data
Your code and scan results are yours. Each scan runs in a throwaway sandbox that is destroyed when the scan finishes, so we hold as little of your code as possible and only for as long as the scan needs it. We never use your code or results to train any model. Full details, including sub-processors, are in the Trust Center and Privacy Policy.
Traceability report
Every scan produces a chain-of-custody report so you can prove — to yourself, a customer, or an auditor — exactly what happened to your data. It's not a marketing claim; each line is derived from a recorded event in the scan's lifecycle.
The report includes:
- A timestamped timeline: scan created → source received → isolated sandbox launched → scan finished → sandbox torn down → (for uploads) uploaded source deleted.
- Source provenance: whether the scan was black-box (live target only), or white-box from an upload or a git repo. Git URLs are recorded by host only — no paths or credentials.
- Attestations: isolated sandbox, sandbox torn down (verified against the container runtime), uploaded source deleted, spend cap enforced, and never used to train a model.
- Usage: tokens and LLM calls for the run.
Get it in the app on any run under Data trail — chain of custody (with a one-click JSON download), or over the API at GET /api/runs/{run_id}/trace.
Continuous scanning & drift
What: turns a point-in-time scan into ongoing coverage. Register your app as an asset, put it on a schedule, and MILLENNIUMS.AI re-scans it for you — and re-checks whenever it changes.
How: two controls, set per asset in the Continuous view (or over the API):
- Schedules — a daily, weekly, or monthly cadence. Re-scans run down the same path a manual scan does, so your quota, concurrency, and spend caps all still apply.
- Drift detection — turn on watch changes and, between scheduled runs, we re-fingerprint the app (preferring its OpenAPI/Swagger spec). A material change kicks off an out-of-cycle scan. Fingerprints ignore CSRF tokens, nonces, timestamps, and cache-busters, so a dynamic page doesn't false-trigger.
Why: apps change every release, and a yearly pentest can't see a hole a Tuesday deploy opened. Continuous scanning catches regressions when they land; drift means you only spend a scan when something actually changed — hands-off monitoring instead of a calendar reminder.
Tools: the same scan engine driven by a per-asset scheduler; app fingerprinting from the OpenAPI/Swagger spec (or a normalized surface crawl), diffed between runs with dynamic-token noise filtered out.
Dashboard & trends
The Continuous view rolls up every asset at a glance: its latest scan status, findings by severity, and the opened/resolved change since the previous scan. Two things make it board-ready:
- Risk trend. Each asset has a findings-over-time series — open count plus opened and resolved per scan — so you can show risk going down, not just a snapshot.
- Export. One click exports the whole org rollup as CSV (
GET /api/overview.csv).
Shadow-AI discovery (AI inventory)
What: finds the AI your organization is using that security doesn't know about — "shadow AI." Repositories quietly calling an LLM API, managed AI services switched on in a cloud account, self-hosted model servers or chatbot UIs exposed on your domains. It is a discovery and inventory capability: it tells you where AI exists, not whether it's vulnerable.
How: connect the surfaces you want looked at, and each run reports exactly what it covered. Every result is a candidate you confirm — it never registers anything on its own. Confirmed candidates merge into one deduped AI inventory with a coverage banner; dismiss the ones already known or out of scope.
| Surface | How it's connected | Finds |
|---|---|---|
| Repos | a GitHub org / account | AI-powered repositories, by their LLM-SDK usage. |
| Cloud | a read-only role / inventory | Managed AI services — Bedrock agents, knowledge bases, provisioned models, SageMaker endpoints, Lex bots, Comprehend. |
| External | your domains | Exposed AI surfaces — self-hosted model servers (Ollama, vLLM, TGI), OpenAI-compatible endpoints, chatbot UIs. |
Why: you can't secure or govern AI you don't know you have. Shadow AI is where data leakage, runaway cost, and compliance gaps hide — an inventory is the first control. What you confirm here becomes the set that gets tested and monitored. See Discovery & inventory.
Tools: GitHub org/repo API + LLM-SDK usage detection (repos); read-only cloud enumeration via boto3 — Bedrock, SageMaker, Lex, Comprehend (cloud); HTTP/TLS surface probing for model servers and OpenAI-compatible endpoints (external). All read-only.
AI supply chain
What: the risk that comes from the AI components your app is built on — third-party foundation models, fine-tuned or pretrained weights, training and RAG datasets, embeddings, plugins/tools, and the ML/LLM libraries and model registries in your stack. A poisoned model, a backdoored dataset, a typosquatted model on a hub, or a vulnerable ML dependency can compromise your app before you write a line of code. This is OWASP LLM03:2025 Supply Chain.
How: during a scan the agent inspects the AI components in scope — where models and datasets come from and whether that source is trusted, how plugins/tools are wired and how much they're trusted, and the dependency tree of your ML/LLM stack. Dependency CVEs are triaged by real exploitability (KEV / EPSS) and reachability, so you see the ones that actually matter rather than a wall of noise.
Why: your AI app is only as trustworthy as the models, data, and libraries it inherits — and those come from outside your codebase, bypassing your app-level controls. It's on the OWASP LLM Top 10 for exactly that reason.
Tools: osv-scanner for the dependency CVE inventory, then the EPSS / CISA-KEV / reachability triage funnel (see Dependency CVEs); our LLM skill pack maps findings to OWASP LLM03 and MITRE ATLAS AI Supply Chain Compromise (AML.T0010).
Infrastructure & Cloud
What: cloud security posture management (CSPM). Beyond the app, it checks your cloud for the exposures attackers hunt for — public buckets, over-broad IAM, ports open to the world, unencrypted or public data, and blind spots in your audit trail. Read-only, agentless, CVSS-scored.
How: connect a read-only role and we assume it, enumerate read-only, and run deterministic checks — no keys, no write access. A finding fires only on a resource that actually fails a check.
| Check | Severity | CVSS |
|---|---|---|
| S3 bucket public access | Critical | 9.8 |
IAM wildcard policy (Action:* on Resource:*) | Critical | 9.1 |
| Security group open to all ports | Critical | 9.8 |
| SSH / RDP open to 0.0.0.0/0 | High | 8.1 |
| RDS publicly accessible | High | 7.5 |
| RDS / S3 unencrypted at rest | Medium | 5.3 |
| CloudTrail logging disabled | Medium | — |
| Lambda function URL with no authentication | Critical | 9.1 |
| EKS API endpoint open to the internet | Critical | 9.0 |
| Cross-cloud federated trust with no subject condition | Critical | 9.6 |
| Secret with automatic rotation disabled | Low | 3.1 |
All three clouds connect automatically. AWS: apply the Terraform the app generates — it creates a read-only role (ReadOnlyAccess + SecurityAudit) trusting our scanner, gated by your unique external ID — then paste the role ARN. No keys leave your account. Azure: an app registration with Reader + Security Reader. GCP: a service account with Viewer + Security Reviewer. Every credential is verified with a real read-only call before it is stored, so a typo fails at connect time rather than silently at 3am. Secrets are write-only — never returned by any screen or API. Kubernetes and any Prowler inventory can still be ingested directly. See the API.
Why: your app can be flawless and still be breached through the infrastructure it runs on — and posture drifts every time a team ships. Because it's read-only and agentless, it's safe to run continuously, not once a quarter.
Tools: read-only cloud enumeration via boto3 (AWS) against a role you grant (ReadOnlyAccess + SecurityAudit), then our deterministic, CVSS-scored posture checks. Any Prowler-format inventory can be ingested instead, and the same checks run for GCP / Azure / Kubernetes.
Risk graph & attack paths
What: every asset, identity, network route and finding across your clouds, in one normalized graph — and the ranked list of attack paths through it. A finding says "this bucket is public." A risk says "the internet reaches this workload, which holds a credential, which reads your customer data." The path is the product.
How: provider terminology stops at the collector. An AWS role, an Entra ID service principal and a GCP service account all become the same kind of node, so a path can cross a cloud boundary. Eight rules run over the result — exposed workload → sensitive data, credential chains, cross-cloud pivots, privilege escalation, shadow data, exposed AI assets, and proven cross-layer chains. Each returns a path, not an alert.
Why: posture tools produce hundreds of criticals and no priority. Ranking by CVSS alone tells you nothing about whether an attacker can actually get there. Reachability is the priority, and reachability only exists in a graph.
Tools: the graph is emitted in graphify's node-link format, so you can traverse it yourself — graphify path "internet" "customer-data" — or download it from the Risks view. No graph database to run, and nothing proprietary about the file.
Cross-cloud risk
What: the paths that begin in one cloud and end in another — the ones neither provider's own tooling can see, because each only looks at itself.
How: we read the trust relationships that actually cross the boundary: an AWS role whose trust policy names an Entra ID or Google issuer, a GCP workload identity pool trusting an AWS account. Both sides must agree before an edge exists.
sts.windows.net/<tenant> proves AWS trusts that Azure identity whether or not you have connected Azure. Connect one cloud, still see the cross-cloud exposure.Why: federated identity is how teams avoid long-lived keys — and it is also how an attacker moves between your clouds. A trust with no sub condition trusts every identity in that external tenant, not the one you meant, and it is easy to ship by accident when copying an OIDC snippet. That is its own critical finding.
Tools: AWS IAM trust policies, Entra ID via Microsoft Graph, GCP workload identity federation. Issuers are identified, never guessed — an unrecognised issuer produces no cross-cloud edge, because a mislabelled one is worse than a missing one.
Data security posture (DSPM)
What: what is actually in your data stores — personal data, cardholder data, health data, credentials — and where a copy of production has quietly ended up.
How: two tiers. Bounded sampling reads a small, capped sample of objects through the same read-only role you already granted — no snapshots, no extra permissions, nothing installed. For unmanaged databases living on a VM disk, an ephemeral snapshot worker runs inside your account, mounts a snapshot read-only, and transmits findings only — never a value, never file contents, never a row — then terminates.
Shadow data: stores are schema-fingerprinted, so a production schema sitting in a staging bucket is flagged as a copy worth reviewing — and escalated to critical when that copy is also internet-exposed.
Why: severity is meaningless without knowing what is at stake. A public bucket of CSS files and a public bucket of customer records are the same finding and completely different risks. Classification is what separates them — and it is why we never guess: an unlabelled store stays unknown, never assumed safe and never assumed sensitive.
Tools: pattern + entropy classifiers with validity checks (Luhn, SSN structure), column-name corroboration, and schema fingerprinting for lineage. Bounded per object and in total; coverage is reported as what was actually sampled, never extrapolated.
Code-to-cloud — stop it before it ships
What: a check on every pull request that reads your Terraform, CloudFormation, ARM and Kubernetes manifests and fails the build when a change would create a real problem.
How: the planned resources are merged into your live risk graph, and we report what the change would introduce — not what your account already looks like. Findings post back to the pull request as a single comment that is edited on each push, never a new one each time.
The gate blocks on exactly what your diff is responsible for: a critical misconfiguration it declares, an attack path it introduces, or a credential it commits. Pre-existing account risk never blocks a pull request — that is how a gate gets switched off. Report-only mode is a one-line change.
Why: the cheapest moment to fix a misconfiguration is before it exists. Everything after that is remediation, a change window, and an argument about priority.
Tools: a real HCL parser (not line matching), plus CloudFormation / ARM / Kubernetes. Secret scanning excludes placeholders and variable references — a scanner that flags password = var.db_password trains people to ignore it. Ships for GitHub Actions and Azure DevOps; authenticates with a per-asset key, never your account token.
Ask the graph
What: saved questions and a query builder for the ones we did not anticipate — "show me every internet-exposed VM holding a plaintext key that grants access to a store containing personal data."
How: pick a kind of asset, filter on its attributes, then follow a relationship to what it can reach. Results are paths with the evidence for each hop, not a list of names.
Why: your questions during an incident are not the ones a vendor pre-wrote. Free-text search cannot express "A and B and reaches C" — that is set membership and reachability, which needs the graph.
Tools: a closed, safe query form — a query is data, never code, with validated relations and bounded traversal, so it can neither be turned into code execution nor hang on a large estate.
Remediation & ticketing
What: a signed webhook when a risk matches your rules — wired to your own automation, or straight into Jira or ServiceNow with the attack path in the ticket.
How: we detect, sign, and fire. A function you own and deploy holds the credentials and makes the change. We ship the reference Lambda and Terraform; you apply it.
Why: a security vendor holding write credentials across your cloud is a single point of catastrophic failure, and it is the hardest thing to get through a security review. Splitting detection from execution removes both problems.
Tools: HMAC-signed, replay-bounded deliveries. Destinations must be HTTPS and resolve to a public address — a webhook cannot be aimed at an internal service or a metadata endpoint.
Runtime sensor Enterprise add-on
What: optional eBPF detections from your nodes, landing on the same risk graph as everything else.
How: a Falco DaemonSet plus an outbound-only forwarder. Enable it in the app, enrol a sensor, deploy with the command shown. Turning it off revokes every sensor immediately.
Why: configuration drifts and a workload can be compromised without a single setting changing. Provider threat feeds catch a lot of that with no agent at all — this is for teams who have decided that sub-minute, in-kernel visibility is worth a privileged DaemonSet, and it is deliberately their decision rather than our default.
Detection, not prevention. It never blocks, kills, or quarantines. The rest of the platform installs nothing on your servers; this deliberately does, which is why it is a separate add-on and off by default. If you would rather stay fully agentless, GuardDuty, Defender for Cloud and Security Command Center findings are ingested onto the same graph with no agent at all.
Tools: Falco (CNCF) for the eBPF probe and rule set. The forwarder — the component holding your key — is unprivileged and drops all capabilities. Ingest is capped; over the limit, events are dropped and counted, never lost silently.
Network penetration testing — the phases
What: testing the network your app runs on — internet-facing and internal — for the openings an attacker uses: exposed services, weak configuration, missing segmentation, and (once certified) exploitable paths. It ships in four phases, P1–P4.
Why: the app is one layer; the hosts, ports, and internal network around it are a separate attack surface — and the one a real intruder pivots through. Confirm-only by default, so you get the coverage without the risk.
How: every phase stays behind the same pre-engagement gate — an explicit scope, proven ownership, and a signed Rules of Engagement (no-DoS by default) — with an always-visible Emergency Stop while a scan runs. The four phases, each with its own What / How / Why:
P1 · External confirm-only live
What: a self-serve, confirm-only assessment of your internet-facing IPs and ranges — host discovery, port and service scan, service enumeration, and non-intrusive vulnerability checks. It confirms exposures; it does not exploit them.
How: in the Network tab, define a scope (IP/CIDR, /24 or narrower), prove you own each public target (host a token at /.well-known/millenniums-scan-authorization), sign the Rules of Engagement, then launch. Findings are CVSS-scored and mapped to PCI DSS 11.4 / SOC 2.
Why: the outsider's view — what an attacker sees before any foothold. Fast, safe, and authorized, so you can run it on demand instead of scheduling a yearly engagement.
P2 · Internal connector live
What: the assumed-breach view — testing internal assets an external scan can't reach (flat networks, exposed internal services, lateral-movement paths).
How: enroll a connector in the Network tab (one-time key + a docker run command) and run it on any host inside the segment you want tested. It is outbound-only — no inbound ports — heartbeats to the platform, runs the Tier-1 scanners locally against your authorized scope, and streams findings back over HTTPS. No LLM key or data leaves your box beyond the findings; the platform does the reasoning and report.
Why: external scanning only sees the edge. Real internal risk needs something on the network — deployed by you, so the connector's presence is itself the authorization.
P3 · Credentialed & Tier-2 sweep live
What: two additions. Credentialed scanning tests what a phished or insider account can reach, using low-privilege credentials you supply. Tier-2 sweep is a broader, rate-capped active scan above the confirm-only default.
How: add credentials (SSH/SMB/web/domain) to the vault in the Network tab — stored server-side, masked on every read, injected into the scan sandbox only at run time, never logged. Choose the Rate-capped sweep intensity (behind its own acknowledgement) for broader coverage; asset-class exclusions (fragile/OT devices) and a lockout-aware policy keep it safe.
Why: unauthenticated scanning finds the exposed surface; credentialed testing finds what's reachable with a foothold — the depth auditors and real engagements expect.
P4 · Exploitation (Tier 3) gated — off by default
What: proving real impact by testing weak points — human-gated per target, non-destructive only. Built as a control plane; execution is disabled by default.
How: a fail-closed gate requires all of: a signed enablement certification (legal/insurance/authorization verified), the enablement-readiness checklist complete, a per-target human approval, the target inside a signed authorization, and a non-destructive catalog entry. It runs only after every one of those holds.
Why: exploitation is legally and operationally sensitive. It stays off until an engagement is certified — the gate is enforced in code, not just policy — so the capability can never fire by accident.
Tools: host/port/service discovery with nmap and naabu (and rate-capped masscan at Tier 2); nuclei + its template library and our own non-destructive detection checks for exposures; enum4linux-ng / smbmap for service enumeration. Tier-3 (gated, off by default) draws on a curated, non-destructive subset of metasploit, netexec/impacket, responder, and bloodhound-style AD analysis.
Compliance report & attestation
Every completed scan produces an audit-support report grounded in the standards enterprises test against (PTES, NIST SP 800-115, OWASP WSTG, CREST, CVSS). It ships in three tiers so each reader gets the right cut:
- Letter of Attestation — a redacted, signed one-pager you can hand to your customers, procurement, or third-party-risk reviewers. It has the scope, dates, methodology, and severity counts — no exploit detail. Get it at
GET /api/runs/{id}/attestation. - Full compliance report — executive summary plus per-finding technical detail: Rules of Engagement, a severity heat-map, a remediation roadmap, and an AI traceability matrix mapping each finding to OWASP LLM 2025 → MITRE ATLAS → NIST AI RMF / ISO 42001 / EU AI Act. Cross-mapped to SOC 2, ISO 27001, and NIST CSF.
GET /api/runs/{id}/compliance. - JSON twin — the same data as
report.jsonfor Vanta, Drata, or Secureframe.GET /api/runs/{id}/compliance.json.
Honest scoring. CVSS is a real advisory score for dependency CVEs, or the standard class base vector for a vulnerability class (labeled as such) — never a fabricated number. Behavioral findings (jailbreaks, prompt injection) carry an Attack Success Rate — a real successes/trials figure — when you measure one (POST /api/runs/{id}/measure-asr), because a jailbreak that fires 3 times in 100 isn't one that fires 90.
Tools: CVSS 3.1 scoring; OWASP LLM 2025 + MITRE ATLAS v5.6.0 traceability; framework cross-maps to PCI DSS 11.4, SOC 2, ISO 27001 / 42001, NIST CSF / AI RMF, EU AI Act; OpenVEX for affected/not-affected determinations; and Attack Success Rate reported with a Wilson 95% interval. Optional certified CREST/OSCP human review.
SSO, SCIM & roles
What: enterprise identity for teams — single sign-on, automatic user provisioning and deprovisioning, role-based access, and an audit log. Every asset, scan, and finding is scoped to your workspace.
How: four roles — owner (everything, incl. billing), admin (manage the workspace), member (do the work), viewer (read-only) — plus:
- Single sign-on (OIDC) — your team signs in with your identity provider (Okta, Entra/Azure AD, Google, Auth0); new users in your email domain are provisioned automatically. Owner-configured with an issuer URL, client ID/secret, and your domain.
- SCIM 2.0 provisioning — your IdP creates and, critically, deprovisions users automatically. Deactivating someone in your IdP cuts their access here immediately: their tokens are revoked, not just a flag flipped.
- Audit log — who triggered which scans, and who viewed or exported which reports.
Why: at team scale the risk isn't only outside — it's a former employee whose access never got cut, or an over-privileged account. SSO centralizes sign-in on your IdP's policy (MFA, conditional access); SCIM guarantees off-boarding is instant, not a ticket someone forgets.
Tools: OpenID Connect (OIDC) for SSO — Okta, Microsoft Entra ID, Google Workspace, Auth0; SCIM 2.0 for provisioning/deprovisioning; per-tenant bearer tokens with constant-time comparison and immediate revocation. Enterprise plan. See SSO & SCIM to set it up.
Guide: run your first scan
Goal: go from a fresh account to a proven finding.
Prerequisites
- A verified account (browser quickstart).
- A target URL — a live, running instance of your app that you own or may test. Use a staging URL, not production. This is required; your code alone isn't a running app. Don't have one? See What to point it at.
- Optional: your source — a git repo URL or a
.zipupload, for a deeper white-box scan.
Steps
- Open the app and paste your target into the scan box, or call
POST /api/scan. - If you have the source, add it — a repo URL or local path in the
sourcefield. This is the single biggest lever on finding quality. - Start the scan. Note the
run_id. - Wait for it to finish (status
running→false). Minutes, typically. - Open the report and triage from the top: criticals first.
Troubleshooting
- 403, "verify your email" — click the verification link, or hit "Resend" /
POST /api/resend. - 402, quota reached — you've used your included scans. Add a card / upgrade (plans).
- 500, "scan did not start" — the engine couldn't launch (Docker or the model key). For self-hosted, check your
.env; on our hosted app, retry. - Zero findings on a URL-only scan — provide
sourceand re-run. Black-box alone often misses AI-specific paths.
Guide: register a target
If you scan the same app repeatedly, register it once on the Targets tab instead of pasting it in every time. Each target remembers its own settings, so a re-scan is a single click on Scan now. Registered targets are also what the CI plans scan automatically on each pull request.
What each field does
| Field | What it does |
|---|---|
| Name optional | A label so you can recognise the target in the list. Defaults to the target itself. |
| Target | The app to test — a URL, a code repository, an IP address, or a domain. Always aim at staging, never production (see What to point it at). |
| Source repo optional | A link to your code repository. Adding it turns on white-box testing — the scan reads your source, which finds far more real bugs than testing only from the outside (see Black-box vs white-box). |
| Focus / instructions optional | Steer the scan in plain English — the areas to focus on (e.g. “login and payments”), test credentials to sign in with, or specific pages to probe first. |
| Scope | Full scan, or Changed files only — a faster, cheaper re-scan that looks at just what changed. Available when the target is a code repository or you’ve added a Source repo. |
| Base ref changed-files only | What to compare against — a branch, tag, or release. Leave it blank to compare against the repository’s main branch. |
What happens when you click Scan now
- Your settings are checked, and anything that can’t work is caught right away — for example Changed files only needs a code repository, so you’re told before the scan starts rather than after it fails.
- Your plan and spend cap are applied, so a scan can never run past your budget (see Spend caps & safety).
- The test runs in a throwaway, isolated sandbox — your code is never reused or kept (see Your data).
- When it finishes, the results appear under Findings (see Read your scan results).
Guide: read your scan results
When a scan finishes, open it from the Scans list. Its results appear under Findings, organised into tabs so you never have to scroll to reach a section.
The tabs
| Tab | What's in it |
|---|---|
| Findings | Every vulnerability the scan proved, as cards you can expand. Where you'll spend most of your time. |
| Dependency CVEs | Known flaws in your third-party libraries (from an automated dependency scan), kept separate from the app-logic findings. Exploitable ones are auto-checked for reachability — whether your code actually calls the vulnerable path — and the unreachable ones are suppressed, with an OpenVEX export (⬇ VEX) of those determinations for your auditors. |
| Penetration Test Report | The formal written report (produced only when a scan runs to completion). Share it, download a branded PDF, or the raw .md. |
| Data Trail | The chain-of-custody record — proof your code ran only in a throwaway sandbox, was never used to train any model, and was deleted when the scan ended. |
Filter the findings
A large scan can surface hundreds of findings. The coloured pills above the list let you focus — click one to filter, click ✕ clear filter to see everything again:
- Severity pills — Critical High Low — show only findings at that level. A big scan opens focused on the most severe by default, so the worst is in front of you first.
- ⚡ PoC — show only findings that come with a working, proven exploit (the highest-confidence ones).
Every finding is also adversarially re-checked before it's shown: anything the scanner judges a likely false positive is hidden by default (a toggle brings it back so you can review the call yourself).
Read one finding
- Start with Critical and High — those are what an attacker reaches soonest.
- Open "Proof of concept" to see, in plain steps, exactly how the vulnerability is exploited. On paid plans, expand ▸ Exploit script for the actual runnable exploit so an engineer can reproduce it.
- Read "Remediation" — the concrete fix — and the code locations.
- Fix it (see the next guide) and re-scan to confirm the hole is closed.
Guide: fix what the scan found
On the Developer plan and above, with GitHub connected, MillenniumsAI can draft the fixes for you as pull requests — you review and merge; nothing changes on its own. Two one-click actions sit in the top row of a completed scan.
Before you start
- You're on the Developer plan or higher.
- You've connected GitHub (Integrations tab), and the scan used a GitHub repository as its source.
- The scan has completed — a scan that stopped early can be Resumed to completion first.
1. Fix your own code — ⚙ Generate fix PR
For application findings — flaws in your code, like broken access control or injection:
- Open the completed scan and click ⚙ Generate fix PR in the top row.
- It locates the code behind each finding, writes a minimal fix, and opens a draft pull request on your repo.
- Click ↗ View fix PR to review it on GitHub.
- Fix not quite right? Click ↻ Regenerate and tell it what to change or preserve (e.g. "keep the existing session check"). It retries with your guidance and updates the same PR — no duplicates.
2. Fix vulnerable libraries — ⬆ Update dependencies
For dependency findings — known CVEs in the third-party libraries your app relies on:
- Click ⬆ Update dependencies in the top row.
- It bumps each vulnerable package to its patched version in your editable manifests (
requirements.txt,package.json) and opens a draft PR. - Vulnerabilities pinned in a lockfile (
package-lock.json,poetry.lock,uv.lock) can't be safely edited by hand — the PR lists each with the exact command to run (e.g.npm install axios@1.6.0). - Review the changes, run any listed lockfile commands, and merge.
Guide: scan on every pull request
On the Developer plan and up, a scoped scan runs on each pull request that touches your AI surface and blocks the merge on a proven, net-new vulnerability. Recurring findings are de-duplicated, so the pipeline stays quiet until something real appears. On the Developer plan and above, a confirmed finding can also open a draft fix pull request (and a dependency-update PR) your engineers review — nothing merges on its own. Review status is available at GET /api/pr-reviews.
Guide: scan with the GitHub Action
Wire scanning into any pipeline with our published GitHub Action. It scans on push and gates the build — or just notifies.
- Get a trigger key. In the Continuous view, open your asset and click CI to reveal its per-asset trigger key and asset id. The key is scoped to that one asset — it can start and read only that asset's scans, never your whole account.
- Store it as a repo secret named
MILLENNIUMS_SCAN_KEY. - Add the workflow at
.github/workflows/security.yml:
name: security
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: bl014h/millenniums-scan-action@v1
with:
api-key: ${{ secrets.MILLENNIUMS_SCAN_KEY }}
asset-id: "abcd1234"
fail-on: high # critical | high | medium | low | never
fail-on: never is notify-only — it runs the scan and posts a job summary, but never fails the build. Rotate a key any time from the same panel; the old one stops working immediately.
Guide: ask questions about a scan
Not sure what a finding means, or how to fix it in your stack? Ask. POST /api/chat takes a list of messages and, optionally, a run_id so the answer is grounded in that specific scan's findings. Use it to turn a report into a fix plan.
Plans, quota & overage
Pick a monthly plan; add scans as you grow. Full pricing is on the pricing page.
| Plan | Price | Scans | Working PoC | Concurrent | Built for |
|---|---|---|---|---|---|
| Free | $0 | A scan to start | — | 1 | Trying it out |
| Starter | $249 / mo | 5 / mo | ✓ | 2 | Solo builders |
| Developer | $999 / mo | 15 / mo | ✓ + CI + fix & dependency PRs | 3 | Growing teams |
| Enterprise | Custom | Unlimited (fair use) | ✓ + SSO/SCIM + on-prem / BYO-key | 8 | Custom, sales-led |
- What's a scan? One run against one target. A pull-request check and a full scan each count as one.
- Overage. On paid plans, scans beyond your monthly quota bill at the posted per-scan rate ($199) rather than blocking you.
- Free tier. A one-time allowance (never resets) so you can see a real finding before you decide. The working exploit and autofix unlock on any paid plan.
Start a self-serve upgrade from the app, or with POST /api/billing/checkout. Enterprise is sales-led — book a walkthrough.
API — Authentication
Every request except signup and the Stripe webhook is authenticated with a bearer token. Your token is created at signup and shown in the app.
Authorization: Bearer YOUR_ACCESS_TOKEN
A missing or unknown token returns 401 Unauthorized. Keep your token secret — it grants full access to your account's scans and findings. If it leaks, rotate it (below) or from Settings in the app.
Passwordless sign-in. Emails a single-use, 15-minute magic link to the address on file. Always returns 200 whether or not an account exists (no account enumeration); rate-limited per network.
| Body | Type | Notes |
|---|---|---|
email | string | Required. The account email. |
Exchange a magic-link token for a fresh access token bound to your account. The token is single-use and expires after 15 minutes. Returns 400 if invalid or expired.
| Body | Type | Notes |
|---|---|---|
token | string | Required. The token from the #login= fragment of the emailed link. |
{ "token": "NEW_ACCESS_TOKEN", "tenant": "acme-1a2b3c", "plan": "free", "verified": true }
Issue a new access token and revoke every previous one — use this if a key leaks. Authenticated with your current token; other signed-in sessions are logged out. Returns { "token": "…" }.
API — Base URL & conventions
- Base URL:
https://scan.millenniums.ai - Content type: requests and responses are JSON. Send
Content-Type: application/jsonon POSTs. - Success:
200 OKwith a JSON body. - Errors: a non-2xx status with
{ "error": "message" }. See status codes.
API — Account
Your account, plan, and quota. The quickest way to confirm a token works.
| Field | Type | Meaning |
|---|---|---|
tenant | string | Your account ID. |
plan | string | free · starter · developer · team · enterprise. |
poc | bool | Whether working PoCs are unlocked on your plan. |
scans_used / scans_limit | int | Usage against your allowance. |
verified | bool | Email verified — required to scan. |
Public. Create an account and get a token. A verification email is sent automatically.
| Body | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Work email preferred. Disposable domains are rejected. |
company | string | no | Used to name your account. |
curl -X POST https://scan.millenniums.ai/api/signup \
-H "Content-Type: application/json" \
-d '{"email":"you@yourcompany.com","company":"Acme"}'
{ "token": "…", "tenant": "acme-3f9a2c", "plan": "free", "verified": false }
Re-send the verification email to your account's address. Requires your token.
API — Scans
Turn one public link into scannable targets. Give the url of your site or app; we fetch the page, detect the AI surface (chat widgets, API/AI endpoints, LLM providers) and stack, and enumerate sibling environments from public certificate-transparency logs. Read-only — it doesn't attack anything.
| Body | Type | Notes |
|---|---|---|
url | string | Required. A public https:// website or app link. |
{ "root_domain": "acme.com",
"ai_surface": [ {"type":"chat-widget","name":"Intercom"}, {"type":"api-endpoint","path":"/api/chat"} ],
"environments": [ "staging.acme.com", "api.acme.com" ],
"recommended_targets": [ "https://acme.com/api/chat", "https://staging.acme.com" ] }
Pick one of recommended_targets and pass it as the target to /api/scan.
Upload your application's source as a .zip for a white-box scan — no git required. The body is the raw zip bytes (Content-Type: application/zip). Returns an upload_id you pass to /api/scan. The upload is stored only for your account, used for that one scan, and deleted when it finishes. Max 100 MB.
curl -X POST https://scan.millenniums.ai/api/upload \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @my-app.zip
{ "upload_id": "7Qb2x9Za" }
.zip into the scan box — or send it to whoever manages your code and have them do it.Start a scan against a target. Gated on email verification, your remaining quota, and your plan's concurrency limit.
| Body | Type | Default | Notes |
|---|---|---|---|
target | string | — | Required. A URL, repository, or API endpoint to attack. |
upload_id | string | none | Optional. The id from POST /api/upload — a white-box scan of your uploaded code. Deleted after the scan. |
source | string | none | Optional. A git URL (https:// or git@) to clone for white-box. Use this or upload_id. |
budget | number | 10 | Per-scan spend cap, US dollars. |
mode | string | standard | Scan profile: quick, standard, or deep. |
curl -X POST https://scan.millenniums.ai/api/scan \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"https://staging.yourapp.com/chat",
"source":"https://github.com/acme/chat-app","budget":15}'
{ "run_id": "20260731-abc123" }
run_id immediately; poll GET /api/runs/<id> for progress and results.run_id. When you scan a linked GitHub repo whose commit is unchanged since a completed scan, no scan is started and no quota is spent — you get the baseline result instead. Check for no_changes before reading run_id, or your integration will break on the cheap path.
{ "no_changes": true, "baseline_run": "20260731-abc123",
"findings_count": 3 }
Pass force_full: true to scan anyway. A baseline from a scan that never finished is not trusted — you get a real scan instead.Incremental re-scans
Re-scanning a repo that hasn't changed is pure cost with no information. When a scan targets a linked GitHub repo, the commit it ran against is recorded as a baseline, and the next scan of that repo takes one of three paths:
| Situation | What happens |
|---|---|
| Same commit, previous scan completed | No scan, no charge. You get no_changes plus the baseline run and its finding count. |
| Commit changed | Only the changed files are mounted, so the expensive reconnaissance pass stays cheap. |
| Previous scan never finished (stopped, or hit its spend cap) | A full scan runs. An incomplete scan is never trusted as a baseline. |
Controlling a run
Stop a running scan. Returns 409 if it isn't running. You keep whatever it proved before you stopped it.
Finish an interrupted scan from its saved state instead of re-running it — for a scan that hit its spend cap, stalled, or was stopped. Returns 409 if it's already running.
The engine's own written pentest report (markdown). 404 when the scan didn't complete far enough to produce one. For the audit-facing document use the compliance report.
What the scan actually did — stages reached, agents run, token usage. Use it to see where a scan spent its budget.
Generate a draft pull request fixing this scan's findings in your own code. Needs a connected GitHub repo. GET the same path for job status. Draft, always — nothing auto-merges.
Generate a draft PR bumping vulnerable dependencies (critical/high CVEs) to patched versions. GET for job status.
Check whether your code actually calls the vulnerable path of each exploitable dependency CVE; unreachable ones are suppressed. GET for job status. Runs automatically on completed scans with exploitable CVEs and a linked repo.
An OpenVEX document of this run's affected / not-affected determinations. POST the same path to set or clear a finding's determination — that's how a finding becomes risk-accepted, and it carries across future scans so it never re-nags.
Share a scan's summary to Slack (channel: "slack") or by email.
Buy the certified human review add-on for this scan ($399) — a CREST/OSCP reviewer validates every finding and the report is signed with their name. Returns a checkout URL; idempotent if already purchased, and 400 if your plan already includes review.
List your scans, most recent first — each with id, status, running, and findings_count.
One scan in full, including the findings array (see Findings for every field). On free-tier plans the poc field is withheld. Returns 404 if the run isn't yours.
The scan's chain-of-custody report — see Traceability. A timestamped timeline plus attestations proving your code stayed in an isolated sandbox, the sandbox was torn down, and (for uploads) the source was deleted. Every field is derived from a recorded event, not asserted.
{ "run_id": "20260731-abc123",
"timeline": [
{ "ts": "2026-07-31T05:00:00Z", "event": "Isolated sandbox launched", … },
{ "ts": "2026-07-31T05:04:03Z", "event": "Sandbox torn down", "detail": "verified" },
{ "ts": "2026-07-31T05:04:04Z", "event": "Uploaded source deleted", … } ],
"attestations": { "isolated_sandbox": true, "sandbox_torn_down": true,
"uploaded_source_deleted": true, "no_model_training": true } }
| Field | Type | Meaning |
|---|---|---|
id | string | The run ID. |
status | string | running, completed, stopped: token cap, … |
running | bool | true until the scan finishes. |
findings_count | int | How many proven findings. |
findings | array | The findings (full detail on ?full / single-run fetch). |
API — Targets
Saved, named targets you scan repeatedly. These are what the CI plans scan on each pull request.
List your registered targets.
| Body | Type | Default | Notes |
|---|---|---|---|
target | string | — | Required. The URL / repo / API. |
name | string | = target | A friendly label. |
source | string | none | Source for white-box scans. |
instruction | string | none | Plain-English focus for the scan — areas to prioritise, test credentials, or specific endpoints to probe first. |
scope_mode | string | full | One of full, diff, auto. diff scans only files changed since diff_base and requires a git repo (a source, or a repository target) — otherwise returns 400. |
diff_base | string | default branch | With scope_mode: diff, the branch, tag, or commit to compare against. |
API — Domain verification
Prove you own a domain before scanning it — see Authorization & verification. Scanning a public target you haven't verified returns 403 with the token and instructions.
Returns your per-domain token and re-checks ownership (meta tag, then DNS TXT). Pass attest: true to record the authorization certification. Call it once to get the token, add the tag/record, then call it again to confirm.
| Body | Type | Notes |
|---|---|---|
domain | string | Required. The domain (or a URL — we use its root). |
attest | bool | Certify you own it / are authorized to test it. |
{ "domain": "acme.com", "verified": true, "method": "meta",
"meta_tag": "<meta name=\"millenniums-verification\" content=\"…\">",
"dns_txt": "millenniums-verification=…" }
List your domains and their verification status.
API — Schedules & assets
Register assets and put them on a cadence. Scheduling requires a paid plan. See Continuous scanning.
List your registered assets. POST /api/targets registers one ({ "target", "name" }); DELETE /api/targets/{id} removes it.
Create or update a schedule.
| Body | Type | Notes |
|---|---|---|
asset_id | string | Required. A registered asset. |
cadence | string | daily · weekly · monthly. |
trigger_on_change | bool | Enable drift-triggered re-scans between runs. |
GET /api/schedules lists them; DELETE /api/schedules/{id} removes one.
Per-asset rollup: latest status, severity counts, opened/resolved delta, and schedule. /api/overview.csv returns the same as CSV. GET /api/assets/{id}/trend returns findings-over-time for one asset.
Findings-over-time for one asset — its scans oldest to newest, each with the open count plus what opened and what resolved since the previous scan. This is the series behind the risk-trend chart.
{ "asset": { "id": "a1b2c3", "name": "Support chat", … },
"series": [ { "at": "2026-07-24T…", "run_id": "…",
"open": 5, "opened": 2, "resolved": 1 }, … ] }
Scan a registered target now, using its saved settings — source, scope, instruction, learned knowledge, and its prior findings for regression awareness. Returns {"run_id": …}, subject to your plan's quota and concurrency limits.
Remove a registered target. Past scans and their reports are unaffected. DELETE /api/schedules/{schedule_id} removes a schedule the same way.
Revoke the asset's current CI scan key and issue a new one. The old key stops working immediately.
API — CI trigger keys
A per-asset key lets CI or a webhook start and read scans of one asset without your account token. See the GitHub Action guide.
Reveal (or mint) the asset's trigger key and its trigger/status URLs. POST /api/assets/{id}/key/rotate revokes the old key and mints a new one.
Header X-Scan-Key: {key} (not a bearer token). Starts a scan of the asset; returns { "run_id" }. A quota/concurrency block returns a non-2xx so CI sees it.
Header X-Scan-Key. Poll status: { status, done, findings_count, severity, max_severity }. A key can only read its own asset's runs.
API — Discovery & inventory
Find AI assets and manage the unified inventory. See Shadow-AI discovery.
Scan a connected GitHub org/account ({ "github_org" }, blank = everything you can access) for AI-powered repos. Returns candidates plus honest coverage (repos_scanned/total/skipped). POST /api/discover/repo checks a single repo.
The unified inventory: candidates across every surface that ran, plus a coverage banner. POST /api/shadow/confirm ({ "key" }) registers a candidate as an asset; POST /api/shadow/dismiss hides one.
Scan one connected repo (github_repo: "owner/name") for LLM SDK and API usage. Returns whether it's AI-powered, which providers, and the files that evidence it.
Confirm a candidate — it becomes a registered asset you can scan and schedule. POST /api/shadow/dismiss dismisses one (not AI, already known, out of scope); the ruling survives future discovery runs.
API — Infrastructure & Cloud
Map a read-only cloud inventory to misconfiguration findings. See Infrastructure & Cloud.
Connect a read-only AWS role: { "provider": "aws", "role_arn", "external_id", "region" }. Owner + paid plan. DELETE /api/cloud disconnects.
Assume the connected role, enumerate read-only (S3/IAM/EC2/RDS/CloudTrail), and run the checks. Returns CVSS-scored findings worst-first + a severity summary. Read-only — no model cost.
Connection state + latest scan summary. GET /api/cloud/posture returns the latest findings.
For GCP/Azure/Kubernetes (or a paste path): { "inventory": {…} } with _provider set — the same checks run over your read-only inventory. (Admins can also use POST /api/admin/cspm.)
API — Reports & attestation
Audit-support deliverables from a completed run. See Compliance report & attestation.
The full compliance report (HTML). /api/runs/{id}/compliance.json returns the JSON twin for Vanta/Drata/Secureframe.
The redacted, shareable Letter of Attestation — scope, dates, methodology, severity counts, no exploit detail. Add .json for the structured form.
{ "attestation_id": "MLN-LOA-20260731-abc123",
"entity": { "legal_name": "Acme Ltd", … },
"severity_summary": [ { "severity": "High", "identified": 2,
"remediated_or_accepted": 2, "open": 0 } ],
"material_findings": { "state": "all_remediated", "text": "…" },
"signature": { "signatory": null, "human_review": false, … } }
Declare your AI system description for the report's AI attack-surface section — model, model_version, fine_tuned, rag, tools, autonomy, trust_boundaries. Anything you don't declare is shown as not characterized; we never infer your architecture.
Replay a behavioral finding's PoC N times (≤50) against a verified-owned target to record its Attack Success Rate. Body: { finding_key, url, method, body, n, marker }. Marker/refusal judging — no model cost. In the report JSON, asr is null until a replay runs and cvss.score is null for behavioural findings — treat a missing value as "not measured", never as zero or a pass. POST /api/runs/{id}/human-review starts a $399 certified-review Checkout.
API — SSO & SCIM
Enterprise identity. Owner + Enterprise plan. See SSO, SCIM & roles.
Configure OIDC ({ issuer, client_id, client_secret, domain }). GET /api/sso/status shows config; DELETE /api/sso disables it. Redirect URI: https://scan.millenniums.ai/api/sso/callback, scopes openid email.
Issue the SCIM token (or { "rotate": true }). GET /api/scim/status shows the Base URL + token. The IdP uses the SCIM 2.0 endpoints under /scim/v2/ (Users create/read/update/deactivate) with that token as an OAuth Bearer Token.
API — Team, audit & knowledge
The workspace roster with each member's role. Compliance plan and above; returns 403 with an upgrade hint otherwise. POST invites a teammate by email, POST /api/members/role changes a role, and DELETE /api/members/{email} removes them and revokes their tokens.
| Role | Can |
|---|---|
| Owner | Everything, including billing and the workspace token. |
| Admin | Scans, remediation, knowledge, integrations, members — not billing or the token. |
| Member | Scans, remediation, knowledge. |
| Viewer | Read-only. Sees everything, changes nothing. |
The workspace audit log — who started scans, accepted risk, changed schedules, rotated keys, or changed membership. Enterprise plan.
Per-workspace context the scanner carries into every scan of an asset — how to log in, which endpoints matter, what to leave alone. POST adds an entry; DELETE /api/knowledge/{id} removes one.
Connect a Slack incoming webhook for scan results. GET /api/slack/status reports whether one is set; DELETE disconnects it.
Whether GitHub is connected for this workspace — the prerequisite for white-box scans of private repos, draft fix PRs and repo-based AI discovery.
API — PR reviews
The status of pull-request scans for your registered targets (Developer plan and up). Each entry reports the PR, whether the scoped scan is clean, and any proven net-new findings that blocked the merge. See the CI guide.
API — Chat
Ask questions in natural language. Pass a run_id to ground the answer in a specific scan.
| Body | Type | Required | Notes |
|---|---|---|---|
messages | array | yes | Chat turns, e.g. [{"role":"user","content":"…"}]. |
run_id | string | no | Grounds the reply in that scan's findings. |
{ "reply": "The prompt-injection finding on /chat lets a user…" }
API — Billing
Start a Stripe Checkout session to upgrade. Returns a hosted checkout url to redirect the user to.
| Body | Type | Notes |
|---|---|---|
plan | string | starter · developer · team. Enterprise is sales-led. |
{ "url": "https://checkout.stripe.com/c/pay/cs_live_…" }
Returns 501 if billing isn't configured on the instance, 400 for an unknown plan.
API — Errors & status codes
Errors return a JSON body { "error": "…" } with one of these statuses:
| Code | Meaning | Common cause |
|---|---|---|
400 | Bad request | Missing target, invalid email, unknown plan. |
401 | Unauthorized | Missing or unknown bearer token. |
402 | Payment required | Scan quota reached — upgrade or add a card. Body includes plan, quota, used. |
403 | Forbidden | Email not verified, or account suspended. |
404 | Not found | Unknown route, or a run that isn't yours. |
429 | Too many requests | Signup rate limit, or your plan's concurrent-scan cap. |
500 | Server error | Scan couldn't start (Docker / model key), or chat failed. |
501 | Not implemented | Billing not configured on this instance. |