@quantakrypto/core 0.4.3 → 0.5.0
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/README.md +76 -27
- package/dist/cbom-merge.d.ts +9 -0
- package/dist/cbom-merge.d.ts.map +1 -0
- package/dist/cbom-merge.js +121 -0
- package/dist/cbom-merge.js.map +1 -0
- package/dist/cbom.d.ts +5 -0
- package/dist/cbom.d.ts.map +1 -1
- package/dist/cbom.js +132 -37
- package/dist/cbom.js.map +1 -1
- package/dist/comments.d.ts +6 -26
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +77 -38
- 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 +59 -21
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +143 -8
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/age.d.ts +16 -0
- package/dist/detectors/age.d.ts.map +1 -0
- package/dist/detectors/age.js +40 -0
- package/dist/detectors/age.js.map +1 -0
- package/dist/detectors/ansible.d.ts +13 -0
- package/dist/detectors/ansible.d.ts.map +1 -0
- package/dist/detectors/ansible.js +108 -0
- package/dist/detectors/ansible.js.map +1 -0
- package/dist/detectors/bicep.d.ts +20 -0
- package/dist/detectors/bicep.d.ts.map +1 -0
- package/dist/detectors/bicep.js +74 -0
- package/dist/detectors/bicep.js.map +1 -0
- package/dist/detectors/c.d.ts +4 -2
- package/dist/detectors/c.d.ts.map +1 -1
- package/dist/detectors/c.js +222 -4
- 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 +129 -0
- package/dist/detectors/cicd.js.map +1 -0
- package/dist/detectors/cloud-kms.d.ts +24 -0
- package/dist/detectors/cloud-kms.d.ts.map +1 -0
- package/dist/detectors/cloud-kms.js +99 -0
- package/dist/detectors/cloud-kms.js.map +1 -0
- package/dist/detectors/cloudformation.d.ts +38 -0
- package/dist/detectors/cloudformation.d.ts.map +1 -0
- package/dist/detectors/cloudformation.js +201 -0
- package/dist/detectors/cloudformation.js.map +1 -0
- package/dist/detectors/codesign.d.ts +50 -0
- package/dist/detectors/codesign.d.ts.map +1 -0
- package/dist/detectors/codesign.js +143 -0
- package/dist/detectors/codesign.js.map +1 -0
- package/dist/detectors/csharp.d.ts.map +1 -1
- package/dist/detectors/csharp.js +13 -3
- package/dist/detectors/csharp.js.map +1 -1
- package/dist/detectors/dart.d.ts +35 -0
- package/dist/detectors/dart.d.ts.map +1 -0
- package/dist/detectors/dart.js +177 -0
- package/dist/detectors/dart.js.map +1 -0
- package/dist/detectors/database.d.ts +19 -0
- package/dist/detectors/database.d.ts.map +1 -0
- package/dist/detectors/database.js +97 -0
- package/dist/detectors/database.js.map +1 -0
- package/dist/detectors/dkim.d.ts +54 -0
- package/dist/detectors/dkim.d.ts.map +1 -0
- package/dist/detectors/dkim.js +92 -0
- package/dist/detectors/dkim.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 +116 -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 +227 -0
- package/dist/detectors/elixir.js.map +1 -0
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +34 -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 +72 -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 +131 -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 +88 -0
- package/dist/detectors/k8s.js.map +1 -0
- package/dist/detectors/keystore.d.ts +19 -0
- package/dist/detectors/keystore.d.ts.map +1 -0
- package/dist/detectors/keystore.js +76 -0
- package/dist/detectors/keystore.js.map +1 -0
- package/dist/detectors/mesh.d.ts +34 -0
- package/dist/detectors/mesh.d.ts.map +1 -0
- package/dist/detectors/mesh.js +91 -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 +79 -0
- package/dist/detectors/messaging.js.map +1 -0
- package/dist/detectors/objc.d.ts +41 -0
- package/dist/detectors/objc.d.ts.map +1 -0
- package/dist/detectors/objc.js +146 -0
- package/dist/detectors/objc.js.map +1 -0
- package/dist/detectors/openpgp.d.ts +20 -0
- package/dist/detectors/openpgp.d.ts.map +1 -0
- package/dist/detectors/openpgp.js +151 -0
- package/dist/detectors/openpgp.js.map +1 -0
- package/dist/detectors/pem.d.ts.map +1 -1
- package/dist/detectors/pem.js +33 -1
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/php.d.ts +25 -0
- package/dist/detectors/php.d.ts.map +1 -0
- package/dist/detectors/php.js +171 -0
- package/dist/detectors/php.js.map +1 -0
- package/dist/detectors/pkcs11.d.ts +17 -0
- package/dist/detectors/pkcs11.d.ts.map +1 -0
- package/dist/detectors/pkcs11.js +87 -0
- package/dist/detectors/pkcs11.js.map +1 -0
- package/dist/detectors/proxy.d.ts +52 -0
- package/dist/detectors/proxy.d.ts.map +1 -0
- package/dist/detectors/proxy.js +157 -0
- package/dist/detectors/proxy.js.map +1 -0
- package/dist/detectors/pulumi.d.ts +19 -0
- package/dist/detectors/pulumi.d.ts.map +1 -0
- package/dist/detectors/pulumi.js +80 -0
- package/dist/detectors/pulumi.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 +73 -0
- package/dist/detectors/rust.js.map +1 -1
- package/dist/detectors/secrets.d.ts +24 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +63 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/solidity.d.ts +50 -0
- package/dist/detectors/solidity.d.ts.map +1 -0
- package/dist/detectors/solidity.js +108 -0
- package/dist/detectors/solidity.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +243 -53
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/spire.d.ts +49 -0
- package/dist/detectors/spire.d.ts.map +1 -0
- package/dist/detectors/spire.js +77 -0
- package/dist/detectors/spire.js.map +1 -0
- package/dist/detectors/ssh-ca.d.ts +72 -0
- package/dist/detectors/ssh-ca.d.ts.map +1 -0
- package/dist/detectors/ssh-ca.js +119 -0
- package/dist/detectors/ssh-ca.js.map +1 -0
- 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/supply-chain.d.ts +17 -0
- package/dist/detectors/supply-chain.d.ts.map +1 -0
- package/dist/detectors/supply-chain.js +89 -0
- package/dist/detectors/supply-chain.js.map +1 -0
- package/dist/detectors/swift.d.ts +22 -0
- package/dist/detectors/swift.d.ts.map +1 -0
- package/dist/detectors/swift.js +120 -0
- package/dist/detectors/swift.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 +150 -0
- package/dist/detectors/terraform.js.map +1 -0
- package/dist/detectors/vault.d.ts +15 -0
- package/dist/detectors/vault.d.ts.map +1 -0
- package/dist/detectors/vault.js +97 -0
- package/dist/detectors/vault.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 +73 -0
- package/dist/detectors/vpn.js.map +1 -0
- package/dist/detectors/weak-hash.d.ts +71 -0
- package/dist/detectors/weak-hash.d.ts.map +1 -0
- package/dist/detectors/weak-hash.js +116 -0
- package/dist/detectors/weak-hash.js.map +1 -0
- package/dist/detectors/webauthn.d.ts +61 -0
- package/dist/detectors/webauthn.d.ts.map +1 -0
- package/dist/detectors/webauthn.js +109 -0
- package/dist/detectors/webauthn.js.map +1 -0
- package/dist/detectors/xmldsig.d.ts +23 -0
- package/dist/detectors/xmldsig.d.ts.map +1 -0
- package/dist/detectors/xmldsig.js +84 -0
- package/dist/detectors/xmldsig.js.map +1 -0
- package/dist/evidence.d.ts +79 -4
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +76 -0
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +17 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -9
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +0 -2
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +15 -6
- package/dist/inventory.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 +9 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +81 -4
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +0 -3
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +23 -2
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/remediation.d.ts +10 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +32 -0
- 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 -1
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js +5 -3
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +35 -6
- package/dist/scan.js.map +1 -1
- package/dist/standards-profiles.d.ts +58 -0
- package/dist/standards-profiles.d.ts.map +1 -0
- package/dist/standards-profiles.js +100 -0
- package/dist/standards-profiles.js.map +1 -0
- 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 +6 -5
- 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/dist/vex.d.ts +39 -0
- package/dist/vex.d.ts.map +1 -0
- package/dist/vex.js +87 -0
- package/dist/vex.js.map +1 -0
- package/dist/walk.d.ts +8 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +32 -1
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +146 -0
- package/src/cbom.ts +153 -39
- package/src/comments.ts +84 -39
- package/src/dependencies.ts +2 -1
- package/src/detect-utils.ts +146 -12
- package/src/detectors/age.ts +60 -0
- package/src/detectors/ansible.ts +140 -0
- package/src/detectors/bicep.ts +107 -0
- package/src/detectors/c.ts +240 -6
- package/src/detectors/cicd.ts +171 -0
- package/src/detectors/cloud-kms.ts +141 -0
- package/src/detectors/cloudformation.ts +250 -0
- package/src/detectors/codesign.ts +214 -0
- package/src/detectors/csharp.ts +13 -3
- package/src/detectors/dart.ts +237 -0
- package/src/detectors/database.ts +141 -0
- package/src/detectors/dkim.ts +172 -0
- package/src/detectors/dnssec.ts +195 -0
- package/src/detectors/elixir.ts +268 -0
- package/src/detectors/java.ts +39 -4
- package/src/detectors/jose.ts +110 -0
- package/src/detectors/jwk.ts +170 -0
- package/src/detectors/k8s.ts +115 -0
- package/src/detectors/keystore.ts +109 -0
- package/src/detectors/mesh.ts +130 -0
- package/src/detectors/messaging.ts +106 -0
- package/src/detectors/objc.ts +209 -0
- package/src/detectors/openpgp.ts +194 -0
- package/src/detectors/pem.ts +31 -1
- package/src/detectors/php.ts +230 -0
- package/src/detectors/pkcs11.ts +113 -0
- package/src/detectors/proxy.ts +236 -0
- package/src/detectors/pulumi.ts +106 -0
- package/src/detectors/python.ts +71 -0
- package/src/detectors/rust.ts +76 -0
- package/src/detectors/secrets.ts +103 -0
- package/src/detectors/solidity.ts +176 -0
- package/src/detectors/source.ts +283 -73
- package/src/detectors/spire.ts +143 -0
- package/src/detectors/ssh-ca.ts +215 -0
- package/src/detectors/stateful-hbs.ts +9 -6
- package/src/detectors/supply-chain.ts +117 -0
- package/src/detectors/swift.ts +149 -0
- package/src/detectors/terraform.ts +188 -0
- package/src/detectors/vault.ts +120 -0
- package/src/detectors/vpn.ts +103 -0
- package/src/detectors/weak-hash.ts +217 -0
- package/src/detectors/webauthn.ts +205 -0
- package/src/detectors/xmldsig.ts +115 -0
- package/src/evidence.ts +138 -4
- package/src/index.ts +50 -12
- package/src/inventory.ts +15 -7
- package/src/policy.ts +180 -0
- package/src/registry.ts +81 -4
- package/src/remediate-pipeline.ts +26 -3
- package/src/remediation.ts +39 -0
- package/src/report.ts +43 -1
- package/src/scan-worker.ts +5 -3
- package/src/scan.ts +41 -6
- package/src/standards-profiles.ts +136 -0
- package/src/standards.ts +186 -0
- package/src/types.ts +11 -4
- package/src/version.ts +1 -1
- package/src/vex.ts +122 -0
- package/src/walk.ts +33 -2
package/src/comments.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { Finding } from "./types.js";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
type CommentStyle = "c" | "hash";
|
|
20
20
|
|
|
21
21
|
/** C-style (line + block) comment languages, by extension. */
|
|
22
22
|
const C_LIKE: readonly string[] = [
|
|
@@ -41,45 +41,96 @@ const C_LIKE: readonly string[] = [
|
|
|
41
41
|
".cxx",
|
|
42
42
|
".hpp",
|
|
43
43
|
".hh",
|
|
44
|
+
// PHP and Scala also use C-style `//` + `/* */` comments (PHP additionally uses
|
|
45
|
+
// `#`, handled by the hash lexer running first would miss `//`; C-style covers both
|
|
46
|
+
// since PHP `//` is the common form and `#` lines are rare in modern PHP).
|
|
47
|
+
".php",
|
|
48
|
+
".php3",
|
|
49
|
+
".php4",
|
|
50
|
+
".php5",
|
|
51
|
+
".phtml",
|
|
52
|
+
".scala",
|
|
53
|
+
".sc",
|
|
54
|
+
".swift",
|
|
55
|
+
// Objective-C (.m/.mm) and Dart (.dart) both use C-style `//` + `/* */`.
|
|
56
|
+
".m",
|
|
57
|
+
".mm",
|
|
58
|
+
".dart",
|
|
59
|
+
// Smart-contract languages (Solidity/Move/Cairo) also use C-style comments.
|
|
60
|
+
".sol",
|
|
61
|
+
".move",
|
|
62
|
+
".cairo",
|
|
44
63
|
];
|
|
45
64
|
|
|
46
65
|
/** Hash-style (`#`) comment languages, by extension. */
|
|
47
|
-
const HASH_LIKE: readonly string[] = [".py", ".pyi", ".pyw", ".rb"];
|
|
66
|
+
const HASH_LIKE: readonly string[] = [".py", ".pyi", ".pyw", ".rb", ".ex", ".exs"];
|
|
48
67
|
|
|
49
68
|
/** The comment style for a file path, or null when we don't strip comments for it. */
|
|
50
|
-
|
|
69
|
+
function commentStyleForFile(file: string): CommentStyle | null {
|
|
51
70
|
const lower = file.toLowerCase();
|
|
52
71
|
if (C_LIKE.some((e) => lower.endsWith(e))) return "c";
|
|
53
72
|
if (HASH_LIKE.some((e) => lower.endsWith(e))) return "hash";
|
|
54
73
|
return null;
|
|
55
74
|
}
|
|
56
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Advance past a string / char literal starting at `content[i]` (a quote char);
|
|
78
|
+
* returns the index just after it. Two correctness rules a naive escape-scan got
|
|
79
|
+
* wrong (both verified as real bugs):
|
|
80
|
+
* - a `'` char literal must close within a short window (`'\u{10FFFF}'` is ~11
|
|
81
|
+
* chars); otherwise it is a Rust lifetime (`'a`) or a stray apostrophe, NOT a
|
|
82
|
+
* literal — return `i + 1` so the scan continues instead of consuming the
|
|
83
|
+
* rest of the file.
|
|
84
|
+
* - Go raw strings (backtick, when `rawBacktick`) do NOT process escapes, so a
|
|
85
|
+
* trailing backslash (`` `C:\` ``) must not swallow the closing delimiter.
|
|
86
|
+
*/
|
|
87
|
+
function skipQuoted(content: string, i: number, n: number, rawBacktick: boolean): number {
|
|
88
|
+
const quote = content[i];
|
|
89
|
+
if (quote === "'") {
|
|
90
|
+
const limit = Math.min(n, i + 1 + 12);
|
|
91
|
+
let j = i + 1;
|
|
92
|
+
while (j < limit) {
|
|
93
|
+
if (content[j] === "\\") {
|
|
94
|
+
j += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (content[j] === "'") return j + 1;
|
|
98
|
+
j++;
|
|
99
|
+
}
|
|
100
|
+
return i + 1; // a lifetime / stray apostrophe, not a char literal
|
|
101
|
+
}
|
|
102
|
+
const escapes = !(quote === "`" && rawBacktick);
|
|
103
|
+
let j = i + 1;
|
|
104
|
+
while (j < n) {
|
|
105
|
+
if (escapes && content[j] === "\\") {
|
|
106
|
+
j += 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (content[j] === quote) return j + 1;
|
|
110
|
+
j++;
|
|
111
|
+
}
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
|
|
57
115
|
/**
|
|
58
116
|
* Compute the comment spans (`[start, end)` offsets) of `content`, skipping over
|
|
59
117
|
* string literals so a comment marker inside a string is not treated as a
|
|
60
|
-
* comment. Spans are returned sorted and non-overlapping.
|
|
118
|
+
* comment. Spans are returned sorted and non-overlapping. `rawBacktick` (Go)
|
|
119
|
+
* makes backtick strings raw (no escapes).
|
|
61
120
|
*/
|
|
62
|
-
export function commentSpans(
|
|
121
|
+
export function commentSpans(
|
|
122
|
+
content: string,
|
|
123
|
+
style: CommentStyle,
|
|
124
|
+
rawBacktick = false,
|
|
125
|
+
): Array<[number, number]> {
|
|
63
126
|
const spans: Array<[number, number]> = [];
|
|
64
127
|
const n = content.length;
|
|
65
128
|
let i = 0;
|
|
66
129
|
while (i < n) {
|
|
67
130
|
const c = content[i];
|
|
68
|
-
// String /
|
|
131
|
+
// String / char literal: skip past it (lexically correct for lifetimes + raw strings).
|
|
69
132
|
if (c === '"' || c === "'" || c === "`") {
|
|
70
|
-
|
|
71
|
-
i++;
|
|
72
|
-
while (i < n) {
|
|
73
|
-
if (content[i] === "\\") {
|
|
74
|
-
i += 2;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (content[i] === quote) {
|
|
78
|
-
i++;
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
i++;
|
|
82
|
-
}
|
|
133
|
+
i = skipQuoted(content, i, n, rawBacktick);
|
|
83
134
|
continue;
|
|
84
135
|
}
|
|
85
136
|
if (style === "c" && c === "/" && content[i + 1] === "/") {
|
|
@@ -117,7 +168,7 @@ export function commentSpans(content: string, style: CommentStyle): Array<[numbe
|
|
|
117
168
|
* exempts `pem-*` rules. Comments and normal strings are skipped so a `"""`
|
|
118
169
|
* delimiter inside them is not mis-detected.
|
|
119
170
|
*/
|
|
120
|
-
|
|
171
|
+
function pythonDocstringSpans(content: string): Array<[number, number]> {
|
|
121
172
|
const spans: Array<[number, number]> = [];
|
|
122
173
|
const n = content.length;
|
|
123
174
|
let i = 0;
|
|
@@ -158,7 +209,7 @@ export function pythonDocstringSpans(content: string): Array<[number, number]> {
|
|
|
158
209
|
}
|
|
159
210
|
|
|
160
211
|
/** True if `offset` falls inside one of the (sorted, non-overlapping) spans. */
|
|
161
|
-
|
|
212
|
+
function offsetInSpans(spans: ReadonlyArray<[number, number]>, offset: number): boolean {
|
|
162
213
|
let lo = 0;
|
|
163
214
|
let hi = spans.length - 1;
|
|
164
215
|
while (lo <= hi) {
|
|
@@ -179,7 +230,7 @@ export function offsetInSpans(spans: ReadonlyArray<[number, number]>, offset: nu
|
|
|
179
230
|
* it is language-agnostic. `qscan-ignore-line` is not a substring of
|
|
180
231
|
* `qscan-ignore-next-line`, so the two never collide.
|
|
181
232
|
*/
|
|
182
|
-
|
|
233
|
+
function ignoredLines(content: string): Set<number> {
|
|
183
234
|
const ignored = new Set<number>();
|
|
184
235
|
const lines = content.split("\n");
|
|
185
236
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -210,7 +261,7 @@ export function stripCommentFindings(
|
|
|
210
261
|
if (findings.length === 0) return findings;
|
|
211
262
|
const style = commentStyleForFile(file);
|
|
212
263
|
if (!style) return findings;
|
|
213
|
-
const spans = commentSpans(content, style);
|
|
264
|
+
const spans = commentSpans(content, style, /\.go$/i.test(file));
|
|
214
265
|
// Python docstrings suppress prose *token* rules but keep real PEM material.
|
|
215
266
|
const docSpans = style === "hash" ? pythonDocstringSpans(content) : [];
|
|
216
267
|
if (spans.length === 0 && docSpans.length === 0) return findings;
|
|
@@ -239,7 +290,11 @@ export function stripCommentFindings(
|
|
|
239
290
|
* suppress findings from IDENTIFIER-only rules (e.g. a Go `SigningMethodRS256`
|
|
240
291
|
* mentioned inside an error-message string) — the mirror of {@link commentSpans}.
|
|
241
292
|
*/
|
|
242
|
-
export function stringSpans(
|
|
293
|
+
export function stringSpans(
|
|
294
|
+
content: string,
|
|
295
|
+
style: CommentStyle,
|
|
296
|
+
rawBacktick = false,
|
|
297
|
+
): Array<[number, number]> {
|
|
243
298
|
const spans: Array<[number, number]> = [];
|
|
244
299
|
const n = content.length;
|
|
245
300
|
let i = 0;
|
|
@@ -262,21 +317,11 @@ export function stringSpans(content: string, style: CommentStyle): Array<[number
|
|
|
262
317
|
continue;
|
|
263
318
|
}
|
|
264
319
|
if (c === '"' || c === "'" || c === "`") {
|
|
265
|
-
const quote = c;
|
|
266
320
|
const start = i;
|
|
267
|
-
i
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (content[i] === quote) {
|
|
274
|
-
i++;
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
i++;
|
|
278
|
-
}
|
|
279
|
-
spans.push([start, i]);
|
|
321
|
+
const end = skipQuoted(content, i, n, rawBacktick);
|
|
322
|
+
// A lone `'` (Rust lifetime, end === start+1) is not a string; skip it.
|
|
323
|
+
if (c !== "'" || end > start + 1) spans.push([start, end]);
|
|
324
|
+
i = end;
|
|
280
325
|
continue;
|
|
281
326
|
}
|
|
282
327
|
i++;
|
|
@@ -302,7 +347,7 @@ export function stripStringLiteralFindings(
|
|
|
302
347
|
if (findings.length === 0 || !findings.some((f) => ruleIds.has(f.ruleId))) return findings;
|
|
303
348
|
const style = commentStyleForFile(file);
|
|
304
349
|
if (!style) return findings;
|
|
305
|
-
const spans = stringSpans(content, style);
|
|
350
|
+
const spans = stringSpans(content, style, /\.go$/i.test(file));
|
|
306
351
|
if (spans.length === 0) return findings;
|
|
307
352
|
|
|
308
353
|
const lineStarts: number[] = [0];
|
package/src/dependencies.ts
CHANGED
|
@@ -501,7 +501,8 @@ export const vulnerableDependencies: VulnerableDependency[] = [
|
|
|
501
501
|
{
|
|
502
502
|
name: "github.com/cloudflare/circl",
|
|
503
503
|
ecosystem: "go",
|
|
504
|
-
reason:
|
|
504
|
+
reason:
|
|
505
|
+
"Cloudflare CIRCL — classical ECDH/EdDSA curves (X25519, X448, Ed25519, P-256); also ships PQC (ML-KEM/ML-DSA + hybrids), so migrate the classical *usage*, not the package.",
|
|
505
506
|
algorithms: ["ECDH", "EdDSA"],
|
|
506
507
|
severity: "medium",
|
|
507
508
|
},
|
package/src/detect-utils.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
import { remediationText } from "./remediation.js";
|
|
15
15
|
|
|
16
16
|
/** A 1-based line/column position derived from a character offset. */
|
|
17
|
-
|
|
17
|
+
interface LineCol {
|
|
18
18
|
line: number;
|
|
19
19
|
column: number;
|
|
20
20
|
}
|
|
@@ -62,14 +62,14 @@ function lineIndexFor(starts: readonly number[], offset: number): number {
|
|
|
62
62
|
* line/column. Newlines are LF; CR is treated as an ordinary character, so on
|
|
63
63
|
* CRLF files the column includes the trailing CR offset harmlessly.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
function offsetToLineCol(content: string, offset: number): LineCol {
|
|
66
66
|
const starts = lineStartsFor(content);
|
|
67
67
|
const idx = lineIndexFor(starts, offset);
|
|
68
68
|
return { line: idx + 1, column: offset - starts[idx] + 1 };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** Extract the (trimmed) single source line containing `offset`. */
|
|
72
|
-
|
|
72
|
+
function lineAt(content: string, offset: number): string {
|
|
73
73
|
const starts = lineStartsFor(content);
|
|
74
74
|
const idx = lineIndexFor(starts, offset);
|
|
75
75
|
const start = starts[idx];
|
|
@@ -80,7 +80,7 @@ export function lineAt(content: string, offset: number): string {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** Inputs for {@link makeFinding}. */
|
|
83
|
-
|
|
83
|
+
interface FindingSpec {
|
|
84
84
|
ruleId: string;
|
|
85
85
|
title: string;
|
|
86
86
|
category: FindingCategory;
|
|
@@ -146,7 +146,7 @@ export function makeFinding(spec: FindingSpec): Finding {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** Where a match occurred, plus optional per-finding field overrides. */
|
|
149
|
-
|
|
149
|
+
interface RuleMatch {
|
|
150
150
|
file: string;
|
|
151
151
|
content: string;
|
|
152
152
|
/** Match start offset within `content`. */
|
|
@@ -160,7 +160,7 @@ export interface RuleMatch {
|
|
|
160
160
|
* rules (e.g. key generation across algorithm families) override these; fixed
|
|
161
161
|
* rules pass none and inherit the catalog metadata verbatim.
|
|
162
162
|
*/
|
|
163
|
-
|
|
163
|
+
type RuleOverrides = Partial<
|
|
164
164
|
Pick<
|
|
165
165
|
RuleMeta,
|
|
166
166
|
"title" | "category" | "severity" | "confidence" | "algorithm" | "hndl" | "message" | "cwe"
|
|
@@ -223,7 +223,10 @@ export const PYTHON_EXTENSIONS: readonly string[] = [".py", ".pyi", ".pyw"];
|
|
|
223
223
|
export const GO_EXTENSIONS: readonly string[] = [".go"];
|
|
224
224
|
|
|
225
225
|
/** Java / Kotlin source extensions handled by the JCA detector. */
|
|
226
|
-
|
|
226
|
+
// The JVM detector also covers Kotlin (`.kt`/`.kts`) and Scala (`.scala`/`.sc`):
|
|
227
|
+
// all three compile against the same JCA (`KeyPairGenerator`, `Signature`,
|
|
228
|
+
// `KeyAgreement`) and BouncyCastle APIs the Java rules match.
|
|
229
|
+
export const JAVA_EXTENSIONS: readonly string[] = [".java", ".kt", ".kts", ".scala", ".sc"];
|
|
227
230
|
|
|
228
231
|
/** C# source extensions handled by the .NET detector. */
|
|
229
232
|
export const CSHARP_EXTENSIONS: readonly string[] = [".cs"];
|
|
@@ -234,9 +237,27 @@ export const RUST_EXTENSIONS: readonly string[] = [".rs"];
|
|
|
234
237
|
/** Ruby source extensions handled by the Ruby detector. */
|
|
235
238
|
export const RUBY_EXTENSIONS: readonly string[] = [".rb"];
|
|
236
239
|
|
|
240
|
+
/** Elixir source extensions handled by the Elixir detector. */
|
|
241
|
+
export const ELIXIR_EXTENSIONS: readonly string[] = [".ex", ".exs"];
|
|
242
|
+
|
|
243
|
+
/** PHP source extensions handled by the PHP detector. */
|
|
244
|
+
export const PHP_EXTENSIONS: readonly string[] = [".php", ".phtml", ".php3", ".php4", ".php5"];
|
|
245
|
+
|
|
237
246
|
/** C / C++ source extensions handled by the OpenSSL detector. */
|
|
238
247
|
export const C_EXTENSIONS: readonly string[] = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"];
|
|
239
248
|
|
|
249
|
+
/** Swift source extensions handled by the CryptoKit / Security-framework detector. */
|
|
250
|
+
export const SWIFT_EXTENSIONS: readonly string[] = [".swift"];
|
|
251
|
+
|
|
252
|
+
/** Objective-C implementation extensions (`.h` is owned by the C pack). */
|
|
253
|
+
export const OBJC_EXTENSIONS: readonly string[] = [".m", ".mm"];
|
|
254
|
+
|
|
255
|
+
/** Dart / Flutter source extensions. */
|
|
256
|
+
export const DART_EXTENSIONS: readonly string[] = [".dart"];
|
|
257
|
+
|
|
258
|
+
/** Smart-contract source extensions (Solidity/EVM, Move, Cairo/Starknet). */
|
|
259
|
+
export const SMART_CONTRACT_EXTENSIONS: readonly string[] = [".sol", ".move", ".cairo"];
|
|
260
|
+
|
|
240
261
|
/**
|
|
241
262
|
* Prose/documentation extensions. The language-agnostic *token* detectors (SSH
|
|
242
263
|
* public keys, TLS cipher suites, certificate signature algorithms) must not run
|
|
@@ -273,6 +294,9 @@ export const JWT_HOST_EXTENSIONS: readonly string[] = [
|
|
|
273
294
|
...PYTHON_EXTENSIONS,
|
|
274
295
|
...GO_EXTENSIONS,
|
|
275
296
|
...RUBY_EXTENSIONS,
|
|
297
|
+
// PHP's firebase/php-jwt passes the alg as a quoted token (`'RS256'`, `'ES256'`),
|
|
298
|
+
// the same shape RE_JWT_ALG matches — so PHP files are in scope for it too.
|
|
299
|
+
...PHP_EXTENSIONS,
|
|
276
300
|
];
|
|
277
301
|
|
|
278
302
|
/**
|
|
@@ -291,7 +315,13 @@ export const ANALYZABLE_SOURCE_EXTENSIONS: readonly string[] = [
|
|
|
291
315
|
...CSHARP_EXTENSIONS,
|
|
292
316
|
...RUST_EXTENSIONS,
|
|
293
317
|
...RUBY_EXTENSIONS,
|
|
318
|
+
...PHP_EXTENSIONS,
|
|
319
|
+
...ELIXIR_EXTENSIONS,
|
|
294
320
|
...C_EXTENSIONS,
|
|
321
|
+
...SWIFT_EXTENSIONS,
|
|
322
|
+
...OBJC_EXTENSIONS,
|
|
323
|
+
...DART_EXTENSIONS,
|
|
324
|
+
...SMART_CONTRACT_EXTENSIONS,
|
|
295
325
|
];
|
|
296
326
|
|
|
297
327
|
/**
|
|
@@ -299,7 +329,8 @@ export const ANALYZABLE_SOURCE_EXTENSIONS: readonly string[] = [
|
|
|
299
329
|
* crypto, shown in coverage output. Kept next to {@link
|
|
300
330
|
* ANALYZABLE_SOURCE_EXTENSIONS} so a new language pack updates one place.
|
|
301
331
|
*/
|
|
302
|
-
export const ANALYZABLE_LANGUAGES_LABEL =
|
|
332
|
+
export const ANALYZABLE_LANGUAGES_LABEL =
|
|
333
|
+
"JS/TS, Python, Go, Java/Kotlin/Scala, C#, Rust, Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart, Solidity/Move/Cairo";
|
|
303
334
|
|
|
304
335
|
/** True when a path is in a source language the scanner can analyze for crypto. */
|
|
305
336
|
export function isAnalyzableSource(filePath: string): boolean {
|
|
@@ -307,10 +338,13 @@ export function isAnalyzableSource(filePath: string): boolean {
|
|
|
307
338
|
}
|
|
308
339
|
|
|
309
340
|
/**
|
|
310
|
-
* Given a SORTED ascending array of call offsets, return true when `idx` is at
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
341
|
+
* Given a SORTED ascending array of call offsets, return true when `idx` is at or
|
|
342
|
+
* after some call offset `c` with `idx - c < window`. Deliberately ONE-SIDED
|
|
343
|
+
* (forward only): the WebCrypto algorithm name follows the `subtle.*(` call it
|
|
344
|
+
* belongs to, and reaching backward would wrongly attribute an UNRELATED earlier
|
|
345
|
+
* call's argument (e.g. a Node `generateKeyPairSync('rsa-pss', …)` sitting a few
|
|
346
|
+
* lines above a subtle call) to the wrong API. Runs in O(log n) by binary-searching
|
|
347
|
+
* the largest call offset ≤ `idx` and checking the gap.
|
|
314
348
|
*/
|
|
315
349
|
export function nearSortedCall(
|
|
316
350
|
sortedCalls: readonly number[],
|
|
@@ -351,3 +385,103 @@ export function eachMatch(
|
|
|
351
385
|
if (m.index === g.lastIndex) g.lastIndex++; // avoid infinite loop on empty match
|
|
352
386
|
}
|
|
353
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Return the innermost `{ … }` object that contains `index`, as a substring — so a
|
|
391
|
+
* per-match check (e.g. a JWK's own `use`/`alg`, or whether an `alg` sits inside a
|
|
392
|
+
* JWK) analyses ONLY that object and can't be contaminated by a neighbouring object
|
|
393
|
+
* in a packed array. The scan is STRING-AWARE (braces and quotes inside JSON string
|
|
394
|
+
* values, honouring `\"`, are ignored), so a value like `"a}b{c"` can't mis-scope the
|
|
395
|
+
* object. Bounded by `maxSpan` each way so a pathological input can't blow up; falls
|
|
396
|
+
* back to a bounded ±window when no enclosing object is found in range.
|
|
397
|
+
*/
|
|
398
|
+
export function enclosingObject(content: string, index: number, maxSpan = 4000): string {
|
|
399
|
+
const lo = Math.max(0, index - maxSpan);
|
|
400
|
+
const hi = Math.min(content.length, index + maxSpan);
|
|
401
|
+
const stack: number[] = []; // positions of currently-open `{`
|
|
402
|
+
let inStr = false;
|
|
403
|
+
let esc = false;
|
|
404
|
+
let enclosingOpen = -1; // innermost `{` open at `index`
|
|
405
|
+
for (let i = lo; i < hi; i++) {
|
|
406
|
+
const c = content[i];
|
|
407
|
+
if (inStr) {
|
|
408
|
+
if (esc) esc = false;
|
|
409
|
+
else if (c === "\\") esc = true;
|
|
410
|
+
else if (c === '"') inStr = false;
|
|
411
|
+
} else if (c === '"') {
|
|
412
|
+
inStr = true;
|
|
413
|
+
} else if (c === "{") {
|
|
414
|
+
stack.push(i);
|
|
415
|
+
} else if (c === "}") {
|
|
416
|
+
const open = stack.pop();
|
|
417
|
+
if (open !== undefined && open === enclosingOpen) {
|
|
418
|
+
return content.slice(enclosingOpen, i + 1); // closed the enclosing object
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (i === index && enclosingOpen < 0 && stack.length > 0) {
|
|
422
|
+
enclosingOpen = stack[stack.length - 1];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// No complete enclosing object in range: if we found an (unclosed) enclosing open
|
|
426
|
+
// brace, return from it to hi. Otherwise the token is not inside any object (top-level
|
|
427
|
+
// token / brace-less config like YAML) — return a bounded ±250 window, NOT the full
|
|
428
|
+
// forward span, so a `"kty"` far AFTER the token can't pull an unrelated JWK into
|
|
429
|
+
// scope and over-suppress a legitimate standalone finding.
|
|
430
|
+
if (enclosingOpen >= 0) return content.slice(enclosingOpen, hi);
|
|
431
|
+
return content.slice(Math.max(0, index - 250), Math.min(content.length, index + 250));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Blank out FULL-LINE comments so a commented-out directive isn't reported as an
|
|
436
|
+
* active setting. A line counts as a comment when its first non-whitespace characters
|
|
437
|
+
* match one of `markers` (e.g. `#`, `//`, `;`, `!`). Each such line's characters are
|
|
438
|
+
* replaced with spaces of the SAME length (newlines preserved), so every byte offset
|
|
439
|
+
* is unchanged — the finding line/column/snippet for the non-comment lines that remain
|
|
440
|
+
* stay exactly correct. Inline trailing comments are intentionally left alone (the
|
|
441
|
+
* directive before them is still active, so it should still be flagged).
|
|
442
|
+
*
|
|
443
|
+
* Config detectors run their rules over the masked content; matches can then only land
|
|
444
|
+
* on live config, not on commented examples (mosquitto.conf, CI YAML, ipsec.conf, and
|
|
445
|
+
* HCL all conventionally ship large blocks of commented-out directives).
|
|
446
|
+
*/
|
|
447
|
+
export function maskCommentLines(content: string, markers: readonly string[]): string {
|
|
448
|
+
if (markers.length === 0) return content;
|
|
449
|
+
const lines = content.split("\n");
|
|
450
|
+
for (let i = 0; i < lines.length; i++) {
|
|
451
|
+
const line = lines[i];
|
|
452
|
+
const lead = line.trimStart();
|
|
453
|
+
if (lead !== "" && markers.some((mk) => lead.startsWith(mk))) {
|
|
454
|
+
lines[i] = " ".repeat(line.length);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return lines.join("\n");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Blank out C-style block comments (the `slash-star … star-slash` form, multi-line
|
|
462
|
+
* included), replacing every
|
|
463
|
+
* character with a space and preserving newlines so byte offsets — and therefore
|
|
464
|
+
* finding line/column — stay exact. Complements {@link maskCommentLines}, which only
|
|
465
|
+
* masks whole single-line comments: an HCL/Bicep resource wrapped in a block comment
|
|
466
|
+
* has its inner lines mid-block (they don't START with `/*`), so a line-only masker
|
|
467
|
+
* leaves them live. Not string-aware — a literal `/*` inside a config string value is
|
|
468
|
+
* rare and, for these token detectors, a tolerable over-mask.
|
|
469
|
+
*/
|
|
470
|
+
export function maskBlockComments(content: string): string {
|
|
471
|
+
if (!content.includes("/*")) return content;
|
|
472
|
+
const out: string[] = [];
|
|
473
|
+
let i = 0;
|
|
474
|
+
const n = content.length;
|
|
475
|
+
while (i < n) {
|
|
476
|
+
if (content[i] === "/" && content[i + 1] === "*") {
|
|
477
|
+
const end = content.indexOf("*/", i + 2);
|
|
478
|
+
const stop = end === -1 ? n : end + 2;
|
|
479
|
+
for (let j = i; j < stop; j++) out.push(content[j] === "\n" ? "\n" : " ");
|
|
480
|
+
i = stop;
|
|
481
|
+
} else {
|
|
482
|
+
out.push(content[i]);
|
|
483
|
+
i++;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return out.join("");
|
|
487
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: age IDENTITY (private) keys committed to disk. The secrets
|
|
3
|
+
* detector flags age *recipients* (`age1…` public keys); this flags the far worse
|
|
4
|
+
* case — an `AGE-SECRET-KEY-1…` private identity checked into a repo or config.
|
|
5
|
+
* age identities are X25519 secret keys: a classical key-agreement secret that,
|
|
6
|
+
* once leaked or committed, lets an attacker decrypt every age-wrapped payload
|
|
7
|
+
* addressed to it (harvest-now-decrypt-later, and retroactively un-fixable if the
|
|
8
|
+
* ciphertext is already in git history).
|
|
9
|
+
*
|
|
10
|
+
* The matched value IS the private key, so it is marked sensitive and reporters
|
|
11
|
+
* drop the snippet.
|
|
12
|
+
*/
|
|
13
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
14
|
+
import { eachMatch, findingFromRule } from "../detect-utils.js";
|
|
15
|
+
import { CWE_HARDCODED_KEY } from "../cwe.js";
|
|
16
|
+
|
|
17
|
+
// age secret keys are Bech32 in uppercase: `AGE-SECRET-KEY-1` + data part.
|
|
18
|
+
const RE_AGE_SECRET = /\bAGE-SECRET-KEY-1[0-9A-Z]{50,}\b/g;
|
|
19
|
+
|
|
20
|
+
const RULE_AGE_SECRET: RuleMeta = {
|
|
21
|
+
id: "age-secret-key",
|
|
22
|
+
title: "age identity (X25519 private key)",
|
|
23
|
+
description: "An age AGE-SECRET-KEY-1 private identity key committed to disk",
|
|
24
|
+
category: "key-exchange",
|
|
25
|
+
severity: "high",
|
|
26
|
+
confidence: "high",
|
|
27
|
+
algorithm: "X25519",
|
|
28
|
+
hndl: true,
|
|
29
|
+
cwe: CWE_HARDCODED_KEY,
|
|
30
|
+
sensitive: true,
|
|
31
|
+
message:
|
|
32
|
+
"An age identity (X25519 private key) is committed to disk; every age-wrapped payload addressed to it is decryptable by anyone with this key, and the exposure is retroactive if the ciphertext is already in git history.",
|
|
33
|
+
remediation:
|
|
34
|
+
"Rotate the age identity and re-encrypt affected data; move the private key to a secret manager. Plan for a post-quantum KEM (ML-KEM) recipient when available.",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Detects committed age identity (private) keys. */
|
|
38
|
+
export const ageDetector: Detector = {
|
|
39
|
+
id: "age-identity",
|
|
40
|
+
description: "Committed age identity (X25519 private) keys",
|
|
41
|
+
scope: "config",
|
|
42
|
+
language: "any",
|
|
43
|
+
rules: [RULE_AGE_SECRET],
|
|
44
|
+
appliesTo: () => true,
|
|
45
|
+
detect({ file, content }): Finding[] {
|
|
46
|
+
if (!content.includes("AGE-SECRET-KEY-1")) return [];
|
|
47
|
+
const findings: Finding[] = [];
|
|
48
|
+
eachMatch(RE_AGE_SECRET, content, (m) =>
|
|
49
|
+
findings.push(
|
|
50
|
+
findingFromRule(RULE_AGE_SECRET, {
|
|
51
|
+
file,
|
|
52
|
+
content,
|
|
53
|
+
index: m.index,
|
|
54
|
+
matchLength: m[0].length,
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
return findings;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical asymmetric keys provisioned by Ansible's
|
|
3
|
+
* `community.crypto` collection. Playbooks that mint TLS/SSH key material with
|
|
4
|
+
* `openssl_privatekey` / `openssl_csr` / `x509_certificate` provision real
|
|
5
|
+
* classical keys the language packs never see — the IaC surface, in YAML.
|
|
6
|
+
*
|
|
7
|
+
* Gated to a `community.crypto` / `openssl_privatekey` marker so a bare
|
|
8
|
+
* `type: RSA` in unrelated YAML does not fire.
|
|
9
|
+
*/
|
|
10
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
11
|
+
import { eachMatch, findingFromRule, hasExtension, maskCommentLines } from "../detect-utils.js";
|
|
12
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
13
|
+
|
|
14
|
+
const YAML_EXTENSIONS: readonly string[] = [".yml", ".yaml"];
|
|
15
|
+
|
|
16
|
+
const RE_ANSIBLE_RSA = /\btype:\s*["']?RSA\b/g;
|
|
17
|
+
const RE_ANSIBLE_ECC = /\btype:\s*["']?ECC\b/g;
|
|
18
|
+
// community.crypto also supports these `type:` values, all classical.
|
|
19
|
+
const RE_ANSIBLE_EDDSA = /\btype:\s*["']?Ed(?:25519|448)\b/g;
|
|
20
|
+
const RE_ANSIBLE_XDH = /\btype:\s*["']?X(?:25519|448)\b/g;
|
|
21
|
+
const RE_ANSIBLE_DSA = /\btype:\s*["']?DSA\b/g;
|
|
22
|
+
|
|
23
|
+
const RULE_ANSIBLE_RSA: RuleMeta = {
|
|
24
|
+
id: "ansible-openssl-rsa",
|
|
25
|
+
title: "Ansible community.crypto RSA key",
|
|
26
|
+
description: "Ansible community.crypto openssl_privatekey/csr with type: RSA",
|
|
27
|
+
category: "kem",
|
|
28
|
+
severity: "medium",
|
|
29
|
+
confidence: "high",
|
|
30
|
+
algorithm: "RSA",
|
|
31
|
+
hndl: true,
|
|
32
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
33
|
+
message: "Ansible provisions a classical RSA key (community.crypto), which is not quantum-safe.",
|
|
34
|
+
remediation:
|
|
35
|
+
"Plan migration to PQC keys (ML-KEM-768 / ML-DSA-65) as the collection adds support.",
|
|
36
|
+
};
|
|
37
|
+
const RULE_ANSIBLE_ECC: RuleMeta = {
|
|
38
|
+
id: "ansible-openssl-ecc",
|
|
39
|
+
title: "Ansible community.crypto EC key",
|
|
40
|
+
description: "Ansible community.crypto openssl_privatekey/csr with type: ECC",
|
|
41
|
+
category: "key-exchange",
|
|
42
|
+
severity: "medium",
|
|
43
|
+
confidence: "high",
|
|
44
|
+
algorithm: "ECDH",
|
|
45
|
+
hndl: true,
|
|
46
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
47
|
+
message:
|
|
48
|
+
"Ansible provisions a classical EC key (community.crypto); EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
|
|
49
|
+
remediation:
|
|
50
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const RULE_ANSIBLE_EDDSA: RuleMeta = {
|
|
54
|
+
id: "ansible-openssl-eddsa",
|
|
55
|
+
title: "Ansible community.crypto Ed25519/Ed448 key",
|
|
56
|
+
description: "Ansible community.crypto openssl_privatekey with type: Ed25519/Ed448",
|
|
57
|
+
category: "signature",
|
|
58
|
+
severity: "medium",
|
|
59
|
+
confidence: "high",
|
|
60
|
+
algorithm: "EdDSA",
|
|
61
|
+
hndl: false,
|
|
62
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
63
|
+
message:
|
|
64
|
+
"Ansible provisions a classical Ed25519/Ed448 key (community.crypto), forgeable by a quantum attacker.",
|
|
65
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
66
|
+
};
|
|
67
|
+
const RULE_ANSIBLE_XDH: RuleMeta = {
|
|
68
|
+
id: "ansible-openssl-xdh",
|
|
69
|
+
title: "Ansible community.crypto X25519/X448 key",
|
|
70
|
+
description: "Ansible community.crypto openssl_privatekey with type: X25519/X448",
|
|
71
|
+
category: "key-exchange",
|
|
72
|
+
severity: "medium",
|
|
73
|
+
confidence: "high",
|
|
74
|
+
algorithm: "X25519",
|
|
75
|
+
hndl: true,
|
|
76
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
77
|
+
message:
|
|
78
|
+
"Ansible provisions a classical X25519/X448 key-agreement key (community.crypto), which is harvest-now-decrypt-later exposed.",
|
|
79
|
+
remediation: "Migrate key agreement to hybrid X25519MLKEM768 (ML-KEM-768).",
|
|
80
|
+
};
|
|
81
|
+
const RULE_ANSIBLE_DSA: RuleMeta = {
|
|
82
|
+
id: "ansible-openssl-dsa",
|
|
83
|
+
title: "Ansible community.crypto DSA key (deprecated)",
|
|
84
|
+
description: "Ansible community.crypto openssl_privatekey with type: DSA",
|
|
85
|
+
category: "signature",
|
|
86
|
+
severity: "medium",
|
|
87
|
+
confidence: "high",
|
|
88
|
+
algorithm: "DSA",
|
|
89
|
+
hndl: false,
|
|
90
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
91
|
+
message:
|
|
92
|
+
"Ansible provisions a classical DSA key (community.crypto); deprecated and forgeable by a quantum attacker.",
|
|
93
|
+
remediation: "Rotate off DSA today; migrate to ML-DSA-65 (FIPS 204).",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Detects classical asymmetric keys provisioned by Ansible community.crypto. */
|
|
97
|
+
export const ansibleDetector: Detector = {
|
|
98
|
+
id: "ansible-crypto",
|
|
99
|
+
description: "Classical asymmetric keys provisioned by Ansible community.crypto",
|
|
100
|
+
scope: "config",
|
|
101
|
+
language: "any",
|
|
102
|
+
rules: [
|
|
103
|
+
RULE_ANSIBLE_RSA,
|
|
104
|
+
RULE_ANSIBLE_ECC,
|
|
105
|
+
RULE_ANSIBLE_EDDSA,
|
|
106
|
+
RULE_ANSIBLE_XDH,
|
|
107
|
+
RULE_ANSIBLE_DSA,
|
|
108
|
+
],
|
|
109
|
+
appliesTo: (f) => hasExtension(f, YAML_EXTENSIONS),
|
|
110
|
+
detect({ file, content }): Finding[] {
|
|
111
|
+
// Gate: only inside a community.crypto / openssl_privatekey context.
|
|
112
|
+
if (!content.includes("community.crypto") && !content.includes("openssl_privatekey")) return [];
|
|
113
|
+
// YAML `#` comments are not active settings; match over comment-masked content
|
|
114
|
+
// (offsets preserved) so a `# type: RSA` note does not fire.
|
|
115
|
+
const scan = maskCommentLines(content, ["#"]);
|
|
116
|
+
const findings: Finding[] = [];
|
|
117
|
+
const add = (re: RegExp, rule: RuleMeta) =>
|
|
118
|
+
eachMatch(re, scan, (m) =>
|
|
119
|
+
findings.push(
|
|
120
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
add(RE_ANSIBLE_RSA, RULE_ANSIBLE_RSA);
|
|
124
|
+
add(RE_ANSIBLE_ECC, RULE_ANSIBLE_ECC);
|
|
125
|
+
add(RE_ANSIBLE_EDDSA, RULE_ANSIBLE_EDDSA);
|
|
126
|
+
// Report the matched curve's own algorithm (X448 vs X25519) instead of the rule's
|
|
127
|
+
// default label.
|
|
128
|
+
eachMatch(RE_ANSIBLE_XDH, scan, (m) =>
|
|
129
|
+
findings.push(
|
|
130
|
+
findingFromRule(
|
|
131
|
+
RULE_ANSIBLE_XDH,
|
|
132
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
133
|
+
m[0].includes("448") ? { algorithm: "X448" } : undefined,
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
add(RE_ANSIBLE_DSA, RULE_ANSIBLE_DSA);
|
|
138
|
+
return findings;
|
|
139
|
+
},
|
|
140
|
+
};
|