@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
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Source detector for classical asymmetric cryptography in Objective-C on Apple
3
+ * platforms (iOS / macOS / tvOS / watchOS), via the Security framework's
4
+ * `SecKey*` API (and, underneath it, CommonCrypto). This is the Objective-C
5
+ * sibling of the Swift pack: the same Security-framework surface, reached from
6
+ * `.m` / `.mm` translation units instead of `.swift`. Lexical, same strategy as
7
+ * the other language packs — the Security-framework identifiers
8
+ * (`kSecAttrKeyType*`, `kSecKeyAlgorithm*`) are distinctive enough to keep the
9
+ * false-positive risk low.
10
+ *
11
+ * WHY a dedicated Objective-C pack (and not the C/C++ pack):
12
+ * - The C/C++ pack (`c.ts`) matches OpenSSL / libsodium / Mbed TLS / wolfSSL
13
+ * symbols and is gated to `.c/.h/.cc/.cpp/...` (which INCLUDES `.h`). Apple's
14
+ * Security framework is a different API surface entirely, so a separate pack
15
+ * is the clean home for it.
16
+ * - Objective-C headers are `.h` — shared with C/C++. To avoid double-scanning
17
+ * a header the C pack already owns, this detector is gated to `.m` and `.mm`
18
+ * ONLY (implementation files). Declarations of `SecKey*` usage live in the
19
+ * implementation, not the interface, so no real coverage is lost.
20
+ *
21
+ * Security-framework surface mapped here:
22
+ * - Key generation attributes (`SecKeyCreateRandomKey` / `SecKeyGeneratePair`):
23
+ * `kSecAttrKeyTypeRSA` → RSA keygen (kem, hndl:true)
24
+ * `kSecAttrKeyTypeEC` / `…ECSECPrimeRandom` → EC keygen (ECDSA, signature,
25
+ * hndl:false — EC keys default
26
+ * to signing; conservative)
27
+ * - Signing / verifying (`SecKeyCreateSignature` / `SecKeyVerifySignature`):
28
+ * `kSecKeyAlgorithmRSASignature*` → RSA signature (hndl:false)
29
+ * `kSecKeyAlgorithmECDSASignature*` → ECDSA signature (hndl:false)
30
+ * - Encryption / key agreement (`SecKeyCreateEncryptedData` / `SecKeyCopyKeyExchangeResult`):
31
+ * `kSecKeyAlgorithmRSAEncryption*` → RSA encryption (kem, hndl:true)
32
+ * `kSecKeyAlgorithmECDHKeyExchange*` → ECDH key agreement (hndl:true)
33
+ *
34
+ * Comment suppression is handled inline here (Objective-C is a C-style comment
35
+ * language): C-style block comments and `//` line comments are masked before the
36
+ * regexes run, so commented-out crypto never fires.
37
+ */
38
+ import type { Detector, Finding, RuleMeta } from "../types.js";
39
+ import {
40
+ eachMatch,
41
+ findingFromRule,
42
+ hasExtension,
43
+ maskBlockComments,
44
+ maskCommentLines,
45
+ OBJC_EXTENSIONS,
46
+ } from "../detect-utils.js";
47
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
48
+
49
+ // Security-framework key-type attributes (used with SecKeyCreateRandomKey /
50
+ // SecKeyGeneratePair). RSA vs EC. The EC form covers both the modern
51
+ // `kSecAttrKeyTypeECSECPrimeRandom` and the legacy `kSecAttrKeyTypeEC` alias.
52
+ const RE_OBJC_SEC_RSA = /\bkSecAttrKeyTypeRSA\b/g;
53
+ const RE_OBJC_SEC_EC = /\bkSecAttrKeyType(?:ECSECPrimeRandom|EC)\b/g;
54
+ // SecKey algorithm constants for signing / encryption / key agreement. The `\w*`
55
+ // tail captures the full family suffix (…PKCS1v15SHA256, …OAEPSHA256, …Standard,
56
+ // …CofactorX963SHA256, …) without needing to enumerate every variant.
57
+ const RE_OBJC_RSA_SIGN = /\bkSecKeyAlgorithmRSASignature\w*/g;
58
+ const RE_OBJC_RSA_ENCRYPT = /\bkSecKeyAlgorithmRSAEncryption\w*/g;
59
+ const RE_OBJC_ECDSA_SIGN = /\bkSecKeyAlgorithmECDSASignature\w*/g;
60
+ const RE_OBJC_ECDH = /\bkSecKeyAlgorithmECDHKeyExchange\w*/g;
61
+
62
+ const RULE_OBJC_SEC_RSA: RuleMeta = {
63
+ id: "objc-seckey-rsa",
64
+ title: "Objective-C Security-framework RSA key",
65
+ description: "SecKeyCreateRandomKey / SecKeyGeneratePair with kSecAttrKeyTypeRSA",
66
+ category: "kem",
67
+ severity: "high",
68
+ confidence: "high",
69
+ algorithm: "RSA",
70
+ hndl: true,
71
+ cwe: CWE_BROKEN_CRYPTO,
72
+ message: "Security-framework RSA key (Objective-C) is classical and not quantum-safe.",
73
+ remediation:
74
+ "Migrate to PQC as Apple's CryptoKit / Security add support: ML-KEM for encryption/key-agreement, ML-DSA for signatures.",
75
+ };
76
+ const RULE_OBJC_SEC_EC: RuleMeta = {
77
+ id: "objc-seckey-ec",
78
+ title: "Objective-C Security-framework EC key",
79
+ description: "SecKeyCreateRandomKey / SecKeyGeneratePair with kSecAttrKeyTypeECSECPrimeRandom",
80
+ // An EC key at generation is AMBIGUOUS (it can feed ECDSA signing OR ECDH key
81
+ // agreement). The fleet convention (java-ec-keygen, python-ec-keygen, swift-sec-ec,
82
+ // cloud-kms-ec, …) classifies ambiguous EC keygen as key-exchange/ECDH/hndl:true —
83
+ // the HNDL-SAFE choice, so a possible key-agreement use is never under-reported.
84
+ category: "key-exchange",
85
+ severity: "high",
86
+ confidence: "high",
87
+ algorithm: "ECDH",
88
+ hndl: true,
89
+ cwe: CWE_BROKEN_CRYPTO,
90
+ message:
91
+ "Security-framework EC key (Objective-C); EC keys feed ECDH key agreement (harvest-now-decrypt-later exposed) as well as ECDSA signatures, and are not quantum-safe.",
92
+ remediation:
93
+ "Migrate to PQC as Apple's CryptoKit / Security add support: ML-KEM for the ECDH key-agreement path, ML-DSA for signatures.",
94
+ };
95
+ const RULE_OBJC_RSA_SIGN: RuleMeta = {
96
+ id: "objc-rsa-sign",
97
+ title: "Objective-C RSA signature",
98
+ description: "SecKeyCreateSignature / SecKeyVerifySignature with kSecKeyAlgorithmRSASignature*",
99
+ category: "signature",
100
+ // `high`, consistent with every sibling pack's signature severity (swift-rsa,
101
+ // go-rsa-sign, java-rsa-sign) — the same primitive must not flip CI exit codes
102
+ // based on which language wrote it.
103
+ severity: "high",
104
+ confidence: "high",
105
+ algorithm: "RSA",
106
+ hndl: false,
107
+ cwe: CWE_BROKEN_CRYPTO,
108
+ message:
109
+ "Classical RSA signing (Objective-C, Security framework) is forgeable by a quantum attacker.",
110
+ remediation: "Migrate to ML-DSA (FIPS 204) as Apple's CryptoKit / Security add PQC support.",
111
+ };
112
+ const RULE_OBJC_RSA_ENCRYPT: RuleMeta = {
113
+ id: "objc-rsa-encrypt",
114
+ title: "Objective-C RSA encryption",
115
+ description: "SecKeyCreateEncryptedData with kSecKeyAlgorithmRSAEncryption*",
116
+ category: "kem",
117
+ severity: "high",
118
+ confidence: "high",
119
+ algorithm: "RSA",
120
+ hndl: true,
121
+ cwe: CWE_BROKEN_CRYPTO,
122
+ message:
123
+ "Classical RSA encryption (Objective-C, Security framework) is harvest-now-decrypt-later exposed.",
124
+ remediation: "Migrate to ML-KEM (FIPS 203) as Apple's CryptoKit / Security add PQC support.",
125
+ };
126
+ const RULE_OBJC_ECDSA_SIGN: RuleMeta = {
127
+ id: "objc-ecdsa-sign",
128
+ title: "Objective-C ECDSA signature",
129
+ description: "SecKeyCreateSignature / SecKeyVerifySignature with kSecKeyAlgorithmECDSASignature*",
130
+ category: "signature",
131
+ // `high`, consistent with swift-ecdsa / go-ecdsa / java-ecdsa-sign / dart-ecdsa.
132
+ severity: "high",
133
+ confidence: "high",
134
+ algorithm: "ECDSA",
135
+ hndl: false,
136
+ cwe: CWE_BROKEN_CRYPTO,
137
+ message:
138
+ "Classical ECDSA signing (Objective-C, Security framework) is forgeable by a quantum attacker.",
139
+ remediation: "Migrate to ML-DSA (FIPS 204) as Apple's CryptoKit / Security add PQC support.",
140
+ };
141
+ const RULE_OBJC_ECDH: RuleMeta = {
142
+ id: "objc-ecdh",
143
+ title: "Objective-C ECDH key agreement",
144
+ description: "SecKeyCopyKeyExchangeResult with kSecKeyAlgorithmECDHKeyExchange*",
145
+ category: "key-exchange",
146
+ severity: "high",
147
+ confidence: "high",
148
+ algorithm: "ECDH",
149
+ hndl: true,
150
+ cwe: CWE_BROKEN_CRYPTO,
151
+ message:
152
+ "Elliptic-curve Diffie-Hellman (Objective-C, Security framework) is broken by Shor's algorithm (harvest-now-decrypt-later).",
153
+ remediation:
154
+ "Migrate key agreement to ML-KEM (FIPS 203) as Apple's CryptoKit / Security add PQC support.",
155
+ };
156
+
157
+ /** Detects classical asymmetric crypto in Objective-C (Apple Security framework). */
158
+ export const objcDetector: Detector = {
159
+ id: "objc-crypto",
160
+ description: "Classical asymmetric crypto in Objective-C (Apple Security framework / SecKey)",
161
+ scope: "source",
162
+ language: "objc",
163
+ rules: [
164
+ RULE_OBJC_SEC_RSA,
165
+ RULE_OBJC_SEC_EC,
166
+ RULE_OBJC_RSA_SIGN,
167
+ RULE_OBJC_RSA_ENCRYPT,
168
+ RULE_OBJC_ECDSA_SIGN,
169
+ RULE_OBJC_ECDH,
170
+ ],
171
+ appliesTo: (f) => hasExtension(f, OBJC_EXTENSIONS),
172
+ detect({ file, content }): Finding[] {
173
+ // Fast reject: none of the Security-framework surface can appear without one
174
+ // of these substrings, so most Objective-C files bail before any regex runs.
175
+ if (
176
+ !content.includes("SecKey") &&
177
+ !content.includes("kSecAttrKeyType") &&
178
+ !content.includes("kSecKeyAlgorithm")
179
+ ) {
180
+ return [];
181
+ }
182
+ // Mask C-style block comments then `//` line comments so commented-out crypto
183
+ // does not fire. Both maskers preserve byte offsets, so finding line/column
184
+ // stay exact.
185
+ const masked = maskCommentLines(maskBlockComments(content), ["//"]);
186
+
187
+ const findings: Finding[] = [];
188
+ // Scan the masked text, but build the finding (its snippet) from the ORIGINAL
189
+ // `content` so a line with a trailing comment renders live, not blanked.
190
+ const add = (re: RegExp, rule: RuleMeta) =>
191
+ eachMatch(re, masked, (m) =>
192
+ findings.push(
193
+ findingFromRule(rule, {
194
+ file,
195
+ content,
196
+ index: m.index,
197
+ matchLength: m[0].length,
198
+ }),
199
+ ),
200
+ );
201
+ add(RE_OBJC_SEC_RSA, RULE_OBJC_SEC_RSA);
202
+ add(RE_OBJC_SEC_EC, RULE_OBJC_SEC_EC);
203
+ add(RE_OBJC_RSA_SIGN, RULE_OBJC_RSA_SIGN);
204
+ add(RE_OBJC_RSA_ENCRYPT, RULE_OBJC_RSA_ENCRYPT);
205
+ add(RE_OBJC_ECDSA_SIGN, RULE_OBJC_ECDSA_SIGN);
206
+ add(RE_OBJC_ECDH, RULE_OBJC_ECDH);
207
+ return findings;
208
+ },
209
+ };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Config detector: BINARY OpenPGP key material and encrypted messages committed to
3
+ * a repository — `secring.gpg` / `pubring.gpg` / `.gpg` / `.pgp` / `.kbx`. Armored
4
+ * blocks (`-----BEGIN PGP …-----`, base64 text) are handled by the PEM/secrets
5
+ * detectors; this covers the BINARY packet form, which the scan pipeline now reads
6
+ * byte-preserving (latin1; see {@link ../walk.ts isKeystorePath}) so we can read
7
+ * packet tags and the public-key algorithm id.
8
+ *
9
+ * A committed OpenPGP SECRET key is the sharp finding: a classical private key
10
+ * (RSA/DSA/ElGamal/ECDSA/EdDSA/ECDH) in version control is exposed and
11
+ * retroactively un-fixable. A binary PGP-encrypted message (PKESK) is
12
+ * harvest-now-decrypt-later exposed. Secret-key matches are sensitive material, so
13
+ * reporters drop the (binary) snippet.
14
+ *
15
+ * Packet framing per RFC 4880/9580. The parser is pure and bounds-checked (fuzzed).
16
+ */
17
+ import type { AlgorithmFamily, Detector, Finding, RuleMeta } from "../types.js";
18
+ import { findingFromRule, hasExtension } from "../detect-utils.js";
19
+ import { CWE_HARDCODED_KEY, CWE_BROKEN_CRYPTO } from "../cwe.js";
20
+
21
+ const OPENPGP_EXTENSIONS: readonly string[] = [".gpg", ".pgp"];
22
+
23
+ // Packet tags we care about.
24
+ const TAG_PKESK = 1; // public-key encrypted session key (an encrypted message)
25
+ const TAG_SECRET_KEY = 5;
26
+ const TAG_PUBLIC_KEY = 6;
27
+ const TAG_SECRET_SUBKEY = 7;
28
+ const TAG_PUBLIC_SUBKEY = 14;
29
+
30
+ interface Packet {
31
+ tag: number;
32
+ /** Offset where the packet body begins (after the length octets). */
33
+ bodyOffset: number;
34
+ }
35
+
36
+ /** Parse the FIRST OpenPGP packet header; return its tag and body offset. */
37
+ function firstPacket(content: string): Packet | undefined {
38
+ if (content.length < 2) return undefined;
39
+ const b0 = content.charCodeAt(0);
40
+ if ((b0 & 0x80) === 0) return undefined; // high bit always set on a packet tag
41
+ if (b0 & 0x40) {
42
+ // New-format packet: tag is the low 6 bits; length octet(s) follow.
43
+ const tag = b0 & 0x3f;
44
+ const l = content.charCodeAt(1);
45
+ let bodyOffset: number;
46
+ if (l < 192) bodyOffset = 2;
47
+ else if (l < 224) bodyOffset = 3;
48
+ else if (l === 255) bodyOffset = 6;
49
+ else bodyOffset = 2; // partial-body length
50
+ return { tag, bodyOffset };
51
+ }
52
+ // Old-format packet: tag in bits 5-2; length-type in the low 2 bits.
53
+ const tag = (b0 >> 2) & 0x0f;
54
+ const lt = b0 & 0x03;
55
+ const bodyOffset = lt === 0 ? 2 : lt === 1 ? 3 : lt === 2 ? 5 : 1;
56
+ return { tag, bodyOffset };
57
+ }
58
+
59
+ /** Map an OpenPGP public-key algorithm id to a family + HNDL (confidentiality) flag. */
60
+ function algo(id: number): { family: AlgorithmFamily; hndl: boolean } | undefined {
61
+ switch (id) {
62
+ case 1:
63
+ case 2:
64
+ case 3:
65
+ return { family: "RSA", hndl: true }; // RSA (encrypt-capable)
66
+ case 16:
67
+ case 20:
68
+ return { family: "unknown", hndl: true }; // ElGamal (encrypt)
69
+ case 17:
70
+ return { family: "DSA", hndl: false }; // DSA (sign only)
71
+ case 18:
72
+ return { family: "ECDH", hndl: true }; // ECDH (encrypt)
73
+ case 19:
74
+ return { family: "ECDSA", hndl: false }; // ECDSA (sign only)
75
+ case 22:
76
+ return { family: "EdDSA", hndl: false }; // EdDSA (sign only)
77
+ // RFC 9580 (crypto-refresh) v6 algorithm ids.
78
+ case 25:
79
+ return { family: "X25519", hndl: true }; // X25519 (encrypt / key agreement)
80
+ case 26:
81
+ return { family: "X448", hndl: true }; // X448 (encrypt / key agreement)
82
+ case 27:
83
+ return { family: "EdDSA", hndl: false }; // Ed25519 (sign only)
84
+ case 28:
85
+ return { family: "EdDSA", hndl: false }; // Ed448 (sign only)
86
+ default:
87
+ return undefined;
88
+ }
89
+ }
90
+
91
+ /** Read the public-key algorithm id from a key packet body, or undefined. */
92
+ function keyPacketAlgo(content: string, bodyOffset: number): number | undefined {
93
+ if (bodyOffset >= content.length) return undefined;
94
+ const version = content.charCodeAt(bodyOffset);
95
+ // v2/v3: version(1) created(4) validity(2) algo; v4/v5/v6: version(1) created(4) algo.
96
+ const algoOffset = version === 2 || version === 3 ? bodyOffset + 7 : bodyOffset + 5;
97
+ if (algoOffset >= content.length) return undefined;
98
+ return content.charCodeAt(algoOffset);
99
+ }
100
+
101
+ /** Read the wrapping algorithm from a PKESK (encrypted-message) packet body. */
102
+ function pkeskAlgo(content: string, bodyOffset: number): number | undefined {
103
+ if (bodyOffset >= content.length) return undefined;
104
+ const version = content.charCodeAt(bodyOffset);
105
+ // v3: version(1) keyid(8) algo. (v6 differs; v3 dominates on-disk.)
106
+ if (version !== 3) return undefined;
107
+ const algoOffset = bodyOffset + 9;
108
+ if (algoOffset >= content.length) return undefined;
109
+ return content.charCodeAt(algoOffset);
110
+ }
111
+
112
+ function rule(id: string, title: string, message: string, opts: Partial<RuleMeta>): RuleMeta {
113
+ return {
114
+ id,
115
+ title,
116
+ description: message,
117
+ category: "certificate",
118
+ severity: "high",
119
+ confidence: "high",
120
+ hndl: false,
121
+ cwe: CWE_HARDCODED_KEY,
122
+ message,
123
+ remediation:
124
+ "Remove key material from version control and rotate it; store keys in a secret manager / HSM. Plan re-issuance with PQC keys (ML-DSA-65 / ML-KEM-768) as OpenPGP tooling supports them.",
125
+ ...opts,
126
+ };
127
+ }
128
+
129
+ const RULE_SECRET = rule(
130
+ "openpgp-secret-key",
131
+ "OpenPGP secret key (binary)",
132
+ "A binary OpenPGP SECRET key is committed to version control — a classical private key that is exposed and retroactively un-fixable.",
133
+ { sensitive: true },
134
+ );
135
+ const RULE_PUBLIC = rule(
136
+ "openpgp-public-key",
137
+ "OpenPGP public key (binary)",
138
+ "A binary OpenPGP public key uses classical asymmetric crypto (RSA/DSA/EC); plan a PQC migration path.",
139
+ { severity: "medium" },
140
+ );
141
+ const RULE_PKESK = rule(
142
+ "openpgp-encrypted-message",
143
+ "OpenPGP-encrypted message (binary)",
144
+ "A binary OpenPGP-encrypted message wraps its session key with classical asymmetric crypto (RSA/ElGamal/ECDH); harvest-now-decrypt-later exposed.",
145
+ { category: "kem", hndl: true, cwe: CWE_BROKEN_CRYPTO },
146
+ );
147
+ const RULE_KEYBOX = rule(
148
+ "openpgp-keybox",
149
+ "GnuPG keybox (.kbx)",
150
+ "A GnuPG keybox (.kbx) is a database of classical OpenPGP/X.509 keys and certificates.",
151
+ { severity: "medium", cwe: CWE_BROKEN_CRYPTO },
152
+ );
153
+
154
+ /** Detects binary OpenPGP key material / messages and GnuPG keyboxes. */
155
+ export const openpgpDetector: Detector = {
156
+ id: "openpgp-material",
157
+ description: "Binary OpenPGP key material / encrypted messages and GnuPG keyboxes",
158
+ scope: "config",
159
+ language: "any",
160
+ rules: [RULE_SECRET, RULE_PUBLIC, RULE_PKESK, RULE_KEYBOX],
161
+ appliesTo: (f) => hasExtension(f, OPENPGP_EXTENSIONS) || f.toLowerCase().endsWith(".kbx"),
162
+ detect({ file, content }): Finding[] {
163
+ const at = { file, content, index: 0, matchLength: Math.min(4, content.length) };
164
+
165
+ // GnuPG keybox: a key/cert database, identified by the "KBXf" magic in its
166
+ // first blob header (so a garbage/empty .kbx does not fire).
167
+ if (file.toLowerCase().endsWith(".kbx")) {
168
+ return content.includes("KBXf") ? [findingFromRule(RULE_KEYBOX, at)] : [];
169
+ }
170
+
171
+ const pkt = firstPacket(content);
172
+ if (!pkt) return [];
173
+ const { tag, bodyOffset } = pkt;
174
+
175
+ if (tag === TAG_SECRET_KEY || tag === TAG_SECRET_SUBKEY) {
176
+ const id = keyPacketAlgo(content, bodyOffset);
177
+ const a = id !== undefined ? algo(id) : undefined;
178
+ return [
179
+ findingFromRule(RULE_SECRET, at, a ? { algorithm: a.family, hndl: a.hndl } : undefined),
180
+ ];
181
+ }
182
+ if (tag === TAG_PUBLIC_KEY || tag === TAG_PUBLIC_SUBKEY) {
183
+ const id = keyPacketAlgo(content, bodyOffset);
184
+ const a = id !== undefined ? algo(id) : undefined;
185
+ return [findingFromRule(RULE_PUBLIC, at, a ? { algorithm: a.family } : undefined)];
186
+ }
187
+ if (tag === TAG_PKESK) {
188
+ const id = pkeskAlgo(content, bodyOffset);
189
+ const a = id !== undefined ? algo(id) : undefined;
190
+ return [findingFromRule(RULE_PKESK, at, a ? { algorithm: a.family } : undefined)];
191
+ }
192
+ return [];
193
+ },
194
+ };
@@ -51,7 +51,7 @@ const PEM_RULES: PemRule[] = [
51
51
  sensitive: true,
52
52
  message: "Embedded EC private key (SEC1 PEM); classical ECDSA/ECDH key, not quantum-safe.",
53
53
  remediation:
54
- "Migrate to ML-DSA (FIPS 204) keys and remove embedded private keys from source.",
54
+ "Migrate to ML-DSA (FIPS 204) for signatures or hybrid X25519MLKEM768 for key agreement; remove embedded private keys from source.",
55
55
  },
56
56
  },
57
57
  {
@@ -211,6 +211,31 @@ const PEM_RULES: PemRule[] = [
211
211
  },
212
212
  ];
213
213
 
214
+ /**
215
+ * True when what follows the begin marker looks like a REAL PEM block rather than a
216
+ * `-----BEGIN …-----` header STRING LITERAL (as appears in PEM parsers, tests, and
217
+ * i18n messages). `from` points just past the begin marker. Three accepting signals:
218
+ *
219
+ * (a) a full LINE of base64 (≥24 chars, optional `=` padding) — real multi-line PEM
220
+ * bodies are line-wrapped base64; requiring the whole line to be base64 stops a
221
+ * long camelCase identifier on a code line from validating a bare header constant.
222
+ * (b) an ESCAPED-newline base64 run (`\n<base64>`, a LITERAL backslash-n) — a key
223
+ * embedded in a single-line JSON/.env string (GCP service accounts, `PRIVATE_KEY=
224
+ * "…\n…"`), whose body can run past the window before its `-----END`. The literal
225
+ * `\n` escape distinguishes it from a source constant, which has a REAL newline.
226
+ * (c) a matching `-----END …-----` marker with NO double-quote between it and the
227
+ * begin marker. This admits short/placeholder bodies (`x`, `abc`, `MIIB` in tests)
228
+ * while rejecting a parser's PAIRED header/footer constants, whose intervening
229
+ * `";` … `= "` always contains a `"` — a char a real PEM body never holds.
230
+ */
231
+ function hasBase64Body(content: string, from: number): boolean {
232
+ const window = content.slice(from, from + 800);
233
+ if (/^[ \t]*[A-Za-z0-9+/]{24,}={0,2}[ \t]*$/m.test(window)) return true;
234
+ if (/\\n[A-Za-z0-9+/]{24,}/.test(window)) return true;
235
+ const end = window.search(/-----END [A-Z0-9 ]+-----/);
236
+ return end >= 0 && !window.slice(0, end).includes('"');
237
+ }
238
+
214
239
  /** Detects PEM key/certificate material in arbitrary files. */
215
240
  export const pemDetector: Detector = {
216
241
  id: "pem-material",
@@ -227,6 +252,11 @@ export const pemDetector: Detector = {
227
252
  const findings: Finding[] = [];
228
253
  for (const rule of PEM_RULES) {
229
254
  eachMatch(rule.re, content, (m) => {
255
+ // Require an actual base64 body after the marker, so a bare
256
+ // `-----BEGIN RSA PRIVATE KEY-----` string literal in a PEM parser / test /
257
+ // i18n message does NOT get reported as an embedded key. A real PEM block has
258
+ // a run of base64 within the next few hundred chars.
259
+ if (!hasBase64Body(content, m.index + m[0].length)) return;
230
260
  findings.push(
231
261
  findingFromRule(rule.meta, {
232
262
  file,
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Source-code detector for classical asymmetric cryptography in PHP — one of the
3
+ * most-deployed backend languages, previously uncovered. Handles the three
4
+ * dominant crypto surfaces:
5
+ *
6
+ * - **ext/openssl** — `openssl_pkey_new()` (classified by its
7
+ * `OPENSSL_KEYTYPE_*` config, defaulting to RSA), `openssl_public_encrypt` /
8
+ * `openssl_private_decrypt` / `openssl_seal` / `openssl_open` (RSA), and
9
+ * `openssl_sign` / `openssl_verify`.
10
+ * - **phpseclib3** — `RSA::createKey()` / `EC::createKey()` / `DSA::createKey()`.
11
+ * - **libsodium** — `sodium_crypto_box`/`kx` keypairs (X25519) and
12
+ * `sodium_crypto_sign` keypairs (Ed25519).
13
+ *
14
+ * Lexical, like the other packs; the `openssl_*` / `sodium_crypto_*` prefixes and
15
+ * `OPENSSL_KEYTYPE_*` constants are distinctive, so the false-positive rate is low.
16
+ *
17
+ * HNDL: RSA encryption and (EC/X)DH key agreement are harvest-now-decrypt-later
18
+ * exposed (hndl:true); ECDSA / DSA / Ed25519 signatures are hndl:false but
19
+ * forgeable. `openssl_pkey_new` EC and `EC::createKey` are ambiguous (feed BOTH
20
+ * ECDSA and ECDH), classified conservatively as key-exchange-capable (hndl:true).
21
+ */
22
+ import type { Detector, Finding, RuleMeta } from "../types.js";
23
+ import { PHP_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
24
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
25
+
26
+ const RE_PHP_PKEY_NEW = /\bopenssl_pkey_new\s*\(/g;
27
+ // public_encrypt/private_decrypt (direct RSA) plus seal/open (RSA-envelope: seal
28
+ // encrypts a random symmetric key under each recipient's RSA public key, open
29
+ // decrypts it with the private key) — all RSA key transport, harvest-now exposed.
30
+ const RE_PHP_RSA_CRYPT = /\bopenssl_(?:public_encrypt|private_decrypt|seal|open)\s*\(/g;
31
+ const RE_PHP_SIGN = /\bopenssl_(?:sign|verify)\s*\(/g;
32
+ // phpseclib3 factory methods. The class names are generic, so require `::createKey`.
33
+ const RE_PHP_SECLIB = /\b(RSA|EC|DSA|DH)::createKey\s*\(/g;
34
+ // libsodium X25519 (box / kx / scalarmult) and Ed25519 (sign) key pairs.
35
+ const RE_PHP_SODIUM_X25519 =
36
+ /\bsodium_crypto_(?:box|kx)_(?:seed_)?keypair\s*\(|\bsodium_crypto_scalarmult(?:_base)?\s*\(/g;
37
+ // Also the explicit-algorithm variant sodium_crypto_sign_ed25519_keypair().
38
+ const RE_PHP_SODIUM_ED25519 = /\bsodium_crypto_sign_(?:ed25519_|seed_)?keypair\s*\(/g;
39
+
40
+ /** Key-type classification for `openssl_pkey_new`, keyed by its config constant. */
41
+ interface KeyInfo {
42
+ algo: Finding["algorithm"];
43
+ cat: Finding["category"];
44
+ sev: Finding["severity"];
45
+ hndl: boolean;
46
+ label: string;
47
+ remediation?: string;
48
+ }
49
+ const HYBRID = "hybrid X25519MLKEM768 (ML-KEM-768) for key agreement; ML-DSA-65 (FIPS 204) to sign";
50
+
51
+ const RULE_PHP_KEYGEN: RuleMeta = {
52
+ id: "php-openssl-keygen",
53
+ title: "PHP openssl key generation",
54
+ description: "openssl_pkey_new (RSA/EC/DSA/DH, by OPENSSL_KEYTYPE_*)",
55
+ category: "kem",
56
+ severity: "high",
57
+ confidence: "high",
58
+ algorithm: "RSA",
59
+ hndl: true,
60
+ cwe: CWE_BROKEN_CRYPTO,
61
+ message: "Generates a classical key pair via PHP openssl_pkey_new — not quantum-safe.",
62
+ };
63
+ const RULE_PHP_RSA_CRYPT: RuleMeta = {
64
+ id: "php-openssl-rsa-crypt",
65
+ title: "PHP openssl RSA public-key encryption",
66
+ description: "openssl_public_encrypt / openssl_private_decrypt / openssl_seal / openssl_open",
67
+ category: "kem",
68
+ severity: "high",
69
+ confidence: "high",
70
+ algorithm: "RSA",
71
+ hndl: true,
72
+ cwe: CWE_BROKEN_CRYPTO,
73
+ message:
74
+ "RSA public-key encryption/decryption (PHP openssl) is harvest-now-decrypt-later exposed.",
75
+ };
76
+ const RULE_PHP_SIGN: RuleMeta = {
77
+ id: "php-openssl-sign",
78
+ title: "PHP openssl signature",
79
+ description: "openssl_sign / openssl_verify",
80
+ category: "signature",
81
+ severity: "high",
82
+ confidence: "high",
83
+ algorithm: "unknown",
84
+ hndl: false,
85
+ cwe: CWE_BROKEN_CRYPTO,
86
+ message:
87
+ "Classical signature via PHP openssl (RSA/ECDSA/DSA) is forgeable by a quantum attacker.",
88
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
89
+ };
90
+ const RULE_PHP_SECLIB: RuleMeta = {
91
+ id: "php-phpseclib-keygen",
92
+ title: "PHP phpseclib key generation",
93
+ description: "phpseclib3 RSA/EC/DSA/DH ::createKey",
94
+ category: "kem",
95
+ severity: "high",
96
+ confidence: "medium",
97
+ algorithm: "RSA",
98
+ hndl: true,
99
+ cwe: CWE_BROKEN_CRYPTO,
100
+ message: "Generates a classical key pair via phpseclib3 (createKey) — not quantum-safe.",
101
+ };
102
+ const RULE_PHP_SODIUM_X25519: RuleMeta = {
103
+ id: "php-sodium-x25519",
104
+ title: "PHP libsodium X25519 key agreement",
105
+ description: "sodium_crypto_box/kx keypair + scalarmult",
106
+ category: "key-exchange",
107
+ severity: "medium",
108
+ confidence: "high",
109
+ algorithm: "X25519",
110
+ hndl: true,
111
+ cwe: CWE_BROKEN_CRYPTO,
112
+ message:
113
+ "libsodium crypto_box/kx (PHP) uses X25519 key agreement — modern but classical, harvest-now-decrypt-later exposed.",
114
+ };
115
+ const RULE_PHP_SODIUM_ED25519: RuleMeta = {
116
+ id: "php-sodium-ed25519",
117
+ title: "PHP libsodium Ed25519 signature",
118
+ description: "sodium_crypto_sign keypair",
119
+ category: "signature",
120
+ severity: "low",
121
+ confidence: "high",
122
+ algorithm: "EdDSA",
123
+ hndl: false,
124
+ cwe: CWE_BROKEN_CRYPTO,
125
+ message:
126
+ "libsodium crypto_sign (PHP) uses Ed25519 — classical and forgeable by a quantum attacker.",
127
+ };
128
+
129
+ /** phpseclib class name → its classification. */
130
+ const SECLIB_INFO: Record<string, KeyInfo> = {
131
+ RSA: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
132
+ EC: {
133
+ algo: "ECDH",
134
+ cat: "key-exchange",
135
+ sev: "high",
136
+ hndl: true,
137
+ label: "EC (ECDSA/ECDH)",
138
+ remediation: HYBRID,
139
+ },
140
+ DSA: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
141
+ DH: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
142
+ };
143
+
144
+ /** Classify `openssl_pkey_new` by the `OPENSSL_KEYTYPE_*` constant in its config.
145
+ * The window is bounded to the current statement (up to the next `;`, capped at
146
+ * 300 chars) so it can't bleed into a following `openssl_pkey_new` call's key
147
+ * type. Defaults to RSA — openssl_pkey_new's own default private_key_type is RSA
148
+ * when none is given. */
149
+ function classifyPkeyNew(content: string, index: number): KeyInfo {
150
+ const semi = content.indexOf(";", index);
151
+ const end = Math.min(index + 300, semi === -1 ? content.length : semi);
152
+ const w = content.slice(index, end);
153
+ if (/\bOPENSSL_KEYTYPE_EC\b/.test(w)) return SECLIB_INFO.EC;
154
+ if (/\bOPENSSL_KEYTYPE_DSA\b/.test(w)) return SECLIB_INFO.DSA;
155
+ if (/\bOPENSSL_KEYTYPE_DH\b/.test(w)) return SECLIB_INFO.DH;
156
+ return SECLIB_INFO.RSA;
157
+ }
158
+
159
+ /** Detects classical asymmetric crypto in PHP (openssl, phpseclib, libsodium). */
160
+ export const phpDetector: Detector = {
161
+ id: "php-crypto",
162
+ description: "Classical asymmetric crypto in PHP (openssl, phpseclib3, libsodium)",
163
+ scope: "source",
164
+ language: "php",
165
+ rules: [
166
+ RULE_PHP_KEYGEN,
167
+ RULE_PHP_RSA_CRYPT,
168
+ RULE_PHP_SIGN,
169
+ RULE_PHP_SECLIB,
170
+ RULE_PHP_SODIUM_X25519,
171
+ RULE_PHP_SODIUM_ED25519,
172
+ ],
173
+ appliesTo: (f) => hasExtension(f, PHP_EXTENSIONS),
174
+ detect({ file, content }): Finding[] {
175
+ const findings: Finding[] = [];
176
+ const add = (re: RegExp, rule: RuleMeta): void =>
177
+ eachMatch(re, content, (m) =>
178
+ findings.push(
179
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
180
+ ),
181
+ );
182
+
183
+ // openssl_pkey_new — one finding per call, classified by its key-type config.
184
+ eachMatch(RE_PHP_PKEY_NEW, content, (m) => {
185
+ const info = classifyPkeyNew(content, m.index);
186
+ findings.push(
187
+ findingFromRule(
188
+ RULE_PHP_KEYGEN,
189
+ { file, content, index: m.index, matchLength: m[0].length },
190
+ {
191
+ title: `PHP openssl ${info.label} key generation`,
192
+ category: info.cat,
193
+ severity: info.sev,
194
+ algorithm: info.algo,
195
+ hndl: info.hndl,
196
+ message: `Generates a classical ${info.label} key pair via PHP openssl_pkey_new — not quantum-safe.`,
197
+ ...(info.remediation ? { remediation: info.remediation } : {}),
198
+ },
199
+ ),
200
+ );
201
+ });
202
+
203
+ add(RE_PHP_RSA_CRYPT, RULE_PHP_RSA_CRYPT);
204
+ add(RE_PHP_SIGN, RULE_PHP_SIGN);
205
+
206
+ // phpseclib3 ::createKey — classify by the class (RSA/EC/DSA/DH).
207
+ eachMatch(RE_PHP_SECLIB, content, (m) => {
208
+ const info = SECLIB_INFO[m[1]];
209
+ findings.push(
210
+ findingFromRule(
211
+ RULE_PHP_SECLIB,
212
+ { file, content, index: m.index, matchLength: m[0].length },
213
+ {
214
+ title: `PHP phpseclib ${info.label} key generation`,
215
+ category: info.cat,
216
+ severity: info.sev,
217
+ algorithm: info.algo,
218
+ hndl: info.hndl,
219
+ message: `Generates a classical ${info.label} key pair via phpseclib3 createKey — not quantum-safe.`,
220
+ ...(info.remediation ? { remediation: info.remediation } : {}),
221
+ },
222
+ ),
223
+ );
224
+ });
225
+
226
+ add(RE_PHP_SODIUM_X25519, RULE_PHP_SODIUM_X25519);
227
+ add(RE_PHP_SODIUM_ED25519, RULE_PHP_SODIUM_ED25519);
228
+ return findings;
229
+ },
230
+ };