@quantakrypto/core 0.4.3 → 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 (163) hide show
  1. package/dist/cbom-merge.d.ts +9 -0
  2. package/dist/cbom-merge.d.ts.map +1 -0
  3. package/dist/cbom-merge.js +99 -0
  4. package/dist/cbom-merge.js.map +1 -0
  5. package/dist/comments.d.ts +4 -3
  6. package/dist/comments.d.ts.map +1 -1
  7. package/dist/comments.js +53 -33
  8. package/dist/comments.js.map +1 -1
  9. package/dist/dependencies.d.ts.map +1 -1
  10. package/dist/dependencies.js +1 -1
  11. package/dist/dependencies.js.map +1 -1
  12. package/dist/detect-utils.d.ts +5 -1
  13. package/dist/detect-utils.d.ts.map +1 -1
  14. package/dist/detect-utils.js +11 -2
  15. package/dist/detect-utils.js.map +1 -1
  16. package/dist/detectors/c.d.ts +4 -2
  17. package/dist/detectors/c.d.ts.map +1 -1
  18. package/dist/detectors/c.js +192 -1
  19. package/dist/detectors/c.js.map +1 -1
  20. package/dist/detectors/cicd.d.ts +27 -0
  21. package/dist/detectors/cicd.d.ts.map +1 -0
  22. package/dist/detectors/cicd.js +116 -0
  23. package/dist/detectors/cicd.js.map +1 -0
  24. package/dist/detectors/cloud-kms.d.ts +21 -0
  25. package/dist/detectors/cloud-kms.d.ts.map +1 -0
  26. package/dist/detectors/cloud-kms.js +57 -0
  27. package/dist/detectors/cloud-kms.js.map +1 -0
  28. package/dist/detectors/cloudformation.d.ts +30 -0
  29. package/dist/detectors/cloudformation.d.ts.map +1 -0
  30. package/dist/detectors/cloudformation.js +170 -0
  31. package/dist/detectors/cloudformation.js.map +1 -0
  32. package/dist/detectors/database.d.ts +17 -0
  33. package/dist/detectors/database.d.ts.map +1 -0
  34. package/dist/detectors/database.js +60 -0
  35. package/dist/detectors/database.js.map +1 -0
  36. package/dist/detectors/dnssec.d.ts +55 -0
  37. package/dist/detectors/dnssec.d.ts.map +1 -0
  38. package/dist/detectors/dnssec.js +107 -0
  39. package/dist/detectors/dnssec.js.map +1 -0
  40. package/dist/detectors/elixir.d.ts +20 -0
  41. package/dist/detectors/elixir.d.ts.map +1 -0
  42. package/dist/detectors/elixir.js +195 -0
  43. package/dist/detectors/elixir.js.map +1 -0
  44. package/dist/detectors/java.d.ts.map +1 -1
  45. package/dist/detectors/java.js +1 -1
  46. package/dist/detectors/java.js.map +1 -1
  47. package/dist/detectors/jose.d.ts +20 -0
  48. package/dist/detectors/jose.d.ts.map +1 -0
  49. package/dist/detectors/jose.js +62 -0
  50. package/dist/detectors/jose.js.map +1 -0
  51. package/dist/detectors/jwk.d.ts +25 -0
  52. package/dist/detectors/jwk.d.ts.map +1 -0
  53. package/dist/detectors/jwk.js +95 -0
  54. package/dist/detectors/jwk.js.map +1 -0
  55. package/dist/detectors/k8s.d.ts +17 -0
  56. package/dist/detectors/k8s.d.ts.map +1 -0
  57. package/dist/detectors/k8s.js +85 -0
  58. package/dist/detectors/k8s.js.map +1 -0
  59. package/dist/detectors/mesh.d.ts +35 -0
  60. package/dist/detectors/mesh.d.ts.map +1 -0
  61. package/dist/detectors/mesh.js +109 -0
  62. package/dist/detectors/mesh.js.map +1 -0
  63. package/dist/detectors/messaging.d.ts +16 -0
  64. package/dist/detectors/messaging.d.ts.map +1 -0
  65. package/dist/detectors/messaging.js +72 -0
  66. package/dist/detectors/messaging.js.map +1 -0
  67. package/dist/detectors/pem.js +1 -1
  68. package/dist/detectors/pem.js.map +1 -1
  69. package/dist/detectors/php.d.ts +24 -0
  70. package/dist/detectors/php.d.ts.map +1 -0
  71. package/dist/detectors/php.js +167 -0
  72. package/dist/detectors/php.js.map +1 -0
  73. package/dist/detectors/python.d.ts.map +1 -1
  74. package/dist/detectors/python.js +70 -0
  75. package/dist/detectors/python.js.map +1 -1
  76. package/dist/detectors/rust.d.ts.map +1 -1
  77. package/dist/detectors/rust.js +68 -0
  78. package/dist/detectors/rust.js.map +1 -1
  79. package/dist/detectors/secrets.d.ts +21 -0
  80. package/dist/detectors/secrets.d.ts.map +1 -0
  81. package/dist/detectors/secrets.js +79 -0
  82. package/dist/detectors/secrets.js.map +1 -0
  83. package/dist/detectors/source.d.ts.map +1 -1
  84. package/dist/detectors/source.js +190 -47
  85. package/dist/detectors/source.js.map +1 -1
  86. package/dist/detectors/stateful-hbs.d.ts.map +1 -1
  87. package/dist/detectors/stateful-hbs.js +9 -6
  88. package/dist/detectors/stateful-hbs.js.map +1 -1
  89. package/dist/detectors/terraform.d.ts +21 -0
  90. package/dist/detectors/terraform.d.ts.map +1 -0
  91. package/dist/detectors/terraform.js +123 -0
  92. package/dist/detectors/terraform.js.map +1 -0
  93. package/dist/detectors/vpn.d.ts +22 -0
  94. package/dist/detectors/vpn.d.ts.map +1 -0
  95. package/dist/detectors/vpn.js +99 -0
  96. package/dist/detectors/vpn.js.map +1 -0
  97. package/dist/evidence.d.ts +5 -0
  98. package/dist/evidence.d.ts.map +1 -1
  99. package/dist/evidence.js +5 -0
  100. package/dist/evidence.js.map +1 -1
  101. package/dist/index.d.ts +5 -0
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +6 -0
  104. package/dist/index.js.map +1 -1
  105. package/dist/policy.d.ts +67 -0
  106. package/dist/policy.d.ts.map +1 -0
  107. package/dist/policy.js +115 -0
  108. package/dist/policy.js.map +1 -0
  109. package/dist/registry.d.ts.map +1 -1
  110. package/dist/registry.js +30 -0
  111. package/dist/registry.js.map +1 -1
  112. package/dist/remediate-pipeline.d.ts.map +1 -1
  113. package/dist/remediate-pipeline.js +22 -1
  114. package/dist/remediate-pipeline.js.map +1 -1
  115. package/dist/report.d.ts.map +1 -1
  116. package/dist/report.js +7 -0
  117. package/dist/report.js.map +1 -1
  118. package/dist/standards.d.ts +96 -0
  119. package/dist/standards.d.ts.map +1 -0
  120. package/dist/standards.js +107 -0
  121. package/dist/standards.js.map +1 -0
  122. package/dist/types.d.ts +1 -1
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/types.js.map +1 -1
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/dist/version.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/cbom-merge.ts +116 -0
  130. package/src/comments.ts +59 -33
  131. package/src/dependencies.ts +2 -1
  132. package/src/detect-utils.ts +14 -2
  133. package/src/detectors/c.ts +209 -3
  134. package/src/detectors/cicd.ts +158 -0
  135. package/src/detectors/cloud-kms.ts +87 -0
  136. package/src/detectors/cloudformation.ts +215 -0
  137. package/src/detectors/database.ts +88 -0
  138. package/src/detectors/dnssec.ts +186 -0
  139. package/src/detectors/elixir.ts +237 -0
  140. package/src/detectors/java.ts +2 -1
  141. package/src/detectors/jose.ts +94 -0
  142. package/src/detectors/jwk.ts +128 -0
  143. package/src/detectors/k8s.ts +112 -0
  144. package/src/detectors/mesh.ts +154 -0
  145. package/src/detectors/messaging.ts +98 -0
  146. package/src/detectors/pem.ts +1 -1
  147. package/src/detectors/php.ts +225 -0
  148. package/src/detectors/python.ts +71 -0
  149. package/src/detectors/rust.ts +71 -0
  150. package/src/detectors/secrets.ts +114 -0
  151. package/src/detectors/source.ts +235 -68
  152. package/src/detectors/stateful-hbs.ts +9 -6
  153. package/src/detectors/terraform.ts +154 -0
  154. package/src/detectors/vpn.ts +131 -0
  155. package/src/evidence.ts +11 -0
  156. package/src/index.ts +10 -0
  157. package/src/policy.ts +180 -0
  158. package/src/registry.ts +30 -0
  159. package/src/remediate-pipeline.ts +25 -2
  160. package/src/report.ts +7 -0
  161. package/src/standards.ts +186 -0
  162. package/src/types.ts +2 -0
  163. package/src/version.ts +1 -1
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Config detector: classical crypto in SERVICE MESH configuration — the
3
+ * control-plane identity/CA systems and mesh-internal TLS settings of
4
+ * Linkerd, HashiCorp Consul Connect, and Istio. These mint the mTLS identity
5
+ * certificates that secure sidecar-to-sidecar traffic and are configured in
6
+ * their own Helm values / HCL / CRD surfaces, distinct from what `k8s.ts`
7
+ * already covers (cert-manager `Certificate`/`Issuer` keys and Istio's
8
+ * `minProtocolVersion` TLS floor — NOT duplicated here).
9
+ *
10
+ * Covered (gated by a mesh marker in the document to keep generic keys like
11
+ * `scheme:` / `private_key_type` from firing on unrelated YAML/HCL):
12
+ * - Linkerd control-plane identity issuer: Helm values `identityTrustAnchorsPEM`
13
+ * and `identity.issuer.scheme` / nested `issuer.scheme: linkerd.io/tls` — the
14
+ * default self-managed Linkerd identity scheme, which mints workload
15
+ * certificates from an ECDSA P-256 trust anchor. Flagged as a classical mesh
16
+ * CA (certificate, ECDSA, hndl:false) since Linkerd's identity issuer signs
17
+ * forgeable-by-quantum leaf certs, not a confidentiality key.
18
+ * - HashiCorp Consul Connect: the `connect { ca_config { private_key_type =
19
+ * "ec" | "rsa" } } }` mesh CA provider setting (also `ca_provider`). Flagged
20
+ * as a classical mesh CA (certificate; ECDSA for "ec", RSA for "rsa").
21
+ * - Istio `DestinationRule` / `Gateway` `tls.cipherSuites:` listing a classical
22
+ * `ECDHE-RSA-*` / `ECDHE-ECDSA-*` suite — the (EC)DHE key exchange in these
23
+ * suites is classical and harvest-now-decrypt-later exposed (tls, ECDH,
24
+ * hndl:true). This complements, and does not duplicate, `k8s.ts`'s
25
+ * `minProtocolVersion` legacy-TLS-floor rule.
26
+ */
27
+ import type { Detector, Finding, RuleMeta } from "../types.js";
28
+ import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
29
+ import { CWE_BROKEN_CRYPTO, CWE_RISKY_PRIMITIVE } from "../cwe.js";
30
+
31
+ const MESH_EXTENSIONS: readonly string[] = [".yaml", ".yml", ".hcl"];
32
+
33
+ // Linkerd's default self-managed identity scheme (`linkerd.io/tls`, as opposed
34
+ // to a `kubernetes.io/tls` bring-your-own-cert secret) mints its trust anchor
35
+ // and issuer credentials as ECDSA P-256 by default. Matched in both nested
36
+ // YAML form (`scheme: linkerd.io/tls`) and the flattened Helm `--set`/values
37
+ // key form (`identity.issuer.scheme: linkerd.io/tls`), plus the distinctive
38
+ // `identityTrustAnchorsPEM` values key which only exists in Linkerd charts.
39
+ const RE_MESH_LINKERD_ECDSA =
40
+ /identityTrustAnchorsPEM\b|(?:identity\.issuer\.scheme|scheme)\s*[:=]\s*["']?linkerd\.io\/tls\b/g;
41
+
42
+ // Consul Connect's CA provider config: `private_key_type = "ec" | "rsa"`
43
+ // inside a `ca_config` block. Matched in both HCL (`=`) and JSON (`:`) forms,
44
+ // mirroring the terraform.ts attribute-matching convention.
45
+ const RE_MESH_CONSUL_RSA = /(?<![\w"-])"?private_key_type"?\s*[:=]\s*"rsa"/gi;
46
+ const RE_MESH_CONSUL_EC = /(?<![\w"-])"?private_key_type"?\s*[:=]\s*"ec"/gi;
47
+
48
+ // Istio mesh TLS cipher suites: OpenSSL-style suite names naming classical
49
+ // ECDHE key exchange combined with an RSA or ECDSA certificate. These are
50
+ // distinctive tokens (no generic-config collision risk).
51
+ const RE_MESH_ISTIO_CLASSICAL_CIPHER = /\bECDHE-(?:RSA|ECDSA)-[A-Z0-9_-]+\b/g;
52
+
53
+ const RULE_MESH_LINKERD_ECDSA: RuleMeta = {
54
+ id: "mesh-linkerd-identity-ecdsa",
55
+ title: "Linkerd ECDSA identity issuer",
56
+ description: "Linkerd control-plane identity issuer (default ECDSA P-256 mesh CA)",
57
+ category: "certificate",
58
+ severity: "medium",
59
+ confidence: "high",
60
+ algorithm: "ECDSA",
61
+ hndl: false,
62
+ cwe: CWE_BROKEN_CRYPTO,
63
+ message:
64
+ "Linkerd's control-plane identity issuer mints workload certificates from a classical ECDSA P-256 trust anchor by default, forgeable by a quantum attacker.",
65
+ remediation:
66
+ "Plan migration to ML-DSA-65 (FIPS 204) once Linkerd's identity issuer supports PQC signing.",
67
+ };
68
+ const RULE_MESH_CONSUL_RSA: RuleMeta = {
69
+ id: "mesh-consul-connect-rsa",
70
+ title: "Consul Connect RSA mesh CA",
71
+ description: 'Consul Connect ca_config private_key_type = "rsa"',
72
+ category: "certificate",
73
+ severity: "medium",
74
+ confidence: "high",
75
+ algorithm: "RSA",
76
+ hndl: true,
77
+ cwe: CWE_BROKEN_CRYPTO,
78
+ message:
79
+ "Consul Connect's mesh CA issues leaf certificates from a classical RSA private key, which is not quantum-safe.",
80
+ remediation:
81
+ "Plan migration to PQC certificate keys (ML-DSA-65) as the Connect CA provider adds support.",
82
+ };
83
+ const RULE_MESH_CONSUL_EC: RuleMeta = {
84
+ id: "mesh-consul-connect-ec",
85
+ title: "Consul Connect EC mesh CA",
86
+ description: 'Consul Connect ca_config private_key_type = "ec"',
87
+ category: "certificate",
88
+ severity: "medium",
89
+ confidence: "high",
90
+ algorithm: "ECDSA",
91
+ hndl: false,
92
+ cwe: CWE_BROKEN_CRYPTO,
93
+ message:
94
+ "Consul Connect's mesh CA issues leaf certificates from a classical EC private key, forgeable by a quantum attacker.",
95
+ remediation: "Plan migration to ML-DSA-65 (FIPS 204) as the Connect CA provider adds support.",
96
+ };
97
+ const RULE_MESH_ISTIO_CLASSICAL_CIPHER: RuleMeta = {
98
+ id: "mesh-istio-classical-cipher",
99
+ title: "Istio classical ECDHE cipher suite",
100
+ description:
101
+ "Istio DestinationRule/Gateway tls.cipherSuites lists a classical ECDHE-RSA/ECDHE-ECDSA suite",
102
+ category: "tls",
103
+ severity: "medium",
104
+ confidence: "high",
105
+ algorithm: "ECDH",
106
+ hndl: true,
107
+ cwe: CWE_RISKY_PRIMITIVE,
108
+ message:
109
+ "Istio mesh TLS cipher suite allows classical ECDHE key exchange, which is harvest-now-decrypt-later exposed.",
110
+ remediation:
111
+ "Prefer TLS 1.3 AEAD suites and track PQC-hybrid mesh key exchange (X25519MLKEM768) as Envoy/BoringSSL adds support.",
112
+ };
113
+
114
+ /**
115
+ * Detects classical asymmetric crypto in service-mesh configuration: Linkerd's
116
+ * default ECDSA identity issuer, Consul Connect's mesh CA private key type,
117
+ * and classical ECDHE cipher suites in Istio DestinationRule/Gateway TLS
118
+ * policy.
119
+ */
120
+ export const meshDetector: Detector = {
121
+ id: "service-mesh-crypto",
122
+ description:
123
+ "Classical crypto in service-mesh config (Linkerd identity, Consul Connect CA, Istio cipher suites)",
124
+ scope: "config",
125
+ language: "any",
126
+ rules: [
127
+ RULE_MESH_LINKERD_ECDSA,
128
+ RULE_MESH_CONSUL_RSA,
129
+ RULE_MESH_CONSUL_EC,
130
+ RULE_MESH_ISTIO_CLASSICAL_CIPHER,
131
+ ],
132
+ appliesTo: (f) => hasExtension(f, MESH_EXTENSIONS),
133
+ detect({ file, content }): Finding[] {
134
+ const isLinkerd = content.includes("linkerd") || content.includes("identityTrustAnchors");
135
+ const isConsulConnect = content.includes("consul") && content.includes("connect");
136
+ const isIstioCipher = content.includes("DestinationRule") || content.includes("cipherSuites");
137
+ if (!isLinkerd && !isConsulConnect && !isIstioCipher) return [];
138
+
139
+ const findings: Finding[] = [];
140
+ const add = (re: RegExp, rule: RuleMeta) =>
141
+ eachMatch(re, content, (m) =>
142
+ findings.push(
143
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
144
+ ),
145
+ );
146
+ if (isLinkerd) add(RE_MESH_LINKERD_ECDSA, RULE_MESH_LINKERD_ECDSA);
147
+ if (isConsulConnect) {
148
+ add(RE_MESH_CONSUL_RSA, RULE_MESH_CONSUL_RSA);
149
+ add(RE_MESH_CONSUL_EC, RULE_MESH_CONSUL_EC);
150
+ }
151
+ if (isIstioCipher) add(RE_MESH_ISTIO_CLASSICAL_CIPHER, RULE_MESH_ISTIO_CLASSICAL_CIPHER);
152
+ return findings;
153
+ },
154
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Config detector: classical transport crypto in message brokers / event streams
3
+ * (Kafka, RabbitMQ, MQTT/Mosquitto, NATS). Broker traffic is "communication
4
+ * between things"; a legacy TLS floor or a classical (EC)DHE cipher suite makes
5
+ * every message in flight harvest-now-decrypt-later exposed.
6
+ *
7
+ * Covered in broker config files (`.properties`, `.conf`, `.cfg`, `.ini`):
8
+ * - Kafka `ssl.protocol` / `ssl.enabled.protocols = TLSv1 | TLSv1.1` (legacy).
9
+ * - MQTT/Mosquitto `tls_version tlsv1 | tlsv1.1` (legacy).
10
+ * - Kafka `ssl.cipher.suites` naming a classical `ECDHE_RSA` / `ECDHE_ECDSA` /
11
+ * `TLS_RSA` suite — the harvestable key-exchange path.
12
+ */
13
+ import type { Detector, Finding, RuleMeta } from "../types.js";
14
+ import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
15
+ import { CWE_RISKY_PRIMITIVE, CWE_BROKEN_CRYPTO } from "../cwe.js";
16
+
17
+ const MQ_EXTENSIONS: readonly string[] = [".properties", ".conf", ".cfg", ".ini"];
18
+
19
+ interface MqRule {
20
+ re: RegExp;
21
+ meta: RuleMeta;
22
+ }
23
+
24
+ const MQ_RULES: MqRule[] = [
25
+ {
26
+ // Match TLSv1 (=1.0) and TLSv1.1 but never TLSv1.2 / TLSv1.3: the negative
27
+ // lookahead stops "TLSv1" from matching the "TLSv1" inside "TLSv1.3".
28
+ re: /\bssl\.(?:enabled\.)?protocols?\s*=\s*[^\n]*\bTLSv1(?:\.1)?(?![.\d])/gi,
29
+ meta: {
30
+ id: "mq-kafka-legacy-tls",
31
+ title: "Kafka legacy TLS protocol",
32
+ description: "Kafka ssl.protocol / ssl.enabled.protocols permits TLS 1.0 / 1.1",
33
+ category: "tls",
34
+ severity: "medium",
35
+ confidence: "high",
36
+ hndl: false,
37
+ cwe: CWE_RISKY_PRIMITIVE,
38
+ message:
39
+ "Kafka broker permits legacy TLS 1.0/1.1; its classical key exchange is weak and harvestable.",
40
+ remediation: "Require TLS 1.3 and track PQC-hybrid KEX (X25519MLKEM768).",
41
+ },
42
+ },
43
+ {
44
+ re: /\btls_version\s+tlsv1(?:\.1)?(?![.\d])/gi,
45
+ meta: {
46
+ id: "mq-mqtt-legacy-tls",
47
+ title: "MQTT legacy TLS version",
48
+ description: "Mosquitto/MQTT tls_version permits TLS 1.0 / 1.1",
49
+ category: "tls",
50
+ severity: "medium",
51
+ confidence: "high",
52
+ hndl: false,
53
+ cwe: CWE_RISKY_PRIMITIVE,
54
+ message:
55
+ "MQTT broker permits legacy TLS 1.0/1.1; its classical key exchange is weak and harvestable.",
56
+ remediation: "Require TLS 1.3 and track PQC-hybrid KEX for device fleets.",
57
+ },
58
+ },
59
+ {
60
+ re: /\bssl\.cipher\.suites\s*=\s*[^\n]*(?:ECDHE_RSA|ECDHE_ECDSA|TLS_RSA|_DHE_RSA)/g,
61
+ meta: {
62
+ id: "mq-classical-cipher",
63
+ title: "Broker classical (EC)DHE cipher suite",
64
+ description: "Kafka ssl.cipher.suites names a classical ECDHE/DHE/RSA suite",
65
+ category: "tls",
66
+ severity: "medium",
67
+ confidence: "high",
68
+ algorithm: "ECDH",
69
+ hndl: true,
70
+ cwe: CWE_BROKEN_CRYPTO,
71
+ message:
72
+ "Broker TLS is pinned to a classical (EC)DHE/RSA cipher suite; the key exchange is harvest-now-decrypt-later exposed.",
73
+ remediation:
74
+ "Move to TLS 1.3 with a PQC-hybrid group (X25519MLKEM768) once the broker/runtime supports it.",
75
+ },
76
+ },
77
+ ];
78
+
79
+ /** Detects classical transport crypto in message-broker / event-stream config. */
80
+ export const messagingDetector: Detector = {
81
+ id: "messaging-transport",
82
+ description: "Classical transport crypto in message brokers (Kafka, MQTT, RabbitMQ, NATS)",
83
+ scope: "config",
84
+ language: "any",
85
+ rules: MQ_RULES.map((r) => r.meta),
86
+ appliesTo: (f) => hasExtension(f, MQ_EXTENSIONS),
87
+ detect({ file, content }): Finding[] {
88
+ const findings: Finding[] = [];
89
+ for (const rule of MQ_RULES) {
90
+ eachMatch(rule.re, content, (m) => {
91
+ findings.push(
92
+ findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),
93
+ );
94
+ });
95
+ }
96
+ return findings;
97
+ },
98
+ };
@@ -51,7 +51,7 @@ const PEM_RULES: PemRule[] = [
51
51
  sensitive: true,
52
52
  message: "Embedded EC private key (SEC1 PEM); classical ECDSA/ECDH key, not quantum-safe.",
53
53
  remediation:
54
- "Migrate to ML-DSA (FIPS 204) keys and remove embedded private keys from source.",
54
+ "Migrate to ML-DSA (FIPS 204) for signatures or hybrid X25519MLKEM768 for key agreement; remove embedded private keys from source.",
55
55
  },
56
56
  },
57
57
  {
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Source-code detector for classical asymmetric cryptography in PHP — one of the
3
+ * most-deployed backend languages, previously uncovered. Handles the three
4
+ * dominant crypto surfaces:
5
+ *
6
+ * - **ext/openssl** — `openssl_pkey_new()` (classified by its
7
+ * `OPENSSL_KEYTYPE_*` config, defaulting to RSA), `openssl_public_encrypt` /
8
+ * `openssl_private_decrypt` (RSA), and `openssl_sign` / `openssl_verify`.
9
+ * - **phpseclib3** — `RSA::createKey()` / `EC::createKey()` / `DSA::createKey()`.
10
+ * - **libsodium** — `sodium_crypto_box`/`kx` keypairs (X25519) and
11
+ * `sodium_crypto_sign` keypairs (Ed25519).
12
+ *
13
+ * Lexical, like the other packs; the `openssl_*` / `sodium_crypto_*` prefixes and
14
+ * `OPENSSL_KEYTYPE_*` constants are distinctive, so the false-positive rate is low.
15
+ *
16
+ * HNDL: RSA encryption and (EC/X)DH key agreement are harvest-now-decrypt-later
17
+ * exposed (hndl:true); ECDSA / DSA / Ed25519 signatures are hndl:false but
18
+ * forgeable. `openssl_pkey_new` EC and `EC::createKey` are ambiguous (feed BOTH
19
+ * ECDSA and ECDH), classified conservatively as key-exchange-capable (hndl:true).
20
+ */
21
+ import type { Detector, Finding, RuleMeta } from "../types.js";
22
+ import { PHP_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
23
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
24
+
25
+ const RE_PHP_PKEY_NEW = /\bopenssl_pkey_new\s*\(/g;
26
+ const RE_PHP_RSA_CRYPT = /\bopenssl_(?:public_encrypt|private_decrypt)\s*\(/g;
27
+ const RE_PHP_SIGN = /\bopenssl_(?:sign|verify)\s*\(/g;
28
+ // phpseclib3 factory methods. The class names are generic, so require `::createKey`.
29
+ const RE_PHP_SECLIB = /\b(RSA|EC|DSA|DH)::createKey\s*\(/g;
30
+ // libsodium X25519 (box / kx / scalarmult) and Ed25519 (sign) key pairs.
31
+ const RE_PHP_SODIUM_X25519 =
32
+ /\bsodium_crypto_(?:box|kx)_(?:seed_)?keypair\s*\(|\bsodium_crypto_scalarmult(?:_base)?\s*\(/g;
33
+ const RE_PHP_SODIUM_ED25519 = /\bsodium_crypto_sign_(?:seed_)?keypair\s*\(/g;
34
+
35
+ /** Key-type classification for `openssl_pkey_new`, keyed by its config constant. */
36
+ interface KeyInfo {
37
+ algo: Finding["algorithm"];
38
+ cat: Finding["category"];
39
+ sev: Finding["severity"];
40
+ hndl: boolean;
41
+ label: string;
42
+ remediation?: string;
43
+ }
44
+ const HYBRID = "hybrid X25519MLKEM768 (ML-KEM-768) for key agreement; ML-DSA-65 (FIPS 204) to sign";
45
+
46
+ const RULE_PHP_KEYGEN: RuleMeta = {
47
+ id: "php-openssl-keygen",
48
+ title: "PHP openssl key generation",
49
+ description: "openssl_pkey_new (RSA/EC/DSA/DH, by OPENSSL_KEYTYPE_*)",
50
+ category: "kem",
51
+ severity: "high",
52
+ confidence: "high",
53
+ algorithm: "RSA",
54
+ hndl: true,
55
+ cwe: CWE_BROKEN_CRYPTO,
56
+ message: "Generates a classical key pair via PHP openssl_pkey_new — not quantum-safe.",
57
+ };
58
+ const RULE_PHP_RSA_CRYPT: RuleMeta = {
59
+ id: "php-openssl-rsa-crypt",
60
+ title: "PHP openssl RSA public-key encryption",
61
+ description: "openssl_public_encrypt / openssl_private_decrypt",
62
+ category: "kem",
63
+ severity: "high",
64
+ confidence: "high",
65
+ algorithm: "RSA",
66
+ hndl: true,
67
+ cwe: CWE_BROKEN_CRYPTO,
68
+ message:
69
+ "RSA public-key encryption/decryption (PHP openssl) is harvest-now-decrypt-later exposed.",
70
+ };
71
+ const RULE_PHP_SIGN: RuleMeta = {
72
+ id: "php-openssl-sign",
73
+ title: "PHP openssl signature",
74
+ description: "openssl_sign / openssl_verify",
75
+ category: "signature",
76
+ severity: "high",
77
+ confidence: "high",
78
+ algorithm: "unknown",
79
+ hndl: false,
80
+ cwe: CWE_BROKEN_CRYPTO,
81
+ message:
82
+ "Classical signature via PHP openssl (RSA/ECDSA/DSA) is forgeable by a quantum attacker.",
83
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
84
+ };
85
+ const RULE_PHP_SECLIB: RuleMeta = {
86
+ id: "php-phpseclib-keygen",
87
+ title: "PHP phpseclib key generation",
88
+ description: "phpseclib3 RSA/EC/DSA/DH ::createKey",
89
+ category: "kem",
90
+ severity: "high",
91
+ confidence: "medium",
92
+ algorithm: "RSA",
93
+ hndl: true,
94
+ cwe: CWE_BROKEN_CRYPTO,
95
+ message: "Generates a classical key pair via phpseclib3 (createKey) — not quantum-safe.",
96
+ };
97
+ const RULE_PHP_SODIUM_X25519: RuleMeta = {
98
+ id: "php-sodium-x25519",
99
+ title: "PHP libsodium X25519 key agreement",
100
+ description: "sodium_crypto_box/kx keypair + scalarmult",
101
+ category: "key-exchange",
102
+ severity: "medium",
103
+ confidence: "high",
104
+ algorithm: "X25519",
105
+ hndl: true,
106
+ cwe: CWE_BROKEN_CRYPTO,
107
+ message:
108
+ "libsodium crypto_box/kx (PHP) uses X25519 key agreement — modern but classical, harvest-now-decrypt-later exposed.",
109
+ };
110
+ const RULE_PHP_SODIUM_ED25519: RuleMeta = {
111
+ id: "php-sodium-ed25519",
112
+ title: "PHP libsodium Ed25519 signature",
113
+ description: "sodium_crypto_sign keypair",
114
+ category: "signature",
115
+ severity: "low",
116
+ confidence: "high",
117
+ algorithm: "EdDSA",
118
+ hndl: false,
119
+ cwe: CWE_BROKEN_CRYPTO,
120
+ message:
121
+ "libsodium crypto_sign (PHP) uses Ed25519 — classical and forgeable by a quantum attacker.",
122
+ };
123
+
124
+ /** phpseclib class name → its classification. */
125
+ const SECLIB_INFO: Record<string, KeyInfo> = {
126
+ RSA: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
127
+ EC: {
128
+ algo: "ECDH",
129
+ cat: "key-exchange",
130
+ sev: "high",
131
+ hndl: true,
132
+ label: "EC (ECDSA/ECDH)",
133
+ remediation: HYBRID,
134
+ },
135
+ DSA: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
136
+ DH: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
137
+ };
138
+
139
+ /** Classify `openssl_pkey_new` by the `OPENSSL_KEYTYPE_*` constant in its config.
140
+ * The window is bounded to the current statement (up to the next `;`, capped at
141
+ * 300 chars) so it can't bleed into a following `openssl_pkey_new` call's key
142
+ * type. Defaults to RSA — openssl_pkey_new's own default private_key_type is RSA
143
+ * when none is given. */
144
+ function classifyPkeyNew(content: string, index: number): KeyInfo {
145
+ const semi = content.indexOf(";", index);
146
+ const end = Math.min(index + 300, semi === -1 ? content.length : semi);
147
+ const w = content.slice(index, end);
148
+ if (/\bOPENSSL_KEYTYPE_EC\b/.test(w)) return SECLIB_INFO.EC;
149
+ if (/\bOPENSSL_KEYTYPE_DSA\b/.test(w)) return SECLIB_INFO.DSA;
150
+ if (/\bOPENSSL_KEYTYPE_DH\b/.test(w)) return SECLIB_INFO.DH;
151
+ return SECLIB_INFO.RSA;
152
+ }
153
+
154
+ /** Detects classical asymmetric crypto in PHP (openssl, phpseclib, libsodium). */
155
+ export const phpDetector: Detector = {
156
+ id: "php-crypto",
157
+ description: "Classical asymmetric crypto in PHP (openssl, phpseclib3, libsodium)",
158
+ scope: "source",
159
+ language: "php",
160
+ rules: [
161
+ RULE_PHP_KEYGEN,
162
+ RULE_PHP_RSA_CRYPT,
163
+ RULE_PHP_SIGN,
164
+ RULE_PHP_SECLIB,
165
+ RULE_PHP_SODIUM_X25519,
166
+ RULE_PHP_SODIUM_ED25519,
167
+ ],
168
+ appliesTo: (f) => hasExtension(f, PHP_EXTENSIONS),
169
+ detect({ file, content }): Finding[] {
170
+ const findings: Finding[] = [];
171
+ const add = (re: RegExp, rule: RuleMeta): void =>
172
+ eachMatch(re, content, (m) =>
173
+ findings.push(
174
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
175
+ ),
176
+ );
177
+
178
+ // openssl_pkey_new — one finding per call, classified by its key-type config.
179
+ eachMatch(RE_PHP_PKEY_NEW, content, (m) => {
180
+ const info = classifyPkeyNew(content, m.index);
181
+ findings.push(
182
+ findingFromRule(
183
+ RULE_PHP_KEYGEN,
184
+ { file, content, index: m.index, matchLength: m[0].length },
185
+ {
186
+ title: `PHP openssl ${info.label} key generation`,
187
+ category: info.cat,
188
+ severity: info.sev,
189
+ algorithm: info.algo,
190
+ hndl: info.hndl,
191
+ message: `Generates a classical ${info.label} key pair via PHP openssl_pkey_new — not quantum-safe.`,
192
+ ...(info.remediation ? { remediation: info.remediation } : {}),
193
+ },
194
+ ),
195
+ );
196
+ });
197
+
198
+ add(RE_PHP_RSA_CRYPT, RULE_PHP_RSA_CRYPT);
199
+ add(RE_PHP_SIGN, RULE_PHP_SIGN);
200
+
201
+ // phpseclib3 ::createKey — classify by the class (RSA/EC/DSA/DH).
202
+ eachMatch(RE_PHP_SECLIB, content, (m) => {
203
+ const info = SECLIB_INFO[m[1]];
204
+ findings.push(
205
+ findingFromRule(
206
+ RULE_PHP_SECLIB,
207
+ { file, content, index: m.index, matchLength: m[0].length },
208
+ {
209
+ title: `PHP phpseclib ${info.label} key generation`,
210
+ category: info.cat,
211
+ severity: info.sev,
212
+ algorithm: info.algo,
213
+ hndl: info.hndl,
214
+ message: `Generates a classical ${info.label} key pair via phpseclib3 createKey — not quantum-safe.`,
215
+ ...(info.remediation ? { remediation: info.remediation } : {}),
216
+ },
217
+ ),
218
+ );
219
+ });
220
+
221
+ add(RE_PHP_SODIUM_X25519, RULE_PHP_SODIUM_X25519);
222
+ add(RE_PHP_SODIUM_ED25519, RULE_PHP_SODIUM_ED25519);
223
+ return findings;
224
+ },
225
+ };
@@ -241,6 +241,63 @@ const RULE_PY_TLS_LEGACY: RuleMeta = {
241
241
  "Use ssl.PROTOCOL_TLS_CLIENT with minimum_version = ssl.TLSVersion.TLSv1_3 and prefer PQC-hybrid key exchange.",
242
242
  };
243
243
 
244
+ /**
245
+ * Aliasable `cryptography` / PyCryptodome modules → the (method, rule) pairs
246
+ * reachable through them. Lets an aliased module import
247
+ * (`from ... import rsa as _rsa` → `_rsa.generate_private_key(`) resolve back to
248
+ * the same rule the direct `rsa.generate_private_key(` would fire, since the
249
+ * detector's regexes are module-qualified and miss a renamed prefix.
250
+ */
251
+ const PY_MODULE_RULES: Record<string, ReadonlyArray<{ method: string; rule: RuleMeta }>> = {
252
+ rsa: [{ method: "generate_private_key", rule: RULE_PY_RSA_KEYGEN }],
253
+ ec: [
254
+ { method: "generate_private_key", rule: RULE_PY_EC_KEYGEN },
255
+ { method: "ECDSA", rule: RULE_PY_ECDSA },
256
+ { method: "ECDH", rule: RULE_PY_ECDH },
257
+ ],
258
+ dsa: [{ method: "generate_private_key", rule: RULE_PY_HAZMAT_DSA }],
259
+ dh: [{ method: "generate_parameters", rule: RULE_PY_DH }],
260
+ padding: [{ method: "OAEP", rule: RULE_PY_RSA_ENCRYPT }],
261
+ // PyCryptodome factory modules (`.generate(`).
262
+ RSA: [{ method: "generate", rule: RULE_PY_RSA_KEYGEN }],
263
+ ECC: [{ method: "generate", rule: RULE_PY_EC_KEYGEN }],
264
+ DSA: [{ method: "generate", rule: RULE_PY_DSA }],
265
+ };
266
+
267
+ /** Escape a string for interpolation into a dynamically-built RegExp. */
268
+ function escapePyRe(s: string): string {
269
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
270
+ }
271
+
272
+ /**
273
+ * Collect module aliases for the aliasable crypto modules, so a later
274
+ * `<alias>.<method>(` call still resolves. Handles both `from <path> import
275
+ * <mod> as <alias>` (including comma-separated specifiers) and the bare
276
+ * `import <path.mod> as <alias>`. Returns Map<module, alias[]>; an alias equal to
277
+ * its own module name is skipped (matched directly already).
278
+ */
279
+ function collectPyModuleAliases(content: string): Map<string, string[]> {
280
+ const out = new Map<string, string[]>();
281
+ const add = (mod: string, alias: string): void => {
282
+ if (!alias || alias === mod || !(mod in PY_MODULE_RULES)) return;
283
+ const list = out.get(mod) ?? [];
284
+ if (!list.includes(alias)) list.push(alias);
285
+ out.set(mod, list);
286
+ };
287
+ // `from <path> import a as b, c as d` — scan the import list for `X as Y`.
288
+ const fromRe = /(?:^|\n)[ \t]*from\s+[\w.]+\s+import\s+([^\n#]+)/g;
289
+ for (let m = fromRe.exec(content); m; m = fromRe.exec(content)) {
290
+ const specRe = /([A-Za-z_]\w*)\s+as\s+([A-Za-z_]\w*)/g;
291
+ for (let s = specRe.exec(m[1]); s; s = specRe.exec(m[1])) add(s[1], s[2]);
292
+ }
293
+ // `import <path.mod> as <alias>` — the aliased module is the last dotted segment.
294
+ const impRe = /(?:^|\n)[ \t]*import\s+([\w.]+)\s+as\s+([A-Za-z_]\w*)/g;
295
+ for (let m = impRe.exec(content); m; m = impRe.exec(content)) {
296
+ add(m[1].split(".").pop() ?? m[1], m[2]);
297
+ }
298
+ return out;
299
+ }
300
+
244
301
  /** Detects classical asymmetric crypto in Python source. */
245
302
  export const pythonDetector: Detector = {
246
303
  id: "python-crypto",
@@ -286,6 +343,20 @@ export const pythonDetector: Detector = {
286
343
  add(RE_PY_TLS_REJECT, RULE_PY_TLS_REJECT);
287
344
  add(RE_PY_TLS_LEGACY, RULE_PY_TLS_LEGACY);
288
345
 
346
+ // Module-alias resolution: `from ... import rsa as _rsa` then
347
+ // `_rsa.generate_private_key(` — the direct regexes are module-qualified
348
+ // (`\brsa\.`) and miss a renamed prefix. Runs on the ORIGINAL content so
349
+ // locations stay exact; fires only for an alias explicitly bound to a known
350
+ // crypto module, so precision is unaffected.
351
+ for (const [mod, aliasList] of collectPyModuleAliases(content)) {
352
+ for (const alias of aliasList) {
353
+ const a = escapePyRe(alias);
354
+ for (const { method, rule } of PY_MODULE_RULES[mod]) {
355
+ add(new RegExp(`\\b${a}\\.${method}\\s*\\(`, "g"), rule);
356
+ }
357
+ }
358
+ }
359
+
289
360
  return findings;
290
361
  },
291
362
  };
@@ -115,6 +115,18 @@ const RULE_RUST_X25519: RuleMeta = {
115
115
  cwe: CWE_BROKEN_CRYPTO,
116
116
  message: "X25519 (Rust) is modern but still classical key agreement — harvest-now-decrypt-later.",
117
117
  };
118
+ const RULE_RUST_X448: RuleMeta = {
119
+ id: "rust-x448",
120
+ title: "Rust X448 key agreement",
121
+ description: "the `x448` crate Secret key agreement",
122
+ category: "key-exchange",
123
+ severity: "medium",
124
+ confidence: "high",
125
+ algorithm: "X448",
126
+ hndl: true,
127
+ cwe: CWE_BROKEN_CRYPTO,
128
+ message: "X448 (Rust) is modern but still classical key agreement — harvest-now-decrypt-later.",
129
+ };
118
130
  const RULE_RUST_OPENSSL_RSA: RuleMeta = {
119
131
  id: "rust-openssl-rsa",
120
132
  title: "Rust openssl RSA key generation",
@@ -251,6 +263,51 @@ const RULE_RUST_TLS_DANGEROUS: RuleMeta = {
251
263
  remediation: "Avoid the dangerous() escape hatch; keep the default certificate verifier.",
252
264
  };
253
265
 
266
+ /**
267
+ * Aliasable Rust crypto types (`<crate>::<OrigType>` → rule). A renamed `use`
268
+ * (`use x25519_dalek::{EphemeralSecret as MontgomerySecret}`) binds the alias to a
269
+ * classical-key type; a later `MontgomerySecret::<ctor>(` is that key's
270
+ * construction. The `::`-qualified and braced rules above miss this because the
271
+ * call site uses the renamed identifier, not the original path.
272
+ */
273
+ const RUST_ALIASABLE: Record<string, RuleMeta> = {
274
+ "x25519_dalek::EphemeralSecret": RULE_RUST_X25519,
275
+ "x25519_dalek::StaticSecret": RULE_RUST_X25519,
276
+ "x448::Secret": RULE_RUST_X448,
277
+ "ed25519_dalek::SigningKey": RULE_RUST_ED25519,
278
+ "ed25519_dalek::Keypair": RULE_RUST_ED25519,
279
+ "ed25519_dalek::SecretKey": RULE_RUST_ED25519,
280
+ };
281
+
282
+ /** Escape a string for interpolation into a dynamically-built RegExp. */
283
+ function escapeRustRe(s: string): string {
284
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
285
+ }
286
+
287
+ /**
288
+ * Collect `use <crate>::<OrigType> as <Alias>` bindings (braced or single) that
289
+ * rename an aliasable crypto type, returning {alias, rule} pairs. An alias equal
290
+ * to its own type name is skipped (the qualified/bare rules already catch it).
291
+ */
292
+ function collectRustTypeAliases(content: string): Array<{ alias: string; rule: RuleMeta }> {
293
+ const out: Array<{ alias: string; rule: RuleMeta }> = [];
294
+ const push = (crate: string, orig: string, alias: string): void => {
295
+ if (!alias || alias === orig) return;
296
+ const rule = RUST_ALIASABLE[`${crate}::${orig}`];
297
+ if (rule) out.push({ alias, rule });
298
+ };
299
+ // Braced: `use <crate>::{ Orig as Alias, ... }`.
300
+ const braced = /\buse\s+([\w:]+)::\{([^}]*)\}/g;
301
+ for (let m = braced.exec(content); m; m = braced.exec(content)) {
302
+ const specRe = /([A-Za-z_]\w*)\s+as\s+([A-Za-z_]\w*)/g;
303
+ for (let s = specRe.exec(m[2]); s; s = specRe.exec(m[2])) push(m[1], s[1], s[2]);
304
+ }
305
+ // Single: `use <crate>::Orig as Alias;`.
306
+ const single = /\buse\s+([\w:]+)::([A-Za-z_]\w*)\s+as\s+([A-Za-z_]\w*)/g;
307
+ for (let m = single.exec(content); m; m = single.exec(content)) push(m[1], m[2], m[3]);
308
+ return out;
309
+ }
310
+
254
311
  /** Detects classical asymmetric crypto in Rust (rsa, ring, dalek, p256/k256). */
255
312
  export const rustDetector: Detector = {
256
313
  id: "rust-crypto",
@@ -263,6 +320,7 @@ export const rustDetector: Detector = {
263
320
  RULE_RUST_ECDH,
264
321
  RULE_RUST_ED25519,
265
322
  RULE_RUST_X25519,
323
+ RULE_RUST_X448,
266
324
  RULE_RUST_OPENSSL_RSA,
267
325
  RULE_RUST_OPENSSL_EC,
268
326
  RULE_RUST_OPENSSL_DSA,
@@ -311,6 +369,19 @@ export const rustDetector: Detector = {
311
369
  });
312
370
  add(RE_RUST_TLS_ACCEPT_INVALID, RULE_RUST_TLS_ACCEPT_INVALID);
313
371
  add(RE_RUST_TLS_DANGEROUS, RULE_RUST_TLS_DANGEROUS);
372
+
373
+ // Type-alias resolution: `use x25519_dalek::{EphemeralSecret as MontgomerySecret}`
374
+ // then `MontgomerySecret::random_from_rng(` — the braced+renamed `use` defeats
375
+ // the `::`-qualified rules. Match the aliased type's construction calls; runs
376
+ // on the ORIGINAL content so locations stay exact, and fires only for an alias
377
+ // explicitly bound to a known crypto type (precision-safe).
378
+ for (const { alias, rule } of collectRustTypeAliases(content)) {
379
+ const a = escapeRustRe(alias);
380
+ add(
381
+ new RegExp(`\\b${a}::(?:new|random|random_from_rng|generate|from_bytes)\\s*\\(`, "g"),
382
+ rule,
383
+ );
384
+ }
314
385
  return findings;
315
386
  },
316
387
  };