@quantakrypto/qprobe 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +109 -0
  2. package/THREAT-MODEL.md +83 -0
  3. package/dist/args.d.ts +19 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +94 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/attest.d.ts +31 -0
  8. package/dist/attest.d.ts.map +1 -0
  9. package/dist/attest.js +39 -0
  10. package/dist/attest.js.map +1 -0
  11. package/dist/classify.d.ts +19 -0
  12. package/dist/classify.d.ts.map +1 -0
  13. package/dist/classify.js +105 -0
  14. package/dist/classify.js.map +1 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +118 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/clienthello.d.ts +40 -0
  20. package/dist/clienthello.d.ts.map +1 -0
  21. package/dist/clienthello.js +182 -0
  22. package/dist/clienthello.js.map +1 -0
  23. package/dist/index.d.ts +59 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +74 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/report.d.ts +22 -0
  28. package/dist/report.d.ts.map +1 -0
  29. package/dist/report.js +51 -0
  30. package/dist/report.js.map +1 -0
  31. package/dist/smtp.d.ts +15 -0
  32. package/dist/smtp.d.ts.map +1 -0
  33. package/dist/smtp.js +103 -0
  34. package/dist/smtp.js.map +1 -0
  35. package/dist/ssh.d.ts +28 -0
  36. package/dist/ssh.d.ts.map +1 -0
  37. package/dist/ssh.js +118 -0
  38. package/dist/ssh.js.map +1 -0
  39. package/dist/target.d.ts +25 -0
  40. package/dist/target.d.ts.map +1 -0
  41. package/dist/target.js +81 -0
  42. package/dist/target.js.map +1 -0
  43. package/dist/tls.d.ts +53 -0
  44. package/dist/tls.d.ts.map +1 -0
  45. package/dist/tls.js +109 -0
  46. package/dist/tls.js.map +1 -0
  47. package/dist/version.d.ts +3 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/dist/version.js +3 -0
  50. package/dist/version.js.map +1 -0
  51. package/dist/x509.d.ts +28 -0
  52. package/dist/x509.d.ts.map +1 -0
  53. package/dist/x509.js +73 -0
  54. package/dist/x509.js.map +1 -0
  55. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @quantakrypto/qprobe
2
+
3
+ Actively inspect **live TLS/SSH endpoints you own** for post-quantum readiness —
4
+ whether they negotiate a PQC-hybrid key exchange, and what their classical exposure
5
+ is — with **zero runtime dependencies** (Node built-ins only).
6
+
7
+ qProbe is the live-endpoint sibling of [qScan](../qscan) (which scans code) and the
8
+ infrastructure detectors in [`@quantakrypto/core`](../core) (which scan config). It
9
+ is the **only** package in this repo that opens network connections, and it is gated
10
+ behind a mandatory ownership attestation — read **[THREAT-MODEL.md](THREAT-MODEL.md)**
11
+ before using it.
12
+
13
+ > **Authorization required.** qProbe sends nothing until you attest you are
14
+ > authorized to test the target. Active probing of endpoints you do not own may be
15
+ > unlawful. qProbe refuses CIDR blocks, IP ranges, wildcards, and target lists — it
16
+ > probes **one host you operate** at a time, and it only ever performs a benign,
17
+ > unauthenticated handshake. It reports the negotiated reality; it never modifies an
18
+ > endpoint ("engine disposes").
19
+
20
+ ## What it checks
21
+
22
+ - **TLS** — the negotiated TLS version, cipher suite, ephemeral key-exchange group,
23
+ and leaf-certificate key type, **plus** whether the server supports the
24
+ post-quantum hybrid group **X25519MLKEM768** (codepoint `0x11EC`). Because Node's
25
+ bundled OpenSSL cannot negotiate that group, qProbe detects it with a hand-rolled
26
+ raw ClientHello and reads the group the server selects.
27
+ - **SSH** — the offered key-exchange and host-key algorithms (from the cleartext
28
+ `KEXINIT`), flagging classical-only servers and surfacing PQC KEX
29
+ (`sntrup761x25519-sha512@openssh.com`, `mlkem768x25519-sha256`) as a positive.
30
+
31
+ Findings are `@quantakrypto/core` Findings, so they score (`readinessScore`) and
32
+ read exactly like qScan output and can be merged into the same posture.
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # TLS 443: hybrid KEX + certificate posture (endpoint you control)
38
+ npx @quantakrypto/qprobe --i-own-this example.com
39
+
40
+ # SSH 22: KEXINIT algorithms
41
+ npx @quantakrypto/qprobe --ssh --i-own-this git.example.com
42
+
43
+ # Multiple endpoints from an ownership manifest, JSON output
44
+ npx @quantakrypto/qprobe --owned-hosts hosts.txt api.example.com:8443 --json
45
+ ```
46
+
47
+ ### Options
48
+
49
+ | Flag | Meaning |
50
+ |---|---|
51
+ | `--i-own-this` | Attest you are authorized to probe the target(s). Required (or `--owned-hosts`). |
52
+ | `--owned-hosts <file>` | Ownership manifest (one host per line, `#` comments). Every target must be listed. |
53
+ | `--tls` / `--ssh` | Force a probe mode (default: auto — SSH on `:22`, TLS otherwise). |
54
+ | `--servername <name>` | TLS SNI server name (default: the host; omitted for bare IPs). |
55
+ | `--timeout <ms>` | Per-connection timeout (default: 8000). |
56
+ | `--format <human\|json\|sarif\|cbom>` | Output format (default: human). `--json` / `--sarif` / `--cbom` are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM are the **same formats qScan emits**. |
57
+
58
+ Exit codes mirror qScan: `0` clean, `1` findings, `2` error / not authorized.
59
+
60
+ ## Example
61
+
62
+ ```
63
+ $ qprobe --i-own-this example.com
64
+
65
+ example.com:443 [tls]
66
+ TLSv1.3 · TLS_AES_256_GCM_SHA384 · KEX X25519 · cert EC(prime256v1)-256
67
+ PQC-hybrid (X25519MLKEM768): not negotiated
68
+ [medium] Classical TLS key exchange (no PQC hybrid) — the session key is
69
+ harvest-now-decrypt-later exposed …
70
+ [low] Classical certificate key — forgeable once a CRQC exists …
71
+
72
+ 2 findings · 1 HNDL-exposed · readiness 90/100
73
+ ```
74
+
75
+ ## One post-quantum posture: code + infra + live endpoints
76
+
77
+ qProbe findings are `@quantakrypto/core` Findings and emit the **same** SARIF 2.1.0
78
+ and CycloneDX 1.6 CBOM as qScan. That means the three planes compose into a single
79
+ readiness picture:
80
+
81
+ - **Code + infrastructure-as-code / config** — `qscan .` (the language packs *and*
82
+ the Terraform / Kubernetes / CI-CD / secrets / messaging / database / JOSE
83
+ detectors all run in one scan).
84
+ - **Live endpoints** — `qprobe --i-own-this …`.
85
+
86
+ Because both CBOMs are CycloneDX 1.6 `cryptographic-asset` documents, they merge
87
+ into one org-wide crypto bill of materials, and each readiness score comes from the
88
+ same `buildInventory` math so they read on the same scale:
89
+
90
+ ```bash
91
+ qscan . --cbom -o code-infra.cbom.json # code + infra crypto assets
92
+ qprobe --owned-hosts hosts.txt api.example.com --cbom -o endpoints.cbom.json
93
+ ```
94
+
95
+ Combine them into one CBOM with `mergeCboms` from `@quantakrypto/core` (components
96
+ with the same `bom-ref` collapse to one asset whose evidence spans both planes):
97
+
98
+ ```js
99
+ import { mergeCboms } from "@quantakrypto/core";
100
+ import { readFileSync } from "node:fs";
101
+ const combined = mergeCboms([
102
+ JSON.parse(readFileSync("code-infra.cbom.json", "utf8")),
103
+ JSON.parse(readFileSync("endpoints.cbom.json", "utf8")),
104
+ ]);
105
+ ```
106
+
107
+ ## License
108
+
109
+ Apache-2.0. See [THREAT-MODEL.md](THREAT-MODEL.md) for the authorization model.
@@ -0,0 +1,83 @@
1
+ # qProbe threat model
2
+
3
+ qProbe is the only `@quantakrypto/*` package that opens network connections to
4
+ systems it did not create. Every other tool reads files you already have; qProbe
5
+ reaches out and touches a live endpoint. That single difference is the whole reason
6
+ this document exists, and it drives the design.
7
+
8
+ ## Authorization is the primary control
9
+
10
+ **qProbe refuses to send a single byte to any endpoint until the operator has
11
+ attested authorization.** This is enforced in code (`authorizeTargets` in
12
+ `src/attest.ts`), not by a prompt or a README warning, and it runs **before any
13
+ socket is opened** — `runProbe` calls the gate first and throws `AttestationError`
14
+ on failure, so no connection attempt happens on the unauthorized path.
15
+
16
+ Authorization is exactly one of:
17
+
18
+ - `--i-own-this` — an explicit per-run attestation that you control the target(s).
19
+ - `--owned-hosts <file>` — an ownership manifest (one host per line); every target
20
+ host must appear in it, or the run is refused.
21
+
22
+ There is no third path and no default-on. A run with neither flag exits non-zero
23
+ and connects to nothing.
24
+
25
+ ### Scope is one host at a time
26
+
27
+ qProbe **refuses CIDR blocks, IP ranges, wildcards, and target lists** outright
28
+ (`parseTarget` in `src/target.ts`). `10.0.0.0/24`, `10.0.0.1-50`, `*.example.com`,
29
+ and `a.com,b.com` are all rejected before parsing. This is deliberate: mass scanning
30
+ is a categorically different activity from checking an endpoint you operate, and the
31
+ tool will not make it convenient. Probes are sequential with a minimum interval
32
+ between connections.
33
+
34
+ ### Legal note (operator responsibility)
35
+
36
+ Actively probing endpoints you do not own or lack permission to test may violate the
37
+ US Computer Fraud and Abuse Act (CFAA), the UK Computer Misuse Act, and equivalent
38
+ laws elsewhere, as well as your provider's acceptable-use policy. The attestation is
39
+ your assertion that you are authorized; qProbe cannot verify it for you. **You are
40
+ responsible for the legality of every target you pass.**
41
+
42
+ ## What qProbe actually does on the wire
43
+
44
+ - **TLS:** one normal TLS 1.3 handshake (`node:tls`) to read the negotiated
45
+ version / cipher / key-exchange group and the leaf certificate's public metadata;
46
+ plus one raw ClientHello (`node:net`) advertising `X25519MLKEM768` to observe the
47
+ group the server selects. Both are ordinary, unauthenticated handshakes — the same
48
+ thing any browser or `openssl s_client` does. qProbe does **not** complete the
49
+ handshake with real key material, send application data, attempt authentication,
50
+ or exercise any vulnerability.
51
+ - **SSH:** it reads the server's cleartext identification banner and the
52
+ `SSH_MSG_KEXINIT` it sends before encryption, to list offered key-exchange and
53
+ host-key algorithms. **No authentication is attempted; no crypto is performed.**
54
+
55
+ **"Engine disposes."** qProbe reports the negotiated reality. It never modifies,
56
+ reconfigures, or writes to an endpoint. It has no capability to do so.
57
+
58
+ ## Data handling
59
+
60
+ - The only data qProbe reads is **already public by construction**: negotiated
61
+ protocol parameters, a server certificate (served to every client), and an SSH
62
+ banner + algorithm name-lists (sent to every client before auth).
63
+ - qProbe captures **no secret or key material**. It does not read private keys,
64
+ credentials, or application data. Findings carry only `host:port`, the negotiated
65
+ parameters, and the readiness classification.
66
+ - There is no telemetry and no network egress other than to the attested target(s).
67
+
68
+ ## Non-goals
69
+
70
+ - qProbe is **not** a vulnerability scanner, exploit framework, or penetration-testing
71
+ tool. It classifies post-quantum readiness of the key exchange, cipher, and
72
+ certificate; it does not test for CVEs or attempt compromise.
73
+ - It is **not** a discovery tool. It will not enumerate hosts, sweep ranges, or find
74
+ endpoints for you — you bring the specific endpoint you operate.
75
+
76
+ ## Residual risks & mitigations
77
+
78
+ | Risk | Mitigation |
79
+ |---|---|
80
+ | Operator probes a third-party endpoint | Attestation is required and is the operator's explicit, logged assertion; CIDR/range/list refused; legal responsibility documented. |
81
+ | Accidental mass scan | CIDR/range/wildcard/list rejected at parse time; sequential probing with a minimum interval. |
82
+ | Endpoint mistakes the probe for an attack | Probes are single, benign, standards-compliant handshakes; no auth, no app data, no exploitation. |
83
+ | Sensitive data captured | Only public handshake/cert/banner data is read; no key/secret material; findings hold only `host:port` + negotiated parameters. |
package/dist/args.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hand-rolled CLI argument parser (zero dependencies), mirroring qScan's approach:
3
+ * the parser is pure and total — it never throws and never does I/O.
4
+ */
5
+ export type ProbeModeArg = "tls" | "ssh" | "smtp" | "auto";
6
+ export type FormatArg = "human" | "json" | "sarif" | "cbom";
7
+ export interface CliArgs {
8
+ targets: string[];
9
+ mode: ProbeModeArg;
10
+ iOwnThis: boolean;
11
+ ownedHostsFile?: string;
12
+ servername?: string;
13
+ timeoutMs: number;
14
+ format: FormatArg;
15
+ help: boolean;
16
+ }
17
+ export declare function parseArgs(argv: readonly string[]): CliArgs;
18
+ export declare const HELP = "qprobe \u2014 active post-quantum readiness probing of live TLS/SSH endpoints you OWN.\n\nUSAGE\n qprobe [options] <host[:port]> [host[:port] ...]\n\nAUTHORIZATION (required \u2014 one of)\n --i-own-this Attest you are authorized to probe the given endpoint(s).\n --owned-hosts <file> Ownership manifest (one host per line, # comments).\n\n Active probing of endpoints you do not own may be unlawful. qprobe refuses CIDR\n blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.\n\nOPTIONS\n --tls | --ssh | --smtp Force a probe mode (default: auto \u2014 SSH on :22, SMTP\n STARTTLS on :25/:587, else TLS).\n --servername <name> TLS SNI server name (default: the host).\n --timeout <ms> Per-connection timeout (default: 8000).\n --format <human|json|sarif|cbom>\n Output format (default: human). --json / --sarif / --cbom\n are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM compose\n with qScan output (same formats).\n -h, --help Show this help.\n\nEXAMPLES\n qprobe --i-own-this example.com # TLS 443: hybrid KEX + cert posture\n qprobe --ssh --i-own-this git.example.com # SSH 22: KEXINIT algorithms\n qprobe --owned-hosts hosts.txt api.example.com:8443 --json\n\nqprobe reports the negotiated reality; it never modifies an endpoint (\"engine disposes\").";
19
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAyD1D;AAED,eAAO,MAAM,IAAI,06CA4ByE,CAAC"}
package/dist/args.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Hand-rolled CLI argument parser (zero dependencies), mirroring qScan's approach:
3
+ * the parser is pure and total — it never throws and never does I/O.
4
+ */
5
+ export function parseArgs(argv) {
6
+ const args = {
7
+ targets: [],
8
+ mode: "auto",
9
+ iOwnThis: false,
10
+ timeoutMs: 8000,
11
+ format: "human",
12
+ help: false,
13
+ };
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i];
16
+ switch (a) {
17
+ case "-h":
18
+ case "--help":
19
+ args.help = true;
20
+ break;
21
+ case "--i-own-this":
22
+ args.iOwnThis = true;
23
+ break;
24
+ case "--tls":
25
+ args.mode = "tls";
26
+ break;
27
+ case "--ssh":
28
+ args.mode = "ssh";
29
+ break;
30
+ case "--smtp":
31
+ args.mode = "smtp";
32
+ break;
33
+ case "--owned-hosts":
34
+ args.ownedHostsFile = argv[++i];
35
+ break;
36
+ case "--servername":
37
+ args.servername = argv[++i];
38
+ break;
39
+ case "--timeout":
40
+ args.timeoutMs = Number(argv[++i]) || args.timeoutMs;
41
+ break;
42
+ case "--format": {
43
+ const v = argv[++i];
44
+ if (v === "json" || v === "sarif" || v === "cbom" || v === "human")
45
+ args.format = v;
46
+ break;
47
+ }
48
+ case "--json":
49
+ args.format = "json";
50
+ break;
51
+ case "--sarif":
52
+ args.format = "sarif";
53
+ break;
54
+ case "--cbom":
55
+ args.format = "cbom";
56
+ break;
57
+ default:
58
+ if (a && !a.startsWith("-"))
59
+ args.targets.push(a);
60
+ break;
61
+ }
62
+ }
63
+ return args;
64
+ }
65
+ export const HELP = `qprobe — active post-quantum readiness probing of live TLS/SSH endpoints you OWN.
66
+
67
+ USAGE
68
+ qprobe [options] <host[:port]> [host[:port] ...]
69
+
70
+ AUTHORIZATION (required — one of)
71
+ --i-own-this Attest you are authorized to probe the given endpoint(s).
72
+ --owned-hosts <file> Ownership manifest (one host per line, # comments).
73
+
74
+ Active probing of endpoints you do not own may be unlawful. qprobe refuses CIDR
75
+ blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.
76
+
77
+ OPTIONS
78
+ --tls | --ssh | --smtp Force a probe mode (default: auto — SSH on :22, SMTP
79
+ STARTTLS on :25/:587, else TLS).
80
+ --servername <name> TLS SNI server name (default: the host).
81
+ --timeout <ms> Per-connection timeout (default: 8000).
82
+ --format <human|json|sarif|cbom>
83
+ Output format (default: human). --json / --sarif / --cbom
84
+ are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM compose
85
+ with qScan output (same formats).
86
+ -h, --help Show this help.
87
+
88
+ EXAMPLES
89
+ qprobe --i-own-this example.com # TLS 443: hybrid KEX + cert posture
90
+ qprobe --ssh --i-own-this git.example.com # SSH 22: KEXINIT algorithms
91
+ qprobe --owned-hosts hosts.txt api.example.com:8443 --json
92
+
93
+ qprobe reports the negotiated reality; it never modifies an endpoint ("engine disposes").`;
94
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,MAAM,UAAU,SAAS,CAAC,IAAuB;IAC/C,MAAM,IAAI,GAAY;QACpB,OAAO,EAAE,EAAE;QACX,IAAI,EAAE,MAAM;QACZ,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,OAAO;QACf,IAAI,EAAE,KAAK;KACZ,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,QAAQ,CAAC,EAAE,CAAC;YACV,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACrB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;gBACnB,MAAM;YACR,KAAK,eAAe;gBAClB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAChC,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC5B,MAAM;YACR,KAAK,WAAW;gBACd,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;gBACrD,MAAM;YACR,KAAK,UAAU,CAAC,CAAC,CAAC;gBAChB,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO;oBAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpF,MAAM;YACR,CAAC;YACD,KAAK,QAAQ;gBACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,MAAM;YACR,KAAK,SAAS;gBACZ,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;gBACtB,MAAM;YACR,KAAK,QAAQ;gBACX,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,MAAM;YACR;gBACE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClD,MAAM;QACV,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;0FA4BsE,CAAC","sourcesContent":["/**\n * Hand-rolled CLI argument parser (zero dependencies), mirroring qScan's approach:\n * the parser is pure and total — it never throws and never does I/O.\n */\n\nexport type ProbeModeArg = \"tls\" | \"ssh\" | \"smtp\" | \"auto\";\nexport type FormatArg = \"human\" | \"json\" | \"sarif\" | \"cbom\";\n\nexport interface CliArgs {\n targets: string[];\n mode: ProbeModeArg;\n iOwnThis: boolean;\n ownedHostsFile?: string;\n servername?: string;\n timeoutMs: number;\n format: FormatArg;\n help: boolean;\n}\n\nexport function parseArgs(argv: readonly string[]): CliArgs {\n const args: CliArgs = {\n targets: [],\n mode: \"auto\",\n iOwnThis: false,\n timeoutMs: 8000,\n format: \"human\",\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n switch (a) {\n case \"-h\":\n case \"--help\":\n args.help = true;\n break;\n case \"--i-own-this\":\n args.iOwnThis = true;\n break;\n case \"--tls\":\n args.mode = \"tls\";\n break;\n case \"--ssh\":\n args.mode = \"ssh\";\n break;\n case \"--smtp\":\n args.mode = \"smtp\";\n break;\n case \"--owned-hosts\":\n args.ownedHostsFile = argv[++i];\n break;\n case \"--servername\":\n args.servername = argv[++i];\n break;\n case \"--timeout\":\n args.timeoutMs = Number(argv[++i]) || args.timeoutMs;\n break;\n case \"--format\": {\n const v = argv[++i];\n if (v === \"json\" || v === \"sarif\" || v === \"cbom\" || v === \"human\") args.format = v;\n break;\n }\n case \"--json\":\n args.format = \"json\";\n break;\n case \"--sarif\":\n args.format = \"sarif\";\n break;\n case \"--cbom\":\n args.format = \"cbom\";\n break;\n default:\n if (a && !a.startsWith(\"-\")) args.targets.push(a);\n break;\n }\n }\n return args;\n}\n\nexport const HELP = `qprobe — active post-quantum readiness probing of live TLS/SSH endpoints you OWN.\n\nUSAGE\n qprobe [options] <host[:port]> [host[:port] ...]\n\nAUTHORIZATION (required — one of)\n --i-own-this Attest you are authorized to probe the given endpoint(s).\n --owned-hosts <file> Ownership manifest (one host per line, # comments).\n\n Active probing of endpoints you do not own may be unlawful. qprobe refuses CIDR\n blocks, IP ranges, wildcards and target lists. See THREAT-MODEL.md.\n\nOPTIONS\n --tls | --ssh | --smtp Force a probe mode (default: auto — SSH on :22, SMTP\n STARTTLS on :25/:587, else TLS).\n --servername <name> TLS SNI server name (default: the host).\n --timeout <ms> Per-connection timeout (default: 8000).\n --format <human|json|sarif|cbom>\n Output format (default: human). --json / --sarif / --cbom\n are aliases. SARIF 2.1.0 and CycloneDX 1.6 CBOM compose\n with qScan output (same formats).\n -h, --help Show this help.\n\nEXAMPLES\n qprobe --i-own-this example.com # TLS 443: hybrid KEX + cert posture\n qprobe --ssh --i-own-this git.example.com # SSH 22: KEXINIT algorithms\n qprobe --owned-hosts hosts.txt api.example.com:8443 --json\n\nqprobe reports the negotiated reality; it never modifies an endpoint (\"engine disposes\").`;\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Ownership attestation — the first-class authorization gate for active probing.
3
+ *
4
+ * qProbe MUST NOT connect to an endpoint until the operator has attested they are
5
+ * authorized to test it. Attestation comes from exactly one of:
6
+ * - `--i-own-this` : an explicit per-run attestation for the target(s).
7
+ * - `--owned-hosts <file>` : an ownership manifest (one host per line, `#`
8
+ * comments allowed); every target host must appear.
9
+ *
10
+ * With neither, {@link authorizeTargets} throws and NOTHING connects. This is a
11
+ * code-enforced control, not a prompt: see THREAT-MODEL.md §Authorization.
12
+ */
13
+ import type { Target } from "./target.js";
14
+ export declare class AttestationError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ export interface AttestationInput {
18
+ /** `--i-own-this` was passed. */
19
+ iOwnThis: boolean;
20
+ /** Parsed contents of an ownership manifest, if `--owned-hosts` was given. */
21
+ ownedHosts?: readonly string[];
22
+ }
23
+ /** Parse an ownership manifest file's text into a host allow-list. */
24
+ export declare function parseOwnedHosts(text: string): string[];
25
+ /**
26
+ * Authorize a set of targets or throw {@link AttestationError}. Returns silently
27
+ * when every target is covered by the attestation. No network I/O happens here or
28
+ * anywhere upstream of a successful return.
29
+ */
30
+ export declare function authorizeTargets(targets: readonly Target[], attest: AttestationInput): void;
31
+ //# sourceMappingURL=attest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../src/attest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAStD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAsB3F"}
package/dist/attest.js ADDED
@@ -0,0 +1,39 @@
1
+ export class AttestationError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "AttestationError";
5
+ }
6
+ }
7
+ /** Parse an ownership manifest file's text into a host allow-list. */
8
+ export function parseOwnedHosts(text) {
9
+ return (text
10
+ .split("\n")
11
+ .map((l) => l.trim())
12
+ .filter((l) => l !== "" && !l.startsWith("#"))
13
+ // A manifest line may be `host` or `host:port`; ownership is per host.
14
+ .map((l) => (l.startsWith("[") ? l : l.split(":")[0])));
15
+ }
16
+ /**
17
+ * Authorize a set of targets or throw {@link AttestationError}. Returns silently
18
+ * when every target is covered by the attestation. No network I/O happens here or
19
+ * anywhere upstream of a successful return.
20
+ */
21
+ export function authorizeTargets(targets, attest) {
22
+ if (targets.length === 0)
23
+ throw new AttestationError("no targets to authorize");
24
+ if (attest.ownedHosts && attest.ownedHosts.length > 0) {
25
+ const owned = new Set(attest.ownedHosts.map((h) => h.toLowerCase()));
26
+ const missing = targets.filter((t) => !owned.has(t.host.toLowerCase()));
27
+ if (missing.length > 0) {
28
+ throw new AttestationError(`not authorized: ${missing.map((t) => t.host).join(", ")} not in the ownership manifest. ` +
29
+ `Add the host(s) to the manifest, or pass --i-own-this only for endpoints you control.`);
30
+ }
31
+ return;
32
+ }
33
+ if (attest.iOwnThis)
34
+ return;
35
+ throw new AttestationError("refusing to probe: no ownership attestation. Pass --i-own-this (endpoints you control) " +
36
+ "or --owned-hosts <manifest>. Active probing of endpoints you do not own may be unlawful; " +
37
+ "see THREAT-MODEL.md.");
38
+ }
39
+ //# sourceMappingURL=attest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attest.js","sourceRoot":"","sources":["../src/attest.ts"],"names":[],"mappings":"AAcA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AASD,sEAAsE;AACtE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,CACL,IAAI;SACD,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC9C,uEAAuE;SACtE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACzD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA0B,EAAE,MAAwB;IACnF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,yBAAyB,CAAC,CAAC;IAEhF,IAAI,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACrE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACxE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,gBAAgB,CACxB,mBAAmB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kCAAkC;gBACxF,uFAAuF,CAC1F,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ;QAAE,OAAO;IAE5B,MAAM,IAAI,gBAAgB,CACxB,yFAAyF;QACvF,2FAA2F;QAC3F,sBAAsB,CACzB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Ownership attestation — the first-class authorization gate for active probing.\n *\n * qProbe MUST NOT connect to an endpoint until the operator has attested they are\n * authorized to test it. Attestation comes from exactly one of:\n * - `--i-own-this` : an explicit per-run attestation for the target(s).\n * - `--owned-hosts <file>` : an ownership manifest (one host per line, `#`\n * comments allowed); every target host must appear.\n *\n * With neither, {@link authorizeTargets} throws and NOTHING connects. This is a\n * code-enforced control, not a prompt: see THREAT-MODEL.md §Authorization.\n */\nimport type { Target } from \"./target.js\";\n\nexport class AttestationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AttestationError\";\n }\n}\n\nexport interface AttestationInput {\n /** `--i-own-this` was passed. */\n iOwnThis: boolean;\n /** Parsed contents of an ownership manifest, if `--owned-hosts` was given. */\n ownedHosts?: readonly string[];\n}\n\n/** Parse an ownership manifest file's text into a host allow-list. */\nexport function parseOwnedHosts(text: string): string[] {\n return (\n text\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l !== \"\" && !l.startsWith(\"#\"))\n // A manifest line may be `host` or `host:port`; ownership is per host.\n .map((l) => (l.startsWith(\"[\") ? l : l.split(\":\")[0]))\n );\n}\n\n/**\n * Authorize a set of targets or throw {@link AttestationError}. Returns silently\n * when every target is covered by the attestation. No network I/O happens here or\n * anywhere upstream of a successful return.\n */\nexport function authorizeTargets(targets: readonly Target[], attest: AttestationInput): void {\n if (targets.length === 0) throw new AttestationError(\"no targets to authorize\");\n\n if (attest.ownedHosts && attest.ownedHosts.length > 0) {\n const owned = new Set(attest.ownedHosts.map((h) => h.toLowerCase()));\n const missing = targets.filter((t) => !owned.has(t.host.toLowerCase()));\n if (missing.length > 0) {\n throw new AttestationError(\n `not authorized: ${missing.map((t) => t.host).join(\", \")} not in the ownership manifest. ` +\n `Add the host(s) to the manifest, or pass --i-own-this only for endpoints you control.`,\n );\n }\n return;\n }\n\n if (attest.iOwnThis) return;\n\n throw new AttestationError(\n \"refusing to probe: no ownership attestation. Pass --i-own-this (endpoints you control) \" +\n \"or --owned-hosts <manifest>. Active probing of endpoints you do not own may be unlawful; \" +\n \"see THREAT-MODEL.md.\",\n );\n}\n"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Map live-probe results onto `@quantakrypto/core` Findings, so qProbe output
3
+ * scores and reports through the same engine as qScan (buildInventory, toSarif,
4
+ * toJson, toCbom). A positive result (PQC-hybrid TLS selected, or PQC SSH KEX
5
+ * offered) intentionally emits NO finding — good news keeps the readiness score
6
+ * high; it is surfaced separately in the human summary.
7
+ *
8
+ * Findings are synthesised with `location.file = "host:port"` and line 1, since a
9
+ * live endpoint has no source position.
10
+ */
11
+ import type { Finding } from "@quantakrypto/core";
12
+ import type { Target } from "./target.js";
13
+ import type { TlsNegotiated, HybridSupport } from "./tls.js";
14
+ import type { SshProbeResult } from "./ssh.js";
15
+ /** Findings for a TLS endpoint from the negotiated params + the hybrid probe. */
16
+ export declare function classifyTls(target: Target, neg: TlsNegotiated, hybrid: HybridSupport): Finding[];
17
+ /** Findings for an SSH endpoint from its KEXINIT. */
18
+ export declare function classifySsh(target: Target, ssh: SshProbeResult): Finding[];
19
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAmB,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAkB/C,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,EAAE,CAkEhG;AAED,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,EAAE,CAsB1E"}
@@ -0,0 +1,105 @@
1
+ import { CWE_BROKEN_CRYPTO, CWE_RISKY_PRIMITIVE, CWE_WEAK_STRENGTH } from "@quantakrypto/core";
2
+ function endpoint(t) {
3
+ return { file: `${t.host}:${t.port}`, line: 1 };
4
+ }
5
+ /** Classify a TLS ephemeral group name into a classical algorithm family, or undefined if PQ/unknown. */
6
+ function classicalKexFamily(group) {
7
+ if (!group)
8
+ return undefined;
9
+ const g = group.toLowerCase();
10
+ if (g.includes("mlkem") || g.includes("kyber"))
11
+ return undefined; // already PQ-hybrid
12
+ if (g === "x25519")
13
+ return "X25519";
14
+ if (g === "x448")
15
+ return "X448";
16
+ if (g.startsWith("p-") || g.includes("prime256") || g.includes("secp"))
17
+ return "ECDH";
18
+ if (g === "dh" || g.includes("ffdhe") || g.includes("modp"))
19
+ return "DH";
20
+ return undefined;
21
+ }
22
+ /** Findings for a TLS endpoint from the negotiated params + the hybrid probe. */
23
+ export function classifyTls(target, neg, hybrid) {
24
+ const out = [];
25
+ const loc = endpoint(target);
26
+ if (neg.protocol === "TLSv1" || neg.protocol === "TLSv1.1") {
27
+ out.push({
28
+ ruleId: "qprobe-tls-legacy-version",
29
+ title: "Legacy TLS version negotiated",
30
+ category: "tls",
31
+ severity: "high",
32
+ confidence: "high",
33
+ hndl: false,
34
+ cwe: CWE_RISKY_PRIMITIVE,
35
+ message: `Endpoint negotiated ${neg.protocol}; obsolete TLS with weak, harvestable key exchange.`,
36
+ remediation: "Require TLS 1.3.",
37
+ location: loc,
38
+ });
39
+ }
40
+ // Flag classical KEX when the server did NOT select a PQC-hybrid group. The
41
+ // negotiated group (from node:tls, which cannot do hybrid) is factual; only the
42
+ // "did not select hybrid" claim depends on the raw probe, so soften it when the
43
+ // hybrid probe was inconclusive (e.g. a firewall dropped the raw ClientHello).
44
+ const fam = classicalKexFamily(neg.kexGroup);
45
+ if (!hybrid.hybridSelected && fam) {
46
+ const hybridNote = hybrid.error
47
+ ? "the PQC-hybrid probe was inconclusive"
48
+ : "the server did not select a PQC-hybrid group (X25519MLKEM768)";
49
+ out.push({
50
+ ruleId: "qprobe-tls-classical-kex",
51
+ title: "Classical TLS key exchange (no PQC hybrid)",
52
+ category: "key-exchange",
53
+ severity: "medium",
54
+ confidence: hybrid.error ? "medium" : "high",
55
+ algorithm: fam,
56
+ hndl: true,
57
+ cwe: CWE_BROKEN_CRYPTO,
58
+ message: `TLS key exchange is classical ${neg.kexGroup}; the session key is harvest-now-decrypt-later exposed and ${hybridNote}.`,
59
+ remediation: "Enable a PQC-hybrid key-exchange group (X25519MLKEM768) on the TLS terminator.",
60
+ location: loc,
61
+ });
62
+ }
63
+ if (neg.certKeyType) {
64
+ const isRsa = neg.certKeyType === "RSA";
65
+ out.push({
66
+ ruleId: "qprobe-tls-classical-cert",
67
+ title: "Classical certificate key",
68
+ category: "certificate",
69
+ severity: "low",
70
+ confidence: "high",
71
+ algorithm: isRsa ? "RSA" : "ECDSA",
72
+ hndl: false,
73
+ cwe: isRsa ? CWE_BROKEN_CRYPTO : CWE_WEAK_STRENGTH,
74
+ message: `Leaf certificate uses a classical ${neg.certKeyType}${neg.certKeyBits ? `-${neg.certKeyBits}` : ""} key${neg.certSigFamily ? `, signed by the CA with ${neg.certSigFamily}` : ""}; its signature is forgeable once a CRQC exists.`,
75
+ remediation: "Plan migration to ML-DSA-65 (FIPS 204) certificate keys as your CA adds support.",
76
+ location: loc,
77
+ });
78
+ }
79
+ return out;
80
+ }
81
+ /** Findings for an SSH endpoint from its KEXINIT. */
82
+ export function classifySsh(target, ssh) {
83
+ const out = [];
84
+ if (ssh.error || !ssh.kex)
85
+ return out;
86
+ if (!ssh.pqKexOffered) {
87
+ out.push({
88
+ ruleId: "qprobe-ssh-classical-kex",
89
+ title: "SSH offers only classical key exchange",
90
+ category: "key-exchange",
91
+ severity: "medium",
92
+ confidence: "high",
93
+ algorithm: "ECDH",
94
+ hndl: true,
95
+ cwe: CWE_BROKEN_CRYPTO,
96
+ message: `SSH endpoint offers no post-quantum key exchange (${ssh.kex.kexAlgorithms
97
+ .slice(0, 4)
98
+ .join(", ")}…); session keys are harvest-now-decrypt-later exposed.`,
99
+ remediation: "Enable a PQC hybrid SSH KEX (sntrup761x25519-sha512@openssh.com or mlkem768x25519-sha256).",
100
+ location: endpoint(target),
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Map live-probe results onto `@quantakrypto/core` Findings, so qProbe output\n * scores and reports through the same engine as qScan (buildInventory, toSarif,\n * toJson, toCbom). A positive result (PQC-hybrid TLS selected, or PQC SSH KEX\n * offered) intentionally emits NO finding — good news keeps the readiness score\n * high; it is surfaced separately in the human summary.\n *\n * Findings are synthesised with `location.file = \"host:port\"` and line 1, since a\n * live endpoint has no source position.\n */\nimport type { AlgorithmFamily, Finding } from \"@quantakrypto/core\";\nimport { CWE_BROKEN_CRYPTO, CWE_RISKY_PRIMITIVE, CWE_WEAK_STRENGTH } from \"@quantakrypto/core\";\nimport type { Target } from \"./target.js\";\nimport type { TlsNegotiated, HybridSupport } from \"./tls.js\";\nimport type { SshProbeResult } from \"./ssh.js\";\n\nfunction endpoint(t: Target): Finding[\"location\"] {\n return { file: `${t.host}:${t.port}`, line: 1 };\n}\n\n/** Classify a TLS ephemeral group name into a classical algorithm family, or undefined if PQ/unknown. */\nfunction classicalKexFamily(group?: string): AlgorithmFamily | undefined {\n if (!group) return undefined;\n const g = group.toLowerCase();\n if (g.includes(\"mlkem\") || g.includes(\"kyber\")) return undefined; // already PQ-hybrid\n if (g === \"x25519\") return \"X25519\";\n if (g === \"x448\") return \"X448\";\n if (g.startsWith(\"p-\") || g.includes(\"prime256\") || g.includes(\"secp\")) return \"ECDH\";\n if (g === \"dh\" || g.includes(\"ffdhe\") || g.includes(\"modp\")) return \"DH\";\n return undefined;\n}\n\n/** Findings for a TLS endpoint from the negotiated params + the hybrid probe. */\nexport function classifyTls(target: Target, neg: TlsNegotiated, hybrid: HybridSupport): Finding[] {\n const out: Finding[] = [];\n const loc = endpoint(target);\n\n if (neg.protocol === \"TLSv1\" || neg.protocol === \"TLSv1.1\") {\n out.push({\n ruleId: \"qprobe-tls-legacy-version\",\n title: \"Legacy TLS version negotiated\",\n category: \"tls\",\n severity: \"high\",\n confidence: \"high\",\n hndl: false,\n cwe: CWE_RISKY_PRIMITIVE,\n message: `Endpoint negotiated ${neg.protocol}; obsolete TLS with weak, harvestable key exchange.`,\n remediation: \"Require TLS 1.3.\",\n location: loc,\n });\n }\n\n // Flag classical KEX when the server did NOT select a PQC-hybrid group. The\n // negotiated group (from node:tls, which cannot do hybrid) is factual; only the\n // \"did not select hybrid\" claim depends on the raw probe, so soften it when the\n // hybrid probe was inconclusive (e.g. a firewall dropped the raw ClientHello).\n const fam = classicalKexFamily(neg.kexGroup);\n if (!hybrid.hybridSelected && fam) {\n const hybridNote = hybrid.error\n ? \"the PQC-hybrid probe was inconclusive\"\n : \"the server did not select a PQC-hybrid group (X25519MLKEM768)\";\n out.push({\n ruleId: \"qprobe-tls-classical-kex\",\n title: \"Classical TLS key exchange (no PQC hybrid)\",\n category: \"key-exchange\",\n severity: \"medium\",\n confidence: hybrid.error ? \"medium\" : \"high\",\n algorithm: fam,\n hndl: true,\n cwe: CWE_BROKEN_CRYPTO,\n message: `TLS key exchange is classical ${neg.kexGroup}; the session key is harvest-now-decrypt-later exposed and ${hybridNote}.`,\n remediation: \"Enable a PQC-hybrid key-exchange group (X25519MLKEM768) on the TLS terminator.\",\n location: loc,\n });\n }\n\n if (neg.certKeyType) {\n const isRsa = neg.certKeyType === \"RSA\";\n out.push({\n ruleId: \"qprobe-tls-classical-cert\",\n title: \"Classical certificate key\",\n category: \"certificate\",\n severity: \"low\",\n confidence: \"high\",\n algorithm: isRsa ? \"RSA\" : \"ECDSA\",\n hndl: false,\n cwe: isRsa ? CWE_BROKEN_CRYPTO : CWE_WEAK_STRENGTH,\n message: `Leaf certificate uses a classical ${neg.certKeyType}${\n neg.certKeyBits ? `-${neg.certKeyBits}` : \"\"\n } key${\n neg.certSigFamily ? `, signed by the CA with ${neg.certSigFamily}` : \"\"\n }; its signature is forgeable once a CRQC exists.`,\n remediation:\n \"Plan migration to ML-DSA-65 (FIPS 204) certificate keys as your CA adds support.\",\n location: loc,\n });\n }\n\n return out;\n}\n\n/** Findings for an SSH endpoint from its KEXINIT. */\nexport function classifySsh(target: Target, ssh: SshProbeResult): Finding[] {\n const out: Finding[] = [];\n if (ssh.error || !ssh.kex) return out;\n if (!ssh.pqKexOffered) {\n out.push({\n ruleId: \"qprobe-ssh-classical-kex\",\n title: \"SSH offers only classical key exchange\",\n category: \"key-exchange\",\n severity: \"medium\",\n confidence: \"high\",\n algorithm: \"ECDH\",\n hndl: true,\n cwe: CWE_BROKEN_CRYPTO,\n message: `SSH endpoint offers no post-quantum key exchange (${ssh.kex.kexAlgorithms\n .slice(0, 4)\n .join(\", \")}…); session keys are harvest-now-decrypt-later exposed.`,\n remediation:\n \"Enable a PQC hybrid SSH KEX (sntrup761x25519-sha512@openssh.com or mlkem768x25519-sha256).\",\n location: endpoint(target),\n });\n }\n return out;\n}\n"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}