Skip to content

English · 한국어

pqcota-discovery — observation (stage 1)

Observes which cryptographic algorithms a running system actually uses — not the cryptography itself (no ciphertext, no keys), but which libraries, providers, and algorithms are loaded, registered, and negotiated. It captures the runtime reality that static document and source scans cannot see (providers registered at runtime, libraries actually loaded, groups actually negotiated on the wire), and grades each observed connection by the group it negotiated (🟢 PQC/hybrid · 🔴 classical · ⚪ unknown).

Why runtime — configuration (openssl.cnf, java.security, nginx ssl_ciphers) is an allow list, so it diverges from reality. A PQC group written there falls back to classical if the peer does not support it, and an app may register a provider at runtime that never appears in java.security. So the evidence comes from libraries loaded in the running process and algorithms negotiated in the handshake. When the runtime cannot be reached, it falls back to configuration — but then evidence_strength is lowered and the gap is recorded.

It is one of five repositories that make up pqcota: pqcota-common, pqcota-inventory, pqcota-discovery, pqcota-provisioning, and the integration repository pqcota (demo, examples, release bundles, contributing guide).

At a glance

flowchart LR
    H["hosts.csv<br/>access info"] --> T["targets.ini"] --> C["three collectors<br/>run on each node"]
    C --> J["CollectionResult<br/>JSON"] --> I["ingested into the inventory"]

What it consists of

Piece What it is
Access prep — pqcota-hosts builds an Ansible inventory from the hosts.csv you wrote
The three collectors listed below. They run on the observed machine and emit a CollectionResult
Reference playbook — ansible/ ships and runs collectors across prepared nodes, retrieves the results, and removes its staging directory after a successful run (the Java attach file in the target's /tmp stays)
Collector What it observes How
openssl loaded libcrypto/libssl, fork, app attribution parses /proc and ELF itself (Linux) — no dependency on ldd or readelf
jvm ★ the actual live JCA provider chain (registration order included) JVM attach → getProviders() (pure-Java sidecar)
network TLS/SSH handshake groups → communication edges passive AF_PACKET capture (Linux), no decryption

★ The killer capability of this stage is that jvm attach catches dynamically registered providers (BouncyCastle added via addProvider at runtime, for instance) that static scanning cannot see — a gap no dedicated OSS filled.

Try it quickly

A single node, right where it is — nothing to install, no Ansible needed.

The commands below are for a Linux node. On Windows it is pqcota-cngscan and pqcota-jvmscan — which collector runs on which OS is in the command reference.

pqcota-nodescan --output table            # a table on screen (nothing is stored)
pqcota-nodescan node-01 > result.json     # JSON (when accumulating centrally)

Several nodes — write down the access info and run them all through the reference playbook.

pqcota-hosts --ansible-out targets.ini hosts.csv
ansible-playbook -i targets.ini ansible/discover.yml
pqcota-ingest ./results                   # ingest the retrieved results into the inventory

Arguments, privileges, and environment variables per command → cmd/README.

When it doesn't work — symptom and cause

Symptom Cause
could not open /proc, so nothing was observed not Linux. The result goes out as a gap, not as empty
only the scanning process's own assets show up not root — another user's /proc is unreadable (the denied processes are counted in the scan statistics on stderr, not recorded as a gap)
no CAP_NET_RAW — could not observe setcap cap_net_raw+ep, or root. The exit code is 0 on purpose — so the gap reaches the center
JVM providers show only the static chain attach was blocked and it fell back (DisableAttachMechanism, JEP 451, permissions). Runtime-registered providers are a blind spot there, and that is reported as a gap
zero observed edges no handshake flowed during the observation window. Idle links are invisible in production too — this is not absence, it is not-observed

What is here

Path What
collectors/ the collectors: openssl, jvm (with the Java sidecar), network, cng
cmd/ the commands: pqcota-nodescan, pqcota-jvmscan, pqcota-netcap, pqcota-cngscan, pqcota-hosts, pqcota-procs
ansible/ the reference playbook that runs the collectors across prepared nodes
pkg/discovery/procs/ process attribution shared by the collectors
examples/ runnable examples: access prep, ingest of collected results, and the JVM reconnaissance → attach

Depends on

pqcota-common and pqcota-inventory. Inside this module the collectors import only pqcota-common and pkg/discovery/procs, because they are built into binaries that go onto the observed nodes.

Build and test

make            # every check of this repository
go test ./...   # unit tests only

make build-jar builds the Java attach sidecar (build/collector.jar, needs JDK 11+). make build also cross-compiles for linux/amd64 and windows/amd64, since the collectors' core is Linux-only code behind build tags.

go.mod reads the sibling repositories from ../ through replace directives (../pqcota-common and so on), so clone the repositories side by side. The replace lines stay: they are the local link between the repositories, while the require lines point at the release tag (currently v0.10.6), which is what a consumer outside this workspace receives. See the build guide.

See also

process-attribution library pkg/discovery/procs · normalization and history libraries pkg/inventory/ · runnable examples examples/discovery/

Contributing · security · license

Contributing and security reporting are described in the pqcota repository.

Copyright 2026 Great Honor randyinthedev@gmail.com. Licensed under the Apache License 2.0.

한국어