@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

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 (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -21,7 +21,9 @@ export function normalizeWhoisChain(value) {
21
21
  ...(typeof hop.queriedAt === 'string' ? { queriedAt: hop.queriedAt.slice(0, 64) } : {}),
22
22
  ...(typeof hop.queryProfile === 'string' ? { queryProfile: hop.queryProfile.slice(0, 80) } : {}),
23
23
  ...(typeof hop.responseEncoding === 'string' ? { responseEncoding: hop.responseEncoding.slice(0, 40) } : {}),
24
- ...(typeof hop.response === 'string' ? { response: hop.response.slice(0, MAX_WHOIS_BYTES) } : {}),
24
+ // Parser and authority analysis share protocol line endings, while the
25
+ // original hop retains its wire text. Lone and embedded controls remain.
26
+ ...(typeof hop.response === 'string' ? { response: hop.response.slice(0, MAX_WHOIS_BYTES).replace(/\r\n/g, '\n') } : {}),
25
27
  ...(typeof hop.error === 'string' ? { error: hop.error.slice(0, 1000) } : {}),
26
28
  });
27
29
  }
@@ -1,13 +1,13 @@
1
1
  // WHOIS response parsing. Transport, referral-chain and stable public exports
2
2
  // remain owned by the facade in whois.mts.
3
3
  import { registryDateIso } from "./registry-dates.mjs";
4
- import { analyzeWhoisChainAuthority } from "./whois-authority.mjs";
4
+ import { analyzeWhoisChainAuthority, whoisResponseMatchesDomain } from "./whois-authority.mjs";
5
5
  import { normalizeWhoisChain } from "./whois-normalization.mjs";
6
6
  import { applyWhoisCommonFormats } from "./whois-common-formats.mjs";
7
7
  import { applyWhoisDialects } from "./whois-dialects.mjs";
8
8
  import { WHOIS_FIELD_PATTERNS, normalizedWhoisContact, } from "./whois-parser-support.mjs";
9
9
  import { boundedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
10
- function parseWhoisChain(chain) {
10
+ function parseWhoisChain(chain, requestedDomain) {
11
11
  const source = normalizeWhoisChain(chain);
12
12
  const fields = {};
13
13
  const truncatedFields = new Set();
@@ -18,6 +18,8 @@ function parseWhoisChain(chain) {
18
18
  const text = hop.response;
19
19
  if (!text)
20
20
  return;
21
+ if (hopIndex > 0 && requestedDomain !== undefined && !whoisResponseMatchesDomain(text, requestedDomain))
22
+ return;
21
23
  // hopIndex 0 is always whois.iana.org, whose "domain:"/"created:"/
22
24
  // "changed:" fields describe the TLD's own root delegation record, not
23
25
  // the queried domain - e.g. "created: 1992-08-14" for .gt is Guatemala's
@@ -118,7 +120,7 @@ function parseWhoisChain(chain) {
118
120
  // Existence is decided authority-aware, not by a global "any hop said no
119
121
  // match" flag: positive registry evidence is never overridden by a later
120
122
  // registrar hop that failed, rate-limited, or returned "no match".
121
- const authority = analyzeWhoisChainAuthority(source);
123
+ const authority = analyzeWhoisChainAuthority(source, requestedDomain);
122
124
  return {
123
125
  ...fields,
124
126
  nameservers: [...nameservers],
package/package.json CHANGED
@@ -33,38 +33,37 @@
33
33
  "bugs": {
34
34
  "url": "https://github.com/slicedearth/whoisleuth/issues"
35
35
  },
36
- "version": "2.4.0",
36
+ "version": "2.5.1",
37
37
  "dependencies": {
38
- "@peculiar/x509": "2.0.0",
38
+ "@nuintun/qrcode": "5.0.3",
39
+ "@peculiar/x509": "2.1.0",
40
+ "fast-png": "8.0.0",
39
41
  "fflate": "0.8.3",
40
42
  "maxmind": "5.0.7",
41
43
  "parse5": "8.0.1",
44
+ "pdfjs-dist": "6.3.289",
45
+ "postal-mime": "3.0.0",
42
46
  "reflect-metadata": "0.2.2",
43
- "tldts": "7.4.11",
44
- "undici": "8.10.1"
47
+ "saxes": "6.0.0",
48
+ "tldts": "7.4.14",
49
+ "undici": "8.11.0"
45
50
  },
46
51
  "files": [
47
52
  "bin/**/*.mjs",
48
53
  "cli/**/*.mjs",
49
54
  "lib/**/*.mjs",
50
55
  "frontend/src/lib/**/*.js",
51
- "packages/cases/**/*.mjs",
52
- "packages/comparison/**/*.mjs",
53
- "packages/contracts/**/*.mjs",
54
- "packages/evidence/**/*.mjs",
55
- "packages/interchange/**/*.mjs",
56
- "packages/investigation/**/*.mjs",
57
- "packages/monitoring/**/*.mjs",
58
- "packages/workspace/**/*.mjs",
56
+ "packages/**/*.mjs",
57
+ "README.md",
59
58
  "docs/cli.md",
60
59
  "docs/cli-reference.md",
60
+ "docs/contextual-reviews.md",
61
61
  "DISCLOSURE",
62
62
  "LICENSE",
63
- "LICENSES/*.txt",
64
63
  "NOTICE",
65
- "README.md",
66
64
  "SECURITY.md",
67
65
  "TRADEMARKS.md",
66
+ "LICENSES/Retire.js-Apache-2.0.txt",
68
67
  "third-party-notices.txt"
69
68
  ],
70
69
  "publishConfig": {
@@ -0,0 +1,40 @@
1
+ // Deliberately small interoperability projection over reviewed MISP
2
+ // taxonomies. WHOISleuth retains its own bounded workflow vocabulary and
3
+ // derives machine tags only when an analyst-owned disposition and reason make
4
+ // the mapping unambiguous. These tags never alter collection or scoring.
5
+ const ANALYST_REVIEW_REASONS = Object.freeze([
6
+ Object.freeze({ value: '', label: 'Not recorded' }),
7
+ Object.freeze({ value: 'authorized_or_owned', label: 'Authorised or owned domain' }),
8
+ Object.freeze({ value: 'shared_infrastructure', label: 'Shared infrastructure explained the signal' }),
9
+ Object.freeze({ value: 'generic_platform_or_template', label: 'Generic platform or template' }),
10
+ Object.freeze({ value: 'parked_or_reseller', label: 'Parking or reseller page' }),
11
+ Object.freeze({ value: 'insufficient_evidence', label: 'Insufficient evidence' }),
12
+ Object.freeze({ value: 'legitimate_third_party', label: 'Legitimate third party' }),
13
+ Object.freeze({ value: 'confirmed_credential_abuse', label: 'Confirmed credential abuse' }),
14
+ Object.freeze({ value: 'confirmed_malware', label: 'Confirmed malware' }),
15
+ Object.freeze({ value: 'other_reviewed', label: 'Other reviewed reason' }),
16
+ ]);
17
+ const ANALYST_REVIEW_REASON_VALUES = new Set(ANALYST_REVIEW_REASONS.map(({ value }) => value).filter(Boolean));
18
+ const MISP_TAXONOMY_REFERENCES = Object.freeze([
19
+ Object.freeze({ namespace: 'domain-abuse', version: 2, sourceSha256: '5fcfb44742daac97061726faaaadd2ce6b7eac55306acaae3d30c3882d18f991', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/domain-abuse/machinetag.json' }),
20
+ Object.freeze({ namespace: 'estimative-language', version: 5, sourceSha256: '1010927d396f66f7cc58a229ebb8af6ad2cdc0250ad5f8ce3b1bd9aacf5ab4da', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/estimative-language/machinetag.json' }),
21
+ Object.freeze({ namespace: 'event-assessment', version: 2, sourceSha256: '39291bdd50e1b2975a93e33e396f42b398178015580a84402bc3d8d549dd721e', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/event-assessment/machinetag.json' }),
22
+ Object.freeze({ namespace: 'false-positive', version: 7, sourceSha256: 'ffc8d35ae36eb1d39f33611d1090793021f2bfe3b4b31ae41631e22cf544a2fb', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/false-positive/machinetag.json' }),
23
+ Object.freeze({ namespace: 'phishing', version: 5, sourceSha256: '8628ebe4d5935216a7b006a4d975669c4ae1a554ef8ed78ffaadd62f763d7263', sourceUrl: 'https://github.com/MISP/misp-taxonomies/blob/main/phishing/machinetag.json' }),
24
+ ]);
25
+ function analystInteroperabilityTags(disposition, reviewReasonCode) {
26
+ const tags = new Set();
27
+ if (disposition === 'false_positive')
28
+ tags.add('false-positive:confirmed="true"');
29
+ if ((disposition === 'false_positive' || disposition === 'expected')
30
+ && ['authorized_or_owned', 'shared_infrastructure', 'generic_platform_or_template', 'parked_or_reseller', 'legitimate_third_party'].includes(String(reviewReasonCode)))
31
+ tags.add('false-positive:risk="high"');
32
+ if (reviewReasonCode === 'insufficient_evidence') {
33
+ tags.add('estimative-language:confidence-in-analytic-judgment="low"');
34
+ }
35
+ if (disposition === 'confirmed_abuse' && reviewReasonCode === 'confirmed_credential_abuse') {
36
+ tags.add('phishing:techniques="fake-website"');
37
+ }
38
+ return [...tags].sort();
39
+ }
40
+ export { ANALYST_REVIEW_REASONS, ANALYST_REVIEW_REASON_VALUES, MISP_TAXONOMY_REFERENCES, analystInteroperabilityTags, };
@@ -0,0 +1,330 @@
1
+ export const MAX_BOUNDED_JSON_DEPTH = 48;
2
+ export const MAX_BOUNDED_JSON_KEYS = 50_000;
3
+ export const MAX_BOUNDED_JSON_VALUES = 100_000;
4
+ export const MAX_BOUNDED_JSON_CONTAINER_ITEMS = 10_000;
5
+ export const UNSAFE_JSON_OBJECT_KEYS = Object.freeze(['__proto__']);
6
+ /** Aggregate work allowed by a separately enforced UTF-8 byte budget. */
7
+ export function boundedJsonLimitsForBytes(maximumBytes) {
8
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
9
+ throw new TypeError('The JSON byte budget must be a positive safe integer.');
10
+ }
11
+ // A member needs at least five bytes (comma, two quotes, colon, value).
12
+ // Values need at least two bytes each, apart from a scalar root or the last
13
+ // array item. These upper bounds do not depend on a model's current fields.
14
+ // Nesting and individual-container limits remain independently bounded.
15
+ return {
16
+ maximumKeys: Math.floor(maximumBytes / 5),
17
+ maximumValues: Math.ceil(maximumBytes / 2),
18
+ // Decoded text is a lower bound on its UTF-8 JSON representation. Checking
19
+ // it before stringify also prevents an oversized string allocation.
20
+ maximumStringCodeUnits: maximumBytes,
21
+ };
22
+ }
23
+ function syntaxError() {
24
+ throw new TypeError('Artefact input is not valid JSON.');
25
+ }
26
+ export function isSafeJsonObjectKey(value) {
27
+ return !UNSAFE_JSON_OBJECT_KEYS.includes(value);
28
+ }
29
+ export function assertBoundedJsonStructure(value, label = 'JSON value', limits = {}) {
30
+ const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
31
+ const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
32
+ const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
33
+ const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
34
+ let keys = 0;
35
+ let values = 0;
36
+ let stringCodeUnits = 0;
37
+ const ancestors = new Set();
38
+ const text = (value) => {
39
+ stringCodeUnits += value.length;
40
+ if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
41
+ throw new TypeError(`${label} exceeds the aggregate text limit.`);
42
+ }
43
+ };
44
+ const visit = (current, depth) => {
45
+ values += 1;
46
+ if (values > maximumValues) {
47
+ throw new TypeError(`${label} exceeds the ${maximumValues}-value limit.`);
48
+ }
49
+ if (depth > maximumDepth) {
50
+ throw new TypeError(`${label} exceeds the ${maximumDepth}-level nesting limit.`);
51
+ }
52
+ if (typeof current === 'string') {
53
+ text(current);
54
+ return;
55
+ }
56
+ if (current === null || typeof current === 'boolean')
57
+ return;
58
+ if (typeof current === 'number') {
59
+ if (!Number.isFinite(current))
60
+ throw new TypeError(`${label} contains a non-JSON number.`);
61
+ return;
62
+ }
63
+ if (typeof current !== 'object')
64
+ throw new TypeError(`${label} contains a non-JSON value.`);
65
+ if (ancestors.has(current))
66
+ throw new TypeError(`${label} contains a cyclic object reference.`);
67
+ let prototype;
68
+ let ownKeys;
69
+ let descriptors;
70
+ try {
71
+ prototype = Object.getPrototypeOf(current);
72
+ ownKeys = Reflect.ownKeys(current);
73
+ descriptors = Object.getOwnPropertyDescriptors(current);
74
+ }
75
+ catch {
76
+ throw new TypeError(`${label} could not be inspected safely.`);
77
+ }
78
+ if (ownKeys.some((key) => typeof key !== 'string')) {
79
+ throw new TypeError(`${label} contains a symbol key.`);
80
+ }
81
+ if (ownKeys.some((key) => {
82
+ const descriptor = descriptors[String(key)];
83
+ return !descriptor || !Object.hasOwn(descriptor, 'value');
84
+ })) {
85
+ throw new TypeError(`${label} contains an accessor property.`);
86
+ }
87
+ ancestors.add(current);
88
+ if (Array.isArray(current)) {
89
+ if (prototype !== Array.prototype) {
90
+ throw new TypeError(`${label} contains an array with a custom prototype.`);
91
+ }
92
+ const length = descriptors.length?.value;
93
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximumContainerItems) {
94
+ throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
95
+ }
96
+ const indexes = ownKeys.filter((key) => key !== 'length');
97
+ if (indexes.length !== length
98
+ || indexes.some((key, index) => key !== String(index) || descriptors[key]?.enumerable !== true)) {
99
+ throw new TypeError(`${label} contains a sparse, extended, or non-enumerable array.`);
100
+ }
101
+ for (const key of indexes)
102
+ visit(descriptors[key].value, depth + 1);
103
+ }
104
+ else {
105
+ if (prototype !== Object.prototype && prototype !== null) {
106
+ throw new TypeError(`${label} contains an object with a custom prototype.`);
107
+ }
108
+ const objectKeys = ownKeys;
109
+ if (objectKeys.length > maximumContainerItems) {
110
+ throw new TypeError(`${label} contains a container with more than ${maximumContainerItems} items.`);
111
+ }
112
+ if (objectKeys.some((key) => descriptors[key]?.enumerable !== true)) {
113
+ throw new TypeError(`${label} contains a non-enumerable object property.`);
114
+ }
115
+ keys += objectKeys.length;
116
+ if (keys > maximumKeys) {
117
+ throw new TypeError(`${label} exceeds the ${maximumKeys}-key limit.`);
118
+ }
119
+ for (const key of objectKeys) {
120
+ if (!isSafeJsonObjectKey(key))
121
+ throw new TypeError(`${label} contains an unsafe object key.`);
122
+ text(key);
123
+ visit(descriptors[key].value, depth + 1);
124
+ }
125
+ }
126
+ ancestors.delete(current);
127
+ };
128
+ visit(value, 0);
129
+ }
130
+ export function scanBoundedJson(raw, limits = {}, observeString) {
131
+ const maximumDepth = limits.maximumDepth ?? MAX_BOUNDED_JSON_DEPTH;
132
+ const maximumKeys = limits.maximumKeys ?? MAX_BOUNDED_JSON_KEYS;
133
+ const maximumValues = limits.maximumValues ?? MAX_BOUNDED_JSON_VALUES;
134
+ const maximumContainerItems = limits.maximumContainerItems ?? MAX_BOUNDED_JSON_CONTAINER_ITEMS;
135
+ let index = 0;
136
+ let keys = 0;
137
+ let values = 0;
138
+ let stringCodeUnits = 0;
139
+ const whitespace = () => {
140
+ while (index < raw.length) {
141
+ const code = raw.charCodeAt(index);
142
+ if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
143
+ break;
144
+ index += 1;
145
+ }
146
+ };
147
+ const stringToken = () => {
148
+ const start = index;
149
+ index += 1;
150
+ while (index < raw.length) {
151
+ const character = raw[index];
152
+ if (character === '"') {
153
+ index += 1;
154
+ let value;
155
+ try {
156
+ value = JSON.parse(raw.slice(start, index));
157
+ }
158
+ catch {
159
+ syntaxError();
160
+ }
161
+ stringCodeUnits += value.length;
162
+ if (limits.maximumStringCodeUnits !== undefined && stringCodeUnits > limits.maximumStringCodeUnits) {
163
+ throw new TypeError('Artefact JSON exceeds the aggregate text limit.');
164
+ }
165
+ return { value, offset: start };
166
+ }
167
+ if (character === '\\') {
168
+ index += 1;
169
+ if (index >= raw.length)
170
+ syntaxError();
171
+ if (raw[index] === 'u') {
172
+ if (!/^[a-fA-F0-9]{4}$/u.test(raw.slice(index + 1, index + 5)))
173
+ syntaxError();
174
+ index += 5;
175
+ }
176
+ else {
177
+ if (!/["\\/bfnrt]/u.test(raw[index]))
178
+ syntaxError();
179
+ index += 1;
180
+ }
181
+ continue;
182
+ }
183
+ if (character.charCodeAt(0) <= 0x1f)
184
+ syntaxError();
185
+ index += 1;
186
+ }
187
+ return syntaxError();
188
+ };
189
+ const literal = (token) => {
190
+ if (raw.slice(index, index + token.length) !== token)
191
+ syntaxError();
192
+ index += token.length;
193
+ };
194
+ const number = () => {
195
+ const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(raw.slice(index));
196
+ if (!match)
197
+ syntaxError();
198
+ if (!Number.isFinite(Number(match[0]))) {
199
+ throw new TypeError('Artefact JSON contains a non-finite number.');
200
+ }
201
+ index += match[0].length;
202
+ };
203
+ const value = (depth, propertyKey) => {
204
+ values += 1;
205
+ if (values > maximumValues) {
206
+ throw new TypeError(`Artefact JSON exceeds the ${maximumValues}-value limit.`);
207
+ }
208
+ if (depth > maximumDepth) {
209
+ throw new TypeError(`Artefact JSON exceeds the ${maximumDepth}-level nesting limit.`);
210
+ }
211
+ whitespace();
212
+ const character = raw[index];
213
+ if (character === '"') {
214
+ const parsedString = stringToken();
215
+ observeString?.({ ...parsedString, kind: 'value', ...(propertyKey === undefined ? {} : { propertyKey }) });
216
+ return;
217
+ }
218
+ if (character === '{') {
219
+ index += 1;
220
+ whitespace();
221
+ const seen = new Set();
222
+ if (raw[index] === '}') {
223
+ index += 1;
224
+ return;
225
+ }
226
+ while (index < raw.length) {
227
+ if (raw[index] !== '"')
228
+ syntaxError();
229
+ const parsedKey = stringToken();
230
+ const key = parsedKey.value;
231
+ if (seen.size >= maximumContainerItems) {
232
+ throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
233
+ }
234
+ keys += 1;
235
+ if (keys > maximumKeys) {
236
+ throw new TypeError(`Artefact JSON exceeds the ${maximumKeys}-key limit.`);
237
+ }
238
+ if (seen.has(key))
239
+ throw new TypeError('Artefact JSON contains a duplicate object key.');
240
+ if (!isSafeJsonObjectKey(key))
241
+ throw new TypeError('Artefact JSON contains an unsafe object key.');
242
+ seen.add(key);
243
+ observeString?.({ ...parsedKey, kind: 'key' });
244
+ whitespace();
245
+ if (raw[index] !== ':')
246
+ syntaxError();
247
+ index += 1;
248
+ value(depth + 1, key);
249
+ whitespace();
250
+ if (raw[index] === '}') {
251
+ index += 1;
252
+ return;
253
+ }
254
+ if (raw[index] !== ',')
255
+ syntaxError();
256
+ index += 1;
257
+ whitespace();
258
+ }
259
+ syntaxError();
260
+ }
261
+ if (character === '[') {
262
+ index += 1;
263
+ whitespace();
264
+ if (raw[index] === ']') {
265
+ index += 1;
266
+ return;
267
+ }
268
+ let items = 0;
269
+ while (index < raw.length) {
270
+ items += 1;
271
+ if (items > maximumContainerItems) {
272
+ throw new TypeError(`Artefact JSON contains a container with more than ${maximumContainerItems} items.`);
273
+ }
274
+ value(depth + 1);
275
+ whitespace();
276
+ if (raw[index] === ']') {
277
+ index += 1;
278
+ return;
279
+ }
280
+ if (raw[index] !== ',')
281
+ syntaxError();
282
+ index += 1;
283
+ whitespace();
284
+ }
285
+ syntaxError();
286
+ }
287
+ if (character === 't') {
288
+ literal('true');
289
+ return;
290
+ }
291
+ if (character === 'f') {
292
+ literal('false');
293
+ return;
294
+ }
295
+ if (character === 'n') {
296
+ literal('null');
297
+ return;
298
+ }
299
+ number();
300
+ };
301
+ whitespace();
302
+ value(0);
303
+ whitespace();
304
+ if (index !== raw.length)
305
+ syntaxError();
306
+ }
307
+ export function parseBoundedJson(raw, options) {
308
+ const label = options.label ?? 'Artefact input';
309
+ if (typeof raw !== 'string')
310
+ throw new TypeError(`${label} must be UTF-8 JSON text.`);
311
+ const bytes = new TextEncoder().encode(raw).byteLength;
312
+ if (bytes < 1 || bytes > options.maximumBytes) {
313
+ throw new TypeError(`${label} must be between 1 byte and ${options.maximumBytes} bytes.`);
314
+ }
315
+ scanBoundedJson(raw, options.limits);
316
+ try {
317
+ return JSON.parse(raw);
318
+ }
319
+ catch {
320
+ return syntaxError();
321
+ }
322
+ }
323
+ export function parseBoundedJsonObject(raw, options) {
324
+ const label = options.label ?? 'Artefact input';
325
+ const parsed = parseBoundedJson(raw, options);
326
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
327
+ throw new TypeError(`${label} must contain one JSON object.`);
328
+ }
329
+ return parsed;
330
+ }
@@ -0,0 +1,4 @@
1
+ // Browser-safe bounds shared by candidate discovery responses and the
2
+ // Discover-to-Bulk handoff. These are provenance labels, not domain names;
3
+ // they remain bounded independently of the target-domain contract.
4
+ export const MAX_CANDIDATE_SOURCE_LENGTH = 253;