@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -2,15 +2,15 @@
2
2
  // by deep availability analysis. This module does not perform network work; it
3
3
  // converts the shared safe-fetch trace and selected response metadata into an
4
4
  // additive evidence object suitable for API responses and exports.
5
- import { createObservation } from "./observation.mjs";
6
- import { MAX_HTTP_EVIDENCE_REDIRECTS } from "./http-evidence-bounds.mjs";
7
- const MAX_HTTP_PROVENANCE_URL = 2048;
5
+ import { createObservation } from "../packages/evidence/observation.mjs";
6
+ import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, MAX_HTTP_PROVENANCE_URL, } from "./http-evidence-bounds.mjs";
7
+ import { HTTP_DELIVERY_LIMITATIONS, HTTP_DELIVERY_METADATA_VERSION, MAX_HTTP_CACHE_SECONDS, MAX_HTTP_DELIVERY_HEADER_BYTES, MAX_HTTP_DELIVERY_TOKENS, } from "./homepage-metadata-contract.mjs";
8
8
  const MAX_HTTP_REDIRECTS = MAX_HTTP_EVIDENCE_REDIRECTS;
9
- const MAX_HTTP_ATTEMPTS = 2;
10
- const MAX_HTTP_ERROR_LENGTH = 180;
11
9
  const MAX_HTTP_HEADER_LENGTH = 1024;
10
+ const MAX_HTTP_DELIVERY_HEADER_LENGTH = MAX_HTTP_DELIVERY_HEADER_BYTES;
12
11
  const MAX_CAPTURED_BODY_BYTES = 5 * 1024 * 1024;
13
12
  const OBSERVED_HEADER_MARKER = 'observed';
13
+ const HTTP_TOKEN_RE = /^[!#$%&'*+.^_`|~0-9a-z-]+$/u;
14
14
  const SECURITY_HEADERS = Object.freeze({
15
15
  'strict-transport-security': 'strictTransportSecurity',
16
16
  'content-security-policy': 'contentSecurityPolicy',
@@ -60,6 +60,331 @@ function headerValue(headers, name, maxLength = MAX_HTTP_HEADER_LENGTH) {
60
60
  return null;
61
61
  return boundedString(headers.get(name), maxLength);
62
62
  }
63
+ function deliveryHeader(headers, name, maximum = MAX_HTTP_DELIVERY_HEADER_LENGTH) {
64
+ if (!headers || typeof headers.get !== 'function')
65
+ return { state: 'not_observed', value: null };
66
+ let raw;
67
+ try {
68
+ raw = headers.get(name);
69
+ }
70
+ catch {
71
+ return { state: 'malformed', value: null };
72
+ }
73
+ if (raw === null)
74
+ return { state: 'not_observed', value: null };
75
+ if (typeof raw !== 'string' || /[\u0000-\u001f\u007f-\u009f]/u.test(raw))
76
+ return { state: 'malformed', value: null };
77
+ if (raw.length > maximum || Buffer.byteLength(raw, 'utf8') > maximum)
78
+ return { state: 'partial', value: null };
79
+ const value = raw.trim();
80
+ return value ? { state: 'observed', value } : { state: 'malformed', value: null };
81
+ }
82
+ function parseContentEncoding(headers) {
83
+ const header = deliveryHeader(headers, 'content-encoding');
84
+ if (header.state !== 'observed' || !header.value) {
85
+ return { status: header.state, codings: [], encoded: null, unknownCodingCount: 0 };
86
+ }
87
+ const supplied = header.value.split(',').map((item) => item.trim().toLowerCase());
88
+ const malformed = supplied.some((item) => !item || !HTTP_TOKEN_RE.test(item));
89
+ const truncated = supplied.length > MAX_HTTP_DELIVERY_TOKENS;
90
+ if (malformed)
91
+ return { status: 'malformed', codings: [], encoded: null, unknownCodingCount: 0 };
92
+ if (truncated)
93
+ return { status: 'partial', codings: [], encoded: null, unknownCodingCount: 0 };
94
+ const unknownCodingCount = supplied.filter((coding) => !['gzip', 'br', 'deflate', 'zstd', 'identity'].includes(coding)).length;
95
+ const codings = [...new Set(supplied.map((coding) => (['gzip', 'br', 'deflate', 'zstd', 'identity'].includes(coding) ? coding : 'other')))].sort();
96
+ if (codings.includes('identity') && codings.length > 1) {
97
+ return { status: 'malformed', codings: [], encoded: null, unknownCodingCount: 0 };
98
+ }
99
+ return {
100
+ status: 'observed',
101
+ codings,
102
+ encoded: codings.some((coding) => coding !== 'identity'),
103
+ unknownCodingCount,
104
+ };
105
+ }
106
+ function splitCacheControl(value) {
107
+ const directives = [];
108
+ let current = '';
109
+ let quoted = false;
110
+ let escaped = false;
111
+ for (const character of value) {
112
+ if (escaped) {
113
+ current += character;
114
+ escaped = false;
115
+ continue;
116
+ }
117
+ if (quoted && character === '\\') {
118
+ current += character;
119
+ escaped = true;
120
+ continue;
121
+ }
122
+ if (character === '"') {
123
+ quoted = !quoted;
124
+ current += character;
125
+ continue;
126
+ }
127
+ if (character === ',' && !quoted) {
128
+ directives.push(current.trim());
129
+ current = '';
130
+ continue;
131
+ }
132
+ current += character;
133
+ }
134
+ directives.push(current.trim());
135
+ return {
136
+ directives: directives.slice(0, MAX_HTTP_DELIVERY_TOKENS),
137
+ truncated: directives.length > MAX_HTTP_DELIVERY_TOKENS,
138
+ malformed: quoted || escaped || directives.some((item) => !item),
139
+ };
140
+ }
141
+ function cacheSeconds(value, allowQuoted = true) {
142
+ if (value === null)
143
+ return null;
144
+ const quoted = value.startsWith('"') && value.endsWith('"');
145
+ if (quoted && !allowQuoted)
146
+ return null;
147
+ const unquoted = quoted ? value.slice(1, -1) : value;
148
+ if (!/^\d+$/u.test(unquoted))
149
+ return null;
150
+ const parsed = Number(unquoted);
151
+ return Number.isSafeInteger(parsed) && parsed >= 0 && parsed <= MAX_HTTP_CACHE_SECONDS ? parsed : null;
152
+ }
153
+ function validCacheFieldNameList(value) {
154
+ if (value === null)
155
+ return true;
156
+ if (value.length < 3 || value[0] !== '"' || value.at(-1) !== '"')
157
+ return false;
158
+ const fields = value.slice(1, -1).split(',').map((field) => field.trim());
159
+ return fields.length > 0 && fields.every((field) => Boolean(field) && HTTP_TOKEN_RE.test(field));
160
+ }
161
+ function headerValidity(header, validator) {
162
+ if (header.state === 'not_observed')
163
+ return { present: false, valid: null };
164
+ if (header.state === 'partial')
165
+ return { present: true, valid: null };
166
+ return { present: true, valid: header.state === 'observed' && Boolean(header.value && validator(header.value)) };
167
+ }
168
+ function validHttpDate(value) {
169
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
170
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
171
+ const longWeekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
172
+ const time = '([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)';
173
+ const imfFixdate = value.match(new RegExp(`^(${weekdays.join('|')}), (0[1-9]|[12]\\d|3[01]) (${months.join('|')}) (\\d{4}) ${time} GMT$`, 'u'));
174
+ const rfc850 = value.match(new RegExp(`^(${longWeekdays.join('|')}), (0[1-9]|[12]\\d|3[01])-(${months.join('|')})-(\\d{2}) ${time} GMT$`, 'u'));
175
+ const asctime = value.match(new RegExp(`^(${weekdays.join('|')}) (${months.join('|')}) ( {2}[1-9]| [12]\\d| 3[01]) ${time} (\\d{4})$`, 'u'));
176
+ let weekday;
177
+ let day;
178
+ let month;
179
+ let year;
180
+ let hour;
181
+ let minute;
182
+ let second;
183
+ let expectedWeekdays;
184
+ if (imfFixdate) {
185
+ weekday = imfFixdate[1];
186
+ day = Number(imfFixdate[2]);
187
+ month = months.indexOf(imfFixdate[3]);
188
+ year = Number(imfFixdate[4]);
189
+ hour = Number(imfFixdate[5]);
190
+ minute = Number(imfFixdate[6]);
191
+ second = Number(imfFixdate[7]);
192
+ expectedWeekdays = weekdays;
193
+ }
194
+ else if (rfc850) {
195
+ weekday = rfc850[1];
196
+ day = Number(rfc850[2]);
197
+ month = months.indexOf(rfc850[3]);
198
+ const shortYear = Number(rfc850[4]);
199
+ year = shortYear >= 70 ? 1900 + shortYear : 2000 + shortYear;
200
+ hour = Number(rfc850[5]);
201
+ minute = Number(rfc850[6]);
202
+ second = Number(rfc850[7]);
203
+ expectedWeekdays = longWeekdays;
204
+ }
205
+ else if (asctime) {
206
+ weekday = asctime[1];
207
+ month = months.indexOf(asctime[2]);
208
+ day = Number(asctime[3].trim());
209
+ hour = Number(asctime[4]);
210
+ minute = Number(asctime[5]);
211
+ second = Number(asctime[6]);
212
+ year = Number(asctime[7]);
213
+ expectedWeekdays = weekdays;
214
+ }
215
+ else
216
+ return false;
217
+ if (year < 1601 || month < 0)
218
+ return false;
219
+ const parsed = new Date(0);
220
+ parsed.setUTCFullYear(year, month, day);
221
+ parsed.setUTCHours(hour, minute, second, 0);
222
+ return parsed.getUTCFullYear() === year
223
+ && parsed.getUTCMonth() === month
224
+ && parsed.getUTCDate() === day
225
+ && expectedWeekdays[parsed.getUTCDay()] === weekday;
226
+ }
227
+ function parseCachePolicy(headers) {
228
+ const cacheControl = deliveryHeader(headers, 'cache-control');
229
+ const age = deliveryHeader(headers, 'age', 64);
230
+ const etagHeader = deliveryHeader(headers, 'etag');
231
+ const lastModifiedHeader = deliveryHeader(headers, 'last-modified');
232
+ const expiresHeader = deliveryHeader(headers, 'expires');
233
+ const parsed = cacheControl.state === 'observed' && cacheControl.value
234
+ ? splitCacheControl(cacheControl.value)
235
+ : { directives: [], truncated: false, malformed: false };
236
+ const flags = {
237
+ noStore: false,
238
+ noCache: false,
239
+ mustRevalidate: false,
240
+ public: false,
241
+ private: false,
242
+ immutable: false,
243
+ };
244
+ let maxAgeSeconds = null;
245
+ let sMaxAgeSeconds = null;
246
+ let maxAgeSeen = false;
247
+ let sMaxAgeSeen = false;
248
+ let maxAgeMalformed = false;
249
+ let sMaxAgeMalformed = false;
250
+ let unknownDirectiveCount = 0;
251
+ let malformed = parsed.malformed;
252
+ for (const directive of parsed.truncated ? [] : parsed.directives) {
253
+ const separator = directive.indexOf('=');
254
+ const name = (separator === -1 ? directive : directive.slice(0, separator)).trim().toLowerCase();
255
+ const value = separator === -1 ? null : directive.slice(separator + 1).trim();
256
+ if (!HTTP_TOKEN_RE.test(name)) {
257
+ malformed = true;
258
+ continue;
259
+ }
260
+ if (name === 'no-store') {
261
+ flags.noStore = true;
262
+ if (value !== null)
263
+ malformed = true;
264
+ }
265
+ else if (name === 'no-cache') {
266
+ flags.noCache = true;
267
+ if (!validCacheFieldNameList(value))
268
+ malformed = true;
269
+ }
270
+ else if (name === 'must-revalidate') {
271
+ flags.mustRevalidate = true;
272
+ if (value !== null)
273
+ malformed = true;
274
+ }
275
+ else if (name === 'public') {
276
+ flags.public = true;
277
+ if (value !== null)
278
+ malformed = true;
279
+ }
280
+ else if (name === 'private') {
281
+ flags.private = true;
282
+ if (!validCacheFieldNameList(value))
283
+ malformed = true;
284
+ }
285
+ else if (name === 'immutable') {
286
+ flags.immutable = true;
287
+ if (value !== null)
288
+ malformed = true;
289
+ }
290
+ else if (name === 'max-age') {
291
+ const parsedSeconds = cacheSeconds(value);
292
+ if (parsedSeconds === null || maxAgeSeen && parsedSeconds !== maxAgeSeconds) {
293
+ malformed = true;
294
+ maxAgeMalformed = true;
295
+ maxAgeSeconds = null;
296
+ }
297
+ else if (!maxAgeMalformed)
298
+ maxAgeSeconds = parsedSeconds;
299
+ maxAgeSeen = true;
300
+ }
301
+ else if (name === 's-maxage') {
302
+ const parsedSeconds = cacheSeconds(value);
303
+ if (parsedSeconds === null || sMaxAgeSeen && parsedSeconds !== sMaxAgeSeconds) {
304
+ malformed = true;
305
+ sMaxAgeMalformed = true;
306
+ sMaxAgeSeconds = null;
307
+ }
308
+ else if (!sMaxAgeMalformed)
309
+ sMaxAgeSeconds = parsedSeconds;
310
+ sMaxAgeSeen = true;
311
+ }
312
+ else
313
+ unknownDirectiveCount += 1;
314
+ }
315
+ if (parsed.truncated) {
316
+ flags.noStore = false;
317
+ flags.noCache = false;
318
+ flags.mustRevalidate = false;
319
+ flags.public = false;
320
+ flags.private = false;
321
+ flags.immutable = false;
322
+ maxAgeSeconds = null;
323
+ sMaxAgeSeconds = null;
324
+ maxAgeSeen = false;
325
+ sMaxAgeSeen = false;
326
+ unknownDirectiveCount = 0;
327
+ }
328
+ const ageSeconds = age.state === 'observed' ? cacheSeconds(age.value, false) : null;
329
+ if (age.state === 'observed' && ageSeconds === null)
330
+ malformed = true;
331
+ const etag = headerValidity(etagHeader, (value) => /^(?:W\/)?"[\x21\x23-\x7e]*"$/u.test(value));
332
+ const lastModified = headerValidity(lastModifiedHeader, validHttpDate);
333
+ const expires = headerValidity(expiresHeader, validHttpDate);
334
+ malformed = malformed || [etag, lastModified, expires].some((item) => item.present && item.valid === false);
335
+ const headersObserved = [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
336
+ .some((header) => header.state !== 'not_observed');
337
+ const partial = parsed.truncated || [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
338
+ .some((header) => header.state === 'partial');
339
+ malformed = malformed || [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
340
+ .some((header) => header.state === 'malformed');
341
+ const status = partial
342
+ ? 'partial'
343
+ : malformed
344
+ ? 'malformed'
345
+ : headersObserved ? 'observed' : 'not_observed';
346
+ return {
347
+ value: {
348
+ status,
349
+ ...flags,
350
+ maxAgeSeconds,
351
+ sMaxAgeSeconds,
352
+ ageSeconds,
353
+ maxAgePresent: maxAgeSeen,
354
+ sMaxAgePresent: sMaxAgeSeen,
355
+ agePresent: age.state !== 'not_observed',
356
+ unknownDirectiveCount,
357
+ etag,
358
+ lastModified,
359
+ expires,
360
+ },
361
+ malformed,
362
+ truncated: partial,
363
+ };
364
+ }
365
+ function buildDeliveryMetadata(headers) {
366
+ const contentEncoding = parseContentEncoding(headers);
367
+ const cacheResult = parseCachePolicy(headers);
368
+ const cachePolicy = cacheResult.value;
369
+ const incomplete = [contentEncoding.status, cachePolicy.status].some((status) => ['partial', 'malformed'].includes(status));
370
+ const truncated = contentEncoding.status === 'partial' || cacheResult.truncated;
371
+ const limitations = [
372
+ HTTP_DELIVERY_LIMITATIONS.scope,
373
+ ];
374
+ if (truncated)
375
+ limitations.push(HTTP_DELIVERY_LIMITATIONS.bounds);
376
+ if (contentEncoding.status === 'malformed' || cacheResult.malformed)
377
+ limitations.push(HTTP_DELIVERY_LIMITATIONS.malformed);
378
+ return {
379
+ version: HTTP_DELIVERY_METADATA_VERSION,
380
+ status: incomplete ? 'partial' : 'success',
381
+ complete: !incomplete,
382
+ truncated,
383
+ limitations,
384
+ contentEncoding,
385
+ cachePolicy,
386
+ };
387
+ }
63
388
  function declaredContentLength(headers) {
64
389
  const raw = headerValue(headers, 'content-length', 32);
65
390
  if (!raw || !/^\d+$/.test(raw))
@@ -181,6 +506,7 @@ function buildHttpObservation(detail, options = {}) {
181
506
  bytes: capturedBodyBytes,
182
507
  } : null,
183
508
  securityHeaders: securityHeaders(response && response.headers),
509
+ deliveryMetadata: buildDeliveryMetadata(response && response.headers),
184
510
  },
185
511
  };
186
512
  }
@@ -230,4 +556,4 @@ function skippedHttpObservation() {
230
556
  response: null,
231
557
  };
232
558
  }
233
- export { MAX_HTTP_PROVENANCE_URL, MAX_HTTP_REDIRECTS, MAX_HTTP_ATTEMPTS, normalizeProvenanceUrl, buildHttpObservation, failedHttpObservation, skippedHttpObservation, };
559
+ export { MAX_HTTP_ERROR_LENGTH, MAX_HTTP_PROVENANCE_URL, MAX_HTTP_REDIRECTS, MAX_HTTP_ATTEMPTS, HTTP_DELIVERY_METADATA_VERSION, MAX_HTTP_DELIVERY_HEADER_LENGTH, MAX_HTTP_DELIVERY_TOKENS, normalizeProvenanceUrl, buildHttpObservation, failedHttpObservation, skippedHttpObservation, };
@@ -0,0 +1,124 @@
1
+ import { DOMAIN_CONTROL_MANIFEST_COMPATIBILITY } from "../packages/contracts/domain-control-manifest.mjs";
2
+ import { CLI_CASE_PACK_COMPATIBILITY, ENCRYPTED_WORKSPACE_ARCHIVE_COMPATIBILITY, WORKSPACE_ARCHIVE_COMPATIBILITY, } from "../packages/contracts/case-portability.mjs";
3
+ import { LOOKUP_CLAIM_PASSPORT_COMPATIBILITY } from "../packages/contracts/investigation-portability.mjs";
4
+ import { BRAND_PROFILE_EXPORT_COMPATIBILITY } from "../packages/contracts/workspace-portability.mjs";
5
+ import { LOOKUP_EVIDENCE_COMPATIBILITY } from "./evidence-export.mjs";
6
+ const exact = Object.freeze(['domain_identity', 'dns_control_expectations', 'certificate_control_expectations', 'registrar_control_expectations', 'renewal_review']);
7
+ function compatibilityReference(descriptor) {
8
+ if (!descriptor.schema || descriptor.futureVersionBehavior !== 'reject') {
9
+ throw new TypeError(`Interchange compatibility ${descriptor.id} must name a schema and reject future versions.`);
10
+ }
11
+ return Object.freeze({
12
+ compatibilityEntryId: descriptor.id,
13
+ schema: descriptor.schema,
14
+ versions: descriptor.supportedVersions,
15
+ futureVersionBehaviour: 'reject',
16
+ });
17
+ }
18
+ export const INTERCHANGE_ARTIFACT_CONTRACTS = Object.freeze([
19
+ Object.freeze({
20
+ id: 'lookup_claim_passport',
21
+ ...compatibilityReference(LOOKUP_CLAIM_PASSPORT_COMPATIBILITY),
22
+ versionField: 'version',
23
+ nestedSchemaPath: Object.freeze([]),
24
+ browser: Object.freeze({ import: 'unsupported', export: 'supported' }),
25
+ cli: Object.freeze({ read: 'supported', write: 'unsupported', verify: 'supported' }),
26
+ fidelity: 'semantic_exact_after_normalisation',
27
+ requiredAssurance: 'whole_integrity',
28
+ preservedFieldGroups: Object.freeze(['target_identity', 'claim_identity', 'evidence_requirement_ids', 'source_states', 'observation_times', 'model_versions', 'limitations']),
29
+ excludedFieldGroups: Object.freeze(['raw_registry_payloads', 'contacts', 'page_values', 'request_paths', 'credentials', 'browser_local_records', 'signer_authentication']),
30
+ }),
31
+ Object.freeze({
32
+ id: 'lookup_evidence',
33
+ ...compatibilityReference(LOOKUP_EVIDENCE_COMPATIBILITY),
34
+ versionField: 'schemaVersion',
35
+ nestedSchemaPath: Object.freeze([]),
36
+ browser: Object.freeze({ import: 'supported', export: 'supported' }),
37
+ cli: Object.freeze({ read: 'supported', write: 'supported', verify: 'supported' }),
38
+ fidelity: 'lossy_by_design',
39
+ requiredAssurance: 'structure',
40
+ preservedFieldGroups: Object.freeze(['query_context', 'generator_metadata', 'source_health', 'normalised_registration_evidence', 'bounded_replay_facts', 'observation_times', 'limitations']),
41
+ excludedFieldGroups: Object.freeze(['browser_local_records', 'case_context', 'brand_profiles', 'credentials', 'session_material', 'request_headers', 'cookies', 'signer_authentication', 'raw_payload_rendering_during_replay']),
42
+ }),
43
+ Object.freeze({
44
+ id: 'domain_control_passport',
45
+ ...compatibilityReference(DOMAIN_CONTROL_MANIFEST_COMPATIBILITY),
46
+ versionField: 'version',
47
+ nestedSchemaPath: Object.freeze([]),
48
+ browser: Object.freeze({ import: 'supported', export: 'supported' }),
49
+ cli: Object.freeze({ read: 'supported', write: 'supported', verify: 'supported' }),
50
+ fidelity: 'normalised_merge',
51
+ requiredAssurance: 'whole_integrity',
52
+ preservedFieldGroups: exact,
53
+ excludedFieldGroups: Object.freeze(['profile_identity', 'brand_context', 'contacts', 'notes', 'observations', 'planning_context', 'suppression_context']),
54
+ }),
55
+ Object.freeze({
56
+ id: 'brand_profiles',
57
+ ...compatibilityReference(BRAND_PROFILE_EXPORT_COMPATIBILITY),
58
+ versionField: 'version',
59
+ nestedSchemaPath: Object.freeze([]),
60
+ browser: Object.freeze({ import: 'supported', export: 'supported' }),
61
+ cli: Object.freeze({ read: 'supported', write: 'unsupported', verify: 'verification_only' }),
62
+ fidelity: 'normalised_merge',
63
+ requiredAssurance: 'structure',
64
+ preservedFieldGroups: Object.freeze(['profile_identity', 'brand_context', 'domain_scope', 'mail_posture', 'desired_posture', 'reviewed_planning', 'retained_observations']),
65
+ excludedFieldGroups: Object.freeze(['login_sessions', 'credentials', 'raw_upstream_payloads']),
66
+ }),
67
+ Object.freeze({
68
+ id: 'workspace',
69
+ ...compatibilityReference(WORKSPACE_ARCHIVE_COMPATIBILITY),
70
+ versionField: 'version',
71
+ nestedSchemaPath: Object.freeze([]),
72
+ browser: Object.freeze({ import: 'supported', export: 'supported' }),
73
+ cli: Object.freeze({ read: 'verification_only', write: 'unsupported', verify: 'supported' }),
74
+ fidelity: 'normalised_merge',
75
+ requiredAssurance: 'applicable_integrity',
76
+ preservedFieldGroups: Object.freeze(['supported_workspace_sections', 'section_versions', 'section_checksums', 'merge_metadata', 'analyst_review_lifecycle']),
77
+ excludedFieldGroups: Object.freeze(['login_sessions', 'credentials', 'hosted_monitor_keys', 'raw_upstream_payloads', 'tab_state', 'certificate_transparency_history']),
78
+ }),
79
+ Object.freeze({
80
+ id: 'encrypted_workspace',
81
+ ...compatibilityReference(ENCRYPTED_WORKSPACE_ARCHIVE_COMPATIBILITY),
82
+ versionField: 'version',
83
+ nestedSchemaPath: Object.freeze([]),
84
+ browser: Object.freeze({ import: 'supported', export: 'supported' }),
85
+ cli: Object.freeze({ read: 'verification_only', write: 'unsupported', verify: 'supported' }),
86
+ fidelity: 'verification_only',
87
+ requiredAssurance: 'authenticated_whole_integrity',
88
+ preservedFieldGroups: Object.freeze(['encrypted_workspace_envelope', 'authenticated_ciphertext', 'inner_workspace_after_decryption']),
89
+ excludedFieldGroups: Object.freeze(['passphrase', 'derived_key']),
90
+ }),
91
+ Object.freeze({
92
+ id: 'case_pack',
93
+ ...compatibilityReference(CLI_CASE_PACK_COMPATIBILITY),
94
+ versionField: 'version',
95
+ nestedSchemaPath: Object.freeze(['packet']),
96
+ browser: Object.freeze({ import: 'supported', export: 'unsupported' }),
97
+ cli: Object.freeze({ read: 'supported', write: 'supported', verify: 'supported' }),
98
+ fidelity: 'lossy_by_design',
99
+ requiredAssurance: 'whole_integrity',
100
+ preservedFieldGroups: Object.freeze(['redacted_case_collection', 'case_schema_version', 'trusted_internal_case_brand_profile_references']),
101
+ excludedFieldGroups: Object.freeze(['package_reports', 'package_redaction_manifest', 'package_integrity_envelope', 'audience_excluded_case_fields', 'public_audience_case_brand_profile_references']),
102
+ }),
103
+ Object.freeze({
104
+ id: 'legacy_desired_baseline',
105
+ compatibilityEntryId: null,
106
+ schema: 'whoisleuth.desired-posture-baseline',
107
+ versions: Object.freeze([1]),
108
+ versionField: 'schemaVersion',
109
+ nestedSchemaPath: Object.freeze([]),
110
+ browser: Object.freeze({ import: 'unsupported', export: 'unsupported' }),
111
+ cli: Object.freeze({ read: 'unsupported', write: 'unsupported', verify: 'unsupported' }),
112
+ fidelity: 'unsupported',
113
+ requiredAssurance: 'unsupported',
114
+ preservedFieldGroups: Object.freeze([]),
115
+ excludedFieldGroups: Object.freeze(['entire_legacy_document']),
116
+ futureVersionBehaviour: 'reject',
117
+ }),
118
+ ]);
119
+ export function interchangeContractFor(id) {
120
+ const contract = INTERCHANGE_ARTIFACT_CONTRACTS.find((item) => item.id === id);
121
+ if (!contract)
122
+ throw new TypeError('Interchange contract is not registered.');
123
+ return contract;
124
+ }
@@ -64,8 +64,9 @@ function lookupLocalGeoIp(database, address) {
64
64
  return Object.freeze({ state: 'invalid', address: null, match: null, source: null, limitations: Object.freeze([]) });
65
65
  }
66
66
  const match = database.records.find((item) => prefixContains(item.parsedNetwork, target)) ?? null;
67
+ const incomplete = database.truncated || database.rejectedCount > 0;
67
68
  return Object.freeze({
68
- state: match ? database.truncated || database.rejectedCount > 0 ? 'partial' : 'matched' : 'not_found',
69
+ state: match ? incomplete ? 'partial' : 'matched' : incomplete ? 'partial' : 'not_found',
69
70
  address: normalized,
70
71
  match: match ? Object.freeze({
71
72
  network: match.network,
@@ -18,10 +18,12 @@ const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
18
18
  // RDAP/WHOIS response, before the next sweep has a chance to run; repeated
19
19
  // or concurrent large scans compound that. `Map` preserves insertion
20
20
  // order, so the oldest entry is always `store.keys().next().value` -
21
- // simple FIFO eviction, not a full LRU (re-caching an existing key doesn't
22
- // move it), which is a fine fit for a cache whose primary job is
23
- // deduplicating near-simultaneous repeat lookups within a few minutes, not
24
- // long-term hit-rate optimization.
21
+ // simple FIFO eviction by expiry, not a full LRU (a cache *hit* doesn't
22
+ // move an entry). Re-caching a key does move it, because setCached deletes
23
+ // the key before re-inserting it, which keeps insertion order aligned with
24
+ // the refreshed expiresAt. That is a fine fit for a cache whose primary job
25
+ // is deduplicating near-simultaneous repeat lookups within a few minutes,
26
+ // not long-term hit-rate optimization.
25
27
  const MAX_ENTRIES = 3000;
26
28
  // MAX_ENTRIES alone bounds count, not size - it doesn't stop a hostile or
27
29
  // compromised registry from serving a near-maximum-size response (RDAP:
@@ -0,0 +1,9 @@
1
+ // Browser-safe collection bounds shared by the network evidence producers,
2
+ // the Lookup response trust boundary, and the display projection.
3
+ export const MAX_LOOKUP_DNS_RECORDS_PER_TYPE = 16;
4
+ export const MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS = 8;
5
+ export const MAX_LOOKUP_TLS_CHAIN_CERTIFICATES = 8;
6
+ export const MAX_LOOKUP_TLS_ALT_NAMES = 50;
7
+ export const MAX_LOOKUP_TLS_NAME_VALUES = 4;
8
+ export const MAX_LOOKUP_TLS_FINDINGS = 5;
9
+ export const MAX_LOOKUP_TLS_CERTIFICATE_POLICIES = 16;
package/lib/lookup.mjs CHANGED
@@ -21,6 +21,7 @@ import { lookupUrlhausDomain } from "./urlhaus-intelligence.mjs";
21
21
  import { lookupThreatfoxDomain } from "./threatfox-intelligence.mjs";
22
22
  import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
23
23
  import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
24
+ import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
24
25
  import { buildRegistryInsights } from "./registry-insights.mjs";
25
26
  import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
26
27
  import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
@@ -67,6 +68,24 @@ function boundedSourceDetail(err, fallback) {
67
68
  .trim()
68
69
  .slice(0, 240) || fallback;
69
70
  }
71
+ function withoutNestedPublicationMetadata(value) {
72
+ if (!value || typeof value !== 'object' || Array.isArray(value))
73
+ return value;
74
+ const output = { ...value };
75
+ delete output.publicationMetadata;
76
+ return output;
77
+ }
78
+ function withoutNestedDeliveryMetadata(value) {
79
+ if (!value || typeof value !== 'object' || Array.isArray(value))
80
+ return value;
81
+ const output = { ...value };
82
+ if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
83
+ const response = { ...output.response };
84
+ delete response.deliveryMetadata;
85
+ output.response = response;
86
+ }
87
+ return output;
88
+ }
70
89
  function boundedTimingMs(value) {
71
90
  if (!Number.isFinite(value))
72
91
  return 0;
@@ -167,6 +186,8 @@ async function runUnifiedLookup(classified, options = {}) {
167
186
  includeExtendedDnsContext: !compact,
168
187
  includeInheritedCaa: !fast && !compact,
169
188
  includeCredentialSurfaceProfile: !fast && !compact,
189
+ includePublicationMetadata: !fast && !compact,
190
+ includeDeliveryMetadata: !fast && !compact,
170
191
  includeStructuredDataIdentity: !fast && !compact,
171
192
  includeTechnologyProfile: !fast,
172
193
  includeSecurityPosture: !compact,
@@ -320,7 +341,7 @@ async function runUnifiedLookup(classified, options = {}) {
320
341
  ? { ...rdapRecord, ...(registrarRdap ? { registrarRdap } : {}) }
321
342
  : {
322
343
  error: rdapResult.status === 'rejected'
323
- ? errorMessage(rdapResult.reason, 'RDAP lookup failed')
344
+ ? boundedSourceDetail(rdapResult.reason, 'RDAP lookup failed')
324
345
  : 'No RDAP registry found for this query via IANA bootstrap',
325
346
  attempts: rdapAttempts,
326
347
  };
@@ -337,7 +358,7 @@ async function runUnifiedLookup(classified, options = {}) {
337
358
  else {
338
359
  whois = {
339
360
  error: whoisResult.status === 'rejected'
340
- ? errorMessage(whoisResult.reason, 'WHOIS lookup failed')
361
+ ? boundedSourceDetail(whoisResult.reason, 'WHOIS lookup failed')
341
362
  : 'WHOIS returned no referral chain',
342
363
  };
343
364
  }
@@ -528,10 +549,12 @@ async function runUnifiedLookup(classified, options = {}) {
528
549
  const bulkComparison = !fast
529
550
  ? buildBulkComparisonEvidence(availability)
530
551
  : null;
531
- const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, ...compactAvailability } = availability;
552
+ const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, pageIdentity: richPageIdentity, http: richHttp, ...compactAvailability } = availability;
532
553
  return {
533
554
  availability: {
534
555
  ...compactAvailability,
556
+ ...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
557
+ ...(richHttp !== undefined ? { http: withoutNestedDeliveryMetadata(richHttp) } : {}),
535
558
  ...(bulkComparison ? { bulkComparison } : {}),
536
559
  },
537
560
  diagnostics,
@@ -555,7 +578,7 @@ async function runUnifiedLookup(classified, options = {}) {
555
578
  : createThreatIntelligenceResult(THREATFOX_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-IOC intelligence could not be completed.' }));
556
579
  }
557
580
  const threatIntelligence = threatIntelligenceProviders.length
558
- ? { version: 1, providers: threatIntelligenceProviders }
581
+ ? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers: threatIntelligenceProviders }
559
582
  : null;
560
583
  const registrarRdapParsed = registrarRdap
561
584
  && typeof registrarRdap.parsed === 'object'
@@ -4,7 +4,7 @@
4
4
  // evidence. It performs one logical IP RDAP enrichment through the existing
5
5
  // cached, SSRF-safe RDAP boundary and never participates in domain availability.
6
6
  import * as net from 'node:net';
7
- import { createObservation } from "./observation.mjs";
7
+ import { createObservation } from "../packages/evidence/observation.mjs";
8
8
  import { fetchRdapRecord } from "./rdap.mjs";
9
9
  import { isPrivateAddress } from "./safe-fetch.mjs";
10
10
  const OBSERVED_NETWORK_CONTEXT_VERSION = 1;
@@ -1,8 +1,7 @@
1
- import packageDocument from '../package.json' with { type: 'json' };
1
+ import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
2
2
  import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "./project-metadata.mjs";
3
- import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
4
3
  export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
5
- export const WHOISLEUTH_USER_AGENT_VERSION = normalizeBoundedSemanticVersion(packageDocument.version, 'Application manifest');
4
+ export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
6
5
  export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
7
6
  export function whoisleuthRequestHeaders(headers = {}) {
8
7
  return { ...headers, 'User-Agent': WHOISLEUTH_USER_AGENT };
@@ -13,8 +13,8 @@ const MAX_FORM_FINGERPRINTS = 50;
13
13
  const MAX_FORM_CONTROLS = 500;
14
14
  const MAX_RESOURCE_HOSTS = 30;
15
15
  const MAX_IDENTIFIER_VALUES = 30;
16
- const CONTROL_RE = /[\u0000-\u001f\u007f]/g;
17
- const HAS_CONTROL_RE = /[\u0000-\u001f\u007f]/;
16
+ const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
17
+ const HAS_CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
18
18
  const COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g;
19
19
  const RAW_BODY_RE = /<(script|style|noscript|textarea|template)\b([^>]*)>[\s\S]*?(?:<\/\1\s*>|$)/gi;
20
20
  const MARKUP_TOKEN_RE = /<\/?[A-Za-z][A-Za-z0-9:-]*\b[^>]*>|[^<]+/g;
@@ -2,7 +2,7 @@
2
2
  // captured by Deep Lookup. The classifier emits fixed labels and evidence
3
3
  // descriptions only; it does not retain page text, arbitrary attributes, URLs,
4
4
  // or matched markup, and it never treats a role as proof of intent or safety.
5
- import { createObservation } from "./observation.mjs";
5
+ import { createObservation } from "../packages/evidence/observation.mjs";
6
6
  import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
7
7
  const PAGE_ROLE_PROFILE_VERSION = 1;
8
8
  const MAX_PAGE_ROLE_FINDINGS = 4;