Docs

Documentation

From first probe to compliance report in four steps.

Getting Started

Step 1: Probe a domain

pqprobe scan yourcompany.com --verbose
Target: yourcompany.com:443
Timestamp: 2026-09-30 01:26:52.735265366 +0200 CEST m=+0.000960100
Protocols: TLS 1.3, TLS 1.2
Resolver: system
Addresses: 1 dialed, worst 203.0.113.10:443
  203.0.113.10:443 v4 full TLS 1.3,TLS 1.2 x25519 D(57)
Complete: yes
Ciphers: TLS_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
Chain (3, as presented):
  0: CN=yourcompany.com | ECDSA 256 | ECDSA-SHA256 | until 2026-11-29
  1: CN=Sectigo Public Server Authentication CA DV E36,O=Sectigo Limited,C=GB | ECDSA 256 | ECDSA-SHA384 | until 2036-03-21
  2: CN=Sectigo Public Server Authentication Root E46,O=Sectigo Limited,C=GB | ECDSA 384 | ECDSA-SHA384 | until 2038-01-18

Security Score: 57/100 (Grade: D)
Score Deductions:
  - Cipher: CBC mode ciphers supported - vulnerable to padding oracle attacks (-16)
  - Key Exchange: RSA key exchange detected - no forward secrecy (-27)
  - Post-Quantum: No PQC key exchange. Classical KEX is exposed to active downgrade risk. Max grade C. (-0)
Risk Priority: EMERGENCY (Risk Score: 43)
Recommendation: CRITICAL: INTERNAL/PII data with security grade D (score: 57). Immediate remediation required. Deploy emergency fixes, escalate to security team, implement temporary mitigations if needed.

PASS   yourcompany.com:443  grade D
1 target: 1 passed, 0 failed policy, 0 not scanned. Exit code 0.

That is real output from a public host, with its name and address replaced. A name resolves to one or more addresses; the first address of each family gets the full probe sequence and the rest a short one, and the grade is the worst full-sequence address. yourcompany.com negotiates TLS 1.3 with x25519 and no post-quantum key exchange, which caps it at 79. It also accepts CBC suites and RSA key exchange, which take it to 57, a D. Without --verbose the ciphers, chain, deductions and recommendation are left out. The CLI scores at MEDIUM criticality unless --sensitivity says otherwise, which scales each deduction by 1.1.

In a pipeline, the same probe gates the job. Exit code 0 means every target passed, 1 means a target failed a --fail-on condition, and 2 means a target could not be scanned, including a target the address policy refuses (loopback, and private ranges unless --allow-private-targets opens them). --sarif writes SARIF 2.1.0 for this run's targets.

pqprobe --db pqprobe.db scan --targets-file targets.txt \
  --fail-on 'grade<B' --fail-on tls10 --fail-on no-pqc --fail-on regression \
  --sarif pqprobe-network.sarif

Conditions: grade<X, tls10, tls11, no-pqc, incomplete (a probe got no answer; see Probe Outcomes), and regression, which compares the grade with the last stored scan of the target under the same scoring version. Keep the SARIF as a job artifact on GitLab, not in the sast report slot: the results locate network endpoints, not files in the repository.

Step 2: Analyze your code

pqprobe-static scan . --output sarif

Analyzes source code for cryptographic usage — hardcoded algorithms, weak key sizes, deprecated ciphers, insecure TLS config. 188 detection patterns across Go, Python, Java, C, C++, JavaScript, TypeScript, Rust, C#, Ruby, PHP, Zig, Swift and Kotlin, plus config files (nginx, Apache, YAML, JSON). Cryptographic libraries are detected by name, 21 of them, and an import is reported as a library component distinct from an algorithm asset.

SARIF output uploads directly to GitHub or GitLab Security tabs. No new dashboard — findings appear alongside your existing code scanning results.

# GitHub Actions
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: pqprobe-results.sarif

# GitLab CI, 19.2 and later
artifacts:
  when: always
  paths: [pqprobe-results.sarif]
  reports:
    sarif: pqprobe-results.sarif

GitLab's sast report slot takes GitLab's own SAST JSON, not SARIF. Before 19.2, convert the SARIF first; docs/ci-integration.md in the source distribution has both paths.

Step 3: Check your dependencies

pqprobe dep-audit .
go.mod — 3 crypto dependencies

  crypto/tls (stdlib)     PQC: ✓ since Go 1.24    Current: Go 1.22 ⚠ upgrade
  golang.org/x/crypto     PQC: ✓ since v0.31.0    Current: v0.28.0 ⚠ upgrade
  github.com/lib/pq       PQC: inherits stdlib     No action needed

Summary: 2 dependencies need version bumps for PQC support

Covers nine ecosystems: go.mod, package.json, requirements.txt, Pipfile and pyproject.toml, Cargo.toml, pom.xml and build.gradle, Gemfile, composer.json, .csproj and packages.config, and Conan (conanfile.txt, conanfile.py). Reports which libraries use cryptography, whether they support PQC, and the minimum version to upgrade to. Results are printed, not stored.

Step 4: Track your trajectory

Probe the same targets over time. pqprobe compares results and assigns a migration trajectory:

  • IMPROVING — you enabled PQC key exchange, upgraded a library, rotated to a stronger cert. The gap between your posture and the deadline is closing.
  • STABLE — nothing changed. The deadline is closer but you haven't moved.
  • DEGRADING — a new service appeared without PQC, something regressed, a cert downgraded. The gap is growing.
pqprobe analyze example.com --days 90

Most organizations right now are STABLE. That's the finding — you haven't started, and the deadline hasn't stopped.

Add --project to extrapolate your trajectory against actual compliance deadlines:

pqprobe analyze example.com --project

For each CNSA 2.0 date (1 January 2027, 31 December 2030, 31 December 2031) and other frameworks with dates, you get: on track, at risk, stalled, or regressing — with projected completion date. Requires 3+ scans.

Export

pqprobe export-cbom output.json          # CycloneDX Cryptographic Bill of Materials
pqprobe export-sarif findings.sarif      # SARIF 2.1.0 for every stored endpoint
pqprobe export inventory.csv             # Full inventory for CMDB/SIEM
pqprobe report example.com               # Detailed report for a single target

Exports read what is stored. scan, probe, the protocol scan-* commands, passive analyze, import mde, and scan-local, audit-windows and dep-audit (at sensitivity 1 or 2) store their results, and import static stores a pqprobe-static record; scan-ssh and scan-fhir print theirs and store nothing. Every export reads each stored row by its kind (TLS, SMTP, SMB, SSH, HL7, DICOM, Defender inventory, filesystem, Windows audit, dependency audit, static analysis): an SMB row is exported as SMB, not as a TLS endpoint.

Grades

pqprobe grades measure post-quantum preparedness, not just classical TLS hygiene. Scoring version 11. A TLS endpoint without post-quantum key exchange caps at 79, grade C, however clean its classical configuration.

GradeScoreWhat it usually means
A90–100Post-quantum key exchange, hybrid or pure, and few or no deductions.
B80–89Post-quantum key exchange with deductions to resolve. Also the ceiling of SMB and filesystem results, which cap at 85.
C70–79No post-quantum key exchange, capped at 79, and deductions of 30 points or fewer.
D50–69Deductions such as CBC suites, RSA key exchange, weak DHE, a short RSA key, or an expired certificate.
F0–49A fatal condition, which scores 0, or deductions of more than 50 points.

Every scan records its deductions: --verbose prints them, and the JSON output and the dashboard show them.

Deductions

Scores start at 100 and each issue subtracts points. A criticality multiplier scales each deduction, rounded down: LOW 1.0, MEDIUM 1.1, HIGH 1.2. The CLI maps --sensitivity 1 and 2 to MEDIUM and HIGH, defaulting to 1; 0 (LOW) is reserved for the scheduler, and a command that stores its result refuses it. The server scores submitted scans at MEDIUM and the scheduler at LOW.

CategoryConditionBase Points
CertificateExpired (a deduction, not fatal: an expired certificate alone scores 50, a D)-50
CertificateSHA-1 signature-30
Key ExchangeRSA key exchange (no forward secrecy)-25
CertificateRSA key < 2048 bits-25
Key ExchangeDHE with a prime < 2048 bits-20
CertificateSelf-signed-20
CipherCBC mode suites accepted-15
ProtocolTLS 1.2 without TLS 1.3-8
CertificateValidity > 398 days-5

There is no deduction for a certificate close to expiry; only an expired one counts.

PQC Cap

After deductions, the score is capped by post-quantum posture:

ResultMax ScoreBest Possible Grade
TLS, no post-quantum key exchange79C
TLS, hybrid post-quantum key exchange100 (no cap)A
TLS, pure post-quantum key exchange100; 95 when seen only in a passive captureA
MDE import, no post-quantum capability79C
SMB (NTLM has no post-quantum option)85B
Filesystem scan, no post-quantum algorithm found85B

A post-quantum certificate signature also lifts the TLS cap, since the scanner marks the result post-quantum either way.

Fatal Conditions

These set the score to 0, grade F, before any deduction or cap:

  • SSL 2.0 or SSL 3.0 accepted
  • TLS 1.0 or TLS 1.1 accepted (deprecated by RFC 8996, PCI DSS 4.0, BSI TR-02102)
  • An EXPORT cipher suite
  • A NULL cipher suite
  • DES, 3DES or DES40, whatever the key exchange, ECDHE included
  • RC4
  • An MD5 certificate signature

Which address is graded

Every address that completed the full probe sequence is scored on its own. The host takes the lowest score; a tie goes to the address with the larger deduction total, which the cap would otherwise hide. When the scoring version changes, the server rescores stored scans after startup, in the background, each under the criticality it was first scored under, and stamps the new version. The API serves throughout; until the rescore finishes, a stored grade is the one its scoring_version gave, and /api/v1/health reports "rescoring": true.

Probe Outcomes

A TLS scan sends one ClientHello per question: one protocol version, one cipher suite, one key-exchange group at a time. Each probe ends in one of three ways.

OutcomeWhat it means
AcceptedThe server completed the handshake, or selected the offered group through a HelloRetryRequest.
RefusedA negative answer: an alert, a close repeated after a canary hello showed the server up (rejected-by-close), or silence to a lone-group offer after the canary was answered (rejected-by-silence).
UnknownNo answer after one retry, or a group probe the server ignored outright (silent-drop). An unknown is never counted as a refusal.

A scan is complete when every probe on every connected address got an answer; otherwise it lists the checks that did not, as version:TLS 1.0, group:x448, suite:…, dhe or classic. Text output shows Complete: yes or Complete: no, 2 checks unknown: group:x448, version:TLS 1.0, and marks the score [incomplete: 2 checks unknown]; JSON carries complete and unknown. The grade stands on what was answered: a deduction whose evidence never arrived does not fire, and the result is marked incomplete rather than guessed. --fail-on incomplete fails a target that left any check unknown.

Only the full sequence probes TLS 1.1 and older and every suite; a short-sequence address is probed for TLS 1.3, TLS 1.2 and X25519MLKEM768 only, so it is evidence of what it accepted, never of what it refused.

Suites are probed one at a time at TLS 1.2. When a version probe finds TLS 1.1, TLS 1.0 or SSL 3.0 accepted, the suite the server chose for that probe is listed at that version. Each suite in the result lists the versions the server negotiated it at, newest first, in versions in JSON and in the TLS column of the result card: a suite the server takes on TLS 1.2 and TLS 1.1 reads 1.2, 1.1.

RSA key-exchange fallback. When the default handshake meets a handshake-failure alert, a TLS 1.2 hello offering only RSA key exchange is tried. If the server answers it, the address is probed as usual and its record carries "fallback": "rsa-key-exchange" in JSON.

Legacy-version fallback. When the default handshake is refused on its version, a protocol-version alert or a server settling on a version below TLS 1.2, a hello offering TLS 1.1, TLS 1.0 and SSL 3.0 with every suite those versions carry is tried; it is also tried when the RSA key-exchange hello was refused. If the server answers it, the address is probed as usual and its record carries "fallback": "legacy-version".

Network check: post-quantum handshake sizes

pqprobe netcheck runs from inside a network against hosts, a file of hosts or CIDR ranges. Per host it reports whether the endpoint offers post-quantum key exchange, the result of the TLS scan's own default handshake and X25519MLKEM768 probe, and a handshake size test. Nothing is graded or stored, and a TLS scan sends none of the test's hellos.

The size test sends a small control ClientHello, then one ClientHello at each client-flight size of the calculator, every key exchange except Classic McEliece: 550, 583, 1,318, 1,702, 1,734, 1,767 and 2,086 B at the default fixed part. Each carries that key exchange's key share and one for X25519, padded to the size, so a server without the group still answers and the result is about the size. A ClientHello longer than the connection's segment size, as the kernel reports it, is written as two segments, the first exactly that size, with a one-second pause between them, the reproduction tldr.fail uses; --force-split cuts at 1,460 B.

ResultWhat was observed
answeredThe server answered the ClientHello of this size.
failedThe control completed and this size got no answer, was reset or was closed on; sent again ahead of a second control, it went unanswered again while the control completed.
unknownAnything else: no completed control, a failure that did not come again, an alert. Unknown counts neither for nor against the host.

The summary's first line states the counts: hosts checked, hosts that do not offer post-quantum key exchange, hosts where a post-quantum-sized handshake fails. The failing hosts come next, grouped by shared failure, with the largest size answered. One line per host says whether the server asks for the hybrid group when it is offered without a key share, or completes on classical. In a range, an address with nothing listening is skipped and counted. --format json and csv write one row per host, and --label puts a label on every row so runs from several sites can be concatenated. The report names hosts and the path from where the command ran; a device is described only as what the failing hosts have in common.

The Test button. The calculator lists the same sizes, one row per key exchange. Enter a host and press Test: our server runs the size test against that host on port 443 and marks each row. The page makes no network call until the button is pressed. The test runs under the public probe's limits: rate limit, pause between hellos, opt-out list and address policy. The calculator's server-flight rows are not testable against someone else's server, and hosts on an internal network need pqprobe netcheck.

Sources

Refused addresses. Loopback, 0.0.0.0/8, ::/128, link-local, multicast, 240.0.0.0/4, 100::/64 and the scanning host's own addresses are never dialed. The private and other special-purpose ranges are refused too unless --allow-private-targets opens them; pqprobe netcheck checks a private range or host named to it without the flag. Refused addresses are listed in reserved_addresses; a name that resolves only to refused addresses fails with reserved address space, exit code 2 in the CLI and a 400 when submitted to the server.

CLI Reference

Code Analysis

Analyze source code for cryptographic usage with pqprobe-static.

pqprobe-static scan . Scan current directory for crypto usage
pqprobe-static scan . --output json JSON output for automation
pqprobe-static scan . --output sarif SARIF output for GitHub/GitLab Code Scanning
pqprobe-static scan . --output cbom CycloneDX 1.6 Cryptographic Bill of Materials
pqprobe-static scan . --output record > static.record.json A record of the findings, classified here, to store and upload
pqprobe import static static.record.json --label payments-api Store the record as a static analysis; pqprobe push uploads it

A record is the findings classified on the machine that read the code: each finding's asset kind, name, primitive, parameters and confidence, grouped by rule, language, severity, primitive and file with line numbers and counts, file paths relative to the scanned directory, and the analyzer's rules version. It holds no code, no messages, no absolute paths, no certificate subjects, issuers or serial numbers and no configuration values; a dependency keeps its version only when it is a version, not a git, file or URL reference. The record carries the time the run finished, in UTC. import static stores it at sensitivity 1 or 2 as an ungraded inventory under --label or code- and a hash of the host's name and the record's directory, filed at that time, with the time of the import kept beside it; a scan time more than five minutes ahead is refused. A server judges an uploaded analysis again: dependencies under its catalogue, each asset's quantum status from its primitive, and the totals. export counts its findings by rule and severity, export-cbom lists its assets at each file and line, export-sarif reports them under the analyzer's rules, and each rule, file and primitive is tracked as a finding identity.

Output formats: SARIF, JSON, text, CycloneDX 1.6 CBOM. The CBOM is emitted natively by the binary, so it works in CI pipelines and benchmark harnesses with no server. Identical sources produce byte-identical output apart from the serial number and the timestamp, with stable bom-refs; the serial number is a new random UUID on every run, as CycloneDX asks. Findings with no CycloneDX representation are counted in metadata.properties and reported on stderr rather than dropped.

188 detection patterns across Go, Python, Java, C, C++, JavaScript, TypeScript, Rust, C#, Ruby, PHP, Zig, Swift and Kotlin. Cryptographic libraries are detected by name, 21 of them, and an import is reported as a library component distinct from an algorithm asset. Detects insecure random, deprecated algorithms (MD5, SHA-1, DES, RC4, Blowfish), weak KDFs, hardcoded IVs/nonces, insecure TLS config, weak key sizes, and certificate pinning. Also scans config files (nginx, Apache, YAML, JSON), and parses X.509 certificates in PEM and DER form for subject, issuer, validity, signature algorithm and key size.

Languages and libraries

188 detection patterns across 21 source extensions, plus configuration, certificate, keystore and documentation formats. Selection is by extension: a file whose extension is not listed is never opened.

LanguageExtensionsWhat is read
C.cOpenSSL, libsodium and liboqs call sites, named curves and algorithm string literals
C#.csSystem.Security.Cryptography call sites and NuGet dependencies
C++.cc, .cppas C, plus C++ wrappers around the same libraries
C++ header.hppdeclarations and macro-defined algorithm selections
C/C++ header.hdeclarations and macro-defined algorithm selections
Go.gocall sites, imports, struct tags and go.mod/go.sum dependencies
Java.javaJCA/JCE call sites, provider strings, keystore loads and Maven/Gradle dependencies
JavaScript.cjs, .js, .jsx, .mjsWebCrypto, Node crypto, and package.json dependencies; .jsx, .cjs and .mjs as .js, with JSX text that reads as prose left out
Kotlin.ktJCA/JCE call sites, as Java
PHP.phpOpenSSL and sodium bindings, and composer.json dependencies
Python.pycall sites, imports and requirements.txt/pyproject.toml dependencies
Ruby.rbOpenSSL bindings and Gemfile dependencies
Rust.rsRustCrypto and ring call sites, and Cargo.toml dependencies
Swift.swiftCryptoKit and CommonCrypto call sites
TypeScript.ts, .tsxas JavaScript; .tsx as .ts, with JSX text that reads as prose left out
Zig.zigstd.crypto call sites

Configuration: .cfg, .cnf, .conf, .config, .ini, .options, .properties, .toml, .yaml, .yml. Certificates and keys: .cer, .crt, .der, .key, .pem. Keystore containers: .bcfks, .jks, .keystore, .ks, .p12, .pfx, .truststore. Documentation and structured data: .json, .jsonc, .md. Dependency manifests are matched by filename rather than extension, for Go, npm, PyPI, Cargo, RubyGems, Maven/Gradle, Conan, Composer and NuGet.

21 cryptographic libraries are recognised by name. An import is reported as a library component, distinct from an algorithm asset: it is evidence the library is present, not that any particular algorithm is called.

LibraryProvides
@noble/cipherssymmetric
@noble/hashessymmetric
@noble/post-quantumpost-quantum
@noble/secp256k1classical
AWS-LCgeneral
Apple CryptoKitgeneral
Botangeneral
Bouncy Castle PQCpost-quantum
CIRCLgeneral
Cisco hash-sigspost-quantum
Go crypto/mlkempost-quantum
Google Tinkgeneral
PQClean/pqm4 KEM APIpost-quantum
RustCryptogeneral
Zig std.cryptogeneral
libcruxpost-quantum
liboqspost-quantum
oriongeneral
pqm4post-quantum
s2n-tls pq-cryptopost-quantum
wolfSSLgeneral

Frameworks are not modelled as a category. A framework is read through the files it is written in and the dependencies it declares, so a Spring or Django project is covered to the extent its language and manifests are, and no further.

Dependency Audit

Audit dependency manifests for crypto library usage and PQC preparedness.

pqprobe dep-audit . Audit current project for crypto dependencies
pqprobe dep-audit /path/to/project Audit a specific project directory
pqprobe dep-audit . --output json JSON output for automation
pqprobe dep-audit . --label payments-api File the stored audit under a label of your own

Parses go.mod, package.json, requirements.txt, Pipfile, pyproject.toml, setup.cfg, Cargo.toml, pom.xml, build.gradle, Gemfile, composer.json, *.csproj, packages.config and Conan (conanfile.txt, conanfile.py, conandata.yml), with the lockfiles package-lock.json, Pipfile.lock, Cargo.lock, Gemfile.lock and composer.lock, across 9 ecosystems. A library that ships post-quantum algorithms from a version on is judged by the version installed, from the lockfile, or else the version or range the manifest names: ready, below the minimum, or unknown when the range allows both. At sensitivity 1 or 2 the audit is stored as an inventory, not graded, under --label or deps- and a hash, and reaches export, pqprobe push and the result card; each dependency keeps its path relative to the audited directory, and a git, file or URL reference in place of a version is dropped and counted. At 0 it is printed only.

MDE Import

Import Microsoft Defender for Endpoint Advanced Hunting exports and map software inventory to PQC preparedness assessments. Zero-deployment crypto library census across large fleets.

pqprobe import mde --source export.csv Import a single CSV export
pqprobe import mde --source ./mde-exports/ Import a directory of exports
pqprobe import mde --source export.csv --dry-run Preview without database writes
pqprobe import mde --source export.csv --output json JSON output for automation
pqprobe import mde --source export.csv --crypto-map custom.json Use custom crypto mapping file

Supports DeviceTvmSoftwareInventory and DeviceFileCertificateInfo table exports (CSV and JSON). Deduplicates by (device, software, version). Maps 15 crypto libraries (OpenSSL, GnuTLS, NSS, LibreSSL, BouncyCastle, wolfSSL, libssh, libssh2, .NET, Java/OpenJDK, Go, rustls, AWS-LC, BoringSSL, mbedTLS) to PQC capability assessments with version-range matching. Each device becomes a stored scan result, graded A–F with its own MDE scorer (capped at 79 without post-quantum capability), and feeds trajectory tracking and the exports like an active probe. Unrecognized software is tagged as “unknown” for inventory visibility. The --crypto-map flag accepts a custom JSON mapping file to extend coverage without recompiling.

KQL Query

Export from MDE Advanced Hunting:

DeviceTvmSoftwareInventory
| project DeviceId, DeviceName, SoftwareName, SoftwareVersion, SoftwareVendor, OSPlatform, OSVersion

Local Discovery

Find cryptographic artifacts on local filesystems.

pqprobe scan-local Scan default paths for crypto files
sudo pqprobe scan-local --system Scan system paths (requires root)
pqprobe scan-local /etc/ssl ~/.ssh Scan specific directories
pqprobe scan-local --sensitivity 2 Critical data sensitivity (affects scoring)
pqprobe scan-local --output json JSON output for automation
pqprobe scan-local --label web-01 File the stored result under a label of your own

Finds certificates (.pem, .crt, .cer, .der, .p12, .pfx), private keys, Java keystores, SSH keys, GPG keyrings, and crypto config files. Works on Linux, macOS, and Windows. The result is graded on its own scale, capped at 85 without post-quantum algorithms. At sensitivity 1 or 2 it is stored, so it reaches export, priorities and pqprobe push; at 0 it is printed only. The stored result names no path, subject, issuer or configuration line: each file is a keyed hash of its path, and the scan is filed under --label, or under fs- and a hash of the host's name and the scanned paths. export-cbom lists its certificates and keys, and export-sarif its findings under FS001 to FS008, each rule on a file also tracked as a finding identity.

Protocol Probing

Probe individual hosts for cryptographic configuration across multiple protocols.

Web & TLS

pqprobe scan example.com TLS/HTTPS - versions, ciphers, certificates, PQC detection; graded and stored
pqprobe scan example.com api.example.com --fail-on 'grade<B' --sarif run.sarif Several targets, a CI policy, SARIF for this run; exit 0 pass, 1 policy failure, 2 not scanned
pqprobe scan --targets-file targets.txt --fail-on no-pqc,tls10 Targets one per line, # comments; - reads stdin
pqprobe --allow-private-targets 10.0.0.0/8 scan 10.1.2.3:8443 Scan an internal host; private ranges are refused unless listed
pqprobe probe example.com Map a domain through DNS (A, MX, SPF, SRV, NS) and scan every service found; results stored

Every probing command below grades and stores its result, under the protocol's name (imap, ldap, mysql, …), from the TLS the protocol negotiated, with --sensitivity 1 (default) or 2. A service that offered no TLS has nothing to grade and is not stored. Two exceptions store nothing: scan-ssh, which has no SSH scorer, and scan-fhir, whose result carries no handshake to grade; pqprobe scan host:443 stores a FHIR server's TLS.

Remote Access

pqprobe scan-ssh server.example.com SSH - key exchange, host keys, ciphers, MACs
pqprobe scan-rdp server.example.com RDP - TLS, NLA, CredSSP, security protocol

Email

pqprobe scan-smtp mail.example.com SMTP - STARTTLS, TLS config, certificates
pqprobe scan-imap imap.example.com IMAP - STARTTLS, implicit TLS (993)
pqprobe scan-pop3 mail.example.com POP3 - STLS, POP3S (995)

Directory & Authentication

pqprobe scan-ldap ldap.example.com LDAP - STARTTLS, LDAPS (636)
pqprobe scan-smb fileserver.example.com SMB/NTLM - NTLMv1/v2, signing, encryption, ESS

Databases

pqprobe scan-mysql db.example.com MySQL - SSL/TLS capability, auth plugins, ciphers
pqprobe scan-postgres db.example.com PostgreSQL - SSL mode, TLS version, ciphers
pqprobe scan-mongodb mongo.example.com MongoDB - TLS mode, server version, auth
pqprobe scan-redis redis.example.com Redis - TLS (Redis 6+), version, auth
pqprobe scan-cassandra cassandra.example.com Cassandra - CQL TLS (9142), version, auth

Message Queues

pqprobe scan-kafka kafka.example.com Kafka - SSL/SASL_SSL (9093/9094), API versions
pqprobe scan-amqp rabbitmq.example.com RabbitMQ/AMQP - AMQPS (5671), protocol version
pqprobe scan-xmpp xmpp.example.com XMPP - STARTTLS, implicit TLS (5223), SASL

Healthcare

pqprobe scan-fhir fhir.hospital.org FHIR - R4/R5, SMART on FHIR, TLS, HIPAA
pqprobe scan-hl7 hl7.hospital.org HL7/MLLP - HL7v2, secure MLLP, PHI protection
pqprobe scan-dicom pacs.hospital.org DICOM - Medical imaging, DICOM-TLS, AE titles

File Transfer

pqprobe scan-ftp ftp.example.com FTP - AUTH TLS, FTPS (990), explicit/implicit

Passive Analysis

Analyze captured network traffic across 13 protocols. No active connections to targets.

pqprobe passive analyze capture.pcap Analyze pcap/pcapng file — 13-protocol detection
pqprobe passive analyze capture.pcap --sensitivity 2 PHI-level sensitivity for healthcare captures
pqprobe passive analyze capture.pcap --output json JSON output for automation
pqprobe passive analyze capture.pcap --verbose Detailed output with per-connection info

Reads pcap and pcapng files from SPAN ports, network taps, or tcpdump. Detects 13 protocols: TLS (ClientHello/ServerHello, cipher suites, certificates, PQC indicators), SSH (version strings, key exchange algorithms), SMTP/IMAP/POP3/FTP STARTTLS upgrades, PostgreSQL SSL and MySQL SSL negotiation, RDP security protocol detection (TLS, CredSSP, NLA), LDAP STARTTLS, XMPP STARTTLS, HL7v2 MLLP (message type, facility — CRITICAL finding for unencrypted medical data), and DICOM (AE Titles, Implementation UID — CRITICAL finding for unencrypted imaging data). Scores each observed host using the same A–F grading as active probes. Results feed into trajectory tracking, change detection, and compliance exports. TLS 1.3 certificates are encrypted and not available in passive mode — scoring uses cipher suite and key exchange data.

Network Discovery

Discover cryptographic services across entire networks, and check a network for hosts and paths that fail post-quantum-sized handshakes.

pqprobe discover 10.0.0.0/24 Discover all crypto services in a subnet
pqprobe discover 10.0.0.0/24 --concurrency 50 Parallel probing with 50 concurrent connections

Network check for post-quantum handshakes

Check the hosts of a network, from inside it, for post-quantum key exchange and for the largest ClientHello each one answers. The sizes, the results and how to read the summary are under Network check.

pqprobe netcheck 10.20.0.0/24 app.example.internal Network check from inside a network: per host, post-quantum key exchange offered and the largest ClientHello answered; failing hosts first, grouped by shared failure; nothing graded or stored
pqprobe netcheck --targets-file hosts.txt --label site-a --format csv --out site-a.csv Hosts, host:port pairs and CIDR ranges from a file; summary, json or csv; a label on every row

Automatically detects 35 service types on 66 ports, including TLS, SSH, SMTP, IMAP, databases, message queues, healthcare protocols, and more.

Endpoint Auditing

Audit local machine cryptographic configuration.

Windows

pqprobe audit-windows Audit Windows NTLM and crypto configuration
pqprobe audit-windows --output json JSON output for automation

Checks LmCompatibilityLevel registry settings, NTLMv1/v2 policy, NTLM authentication events, and security configuration. Must be run locally on the Windows host. The result is printed with OK, WARNING, CRITICAL or EMERGENCY and a 0–100 score. At sensitivity 1 or 2 it is also stored under the machine's name, graded A–F from the same score, and reaches export, pqprobe push and the result card; at 0 it is printed only. The stored audit holds the registry values and the NTLM logon counts, never a logon's user, address or workstation.

Analysis

Analyze cryptographic posture and track changes over time.

pqprobe analyze example.com --days 90 Analyze crypto trends over the last 90 days
pqprobe priorities --tier EMERGENCY Show highest-priority migration targets
pqprobe efficiency example.com Analyze cryptographic efficiency
pqprobe history example.com View probe history for a target
pqprobe horizon --stale-days 30 What has been scanned, what is stale, and where to focus next

Inventory

Manage your cryptographic asset inventory.

pqprobe certificates expiring --days 30 List certificates expiring within 30 days
pqprobe software list List detected software and versions; outdated and summary also exist
pqprobe targets List all probe targets
pqprobe stats Show inventory statistics
pqprobe push --server <url> --api-key-file <file> Upload the scans stored since the last push to a PQProbe server
pqprobe repos list|add|set|rm|import|export The repositories a server's code source scans, through its API with the same --server and --api-key-file; import reads a repository list and its technology mapping, export prints the list back
pqprobe settings get|set key=value The code source's settings on a server: labels, interval_hours, workers, cycle_budget, retention_days and the page's words under list.
pqprobe cite <file> Record scans a published figure cites, so retention keeps their records whole

Export & Integration

Export data and integrate with existing tools.

Compliance Assessment

pqprobe compliance --list-profiles List available compliance profiles (CNSA 2.0, PCI DSS, NIS2, DORA, BSI TR-02102, NIST SP 800-52, HIPAA, NIST IR 8547)
pqprobe compliance --profile cnsa2 --scan-id latest Run the CNSA 2.0 preparedness assessment
pqprobe compliance --profile pci-dss-4 --scan-id latest -o json Compliance assessment with JSON output

Export Formats

pqprobe export inventory.csv Export full inventory to CSV
pqprobe export-certificates certificates.csv Export certificate inventory
pqprobe export-sarif findings.sarif SARIF 2.1.0 from the latest stored scan of every endpoint; - writes to stdout
pqprobe export-cbom output.json Export CycloneDX Cryptographic Bill of Materials
pqprobe report example.com Generate detailed report for a target
pqprobe scan example.com -o json JSON output for automation
pqprobe --version=json Version, commit, build date and dirty flag of this binary

export-sarif covers the latest stored scan of each endpoint; an endpoint whose latest scan failed is left out without a note. scan --sarif covers only the run's targets and reports a target that could not be scanned as an error notification. Both name the endpoint as network://host:port. Every binary answers --version with its version, commit, build time and whether the tree was modified, and --version=json with the same as JSON; a binary built without scripts/version-stamp.sh reports unknown for all three.

Jira Integration

pqprobe jira test Test Jira connection and validate project
pqprobe jira export --scan-id latest Export findings to Jira as trackable issues
pqprobe jira export --scan-id latest --project KEY Export with explicit project key override

Requires PQPROBE_JIRA_URL, PQPROBE_JIRA_EMAIL, PQPROBE_JIRA_API_TOKEN, PQPROBE_JIRA_PROJECT environment variables. Re-scans sync ticket status (a comment and Done on resolution, a comment and reopen on regression) when they record finding fingerprints: CLI scan, probe, scan-smb and passive analyze -o json, server scans submitted with "async": false, and scheduler scans. Scans the dashboard submits run asynchronously and do not sync.

Tool Integration

pqprobe import mde --source export.csv Import MDE software inventory for PQC assessment
pqprobe analyze-nmap scan.xml Import and analyze nmap results
pqprobe --db inventory.db scan ... Use SQLite for persistent storage

Deployment

PQProbe deploys as a single binary with zero external dependencies. The server stores everything in one SQLite file. Its outbound connections go to the targets you probe and, only when you configure them, to GitHub for sign-in, to your Jira instance, and to your alert webhook.

Install

The tarball contains:

  • bin/pqprobe-server — API server and web dashboard
  • bin/pqprobe — CLI probing tool
  • bin/pqprobe-static — Static code analyzer
  • etc/pqprobe.env.example — Configuration template
  • etc/pqprobe.service — systemd unit file
  • INSTALL.md — Step-by-step deployment guide
# Extract
tar xzf pqprobe-*-linux-amd64.tar.gz
cd pqprobe-*/

# Create system user and directories
sudo useradd -r -s /bin/false pqprobe
sudo mkdir -p /opt/pqprobe/bin /opt/pqprobe/data
sudo cp bin/* /opt/pqprobe/bin/
sudo cp etc/pqprobe.env.example /opt/pqprobe/pqprobe.env
sudo chown -R pqprobe:pqprobe /opt/pqprobe

# Install systemd service
sudo cp etc/pqprobe.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pqprobe

# Verify
curl http://localhost:8080/api/v1/health

Docker

# Build and run with Docker Compose
docker-compose up -d

# Or build individual images
docker build -f deployments/docker/Dockerfile.server -t pqprobe-server .
docker build -f deployments/docker/Dockerfile.cli -t pqprobe-cli .
docker build -f deployments/docker/Dockerfile.static -t pqprobe-static .

Authentication

By default, the dashboard and API are open. PQPROBE_REQUIRE_AUTH=true requires an API key or a signed-in session for the data and export routes marked gated in the API Reference. Scan submission, reading a scan result, the probe preview and the dashboard pages stay open either way; /app and /dashboard/nis2 render recent scans and the NIS2 posture into the page.

# In /opt/pqprobe/pqprobe.env
PQPROBE_REQUIRE_AUTH=true
PQPROBE_JWT_SECRET=your-secret-here  # for persistent sessions

When auth is required and no active API key exists, the server creates one named bootstrap-admin at startup and prints it to the log. Create more at the console with:

sudo -u pqprobe /opt/pqprobe/bin/pqprobe-server \
  -db /opt/pqprobe/data/pqprobe.db create-key

Use the API key with any request, as X-API-Key or Authorization: Bearer pqp_…:

curl http://localhost:8080/api/v1/scans -H "X-API-Key: pqp_your_key_here"

Keys made at the console (the bootstrap key and create-key) can delete any scan but the scheduler's; any other key can delete only the scans it uploaded. Any key can create more through POST /api/v1/keys; each key records the key that created it, lists itself, the keys it created and its user's keys, and revokes only keys it created. A console key cannot be revoked through the API. There is no separate admin role beyond that.

GitHub OAuth (optional)

For browser-based login, create a GitHub OAuth App and set:

PQPROBE_GITHUB_CLIENT_ID=your-client-id
PQPROBE_GITHUB_CLIENT_SECRET=your-client-secret
PQPROBE_GITHUB_CALLBACK_URL=https://your-domain/api/v1/auth/github/callback

Without an allowlist, any GitHub account can sign in, and with PQPROBE_REQUIRE_AUTH every signed-in account passes the gate. Limit sign-in with PQPROBE_GITHUB_ALLOWED_USERS (logins, comma-separated, case-insensitive) or PQPROBE_GITHUB_ALLOWED_ORG (active members of one organization; the server then asks GitHub for the read:org scope). A refused account gets no session. Sessions are JWTs in an HttpOnly cookie valid for 24 hours; a login removed from the user list is refused on its next request.

Scheduler

With PQPROBE_AUTO_SCAN_TARGETS set, the server scans a list of targets on its own: the first cycle at startup, then every --auto-scan-interval hours (24), with --auto-scan-workers targets at a time (8). Each worker pauses 2 to 4 seconds between targets; a TLS handshake times out after 10 seconds, and a mail target is checked for an open port for 3 seconds first. A save the database refuses as busy is retried after 1, 2, 4 and 8 seconds. The first cycle after a start skips every target the scheduler stored a row for within the last interval, failed scans included, so a restart finishes the cycle it interrupted rather than scanning every target again; later cycles scan every target due.

The targets file is a JSON array:

[
  {"host": "www.example.com", "category": "banking", "country": "DE"},
  {"host": "mail.example.com:25", "category": "banking", "probe_target": "example.com"},
  {"host": "archive.example.com", "category": "banking", "cadence": "weekly"}
]
KeyMeaning
hostRequired. host or host:port; the port defaults to 443. A host listed twice is scanned once per cycle.
categoryGrouping for the dashboard; also maps the host to a NIS2 sector.
region, countryISO 3166-1 alpha-2 code, or global for region.
scan_typetls (default) or smtp. Ports 25, 465 and 587 mean smtp anyway: STARTTLS on 25 and 587, implicit TLS on 465.
probe_targetGroups several hosts as one probe in the dashboard.
cadencedaily (default) or weekly; a weekly host is scanned on one fixed UTC weekday derived from its name.
referenceA third party's published result for the host (source, method, measured, group, status, edge_provider, label), compared host by host by pqprobe-edgeattr.

PQPROBE_OPTOUT_LIST names a file of hostnames, IP addresses and CIDRs, one per line, # for comments, that the instance never connects to. A missing file counts as empty. It applies to every scan the instance makes: listed entries are removed from the scheduler's targets at startup, a submitted scan of a listed target is refused with a message that the operator of the host has opted out, and a domain probe leaves listed hosts out. A hostname is matched as written; an address or a network is matched against the address about to be dialed, so a name that resolves into a listed network is covered. A host with only some of its addresses listed is scanned on the others, and its record names each address left out as an address entry with the reason as its error; such an entry does not enter the grade. A result that is an opt-out refusal is never stored. The CLI takes the same file with --optout-list, and the worker with -optout-list. Entries reach the list only by editing the file. pqprobe optout-check --optout-list optout.txt --targets-file targets.json is a dry run of a file: it lists, per entry, the targets it would exclude and the total, looking names up in the DNS and connecting to no target.

Scheduler results are public, never expire, and are scored at LOW criticality. A target that could not be scanned is stored as an ungraded row, so population figures count it. Scheduler rows are never deleted through the API. They feed /app, the NIS2 dashboard and scheduler alerts. Scans submitted with "private": true are hidden from lists and deleted 72 hours after submission.

Stored scans are kept in retention tiers. Each scan's row holds a summary; its record is stored apart, with every certificate stored once and referenced by fingerprint, in every tier.

TierWhich scansWhat is kept
pinnedCited scans, and every scan from before 26 September 2026The full record, always
fullThe four newest scans of each hostThe full record
middleOlder scans up to 365 days oldThe record without per-probe results: everything scoring, compliance, projection and diff read
reducedOlder than 365 daysThe summary: target, time, grade and score, scan type, error, completeness and the certificate's names, marked "reduced": true

An uploaded scan joins its host's tiers by its scan time, with no pin for its date. A job moves records down the tiers once a day. A cited scan is one a published figure was built from, recorded in the cited_scans table: the edge-attribution results and the scan the Aer Lingus post links are recorded at startup, and pqprobe-cohort -cite writes the scan IDs of an export for pqprobe cite <file> to record. A database stored by an earlier version is moved to this layout once, in the background after startup, with NIS2 posture builds paused and the last stored build served meanwhile; the server keeps answering throughout, and a restart continues the move where it stopped.

Moving records frees space inside the database file but does not shrink it: new scans reuse the freed pages. To return the space to the disk, stop the server and run VACUUM on the database with sqlite3, as the user the server runs as, then start it again. VACUUM rewrites the whole file and needs free disk of about twice the size it will end up, for its temporary copy and its WAL. The scheduler resumes its cycle after the restart, so the stop costs no rescanning.

Scheduler Alerts

The automated scanner can compare every scan with the previous scheduler scan of the same host and report what changed. Set --alert-webhook log to write alerts to the log, or a URL to also receive each host's alerts as one JSON POST. Unset, nothing is evaluated. --alert-expiry-days sets the certificate expiry window; 0 turns that rule off.

RuleFires whenSuppressed when
grade_droppedThe grade fell under the same scoring versionThe versions differ, or the two scans left different checks unknown; a host that always leaves the same checks unknown still alerts
cert_expiringThe soonest-expiring leaf enters the window; once per certificateThe rule is off (0 days)
hybrid_lostHybrid post-quantum key exchange was accepted and is now accepted on no addressA hybrid group probe went unanswered
legacy_tls_acceptedTLS 1.0 or 1.1 is accepted where the previous scan saw it refusedThe previous scan did not probe the version on a full-sequence address, or left its probe unanswered
leaf_key_changedA leaf key type and size appears that no previous address presentedEither scan has no certificate
negotiated_group_changedThe negotiated group changed on every address; a new group beside the old one is a rolloutEither scan negotiated no group, or the handshake fell back to RSA key exchange

Addresses are compared as sets, so a rotating address set does not read as a change, and an unanswered probe is never taken for a refusal. Each alert is logged as an [alert] JSON line before the POST. The POST has a 30-second timeout and is not retried; the webhook URL never appears in the log.

{
  "event": "pqprobe.alert", "version": 1,
  "target": "www.example.com:443", "scan_id": "...", "prior_scan_id": "...",
  "observed_at": "2026-09-30T05:12:44Z", "prior_observed_at": "2026-09-29T05:10:02Z",
  "alerts": [
    { "rule": "hybrid_lost", "message": "Hybrid post-quantum key exchange is no longer accepted on any address",
      "old_groups": ["X25519MLKEM768"], "new_groups": ["secp256r1", "x25519"], "addresses": ["192.0.2.10:443"] }
  ]
}

Each rule's fields and the suppression conditions in full are in docs/scheduler-alerts.md in the source distribution.

Configuration Reference

Environment VariableFlagDefaultDescription
PQPROBE_REQUIRE_AUTH--require-authfalseRequire JWT or API key for the data and export API routes; scan submission, scan results and the dashboard pages stay open
PQPROBE_JWT_SECRET--jwt-secretephemeralSecret for signing JWT tokens
PQPROBE_GITHUB_CLIENT_ID--github-client-idGitHub OAuth App client ID
PQPROBE_GITHUB_CLIENT_SECRET--github-client-secretGitHub OAuth App client secret
PQPROBE_GITHUB_CALLBACK_URL--github-callback-urlautoOAuth callback URL
PQPROBE_GITHUB_ALLOWED_USERS--github-allowed-usersGitHub logins allowed to sign in, comma-separated, case-insensitive; unset with no org, any GitHub account can
PQPROBE_GITHUB_ALLOWED_ORG--github-allowed-orgGitHub organization whose active members may sign in; requests the read:org scope
PQPROBE_ALLOW_PRIVATE_TARGETS--allow-private-targetsPrivate ranges this instance may scan, as CIDRs, e.g. 10.0.0.0/8,fd00::/8; loopback, link-local, multicast and the host's own addresses are never scanned. The CLI and the worker take the same flag
PQPROBE_AUTO_SCAN_TARGETS--auto-scan-targetsPath to JSON file with probe targets
PQPROBE_AUTO_SCAN_INTERVAL--auto-scan-interval24Hours between automated probe cycles
PQPROBE_AUTO_SCAN_WORKERS--auto-scan-workers8Targets the automated scanner scans at once (Scheduler)
PQPROBE_SECTOR_MAP--sector-mapJSON file mapping each targets-file category to its default NIS2 sector and naming the categories scanned outside the NIS2 cohort, whose hosts take no sector and stay out of every NIS2 figure. Loaded at start, before the scheduler's first scan and the sector backfill. Unset, a host's sector comes from its hostname alone
PQPROBE_OPTOUT_LIST--optout-listFile of hostnames, IPs and CIDRs no scan connects to: scheduled, submitted or fanned out from a domain probe
--posture-interval300Seconds between background rebuilds of the NIS2 posture. Each build is stored, and after a restart the stored build is served until the first new one lands; with none stored, the posture routes answer 503 until then
--posture-max-grade-age336hHow old a host's latest graded scan may be and still count in the NIS2 posture, as a Go duration. Every figure reads a host at its latest graded scan within that window of the figure's reference time: the build, the 30-day cutoff, each day of the history panel. A host without one has no current grade, is outside every figure and is counted in no_current_grade; 0 keeps every graded scan (or PQPROBE_POSTURE_MAX_GRADE_AGE env)
PQPROBE_ALERT_WEBHOOK--alert-webhookScheduler alerts: log, or a URL that also receives a JSON POST per host; empty evaluates nothing (Scheduler Alerts)
PQPROBE_ALERT_EXPIRY_DAYS--alert-expiry-days0Certificate expiry alert window in days; 0 disables it
PQPROBE_SITE--siteselfhostedSite mode. selfhosted, a customer's install: the nav shows Probes, Dashboard and Docs, Dashboard opens the inventory's shell on the network view with the Network and Code switch, and no sales call to action renders anywhere; public is the pqprobe.com site: its nav keeps NIS2 Monitor and OSS Monitor and the calls to action render
PQPROBE_CONTACT--contactThe address the dashboards' data sections name for questions, corrections and opt-outs; on the public site hello@pqprobe.com unless set; unset elsewhere, the line is left out
PQPROBE_ROLE--rolewebweb serves; source runs the scheduled discovery sources, today the code source's cycle, against the store (--db) and the blob directory (--oss-dir) and nothing else: no listener, no network scheduler; analyze is the source's capped subprocess. Both roles share one store; the web role never scans. pqprobe-worker, the queue worker for submitted probes, is unrelated
PQPROBE_OSS_DIR--oss-dirThe code source's blob directory: scan blobs, the clone cache and scratch, keyed from the store's rows; the source role writes it and refuses to start when it cannot, the web role builds the code view from it. Set it in the environment file both roles read. Unset, the OSS Monitor pages answer 404
PQPROBE_OSS_INTERVAL--oss-interval24Source: hours between code source cycles. The first cycle starts at once when the directory holds no completed build, else one interval after the last one finished; a cycle that outlasts the interval delays the next
--oss-workers4Source: repositories scanned at once
--oss-cycle-budget0Source: how long a cycle may spend scanning (a Go duration, 6h, 20m) before the repositories not reached are deferred to the front of the next cycle; 0 is the interval less an hour
--oss-memory-mb2048Source: memory cap for one repository's analysis, which runs in its own process (--role analyze) so a repository that exhausts it fails alone
--oss-retention-days365Source: scan blobs (CBOM and context lines) older than this are deleted; the scan rows age through the store's tiers
--oss-labelsadvisorySource: advisory counts every post-quantum component with code evidence; required counts only those with an agreed true label from the two-pass review
PQPROBE_JIRA_URLJira instance URL (e.g. https://company.atlassian.net)
PQPROBE_JIRA_EMAILJira account email for API auth
PQPROBE_JIRA_API_TOKENJira API token
PQPROBE_JIRA_PROJECTDefault Jira project key for issue export; the CLI's jira export --project overrides it
PQPROBE_BIND--bindAddress to listen on, e.g. 127.0.0.1 behind a reverse proxy on the same host; empty listens on all interfaces
--port8080Server port
--dbpqprobe.dbSQLite database file path; a URL is refused
--workers16Background worker count
--queue-size5000Maximum queued probes
--rate-limit60Max requests per IP per window
--rate-window60Rate limit window (seconds)
PQPROBE_TRUSTED_PROXIES--trusted-proxiesCIDRs of reverse proxies whose X-Forwarded-For names the client, e.g. 127.0.0.1/32,::1/128 behind a local proxy; from any other peer the header is ignored
--secure-cookiesfalseSet Secure flag on cookies (use behind HTTPS)
--cors-originsComma-separated origins allowed cross-origin requests; empty allows same-origin only
--versionPrint version, commit, build date and dirty flag and exit; --version=json for JSON

API Reference

Every route the server registers. Open routes never ask for credentials. Gated routes are open until PQPROBE_REQUIRE_AUTH=true, then need an API key or a session. The rest always need what they say.

MethodPathAccessWhat it does
GET/api/v1/healthopenLiveness, with scoring_version and rescoring, true while the startup rescore rewrites stored grades
GET/api/v1/versionopen{"version": "…"}
POST/api/v1/scanopenSubmit a scan: {"target", "scan_type": "tls"|"smb"|"domain", "async", "private"}. A bare domain becomes a queued domain probe (202). A target the address policy refuses gets 400. A target on the opt-out list gets 403 and the message that the operator of the host has opted out; a queued probe whose target resolves onto the list fails with the same message.
GET/api/v1/scan/{id}openA scan result; 202 while it is queued. A scan past the retention tiers comes back as its summary with "reduced": true (Scheduler)
DELETE/api/v1/scan/{id}console API keyDelete a scan and its findings. A session or a key created through the API gets 403; scheduler scans are never deleted.
POST/api/v1/probe/previewopenThe hosts a domain probe would scan, without scanning. A domain on the opt-out list gets 403.
POST/api/v1/sizetestopenHandshake size test of one host, the test behind the calculator's Test button: {"target", "fixed_part"}. Returns the control outcome, one result per client-flight size and the largest size answered (Network check). Nothing is stored. A target the address policy refuses gets 400, a target on the opt-out list 403, and 429 when too many tests are running.
GET/api/v1/scans?limit=gatedRecent public graded scans (default 100, at most 1000)
GET/api/v1/history/{target}?days=gatedScan history of a target (default 90 days, at most 365); reduced scans are left out
GET/api/v1/diff/{id}gatedWhat changed since the target's previous scan
GET/api/v1/cbom/{id}gatedCycloneDX 1.6 CBOM of a scan
GET/api/v1/sarif/{id}gatedSARIF 2.1.0 of a scan
GET/api/v1/report/{profile}/{id}gatedCompliance report download (cnsa, pci and the other profiles)
GET/api/v1/compliance/profiles?tag=gatedCompliance profiles
GET/api/v1/compliance/{profile}?scan_id=gatedA scan assessed against one profile
GET/api/v1/horizon?stale_days=gatedCoverage: what was scanned, what is stale
GET/api/v1/nis2/posturegatedThe NIS2 posture as JSON; 503 with Retry-After: 30 until the first build
GET/api/v1/projection/{target}?days=&profile=gatedDeadline projection for a target
POST/api/v1/jira/export?scan_id=key or sessionCreate Jira issues for a scan's findings (latest for the newest)
GET/api/v1/jira/statusgatedWhether Jira is configured and reachable
GET/api/v1/findings?status=&type=&limit=gatedTracked findings by fingerprint
GET/api/v1/finding/{fingerprint}, /api/v1/finding/{fingerprint}/historygatedOne finding and its history
POST/api/v1/resultsAPI keyUpload scans, as pqprobe push does: newline-delimited JSON, one {"sensitivity": 1 or 2, "record": a scan} per line, gzip accepted, at most 8 MB decompressed and 1 MB a record, 4 MB a static analysis. The server grades each scan again from its facts and stores it private, never expiring, as the key's upload; a grade, score, risk or sector sent with it is ignored. Kinds: TLS and the TLS-wrapped protocols, SMTP, SMB (with its smb facts), passive captures and Defender imports. The answer gives each line's status: stored or duplicate (200), conflict (409, the scan ID is someone else's), refused (422, with the reason) or failed (500, send it again)
GET/api/v1/results?since=&limit=API keyThe key's uploads, every key's for a console key, stored since a UTC date or RFC 3339 time (default 100, at most 1000)
POST, GET/api/v1/keysAPI keyCreate a key ({"name", "rate_limit"}; the response carries id and the key, shown once), or list the keys this key may see
DELETE/api/v1/keys/{id}API keyRevoke a key this key created; 403 otherwise
GET, POST/api/v1/repositoriesAPI keyThe repositories the code source scans. GET lists them (?enabled=true for the enabled ones) with what the cycle recorded: last commit and scan, PQ since. POST adds one or an array, each {"name": "owner/name", "clone_url", "host_style", "ecosystem", "category", "source", "holdout", "enabled", "project", "owner", "deadline": "YYYY-MM-DD", "scope_overrides": [{"prefix", "scope"}], "caveats": [{"name", "file", "since", "note"}], "listing": {"slug", "name", "round", "url"}}; the clone URL (https://, ssh:// or git@host:path, no credentials) defaults to GitHub by the name and the host style is derived from it. A name already there has its configuration replaced; the batch is checked whole before any of it is written, and records the key that sent it
GET, PUT, DELETE/api/v1/repositories/{owner}/{name}API keyOne repository. PUT changes the fields sent and keeps the rest (a field sent null clears it; the name does not change). DELETE removes the repository and its asset; its scans stay
GET, PUT/api/v1/settings/ossAPI keyThe code source's settings, which override the source role's flags: labels (advisory or required), interval_hours, cycle_start_utc (HH:MM: cycles start at that time of day, the first at its next occurrence rather than at start), workers, cycle_budget (a duration), retention_days, list (name, subject, eyebrow, source_url, listing_label, listed_at: the page's words) and technologies (the list's source entries, for the page's counts). PUT changes the keys sent, one level into list, and keeps the rest; the source reads them at the start of each stage, the interval before each wait
GET/api/v1/auth/github, /api/v1/auth/github/callbackopenGitHub sign-in
any/api/v1/auth/logoutopenClear the session cookie
GET/api/v1/auth/mesessionThe signed-in user
GET/, /app, /signin, /features, /docs, /timeline, /calculator, /about, /blog, /blog/{slug}, /scorecard, /datenschutz, /impressum, /robots.txt, /static/openPages and assets
GET/dashboard/nis2, /dashboard/nis2/openNIS2 dashboard; 503 until the first posture build
GET/dashboard/ossopenThe code view: the repositories the code source scans, from the generation built in the background from the store and the blob directory (--oss-dir); 404 without one, 503 with Retry-After before the first build. ?view=ecosystems and ?view=algorithms change the group table
GET/dashboard/oss/{owner}/{name}openOne repository's card, as under its row
GET/dashboard/vendors, /dashboard/vendors/openThe Vendor Monitor: the vendor cohort's products, each with its status from dated vendor material, the key exchange and certificate signature its hosts negotiate, how it is served (a named edge operator, the vendor's own network, or hosted on a named network), its grade and its movement since the previous edition; built in the background from the stored products and the scheduler's latest scans. 404 without stored products, 503 with Retry-After before the first build
GET, PUT/api/v1/settings/vendorskeyThe vendor cohort's figures: the pinned targets file's SHA-256 and the cut window (targets_sha256, cut_start, cut_nights); GET adds the stored products' SHA-256
GET, PUT/api/v1/settings/vendors/productskeyThe cohort's products.json, stored byte for byte so its SHA-256 is the file's; PUT checks it (keys once, listed categories, a status with material URL and date, hosts on measured products only)
GET/dashboard/oss/privatekey or session, alwaysThe code view's inventories, classical-only and weak-algorithm figures, per-detector precision
GET, POST/dashboard/oss/reviewkey or session, alwaysLabel disagreements and the agreed-label sample; POST records an answer from this site's own form
GET/dashboard/oss/statuskey or session, alwaysThe code source's last cycle: generation, start, finish and duration, each stage's outcome, the repositories that failed or were deferred, when the next cycle is due; and the inventory generation the code view serves, with its build time

An uploaded scan belongs to the key that uploaded it. A scan's routes (/api/v1/scan/{id}, its CBOM, SARIF, compliance report, diff and Jira export) answer 404 for it to anyone but that key and console keys, and the history, projection and finding routes leave it out for them. Uploads are private: they never appear in /app or on the home page, and the scheduler's alerts and the NIS2 dashboard never read them. They do count in the horizon view and in the statistics, which cover every stored scan. A finding is tracked separately for each source of scans of a host: the scheduler, submissions, and each uploading key.

Authentication methods

  • API key: X-API-Key: pqp_… or Authorization: Bearer pqp_…. A malformed or revoked key gets 401, on open routes too.
  • Session: after GitHub sign-in, a JWT in the HttpOnly pqprobe_token cookie (24 hours, SameSite Lax, Secure with --secure-cookies).
  • Session token: the same JWT as Authorization: Bearer, for a token that does not start with pqp_.

Rate limits

Every route is limited per client address: --rate-limit requests (60) per --rate-window seconds (60), a sliding window. Behind a reverse proxy, list it in --trusted-proxies, or every client is counted as the proxy. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time a slot frees); a refused request gets 429 with Retry-After equal to the window. A request with an API key is also limited per key, rate_limit requests a minute (60), with 429 and Retry-After: 60. A full scan queue answers 429 with Retry-After: 30.

Errors

Errors are JSON, {"error": "message"}, with the HTTP status carrying the class: 400 for a bad request or a refused target, 401 without credentials, 403 with credentials that may not do this, 404, 405 for a wrong method, 429 when limited, 503 while the posture is being built.