@slicedearth/whoisleuth-cli 2.2.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -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 +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  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 +112 -72
  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-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. 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;
@@ -124,11 +125,10 @@ async function buildUnifiedLookupResponse(context) {
124
125
  const result = availabilityResult.value;
125
126
  availability = {
126
127
  applicable: true,
127
- domain: classified.value,
128
- inputHostname: classified.inputHostname,
129
- registrableDomain: classified.registrableDomain,
130
- isSubdomain: classified.isSubdomain,
131
128
  ...result,
129
+ // Registration authority retains its registrable target. Deep source
130
+ // observations declare their independently collected hostname.
131
+ domain: classified.value,
132
132
  };
133
133
  }
134
134
  else {
@@ -137,7 +137,7 @@ async function buildUnifiedLookupResponse(context) {
137
137
  domain: classified.value,
138
138
  state: 'unknown',
139
139
  confidence: 'low',
140
- detail: errorMessage(availabilityResult.reason, 'Availability lookup failed'),
140
+ detail: boundedSourceDetail(availabilityResult.reason, 'Availability lookup failed'),
141
141
  };
142
142
  }
143
143
  }
@@ -152,11 +152,9 @@ async function buildUnifiedLookupResponse(context) {
152
152
  ? 'disabled'
153
153
  : skipWhois
154
154
  ? 'skipped'
155
- : whoisResult.status === 'rejected' || !Array.isArray(whoisChain)
155
+ : whoisResult.status === 'rejected'
156
156
  ? 'error'
157
- : whoisChain.length === 1
158
- ? 'unsupported'
159
- : whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
157
+ : whoisCollectionStatus(whoisChain);
160
158
  const availabilityStatus = classified.type !== 'domain'
161
159
  ? 'not_applicable'
162
160
  : !availabilityEnabled ? 'disabled'
@@ -2,6 +2,8 @@
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";
6
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
5
7
  const RESULT_STATES = new Set([
6
8
  'available',
7
9
  'expiring',
@@ -19,11 +21,22 @@ const DIRECT_STATES = new Set([
19
21
  'unavailable',
20
22
  'rate_limited',
21
23
  ]);
24
+ const THREAT_INTELLIGENCE_SOURCES = new Set([
25
+ 'external_intelligence',
26
+ 'malware_host_intelligence',
27
+ 'malware_ioc_intelligence',
28
+ ]);
29
+ const THREAT_INTELLIGENCE_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
22
30
  function record(value) {
23
31
  return value && typeof value === 'object' && !Array.isArray(value)
24
32
  ? value
25
33
  : {};
26
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
+ }
27
40
  function plannedLookupProgressSources(classified, options = {}) {
28
41
  const sources = ['rdap', 'whois'];
29
42
  if (classified.type === 'domain') {
@@ -51,40 +64,53 @@ function normalizedState(source, outcome, value) {
51
64
  const rdap = record(value);
52
65
  if (rdap.upstreamStatus === 404)
53
66
  return 'not_found';
54
- 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';
55
70
  }
56
71
  if (source === 'whois') {
57
- if (!Array.isArray(value))
58
- return 'error';
59
- if (value.length <= 1)
60
- return 'unsupported';
61
- return 'success';
72
+ const status = whoisCollectionStatus(value);
73
+ return status === 'complete' ? 'success' : status;
62
74
  }
63
75
  if (source === 'domain_evidence') {
64
- return record(value).deepScanComplete === false ? 'partial' : 'success';
76
+ return record(value).deepScanComplete === true ? 'success' : 'partial';
77
+ }
78
+ if (THREAT_INTELLIGENCE_SOURCES.has(source)) {
79
+ const intelligence = record(value);
80
+ return intelligence.schema === THREAT_INTELLIGENCE_SCHEMA
81
+ && intelligence.version === THREAT_INTELLIGENCE_CONTRACT_VERSION
82
+ && typeof intelligence.state === 'string'
83
+ && THREAT_INTELLIGENCE_STATES.has(intelligence.state)
84
+ ? intelligence.state
85
+ : 'error';
65
86
  }
66
87
  const status = record(value).status;
67
88
  if (typeof status === 'string' && DIRECT_STATES.has(status)) {
89
+ if (status === 'success' && (sourceTruncated(value) || record(value).complete === false))
90
+ return 'partial';
68
91
  return status;
69
92
  }
70
- if (status === 'complete' || status === 'observed' || status === 'found')
71
- return 'success';
72
93
  if (status === 'not_applicable' || status === 'disabled')
73
94
  return 'skipped';
74
- return 'success';
95
+ return 'error';
75
96
  }
76
97
  function normalizeLookupSourceSettlement(source, outcome, value) {
77
98
  const state = normalizedState(source, outcome, value);
78
99
  const sourceRecord = record(value);
100
+ const threatObservation = THREAT_INTELLIGENCE_SOURCES.has(source)
101
+ ? record(sourceRecord.observation)
102
+ : null;
79
103
  const resultState = source === 'domain_evidence'
80
104
  && typeof sourceRecord.state === 'string'
81
105
  && RESULT_STATES.has(sourceRecord.state)
82
106
  ? sourceRecord.state
83
107
  : null;
84
- const complete = !['partial', 'error', 'unavailable', 'rate_limited'].includes(state);
85
- const truncated = sourceRecord.truncated === true
86
- || sourceRecord.recordsTruncated === true
87
- || sourceRecord.bodyTruncated === true;
108
+ const complete = threatObservation
109
+ ? state !== 'error' && threatObservation.complete === true
110
+ : ['success', 'not_found'].includes(state) && sourceRecord.complete !== false;
111
+ const truncated = threatObservation
112
+ ? threatObservation.truncated === true
113
+ : sourceTruncated(value);
88
114
  const limitation = outcome === 'rejected'
89
115
  ? 'This source did not complete. No absence or safety conclusion was inferred.'
90
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";
@@ -116,6 +116,7 @@ export function reviewNameserverPreflight(inputRaw, generatedAtValue = new Date(
116
116
  && authorityState === 'authoritative'
117
117
  && servedSetState === 'aligned'
118
118
  && soaState === 'observed'
119
+ && addressState !== 'non_public'
119
120
  && (!inBailiwick || addressState === 'ready');
120
121
  return Object.freeze({
121
122
  nameserver,
@@ -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;