@quantakrypto/core 0.4.2 → 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/LICENSE +201 -0
- package/README.md +38 -10
- package/dist/agent-types.js.map +1 -1
- package/dist/baseline.js.map +1 -1
- package/dist/cache.js.map +1 -1
- 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/cbom.js.map +1 -1
- package/dist/changed.js.map +1 -1
- package/dist/codemods/config-toggle.js.map +1 -1
- package/dist/codemods/registry.js.map +1 -1
- package/dist/comments.d.ts +29 -2
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +178 -18
- package/dist/comments.js.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/cwe.js.map +1 -1
- package/dist/dependencies.d.ts +3 -2
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +138 -2
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +19 -4
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +43 -6
- 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 +382 -3
- 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/csharp.d.ts.map +1 -1
- package/dist/detectors/csharp.js +141 -3
- package/dist/detectors/csharp.js.map +1 -1
- 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/go.d.ts +5 -3
- package/dist/detectors/go.d.ts.map +1 -1
- package/dist/detectors/go.js +187 -3
- package/dist/detectors/go.js.map +1 -1
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +169 -4
- 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.d.ts.map +1 -1
- package/dist/detectors/pem.js +50 -2
- 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 +144 -3
- package/dist/detectors/python.js.map +1 -1
- package/dist/detectors/ruby.d.ts +9 -5
- package/dist/detectors/ruby.d.ts.map +1 -1
- package/dist/detectors/ruby.js +122 -2
- package/dist/detectors/ruby.js.map +1 -1
- package/dist/detectors/rust.d.ts +8 -3
- package/dist/detectors/rust.d.ts.map +1 -1
- package/dist/detectors/rust.js +253 -3
- 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 +305 -52
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/stateful-hbs.d.ts +22 -0
- package/dist/detectors/stateful-hbs.d.ts.map +1 -0
- package/dist/detectors/stateful-hbs.js +142 -0
- package/dist/detectors/stateful-hbs.js.map +1 -0
- 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/errors.js.map +1 -1
- package/dist/evidence.d.ts +53 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +81 -0
- package/dist/evidence.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +2 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +27 -1
- package/dist/inventory.js.map +1 -1
- package/dist/parallel.js.map +1 -1
- package/dist/patch-policy.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/redact.d.ts.map +1 -1
- package/dist/redact.js +39 -0
- package/dist/redact.js.map +1 -1
- package/dist/registry.d.ts +2 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +34 -2
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +3 -0
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +71 -0
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/remediate-request.js.map +1 -1
- package/dist/remediation.d.ts +9 -3
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +17 -5
- package/dist/remediation.js.map +1 -1
- package/dist/report.d.ts +9 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +38 -0
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +8 -1
- package/dist/scan.js.map +1 -1
- package/dist/severity.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/triage.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/verify.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/dist/walk.js.map +1 -1
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +116 -0
- package/src/comments.ts +183 -18
- package/src/dependencies.ts +144 -4
- package/src/detect-utils.ts +47 -6
- package/src/detectors/c.ts +411 -5
- 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/csharp.ts +154 -3
- package/src/detectors/database.ts +88 -0
- package/src/detectors/dnssec.ts +186 -0
- package/src/detectors/elixir.ts +237 -0
- package/src/detectors/go.ts +209 -6
- package/src/detectors/java.ts +191 -4
- 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 +54 -2
- package/src/detectors/php.ts +225 -0
- package/src/detectors/python.ts +153 -3
- package/src/detectors/ruby.ts +138 -7
- package/src/detectors/rust.ts +282 -7
- package/src/detectors/secrets.ts +114 -0
- package/src/detectors/source.ts +382 -72
- package/src/detectors/stateful-hbs.ts +180 -0
- package/src/detectors/terraform.ts +154 -0
- package/src/detectors/vpn.ts +131 -0
- package/src/evidence.ts +135 -0
- package/src/index.ts +15 -1
- package/src/inventory.ts +31 -1
- package/src/policy.ts +180 -0
- package/src/redact.ts +41 -0
- package/src/registry.ts +34 -2
- package/src/remediate-pipeline.ts +78 -0
- package/src/remediation.ts +23 -8
- package/src/report.ts +48 -2
- package/src/scan.ts +13 -1
- package/src/standards.ts +186 -0
- package/src/types.ts +3 -1
- package/src/version.ts +1 -1
|
@@ -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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
24
24
|
import { PYTHON_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
25
|
-
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
25
|
+
import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
|
|
26
26
|
|
|
27
27
|
/* -------------------------------------------------------------------------- */
|
|
28
28
|
/* Precompiled regexes (module scope) */
|
|
@@ -38,8 +38,15 @@ const RE_PY_RSA_ENCRYPT = /\bpadding\.OAEP\s*\(|\bPKCS1_OAEP\.new\s*\(/g;
|
|
|
38
38
|
const RE_PY_EC_KEYGEN = /\bec\.generate_private_key\s*\(|\bECC\.generate\s*\(/g;
|
|
39
39
|
// ECDSA signatures: cryptography `ec.ECDSA(`, paramiko ECDSAKey.
|
|
40
40
|
const RE_PY_ECDSA = /\bec\.ECDSA\s*\(|\bparamiko\.ECDSAKey\b|\bECDSAKey\.generate\s*\(/g;
|
|
41
|
+
// ECDH key agreement: cryptography `ec.ECDH(` — the actual harvest-now event in
|
|
42
|
+
// `private_key.exchange(ec.ECDH(), peer)`; previously missed entirely (audit F3).
|
|
43
|
+
const RE_PY_ECDH = /\bec\.ECDH\s*\(/g;
|
|
41
44
|
// DSA: PyCryptodome `DSA.generate(`, paramiko DSSKey.
|
|
42
45
|
const RE_PY_DSA = /\bDSA\.generate\s*\(|\bparamiko\.DSSKey\b|\bDSSKey\.generate\s*\(/g;
|
|
46
|
+
// hazmat DSA: cryptography `dsa.generate_private_key(` — the `cryptography` DSA
|
|
47
|
+
// keygen path (lowercase module) that RE_PY_DSA's uppercase `DSA.generate` misses
|
|
48
|
+
// (audit F9-python).
|
|
49
|
+
const RE_PY_HAZMAT_DSA = /\bdsa\.generate_private_key\s*\(/g;
|
|
43
50
|
// Finite-field Diffie-Hellman (cryptography `dh`).
|
|
44
51
|
const RE_PY_DH = /\bdh\.generate_parameters\s*\(|\bdh\.DHParameterNumbers\s*\(/g;
|
|
45
52
|
// Modern-but-classical curve primitives (cryptography). `.generate(` anchors so
|
|
@@ -48,6 +55,15 @@ const RE_PY_X25519 = /\bX25519PrivateKey\.generate\s*\(/g;
|
|
|
48
55
|
const RE_PY_X448 = /\bX448PrivateKey\.generate\s*\(/g;
|
|
49
56
|
const RE_PY_EDDSA = /\b(?:Ed25519|Ed448)PrivateKey\.generate\s*\(|\bparamiko\.Ed25519Key\b/g;
|
|
50
57
|
|
|
58
|
+
// Python TLS misconfiguration. Mirrors the JS `tlsDetector` split of source.ts:
|
|
59
|
+
// certificate-verification bypass (requests `verify=False`, `ssl.CERT_NONE`,
|
|
60
|
+
// `check_hostname=False`, `ssl._create_unverified_context(`) is the high-severity
|
|
61
|
+
// MITM surface (CWE-295); a pinned legacy protocol (`ssl.PROTOCOL_TLSv1`) is the
|
|
62
|
+
// medium-severity weak-strength surface (CWE-326).
|
|
63
|
+
const RE_PY_TLS_REJECT =
|
|
64
|
+
/\bverify\s*=\s*False\b|\bssl\.CERT_NONE\b|\bcheck_hostname\s*=\s*False\b|\bssl\._create_unverified_context\s*\(/g;
|
|
65
|
+
const RE_PY_TLS_LEGACY = /\bPROTOCOL_TLSv1\b/g;
|
|
66
|
+
|
|
51
67
|
/* -------------------------------------------------------------------------- */
|
|
52
68
|
/* Rule catalog */
|
|
53
69
|
/* -------------------------------------------------------------------------- */
|
|
@@ -106,6 +122,19 @@ const RULE_PY_ECDSA: RuleMeta = {
|
|
|
106
122
|
message: "Classical ECDSA signing (Python) is forgeable by a quantum attacker.",
|
|
107
123
|
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
108
124
|
};
|
|
125
|
+
const RULE_PY_ECDH: RuleMeta = {
|
|
126
|
+
id: "python-ecdh",
|
|
127
|
+
title: "Python ECDH key agreement",
|
|
128
|
+
description: "cryptography ec.ECDH() exchange",
|
|
129
|
+
category: "key-exchange",
|
|
130
|
+
severity: "high",
|
|
131
|
+
confidence: "high",
|
|
132
|
+
algorithm: "ECDH",
|
|
133
|
+
hndl: true,
|
|
134
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
135
|
+
message:
|
|
136
|
+
"Elliptic-curve Diffie-Hellman key agreement (Python) is broken by Shor's algorithm (harvest-now-decrypt-later).",
|
|
137
|
+
};
|
|
109
138
|
const RULE_PY_DSA: RuleMeta = {
|
|
110
139
|
id: "python-dsa",
|
|
111
140
|
title: "Python DSA key/usage",
|
|
@@ -119,6 +148,20 @@ const RULE_PY_DSA: RuleMeta = {
|
|
|
119
148
|
message: "Classical DSA (Python) is deprecated and forgeable by a quantum attacker.",
|
|
120
149
|
remediation: "Rotate off DSA and migrate to ML-DSA-65 (FIPS 204).",
|
|
121
150
|
};
|
|
151
|
+
const RULE_PY_HAZMAT_DSA: RuleMeta = {
|
|
152
|
+
id: "python-hazmat-dsa",
|
|
153
|
+
title: "Python DSA key generation (cryptography)",
|
|
154
|
+
description: "cryptography dsa.generate_private_key",
|
|
155
|
+
category: "signature",
|
|
156
|
+
severity: "high",
|
|
157
|
+
confidence: "high",
|
|
158
|
+
algorithm: "DSA",
|
|
159
|
+
hndl: false,
|
|
160
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
161
|
+
message:
|
|
162
|
+
"cryptography dsa.generate_private_key (Python) creates a classical DSA key; DSA is deprecated and forgeable by a quantum attacker.",
|
|
163
|
+
remediation: "Rotate off DSA and migrate to ML-DSA-65 (FIPS 204).",
|
|
164
|
+
};
|
|
122
165
|
const RULE_PY_DH: RuleMeta = {
|
|
123
166
|
id: "python-dh",
|
|
124
167
|
title: "Python Diffie-Hellman key exchange",
|
|
@@ -137,7 +180,7 @@ const RULE_PY_X25519: RuleMeta = {
|
|
|
137
180
|
title: "Python X25519 key exchange",
|
|
138
181
|
description: "cryptography X25519PrivateKey.generate",
|
|
139
182
|
category: "key-exchange",
|
|
140
|
-
severity: "
|
|
183
|
+
severity: "medium",
|
|
141
184
|
confidence: "high",
|
|
142
185
|
algorithm: "X25519",
|
|
143
186
|
hndl: true,
|
|
@@ -150,7 +193,7 @@ const RULE_PY_X448: RuleMeta = {
|
|
|
150
193
|
title: "Python X448 key exchange",
|
|
151
194
|
description: "cryptography X448PrivateKey.generate",
|
|
152
195
|
category: "key-exchange",
|
|
153
|
-
severity: "
|
|
196
|
+
severity: "medium",
|
|
154
197
|
confidence: "high",
|
|
155
198
|
algorithm: "X448",
|
|
156
199
|
hndl: true,
|
|
@@ -169,6 +212,91 @@ const RULE_PY_EDDSA: RuleMeta = {
|
|
|
169
212
|
cwe: CWE_BROKEN_CRYPTO,
|
|
170
213
|
message: "Ed25519/Ed448 (Python) is a modern but still classical signature scheme.",
|
|
171
214
|
};
|
|
215
|
+
const RULE_PY_TLS_REJECT: RuleMeta = {
|
|
216
|
+
id: "python-tls-reject",
|
|
217
|
+
title: "Python TLS certificate verification disabled",
|
|
218
|
+
description:
|
|
219
|
+
"requests verify=False / ssl.CERT_NONE / check_hostname=False / _create_unverified_context",
|
|
220
|
+
category: "tls",
|
|
221
|
+
severity: "high",
|
|
222
|
+
confidence: "high",
|
|
223
|
+
hndl: false,
|
|
224
|
+
cwe: CWE_CERT_VALIDATION,
|
|
225
|
+
message:
|
|
226
|
+
"TLS certificate verification is disabled (verify=False / CERT_NONE / check_hostname=False / _create_unverified_context), which allows man-in-the-middle attacks.",
|
|
227
|
+
remediation:
|
|
228
|
+
"Enable certificate verification (verify=True, ssl.CERT_REQUIRED, check_hostname=True) and verify certificates properly.",
|
|
229
|
+
};
|
|
230
|
+
const RULE_PY_TLS_LEGACY: RuleMeta = {
|
|
231
|
+
id: "python-tls-legacy-version",
|
|
232
|
+
title: "Python legacy TLS version pinned",
|
|
233
|
+
description: "ssl.PROTOCOL_TLSv1 (TLS 1.0)",
|
|
234
|
+
category: "tls",
|
|
235
|
+
severity: "medium",
|
|
236
|
+
confidence: "high",
|
|
237
|
+
hndl: false,
|
|
238
|
+
cwe: CWE_WEAK_STRENGTH,
|
|
239
|
+
message: "TLS 1.0 (ssl.PROTOCOL_TLSv1) is deprecated and insecure; require TLS 1.3.",
|
|
240
|
+
remediation:
|
|
241
|
+
"Use ssl.PROTOCOL_TLS_CLIENT with minimum_version = ssl.TLSVersion.TLSv1_3 and prefer PQC-hybrid key exchange.",
|
|
242
|
+
};
|
|
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
|
+
}
|
|
172
300
|
|
|
173
301
|
/** Detects classical asymmetric crypto in Python source. */
|
|
174
302
|
export const pythonDetector: Detector = {
|
|
@@ -181,11 +309,15 @@ export const pythonDetector: Detector = {
|
|
|
181
309
|
RULE_PY_RSA_ENCRYPT,
|
|
182
310
|
RULE_PY_EC_KEYGEN,
|
|
183
311
|
RULE_PY_ECDSA,
|
|
312
|
+
RULE_PY_ECDH,
|
|
184
313
|
RULE_PY_DSA,
|
|
314
|
+
RULE_PY_HAZMAT_DSA,
|
|
185
315
|
RULE_PY_DH,
|
|
186
316
|
RULE_PY_X25519,
|
|
187
317
|
RULE_PY_X448,
|
|
188
318
|
RULE_PY_EDDSA,
|
|
319
|
+
RULE_PY_TLS_REJECT,
|
|
320
|
+
RULE_PY_TLS_LEGACY,
|
|
189
321
|
],
|
|
190
322
|
appliesTo: (f) => hasExtension(f, PYTHON_EXTENSIONS),
|
|
191
323
|
detect({ file, content }): Finding[] {
|
|
@@ -201,11 +333,29 @@ export const pythonDetector: Detector = {
|
|
|
201
333
|
add(RE_PY_RSA_ENCRYPT, RULE_PY_RSA_ENCRYPT);
|
|
202
334
|
add(RE_PY_EC_KEYGEN, RULE_PY_EC_KEYGEN);
|
|
203
335
|
add(RE_PY_ECDSA, RULE_PY_ECDSA);
|
|
336
|
+
add(RE_PY_ECDH, RULE_PY_ECDH);
|
|
204
337
|
add(RE_PY_DSA, RULE_PY_DSA);
|
|
338
|
+
add(RE_PY_HAZMAT_DSA, RULE_PY_HAZMAT_DSA);
|
|
205
339
|
add(RE_PY_DH, RULE_PY_DH);
|
|
206
340
|
add(RE_PY_X25519, RULE_PY_X25519);
|
|
207
341
|
add(RE_PY_X448, RULE_PY_X448);
|
|
208
342
|
add(RE_PY_EDDSA, RULE_PY_EDDSA);
|
|
343
|
+
add(RE_PY_TLS_REJECT, RULE_PY_TLS_REJECT);
|
|
344
|
+
add(RE_PY_TLS_LEGACY, RULE_PY_TLS_LEGACY);
|
|
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
|
+
}
|
|
209
359
|
|
|
210
360
|
return findings;
|
|
211
361
|
},
|
package/src/detectors/ruby.ts
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Source-code detector for classical asymmetric cryptography in Ruby. Ruby's
|
|
3
3
|
* asymmetric crypto is almost entirely `OpenSSL::PKey::{RSA,EC,DSA,DH}`, which
|
|
4
|
-
* makes the signals precise. Lexical, same strategy as the other packs.
|
|
4
|
+
* makes the signals precise. Lexical, same strategy as the other packs. Beyond
|
|
5
|
+
* keygen it also covers RSA public-key encryption on a loaded key, DH key
|
|
6
|
+
* agreement, the type-agnostic `OpenSSL::PKey.read` loader, Ed25519 key
|
|
7
|
+
* generation, and disabled TLS peer verification (`VERIFY_NONE`).
|
|
5
8
|
*
|
|
6
9
|
* HNDL: RSA (keygen/encryption) and (EC)DH key agreement are harvest-now-
|
|
7
|
-
* decrypt-later exposed (hndl:true); DSA signatures are hndl:false but
|
|
8
|
-
* `OpenSSL::PKey::EC` is ambiguous (feeds ECDSA + ECDH), so it is
|
|
9
|
-
* conservatively as key-exchange-capable (hndl:true), like the other
|
|
10
|
-
* rules.
|
|
10
|
+
* decrypt-later exposed (hndl:true); DSA / Ed25519 signatures are hndl:false but
|
|
11
|
+
* forgeable. `OpenSSL::PKey::EC` is ambiguous (feeds ECDSA + ECDH), so it is
|
|
12
|
+
* classified conservatively as key-exchange-capable (hndl:true), like the other
|
|
13
|
+
* EC keygen rules; `OpenSSL::PKey.read` loads a key of unknown type and is
|
|
14
|
+
* likewise treated conservatively.
|
|
11
15
|
*/
|
|
12
16
|
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
13
17
|
import { RUBY_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
14
|
-
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
18
|
+
import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION } from "../cwe.js";
|
|
15
19
|
|
|
16
20
|
const RE_RB_RSA = /\bOpenSSL::PKey::RSA\.(?:new|generate)\s*\(/g;
|
|
17
21
|
const RE_RB_EC = /\bOpenSSL::PKey::EC\.(?:new|generate)\s*\(/g;
|
|
18
22
|
const RE_RB_DSA = /\bOpenSSL::PKey::DSA\.(?:new|generate)\s*\(/g;
|
|
19
23
|
const RE_RB_DH = /\bOpenSSL::PKey::DH\.new\s*\(/g;
|
|
24
|
+
// RSA public-key encryption on an already-loaded key (audit F4-ruby / F9).
|
|
25
|
+
const RE_RB_RSA_CRYPT = /\.public_encrypt\b|\.private_decrypt\b/g;
|
|
26
|
+
// Finite-field DH shared-secret agreement.
|
|
27
|
+
const RE_RB_DH_AGREE = /\bdh_compute_key\s*\(/g;
|
|
28
|
+
// Type-agnostic key loader — could be RSA/EC/DSA/DH (conservative).
|
|
29
|
+
const RE_RB_PKEY_READ = /\bOpenSSL::PKey\.read\s*\(/g;
|
|
30
|
+
// Ed25519 signing key via the generic generate_key factory.
|
|
31
|
+
const RE_RB_ED25519 = /\bOpenSSL::PKey\.generate_key\s*\(\s*["']ED25519["']/g;
|
|
32
|
+
// `ed25519` gem — Ed25519::SigningKey / VerifyKey (signature keys).
|
|
33
|
+
const RE_RB_ED25519_GEM = /\bEd25519::(?:SigningKey|VerifyKey)\b/g;
|
|
34
|
+
// `rbnacl` (libsodium) — Curve25519/X25519 key agreement: PrivateKey, Box, GroupElement.
|
|
35
|
+
const RE_RB_RBNACL = /\bRbNaCl::(?:PrivateKey|Box|GroupElement)\b/g;
|
|
36
|
+
// TLS peer verification disabled (mirrors the JS tlsDetector rejectUnauthorized rule).
|
|
37
|
+
const RE_RB_TLS_VERIFY_NONE = /\bOpenSSL::SSL::VERIFY_NONE\b/g;
|
|
20
38
|
|
|
21
39
|
const RULE_RB_RSA: RuleMeta = {
|
|
22
40
|
id: "ruby-rsa",
|
|
@@ -71,6 +89,100 @@ const RULE_RB_DH: RuleMeta = {
|
|
|
71
89
|
message:
|
|
72
90
|
"Finite-field Diffie-Hellman (Ruby/OpenSSL) is broken by Shor's algorithm (harvest-now-decrypt-later).",
|
|
73
91
|
};
|
|
92
|
+
const RULE_RB_RSA_CRYPT: RuleMeta = {
|
|
93
|
+
id: "ruby-rsa-crypt",
|
|
94
|
+
title: "Ruby RSA public-key encryption",
|
|
95
|
+
description: "OpenSSL::PKey::RSA#public_encrypt / #private_decrypt",
|
|
96
|
+
category: "kem",
|
|
97
|
+
severity: "high",
|
|
98
|
+
confidence: "high",
|
|
99
|
+
algorithm: "RSA",
|
|
100
|
+
hndl: true,
|
|
101
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
102
|
+
message:
|
|
103
|
+
"RSA public-key encryption/decryption (Ruby/OpenSSL) is harvest-now-decrypt-later exposed.",
|
|
104
|
+
};
|
|
105
|
+
const RULE_RB_DH_AGREE: RuleMeta = {
|
|
106
|
+
id: "ruby-dh-agree",
|
|
107
|
+
title: "Ruby Diffie-Hellman key agreement",
|
|
108
|
+
description: "OpenSSL DH compute_key shared-secret agreement",
|
|
109
|
+
category: "key-exchange",
|
|
110
|
+
severity: "high",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
algorithm: "DH",
|
|
113
|
+
hndl: true,
|
|
114
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
115
|
+
message:
|
|
116
|
+
"Finite-field Diffie-Hellman key agreement (Ruby/OpenSSL) is broken by Shor's algorithm (harvest-now-decrypt-later).",
|
|
117
|
+
};
|
|
118
|
+
const RULE_RB_PKEY_READ: RuleMeta = {
|
|
119
|
+
id: "ruby-pkey-read",
|
|
120
|
+
title: "Ruby PKey loaded from serialized key",
|
|
121
|
+
description: "OpenSSL::PKey.read (type-agnostic key loader)",
|
|
122
|
+
category: "key-exchange",
|
|
123
|
+
severity: "high",
|
|
124
|
+
confidence: "medium",
|
|
125
|
+
algorithm: "unknown",
|
|
126
|
+
hndl: true,
|
|
127
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
128
|
+
message:
|
|
129
|
+
"Loads a classical asymmetric key of unknown type (RSA/EC/DSA/DH) via OpenSSL::PKey.read. Treated conservatively as key-exchange-capable (harvest-now-decrypt-later).",
|
|
130
|
+
remediation:
|
|
131
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
132
|
+
};
|
|
133
|
+
const RULE_RB_ED25519: RuleMeta = {
|
|
134
|
+
id: "ruby-ed25519",
|
|
135
|
+
title: "Ruby Ed25519 key generation",
|
|
136
|
+
description: 'OpenSSL::PKey.generate_key("ED25519")',
|
|
137
|
+
category: "signature",
|
|
138
|
+
severity: "low",
|
|
139
|
+
confidence: "high",
|
|
140
|
+
algorithm: "EdDSA",
|
|
141
|
+
hndl: false,
|
|
142
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
143
|
+
message:
|
|
144
|
+
"Generates an Ed25519 signing key (Ruby/OpenSSL) — modern but classical, and forgeable by a quantum attacker.",
|
|
145
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
146
|
+
};
|
|
147
|
+
const RULE_RB_ED25519_GEM: RuleMeta = {
|
|
148
|
+
id: "ruby-ed25519-gem",
|
|
149
|
+
title: "Ruby Ed25519 signature (ed25519 gem)",
|
|
150
|
+
description: "ed25519 gem Ed25519::SigningKey / Ed25519::VerifyKey",
|
|
151
|
+
category: "signature",
|
|
152
|
+
severity: "low",
|
|
153
|
+
confidence: "high",
|
|
154
|
+
algorithm: "EdDSA",
|
|
155
|
+
hndl: false,
|
|
156
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
157
|
+
message:
|
|
158
|
+
"Ed25519 signing/verification via the `ed25519` gem — modern but classical, and forgeable by a quantum attacker.",
|
|
159
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
160
|
+
};
|
|
161
|
+
const RULE_RB_RBNACL: RuleMeta = {
|
|
162
|
+
id: "ruby-rbnacl",
|
|
163
|
+
title: "Ruby X25519 key agreement (rbnacl)",
|
|
164
|
+
description: "rbnacl (libsodium) RbNaCl::PrivateKey / Box / GroupElement",
|
|
165
|
+
category: "key-exchange",
|
|
166
|
+
severity: "medium",
|
|
167
|
+
confidence: "high",
|
|
168
|
+
algorithm: "X25519",
|
|
169
|
+
hndl: true,
|
|
170
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
171
|
+
message:
|
|
172
|
+
"Curve25519/X25519 key agreement via the `rbnacl` gem (libsodium) — modern but classical key agreement, harvest-now-decrypt-later exposed.",
|
|
173
|
+
};
|
|
174
|
+
const RULE_RB_TLS_VERIFY_NONE: RuleMeta = {
|
|
175
|
+
id: "ruby-tls-verify-none",
|
|
176
|
+
title: "Ruby TLS certificate verification disabled",
|
|
177
|
+
description: "OpenSSL::SSL::VERIFY_NONE",
|
|
178
|
+
category: "tls",
|
|
179
|
+
severity: "high",
|
|
180
|
+
confidence: "high",
|
|
181
|
+
hndl: false,
|
|
182
|
+
cwe: CWE_CERT_VALIDATION,
|
|
183
|
+
message: "OpenSSL::SSL::VERIFY_NONE disables TLS peer certificate verification (MITM risk).",
|
|
184
|
+
remediation: "Use OpenSSL::SSL::VERIFY_PEER and verify the certificate chain.",
|
|
185
|
+
};
|
|
74
186
|
|
|
75
187
|
/** Detects classical asymmetric crypto in Ruby (OpenSSL::PKey). */
|
|
76
188
|
export const rubyDetector: Detector = {
|
|
@@ -78,7 +190,19 @@ export const rubyDetector: Detector = {
|
|
|
78
190
|
description: "Classical asymmetric crypto in Ruby (OpenSSL::PKey::{RSA,EC,DSA,DH})",
|
|
79
191
|
scope: "source",
|
|
80
192
|
language: "ruby",
|
|
81
|
-
rules: [
|
|
193
|
+
rules: [
|
|
194
|
+
RULE_RB_RSA,
|
|
195
|
+
RULE_RB_EC,
|
|
196
|
+
RULE_RB_DSA,
|
|
197
|
+
RULE_RB_DH,
|
|
198
|
+
RULE_RB_RSA_CRYPT,
|
|
199
|
+
RULE_RB_DH_AGREE,
|
|
200
|
+
RULE_RB_PKEY_READ,
|
|
201
|
+
RULE_RB_ED25519,
|
|
202
|
+
RULE_RB_ED25519_GEM,
|
|
203
|
+
RULE_RB_RBNACL,
|
|
204
|
+
RULE_RB_TLS_VERIFY_NONE,
|
|
205
|
+
],
|
|
82
206
|
appliesTo: (f) => hasExtension(f, RUBY_EXTENSIONS),
|
|
83
207
|
detect({ file, content }): Finding[] {
|
|
84
208
|
const findings: Finding[] = [];
|
|
@@ -92,6 +216,13 @@ export const rubyDetector: Detector = {
|
|
|
92
216
|
add(RE_RB_EC, RULE_RB_EC);
|
|
93
217
|
add(RE_RB_DSA, RULE_RB_DSA);
|
|
94
218
|
add(RE_RB_DH, RULE_RB_DH);
|
|
219
|
+
add(RE_RB_RSA_CRYPT, RULE_RB_RSA_CRYPT);
|
|
220
|
+
add(RE_RB_DH_AGREE, RULE_RB_DH_AGREE);
|
|
221
|
+
add(RE_RB_PKEY_READ, RULE_RB_PKEY_READ);
|
|
222
|
+
add(RE_RB_ED25519, RULE_RB_ED25519);
|
|
223
|
+
add(RE_RB_ED25519_GEM, RULE_RB_ED25519_GEM);
|
|
224
|
+
add(RE_RB_RBNACL, RULE_RB_RBNACL);
|
|
225
|
+
add(RE_RB_TLS_VERIFY_NONE, RULE_RB_TLS_VERIFY_NONE);
|
|
95
226
|
return findings;
|
|
96
227
|
},
|
|
97
228
|
};
|