@slicedearth/whoisleuth-cli 1.47.4 → 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 (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,5 +1,5 @@
1
- import { CASE_SCHEMA_VERSION, MAX_CASE_STORE_BYTES, } from "./case-record-model.js";
2
- import { normalizeCaseStore } from "./case-migration-model.js";
1
+ import { CASE_SCHEMA_VERSION, MAX_CASE_STORE_BYTES, } from "./case-record-model.mjs";
2
+ import { normalizeCaseStore } from "./case-migration-model.mjs";
3
3
  /** The exact versioned string persisted by the browser storage adapter. */
4
4
  export function serializeCaseStore(cases) {
5
5
  return JSON.stringify({ version: CASE_SCHEMA_VERSION, cases });
@@ -3,7 +3,7 @@
3
3
  // connection attempts. Bulk/watchlist/case records deliberately keep only this
4
4
  // smaller derived shape: no paths, queries, header values, attempt errors, or
5
5
  // redirect inventories.
6
- import { MAX_HTTP_EVIDENCE_REDIRECTS } from "../../../../lib/http-evidence-bounds.mjs";
6
+ import { MAX_HTTP_EVIDENCE_REDIRECTS } from "../../lib/http-evidence-bounds.mjs";
7
7
  export const HTTP_SUMMARY_VERSION = 1;
8
8
  export const MAX_HTTP_SUMMARY_ORIGIN_LENGTH = 300;
9
9
  export const MAX_HTTP_SUMMARY_CONTENT_TYPE_LENGTH = 100;
@@ -1,5 +1,5 @@
1
- import { MAX_BULK_SESSIONS, compareBulkSessions, normalizeBulkSession, } from "./bulk-session-model.js";
2
- import { MAX_COMPARISON_LEDGER_BULK_PAIRS, boundedComparisonLedgerId, comparisonLedgerCollector, comparisonLedgerInputArray, comparisonLedgerRecord, comparisonLedgerValuePresent, comparisonLedgerValueState, makeComparisonLedgerCandidate, stableComparisonLedgerId, } from "./comparison-ledger-contract.js";
1
+ import { MAX_BULK_SESSIONS, compareBulkSessions, normalizeBulkSession, } from "../workspace/bulk-session-model.mjs";
2
+ import { MAX_COMPARISON_LEDGER_BULK_PAIRS, boundedComparisonLedgerId, comparisonLedgerCollector, comparisonLedgerInputArray, comparisonLedgerRecord, comparisonLedgerValuePresent, comparisonLedgerValueState, makeComparisonLedgerCandidate, stableComparisonLedgerId, } from "./comparison-ledger-contract.mjs";
3
3
  function bulkCompleteness(session) {
4
4
  return session.state === 'complete' ? 'complete' : session.state === 'partial' ? 'partial' : 'unavailable';
5
5
  }
@@ -1,7 +1,7 @@
1
- import { safeComparisonLedgerHref, stableComparisonLedgerId, } from "./comparison-ledger-serialization.js";
2
- import { projectComparisonLedgerJson } from "./comparison-ledger-json.js";
3
- export { stableComparisonLedgerJson } from "./comparison-ledger-json.js";
4
- export { safeComparisonLedgerHref, stableComparisonLedgerId } from "./comparison-ledger-serialization.js";
1
+ import { safeComparisonLedgerHref, stableComparisonLedgerId, } from "./comparison-ledger-serialization.mjs";
2
+ import { projectComparisonLedgerJson } from "./comparison-ledger-json.mjs";
3
+ export { stableComparisonLedgerJson } from "./comparison-ledger-json.mjs";
4
+ export { safeComparisonLedgerHref, stableComparisonLedgerId } from "./comparison-ledger-serialization.mjs";
5
5
  export const COMPARISON_LEDGER_MODES = Object.freeze([
6
6
  'publication',
7
7
  'entity',
@@ -1,3 +1,4 @@
1
+ // Bounded deterministic JSON projection shared by browser and CLI comparison paths.
1
2
  const MAX_COLLECTION_SCAN = 256;
2
3
  const MAX_DEPTH = 8;
3
4
  const MAX_SCALAR_LENGTH = 2_000;
@@ -1,3 +1,4 @@
1
+ // Stable comparison-ledger identifiers and safe local destinations are runtime-neutral.
1
2
  const CONTROL_RE = /[\u0000-\u001f\u007f]/gu;
2
3
  const MAX_ID_PARTS = 32;
3
4
  const MAX_ID_PART_LENGTH = 500;
@@ -0,0 +1,78 @@
1
+ import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "../../lib/perceptual-hash-comparison.mjs";
2
+ function plainRecord(value) {
3
+ return value && typeof value === 'object' && !Array.isArray(value)
4
+ ? value
5
+ : null;
6
+ }
7
+ function faviconRecord(value) {
8
+ const record = plainRecord(value);
9
+ if (!record || typeof record.domain !== 'string')
10
+ return null;
11
+ const faviconHash = typeof record.faviconHash === 'string' && record.faviconHash
12
+ ? record.faviconHash
13
+ : null;
14
+ const faviconPHash = isPerceptualHash(record.faviconPHash)
15
+ ? record.faviconPHash
16
+ : null;
17
+ return faviconHash || faviconPHash
18
+ ? { domain: record.domain, faviconHash, faviconPHash }
19
+ : null;
20
+ }
21
+ export function groupBySimilarFavicon(records, maxDistance) {
22
+ const items = Array.isArray(records)
23
+ ? records.map(faviconRecord).filter((record) => record !== null)
24
+ : [];
25
+ const parent = items.map((_, index) => index);
26
+ const find = (value) => {
27
+ let root = value;
28
+ while ((parent[root] ?? root) !== root)
29
+ root = parent[root] ?? root;
30
+ while ((parent[value] ?? value) !== root) {
31
+ const next = parent[value] ?? value;
32
+ parent[value] = root;
33
+ value = next;
34
+ }
35
+ return root;
36
+ };
37
+ const union = (left, right) => {
38
+ const leftRoot = find(left);
39
+ const rightRoot = find(right);
40
+ if (leftRoot !== rightRoot)
41
+ parent[leftRoot] = rightRoot;
42
+ };
43
+ const firstByHash = new Map();
44
+ items.forEach((item, index) => {
45
+ if (!item.faviconHash)
46
+ return;
47
+ const existing = firstByHash.get(item.faviconHash);
48
+ if (existing !== undefined)
49
+ union(index, existing);
50
+ else
51
+ firstByHash.set(item.faviconHash, index);
52
+ });
53
+ const withPerceptualHash = [];
54
+ items.forEach((item, index) => {
55
+ if (isInformativePerceptualHash(item.faviconPHash)) {
56
+ withPerceptualHash.push({ index, hash: item.faviconPHash });
57
+ }
58
+ });
59
+ for (let leftIndex = 0; leftIndex < withPerceptualHash.length; leftIndex += 1) {
60
+ for (let rightIndex = leftIndex + 1; rightIndex < withPerceptualHash.length; rightIndex += 1) {
61
+ const left = withPerceptualHash[leftIndex];
62
+ const right = withPerceptualHash[rightIndex];
63
+ if (!left || !right)
64
+ continue;
65
+ const distance = hammingDistanceHex(left.hash, right.hash);
66
+ if (distance !== null && distance <= maxDistance)
67
+ union(left.index, right.index);
68
+ }
69
+ }
70
+ const groups = new Map();
71
+ items.forEach((item, index) => {
72
+ const root = find(index);
73
+ if (!groups.has(root))
74
+ groups.set(root, []);
75
+ groups.get(root)?.push(item.domain);
76
+ });
77
+ return [...groups.values()].filter((domains) => domains.length >= 2);
78
+ }
@@ -2,9 +2,10 @@
2
2
  // module deliberately reports each component independently: it does not
3
3
  // produce an aggregate similarity score, risk factor, or maliciousness
4
4
  // verdict. Inputs pass through the strict baseline normalizer before use.
5
- import { normalizePageBaseline } from "./page-baseline.js";
6
- import { hammingDistanceHex } from "./utils.js";
7
- export const PAGE_COMPARISON_VERSION = 3;
5
+ import { normalizePageBaseline } from "../workspace/page-baseline.mjs";
6
+ import { hammingDistanceHex } from "../../lib/perceptual-hash-comparison.mjs";
7
+ import { PAGE_COMPARISON_VERSION } from "../contracts/offline-comparison.mjs";
8
+ export { PAGE_COMPARISON_VERSION };
8
9
  function component(id, label, method, status, outcome, detail, partial) {
9
10
  return { id, label, method, status, outcome, detail, partial, sharedValues: [] };
10
11
  }
@@ -3,11 +3,10 @@
3
3
  // proof of common ownership, coordination, intent, or maliciousness. This
4
4
  // module deliberately produces no aggregate score and is never a persistence
5
5
  // boundary.
6
- import { normalizeDomain } from "./case-model.js";
7
- import { groupBySimilarFavicon } from "./utils.js";
8
- export const RELATIONSHIP_EVIDENCE_VERSION = 2;
9
- export const TLS_RELATIONSHIP_PROFILE_VERSION = 2;
10
- export const SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS = Object.freeze([1, TLS_RELATIONSHIP_PROFILE_VERSION]);
6
+ import { normalizeDomain } from "../cases/case-model.mjs";
7
+ import { groupBySimilarFavicon } from "./favicon-similarity.mjs";
8
+ import { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, } from "../contracts/offline-comparison.mjs";
9
+ export { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, };
11
10
  export const MAX_RELATIONSHIP_ROWS = 2000;
12
11
  export const MAX_RELATIONSHIP_GROUPS = 100;
13
12
  export const MAX_RELATIONSHIP_DOMAINS = 50;
@@ -93,7 +92,7 @@ function tlsCertificateFingerprint(availability) {
93
92
  const certificate = record(tls?.certificate);
94
93
  const fingerprint = certificate?.fingerprintSha256;
95
94
  const status = tls?.status;
96
- if (tls?.source !== 'tls' || !SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS.includes(Number(tls?.profileVersion))
95
+ if (tls?.source !== 'tls' || !SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS.some((version) => version === Number(tls?.profileVersion))
97
96
  || typeof status !== 'string' || !['success', 'partial'].includes(status) || typeof fingerprint !== 'string'
98
97
  || fingerprint.length !== 64 || !CERTIFICATE_SHA_RE.test(fingerprint))
99
98
  return null;
@@ -240,7 +239,7 @@ export function buildScanRelationships(rawRows) {
240
239
  }
241
240
  }
242
241
  for (const [value, domains] of officialAssets)
243
- output.push(group('official_asset', 'Official asset relationship', 'Configured-domain host match', value, [...domains].sort(), 'One or more pages loaded an asset from this configured official domain or its subdomain.'));
242
+ output.push(group('official_asset', 'Official asset host match', 'Configured-domain host match', value, [...domains].sort(), 'One or more pages loaded an asset from this configured official domain or its subdomain.'));
244
243
  const order = new Map(['nameserver_set', 'ip_address', 'certificate', 'tracking_identifier', 'favicon', 'official_asset']
245
244
  .map((value, index) => [value, index]));
246
245
  output.sort((left, right) => (Number(order.get(left.type)) - Number(order.get(right.type))) || left.value.localeCompare(right.value) || left.domains.join('|').localeCompare(right.domains.join('|')));
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Bounded analyst Review Item schema metadata and shared value types.
3
+ *
4
+ * Lifecycle behaviour is owned by packages/monitoring/analyst-review-state.mts.
5
+ * This dependency-free contract module lets storage and lifecycle registries
6
+ * depend only on declared shapes, versions, and limits.
7
+ */
8
+ export const ANALYST_REVIEW_STATE_SCHEMA = 'whoisleuth.analyst-review-state';
9
+ export const ANALYST_REVIEW_STATE_SCHEMA_VERSION = 1;
10
+ export const ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS = Object.freeze([ANALYST_REVIEW_STATE_SCHEMA_VERSION]);
11
+ /** Internal IndexedDB revision; it is deliberately independent of the public document version. */
12
+ export const ANALYST_REVIEW_STATE_BROWSER_STORAGE_REVISION = 2;
13
+ export const MAX_ANALYST_REVIEW_STATE_RECORDS = 500;
14
+ export const MAX_ANALYST_REVIEW_STATE_BYTES = 512 * 1024;
15
+ export const MAX_ANALYST_REVIEW_RATIONALE_LENGTH = 1_000;
16
+ export const MAX_ANALYST_REVIEW_ASSOCIATIONS = 20;
17
+ export const MAX_ANALYST_REVIEW_HISTORY = 8;
18
+ export const MAX_ANALYST_REVIEW_ITEMS = 500;
19
+ export const MAX_ANALYST_REVIEW_IDENTITY_BYTES = 1024 * 1024;
20
+ export const ANALYST_REVIEW_KINDS = Object.freeze([
21
+ 'case',
22
+ 'case_action',
23
+ 'observed_effect_review',
24
+ 'evidence_gap',
25
+ 'watchlist_change',
26
+ 'bulk_session',
27
+ 'comparison',
28
+ 'suppression',
29
+ 'change_window',
30
+ 'desired_posture',
31
+ 'certificate',
32
+ 'incomplete_packet',
33
+ 'detection_rule',
34
+ 'orphaned_state',
35
+ ]);
36
+ export const ANALYST_REVIEW_EVIDENCE_FAMILIES = Object.freeze([
37
+ 'case',
38
+ 'registration',
39
+ 'dns',
40
+ 'routing',
41
+ 'http',
42
+ 'certificate_transparency',
43
+ 'live_tls',
44
+ 'caa',
45
+ 'certificate_identity',
46
+ 'comparison',
47
+ 'desired_posture',
48
+ 'suppression',
49
+ 'change_window',
50
+ 'relationship',
51
+ 'bulk',
52
+ 'rule',
53
+ 'packet',
54
+ ]);
55
+ export const ANALYST_REVIEW_DISPOSITIONS = Object.freeze(['open', 'expected', 'suppressed', 'resolved']);
56
+ export const ANALYST_REVIEW_DISPOSITION_OPTIONS = Object.freeze([
57
+ { value: 'open', label: 'Open' },
58
+ { value: 'expected', label: 'Expected' },
59
+ { value: 'suppressed', label: 'Suppressed' },
60
+ { value: 'resolved', label: 'Resolved' },
61
+ ]);
@@ -0,0 +1,50 @@
1
+ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
+ import { buildExtractedLifecycleFamilyV2 } from "./extracted-domain-lifecycle.mjs";
3
+ import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
4
+ import { ANALYST_REVIEW_STATE_SCHEMA, ANALYST_REVIEW_STATE_SCHEMA_VERSION, ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS, MAX_ANALYST_REVIEW_STATE_BYTES, } from "./analyst-review-state-contract.mjs";
5
+ export * from "./analyst-review-state-contract.mjs";
6
+ export const ANALYST_REVIEW_STATE_CONTRACT_OWNER = 'packages/contracts/analyst-review-state.mts';
7
+ export const ANALYST_REVIEW_STATE_COMPATIBILITY = defineSchemaCompatibility({
8
+ id: 'browser.analyst-review-state',
9
+ kind: 'browser_store',
10
+ schema: ANALYST_REVIEW_STATE_SCHEMA,
11
+ currentVersion: ANALYST_REVIEW_STATE_SCHEMA_VERSION,
12
+ supportedVersions: ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS,
13
+ acceptsUnversionedLegacy: false,
14
+ futureVersionBehavior: 'preserve_without_write',
15
+ migration: 'exact_current_only',
16
+ writeSemantics: 'optimistic_replace',
17
+ byteBudget: MAX_ANALYST_REVIEW_STATE_BYTES,
18
+ owner: ANALYST_REVIEW_STATE_CONTRACT_OWNER,
19
+ note: 'Bounded analyst-authored lifecycle overlay. Material evidence changes, expiry, and incomplete evidence reopen review without rewriting source evidence or losing earlier rationale.',
20
+ });
21
+ export const ANALYST_REVIEW_STATE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamilyV2({
22
+ id: 'analyst-review-state',
23
+ owner: ANALYST_REVIEW_STATE_CONTRACT_OWNER,
24
+ serializerModule: 'packages/monitoring/analyst-review-state.mts',
25
+ serializerExportName: 'serialiseAnalystReviewStateJson',
26
+ plane: 'browser',
27
+ projection: 'browser_export',
28
+ retention: 'operator_controlled_output',
29
+ includedCategories: ['subject-identity', 'material-fingerprint', 'analyst-disposition', 'rationale', 'review-times', 'case-and-campaign-references'],
30
+ excludedCategories: ['raw-upstream-responses', 'expanded-contacts', 'complete-query-urls', 'credentials', 'cookies', 'certificate-transparency-history'],
31
+ formats: [{
32
+ descriptor: ANALYST_REVIEW_STATE_COMPATIBILITY,
33
+ lifecycleSchema: ANALYST_REVIEW_STATE_SCHEMA,
34
+ requiredKeys: ['schema', 'version', 'records'],
35
+ optionalKeys: [],
36
+ hook: {
37
+ module: 'packages/monitoring/analyst-review-state.mts',
38
+ exportName: 'normalizeAnalystReviewStateStore',
39
+ role: 'normaliser',
40
+ runtime: 'shared',
41
+ },
42
+ fixtures: [{
43
+ id: 'analyst-review-state-v1',
44
+ path: 'test/fixtures/extracted-domain-lifecycle/analyst-review-state-v1.json',
45
+ bytes: 663,
46
+ sha256: 'af0b191a79a50ef28f82975d181bdd3f7ba0bf7245cf4fc2b3adb84603d8dae6',
47
+ version: ANALYST_REVIEW_STATE_SCHEMA_VERSION,
48
+ }],
49
+ }],
50
+ }));