@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.
- package/dist/cbom-merge.d.ts +9 -0
- package/dist/cbom-merge.d.ts.map +1 -0
- package/dist/cbom-merge.js +99 -0
- package/dist/cbom-merge.js.map +1 -0
- package/dist/comments.d.ts +4 -3
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +53 -33
- package/dist/comments.js.map +1 -1
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +1 -1
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +5 -1
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +11 -2
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/c.d.ts +4 -2
- package/dist/detectors/c.d.ts.map +1 -1
- package/dist/detectors/c.js +192 -1
- package/dist/detectors/c.js.map +1 -1
- package/dist/detectors/cicd.d.ts +27 -0
- package/dist/detectors/cicd.d.ts.map +1 -0
- package/dist/detectors/cicd.js +116 -0
- package/dist/detectors/cicd.js.map +1 -0
- package/dist/detectors/cloud-kms.d.ts +21 -0
- package/dist/detectors/cloud-kms.d.ts.map +1 -0
- package/dist/detectors/cloud-kms.js +57 -0
- package/dist/detectors/cloud-kms.js.map +1 -0
- package/dist/detectors/cloudformation.d.ts +30 -0
- package/dist/detectors/cloudformation.d.ts.map +1 -0
- package/dist/detectors/cloudformation.js +170 -0
- package/dist/detectors/cloudformation.js.map +1 -0
- package/dist/detectors/database.d.ts +17 -0
- package/dist/detectors/database.d.ts.map +1 -0
- package/dist/detectors/database.js +60 -0
- package/dist/detectors/database.js.map +1 -0
- package/dist/detectors/dnssec.d.ts +55 -0
- package/dist/detectors/dnssec.d.ts.map +1 -0
- package/dist/detectors/dnssec.js +107 -0
- package/dist/detectors/dnssec.js.map +1 -0
- package/dist/detectors/elixir.d.ts +20 -0
- package/dist/detectors/elixir.d.ts.map +1 -0
- package/dist/detectors/elixir.js +195 -0
- package/dist/detectors/elixir.js.map +1 -0
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +1 -1
- package/dist/detectors/java.js.map +1 -1
- package/dist/detectors/jose.d.ts +20 -0
- package/dist/detectors/jose.d.ts.map +1 -0
- package/dist/detectors/jose.js +62 -0
- package/dist/detectors/jose.js.map +1 -0
- package/dist/detectors/jwk.d.ts +25 -0
- package/dist/detectors/jwk.d.ts.map +1 -0
- package/dist/detectors/jwk.js +95 -0
- package/dist/detectors/jwk.js.map +1 -0
- package/dist/detectors/k8s.d.ts +17 -0
- package/dist/detectors/k8s.d.ts.map +1 -0
- package/dist/detectors/k8s.js +85 -0
- package/dist/detectors/k8s.js.map +1 -0
- package/dist/detectors/mesh.d.ts +35 -0
- package/dist/detectors/mesh.d.ts.map +1 -0
- package/dist/detectors/mesh.js +109 -0
- package/dist/detectors/mesh.js.map +1 -0
- package/dist/detectors/messaging.d.ts +16 -0
- package/dist/detectors/messaging.d.ts.map +1 -0
- package/dist/detectors/messaging.js +72 -0
- package/dist/detectors/messaging.js.map +1 -0
- package/dist/detectors/pem.js +1 -1
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/php.d.ts +24 -0
- package/dist/detectors/php.d.ts.map +1 -0
- package/dist/detectors/php.js +167 -0
- package/dist/detectors/php.js.map +1 -0
- package/dist/detectors/python.d.ts.map +1 -1
- package/dist/detectors/python.js +70 -0
- package/dist/detectors/python.js.map +1 -1
- package/dist/detectors/rust.d.ts.map +1 -1
- package/dist/detectors/rust.js +68 -0
- package/dist/detectors/rust.js.map +1 -1
- package/dist/detectors/secrets.d.ts +21 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +79 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +190 -47
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/stateful-hbs.d.ts.map +1 -1
- package/dist/detectors/stateful-hbs.js +9 -6
- package/dist/detectors/stateful-hbs.js.map +1 -1
- package/dist/detectors/terraform.d.ts +21 -0
- package/dist/detectors/terraform.d.ts.map +1 -0
- package/dist/detectors/terraform.js +123 -0
- package/dist/detectors/terraform.js.map +1 -0
- package/dist/detectors/vpn.d.ts +22 -0
- package/dist/detectors/vpn.d.ts.map +1 -0
- package/dist/detectors/vpn.js +99 -0
- package/dist/detectors/vpn.js.map +1 -0
- package/dist/evidence.d.ts +5 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +5 -0
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts +67 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +30 -0
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +22 -1
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +7 -0
- package/dist/report.js.map +1 -1
- package/dist/standards.d.ts +96 -0
- package/dist/standards.d.ts.map +1 -0
- package/dist/standards.js +107 -0
- package/dist/standards.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +116 -0
- package/src/comments.ts +59 -33
- package/src/dependencies.ts +2 -1
- package/src/detect-utils.ts +14 -2
- package/src/detectors/c.ts +209 -3
- package/src/detectors/cicd.ts +158 -0
- package/src/detectors/cloud-kms.ts +87 -0
- package/src/detectors/cloudformation.ts +215 -0
- package/src/detectors/database.ts +88 -0
- package/src/detectors/dnssec.ts +186 -0
- package/src/detectors/elixir.ts +237 -0
- package/src/detectors/java.ts +2 -1
- package/src/detectors/jose.ts +94 -0
- package/src/detectors/jwk.ts +128 -0
- package/src/detectors/k8s.ts +112 -0
- package/src/detectors/mesh.ts +154 -0
- package/src/detectors/messaging.ts +98 -0
- package/src/detectors/pem.ts +1 -1
- package/src/detectors/php.ts +225 -0
- package/src/detectors/python.ts +71 -0
- package/src/detectors/rust.ts +71 -0
- package/src/detectors/secrets.ts +114 -0
- package/src/detectors/source.ts +235 -68
- package/src/detectors/stateful-hbs.ts +9 -6
- package/src/detectors/terraform.ts +154 -0
- package/src/detectors/vpn.ts +131 -0
- package/src/evidence.ts +11 -0
- package/src/index.ts +10 -0
- package/src/policy.ts +180 -0
- package/src/registry.ts +30 -0
- package/src/remediate-pipeline.ts +25 -2
- package/src/report.ts +7 -0
- package/src/standards.ts +186 -0
- package/src/types.ts +2 -0
- 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
|
+
};
|
package/src/detectors/pem.ts
CHANGED
|
@@ -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)
|
|
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
|
+
};
|
package/src/detectors/python.ts
CHANGED
|
@@ -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
|
};
|
package/src/detectors/rust.ts
CHANGED
|
@@ -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
|
};
|