OSPulse

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:

KeyDateSourceWhat it requires
cnsa-2.0-20272027-01-01NSANew national-security-system software and firmware signing should be PQC-capable
cab-47day-20292029-03-15CA/Browser ForumMaximum TLS certificate lifetime drops to 47 days, forcing crypto-agility
nist-deprecate-20302030-01-01NISTRSA-2048 / P-256-class public-key algorithms become deprecated
eu-critical-20302030-12-31European CommissionCritical-infrastructure systems targeted for PQC protection
nist-disallow-20352035-01-01NISTThe 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.

bash
curl -X POST https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/scans \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "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.

bash
curl https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/scans/$SCAN_ID \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "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.

bash
curl "https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/findings?classification=Vulnerable&status=Open&take=25" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
scanIduuidOptional

Scan to read. Defaults to the latest completed scan.

classificationstringOptional

Vulnerable, Hybrid, QuantumResistant, or Unknown.

severitystringOptional

Vulnerability severity of the underlying issue.

statusstringOptional

Open, AcceptedRisk, or Resolved.

surfacestringOptional

Dependency, Certificate, TlsEndpoint, Source, or Config.

takeintegerOptional

Page size. Defaults to 50, capped at 200.

skipintegerOptional

Offset into the result set.

The response uses the standard page envelope — items, totalEstimate, take, skip:

json
{
  "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.

bash
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:

bash
curl "https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/certificates?classification=Vulnerable" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "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.

bash
curl https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/score \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "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:

json
{
  "tenantScore": 58,
  "repositories": [
    {
      "repositoryId": "8f2c1d4e-9a3b-4c5d-8e7f-1a2b3c4d5e6f",
      "name": "atlas-gateway",
      "score": 62,
      "worstClassification": "Vulnerable"
    }
  ]
}

Two exportable artefacts:

  • A CBOMGET /api/v1/repositories/{repositoryId}/quantum/scans/{scanId}/cbom renders the scan as a CycloneDX 1.6 cryptographic bill of materials, using the cryptographic-asset component type.
  • A reportPOST /api/v1/repositories/{repositoryId}/quantum/reports with { "format": "pdf" } or { "format": "markdown" } returns 202 and a reportId; fetch it from GET .../reports/{reportId}/download. A repository with no completed scan returns 409.

Prioritising the migration

Findings alone will not tell you where to start. Order the work like this:

  1. 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-2027 is the earliest deadline for a reason.
  2. 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.
  3. Direct dependencies before transitive ones. isDirect: true findings are yours to fix; a transitive one usually needs the intermediate maintainer to move first. Use dependencyPath to see who that is.
  4. Certificates by notAfterUtc. Every renewal is a free migration window. Certificates expiring before your target date cost nothing extra to move.
  5. Unknown last, 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:

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

bash
ospulse quantum scan --repo $REPO_ID --fail-on new-vulnerable
--fail-onBehaviour
noneNever fails the build. The default — use it to establish a baseline first
new-vulnerableFails only on findings new against the repository's baseline scan, diffed by fingerprint
any-vulnerableFails 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.

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

Next steps