@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,7 +1,8 @@
1
- import { canonicalArtifactJson, canonicalArtifactJsonV2, sha256ArtifactDigest, sha256ArtifactDigestV2, SORTED_JSON_V2, } from "./artifact-integrity.js";
2
- export const INVESTIGATION_CAPSULE_SCHEMA = 'whoisleuth.investigation-capsule';
3
- export const INVESTIGATION_CAPSULE_VERSION = 2;
4
- export const LEGACY_INVESTIGATION_CAPSULE_VERSION = 1;
1
+ import { canonicalArtifactJsonV2, sha256ArtifactDigestV2, SORTED_JSON_V2, } from "../evidence/artifact-integrity.mjs";
2
+ import { LOOKUP_ASSET_GRAPH_SCHEMA, } from "./lookup-asset-graph.mjs";
3
+ import { LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, } from "./lookup-investigation-brief.mjs";
4
+ import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, } from "../contracts/investigation-portability.mjs";
5
+ export { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, };
5
6
  function safeVersion(value) {
6
7
  return typeof value === 'string' && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value)
7
8
  ? value
@@ -62,10 +63,16 @@ export async function buildInvestigationCapsule(input) {
62
63
  target: { value: input.brief.target, type: input.brief.targetType },
63
64
  sourceContracts: [
64
65
  { id: 'lookup-evidence', schema: evidenceSchema, version: evidenceVersion, digest: evidenceDigest, embedded: false },
65
- { id: 'investigation-brief', schema: input.brief.schema, version: input.brief.schemaVersion, digest: briefDigest, embedded: true },
66
- { id: 'asset-graph', schema: 'whoisleuth.lookup-asset-graph', version: input.graph.version, digest: graphDigest, embedded: true },
66
+ { id: 'investigation-brief', schema: LOOKUP_INVESTIGATION_BRIEF_SCHEMA, version: LOOKUP_INVESTIGATION_BRIEF_VERSION, digest: briefDigest, embedded: true },
67
+ { id: 'asset-graph', schema: LOOKUP_ASSET_GRAPH_SCHEMA, version: input.graph.version, digest: graphDigest, embedded: true },
67
68
  ...(analystRecords && analystRecordsDigest
68
- ? [{ id: 'analyst-records', schema: 'whoisleuth.case-analyst-records', version: 1, digest: analystRecordsDigest, embedded: true }]
69
+ ? [{
70
+ id: 'analyst-records',
71
+ schema: INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA,
72
+ version: INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION,
73
+ digest: analystRecordsDigest,
74
+ embedded: true,
75
+ }]
69
76
  : []),
70
77
  ],
71
78
  investigationBrief: input.brief,
@@ -74,7 +81,7 @@ export async function buildInvestigationCapsule(input) {
74
81
  limitations: [
75
82
  'The Lookup evidence file is linked by digest but is not embedded; keep or share that exact file separately when verification is required.',
76
83
  'Digest verification can detect changed content but does not establish who created the capsule. No digital signature is applied.',
77
- 'The graph and brief are bounded projections of collected and derived evidence, not attribution, ownership, safety, availability, or maliciousness conclusions.',
84
+ 'The graph and canonical Decision Fact brief are bounded projections of collected and derived evidence, not attribution, ownership, safety, availability, or maliciousness conclusions.',
78
85
  analystRecords
79
86
  ? 'Analyst decisions and assertions were deliberately included; review them for sensitive or personal information before sharing.'
80
87
  : 'Analyst decisions, assertions, notes, contacts, actions, and raw source payloads are excluded.',
@@ -95,21 +102,17 @@ export async function buildInvestigationCapsule(input) {
95
102
  }
96
103
  export async function verifyInvestigationCapsule(capsule) {
97
104
  const current = capsule.schemaVersion === INVESTIGATION_CAPSULE_VERSION;
98
- const legacy = capsule.schemaVersion === LEGACY_INVESTIGATION_CAPSULE_VERSION;
105
+ const publicVersion = capsule.schemaVersion === PUBLIC_INVESTIGATION_CAPSULE_VERSION;
99
106
  const integrityRecord = capsule.integrity;
100
- if ((!current && !legacy)
101
- || (current && (integrityRecord.canonicalization !== SORTED_JSON_V2
102
- || integrityRecord.scope !== 'capsule excluding integrity'))
103
- || (legacy && (Object.hasOwn(integrityRecord, 'canonicalization')
104
- || Object.hasOwn(integrityRecord, 'scope')
105
- || Object.hasOwn(integrityRecord, 'digestSha256')))) {
106
- return { valid: false, brief: false, graph: false, analystRecords: null, whole: current ? false : null };
107
+ if ((!current && !publicVersion)
108
+ || integrityRecord.canonicalization !== SORTED_JSON_V2
109
+ || integrityRecord.scope !== 'capsule excluding integrity') {
110
+ return { valid: false, brief: false, graph: false, analystRecords: null, whole: false };
107
111
  }
108
- const projectionDigest = current ? sha256ArtifactDigestV2 : sha256ArtifactDigest;
109
112
  const [briefDigest, graphDigest, analystRecordsDigest] = await Promise.all([
110
- projectionDigest(capsule.investigationBrief),
111
- projectionDigest(capsule.graphSnapshot),
112
- capsule.analystRecords ? projectionDigest(capsule.analystRecords) : Promise.resolve(null),
113
+ sha256ArtifactDigestV2(capsule.investigationBrief),
114
+ sha256ArtifactDigestV2(capsule.graphSnapshot),
115
+ capsule.analystRecords ? sha256ArtifactDigestV2(capsule.analystRecords) : Promise.resolve(null),
113
116
  ]);
114
117
  const brief = briefDigest === capsule.integrity.briefDigest;
115
118
  const graph = graphDigest === capsule.integrity.graphDigest;
@@ -118,22 +121,17 @@ export async function verifyInvestigationCapsule(capsule) {
118
121
  : capsule.integrity.analystRecordsDigest === null
119
122
  ? null
120
123
  : false;
121
- let whole = null;
122
- if (current) {
123
- const { integrity, ...unsigned } = capsule;
124
- whole = integrity.algorithm === 'SHA-256'
125
- && integrity.canonicalization === SORTED_JSON_V2
126
- && integrity.scope === 'capsule excluding integrity'
127
- && integrity.digestSha256 === await sha256ArtifactDigestV2(unsigned);
128
- }
129
- return { valid: brief && graph && analystRecords !== false && whole !== false, brief, graph, analystRecords, whole };
124
+ const { integrity: _integrity, ...unsigned } = capsule;
125
+ const whole = integrityRecord.algorithm === 'SHA-256'
126
+ && integrityRecord.canonicalization === SORTED_JSON_V2
127
+ && integrityRecord.scope === 'capsule excluding integrity'
128
+ && integrityRecord.digestSha256 === await sha256ArtifactDigestV2(unsigned);
129
+ return { valid: brief && graph && analystRecords !== false && whole, brief, graph, analystRecords, whole };
130
130
  }
131
131
  export function investigationCapsuleFilename(capsule) {
132
132
  const target = capsule.target.value.toLowerCase().replace(/[^a-z0-9.-]+/gu, '-').replace(/^-|-$/gu, '').slice(0, 80) || 'lookup';
133
133
  return `whoisleuth-investigation-capsule-${target}-${capsule.generatedAt.slice(0, 10)}.json`;
134
134
  }
135
135
  export function serializeInvestigationCapsule(capsule) {
136
- return `${capsule.schemaVersion === INVESTIGATION_CAPSULE_VERSION
137
- ? canonicalArtifactJsonV2(capsule)
138
- : canonicalArtifactJson(capsule)}\n`;
136
+ return `${canonicalArtifactJsonV2(capsule)}\n`;
139
137
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,5 @@
1
+ import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../contracts/investigation-portability.mjs";
2
+ export { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION };
1
3
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
2
4
  const MAX_NODES = 72;
3
5
  const MAX_EDGES = 120;
@@ -158,7 +160,7 @@ export function buildLookupAssetGraph(input) {
158
160
  const target = hostname(input.target);
159
161
  if (!target) {
160
162
  return {
161
- version: 2,
163
+ version: LOOKUP_ASSET_GRAPH_VERSION,
162
164
  targetId: '',
163
165
  nodes: [],
164
166
  edges: [],
@@ -705,7 +707,7 @@ export function buildLookupAssetGraph(input) {
705
707
  }
706
708
  }
707
709
  return {
708
- version: 2,
710
+ version: LOOKUP_ASSET_GRAPH_VERSION,
709
711
  targetId,
710
712
  nodes: [...nodes.values()],
711
713
  edges: [...edges.values()],
@@ -1,11 +1,7 @@
1
- import { LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "./lookup-claim-readiness.js";
2
- import { SORTED_JSON_V2, sha256ArtifactDigestV2, } from "./artifact-integrity.js";
3
- export const LOOKUP_CLAIM_PASSPORT_SCHEMA = 'whoisleuth.lookup-claim-passport';
4
- export const LOOKUP_CLAIM_PASSPORT_VERSION = 1;
5
- export const MAX_LOOKUP_CLAIM_PASSPORT_BYTES = 64 * 1024;
6
- export const MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS = 16;
7
- export const MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS = 16;
8
- export const LOOKUP_CLAIM_PASSPORT_TARGET_TYPES = ['domain', 'ipv4', 'ipv6', 'asn'];
1
+ import { LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "./lookup-claim-readiness.mjs";
2
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2, } from "../evidence/artifact-integrity.mjs";
3
+ import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_BYTES, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../contracts/investigation-portability.mjs";
4
+ export { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_BYTES, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, };
9
5
  const SOURCE_STATES = new Set([
10
6
  'complete',
11
7
  'not_found',
@@ -118,10 +114,10 @@ function requirementId(value) {
118
114
  }
119
115
  export async function buildLookupClaimPassport(input) {
120
116
  if (input.readiness.version !== LOOKUP_CLAIM_READINESS_VERSION) {
121
- throw new TypeError('The claim-readiness contract is not supported for passport export.');
117
+ throw new TypeError('The Evidence Readiness contract is not supported for passport export.');
122
118
  }
123
119
  if (input.readiness.entries.length > LOOKUP_CLAIM_IDS.length) {
124
- throw new TypeError('The claim-readiness contract contains too many entries for passport export.');
120
+ throw new TypeError('The Evidence Readiness contract contains too many entries for passport export.');
125
121
  }
126
122
  const rawClaimId = exactText(input.claimId, 80);
127
123
  if (!CLAIM_IDS.has(rawClaimId))
@@ -1,4 +1,5 @@
1
- export const LOOKUP_CLAIM_READINESS_VERSION = 2;
1
+ import { LOOKUP_CLAIM_READINESS_VERSION } from "../contracts/investigation-portability.mjs";
2
+ export { LOOKUP_CLAIM_READINESS_VERSION };
2
3
  export const LOOKUP_CLAIM_IDS = [
3
4
  'registration-state',
4
5
  'current-web-observation',
@@ -89,6 +90,7 @@ function requirement(id, coverage, override) {
89
90
  const definition = LOOKUP_CLAIM_REQUIREMENTS[id];
90
91
  const evidence = definition.evidenceId ? coverage.get(definition.evidenceId) : undefined;
91
92
  const state = override?.state ?? evidence?.state ?? 'unknown';
93
+ const coverageState = definition.evidenceId ? evidence?.state ?? 'unknown' : null;
92
94
  const limitations = override?.limitation
93
95
  ? [override.limitation]
94
96
  : evidence?.limitations.length
@@ -96,7 +98,12 @@ function requirement(id, coverage, override) {
96
98
  : state === 'complete'
97
99
  ? []
98
100
  : [evidence?.statusLabel || 'Not supplied'];
99
- return Object.freeze({ ...definition, state, limitations: Object.freeze(limitations) });
101
+ return Object.freeze({
102
+ ...definition,
103
+ coverageState,
104
+ state,
105
+ limitations: Object.freeze(limitations),
106
+ });
100
107
  }
101
108
  function readinessEntry(input) {
102
109
  const evaluated = evaluateRequirements(input.requirements);
@@ -0,0 +1,172 @@
1
+ import { DECISION_FACT_PRESENTATION_LABELS, projectDecisionFacts, } from "../evidence/decision-fact.mjs";
2
+ import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, SUPPORTED_LOOKUP_INVESTIGATION_BRIEF_VERSIONS, } from "../contracts/investigation-portability.mjs";
3
+ export { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, SUPPORTED_LOOKUP_INVESTIGATION_BRIEF_VERSIONS, };
4
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
5
+ const MAX_LIMITATIONS = 20;
6
+ function text(value, maximum = 320) {
7
+ return String(value ?? '')
8
+ .replace(CONTROL_CHARACTERS, ' ')
9
+ .replace(/\s+/gu, ' ')
10
+ .trim()
11
+ .slice(0, maximum);
12
+ }
13
+ function timestamp(value) {
14
+ if (typeof value !== 'string')
15
+ return null;
16
+ const parsed = Date.parse(value);
17
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
18
+ }
19
+ function uniqueText(values, maximum = MAX_LIMITATIONS) {
20
+ const output = [];
21
+ const seen = new Set();
22
+ for (const value of values) {
23
+ const normalized = text(value);
24
+ if (!normalized || seen.has(normalized))
25
+ continue;
26
+ output.push(normalized);
27
+ seen.add(normalized);
28
+ if (output.length >= maximum)
29
+ break;
30
+ }
31
+ return output;
32
+ }
33
+ function taskQuestion(support) {
34
+ return support.guidance.questions[0]
35
+ ?? 'What does the collected evidence establish, and what remains uncertain?';
36
+ }
37
+ export function buildLookupInvestigationBrief(input) {
38
+ const generatedAt = timestamp(input.generatedAt) ?? new Date().toISOString();
39
+ const decisionFacts = projectDecisionFacts(input.decisionFacts);
40
+ const limitedSources = input.quality.entries
41
+ .filter((entry) => entry.state !== 'complete')
42
+ .map((entry) => `${entry.label}: ${entry.statusLabel}`);
43
+ const graphKinds = uniqueText(input.graph.edges.map((edge) => edge.label), 12);
44
+ const brief = Object.freeze({
45
+ schema: LOOKUP_INVESTIGATION_BRIEF_SCHEMA,
46
+ schemaVersion: LOOKUP_INVESTIGATION_BRIEF_VERSION,
47
+ generatedAt,
48
+ target: text(input.target, 253) || 'Unknown target',
49
+ targetType: text(input.targetType, 40) || 'unknown',
50
+ task: input.task,
51
+ taskLabel: input.decisionSupport.guidance.label,
52
+ question: taskQuestion(input.decisionSupport),
53
+ summary: input.decisionSupport.guidance.summary,
54
+ observation: Object.freeze({
55
+ observedAt: input.quality.observedAt,
56
+ evidenceAgeDays: input.quality.ageDays,
57
+ completeSources: input.quality.completeCount,
58
+ limitedSources: input.quality.limitedCount,
59
+ freshnessPolicy: input.quality.freshnessPolicy,
60
+ }),
61
+ decisionFacts,
62
+ relationships: Object.freeze({
63
+ nodes: input.graph.nodes.length,
64
+ edges: input.graph.edges.length,
65
+ truncated: input.graph.truncated,
66
+ kinds: Object.freeze(graphKinds),
67
+ }),
68
+ limitations: Object.freeze(uniqueText([
69
+ ...limitedSources,
70
+ ...input.graph.limitations,
71
+ 'This brief is a deterministic organisation of collected and derived evidence, not an attribution, ownership, safety, availability, or maliciousness conclusion.',
72
+ 'Analyst assertions, hypotheses, and decisions must remain separate from observed evidence.',
73
+ ])),
74
+ });
75
+ if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
76
+ throw new RangeError('Lookup investigation brief exceeded its byte limit.');
77
+ }
78
+ return brief;
79
+ }
80
+ function markdown(value) {
81
+ return text(value, 2_000)
82
+ .replace(/&/gu, '&')
83
+ .replace(/</gu, '&lt;')
84
+ .replace(/>/gu, '&gt;')
85
+ .replace(/([\\`*_{}[\]()#+\-.!|=~])/gu, '\\$1');
86
+ }
87
+ function countedValues(collection) {
88
+ const values = collection.items.length ? collection.items.map(markdown).join(', ') : 'none';
89
+ return `${collection.displayed} of ${collection.total} displayed; ${collection.omitted} omitted. ${values}`;
90
+ }
91
+ function decisionFactLines(fact) {
92
+ const lines = [
93
+ `### ${markdown(fact.question)}`,
94
+ '',
95
+ `- **Fact ID:** ${markdown(fact.id)}`,
96
+ `- **Conclusion:** ${markdown(fact.conclusion)}`,
97
+ `- **Importance:** ${markdown(DECISION_FACT_PRESENTATION_LABELS.importance[fact.importance])}`,
98
+ `- **Evidence state:** ${markdown(DECISION_FACT_PRESENTATION_LABELS.evidenceState[fact.evidenceState])} (${markdown(fact.evidenceState)})`,
99
+ `- **Completeness:** ${markdown(DECISION_FACT_PRESENTATION_LABELS.completeness[fact.completeness])} (${markdown(fact.completeness)})`,
100
+ `- **Freshness:** ${markdown(DECISION_FACT_PRESENTATION_LABELS.freshness[fact.freshness])} (${markdown(fact.freshness)})`,
101
+ `- **Consistency:** ${markdown(DECISION_FACT_PRESENTATION_LABELS.consistency[fact.consistency])} (${markdown(fact.consistency)})`,
102
+ `- **Dependencies:** ${countedValues(fact.dependencies)}`,
103
+ `- **Source references:** ${countedValues(fact.sourceReferences)}`,
104
+ `- **Attributed sources:** ${fact.sources.displayed} of ${fact.sources.total} displayed; ${fact.sources.omitted} omitted.`,
105
+ ];
106
+ if (fact.sources.items.length) {
107
+ for (const source of fact.sources.items) {
108
+ lines.push(` - **${markdown(source.label)}** (${markdown(source.id)}): ${markdown(DECISION_FACT_PRESENTATION_LABELS.provenance[source.provenance])}; ${markdown(DECISION_FACT_PRESENTATION_LABELS.evidenceState[source.evidenceState])}; observed ${markdown(source.observedAt ?? 'not reported')}.`, ` - References: ${countedValues(source.references)}`, ` - Limitations: ${countedValues(source.limitations)}`);
109
+ }
110
+ }
111
+ else {
112
+ lines.push(' - No attributed source was retained for this fact.');
113
+ }
114
+ lines.push(`- **Contradictions:** ${countedValues(fact.contradictions)}`, `- **Limitations:** ${countedValues(fact.limitations)}`, `- **Safe next actions:** ${fact.safeNextActions.displayed} of ${fact.safeNextActions.total} displayed; ${fact.safeNextActions.omitted} omitted.`);
115
+ if (fact.safeNextActions.items.length) {
116
+ for (const action of fact.safeNextActions.items) {
117
+ lines.push(` - **${markdown(action.label)}** (${markdown(action.id)}; ${markdown(action.importance)}): ${markdown(action.reason)} Expected outcome: ${markdown(action.expectedOutcome)} Review destination: ${markdown(action.href)}.`);
118
+ }
119
+ }
120
+ else {
121
+ lines.push(' - No fact-specific action was retained. Review the attributed evidence and limitations.');
122
+ }
123
+ lines.push('');
124
+ return lines;
125
+ }
126
+ export function formatLookupInvestigationBriefMarkdown(brief) {
127
+ const lines = [
128
+ `# Investigation brief: ${markdown(brief.target)}`,
129
+ '',
130
+ `Generated ${markdown(brief.generatedAt)} from a ${markdown(brief.targetType)} Lookup.`,
131
+ '',
132
+ '## Decision context',
133
+ '',
134
+ `- **Task:** ${markdown(brief.taskLabel)}`,
135
+ `- **Question:** ${markdown(brief.question)}`,
136
+ `- **Summary:** ${markdown(brief.summary)}`,
137
+ `- **Evidence observed:** ${markdown(brief.observation.observedAt ?? 'not reported')}`,
138
+ `- **Source quality:** ${brief.observation.completeSources} complete; ${brief.observation.limitedSources} limited`,
139
+ `- **Freshness policy:** v${brief.observation.freshnessPolicy.version} ${markdown(brief.observation.freshnessPolicy.id)} for ${markdown(brief.observation.freshnessPolicy.task)}; registration ${brief.observation.freshnessPolicy.thresholdsDays.registration}d, network ${brief.observation.freshnessPolicy.thresholdsDays.network}d, web ${brief.observation.freshnessPolicy.thresholdsDays.web}d`,
140
+ '',
141
+ '## Canonical Decision Facts',
142
+ '',
143
+ `Displaying ${brief.decisionFacts.displayed} of ${brief.decisionFacts.total} canonical Decision Facts; ${brief.decisionFacts.omitted} omitted by the bounded projection. ${brief.decisionFacts.contradictory} contradictory and ${brief.decisionFacts.unresolved} unresolved across the complete canonical set.`,
144
+ '',
145
+ ...(brief.decisionFacts.facts.length
146
+ ? brief.decisionFacts.facts.flatMap(decisionFactLines)
147
+ : ['No canonical Decision Fact was available. Review source quality before drawing a conclusion.', '']),
148
+ '',
149
+ '## Observed relationship map',
150
+ '',
151
+ `- ${brief.relationships.nodes} nodes and ${brief.relationships.edges} edges${brief.relationships.truncated ? '; bounded output was truncated' : ''}.`,
152
+ `- Relationship types: ${brief.relationships.kinds.map(markdown).join(', ') || 'none observed'}.`,
153
+ '',
154
+ '## Limitations',
155
+ '',
156
+ ...brief.limitations.map((limitation) => `- ${markdown(limitation)}`),
157
+ '',
158
+ ];
159
+ const output = lines.join('\n');
160
+ if (new TextEncoder().encode(output).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
161
+ throw new RangeError('Lookup investigation brief Markdown exceeded its byte limit.');
162
+ }
163
+ return output;
164
+ }
165
+ export function lookupInvestigationBriefFilename(brief) {
166
+ const target = brief.target
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9.-]+/gu, '-')
169
+ .replace(/^-+|-+$/gu, '')
170
+ .slice(0, 80) || 'lookup';
171
+ return `whoisleuth-investigation-brief-${target}-${brief.generatedAt.slice(0, 10)}.md`;
172
+ }