@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
@@ -10,6 +10,7 @@
10
10
  // In-memory, so (like the bootstrap cache and the rate limiter) this
11
11
  // applies globally on server.mts's one long-lived process, but only within
12
12
  // a single warm container on Netlify Functions.
13
+ import { abortable } from "./abort.mjs";
13
14
  const TTL_MS = 3 * 60 * 1000; // 3 minutes
14
15
  const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
15
16
  // Bounds worst-case memory regardless of the TTL/sweep - without this, a
@@ -72,7 +73,7 @@ function getCached(key) {
72
73
  deleteEntry(key);
73
74
  return undefined;
74
75
  }
75
- return entry.value;
76
+ return structuredClone(entry.value);
76
77
  }
77
78
  function setCached(key, value) {
78
79
  sweepExpiredEntries(Date.now());
@@ -80,9 +81,16 @@ function setCached(key, value) {
80
81
  const size = approxByteSize(value);
81
82
  // A value whose serialized size cannot be measured cannot participate in a
82
83
  // byte-bounded cache. Return it to the current caller without retaining it.
83
- if (size === null)
84
- return;
85
- store.set(key, { value, expiresAt: Date.now() + TTL_MS, size });
84
+ if (size === null || size > MAX_TOTAL_BYTES)
85
+ return { value, owned: false };
86
+ let snapshot;
87
+ try {
88
+ snapshot = structuredClone(value);
89
+ }
90
+ catch {
91
+ return { value, owned: false };
92
+ }
93
+ store.set(key, { value: snapshot, expiresAt: Date.now() + TTL_MS, size });
86
94
  totalBytes += size;
87
95
  while (store.size > 0 && (store.size > MAX_ENTRIES || totalBytes > MAX_TOTAL_BYTES)) {
88
96
  const oldestKey = store.keys().next().value;
@@ -90,6 +98,12 @@ function setCached(key, value) {
90
98
  break;
91
99
  deleteEntry(oldestKey);
92
100
  }
101
+ return { value: snapshot, owned: true };
102
+ }
103
+ // Stored data and coalesced results belong to the cache, not to a caller. Each
104
+ // consumer receives its own copy, including the first and concurrent callers.
105
+ function callerResult(result) {
106
+ return result.owned ? structuredClone(result.value) : result.value;
93
107
  }
94
108
  // Concurrent callers for the same key (e.g. a bulk scan's concurrent
95
109
  // workers, or a fast scan immediately followed by a deep-check, hitting the
@@ -103,25 +117,34 @@ const inFlight = new Map();
103
117
  // The cache intentionally holds heterogeneous public lookup results. Callers
104
118
  // supply distinct result contracts, so this internal compatibility boundary
105
119
  // stays dynamically typed rather than asserting one shared payload shape.
106
- async function cached(key, factory) {
120
+ async function cached(key, factory, signal) {
121
+ signal?.throwIfAborted();
107
122
  const hit = getCached(key);
108
123
  if (hit !== undefined)
109
124
  return hit;
125
+ // A deadline-scoped caller may reuse completed public data, but must own
126
+ // its cancellable work rather than aborting another caller's shared request.
127
+ if (signal) {
128
+ const value = await abortable(factory, signal);
129
+ signal.throwIfAborted();
130
+ return callerResult(setCached(key, value));
131
+ }
110
132
  const pending = inFlight.get(key);
111
133
  if (pending)
112
- return pending;
113
- const promise = (async () => {
134
+ return callerResult(await pending);
135
+ // Register pending work before invoking the factory, including factories
136
+ // that throw synchronously. A rejected promise must never poison the key.
137
+ const promise = Promise.resolve().then(async () => {
114
138
  try {
115
139
  const value = await factory();
116
- setCached(key, value);
117
- return value;
140
+ return setCached(key, value);
118
141
  }
119
142
  finally {
120
143
  inFlight.delete(key);
121
144
  }
122
- })();
145
+ });
123
146
  inFlight.set(key, promise);
124
- return promise;
147
+ return callerResult(await promise);
125
148
  }
126
149
  // Expired entries are swept lazily during cache activity. This preserves the
127
150
  // long-running Express cleanup behavior without creating an import-time timer,
@@ -1,19 +1,21 @@
1
1
  // Browser-safe version and collection boundaries shared by Lookup child
2
2
  // profile producers and the client response trust boundary.
3
3
  import { MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, normalizeExplicitIsoTimestamp, } from "../packages/evidence/observation.mjs";
4
- import { PAGE_FINGERPRINT_VERSION, PAGE_IDENTITY_VERSION, } from "../packages/contracts/workspace-portability.mjs";
4
+ import { PAGE_FINGERPRINT_PARSERS, PAGE_IDENTITY_VERSION, } from "../packages/contracts/workspace-portability.mjs";
5
5
  import { validPagePublicationMetadata } from "./homepage-metadata-contract.mjs";
6
+ import { PAGE_FINGERPRINT_TOKEN_LIMITS } from "../packages/contracts/page-fingerprints.mjs";
6
7
  import { MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, TLS_PROFILE_VERSION, } from "./lookup-network-evidence-bounds.mjs";
7
8
  import { isJsonObject, normalizedDomain, validBoundedString, validHttpProvenanceUrl, validObservationDiagnostics, validOptionalNullableText, validStringArray, validUint, } from "./lookup-contract-primitives.mjs";
8
9
  import { TECHNOLOGY_EVIDENCE_ROLE_ORDER } from "./technology-evidence-role.mjs";
9
- export const TECHNOLOGY_PROFILE_VERSION = 11;
10
- export const SUPPORTED_TECHNOLOGY_PROFILE_VERSIONS = Object.freeze([10, TECHNOLOGY_PROFILE_VERSION]);
10
+ import { MAX_HOMEPAGE_BYTES } from "./outbound-request-bounds.mjs";
11
+ export const TECHNOLOGY_PROFILE_VERSION = 12;
12
+ export const SUPPORTED_TECHNOLOGY_PROFILE_VERSIONS = Object.freeze([10, 11, TECHNOLOGY_PROFILE_VERSION]);
11
13
  export const MAX_TECHNOLOGY_FINDINGS = 24;
12
14
  export const MAX_EVIDENCE_PER_TECHNOLOGY = 4;
13
15
  export const MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH = 180;
14
16
  export const BROWSER_LIBRARY_PROFILE_VERSION = 2;
15
17
  export const MAX_LIBRARY_FINDINGS = 16;
16
- export const WEBSITE_SECURITY_POSTURE_VERSION = 2;
18
+ export const WEBSITE_SECURITY_POSTURE_VERSION = 3;
17
19
  export const MAX_SECURITY_POSTURE_FINDINGS = 32;
18
20
  export const CREDENTIAL_SURFACE_PROFILE_VERSION = 1;
19
21
  export const STRUCTURED_DATA_IDENTITY_VERSION = 1;
@@ -169,6 +171,7 @@ function validFingerprintIdentifier(value) {
169
171
  && /^[A-Z0-9-]{1,64}$/u.test(value.value);
170
172
  }
171
173
  function validPageFingerprintProfile(value) {
174
+ const tokenLimit = PAGE_FINGERPRINT_TOKEN_LIMITS[value.fingerprintVersion];
172
175
  const normalizedHtml = value.normalizedHtml;
173
176
  const visibleText = value.visibleText;
174
177
  const domStructure = value.domStructure;
@@ -181,7 +184,7 @@ function validPageFingerprintProfile(value) {
181
184
  || normalizedHtml.algorithm !== 'sha256'
182
185
  || typeof normalizedHtml.value !== 'string'
183
186
  || !SHA256_RE.test(normalizedHtml.value)
184
- || !validUint(normalizedHtml.tokenCount, 4_096)
187
+ || !validUint(normalizedHtml.tokenCount, tokenLimit)
185
188
  || typeof normalizedHtml.truncated !== 'boolean'
186
189
  || !(visibleText === null || isJsonObject(visibleText)
187
190
  && hasExactKeys(visibleText, ['algorithm', 'value', 'tokenCount', 'featureCount', 'truncated'])
@@ -197,16 +200,16 @@ function validPageFingerprintProfile(value) {
197
200
  || domStructure.algorithm !== 'sha256'
198
201
  || typeof domStructure.value !== 'string'
199
202
  || !SHA256_RE.test(domStructure.value)
200
- || !validUint(domStructure.nodeCount, 4_096)
201
- || domStructure.parser !== 'static-tag-sequence-v1'
203
+ || !validUint(domStructure.nodeCount, tokenLimit)
204
+ || domStructure.parser !== PAGE_FINGERPRINT_PARSERS[value.fingerprintVersion]
202
205
  || typeof domStructure.truncated !== 'boolean'
203
206
  || !(domStructure.similarity === undefined || domStructure.similarity === null || isJsonObject(domStructure.similarity)
204
207
  && hasExactKeys(domStructure.similarity, ['algorithm', 'value', 'tokenCount', 'featureCount', 'truncated'])
205
208
  && domStructure.similarity.algorithm === 'simhash64-v1'
206
209
  && typeof domStructure.similarity.value === 'string'
207
210
  && SIMHASH64_RE.test(domStructure.similarity.value)
208
- && validUint(domStructure.similarity.tokenCount, 4_096)
209
- && validUint(domStructure.similarity.featureCount, 4_096)
211
+ && validUint(domStructure.similarity.tokenCount, tokenLimit)
212
+ && validUint(domStructure.similarity.featureCount, tokenLimit)
210
213
  && typeof domStructure.similarity.truncated === 'boolean')
211
214
  || !(formStructure === null || isJsonObject(formStructure)
212
215
  && hasExactKeys(formStructure, ['algorithm', 'value', 'formCount', 'controlCount', 'truncated'])
@@ -461,7 +464,7 @@ function browserLibraryProfileContractState(value) {
461
464
  || !isJsonObject(knownExploitedCatalog)
462
465
  || !hasOnlyKeys(knownExploitedCatalog, ['name', 'version', 'releasedAt'])
463
466
  || !['name', 'version', 'releasedAt'].every((field) => validBoundedString(knownExploitedCatalog[field], 160))
464
- || normalizeExplicitIsoTimestamp(knownExploitedCatalog.releasedAt) !== knownExploitedCatalog.releasedAt
467
+ || normalizeExplicitIsoTimestamp(knownExploitedCatalog.releasedAt) === null
465
468
  || !Array.isArray(profile.findings)
466
469
  || profile.findings.length > MAX_LIBRARY_FINDINGS)
467
470
  return 'invalid';
@@ -488,6 +491,8 @@ function browserLibraryProfileContractState(value) {
488
491
  || !validExactStringArray(candidate.knownExploitedIdentifiers, 16, 80)
489
492
  || !validExactStringArray(candidate.weaknessClasses, 12, 80))
490
493
  return 'invalid';
494
+ // Published profiles could retain legacy upstream identifier spellings.
495
+ // Keep that reader boundary; current catalogue writers validate syntax.
491
496
  if (findingIds.has(candidate.id)
492
497
  || !candidate.advisoryIdentifiers.every((identifier) => typeof identifier === 'string'
493
498
  && /^(?:CVE-[0-9X-]+|GHSA-[A-Z0-9-]+)$/u.test(identifier))
@@ -495,7 +500,9 @@ function browserLibraryProfileContractState(value) {
495
500
  && /^CVE-[0-9X-]+$/u.test(identifier))
496
501
  || !candidate.weaknessClasses.every((identifier) => typeof identifier === 'string'
497
502
  && /^CWE-[0-9]+$/u.test(identifier))
498
- || Number(candidate.advisoryCount) < candidate.advisoryIdentifiers.length
503
+ // One advisory can have several identifiers, including CVE and GHSA
504
+ // aliases. Their count is not the number of matching advisory records.
505
+ || (Number(candidate.advisoryCount) === 0 && candidate.advisoryIdentifiers.length > 0)
499
506
  || Number(candidate.knownExploitedCount) < candidate.knownExploitedIdentifiers.length)
500
507
  return 'invalid';
501
508
  findingIds.add(candidate.id);
@@ -503,7 +510,7 @@ function browserLibraryProfileContractState(value) {
503
510
  return 'supported';
504
511
  }
505
512
  function pageFingerprintContractState(value) {
506
- const versionState = childVersionState(value, 'fingerprintVersion', [PAGE_FINGERPRINT_VERSION]);
513
+ const versionState = childVersionState(value, 'fingerprintVersion', Object.keys(PAGE_FINGERPRINT_PARSERS).map(Number));
507
514
  if (versionState !== 'supported')
508
515
  return versionState;
509
516
  const profile = value;
@@ -520,7 +527,9 @@ function pageFingerprintContractState(value) {
520
527
  || typeof profile.exact.value !== 'string'
521
528
  || !SHA256_RE.test(profile.exact.value)
522
529
  || !['complete-body', 'captured-prefix'].includes(String(profile.exact.scope))
523
- || !validUint(profile.exact.bytes, 300_000)
530
+ // The published first-generation writer had a fixed 300,000-byte ceiling.
531
+ // Current native fingerprints share the collector's source-size policy.
532
+ || !validUint(profile.exact.bytes, profile.fingerprintVersion === 1 ? 300_000 : MAX_HOMEPAGE_BYTES)
524
533
  || !['captured-response-bytes', 'decoded-markup'].includes(String(profile.exact.source))
525
534
  || !validPageFingerprintProfile(profile)
526
535
  || profile.complete !== (profile.truncated !== true))
@@ -647,7 +656,7 @@ function tlsProfileContractState(value) {
647
656
  return 'supported';
648
657
  }
649
658
  function securityPostureContractState(value) {
650
- const versionState = childVersionState(value, 'postureVersion', [WEBSITE_SECURITY_POSTURE_VERSION]);
659
+ const versionState = childVersionState(value, 'postureVersion', [2, WEBSITE_SECURITY_POSTURE_VERSION]);
651
660
  if (versionState !== 'supported')
652
661
  return versionState;
653
662
  const profile = value;
@@ -855,4 +864,4 @@ function sanitizeLookupChildProfiles(value) {
855
864
  }
856
865
  return output;
857
866
  }
858
- export { sanitizeLookupChildProfiles, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, };
867
+ export { sanitizeLookupChildProfiles, technologyProfileContractState, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, };
@@ -3,6 +3,29 @@ import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
3
3
  import { FEATURE_DISABLED_ERROR_CODE } from "./feature-policy.mjs";
4
4
  const LOOKUP_DIAGNOSTICS_VERSION = 8;
5
5
  const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
6
+ const LOOKUP_SOURCE_STATES = Object.freeze([
7
+ 'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
8
+ 'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
9
+ ]);
10
+ const SOURCE_STATES = new Set(LOOKUP_SOURCE_STATES);
11
+ function lookupDiagnosticStates(value) {
12
+ const record = (input) => input && typeof input === 'object' && !Array.isArray(input)
13
+ ? input : {};
14
+ const diagnostics = record(value);
15
+ const candidates = {
16
+ rdap: record(diagnostics.rdap).status,
17
+ registrar_rdap: record(record(diagnostics.rdap).registrar).status,
18
+ whois: record(diagnostics.whois).status,
19
+ availability: record(diagnostics.availability).status,
20
+ reverse_dns: record(diagnostics.reverseDns).status,
21
+ network_context: record(diagnostics.network).status,
22
+ security_txt: record(diagnostics.securityTxt).status,
23
+ sslbl: record(diagnostics.sslbl).status,
24
+ };
25
+ return Object.freeze(Object.fromEntries(Object.entries(candidates).map(([source, state]) => [
26
+ source, typeof state === 'string' && SOURCE_STATES.has(state) ? state : null,
27
+ ])));
28
+ }
6
29
  const LOOKUP_TIMING_VERSION = 1;
7
30
  const MAX_LOOKUP_TIMING_MS = 120_000;
8
31
  const LOOKUP_TIMING_SOURCE_ORDER = Object.freeze([
@@ -93,4 +116,4 @@ function createLookupTimingTracker(enabled, now) {
93
116
  },
94
117
  };
95
118
  }
96
- export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, boundedSourceDetail, createLookupTimingTracker, errorMessage, };
119
+ export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_SOURCE_STATES, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, boundedSourceDetail, createLookupTimingTracker, errorMessage, lookupDiagnosticStates, };
@@ -1,6 +1,7 @@
1
1
  // Portable bounded NDJSON Lookup progress contract. Partial events are
2
2
  // presentation-only; only a validated final ordinary Lookup result may cross
3
3
  // the persistence boundary.
4
+ import { scanBoundedJson, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES } from "./bounded-json.mjs";
4
5
  export const LOOKUP_PROGRESS_SCHEMA = 'whoisleuth.lookup-progress';
5
6
  export const LOOKUP_PROGRESS_VERSION = 1;
6
7
  export const MAX_LOOKUP_PROGRESS_SOURCES = 16;
@@ -241,20 +242,25 @@ export function createLookupProgressNdjsonDecoder(onEvent) {
241
242
  throw new TypeError('NDJSON decoder requires an event consumer.');
242
243
  const decoder = new TextDecoder('utf-8', { fatal: true });
243
244
  let pending = '';
245
+ let pendingBytes = 0;
244
246
  let totalBytes = 0;
245
247
  let eventCount = 0;
246
248
  function consumeLine(line) {
247
249
  if (!line)
248
250
  return;
249
- if (byteLength(line) > MAX_LOOKUP_PROGRESS_FINAL_BYTES + MAX_LOOKUP_PROGRESS_FRAGMENT_BYTES) {
250
- throw new TypeError('Lookup progress line exceeds its byte bound.');
251
- }
252
251
  eventCount += 1;
253
252
  if (eventCount > MAX_LOOKUP_PROGRESS_EVENTS) {
254
253
  throw new TypeError('Lookup progress stream contains too many events.');
255
254
  }
256
255
  let parsed;
257
256
  try {
257
+ // The ordinary result keeps its original limits after decoding. Allow
258
+ // only the fixed progress envelope overhead around that same document.
259
+ scanBoundedJson(line, {
260
+ maximumDepth: MAX_BOUNDED_JSON_DEPTH + 1,
261
+ maximumKeys: MAX_BOUNDED_JSON_KEYS + 8,
262
+ maximumValues: MAX_BOUNDED_JSON_VALUES + MAX_LOOKUP_PROGRESS_SOURCES + 16,
263
+ });
258
264
  parsed = JSON.parse(line);
259
265
  }
260
266
  catch {
@@ -269,16 +275,26 @@ export function createLookupProgressNdjsonDecoder(onEvent) {
269
275
  if (totalBytes > MAX_LOOKUP_PROGRESS_STREAM_BYTES) {
270
276
  throw new TypeError('Lookup progress stream exceeds its total byte bound.');
271
277
  }
272
- pending += decoder.decode(chunk, { stream: true });
273
- let newline = pending.indexOf('\n');
274
- while (newline >= 0) {
275
- const line = pending.slice(0, newline).replace(/\r$/u, '');
276
- pending = pending.slice(newline + 1);
277
- consumeLine(line);
278
- newline = pending.indexOf('\n');
279
- }
280
- if (byteLength(pending) > MAX_LOOKUP_PROGRESS_FINAL_BYTES + MAX_LOOKUP_PROGRESS_FRAGMENT_BYTES) {
281
- throw new TypeError('Lookup progress line exceeds its byte bound.');
278
+ // Count incoming bytes once, before decoding or accumulation. LF cannot
279
+ // occur inside a valid UTF-8 sequence; the fatal decoder still receives
280
+ // every byte, including framing, so chunk boundaries cannot hide errors.
281
+ for (let start = 0; start < chunk.length;) {
282
+ const newline = chunk.indexOf(10, start);
283
+ const end = newline < 0 ? chunk.length : newline;
284
+ pendingBytes += end - start;
285
+ const maximum = MAX_LOOKUP_PROGRESS_FINAL_BYTES + MAX_LOOKUP_PROGRESS_FRAGMENT_BYTES;
286
+ const trailingCr = end > start ? chunk[end - 1] === 13 : pending.endsWith('\r');
287
+ if (pendingBytes > maximum + (trailingCr ? 1 : 0)) {
288
+ throw new TypeError('Lookup progress line exceeds its byte bound.');
289
+ }
290
+ const decoded = decoder.decode(chunk.subarray(start, end + (newline < 0 ? 0 : 1)), { stream: true });
291
+ pending += newline < 0 ? decoded : decoded.slice(0, -1);
292
+ if (newline >= 0) {
293
+ consumeLine(pending.replace(/\r$/u, ''));
294
+ pending = '';
295
+ pendingBytes = 0;
296
+ }
297
+ start = end + 1;
282
298
  }
283
299
  }
284
300
  function finish() {
@@ -286,6 +302,7 @@ export function createLookupProgressNdjsonDecoder(onEvent) {
286
302
  if (pending)
287
303
  consumeLine(pending.replace(/\r$/u, ''));
288
304
  pending = '';
305
+ pendingBytes = 0;
289
306
  }
290
307
  return Object.freeze({ push, finish });
291
308
  }