@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
@@ -716,4 +716,4 @@ function analyzeStaticHtml(value, options = {}) {
716
716
  inlineCharactersExamined,
717
717
  };
718
718
  }
719
- export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, analyzeStaticHtml, };
719
+ export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_REFERENCE_LENGTH, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, analyzeStaticHtml, };
@@ -6,7 +6,7 @@
6
6
  import { domainToASCII } from 'node:url';
7
7
  import { createObservation } from "../packages/evidence/observation.mjs";
8
8
  import { MAX_STRUCTURED_DATA_ENTITIES, MAX_STRUCTURED_DATA_SAME_AS_HOSTS, STRUCTURED_DATA_IDENTITY_VERSION, } from "./lookup-child-profile-contract.mjs";
9
- import { isUriShapedLabel } from "./portable-generator.mjs";
9
+ import { isUriShapedLabel } from "../packages/analysis/portable-generator.mjs";
10
10
  import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
11
11
  const MAX_STRUCTURED_DATA_SCRIPTS = 8;
12
12
  const MAX_STRUCTURED_DATA_SCRIPT_CHARS = 16_384;
@@ -1,7 +1,7 @@
1
1
  import { registerCuratedConnector, registerThreatIntelligenceProvider, isCuratedConnectorDefinition, isThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
2
2
  import { CURATED_CONNECTOR_CONTRACT_VERSION, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_MS, THREAT_INTELLIGENCE_CONTRACT_VERSION, } from "./threat-intelligence-runtime.mjs";
3
3
  import { boundedInteger, boundedHttpsUrl, enumValue, exactKeys, isRecord, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
4
- import { THREAT_INTELLIGENCE_CAPABILITIES, THREAT_INTELLIGENCE_TARGET_EXPOSURES, CURATED_CONNECTOR_KINDS, CURATED_CONNECTOR_COLLECTIONS, CURATED_CONNECTOR_CREDENTIAL_MODES, CURATED_CONNECTOR_ENTITY_VALUES, CURATED_CONNECTOR_RELATIONSHIP_TYPES, CURATED_CONNECTOR_TARGET_EXPOSURES, CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS as CONNECTOR_RELATIONSHIP_ENDPOINTS, } from "./threat-intelligence-types.mjs";
4
+ import { THREAT_INTELLIGENCE_CAPABILITIES, THREAT_INTELLIGENCE_TARGET_EXPOSURES, CURATED_CONNECTOR_KINDS, CURATED_CONNECTOR_COLLECTIONS, CURATED_CONNECTOR_CREDENTIAL_MODES, CURATED_CONNECTOR_ENTITY_VALUES, CURATED_CONNECTOR_RELATIONSHIP_TYPES, CURATED_CONNECTOR_TARGET_EXPOSURES, CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS as CONNECTOR_RELATIONSHIP_ENDPOINTS, } from "../packages/analysis/threat-intelligence-types.mjs";
5
5
  import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
6
6
  const MAX_PROVIDER_ID_LENGTH = 40;
7
7
  const MAX_PROVIDER_LABEL_LENGTH = 100;
@@ -5,7 +5,7 @@
5
5
  // output budgets before producing separately attributed normalized evidence.
6
6
  import { createObservation } from "../packages/evidence/observation.mjs";
7
7
  import { latestObservationCohort } from "../packages/evidence/latest-observations.mjs";
8
- import { CURATED_CONNECTOR_RELATIONSHIP_TYPES, CURATED_CONNECTOR_RELATIONSHIP_CLASSIFICATIONS, CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS as CONNECTOR_RELATIONSHIP_ENDPOINTS, THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "./threat-intelligence-types.mjs";
8
+ import { CURATED_CONNECTOR_RELATIONSHIP_TYPES, CURATED_CONNECTOR_RELATIONSHIP_CLASSIFICATIONS, CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS as CONNECTOR_RELATIONSHIP_ENDPOINTS, THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "../packages/analysis/threat-intelligence-types.mjs";
9
9
  import { assertCuratedConnectorDefinition, assertThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
10
10
  import { boundedHttpsUrl, boundedString, isRecord, isoTimestamp, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
11
11
  import { CURATED_CONNECTOR_ENTITY_TYPES, MAX_THREAT_INTELLIGENCE_URL_LENGTH, normalizeCuratedConnectorTarget, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-targets.mjs";
@@ -2,7 +2,7 @@
2
2
  // and curated connectors. These helpers never perform network requests.
3
3
  import { classifyQuery } from "./classify.mjs";
4
4
  import { enumValue, exactKeys, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
5
- import { CURATED_CONNECTOR_ENTITY_VALUES, CURATED_CONNECTOR_TARGET_EXPOSURES, THREAT_INTELLIGENCE_TARGET_EXPOSURES, } from "./threat-intelligence-types.mjs";
5
+ import { CURATED_CONNECTOR_ENTITY_VALUES, CURATED_CONNECTOR_TARGET_EXPOSURES, THREAT_INTELLIGENCE_TARGET_EXPOSURES, } from "../packages/analysis/threat-intelligence-types.mjs";
6
6
  export const MAX_THREAT_INTELLIGENCE_URL_LENGTH = 2048;
7
7
  const TARGET_EXPOSURES = Object.freeze({
8
8
  domain: Object.freeze(['registrable_domain']),
@@ -1,75 +1,2 @@
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
- });
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/threat-intelligence-types.mjs";
@@ -2,7 +2,7 @@
2
2
  // objects so a later scan can explain why each domain was generated; callers
3
3
  // that only need the historical string list can use the compatibility wrapper.
4
4
  import { advancedConfusableVariantsForAscii, confusableCharactersForAscii, estimateAdvancedConfusableVariants, MAX_ADVANCED_CONFUSABLE_VARIANTS, wholeLabelConfusableVariantsForAscii, } from "./idn-confusables.mjs";
5
- import { canonicalRegistrableDomain, normalizeDiscoverySuffix } from "./registrable-domain.mjs";
5
+ import { canonicalRegistrableDomain, normalizeDiscoverySuffix } from "../packages/analysis/registrable-domain.mjs";
6
6
  import { MAX_DOMAIN_NAME_LENGTH, MAX_DOMAIN_LABEL_LENGTH } from "../packages/contracts/domain-name.mjs";
7
7
  export { MAX_ADVANCED_CONFUSABLE_VARIANTS };
8
8
  function buildKeyboardAdjacency(rows) {
@@ -775,12 +775,21 @@ export function generateTyposquatCandidateSet(rawInput, fallbackTlds, options =
775
775
  const advanced = advancedConfusableVariantsForAscii(name);
776
776
  const rejectedBefore = state.rejectedVariantCount;
777
777
  let rejectedByEncoding = 0;
778
+ const encodedLabels = new Set();
779
+ let duplicatesAfterEncoding = 0;
778
780
  for (const variant of advanced.variants) {
779
781
  const ascii = toAsciiLabel(variant.unicodeLabel);
780
782
  if (!ascii) {
781
783
  rejectedByEncoding += 1;
782
784
  continue;
783
785
  }
786
+ // Different Unicode spellings can identify the same IDNA domain. They
787
+ // are duplicate candidates, not work omitted by a resource budget.
788
+ if (encodedLabels.has(ascii)) {
789
+ duplicatesAfterEncoding += 1;
790
+ continue;
791
+ }
792
+ encodedLabels.add(ascii);
784
793
  addVariant(state, ascii, 'unicode_homoglyph_depth_2');
785
794
  }
786
795
  const generated = [...state.variants.values()]
@@ -788,7 +797,7 @@ export function generateTyposquatCandidateSet(rawInput, fallbackTlds, options =
788
797
  .length;
789
798
  const rejectedByValidation = state.rejectedVariantCount - rejectedBefore;
790
799
  const omittedByBudget = advanced.omittedByBudget
791
- + Math.max(0, advanced.variants.length - rejectedByEncoding - rejectedByValidation - generated);
800
+ + Math.max(0, advanced.variants.length - duplicatesAfterEncoding - rejectedByEncoding - rejectedByValidation - generated);
792
801
  advancedConfusable = {
793
802
  generated,
794
803
  omittedByPolicy: advanced.omittedByPolicy + rejectedByEncoding + rejectedByValidation,
@@ -0,0 +1,388 @@
1
+ // Bounded website and network enrichment, independent of registration authority.
2
+ import { capturedWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
3
+ import { recordOrEmpty as errorRecord } from "./json-record.mjs";
4
+ import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
5
+ import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
6
+ import { collectDnsIntelligence, skippedDnsIntelligence } from "./dns-intelligence.mjs";
7
+ import { fetchFaviconHash } from "./favicon.mjs";
8
+ import { extractHtmlSignals } from "./html-signals.mjs";
9
+ import { analyzeStaticHtml } from "./static-html-analysis.mjs";
10
+ import { featureDecision, networkFeaturePolicy } from "./feature-policy.mjs";
11
+ import { withoutHttpDeliveryMetadata, buildHttpObservation, failedHttpObservation, skippedHttpObservation } from "./http-intelligence.mjs";
12
+ import { collectTlsIntelligence, skippedTlsObservation } from "./tls-intelligence.mjs";
13
+ import { analyzeWebsiteSecurityPosture } from "./website-security-posture.mjs";
14
+ import { analyzeWebsiteTechnology, captureTechnologyResponseHeaders, } from "./website-technology.mjs";
15
+ import { analyzeResponsePolicyHeaders, qualifyResponsePolicyWithCspMeta, } from "./response-policy.mjs";
16
+ import { nonEmptyErrorMessage } from "./error-detail.mjs";
17
+ import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
18
+ import { HOMEPAGE_FETCH_TIMEOUT_MS, MAX_HOMEPAGE_BYTES, } from "./outbound-request-bounds.mjs";
19
+ // No marketplace (Afternic/Sedo/Dan.com/GoDaddy Auctions/etc.) offers a
20
+ // free, no-auth API to check "is this specific domain listed for sale" -
21
+ // what's checkable without credentials is the parking/landing-page
22
+ // nameservers and homepage copy these services actually use, which is
23
+ // broader coverage of the same signal, not a live cross-marketplace lookup.
24
+ const PARKING_NS_PATTERNS = [
25
+ /sedoparking\.com$/i,
26
+ /sedo\.com$/i,
27
+ /above\.com$/i,
28
+ /bodis\.com$/i,
29
+ /parkingcrew\.net$/i,
30
+ /dan\.com$/i,
31
+ /hugedomains\.com$/i,
32
+ /uniregistry/i,
33
+ /squadhelp/i,
34
+ /afternic/i,
35
+ /voodoo\.com$/i,
36
+ /fabulous\.com$/i,
37
+ /namedrive/i,
38
+ /smartname\.com$/i,
39
+ /domainsponsor\.com$/i,
40
+ /undeveloped\.com$/i,
41
+ /trafficz\.com$/i,
42
+ /dsredirection\.com$/i,
43
+ ];
44
+ const FOR_SALE_PATH_RE = /\/(?:premium-)?domains?-for-sale(?:\/|$)/i;
45
+ function forSaleRedirectSignal(httpObservation) {
46
+ if (!httpObservation || typeof httpObservation !== 'object')
47
+ return null;
48
+ const observation = httpObservation;
49
+ const urls = [
50
+ observation.finalUrl,
51
+ ...(Array.isArray(observation.redirects)
52
+ ? observation.redirects.map((redirect) => redirect && typeof redirect === 'object' ? redirect.to : null)
53
+ : []),
54
+ ];
55
+ for (const value of urls) {
56
+ if (typeof value !== 'string' || value.length > 2048)
57
+ continue;
58
+ try {
59
+ const url = new URL(value);
60
+ if (['http:', 'https:'].includes(url.protocol) && FOR_SALE_PATH_RE.test(url.pathname)) {
61
+ return `for-sale landing-page redirect (${url.origin}${url.pathname})`;
62
+ }
63
+ }
64
+ catch {
65
+ // Malformed retained provenance cannot establish a sale signal.
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ // Fetches enough of the homepage for the lightweight HTML signals below and
71
+ // preserves why the probe failed. A failed request is not evidence that a
72
+ // domain has "no site": transient DNS/TLS/network failures, a slow origin, or
73
+ // an HTTP error can all produce the same null body. Keeping that distinction
74
+ // avoids turning an inconclusive probe into a false inactivity claim.
75
+ async function fetchHomepage(domain, { fetcher = safeFetchDetailed, timeoutMs = HOMEPAGE_FETCH_TIMEOUT_MS, selectedUrl, signal } = {}) {
76
+ const selected = selectedUrl === undefined ? null : prepareSelectedLookupUrl(selectedUrl, domain);
77
+ const requestTimeoutMs = Number.isInteger(timeoutMs) && timeoutMs >= 10 && timeoutMs <= HOMEPAGE_FETCH_TIMEOUT_MS
78
+ ? timeoutMs
79
+ : HOMEPAGE_FETCH_TIMEOUT_MS;
80
+ const headers = whoisleuthRequestHeaders();
81
+ const failures = [];
82
+ const probeStartedAt = Date.now();
83
+ for (const requestUrl of selected ? [selected] : [`https://${domain}`, `http://${domain}`]) {
84
+ signal?.throwIfAborted();
85
+ const scheme = new URL(requestUrl).protocol.slice(0, -1);
86
+ const attemptStartedAt = Date.now();
87
+ const controller = new AbortController();
88
+ const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
89
+ try {
90
+ const fetched = await fetcher(requestUrl, { signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal, headers });
91
+ const fetchedResponse = fetched instanceof Response ? fetched : fetched.response;
92
+ const detail = fetched && typeof fetched === 'object' && 'response' in fetched
93
+ ? fetched
94
+ : {
95
+ response: fetchedResponse,
96
+ requestedUrl: requestUrl,
97
+ finalUrl: requestUrl,
98
+ redirectCount: 0,
99
+ redirectLimitReached: false,
100
+ hops: [{ url: requestUrl, status: fetchedResponse.status, location: null, durationMs: Date.now() - attemptStartedAt }],
101
+ durationMs: Date.now() - attemptStartedAt,
102
+ };
103
+ const res = detail.response;
104
+ const analysisBase = new URL(detail.finalUrl);
105
+ analysisBase.search = '';
106
+ analysisBase.hash = '';
107
+ // Retain an explicitly partial prefix when the body exceeds its bound;
108
+ // downstream analyses must not treat it as a complete page. The
109
+ // timeout stays armed through this read (cleared in `finally` below,
110
+ // not here) - a malicious site could otherwise send headers
111
+ // immediately and then trickle or stall the body forever, hanging
112
+ // this worker with no deadline once the timer above is disarmed.
113
+ if (res.ok) {
114
+ const body = await readTextCapped(res, MAX_HOMEPAGE_BYTES, { includeSha256: true });
115
+ return {
116
+ text: body.text,
117
+ status: 'fetched',
118
+ detail: `${selected ? 'Selected URL' : 'Homepage'} responded over ${scheme.toUpperCase()} (HTTP ${res.status}).`,
119
+ responsePolicy: analyzeResponsePolicyHeaders(res.headers),
120
+ technologyHeaders: captureTechnologyResponseHeaders(res.headers),
121
+ ...(selected ? { analysisBaseUrl: analysisBase.toString() } : {}),
122
+ http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
123
+ previousAttempts: failures,
124
+ capturedBodyBytes: body.bytesRead,
125
+ bodyInspected: true,
126
+ bodyTruncated: body.truncated,
127
+ bodySha256: body.sha256,
128
+ }),
129
+ };
130
+ }
131
+ // A non-2xx HTTP response still conclusively proves that a web service
132
+ // answered on this domain. It may block this probe (403), require auth
133
+ // (401), have a broken homepage route (404), or be unhealthy (5xx), but
134
+ // none of those are equivalent to "no website". We cannot inspect its
135
+ // HTML signals, so preserve a separate responded status and release the
136
+ // unused body.
137
+ await res.body?.cancel().catch(() => { });
138
+ return {
139
+ text: null,
140
+ status: 'responded',
141
+ detail: `Web server responded over ${scheme.toUpperCase()} (HTTP ${res.status}); ${selected ? 'selected page' : 'homepage'} content was not available for inspection.`,
142
+ responsePolicy: analyzeResponsePolicyHeaders(res.headers),
143
+ technologyHeaders: captureTechnologyResponseHeaders(res.headers),
144
+ http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
145
+ previousAttempts: failures,
146
+ capturedBodyBytes: 0,
147
+ bodyInspected: false,
148
+ }),
149
+ };
150
+ }
151
+ catch (err) {
152
+ signal?.throwIfAborted();
153
+ const error = errorRecord(err);
154
+ const reason = error.name === 'AbortError'
155
+ ? `timed out after ${requestTimeoutMs} milliseconds`
156
+ : selected ? 'request failed; the selected URL was not retried at another path or scheme'
157
+ : nonEmptyErrorMessage(err, 'request failed')
158
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
159
+ .slice(0, 180);
160
+ failures.push({ url: requestUrl, error: `${scheme.toUpperCase()} ${reason}` });
161
+ }
162
+ finally {
163
+ clearTimeout(timeout);
164
+ }
165
+ }
166
+ return {
167
+ text: null,
168
+ status: 'inconclusive',
169
+ detail: failures.length
170
+ ? `Could not confirm ${selected ? 'selected URL' : 'homepage'} activity: ${failures.map((attempt) => attempt.error).join('; ')}.`
171
+ : 'Could not confirm homepage activity.',
172
+ http: failedHttpObservation(failures, { durationMs: Date.now() - probeStartedAt }),
173
+ };
174
+ }
175
+ function deriveWebsiteActivity(homepageStatus, hasFavicon, alreadyParked = false) {
176
+ if (alreadyParked)
177
+ return 'parked';
178
+ if (homepageStatus === 'fetched' || homepageStatus === 'responded' || hasFavicon)
179
+ return 'active';
180
+ return 'unreachable';
181
+ }
182
+ async function enrichWebsite(context, options, featurePolicy) {
183
+ const { domain, observationHostname, selectedUrl, registration, registryDnsEvidence, nameservers, dnssec } = context;
184
+ const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
185
+ const collectDns = options.collectDnsIntelligence || collectDnsIntelligence;
186
+ const collectTls = options.collectTlsIntelligence || collectTlsIntelligence;
187
+ const fetchHomepageForDomain = options.fetchHomepage || fetchHomepage;
188
+ const fetchFaviconForDomain = options.fetchFaviconHash || fetchFaviconHash;
189
+ const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
190
+ const tlsIntelligenceEnabled = featureDecision('tls_intelligence', featurePolicy).enabled;
191
+ const deepScanComplete = ['rdap', 'whois', 'dns_intelligence', 'website_probe', 'tls_intelligence']
192
+ .every((feature) => featureDecision(feature, featurePolicy).enabled);
193
+ // Registered - look for for-sale/parked/website signals (homepage fetch,
194
+ // deep mode only), and check for a configured mail exchanger as a
195
+ // phishing-risk signal (a lookalike domain that can receive/send mail is
196
+ // capable of running credential-harvesting or BEC campaigns).
197
+ const nsSignal = registration.state === 'registered' ? nameservers.find((ns) => PARKING_NS_PATTERNS.some((re) => re.test(ns))) : undefined;
198
+ let forSaleSignal = nsSignal ? `parking nameserver (${nsSignal})` : null;
199
+ let activityStatus = nsSignal && observationHostname === domain ? 'parked' : 'unknown';
200
+ // Homepage + bounded DNS evidence resolve in parallel; the favicon fetch is sequenced after
201
+ // the homepage so it can use any <link rel="icon"> the page declares (many
202
+ // sites serve no /favicon.ico and only point to a CDN PNG this way). One
203
+ // extra round-trip on the already-slow deep path, in exchange for finding
204
+ // favicons the bare /favicon.ico probe would miss.
205
+ const [homepage, dnsIntelligence, tlsIntelligence] = await Promise.all([
206
+ websiteProbeEnabled ? fetchHomepageForDomain(observationHostname, {
207
+ ...(selectedUrl ? { selectedUrl } : {}), ...(options.signal ? { signal: options.signal } : {}),
208
+ }).catch((err) => ({
209
+ text: null,
210
+ status: 'inconclusive',
211
+ detail: selectedUrl ? 'Could not confirm selected URL activity.'
212
+ : `Could not confirm homepage activity: ${String(err && err.message ? err.message : 'request failed').slice(0, 180)}.`,
213
+ http: failedHttpObservation([
214
+ { url: selectedUrl ?? `https://${observationHostname}`, error: selectedUrl ? 'request failed' : String(err && err.message ? err.message : 'request failed') },
215
+ ]),
216
+ })) : Promise.resolve({
217
+ text: null,
218
+ status: 'skipped',
219
+ detail: 'Website probing is disabled by deployment policy.',
220
+ http: skippedHttpObservation(),
221
+ }),
222
+ dnsIntelligenceEnabled
223
+ ? collectDns(observationHostname, {
224
+ ...(options.dnsResolvers ? { resolvers: options.dnsResolvers } : {}),
225
+ includeExtendedContext: options.includeExtendedDnsContext === true,
226
+ includeInheritedCaa: options.includeInheritedCaa === true,
227
+ registryEvidence: registryDnsEvidence,
228
+ ...(observationHostname !== domain ? { registrationDomain: domain } : {}),
229
+ })
230
+ : Promise.resolve(skippedDnsIntelligence('DNS intelligence is disabled by deployment policy.', {
231
+ includeExtendedContext: options.includeExtendedDnsContext === true,
232
+ includeInheritedCaa: options.includeInheritedCaa === true,
233
+ })),
234
+ tlsIntelligenceEnabled
235
+ ? collectTls(observationHostname)
236
+ : Promise.resolve(skippedTlsObservation()),
237
+ ]);
238
+ options.signal?.throwIfAborted();
239
+ const page = homepage.text;
240
+ const pageBaseUrl = homepage.analysisBaseUrl ?? (typeof homepage.http?.finalUrl === 'string' ? homepage.http.finalUrl : `https://${observationHostname}/`);
241
+ let pageAnalysis = page ? analyzeStaticHtml(page, { baseUrl: pageBaseUrl, includeVisibleText: true }) : undefined;
242
+ let htmlSignals = {
243
+ domainSaleSignal: null,
244
+ pageTitle: null,
245
+ hasPasswordField: false,
246
+ phishingLanguageMatch: null,
247
+ hasExternalFormAction: null,
248
+ externalAssetHosts: [],
249
+ cspMetaPolicy: null,
250
+ pageIdentity: null,
251
+ credentialSurfaceProfile: null,
252
+ structuredDataIdentity: null,
253
+ technologyProfile: null,
254
+ pageRoleProfile: null,
255
+ clientBehaviorProfile: null,
256
+ };
257
+ if (homepage.status === 'fetched') {
258
+ if (page) {
259
+ const responseContentType = homepage.http?.response?.contentType;
260
+ const pageIdentityEligible = typeof responseContentType !== 'string'
261
+ || responseContentType.trim() === ''
262
+ || /^(?:text\/html|application\/xhtml\+xml)(?:\s*;|$)/i.test(responseContentType.trim());
263
+ htmlSignals = await extractHtmlSignals(page, observationHostname, {
264
+ ...(options.signal ? { signal: options.signal } : {}),
265
+ ...(pageAnalysis ? { htmlAnalysis: pageAnalysis } : {}),
266
+ baseUrl: pageBaseUrl,
267
+ ...(typeof homepage.http?.observedAt === 'string' ? { observedAt: homepage.http.observedAt } : {}),
268
+ sourceTruncated: homepage.http?.response?.bodyTruncated === true,
269
+ exactBodyHash: homepage.http?.response?.bodyHash,
270
+ httpServer: homepage.http?.response?.server,
271
+ responseHeaders: homepage.technologyHeaders,
272
+ activityStatus,
273
+ includePageIdentity: pageIdentityEligible,
274
+ includePublicationMetadata: options.includePublicationMetadata !== false,
275
+ ...(options.includeCredentialSurfaceProfile !== undefined
276
+ ? { includeCredentialSurfaceProfile: options.includeCredentialSurfaceProfile }
277
+ : {}),
278
+ ...(options.includeStructuredDataIdentity !== undefined
279
+ ? { includeStructuredDataIdentity: options.includeStructuredDataIdentity }
280
+ : {}),
281
+ ...(options.includeTechnologyProfile !== undefined
282
+ ? { includeTechnologyProfile: options.includeTechnologyProfile }
283
+ : {}),
284
+ });
285
+ if (pageIdentityEligible && htmlSignals.domainSaleSignal) {
286
+ if (!selectedUrl && observationHostname === domain)
287
+ forSaleSignal = forSaleSignal || htmlSignals.domainSaleSignal;
288
+ activityStatus = 'parked';
289
+ }
290
+ }
291
+ }
292
+ if (options.includeTechnologyProfile !== false && htmlSignals.technologyProfile === null
293
+ && ['fetched', 'responded'].includes(homepage.status)) {
294
+ htmlSignals.technologyProfile = await analyzeWebsiteTechnology({
295
+ ...(options.signal ? { signal: options.signal } : {}),
296
+ htmlAvailable: false,
297
+ httpServer: homepage.http?.response?.server,
298
+ responseHeaders: homepage.technologyHeaders,
299
+ observedAt: homepage.http?.observedAt,
300
+ });
301
+ }
302
+ // Complete document consumers before an optional network wait. Only the
303
+ // small icon projection, not every parsed element/token, crosses that wait.
304
+ const faviconEvidence = pageAnalysis ? {
305
+ iconLinks: pageAnalysis.iconLinks, effectiveBaseUrl: pageAnalysis.effectiveBaseUrl,
306
+ } : undefined;
307
+ const pageCollectionComplete = homepage.status === 'fetched' && homepage.http?.complete === true
308
+ && homepage.http.response?.bodyTruncated !== true && Boolean(pageAnalysis)
309
+ && !pageAnalysis?.inputLimitReached && !pageAnalysis?.tagLimitReached
310
+ && !pageAnalysis?.visibleTextLimitReached && !pageAnalysis?.forms.truncated;
311
+ pageAnalysis = undefined;
312
+ const favicon = websiteProbeEnabled
313
+ ? await fetchFaviconForDomain(observationHostname, {
314
+ baseUrl: pageBaseUrl, ...(faviconEvidence ? { htmlAnalysis: faviconEvidence } : {}),
315
+ }).catch(() => null)
316
+ : null;
317
+ const faviconHash = favicon ? favicon.hash : null;
318
+ const faviconPHash = favicon ? favicon.phash : null;
319
+ const responsePolicy = qualifyResponsePolicyWithCspMeta(homepage.responsePolicy, htmlSignals.cspMetaPolicy);
320
+ const retainedHttp = options.includeDeliveryMetadata === false
321
+ ? withoutHttpDeliveryMetadata(homepage.http)
322
+ : homepage.http;
323
+ const { cspMetaPolicy: _cspMetaPolicy, domainSaleSignal: _domainSaleSignal, ...retainedHtmlSignals } = htmlSignals;
324
+ const securityPosture = options.includeSecurityPosture === false ? null : analyzeWebsiteSecurityPosture({
325
+ http: homepage.http,
326
+ responsePolicy,
327
+ pageIdentity: htmlSignals.pageIdentity,
328
+ tls: tlsIntelligence,
329
+ dns: dnsIntelligence,
330
+ dnssec,
331
+ observedAt: homepage.http?.observedAt,
332
+ });
333
+ const redirectSaleSignal = forSaleRedirectSignal(homepage.http);
334
+ if (!selectedUrl && observationHostname === domain && !forSaleSignal && redirectSaleSignal) {
335
+ forSaleSignal = redirectSaleSignal;
336
+ activityStatus = 'parked';
337
+ }
338
+ const faviconProvedActive = !selectedUrl && homepage.status === 'inconclusive' && Boolean(favicon);
339
+ const websiteProbeStatus = faviconProvedActive ? 'responded' : homepage.status;
340
+ const websiteProbeDetail = faviconProvedActive
341
+ ? `${homepage.detail} A favicon responded successfully, confirming an active web service.`
342
+ : homepage.detail;
343
+ if (websiteProbeEnabled) {
344
+ activityStatus = deriveWebsiteActivity(homepage.status, !selectedUrl && Boolean(favicon), activityStatus === 'parked');
345
+ }
346
+ const registrationResult = registration.state !== 'registered' ? registration
347
+ : !forSaleSignal ? {
348
+ ...registration,
349
+ detail: 'source' in registration && registration.source === 'dns'
350
+ ? 'Authoritative DNS delegation confirms the domain is registered, but RDAP/WHOIS registration details were unavailable.'
351
+ : selectedUrl ? 'The domain is registered. Website observations concern the explicitly selected URL.'
352
+ : observationHostname === domain ? 'Domain is registered and shows no for-sale signals.'
353
+ : 'The domain is registered. Website observations concern the separately identified hostname.',
354
+ } : {
355
+ ...registration,
356
+ state: 'for_sale',
357
+ confidence: 'medium',
358
+ detail: `Detected a for-sale listing (${forSaleSignal}).`,
359
+ };
360
+ return {
361
+ ...registrationResult,
362
+ activityStatus,
363
+ websiteProbeStatus,
364
+ websiteProbeDetail,
365
+ http: retainedHttp,
366
+ ...(options.observationHostname !== undefined || selectedUrl ? { observationHostname } : {}),
367
+ ...(selectedUrl ? { webObservationMode: 'selected_url' } : {}),
368
+ deepScanComplete,
369
+ webCollectionQuality: capturedWebCollectionQuality(!websiteProbeEnabled ? 'not_collected' : pageCollectionComplete ? 'complete'
370
+ : homepage.status === 'fetched' ? 'partial' : 'unavailable',
371
+ // The bounded favicon collector returns no result for both failed and
372
+ // non-image candidates. A miss therefore cannot prove an icon was removed.
373
+ !websiteProbeEnabled ? 'not_collected' : favicon ? 'complete' : 'unknown'),
374
+ faviconHash,
375
+ faviconPHash,
376
+ ...retainedHtmlSignals,
377
+ securityPosture,
378
+ nameservers: nameservers.length || observationHostname !== domain ? nameservers : dnsIntelligence.records.ns,
379
+ dns: dnsIntelligence,
380
+ tls: tlsIntelligence,
381
+ hasMx: dnsIntelligence.hasMx,
382
+ hasNullMx: dnsIntelligence.hasNullMx,
383
+ mxHosts: dnsIntelligence.mxHosts,
384
+ hasSpf: dnsIntelligence.hasSpf,
385
+ hasDmarc: dnsIntelligence.hasDmarc,
386
+ };
387
+ }
388
+ export { enrichWebsite, fetchHomepage, deriveWebsiteActivity, forSaleRedirectSignal };
@@ -2,6 +2,8 @@
2
2
  // registry response referred by IANA can decide existence; later registrar
3
3
  // output is diagnostic and cannot replace an inconclusive registry result.
4
4
  import { normalizeWhoisChain } from "./whois-normalization.mjs";
5
+ import { domainToASCII } from 'node:url';
6
+ import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
5
7
  import { parseIndentedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
6
8
  const LINE_NOT_FOUND_PATTERNS = Object.freeze([
7
9
  /^[ \t]*(?:%{1,2}[ \t]*)?(?:error(?::\d+)?[ \t:.-]*)?no match(?:[ \t]+for(?:[ \t]+domain)?(?:[ \t]+["']?[a-z0-9.-]{1,253}["']?)?)?[.!]?[ \t]*$/im,
@@ -50,7 +52,41 @@ export function hasNicKgRegistrationEvidence(text) {
50
52
  && /^[ \t]*Record created[ \t]*:[ \t]*\S/im.test(text)
51
53
  && /^[ \t]*Name servers in the listed order[ \t]*:[ \t]*$/im.test(text);
52
54
  }
53
- function classifyHopEvidence(hop, index) {
55
+ function canonicalObjectName(value) {
56
+ const name = domainToASCII(value.trim().replace(/^["']|["'.]+$/gu, '')).toLowerCase();
57
+ return isValidAsciiHostname(name) ? name : null;
58
+ }
59
+ /** An echoed object may qualify this response, never a different query. */
60
+ export function whoisResponseMatchesDomain(text, requestedDomain) {
61
+ const expected = canonicalObjectName(requestedDomain);
62
+ if (!expected)
63
+ return false;
64
+ const identityPatterns = [
65
+ /^[ \t*]*(?:domain(?:[ \t]+name)?|domain_name|domainname)[ \t.]*:[ \t]*(\S[^\r\n]*)$/gimu,
66
+ /^[ \t]*\[Domain Name\][ \t]*(\S[^\r\n]*)$/gimu,
67
+ /^[ \t]*Domain[ \t]+([^\s()]+)[ \t]+\([A-Z][A-Z0-9_-]*\)[ \t]*$/gmu,
68
+ /^[ \t]*Domain(?: name)?[ \t]*:[ \t]*\r?\n(?:[ \t]*\r?\n)*[ \t]+(?:name:[ \t]*)?(\S[^\r\n]*)$/gimu,
69
+ ];
70
+ for (const pattern of identityPatterns) {
71
+ for (const match of text.matchAll(pattern)) {
72
+ // A few registries publish Unicode plus its parenthesised ASCII form.
73
+ const names = (match[1] ?? '').split(/[ \t()]+/u).filter(Boolean);
74
+ if (!names.length || names.some(name => canonicalObjectName(name) !== expected))
75
+ return false;
76
+ }
77
+ }
78
+ for (const pattern of LINE_NOT_FOUND_PATTERNS) {
79
+ for (const match of text.matchAll(new RegExp(pattern.source, `${pattern.flags}g`))) {
80
+ const names = match[0].match(/[a-z0-9-]+(?:\.[a-z0-9-]+)+\.?/giu) ?? [];
81
+ if (names.some(name => canonicalObjectName(name) !== expected))
82
+ return false;
83
+ }
84
+ }
85
+ // Known complete negative line formats without an echoed name retain their
86
+ // existing meaning only within the exact IANA-referred registry response.
87
+ return true;
88
+ }
89
+ function classifyHopEvidence(hop, index, requestedDomain) {
54
90
  if (hop.error)
55
91
  return 'error';
56
92
  const text = hop.response || '';
@@ -60,6 +96,8 @@ function classifyHopEvidence(hop, index) {
60
96
  return 'rate_limited';
61
97
  if (NZ_TEMPORARY_FAILURE_RE.test(text))
62
98
  return 'rate_limited';
99
+ if (index > 0 && requestedDomain !== undefined && !whoisResponseMatchesDomain(text, requestedDomain))
100
+ return 'inconclusive';
63
101
  if (NZ_NOT_FOUND_RE.test(text))
64
102
  return 'negative';
65
103
  if (LINE_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(text))) {
@@ -95,12 +133,12 @@ function ianaRegistryReferral(hop) {
95
133
  }
96
134
  return null;
97
135
  }
98
- export function analyzeWhoisChainAuthority(chain) {
136
+ export function analyzeWhoisChainAuthority(chain, requestedDomain) {
99
137
  const source = normalizeWhoisChain(chain);
100
138
  const evidence = source.map((hop, index) => ({
101
139
  server: hop.server,
102
140
  index,
103
- kind: classifyHopEvidence(hop, index),
141
+ kind: classifyHopEvidence(hop, index, requestedDomain),
104
142
  }));
105
143
  const failed = evidence.filter((item) => item.kind === 'error' || item.kind === 'rate_limited');
106
144
  const registryReferral = ianaRegistryReferral(source[0]);
@@ -135,11 +173,11 @@ export function analyzeWhoisChainAuthority(chain) {
135
173
  };
136
174
  }
137
175
  /** Collection status shared by final and incremental Lookup presentation. */
138
- export function whoisCollectionStatus(chain) {
176
+ export function whoisCollectionStatus(chain, requestedDomain) {
139
177
  const source = normalizeWhoisChain(chain);
140
178
  if (!source.length || source[0]?.error)
141
179
  return 'error';
142
180
  if (source.length === 1)
143
181
  return 'unsupported';
144
- return analyzeWhoisChainAuthority(source).chainStatus;
182
+ return analyzeWhoisChainAuthority(source, requestedDomain).chainStatus;
145
183
  }