@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
@@ -1,345 +1,2 @@
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
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/risk-scoring.mjs";
@@ -1,107 +1,2 @@
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
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/scoring-evidence-quality.mjs";
@@ -1,54 +1,2 @@
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
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/semantic-version.mjs";
@@ -376,6 +376,8 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
376
376
  throw new ServiceBindingDnsError('DNS response question does not match the query');
377
377
  }
378
378
  const records = [];
379
+ const aliases = new Map();
380
+ const answerOwners = new Set();
379
381
  let truncated = false;
380
382
  for (let index = 0; index < recordCount; index += 1) {
381
383
  const ownerResult = decodeDnsName(message, offset);
@@ -388,9 +390,18 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
388
390
  offset += 10;
389
391
  const rdataEnd = offset + rdataLength;
390
392
  assertRange(message, offset, rdataLength);
393
+ if (index < answerCount && rrClass === DNS_CLASS_IN && rrType === 5) {
394
+ const alias = decodeDnsName(message, offset);
395
+ if (alias.nextOffset !== rdataEnd || alias.name === '.'
396
+ || (aliases.has(ownerResult.name) && aliases.get(ownerResult.name) !== alias.name)) {
397
+ throw new ServiceBindingDnsError('DNS answer contains an invalid or ambiguous alias');
398
+ }
399
+ aliases.set(ownerResult.name, alias.name);
400
+ }
391
401
  if (index < answerCount
392
402
  && rrType === recordTypeCode(expected.type)
393
403
  && rrClass === DNS_CLASS_IN) {
404
+ answerOwners.add(ownerResult.name);
394
405
  const parsed = parseServiceBindingRecord(message, ownerResult.name, expected.type, ttl, offset, rdataEnd);
395
406
  truncated = parsed.truncated || truncated;
396
407
  if (records.length < MAX_SERVICE_BINDING_RECORDS)
@@ -402,6 +413,22 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
402
413
  }
403
414
  if (offset !== message.length)
404
415
  throw new ServiceBindingDnsError('DNS response has trailing bytes');
416
+ const allowedOwners = new Set();
417
+ let owner = question.name;
418
+ while (true) {
419
+ if (allowedOwners.has(owner))
420
+ throw new ServiceBindingDnsError('DNS answer contains a cyclic alias chain');
421
+ allowedOwners.add(owner);
422
+ const next = aliases.get(owner);
423
+ if (!next)
424
+ break;
425
+ if (answerOwners.has(owner))
426
+ throw new ServiceBindingDnsError('DNS alias owner also contains service-binding records');
427
+ owner = next;
428
+ }
429
+ if ([...answerOwners].some(value => !allowedOwners.has(value))) {
430
+ throw new ServiceBindingDnsError('DNS service-binding answer owner is not bound to the query');
431
+ }
405
432
  const unique = new Map();
406
433
  for (const record of records)
407
434
  unique.set(JSON.stringify(record), record);
@@ -499,6 +526,8 @@ function defaultTcpExchange(query, resolver, options) {
499
526
  }
500
527
  });
501
528
  socket.once('connect', () => {
529
+ if (settled)
530
+ return;
502
531
  const length = Buffer.alloc(2);
503
532
  length.writeUInt16BE(query.length, 0);
504
533
  socket.write(Buffer.concat([length, query]));
@@ -507,6 +536,7 @@ function defaultTcpExchange(query, resolver, options) {
507
536
  }
508
537
  function defaultDnsExchange(query, resolver, options) {
509
538
  return new Promise((resolve, reject) => {
539
+ options.signal?.throwIfAborted();
510
540
  const socket = createSocket(resolver.family === 6 ? 'udp6' : 'udp4');
511
541
  let settled = false;
512
542
  const timer = setTimeout(() => {
@@ -517,6 +547,7 @@ function defaultDnsExchange(query, resolver, options) {
517
547
  return;
518
548
  settled = true;
519
549
  clearTimeout(timer);
550
+ options.signal?.removeEventListener('abort', cancelled);
520
551
  try {
521
552
  socket.close();
522
553
  }
@@ -528,6 +559,12 @@ function defaultDnsExchange(query, resolver, options) {
528
559
  else
529
560
  resolve(response);
530
561
  };
562
+ const cancelled = () => finish(options.signal?.reason ?? new Error('DNS UDP query cancelled'));
563
+ options.signal?.addEventListener('abort', cancelled, { once: true });
564
+ if (options.signal?.aborted) {
565
+ cancelled();
566
+ return;
567
+ }
531
568
  socket.once('error', (error) => finish(error));
532
569
  socket.once('message', (message) => {
533
570
  if (message.length > MAX_DNS_MESSAGE_BYTES) {
@@ -538,6 +575,8 @@ function defaultDnsExchange(query, resolver, options) {
538
575
  }
539
576
  });
540
577
  socket.connect(resolver.port, resolver.address, () => {
578
+ if (settled)
579
+ return;
541
580
  socket.send(query, (error) => {
542
581
  if (error)
543
582
  finish(error);
@@ -565,6 +604,7 @@ function missingResponseError(rcode) {
565
604
  return new ServiceBindingDnsError(`DNS resolver returned ${names[rcode] || `rcode ${rcode}`}`, 'EDNSFAIL');
566
605
  }
567
606
  async function resolveServiceBindingRecords(nameValue, type, options = {}) {
607
+ options.signal?.throwIfAborted();
568
608
  const name = normalizeQueryName(nameValue);
569
609
  const transactionId = options.transactionId ?? randomInt(0x1_0000);
570
610
  const query = buildServiceBindingDnsQuery(name, type, transactionId);
@@ -581,17 +621,20 @@ async function resolveServiceBindingRecords(nameValue, type, options = {}) {
581
621
  const deadline = Date.now() + timeoutMs;
582
622
  let lastError = null;
583
623
  for (const resolver of resolvers) {
624
+ options.signal?.throwIfAborted();
584
625
  const udpTimeoutMs = deadline - Date.now();
585
626
  if (udpTimeoutMs <= 0)
586
627
  break;
587
628
  try {
588
- let response = await exchange(query, resolver, { timeoutMs: udpTimeoutMs });
629
+ let response = await exchange(query, resolver, { timeoutMs: udpTimeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
630
+ options.signal?.throwIfAborted();
589
631
  if (isTruncatedDnsResponse(response, transactionId)) {
590
632
  const tcpTimeoutMs = deadline - Date.now();
591
633
  if (tcpTimeoutMs <= 0) {
592
634
  throw new ServiceBindingDnsError('DNS service-binding query timed out', 'ETIMEOUT');
593
635
  }
594
- response = await (options.tcpExchange || defaultTcpExchange)(query, resolver, { timeoutMs: tcpTimeoutMs });
636
+ response = await (options.tcpExchange || defaultTcpExchange)(query, resolver, { timeoutMs: tcpTimeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
637
+ options.signal?.throwIfAborted();
595
638
  }
596
639
  const parsed = parseServiceBindingDnsResponse(response, { transactionId, name, type });
597
640
  if (parsed.truncatedResponse)
@@ -604,6 +647,7 @@ async function resolveServiceBindingRecords(nameValue, type, options = {}) {
604
647
  return { records: parsed.records, truncated: parsed.truncated };
605
648
  }
606
649
  catch (error) {
650
+ options.signal?.throwIfAborted();
607
651
  lastError = error;
608
652
  const code = error && typeof error === 'object' ? String(error.code || '') : '';
609
653
  if (code === 'ENODATA' || code === 'ENOTFOUND')