@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
@@ -0,0 +1,345 @@
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 8 keeps unreviewed whole-page similarity as visible review context
10
+ // without assigning it Risk points. Reviewed favicon and official-asset
11
+ // observations remain bounded within the brand-presentation family, generic
12
+ // operational properties remain supporting context, and missing evidence never
13
+ // contributes points.
14
+ export const RISK_MODEL_VERSION = 8;
15
+ export const RISK_REVIEW_THRESHOLD = 70;
16
+ const CURRENT_RISK_MODEL = Object.freeze({
17
+ modelVersion: RISK_MODEL_VERSION,
18
+ includePageBaselineMatch: false,
19
+ });
20
+ const RISK_MODEL_V7 = Object.freeze({
21
+ modelVersion: 7,
22
+ includePageBaselineMatch: true,
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].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 (input.hasPasswordField === true && input.hasExternalFormAction === true) {
163
+ add('credential-lure', '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
+ // Retained only for deterministic offline comparison of reviewed calibration
258
+ // datasets. Runtime lookups always use the current model above.
259
+ export function explainRiskScoreV6(input) {
260
+ const state = input.availability ?? input.state;
261
+ if (typeof state !== 'string' || !RISK_STATES.has(state))
262
+ return null;
263
+ const legacyBase = { registered: 10, for_sale: 5, expiring: 8 };
264
+ const factors = [];
265
+ const totals = new Map();
266
+ const add = (family, label, delta) => {
267
+ factors.push({ family, label, delta });
268
+ totals.set(family, (totals.get(family) ?? 0) + delta);
269
+ };
270
+ add('registration', `Base score for “${STATE_LABELS[state] ?? state}”`, legacyBase[state] ?? 0);
271
+ const contextual = new Set();
272
+ const context = mutationContext(input.mutationTypes);
273
+ if (context) {
274
+ add('domain-resemblance', context.label, context.delta);
275
+ contextual.add('domain-resemblance');
276
+ }
277
+ if (input.faviconMatch === true) {
278
+ add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
279
+ contextual.add('brand-presentation');
280
+ }
281
+ else if (input.faviconNearMatch === true) {
282
+ add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
283
+ contextual.add('brand-presentation');
284
+ }
285
+ if (input.reusesOfficialAssets === true) {
286
+ add('brand-presentation', 'Official asset-host relationship observed', 6);
287
+ contextual.add('brand-presentation');
288
+ }
289
+ if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
290
+ add('credential-lure', 'Suspicious urgency language observed', 8);
291
+ contextual.add('credential-lure');
292
+ }
293
+ if (input.hasPasswordField === true) {
294
+ add('credential-lure', 'Login/password form present', 5);
295
+ contextual.add('credential-lure');
296
+ }
297
+ if (contextual.size >= 2)
298
+ add('corroboration', `Corroborating context across ${contextual.size} distinct evidence families`, contextual.size >= 3 ? 20 : 10);
299
+ const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
300
+ if (external.factor)
301
+ add('external-intelligence', external.factor.label, external.contribution);
302
+ if (input.activityStatus === 'active')
303
+ add('operational-support', 'Active site in use', 8);
304
+ if (input.hasMx === true)
305
+ add('operational-support', 'Mail server configured', 8);
306
+ if (input.hasSpf === true && input.hasDmarc === true)
307
+ add('operational-support', 'SPF and DMARC records present', 3);
308
+ else if (input.hasSpf === true || input.hasDmarc === true)
309
+ add('operational-support', 'SPF or DMARC record present', 1);
310
+ if (input.privacyProtected === true)
311
+ add('registration', 'WHOIS privacy protected', 3);
312
+ if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
313
+ if (input.domainAgeDays < 90)
314
+ add('operational-support', `Recently registered (${formatAge(input.domainAgeDays)})`, 10);
315
+ else if (input.domainAgeDays < 365)
316
+ add('operational-support', `Registered under a year ago (${formatAge(input.domainAgeDays)})`, 4);
317
+ }
318
+ const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
319
+ const score = Math.max(0, Math.min(100, rawScore));
320
+ return {
321
+ modelVersion: 6,
322
+ score,
323
+ rawScore,
324
+ capped: score !== rawScore,
325
+ factors,
326
+ families: [...totals].map(([id, contribution]) => ({ id, contribution, cap: null })),
327
+ evidenceQuality: scoreQuality(input),
328
+ };
329
+ }
330
+ export function riskTone(score) {
331
+ if (score === null)
332
+ return 'neutral';
333
+ if (score >= RISK_REVIEW_THRESHOLD)
334
+ return 'danger';
335
+ if (score >= 40)
336
+ return 'warn';
337
+ return 'neutral';
338
+ }
339
+ export function formatScoreBreakdown(explained, separator = '\n') {
340
+ if (!explained)
341
+ return '';
342
+ const parts = explained.factors.map((factor) => `${factor.label} ${factor.delta >= 0 ? '+' : ''}${Math.round(factor.delta)}`);
343
+ parts.push(`Total ${explained.score} · Risk model v${explained.modelVersion} · evidence ${explained.evidenceQuality.state}`);
344
+ return parts.join(separator);
345
+ }
@@ -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
+ });