@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

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 (236) 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/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /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
+ }));