@slicedearth/whoisleuth-cli 2.3.0 → 2.4.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 (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +83 -61
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -2,7 +2,7 @@
2
2
  // collection and preserves compact, diagnostics and enrichment response shapes.
3
3
  import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
4
4
  import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
5
- import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, errorMessage, } from "./lookup-diagnostics.mjs";
5
+ import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, } from "./lookup-diagnostics.mjs";
6
6
  import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
7
7
  import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
8
8
  import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
@@ -12,6 +12,7 @@ import { buildRegistrarStanding } from "./registrar-standing.mjs";
12
12
  import { resolveRegistrarIanaId } from "./registrar-standing-contract.mjs";
13
13
  import { securityTxtUnavailable } from "./security-txt.mjs";
14
14
  import { parseWhoisChain } from "./whois.mjs";
15
+ import { whoisCollectionStatus } from "./whois-authority.mjs";
15
16
  function withoutNestedPublicationMetadata(value) {
16
17
  if (!value || typeof value !== 'object' || Array.isArray(value))
17
18
  return value;
@@ -125,10 +126,8 @@ async function buildUnifiedLookupResponse(context) {
125
126
  availability = {
126
127
  applicable: true,
127
128
  ...result,
128
- // Every observation nested in the availability envelope is collected
129
- // against this registrable target. Request context remains on the
130
- // top-level response; exact-host sources such as security.txt retain
131
- // their own requested/final URL attribution.
129
+ // Registration authority retains its registrable target. Deep source
130
+ // observations declare their independently collected hostname.
132
131
  domain: classified.value,
133
132
  };
134
133
  }
@@ -138,7 +137,7 @@ async function buildUnifiedLookupResponse(context) {
138
137
  domain: classified.value,
139
138
  state: 'unknown',
140
139
  confidence: 'low',
141
- detail: errorMessage(availabilityResult.reason, 'Availability lookup failed'),
140
+ detail: boundedSourceDetail(availabilityResult.reason, 'Availability lookup failed'),
142
141
  };
143
142
  }
144
143
  }
@@ -153,11 +152,9 @@ async function buildUnifiedLookupResponse(context) {
153
152
  ? 'disabled'
154
153
  : skipWhois
155
154
  ? 'skipped'
156
- : whoisResult.status === 'rejected' || !Array.isArray(whoisChain)
155
+ : whoisResult.status === 'rejected'
157
156
  ? 'error'
158
- : whoisChain.length === 1
159
- ? 'unsupported'
160
- : whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
157
+ : whoisCollectionStatus(whoisChain);
161
158
  const availabilityStatus = classified.type !== 'domain'
162
159
  ? 'not_applicable'
163
160
  : !availabilityEnabled ? 'disabled'
@@ -2,6 +2,7 @@
2
2
  // Lookup. These deliberately exclude raw RDAP/WHOIS bodies, contacts, URLs,
3
3
  // provider payloads, and request errors. The ordinary final Lookup response
4
4
  // remains the only authoritative and persistable result.
5
+ import { whoisCollectionStatus } from "./whois-authority.mjs";
5
6
  import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
6
7
  const RESULT_STATES = new Set([
7
8
  'available',
@@ -31,6 +32,11 @@ function record(value) {
31
32
  ? value
32
33
  : {};
33
34
  }
35
+ function sourceTruncated(value) {
36
+ // RDAP's normalised *Truncated fields describe loss independently of HTTP success.
37
+ return [record(value), record(record(value).parsed)].some(part => Object.entries(part)
38
+ .some(([key, field]) => field === true && (key === 'truncated' || key.endsWith('Truncated'))));
39
+ }
34
40
  function plannedLookupProgressSources(classified, options = {}) {
35
41
  const sources = ['rdap', 'whois'];
36
42
  if (classified.type === 'domain') {
@@ -58,17 +64,16 @@ function normalizedState(source, outcome, value) {
58
64
  const rdap = record(value);
59
65
  if (rdap.upstreamStatus === 404)
60
66
  return 'not_found';
61
- return rdap.upstreamStatus === 200 ? 'success' : 'unsupported';
67
+ if (rdap.upstreamStatus !== 200 || !Object.keys(record(rdap.parsed)).length)
68
+ return 'error';
69
+ return sourceTruncated(rdap) ? 'partial' : 'success';
62
70
  }
63
71
  if (source === 'whois') {
64
- if (!Array.isArray(value))
65
- return 'error';
66
- if (value.length <= 1)
67
- return 'unsupported';
68
- return 'success';
72
+ const status = whoisCollectionStatus(value);
73
+ return status === 'complete' ? 'success' : status;
69
74
  }
70
75
  if (source === 'domain_evidence') {
71
- return record(value).deepScanComplete === false ? 'partial' : 'success';
76
+ return record(value).deepScanComplete === true ? 'success' : 'partial';
72
77
  }
73
78
  if (THREAT_INTELLIGENCE_SOURCES.has(source)) {
74
79
  const intelligence = record(value);
@@ -81,13 +86,13 @@ function normalizedState(source, outcome, value) {
81
86
  }
82
87
  const status = record(value).status;
83
88
  if (typeof status === 'string' && DIRECT_STATES.has(status)) {
89
+ if (status === 'success' && (sourceTruncated(value) || record(value).complete === false))
90
+ return 'partial';
84
91
  return status;
85
92
  }
86
- if (status === 'complete' || status === 'observed' || status === 'found')
87
- return 'success';
88
93
  if (status === 'not_applicable' || status === 'disabled')
89
94
  return 'skipped';
90
- return 'success';
95
+ return 'error';
91
96
  }
92
97
  function normalizeLookupSourceSettlement(source, outcome, value) {
93
98
  const state = normalizedState(source, outcome, value);
@@ -102,12 +107,10 @@ function normalizeLookupSourceSettlement(source, outcome, value) {
102
107
  : null;
103
108
  const complete = threatObservation
104
109
  ? state !== 'error' && threatObservation.complete === true
105
- : !['partial', 'error', 'unavailable', 'rate_limited'].includes(state);
110
+ : ['success', 'not_found'].includes(state) && sourceRecord.complete !== false;
106
111
  const truncated = threatObservation
107
112
  ? threatObservation.truncated === true
108
- : sourceRecord.truncated === true
109
- || sourceRecord.recordsTruncated === true
110
- || sourceRecord.bodyTruncated === true;
113
+ : sourceTruncated(value);
111
114
  const limitation = outcome === 'rejected'
112
115
  ? 'This source did not complete. No absence or safety conclusion was inferred.'
113
116
  : state === 'skipped'
package/lib/lookup.mjs CHANGED
@@ -21,6 +21,7 @@ import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
21
21
  import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
22
22
  import { buildUnifiedLookupResponse } from "./lookup-response.mjs";
23
23
  import { createLookupTimingTracker } from "./lookup-diagnostics.mjs";
24
+ import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
24
25
  async function runUnifiedLookup(classified, options = {}) {
25
26
  const fetchRdap = options.fetchRdapRecord || fetchRdapRecord;
26
27
  const fetchRegistrarRdap = options.fetchRegistrarRdapRecord || fetchRegistrarRdapRecord;
@@ -44,19 +45,30 @@ async function runUnifiedLookup(classified, options = {}) {
44
45
  const securityTxtRequested = options.securityTxt === true;
45
46
  const featurePolicy = options.featurePolicy || networkFeaturePolicy();
46
47
  const timing = createLookupTimingTracker(!fast && !compact, options.now || Date.now);
47
- const measure = (source, operation) => (timing.measure(source, () => abortable(operation, options.signal)));
48
+ const startedCollectors = [];
49
+ const measure = (source, operation) => (timing.measure(source, () => abortable(() => {
50
+ const running = Promise.resolve(operation());
51
+ if (options.waitForStartedCollectors)
52
+ startedCollectors.push(running.catch(() => { }));
53
+ return running;
54
+ }, options.signal)));
48
55
  const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
49
56
  const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
50
57
  const availabilityEnabled = featureDecision('availability', featurePolicy).enabled;
51
58
  const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
52
59
  const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
53
60
  const skipWhois = fast || !whoisEnabled;
61
+ const selectedUrl = options.selectedUrl === undefined ? undefined
62
+ : prepareSelectedLookupUrl(options.selectedUrl, classified.inputHostname ?? classified.value);
63
+ if (selectedUrl && (classified.type !== 'domain' || fast || compact || !availabilityEnabled || !websiteProbeEnabled)) {
64
+ throw new TypeError('Selected URL collection requires an enabled full Deep domain lookup.');
65
+ }
54
66
  const rdapPromise = rdapEnabled
55
- ? measure('rdap', () => fetchRdap(classified.type, classified.value))
67
+ ? measure('rdap', () => fetchRdap(classified.type, classified.value, options.signal ? { signal: options.signal } : {}))
56
68
  : Promise.resolve(null);
57
69
  const whoisPromise = skipWhois
58
70
  ? Promise.resolve(null)
59
- : measure('whois', () => fetchWhois(classified.value));
71
+ : measure('whois', () => fetchWhois(classified.value, options.signal ? { signal: options.signal } : {}));
60
72
  // Registrar RDAP is a separately attributed deep-lookup enrichment. It may
61
73
  // overlap the WHOIS chain, but it never joins the promises used to decide
62
74
  // availability and can add up to its own bounded timeout to a deep lookup.
@@ -76,7 +88,10 @@ async function runUnifiedLookup(classified, options = {}) {
76
88
  includeStructuredDataIdentity: !fast && !compact,
77
89
  includeTechnologyProfile: !fast,
78
90
  includeSecurityPosture: !compact,
91
+ ...(!fast && !compact ? { observationHostname: classified.inputHostname } : {}),
92
+ ...(selectedUrl ? { selectedUrl } : {}),
79
93
  featurePolicy,
94
+ ...(options.signal ? { signal: options.signal } : {}),
80
95
  rdapRecordPromise: rdapPromise,
81
96
  whoisChainPromise: whoisPromise,
82
97
  ...(selectedDnsResolvers ? {
@@ -171,30 +186,36 @@ async function runUnifiedLookup(classified, options = {}) {
171
186
  void Promise.resolve(sourcePromise).then((value) => notify(source, 'fulfilled', value), (error) => notify(source, 'rejected', error));
172
187
  }
173
188
  }
174
- return buildUnifiedLookupResponse({
175
- availabilityEnabled,
176
- availabilityPromise,
177
- classified,
178
- compact,
179
- dnsIntelligenceEnabled,
180
- fast,
181
- inspectCertificateWarningData,
182
- networkContextPromise,
183
- options,
184
- rdapEnabled,
185
- rdapPromise,
186
- registrarRdapPromise,
187
- reverseDnsEligible,
188
- reverseDnsPromise,
189
- securityTxtPromise,
190
- skipWhois,
191
- threatfoxIntelligencePromise,
192
- timing,
193
- urlhausIntelligencePromise,
194
- urlscanIntelligencePromise,
195
- whoisEnabled,
196
- whoisPromise,
197
- });
189
+ try {
190
+ return await buildUnifiedLookupResponse({
191
+ availabilityEnabled,
192
+ availabilityPromise,
193
+ classified,
194
+ compact,
195
+ dnsIntelligenceEnabled,
196
+ fast,
197
+ inspectCertificateWarningData,
198
+ networkContextPromise,
199
+ options,
200
+ rdapEnabled,
201
+ rdapPromise,
202
+ registrarRdapPromise,
203
+ reverseDnsEligible,
204
+ reverseDnsPromise,
205
+ securityTxtPromise,
206
+ skipWhois,
207
+ threatfoxIntelligencePromise,
208
+ timing,
209
+ urlhausIntelligencePromise,
210
+ urlscanIntelligencePromise,
211
+ whoisEnabled,
212
+ whoisPromise,
213
+ });
214
+ }
215
+ finally {
216
+ if (options.waitForStartedCollectors)
217
+ await Promise.all(startedCollectors);
218
+ }
198
219
  }
199
220
  export { runUnifiedLookup };
200
221
  export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, } from "./lookup-diagnostics.mjs";
@@ -11,6 +11,7 @@ const OBSERVED_NETWORK_CONTEXT_VERSION = 1;
11
11
  const MAX_NETWORK_CIDRS = 16;
12
12
  const MAX_NETWORK_ATTEMPTS = 3;
13
13
  const MAX_NETWORK_ABUSE_ROUTES = 6;
14
+ const NETWORK_ABUSE_ROUTE_LIMITATION = 'The network abuse-route projection reached its local retention limit; additional published routes may exist.';
14
15
  const MAX_ENDPOINT_LENGTH = 2048;
15
16
  const MAX_DETAIL_LENGTH = 240;
16
17
  const MAX_NAME_LENGTH = 300;
@@ -144,20 +145,25 @@ function networkAbuseEntities(parsed) {
144
145
  const fallback = parsed.abuse && typeof parsed.abuse === 'object' && !Array.isArray(parsed.abuse)
145
146
  ? [parsed.abuse]
146
147
  : [];
147
- return [...roleEntities, ...fallback]
148
- .filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
149
- .slice(0, MAX_NETWORK_ABUSE_ROUTES);
148
+ return {
149
+ entities: [...roleEntities.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1), ...fallback]
150
+ .filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item)),
151
+ truncated: roleEntities.length > MAX_NETWORK_ABUSE_ROUTES + 1,
152
+ };
150
153
  }
151
154
  function projectNetworkAbuseRoutes(parsed, provenance) {
152
155
  const routes = new Map();
153
- for (const entity of networkAbuseEntities(parsed)) {
156
+ const inventory = networkAbuseEntities(parsed);
157
+ let truncated = inventory.truncated;
158
+ for (const entity of inventory.entities) {
154
159
  const emails = Array.isArray(entity.emails) ? entity.emails : [];
155
160
  const phones = Array.isArray(entity.phones) ? entity.phones : [];
161
+ truncated ||= emails.length > MAX_NETWORK_ABUSE_ROUTES + 1 || phones.length > MAX_NETWORK_ABUSE_ROUTES + 1;
156
162
  const candidates = [
157
163
  { channel: 'email', value: entity.email },
158
- ...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'email', value })),
164
+ ...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'email', value })),
159
165
  { channel: 'phone', value: entity.phone },
160
- ...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'phone', value })),
166
+ ...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'phone', value })),
161
167
  ];
162
168
  for (const candidate of candidates) {
163
169
  const contact = normalizedAbuseContact(candidate.value, candidate.channel);
@@ -165,6 +171,10 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
165
171
  continue;
166
172
  const key = `${candidate.channel}:${contact.toLowerCase()}`;
167
173
  if (!routes.has(key)) {
174
+ if (routes.size >= MAX_NETWORK_ABUSE_ROUTES) {
175
+ truncated = true;
176
+ continue;
177
+ }
168
178
  routes.set(key, {
169
179
  kind: 'network_hosting',
170
180
  channel: candidate.channel,
@@ -186,11 +196,14 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
186
196
  ],
187
197
  });
188
198
  }
189
- if (routes.size >= MAX_NETWORK_ABUSE_ROUTES)
190
- return [...routes.values()];
191
199
  }
192
200
  }
193
- return [...routes.values()];
201
+ return {
202
+ routes: [...routes.values()].map((route) => truncated
203
+ ? { ...route, complete: false, truncated: true, limitations: [...route.limitations, NETWORK_ABUSE_ROUTE_LIMITATION] }
204
+ : route),
205
+ truncated,
206
+ };
194
207
  }
195
208
  function networkSummary(parsedValue, family) {
196
209
  const parsed = record(parsedValue);
@@ -204,10 +217,12 @@ function networkSummary(parsedValue, family) {
204
217
  const startAddress = publicAddress(parsed.startAddress, family);
205
218
  const endAddress = publicAddress(parsed.endAddress, family);
206
219
  const country = boundedString(parsed.country, 2);
207
- const truncated = parsed.serverTruncated === true
208
- || parsed.cidrsTruncated === true
209
- || parsed.entitiesTruncated === true
210
- || sourceCidrs.length > MAX_NETWORK_CIDRS;
220
+ const limitations = [
221
+ ...(parsed.serverTruncated === true ? ['The RDAP server declared that part of its response was truncated.'] : []),
222
+ ...(parsed.entitiesTruncated === true ? ['Some IP RDAP contact records or fields were omitted during bounded normalisation.'] : []),
223
+ ...(parsed.cidrsTruncated === true ? ['Some IP RDAP CIDR entries were invalid or exceeded the normalisation limit.'] : []),
224
+ ...(sourceCidrs.length > MAX_NETWORK_CIDRS ? ['The network CIDR summary reached its local retention limit.'] : []),
225
+ ];
211
226
  return {
212
227
  value: {
213
228
  handle: boundedString(parsed.handle, MAX_NAME_LENGTH),
@@ -220,8 +235,8 @@ function networkSummary(parsedValue, family) {
220
235
  networkType: boundedString(parsed.networkType, 160),
221
236
  databaseUpdatedAt: isoTimestamp(lifecycle.databaseUpdatedDateIso || lifecycle.databaseUpdatedDate),
222
237
  },
223
- truncated,
224
- serverTruncated: parsed.serverTruncated === true,
238
+ truncated: limitations.length > 0,
239
+ limitations,
225
240
  };
226
241
  }
227
242
  function baseContext(selection, input) {
@@ -299,28 +314,33 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
299
314
  if (rdap.httpStatus !== 200 || !Object.keys(parsed).length)
300
315
  throw new Error('IP RDAP returned no usable normalised object');
301
316
  const summary = networkSummary(parsed, selection.family);
302
- const partial = summary.truncated;
303
317
  const sourceObservedAt = rdap.fetchedAt || observedAt();
304
- const abuseRouting = projectNetworkAbuseRoutes(parsed, {
318
+ const routeProjection = projectNetworkAbuseRoutes(parsed, {
305
319
  endpoint: rdap.endpoint,
306
320
  observedAt: sourceObservedAt,
307
321
  selection,
308
- complete: !partial,
309
- truncated: partial,
322
+ complete: !summary.truncated,
323
+ truncated: summary.truncated,
310
324
  });
325
+ const incompleteReasons = [
326
+ ...summary.limitations,
327
+ ...(routeProjection.truncated ? [NETWORK_ABUSE_ROUTE_LIMITATION] : []),
328
+ ];
329
+ const partial = incompleteReasons.length > 0;
311
330
  return baseContext(selection, {
312
331
  status: partial ? 'partial' : 'success',
313
332
  observedAt: sourceObservedAt,
314
333
  durationMs,
315
334
  complete: !partial,
316
335
  truncated: partial,
317
- detail: 'The selected public endpoint address was mapped to its separately attributed IP RDAP registration.',
336
+ detail: partial
337
+ ? `Network registration was retained, but the source record is incomplete. ${incompleteReasons[0]}`
338
+ : 'The selected public endpoint address was mapped to its separately attributed IP RDAP registration.',
318
339
  limitations: [
340
+ ...incompleteReasons,
319
341
  'This identifies the registered network for one point-in-time endpoint address, not a definitive origin host or hosting provider.',
320
342
  'CDNs, reverse proxies, load balancers, shared hosting, and location-dependent DNS can present different networks.',
321
343
  'Network registration is an investigative lead and does not prove control, ownership, intent, or maliciousness.',
322
- ...(summary.serverTruncated ? ['The RDAP server declared that part of its response was truncated.'] : []),
323
- ...(sourceCidrsWereCapped(parsed) ? ['The network CIDR summary reached its local retention limit.'] : []),
324
344
  ],
325
345
  diagnostics: {
326
346
  requestCount: 1,
@@ -330,7 +350,7 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
330
350
  },
331
351
  rdap,
332
352
  network: summary.value,
333
- abuseRouting,
353
+ abuseRouting: routeProjection.routes,
334
354
  });
335
355
  }
336
356
  catch (error) {
@@ -348,8 +368,4 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
348
368
  });
349
369
  }
350
370
  }
351
- function sourceCidrsWereCapped(parsed) {
352
- return parsed.cidrsTruncated === true
353
- || (Array.isArray(parsed.cidrs) && parsed.cidrs.length > MAX_NETWORK_CIDRS);
354
- }
355
371
  export { MAX_NETWORK_ATTEMPTS, MAX_NETWORK_ABUSE_ROUTES, MAX_NETWORK_CIDRS, OBSERVED_NETWORK_CONTEXT_VERSION, collectObservedNetworkContext, selectObservedNetworkAddress, };
@@ -3,7 +3,10 @@
3
3
  // disclosures cannot drift from the enforced request boundaries.
4
4
  export const MAX_OUTBOUND_REDIRECTS = 5;
5
5
  export const HOMEPAGE_FETCH_TIMEOUT_MS = 6_000;
6
- export const MAX_HOMEPAGE_BYTES = 300_000;
6
+ // One MiB admits large static application pages while bounding bytes retained
7
+ // from each response. Parsing, derived outputs and request concurrency retain
8
+ // separate work limits; this is not a claim that larger pages lack evidence.
9
+ export const MAX_HOMEPAGE_BYTES = 1024 * 1024;
7
10
  export const FAVICON_FETCH_TIMEOUT_MS = 5_000;
8
11
  export const MAX_FAVICON_BYTES = 200_000;
9
12
  export const MAX_FAVICON_CANDIDATES = 4;