Quantum readiness audit
Post-quantum migration is an inventory problem before it is a cryptography problem. This guide covers running a quantum scan, reading the findings across each crypto surface, interpreting the readiness score, deciding what to migrate first, and stopping new quantum-vulnerable cryptography from entering the codebase.
Why now, and what the deadlines actually say
NIST published the first post-quantum standards — FIPS 203, 204, and 205 — in August 2024. NIST IR 8547 then set the migration timeline: the 112-bit-security classical public-key algorithms in near-universal use today become deprecated around 2030 and disallowed by 2035. NSA's CNSA 2.0 milestones start earlier still, from 2027 for national-security systems.
The reason the 2035 date does not mean "start in 2034" is harvest now, decrypt later: an adversary capturing encrypted traffic today can decrypt it once a cryptographically relevant quantum computer exists. Anything with a confidentiality lifetime longer than the gap is already exposed.
OSPulse ships a curated deadline dataset, and every finding carries the keys of the
milestones it is affected by in affectedDeadlineKeys:
| Key | Date | Source | What it requires |
|---|---|---|---|
cnsa-2.0-2027 | 2027-01-01 | NSA | New national-security-system software and firmware signing should be PQC-capable |
cab-47day-2029 | 2029-03-15 | CA/Browser Forum | Maximum TLS certificate lifetime drops to 47 days, forcing crypto-agility |
nist-deprecate-2030 | 2030-01-01 | NIST | RSA-2048 / P-256-class public-key algorithms become deprecated |
eu-critical-2030 | 2030-12-31 | European Commission | Critical-infrastructure systems targeted for PQC protection |
nist-disallow-2035 | 2035-01-01 | NIST | The deprecated classical public-key algorithms are disallowed outright |
The dataset is reference data, not code — a shifting regulatory calendar re-maps existing findings without a platform release.
The five surfaces
A dependency scan alone misses most of the estate. OSPulse assesses five surfaces, and a finding names the one it came from:
Dependency— a crypto-relevant package in the resolved dependency graph. The default.Certificate— an X.509 certificate committed in a repository or uploaded to the estate.TlsEndpoint— a live endpoint observed by a probe or by the CLI.Source— a quantum-vulnerable primitive used directly in code.Config— crypto declared in manifests, IaC, or application configuration.
Each finding is classified Vulnerable, Hybrid, QuantumResistant, or Unknown.
Unknown means no catalogue match — it degrades confidence rather than asserting safety, and
is never silently dropped.
Prerequisites
A quantum scan reads the repository's resolved dependency graph, so the repository must have
completed at least one core scan first. Triggering before that returns 409 with a
QuantumNoDependencyGraph problem type.
Run a scan
Trigger the scan
A bodyless POST runs the default dependency scan. Supply a surfaces array to target a
different surface.
curl -X POST https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/scans \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"id": "f81b3c07-6d92-4a5e-b0f4-71c8a2e5d039",
"repositoryId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"status": "Pending",
"trigger": "OnDemand",
"startedAtUtc": null,
"catalogVersion": "2026.07.2",
"surfacesCovered": ["Dependency"]
}catalogVersion is stamped on the scan so a result is reproducible against the exact
reference data that produced it.
Poll to completion
Scans move Pending → Running → Completed | Failed. A completed scan is immutable — a
rescan is a new scan, so trend history is preserved.
curl https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/scans/$SCAN_ID \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"id": "f81b3c07-6d92-4a5e-b0f4-71c8a2e5d039",
"repositoryId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"status": "Completed",
"trigger": "OnDemand",
"startedAtUtc": "2026-07-28T10:02:11Z",
"completedAtUtc": "2026-07-28T10:02:47Z",
"catalogVersion": "2026.07.2",
"summaryByClassification": {
"Vulnerable": 14,
"Hybrid": 2,
"QuantumResistant": 5,
"Unknown": 9
},
"scoreId": "7d2e9f13-04ab-4c68-9e57-b3a1c60d8f42",
"failureReason": null,
"surfacesCovered": ["Dependency"]
}Read the findings
The findings endpoint is paged and filterable. Omit scanId and it defaults to the
repository's latest completed scan — which is what you usually want.
curl "https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/findings?classification=Vulnerable&status=Open&take=25" \
-H "Authorization: Bearer $OSPULSE_TOKEN"scanIduuidOptionalScan to read. Defaults to the latest completed scan.
classificationstringOptionalVulnerable, Hybrid, QuantumResistant, or Unknown.
severitystringOptionalVulnerability severity of the underlying issue.
statusstringOptionalOpen, AcceptedRisk, or Resolved.
surfacestringOptionalDependency, Certificate, TlsEndpoint, Source, or Config.
takeintegerOptionalPage size. Defaults to 50, capped at 200.
skipintegerOptionalOffset into the result set.
The response uses the standard page envelope — items, totalEstimate, take, skip:
{
"items": [
{
"id": "2a76c018-4e39-4bd1-90c7-58f3e1a0b6d2",
"surface": "Dependency",
"ecosystem": "Npm",
"packageName": "node-forge",
"packageVersion": "1.3.1",
"isDirect": false,
"dependencyPath": ["app", "some-sdk", "node-forge"],
"classification": "Vulnerable",
"isWeakHashInformational": false,
"severity": "None",
"affectedDeadlineKeys": ["nist-deprecate-2030", "nist-disallow-2035"],
"remediationDirection": "Migrate RSA/ECDSA usage to an ML-KEM / ML-DSA capable library.",
"status": "Open",
"catalogEntryId": "b1f0d7a3-2c46-4e58-9013-7ac5b2e94d61",
"algorithmCatalogEntryId": null,
"certificateAssetId": null,
"sourceFilePath": null,
"sourceLine": null,
"configKey": null,
"fingerprint": "dep:npm:node-forge:1.3.1:rsa"
}
],
"totalEstimate": 14,
"take": 25,
"skip": 0
}fingerprint is the stable identity used for baseline diffing in CI — it is what makes
"new since the last scan" a meaningful question.
Triage
Move a finding through Open ⇄ AcceptedRisk and on to Resolved. Status changes are
audited; an invalid transition is a 400.
curl -X PATCH https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/findings/$FINDING_ID \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "AcceptedRisk" }'AcceptedRisk still counts against the readiness score. That is intentional — accepting a
risk should be visible, not invisible.
Certificates
Legacy RSA and ECC certificates are usually the most concrete finding in an estate, because their expiry dates are already known. List what OSPulse has inventoried:
curl "https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/certificates?classification=Vulnerable" \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"items": [
{
"id": "6f13d8b0-92a4-41c7-85e0-3b7c2d5941ae",
"repositoryId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"fingerprintSha256": "4c7f19b0e2a5…",
"subject": "CN=gateway.internal.example",
"issuer": "CN=Example Internal CA",
"serialNumber": "0A3F91C4",
"notBeforeUtc": "2025-11-02T00:00:00Z",
"notAfterUtc": "2027-11-02T00:00:00Z",
"signatureAlgorithm": "sha256WithRSAEncryption",
"publicKeyAlgorithm": "RSA",
"publicKeyBits": 2048,
"curve": null,
"classification": "Vulnerable",
"algorithmCatalogEntryId": "9e2b…",
"provenance": [],
"createdAtUtc": "2026-07-28T10:02:44Z",
"updatedAtUtc": "2026-07-28T10:02:44Z"
}
]
}Certificates OSPulse cannot see in a repository can be pushed in with
POST /api/v1/repositories/{repositoryId}/quantum/certificates:upload.
For live endpoints, register them with POST /api/v1/quantum/probe-endpoints
({ name, scheme, host, port, scope } where scope is Public or Internal), prove
ownership with POST /api/v1/quantum/probe-endpoints/{endpointId}:verify, then probe with
POST /api/v1/repositories/{repositoryId}/quantum/probe-endpoints/{endpointId}:probe.
Internal endpoints are never probed from OSPulse — observe them with the CLI and push the
result to POST /api/v1/quantum/tls-observations:ingest. Read observations back from
GET /api/v1/quantum/tls-observations, which supports host, origin, state, take,
and skip.
Score and reporting
The readiness score is 0–100 and every point is attributed. breakdown is the per-finding
and per-classification attribution, so a score is reproducible from the finding set rather
than being an opaque number.
curl https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/score \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"value": 62,
"breakdown": { "…": "per-finding point attribution" },
"computedAtUtc": "2026-07-28T10:02:47Z"
}GET .../quantum/score/history returns the trend, and
GET /api/v1/quantum/summary rolls the whole tenant up:
{
"tenantScore": 58,
"repositories": [
{
"repositoryId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
"name": "atlas-gateway",
"score": 62,
"worstClassification": "Vulnerable"
}
]
}Two exportable artefacts:
- A CBOM —
GET /api/v1/repositories/{repositoryId}/quantum/scans/{scanId}/cbomrenders the scan as a CycloneDX 1.6 cryptographic bill of materials, using thecryptographic-assetcomponent type. - A report —
POST /api/v1/repositories/{repositoryId}/quantum/reportswith{ "format": "pdf" }or{ "format": "markdown" }returns202and areportId; fetch it fromGET .../reports/{reportId}/download. A repository with no completed scan returns409.
Prioritising the migration
Findings alone will not tell you where to start. Order the work like this:
- Signing and long-lived roots first. A signing key that validates firmware or updates
is the hardest thing to rotate later and the most damaging to get wrong.
cnsa-2.0-2027is the earliest deadline for a reason. - Long-confidentiality data next. Anything whose secrecy must outlive 2035 is already in harvest-now-decrypt-later range today. Prioritise by data lifetime, not by CVSS.
- Direct dependencies before transitive ones.
isDirect: truefindings are yours to fix; a transitive one usually needs the intermediate maintainer to move first. UsedependencyPathto see who that is. - Certificates by
notAfterUtc. Every renewal is a free migration window. Certificates expiring before your target date cost nothing extra to move. Unknownlast, but not never. Unknown is not safe — it is unmeasured. Work it down so your confidence in the score improves.
Where migration genuinely cannot happen yet, record a scoped, expiring exception rather than letting the finding rot:
curl -X POST https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/exceptions \
-H "Authorization: Bearer $OSPULSE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scopePattern": "legacy/**",
"untilUtc": "2027-03-31T00:00:00Z",
"reason": "Vendor SDK pins RSA; replacement scheduled for Q1 2027."
}'An exception past untilUtc stops suppressing automatically. Revoke early with
DELETE /api/v1/repositories/{repositoryId}/quantum/exceptions/{exceptionId}.
Gate new quantum-vulnerable crypto in CI
The most valuable gate is not "fix everything" — it is "do not add more". The CLI evaluates the quantum gate against the server's stored findings and exits non-zero on a violation.
ospulse quantum scan --repo $REPO_ID --fail-on new-vulnerable--fail-on | Behaviour |
|---|---|
none | Never fails the build. The default — use it to establish a baseline first |
new-vulnerable | Fails only on findings new against the repository's baseline scan, diffed by fingerprint |
any-vulnerable | Fails on any current vulnerable finding, baselined or not |
Exit codes follow the usual convention: 0 pass, 1 gate violation, 2 an error running
the scan. --format sarif emits results your platform can annotate a pull request with, and
--format cbom writes the CycloneDX 1.6 document instead.
- name: Quantum readiness gate
run: ospulse quantum scan --repo ${{ vars.OSPULSE_REPO_ID }} --fail-on new-vulnerable --format sarif --output quantum.sarif
env:
OSPULSE_TOKEN: ${{ secrets.OSPULSE_TOKEN }}Start on new-vulnerable. Moving straight to any-vulnerable on an established codebase
blocks every build on day one, and the usual response to that is to switch the gate off.
Ratchet up once the baseline is falling.
