@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -0,0 +1,348 @@
1
+ import { calibrateExternalIntelligenceRisk } from "./external-intelligence-risk.mjs";
2
+ import { buildScoreEvidenceQuality, } from "./scoring-evidence-quality.mjs";
3
+ const RISK_STATES = new Set(['registered', 'for_sale', 'expiring']);
4
+ const STATE_LABELS = Object.freeze({
5
+ for_sale: 'for sale',
6
+ expiring: 'expiring/pending delete',
7
+ registered: 'registered',
8
+ });
9
+ // Version 9 requires form-scoped password/destination evidence. Older page-wide
10
+ // flags never establish that a password control belongs to an external form.
11
+ export const RISK_MODEL_VERSION = 9;
12
+ export const RISK_REVIEW_THRESHOLD = 70;
13
+ const CURRENT_RISK_MODEL = Object.freeze({
14
+ modelVersion: RISK_MODEL_VERSION,
15
+ includePageBaselineMatch: false,
16
+ requireFormAttribution: true,
17
+ });
18
+ const RISK_MODEL_V8 = Object.freeze({ modelVersion: 8, includePageBaselineMatch: false, requireFormAttribution: false });
19
+ const RISK_MODEL_V7 = Object.freeze({
20
+ modelVersion: 7,
21
+ includePageBaselineMatch: true,
22
+ requireFormAttribution: false,
23
+ });
24
+ const RISK_STATE_BASE = Object.freeze({
25
+ registered: 6,
26
+ for_sale: 4,
27
+ expiring: 5,
28
+ });
29
+ const FAMILY_CAPS = Object.freeze({
30
+ 'domain-resemblance': 20,
31
+ 'brand-presentation': 24,
32
+ 'credential-lure': 18,
33
+ 'external-intelligence': 18,
34
+ 'operational-support': 12,
35
+ });
36
+ const REMOVABLE_RISK_FAMILY_MAP = Object.freeze({
37
+ 'brand-presentation': true,
38
+ 'credential-lure': true,
39
+ 'domain-resemblance': true,
40
+ 'external-intelligence': true,
41
+ 'operational-support': true,
42
+ });
43
+ const REMOVABLE_RISK_FAMILIES = Object.freeze(Object.keys(REMOVABLE_RISK_FAMILY_MAP));
44
+ const HIGH_CONTEXT_MUTATIONS = new Set([
45
+ 'unicode_homoglyph',
46
+ 'unicode_homoglyph_depth_2',
47
+ 'dictionary',
48
+ 'dictionary_token_replacement',
49
+ ]);
50
+ const MEDIUM_CONTEXT_MUTATIONS = new Set(['ascii_homoglyph', 'bitsquatting', 'tld_embedding', 'tld_typo', 'tld_substitution']);
51
+ const LOW_CONTEXT_MUTATIONS = new Set([
52
+ 'character_addition',
53
+ 'character_omission',
54
+ 'character_duplication',
55
+ 'character_transposition',
56
+ 'pluralization',
57
+ 'www_prefix',
58
+ 'hyphenation',
59
+ 'separator_omission',
60
+ 'word_reordering',
61
+ 'keyboard_substitution',
62
+ 'keyboard_insertion',
63
+ 'vowel_swap',
64
+ ]);
65
+ export const RISK_MUTATION_TYPES = Object.freeze([
66
+ ...HIGH_CONTEXT_MUTATIONS,
67
+ ...MEDIUM_CONTEXT_MUTATIONS,
68
+ ...LOW_CONTEXT_MUTATIONS,
69
+ ]);
70
+ export function normalizeRiskModelVersion(value) {
71
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
72
+ }
73
+ function formatAge(days) {
74
+ if (days < 60)
75
+ return `${Math.round(days)}d old`;
76
+ const years = days / 365.25;
77
+ return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
78
+ }
79
+ function mutationContext(mutationTypes) {
80
+ if (!Array.isArray(mutationTypes))
81
+ return null;
82
+ const bounded = mutationTypes.slice(0, 30);
83
+ if (bounded.some((value) => typeof value === 'string' && HIGH_CONTEXT_MUTATIONS.has(value))) {
84
+ return { label: 'High-similarity or phishing-term candidate context', delta: 18 };
85
+ }
86
+ if (bounded.some((value) => typeof value === 'string' && MEDIUM_CONTEXT_MUTATIONS.has(value))) {
87
+ return { label: 'Lookalike candidate context', delta: 12 };
88
+ }
89
+ if (bounded.some((value) => typeof value === 'string' && LOW_CONTEXT_MUTATIONS.has(value))) {
90
+ return { label: 'Generated variation candidate context', delta: 8 };
91
+ }
92
+ return null;
93
+ }
94
+ function hasBoolean(value) {
95
+ return typeof value === 'boolean';
96
+ }
97
+ function scoreQuality(input) {
98
+ const observedFamilies = ['registration'];
99
+ const expectedFamilies = ['registration', 'operational-support'];
100
+ if (Array.isArray(input.mutationTypes) || hasBoolean(input.idnReferenceMatch))
101
+ observedFamilies.push('domain-resemblance');
102
+ if (input.hasActiveBrandProfile === true)
103
+ expectedFamilies.push('domain-resemblance', 'brand-presentation');
104
+ if ([input.faviconMatch, input.faviconNearMatch, input.reusesOfficialAssets, input.pageBaselineMatch].some(hasBoolean)) {
105
+ observedFamilies.push('brand-presentation');
106
+ }
107
+ if ([input.hasPasswordField, input.hasExternalFormAction, input.hasExternalPasswordForm].some(hasBoolean) || typeof input.phishingLanguageMatch === 'string') {
108
+ observedFamilies.push('credential-lure');
109
+ }
110
+ if (calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain).eligibleProviderCount > 0) {
111
+ observedFamilies.push('external-intelligence');
112
+ }
113
+ if ([input.hasMx, input.hasSpf, input.hasDmarc].some(hasBoolean)
114
+ || typeof input.activityStatus === 'string'
115
+ || typeof input.domainAgeDays === 'number')
116
+ observedFamilies.push('operational-support');
117
+ if (input.scanDepth === 'deep')
118
+ expectedFamilies.push('credential-lure');
119
+ return buildScoreEvidenceQuality(input, {
120
+ expectedDepth: 'deep',
121
+ observedFamilies,
122
+ expectedFamilies,
123
+ });
124
+ }
125
+ // Risk prioritizes a registered lookalike/typosquat domain for analyst review.
126
+ // It is a heuristic indicator, never a maliciousness or safety verdict.
127
+ function explainRiskScoreInternal(input, excludedFamily, model) {
128
+ const state = input.availability ?? input.state;
129
+ if (typeof state !== 'string' || !RISK_STATES.has(state))
130
+ return null;
131
+ const factors = [];
132
+ const familyTotals = new Map();
133
+ const add = (family, label, requestedDelta) => {
134
+ if (family === excludedFamily)
135
+ return 0;
136
+ const current = familyTotals.get(family) ?? 0;
137
+ const cap = FAMILY_CAPS[family] ?? Number.POSITIVE_INFINITY;
138
+ const delta = Math.max(0, Math.min(requestedDelta, Math.max(0, cap - current)));
139
+ const capped = delta !== requestedDelta;
140
+ factors.push({ family, label: capped ? `${label} (family cap reached)` : label, delta });
141
+ familyTotals.set(family, current + delta);
142
+ return delta;
143
+ };
144
+ add('registration', `Base context for “${STATE_LABELS[state] ?? state}”`, RISK_STATE_BASE[state] ?? 0);
145
+ const mutation = mutationContext(input.mutationTypes);
146
+ if (mutation)
147
+ add('domain-resemblance', mutation.label, mutation.delta);
148
+ if (input.idnReferenceMatch === true)
149
+ add('domain-resemblance', 'IDN skeleton matches an official Brand Profile domain', 20);
150
+ if (input.faviconMatch === true)
151
+ add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
152
+ else if (input.faviconNearMatch === true)
153
+ add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
154
+ if (input.reusesOfficialAssets === true)
155
+ add('brand-presentation', 'Official asset-host relationship observed', 6);
156
+ if (model.includePageBaselineMatch && input.pageBaselineMatch === true) {
157
+ add('brand-presentation', 'Multiple independently compared page-identity components match the official baseline', 14);
158
+ }
159
+ if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
160
+ add('credential-lure', 'Suspicious urgency language observed', 8);
161
+ }
162
+ if (model.requireFormAttribution ? input.hasExternalPasswordForm === true : input.hasPasswordField === true && input.hasExternalFormAction === true) {
163
+ add('credential-lure', model.requireFormAttribution ? 'Password form declares an external destination' : 'Password form submits to an external origin', 10);
164
+ }
165
+ else if (input.hasPasswordField === true) {
166
+ add('credential-lure', 'Login/password form present', 5);
167
+ }
168
+ else if (input.hasExternalFormAction === true) {
169
+ add('credential-lure', 'External form destination observed without a password field', 0);
170
+ }
171
+ const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
172
+ if (external.factor)
173
+ add('external-intelligence', external.factor.label, external.contribution);
174
+ const primaryFamilies = ['domain-resemblance', 'brand-presentation', 'credential-lure', 'external-intelligence']
175
+ .filter((family) => (familyTotals.get(family) ?? 0) > 0);
176
+ if (primaryFamilies.length >= 2) {
177
+ add('corroboration', `Corroborating context across ${primaryFamilies.length} independent evidence families`, primaryFamilies.length >= 3 ? 18 : 10);
178
+ }
179
+ const hasIndependentContext = primaryFamilies.length > 0;
180
+ const supporting = (label, delta) => {
181
+ add('operational-support', hasIndependentContext ? label : `${label}; neutral without independent suspicious context`, hasIndependentContext ? delta : 0);
182
+ };
183
+ if (input.activityStatus === 'active')
184
+ supporting('Active website observed', 4);
185
+ if (input.hasMx === true)
186
+ supporting('Mail server configured', 4);
187
+ if (input.hasSpf === true && input.hasDmarc === true)
188
+ supporting('SPF and DMARC records present', 1);
189
+ else if (input.hasSpf === true || input.hasDmarc === true)
190
+ supporting('SPF or DMARC record present', 0);
191
+ if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
192
+ if (input.domainAgeDays < 90)
193
+ supporting(`Recently registered (${formatAge(input.domainAgeDays)})`, 6);
194
+ else if (input.domainAgeDays < 365)
195
+ supporting(`Registered under a year ago (${formatAge(input.domainAgeDays)})`, 2);
196
+ }
197
+ if (typeof input.privacyProtected === 'boolean') {
198
+ add('registration', 'Registration privacy is neutral context', 0);
199
+ }
200
+ const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
201
+ const score = Math.max(0, Math.min(100, rawScore));
202
+ const familyOrder = [
203
+ 'registration', 'domain-resemblance', 'brand-presentation', 'credential-lure',
204
+ 'external-intelligence', 'corroboration', 'operational-support',
205
+ ];
206
+ return {
207
+ modelVersion: model.modelVersion,
208
+ score,
209
+ rawScore,
210
+ capped: score !== rawScore,
211
+ factors,
212
+ families: familyOrder
213
+ .filter((id) => familyTotals.has(id))
214
+ .map((id) => ({ id, contribution: familyTotals.get(id) ?? 0, cap: FAMILY_CAPS[id] ?? null })),
215
+ evidenceQuality: scoreQuality(input),
216
+ };
217
+ }
218
+ export function explainRiskScore(input) {
219
+ return explainRiskScoreInternal(input, null, CURRENT_RISK_MODEL);
220
+ }
221
+ export function buildRiskScoreSensitivity(input) {
222
+ const baseline = explainRiskScoreInternal(input, null, CURRENT_RISK_MODEL);
223
+ if (!baseline)
224
+ return null;
225
+ const removableFamilies = REMOVABLE_RISK_FAMILIES
226
+ .filter((family) => baseline.families.some((item) => item.id === family && item.contribution > 0));
227
+ const scenarios = removableFamilies.map((excludedFamily) => {
228
+ const result = explainRiskScoreInternal(input, excludedFamily, CURRENT_RISK_MODEL);
229
+ const score = result?.score ?? baseline.score;
230
+ return Object.freeze({ excludedFamily, score, difference: score - baseline.score });
231
+ }).sort((left, right) => left.score - right.score || left.excludedFamily.localeCompare(right.excludedFamily));
232
+ const minimumScenarioScore = scenarios[0]?.score ?? baseline.score;
233
+ const thresholdState = baseline.score < RISK_REVIEW_THRESHOLD
234
+ ? 'below'
235
+ : minimumScenarioScore < RISK_REVIEW_THRESHOLD ? 'crosses' : 'stable_above';
236
+ return Object.freeze({
237
+ version: 1,
238
+ baselineScore: baseline.score,
239
+ reviewThreshold: RISK_REVIEW_THRESHOLD,
240
+ minimumScenarioScore,
241
+ thresholdState,
242
+ scenarios: Object.freeze(scenarios),
243
+ limitations: Object.freeze([
244
+ 'Each scenario removes one observed evidence family and recalculates family caps and corroboration; it does not predict missing evidence.',
245
+ 'The sensitivity review qualifies prioritisation only and does not determine maliciousness, safety, ownership, or intent.',
246
+ ]),
247
+ });
248
+ }
249
+ export function computeRiskScore(input) {
250
+ return explainRiskScore(input)?.score ?? null;
251
+ }
252
+ // Retained only for deterministic offline comparison of reviewed calibration
253
+ // datasets. Runtime lookups always use the current model above.
254
+ export function explainRiskScoreV7(input) {
255
+ return explainRiskScoreInternal(input, null, RISK_MODEL_V7);
256
+ }
257
+ export function explainRiskScoreV8(input) {
258
+ return explainRiskScoreInternal(input, null, RISK_MODEL_V8);
259
+ }
260
+ // Retained only for deterministic offline comparison of reviewed calibration
261
+ // datasets. Runtime lookups always use the current model above.
262
+ export function explainRiskScoreV6(input) {
263
+ const state = input.availability ?? input.state;
264
+ if (typeof state !== 'string' || !RISK_STATES.has(state))
265
+ return null;
266
+ const legacyBase = { registered: 10, for_sale: 5, expiring: 8 };
267
+ const factors = [];
268
+ const totals = new Map();
269
+ const add = (family, label, delta) => {
270
+ factors.push({ family, label, delta });
271
+ totals.set(family, (totals.get(family) ?? 0) + delta);
272
+ };
273
+ add('registration', `Base score for “${STATE_LABELS[state] ?? state}”`, legacyBase[state] ?? 0);
274
+ const contextual = new Set();
275
+ const context = mutationContext(input.mutationTypes);
276
+ if (context) {
277
+ add('domain-resemblance', context.label, context.delta);
278
+ contextual.add('domain-resemblance');
279
+ }
280
+ if (input.faviconMatch === true) {
281
+ add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
282
+ contextual.add('brand-presentation');
283
+ }
284
+ else if (input.faviconNearMatch === true) {
285
+ add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
286
+ contextual.add('brand-presentation');
287
+ }
288
+ if (input.reusesOfficialAssets === true) {
289
+ add('brand-presentation', 'Official asset-host relationship observed', 6);
290
+ contextual.add('brand-presentation');
291
+ }
292
+ if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
293
+ add('credential-lure', 'Suspicious urgency language observed', 8);
294
+ contextual.add('credential-lure');
295
+ }
296
+ if (input.hasPasswordField === true) {
297
+ add('credential-lure', 'Login/password form present', 5);
298
+ contextual.add('credential-lure');
299
+ }
300
+ if (contextual.size >= 2)
301
+ add('corroboration', `Corroborating context across ${contextual.size} distinct evidence families`, contextual.size >= 3 ? 20 : 10);
302
+ const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
303
+ if (external.factor)
304
+ add('external-intelligence', external.factor.label, external.contribution);
305
+ if (input.activityStatus === 'active')
306
+ add('operational-support', 'Active site in use', 8);
307
+ if (input.hasMx === true)
308
+ add('operational-support', 'Mail server configured', 8);
309
+ if (input.hasSpf === true && input.hasDmarc === true)
310
+ add('operational-support', 'SPF and DMARC records present', 3);
311
+ else if (input.hasSpf === true || input.hasDmarc === true)
312
+ add('operational-support', 'SPF or DMARC record present', 1);
313
+ if (input.privacyProtected === true)
314
+ add('registration', 'WHOIS privacy protected', 3);
315
+ if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
316
+ if (input.domainAgeDays < 90)
317
+ add('operational-support', `Recently registered (${formatAge(input.domainAgeDays)})`, 10);
318
+ else if (input.domainAgeDays < 365)
319
+ add('operational-support', `Registered under a year ago (${formatAge(input.domainAgeDays)})`, 4);
320
+ }
321
+ const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
322
+ const score = Math.max(0, Math.min(100, rawScore));
323
+ return {
324
+ modelVersion: 6,
325
+ score,
326
+ rawScore,
327
+ capped: score !== rawScore,
328
+ factors,
329
+ families: [...totals].map(([id, contribution]) => ({ id, contribution, cap: null })),
330
+ evidenceQuality: scoreQuality(input),
331
+ };
332
+ }
333
+ export function riskTone(score) {
334
+ if (score === null)
335
+ return 'neutral';
336
+ if (score >= RISK_REVIEW_THRESHOLD)
337
+ return 'danger';
338
+ if (score >= 40)
339
+ return 'warn';
340
+ return 'neutral';
341
+ }
342
+ export function formatScoreBreakdown(explained, separator = '\n') {
343
+ if (!explained)
344
+ return '';
345
+ const parts = explained.factors.map((factor) => `${factor.label} ${factor.delta >= 0 ? '+' : ''}${Math.round(factor.delta)}`);
346
+ parts.push(`Total ${explained.score} · Risk model v${explained.modelVersion} · evidence ${explained.evidenceQuality.state}`);
347
+ return parts.join(separator);
348
+ }
@@ -0,0 +1,107 @@
1
+ export const SCORE_EVIDENCE_QUALITY_VERSION = 1;
2
+ const MAX_SOURCES = 24;
3
+ const MAX_FAMILIES = 12;
4
+ const SOURCE_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/u;
5
+ const COMPLETE_STATES = new Set(['complete', 'not_found', 'success']);
6
+ const LIMITED_STATES = new Set(['partial', 'unknown']);
7
+ const UNAVAILABLE_STATES = new Set(['error', 'rate_limited', 'unavailable']);
8
+ const SKIPPED_STATES = new Set(['skipped', 'unsupported']);
9
+ const ACCEPTED_SOURCE_STATES = new Set([
10
+ ...COMPLETE_STATES,
11
+ ...LIMITED_STATES,
12
+ ...UNAVAILABLE_STATES,
13
+ ...SKIPPED_STATES,
14
+ ]);
15
+ function boundedFamilyList(value) {
16
+ const output = new Set();
17
+ for (const item of value.slice(0, MAX_FAMILIES * 2)) {
18
+ const normalized = String(item).trim().toLowerCase().replace(/[^a-z0-9_-]+/gu, '-').slice(0, 64);
19
+ if (SOURCE_ID_RE.test(normalized))
20
+ output.add(normalized);
21
+ if (output.size >= MAX_FAMILIES)
22
+ break;
23
+ }
24
+ return [...output].sort();
25
+ }
26
+ function sourceCoverage(value) {
27
+ if (!Array.isArray(value))
28
+ return [];
29
+ const output = [];
30
+ const seen = new Set();
31
+ for (const candidate of value.slice(0, MAX_SOURCES * 4)) {
32
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
33
+ continue;
34
+ const item = candidate;
35
+ const source = typeof item.source === 'string' ? item.source.trim().toLowerCase() : '';
36
+ const state = typeof item.state === 'string' ? item.state.trim().toLowerCase() : '';
37
+ if (!SOURCE_ID_RE.test(source) || seen.has(source))
38
+ continue;
39
+ if (!ACCEPTED_SOURCE_STATES.has(state))
40
+ continue;
41
+ seen.add(source);
42
+ output.push({ source, state });
43
+ if (output.length >= MAX_SOURCES)
44
+ break;
45
+ }
46
+ return output;
47
+ }
48
+ function scanDepth(value) {
49
+ return value === 'deep' || value === 'fast' ? value : 'unknown';
50
+ }
51
+ function observedFreshness(value) {
52
+ if (typeof value !== 'string' || value.length > 64)
53
+ return 'unknown';
54
+ return Number.isFinite(Date.parse(value)) ? 'observed' : 'unknown';
55
+ }
56
+ export function buildScoreEvidenceQuality(input, options) {
57
+ const depth = scanDepth(input.scanDepth);
58
+ const coverage = sourceCoverage(input.sourceCoverage);
59
+ const completeSources = coverage.filter((item) => COMPLETE_STATES.has(item.state)).length;
60
+ const limitedSources = coverage.filter((item) => LIMITED_STATES.has(item.state)).length;
61
+ const unavailableSources = coverage.filter((item) => UNAVAILABLE_STATES.has(item.state)).length;
62
+ const skippedSources = coverage.filter((item) => SKIPPED_STATES.has(item.state)).length;
63
+ const observedFamilies = boundedFamilyList(options.observedFamilies ?? []);
64
+ const expectedFamilies = boundedFamilyList(options.expectedFamilies ?? []);
65
+ const observedSet = new Set(observedFamilies);
66
+ const missingFamilies = expectedFamilies.filter((family) => !observedSet.has(family));
67
+ const limitations = [];
68
+ let state = 'complete';
69
+ if (!coverage.length || depth === 'unknown')
70
+ state = 'unknown';
71
+ else if (unavailableSources || limitedSources)
72
+ state = 'partial';
73
+ else if (options.expectedDepth === 'deep' && depth !== 'deep')
74
+ state = 'limited';
75
+ if (missingFamilies.length && state === 'complete')
76
+ state = 'limited';
77
+ if (options.expectedDepth === 'deep' && depth !== 'deep') {
78
+ limitations.push('A fast or unknown-depth observation does not evaluate every website and mail scoring family.');
79
+ }
80
+ if (limitedSources || unavailableSources) {
81
+ limitations.push('One or more score-relevant sources were partial, unavailable, or indeterminate.');
82
+ }
83
+ if (skippedSources) {
84
+ limitations.push('One or more reported sources were skipped or unsupported and did not contribute to the score.');
85
+ }
86
+ if (!coverage.length)
87
+ limitations.push('Source-level coverage was not supplied with this score.');
88
+ if (missingFamilies.length)
89
+ limitations.push(`No usable observation was supplied for: ${missingFamilies.join(', ')}.`);
90
+ const boundedLimitations = [
91
+ ...limitations.slice(0, 5),
92
+ 'Evidence coverage qualifies the score; it never adds points or turns missing evidence into a negative finding.',
93
+ ];
94
+ return Object.freeze({
95
+ version: SCORE_EVIDENCE_QUALITY_VERSION,
96
+ state,
97
+ scanDepth: depth,
98
+ freshness: observedFreshness(input.observedAt),
99
+ completeSources,
100
+ limitedSources,
101
+ unavailableSources,
102
+ skippedSources,
103
+ observedFamilies: Object.freeze(observedFamilies),
104
+ missingFamilies: Object.freeze(missingFamilies),
105
+ limitations: Object.freeze(boundedLimitations),
106
+ });
107
+ }
@@ -0,0 +1,54 @@
1
+ // Dependency-free semantic-version validation shared by release tooling,
2
+ // runtime identity, and the frontend build. Callers provide a bounded label so
3
+ // errors retain their local context without reimplementing SemVer parsing.
4
+ export const MAX_SEMANTIC_VERSION_LENGTH = 128;
5
+ function fail(label, detail) {
6
+ throw new TypeError(`${label} ${detail}`);
7
+ }
8
+ function validateIdentifierList(value, label, kind, forbidNumericLeadingZero) {
9
+ const identifiers = value.split('.');
10
+ if (identifiers.some((identifier) => identifier.length === 0)) {
11
+ fail(label, `${kind} contains an empty identifier.`);
12
+ }
13
+ for (const identifier of identifiers) {
14
+ if (!/^[0-9A-Za-z-]+$/u.test(identifier)) {
15
+ fail(label, `${kind} contains an invalid identifier.`);
16
+ }
17
+ if (forbidNumericLeadingZero && /^[0-9]+$/u.test(identifier) && identifier.length > 1 && identifier.startsWith('0')) {
18
+ fail(label, `${kind} numeric identifiers must not contain leading zeroes.`);
19
+ }
20
+ }
21
+ }
22
+ export function normalizeBoundedSemanticVersion(value, label = 'Semantic') {
23
+ if (typeof value !== 'string'
24
+ || value.length === 0
25
+ || value.length > MAX_SEMANTIC_VERSION_LENGTH
26
+ || value.trim() !== value) {
27
+ fail(label, 'version must be a bounded semantic-version string.');
28
+ }
29
+ const buildParts = value.split('+');
30
+ if (buildParts.length > 2)
31
+ fail(label, 'version contains more than one build-metadata separator.');
32
+ const [precedence, buildMetadata] = buildParts;
33
+ if (!precedence)
34
+ fail(label, 'version must include numeric precedence.');
35
+ const prereleaseSeparator = precedence.indexOf('-');
36
+ const core = prereleaseSeparator === -1 ? precedence : precedence.slice(0, prereleaseSeparator);
37
+ const prerelease = prereleaseSeparator === -1 ? undefined : precedence.slice(prereleaseSeparator + 1);
38
+ const coreParts = core.split('.');
39
+ if (coreParts.length !== 3 || coreParts.some((part) => !/^(?:0|[1-9][0-9]*)$/u.test(part))) {
40
+ fail(label, 'version must contain major, minor, and patch numbers without leading zeroes.');
41
+ }
42
+ if (prerelease !== undefined)
43
+ validateIdentifierList(prerelease, label, 'prerelease', true);
44
+ if (buildMetadata !== undefined)
45
+ validateIdentifierList(buildMetadata, label, 'build metadata', false);
46
+ return value;
47
+ }
48
+ export function normalizeBoundedStableSemanticVersion(value, label = 'Stable semantic') {
49
+ const version = normalizeBoundedSemanticVersion(value, label);
50
+ if (version.includes('-') || version.includes('+')) {
51
+ fail(label, 'version must not include prerelease or build identifiers.');
52
+ }
53
+ return version;
54
+ }
@@ -0,0 +1,75 @@
1
+ // Provider-neutral types shared by threat-intelligence and curated connector
2
+ // manifests, fixture harnesses, normalizers, and result builders.
3
+ export const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
4
+ export const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
5
+ export const THREAT_INTELLIGENCE_ENVELOPE_VERSION = 1;
6
+ export const THREAT_INTELLIGENCE_TARGET_EXPOSURES = Object.freeze([
7
+ 'registrable_domain', 'hostname', 'origin', 'full_url',
8
+ ]);
9
+ export const THREAT_INTELLIGENCE_CAPABILITIES = Object.freeze([
10
+ 'domain_lookup', 'url_lookup', 'indicator_search',
11
+ ]);
12
+ export const THREAT_INTELLIGENCE_CATEGORIES = Object.freeze([
13
+ 'phishing',
14
+ 'malware',
15
+ 'spam',
16
+ 'suspicious',
17
+ 'abuse',
18
+ 'unknown',
19
+ ]);
20
+ export const THREAT_INTELLIGENCE_SEVERITIES = Object.freeze([
21
+ 'critical',
22
+ 'high',
23
+ 'medium',
24
+ 'low',
25
+ 'unknown',
26
+ ]);
27
+ export const THREAT_INTELLIGENCE_CONFIDENCES = Object.freeze([
28
+ 'high',
29
+ 'medium',
30
+ 'low',
31
+ 'unknown',
32
+ ]);
33
+ export const THREAT_INTELLIGENCE_RESULT_STATES = Object.freeze([
34
+ 'success',
35
+ 'partial',
36
+ 'not_found',
37
+ 'unsupported',
38
+ 'skipped',
39
+ 'rate_limited',
40
+ 'unavailable',
41
+ 'error',
42
+ ]);
43
+ export const CURATED_CONNECTOR_KINDS = Object.freeze(['discovery', 'enrichment']);
44
+ export const CURATED_CONNECTOR_COLLECTIONS = Object.freeze(['passive', 'active', 'third_party']);
45
+ export const CURATED_CONNECTOR_CREDENTIAL_MODES = Object.freeze(['none', 'optional', 'required']);
46
+ export const CURATED_CONNECTOR_ENTITY_VALUES = Object.freeze([
47
+ 'domain', 'hostname', 'url', 'ipv4', 'ipv6', 'asn', 'certificate',
48
+ ]);
49
+ export const CURATED_CONNECTOR_RELATIONSHIP_TYPES = Object.freeze([
50
+ 'domain_resolves_to_ip', 'domain_uses_nameserver', 'domain_uses_mail_server',
51
+ 'domain_presented_certificate', 'certificate_names_domain', 'ip_hosts_domain',
52
+ 'domain_related_to_domain',
53
+ ]);
54
+ export const CURATED_CONNECTOR_RELATIONSHIP_CLASSIFICATIONS = Object.freeze(['direct', 'normalized', 'derived']);
55
+ export const CURATED_CONNECTOR_TARGET_EXPOSURES = Object.freeze({
56
+ domain: Object.freeze(['registrable_domain']),
57
+ hostname: Object.freeze(['hostname']),
58
+ url: THREAT_INTELLIGENCE_TARGET_EXPOSURES,
59
+ ipv4: Object.freeze(['ip_address']),
60
+ ipv6: Object.freeze(['ip_address']),
61
+ asn: Object.freeze(['asn']),
62
+ certificate: Object.freeze(['certificate_fingerprint']),
63
+ });
64
+ function relationshipEndpoints(from, to) {
65
+ return Object.freeze({ from: Object.freeze(from), to: Object.freeze(to) });
66
+ }
67
+ export const CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS = Object.freeze({
68
+ domain_resolves_to_ip: relationshipEndpoints(['domain', 'hostname'], ['ipv4', 'ipv6']),
69
+ domain_uses_nameserver: relationshipEndpoints(['domain'], ['hostname']),
70
+ domain_uses_mail_server: relationshipEndpoints(['domain'], ['hostname']),
71
+ domain_presented_certificate: relationshipEndpoints(['domain', 'hostname'], ['certificate']),
72
+ certificate_names_domain: relationshipEndpoints(['certificate'], ['domain', 'hostname']),
73
+ ip_hosts_domain: relationshipEndpoints(['ipv4', 'ipv6'], ['domain', 'hostname']),
74
+ domain_related_to_domain: relationshipEndpoints(['domain', 'hostname'], ['domain', 'hostname']),
75
+ });