Documentation

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.

New here? Start with the browser quickstart — you'll have a real, provable finding in a few minutes, no code required.

Quickstart — in the browser

The fastest way to see a result. No install, no card.

  1. 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.
  2. Verify your email. Click the link we send you. This unlocks scanning. (Didn't arrive? Use "Resend" in the app.)
  3. 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.
  4. Watch it work. The scan runs in an isolated sandbox and typically finishes in minutes. You'll see it probe each category live.
  5. 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.
Only scan what you're allowed to. The tool actively tries to exploit whatever you point it at. Use it against your own apps, or ones you have explicit permission to test. See Terms.

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:

  1. Point. You register a target: a staging URL, a repository, or an API endpoint.
  2. Attack. Autonomous agents spin up in an isolated Docker sandbox and probe the AI attack surface — no scripts for you to write.
  3. 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.
  4. Fix. Each finding comes with a plain remediation and, when you want it, a draft pull request your engineers review.
  5. 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)
WhatYour live, running app — a URL or API endpoint that answers requests right now.Your code — a git repo or an uploaded .zip.
WhyIt's what the scanner actually attacks.Lets the scanner read the code as it attacks (white-box) — deeper findings.
Examplehttps://staging.yourapp.com/chatgithub.com/acme/app or my-app.zip
Code is not a running app. Uploading a zip or linking a repo does not let us find or start your app — we can't run your code for you. We attack your app where it's already running. So you always give a target URL; the source is an extra on top of it, never a replacement.

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.com or app-staging.yourapp.com
  • A preview URL from Vercel or Netlify (they mint one per change)
  • A *-staging.onrender.com or *.herokuapp.com app
  • 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:

  1. 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.
  2. 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.

Still have a developer or agency? The fastest path is to ask them "what's our staging URL?" — but if that's not an option, the two steps above get you there on your own.

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 .zip of your project. In the app, drop it into the scan box; over the API, upload it and pass the returned upload_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 source field, 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 .zip upload.
Recommendation: for AI apps, always provide source when you can. A URL alone frequently misses injection and tool-abuse paths that are obvious once the code is in view. Not the person who holds the code? Have your developer do the upload.

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:

FieldWhat it is
titleOne-line summary of the vulnerability.
severitycritical high medium low — impact-ranked.
cwe / cvssStandard weakness ID and CVSS score, when applicable.
impactWhat an attacker gains in plain terms.
technical_analysisWhy it works, for an engineer.
remediationThe concrete fix.
pocA working, re-runnable proof of concept. Paid plans only.
endpoint / method / locationsWhere it lives — the request and the code locations.
Proof of concept lock. On the free tier, findings are fully visible but the working exploit (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.

StageThe questionWhat it removes
1. InventoryWhat 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. ExploitabilityIs anyone exploiting this? CISA KEV, or EPSS ≥ 10%.The large majority — high-severity CVEs with no observed or predicted exploitation.
3. ReachabilityDoes our code call the vulnerable path?Vulnerable libraries you use, but not the vulnerable part of. Checked automatically when a repo is linked.
4. DeterminationHave 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.

Nothing is deleted, only ranked. Suppressed and inventory CVEs stay queryable — the filtering decides what competes for your attention, not what you're allowed to see.

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.

LabelWhat it is & how to read itWhy it matters
Severitycritical 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 10The 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 ATLASThe 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.
ReachabilityWhether 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.
Two kinds of finding. A dependency finding (a 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

CodeRiskIn plain terms
LLM01Prompt InjectionAttacker-supplied text overrides the model's instructions or hijacks its tools.
LLM02Sensitive Information DisclosureThe app leaks secrets, personal data, or another user's data.
LLM03Supply ChainVulnerable or untrusted dependencies, models, or plugins.
LLM04Data & Model PoisoningAttacker-controlled data corrupts training, fine-tuning, or the RAG corpus.
LLM05Improper Output HandlingThe app trusts model output unsafely — passing it into SQL, a shell, or HTML.
LLM06Excessive AgencyThe agent has too much power or access; a manipulated model can act on it.
LLM07System Prompt LeakageThe hidden system prompt — and any secrets or logic in it — can be extracted.
LLM08Vector & Embedding WeaknessesRAG/vector-store flaws: cross-tenant retrieval, corpus poisoning, embedding leakage.
LLM09MisinformationFalse or fabricated output that causes real harm — unsafe reliance, hallucinated code.
LLM10Unbounded ConsumptionNo 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, default 10). 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.

Exempt: local and private targets — 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.

Why it matters: when a customer or regulator asks "what did you do with our code?", you hand them this — a per-scan record showing the code stayed sandboxed, the sandbox was destroyed, and nothing was retained or used for training.

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.

Retainer-friendly. Point a client's assets at a cadence with drift on, and you have hands-off monitoring that only spends a scan when something actually changes.

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.

SurfaceHow it's connectedFinds
Reposa GitHub org / accountAI-powered repositories, by their LLM-SDK usage.
Clouda read-only role / inventoryManaged AI services — Bedrock agents, knowledge bases, provisioned models, SageMaker endpoints, Lex bots, Comprehend.
Externalyour domainsExposed 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.

Not the same as cloud penetration testing. Discovery answers "what AI do we have, and where?" — an inventory. Infrastructure & Cloud (CSPM) answers "is our cloud misconfigured or exposed?" — it finds and scores vulnerabilities. Different jobs: one builds the map, the other tests what's on it. Discovery often feeds the others (find an AI service → then test it).
Honest coverage. Every run reports exactly what it scanned (which repos, which cloud services, which hosts and ports). It is never a claim that no other AI exists — only what we looked at.

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

Not the same as Shadow-AI. Shadow-AI discovery finds where AI is used in your org (inventory). AI supply chain asks whether the AI components you depend on are trustworthy and un-compromised (a vulnerability class tested during a scan). Discovery can feed it — find an AI service, then check its supply chain.

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.

CheckSeverityCVSS
S3 bucket public accessCritical9.8
IAM wildcard policy (Action:* on Resource:*)Critical9.1
Security group open to all portsCritical9.8
SSH / RDP open to 0.0.0.0/0High8.1
RDS publicly accessibleHigh7.5
RDS / S3 unencrypted at restMedium5.3
CloudTrail logging disabledMedium—
Lambda function URL with no authenticationCritical9.1
EKS API endpoint open to the internetCritical9.0
Cross-cloud federated trust with no subject conditionCritical9.6
Secret with automatic rotation disabledLow3.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.

Honest coverage. Findings are read-only configuration facts. Coverage is exactly which services you enumerated — never a claim that nothing else is misconfigured.

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.

The number that matters. On a real AWS account this produced 17 findings but only 1 risk — because a public bucket is a finding, and a public bucket known to hold personal data is a risk. Ranking is by severity, then proven before potential, then how direct the path is.

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.

Discoverable from one cloud alone. An AWS role trusting 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.

Accuracy over volume. Card numbers are Luhn-checked, so a 16-digit order id is not a PCI finding. Social security numbers are checked against structurally-issued ranges. A single email address in a log file does not make a store "personal data". One value never classifies anything.

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.

Why this is not a linter. A private, encrypted bucket has zero findings on its own, and your account may have zero risks. Merge it into the live graph and it can still be critical — because an existing internet-reachable role can already read whatever this pull request creates. That context lives in the graph, not the template, which is why a standalone IaC scanner cannot produce it.

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.

We never hold write access to your cloud. Our access stays read-only, a compromise of us cannot mutate your environment, and every change appears in your audit trail under your role. The reference function is dry-run by default, allow-lists the resources it may touch, and has no delete permissions at all — it can only ever restrict access, never destroy data.

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.

Configuration tells you a workload is reachable. Runtime tells you it is compromised. Neither is the finding — the join is. A runtime detection on an exposed workload completes a path that configuration alone can only show as theoretical.

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.

Honest scope. P1–P3 are confirm-only / credentialed detection — they observe and confirm, never exploit. P4 exploitation is off until certified. Nothing runs without proven ownership and a signed RoE.

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.json for 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.

Certified human review. On the Compliance and Enterprise plans a certified (CREST/OSCP) reviewer validates the findings and the report names them; on other plans it's a $399 per-scan add-on. A reviewer is named only when a human actually reviewed.

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 .zip upload, for a deeper white-box scan.

Steps

  1. Open the app and paste your target into the scan box, or call POST /api/scan.
  2. If you have the source, add it — a repo URL or local path in the source field. This is the single biggest lever on finding quality.
  3. Start the scan. Note the run_id.
  4. Wait for it to finish (status running → false). Minutes, typically.
  5. 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 source and 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

FieldWhat it does
Name optionalA label so you can recognise the target in the list. Defaults to the target itself.
TargetThe 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 optionalA 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 optionalSteer 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.
ScopeFull 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 onlyWhat 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

  1. 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.
  2. Your plan and spend cap are applied, so a scan can never run past your budget (see Spend caps & safety).
  3. The test runs in a throwaway, isolated sandbox — your code is never reused or kept (see Your data).
  4. When it finishes, the results appear under Findings (see Read your scan results).
Prefer the API? You can register and manage targets programmatically — see Targets in the API reference.

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

TabWhat's in it
FindingsEvery vulnerability the scan proved, as cards you can expand. Where you'll spend most of your time.
Dependency CVEsKnown 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 ReportThe formal written report (produced only when a scan runs to completion). Share it, download a branded PDF, or the raw .md.
Data TrailThe 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:

  1. 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.
  2. ⚡ 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

  1. Start with Critical and High — those are what an attacker reaches soonest.
  2. 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.
  3. Read "Remediation" — the concrete fix — and the code locations.
  4. 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:

  1. Open the completed scan and click ⚙ Generate fix PR in the top row.
  2. It locates the code behind each finding, writes a minimal fix, and opens a draft pull request on your repo.
  3. Click ↗ View fix PR to review it on GitHub.
  4. 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.
In plain terms: the tool drafts the repair and hands it to your engineer to check off. It never merges by itself.

2. Fix vulnerable libraries — ⬆ Update dependencies

For dependency findings — known CVEs in the third-party libraries your app relies on:

  1. Click ⬆ Update dependencies in the top row.
  2. It bumps each vulnerable package to its patched version in your editable manifests (requirements.txt, package.json) and opens a draft PR.
  3. 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).
  4. Review the changes, run any listed lockfile commands, and merge.
Which button? Generate fix PR repairs your code; Update dependencies patches the libraries you depend on. A thorough clean-up usually uses both. Every result is a draft — reviewed by a human before anything merges.

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.

  1. 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.
  2. Store it as a repo secret named MILLENNIUMS_SCAN_KEY.
  3. 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.

Webhook drift too. Because a push runs the Action, a repo change re-scans automatically — the same trigger key powers both CI gating and drift.

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.

PlanPriceScansWorking PoCConcurrentBuilt for
Free$0A scan to start—1Trying it out
Starter$249 / mo5 / mo✓2Solo builders
Developer$999 / mo15 / mo✓ + CI + fix & dependency PRs3Growing teams
EnterpriseCustomUnlimited (fair use)✓ + SSO/SCIM + on-prem / BYO-key8Custom, 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.

POST/api/signin

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.

BodyTypeNotes
emailstringRequired. The account email.
POST/api/login

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.

BodyTypeNotes
tokenstringRequired. The token from the #login= fragment of the emailed link.
{ "token": "NEW_ACCESS_TOKEN", "tenant": "acme-1a2b3c", "plan": "free", "verified": true }
POST/api/token/rotate

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/json on POSTs.
  • Success: 200 OK with a JSON body.
  • Errors: a non-2xx status with { "error": "message" }. See status codes.

API — Account

GET/api/me

Your account, plan, and quota. The quickest way to confirm a token works.

FieldTypeMeaning
tenantstringYour account ID.
planstringfree · starter · developer · team · enterprise.
pocboolWhether working PoCs are unlocked on your plan.
scans_used / scans_limitintUsage against your allowance.
verifiedboolEmail verified — required to scan.
POST/api/signup

Public. Create an account and get a token. A verification email is sent automatically.

BodyTypeRequiredNotes
emailstringyesWork email preferred. Disposable domains are rejected.
companystringnoUsed 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 }
POST/api/resend

Re-send the verification email to your account's address. Requires your token.

API — Scans

POST/api/discover

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.

BodyTypeNotes
urlstringRequired. 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.

POST/api/upload

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" }
Non-technical owner? You don't need to touch git or the API. In the app, just drag your project .zip into the scan box — or send it to whoever manages your code and have them do it.
POST/api/scan

Start a scan against a target. Gated on email verification, your remaining quota, and your plan's concurrency limit.

BodyTypeDefaultNotes
targetstring—Required. A URL, repository, or API endpoint to attack.
upload_idstringnoneOptional. The id from POST /api/upload — a white-box scan of your uploaded code. Deleted after the scan.
sourcestringnoneOptional. A git URL (https:// or git@) to clone for white-box. Use this or upload_id.
budgetnumber10Per-scan spend cap, US dollars.
modestringstandardScan 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" }
Returns before the scan finishes. You get a run_id immediately; poll GET /api/runs/<id> for progress and results.
A 200 does not always contain a 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:

SituationWhat happens
Same commit, previous scan completedNo scan, no charge. You get no_changes plus the baseline run and its finding count.
Commit changedOnly 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.
Why it's worth knowing: on a white-box scan, reading the source is the dominant cost. Scanning per-commit in CI is affordable precisely because unchanged commits cost nothing and changed ones only pay for the diff.

Controlling a run

POST/api/runs/{run_id}/stop

Stop a running scan. Returns 409 if it isn't running. You keep whatever it proved before you stopped it.

POST/api/runs/{run_id}/resume

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.

GET/api/runs/{run_id}/report

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.

GET/api/runs/{run_id}/coverage

What the scan actually did — stages reached, agents run, token usage. Use it to see where a scan spent its budget.

POST/api/runs/{run_id}/autofix

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.

POST/api/runs/{run_id}/dep-update

Generate a draft PR bumping vulnerable dependencies (critical/high CVEs) to patched versions. GET for job status.

POST/api/runs/{run_id}/reachability

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.

GET/api/runs/{run_id}/vex

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.

POST/api/runs/{run_id}/share

Share a scan's summary to Slack (channel: "slack") or by email.

POST/api/runs/{run_id}/human-review

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.

GET/api/runs

List your scans, most recent first — each with id, status, running, and findings_count.

GET/api/runs/{run_id}

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.

GET/api/runs/{run_id}/trace

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 } }
FieldTypeMeaning
idstringThe run ID.
statusstringrunning, completed, stopped: token cap, …
runningbooltrue until the scan finishes.
findings_countintHow many proven findings.
findingsarrayThe 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.

GET/api/targets

List your registered targets.

POST/api/targets
BodyTypeDefaultNotes
targetstring—Required. The URL / repo / API.
namestring= targetA friendly label.
sourcestringnoneSource for white-box scans.
instructionstringnonePlain-English focus for the scan — areas to prioritise, test credentials, or specific endpoints to probe first.
scope_modestringfullOne 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_basestringdefault branchWith 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.

POST/api/domains/verify

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.

BodyTypeNotes
domainstringRequired. The domain (or a URL — we use its root).
attestboolCertify 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=…" }
GET/api/domains

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.

GET/api/targets

List your registered assets. POST /api/targets registers one ({ "target", "name" }); DELETE /api/targets/{id} removes it.

POST/api/schedules

Create or update a schedule.

BodyTypeNotes
asset_idstringRequired. A registered asset.
cadencestringdaily · weekly · monthly.
trigger_on_changeboolEnable drift-triggered re-scans between runs.

GET /api/schedules lists them; DELETE /api/schedules/{id} removes one.

GET/api/overview

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.

GET/api/assets/{asset_id}/trend

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 }, … ] }
POST/api/targets/{target_id}/scan

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.

DELETE/api/targets/{target_id}

Remove a registered target. Past scans and their reports are unaffected. DELETE /api/schedules/{schedule_id} removes a schedule the same way.

POST/api/assets/{asset_id}/key/rotate

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.

GET/api/assets/{id}/key

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.

POST/api/assets/{id}/trigger

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.

GET/api/assets/{id}/runs/{run_id}

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.

POST/api/discover/org

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.

GET/api/shadow

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.

POST/api/discover/repo

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.

POST/api/shadow/confirm

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.

POST/api/cloud/connect

Connect a read-only AWS role: { "provider": "aws", "role_arn", "external_id", "region" }. Owner + paid plan. DELETE /api/cloud disconnects.

POST/api/cloud/scan

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.

GET/api/cloud/status

Connection state + latest scan summary. GET /api/cloud/posture returns the latest findings.

POST/api/cloud/ingest

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.

GET/api/runs/{id}/compliance

The full compliance report (HTML). /api/runs/{id}/compliance.json returns the JSON twin for Vanta/Drata/Secureframe.

GET/api/runs/{id}/attestation

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, … } }
POST/api/runs/{id}/ai-surface

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.

POST/api/runs/{id}/measure-asr

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.

POST/api/sso

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.

POST/api/scim

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

GET/api/members

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.

RoleCan
OwnerEverything, including billing and the workspace token.
AdminScans, remediation, knowledge, integrations, members — not billing or the token.
MemberScans, remediation, knowledge.
ViewerRead-only. Sees everything, changes nothing.
GET/api/audit

The workspace audit log — who started scans, accepted risk, changed schedules, rotated keys, or changed membership. Enterprise plan.

GET/api/knowledge

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.

Biggest single lever on scan quality. If your app needs a login to reach the interesting surface, say so — an unauthenticated scan of an authenticated app tests the front door and nothing behind it. Scans also learn: what one scan discovers about an asset is retained for the next, so repeat scans start where the last finished.
POST/api/slack

Connect a Slack incoming webhook for scan results. GET /api/slack/status reports whether one is set; DELETE disconnects it.

GET/api/github/status

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

GET/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

POST/api/chat

Ask questions in natural language. Pass a run_id to ground the answer in a specific scan.

BodyTypeRequiredNotes
messagesarrayyesChat turns, e.g. [{"role":"user","content":"…"}].
run_idstringnoGrounds the reply in that scan's findings.
{ "reply": "The prompt-injection finding on /chat lets a user…" }

API — Billing

POST/api/billing/checkout

Start a Stripe Checkout session to upgrade. Returns a hosted checkout url to redirect the user to.

BodyTypeNotes
planstringstarter · 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:

CodeMeaningCommon cause
400Bad requestMissing target, invalid email, unknown plan.
401UnauthorizedMissing or unknown bearer token.
402Payment requiredScan quota reached — upgrade or add a card. Body includes plan, quota, used.
403ForbiddenEmail not verified, or account suspended.
404Not foundUnknown route, or a run that isn't yours.
429Too many requestsSignup rate limit, or your plan's concurrent-scan cap.
500Server errorScan couldn't start (Docker / model key), or chat failed.
501Not implementedBilling not configured on this instance.
Need a hand? Email support@millenniums.ai, or open the app and use in-app chat.