@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,11 +1,15 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash, createPrivateKey, createPublicKey, sign, verify, } from 'node:crypto';
3
- import { canonicalArtifactJson } from "../frontend/src/lib/analysis/artifact-integrity.js";
4
- import { MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
3
+ import { canonicalArtifactJsonFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
4
+ import { hasVerifiedApplicableIntegrity, hasVerifiedArtifactStructure, MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
5
+ import { parseBoundedJsonObject } from "./bounded-json.mjs";
6
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
7
  export const SIGNED_EVIDENCE_PACKAGE_SCHEMA = 'whoisleuth.signed-evidence-package';
6
- export const SIGNED_EVIDENCE_PACKAGE_VERSION = 1;
8
+ export const SIGNED_EVIDENCE_PACKAGE_VERSION = 2;
9
+ export const EVIDENCE_SIGNATURE_VERIFICATION_SCHEMA = 'whoisleuth.evidence-signature-verification';
10
+ export const EVIDENCE_SIGNATURE_VERIFICATION_VERSION = 2;
7
11
  export const EVIDENCE_SIGNATURE_ALGORITHM = 'Ed25519';
8
- export const EVIDENCE_SIGNATURE_CANONICALIZATION = 'sorted-json-v1';
12
+ export const EVIDENCE_SIGNATURE_CANONICALIZATION = SORTED_JSON_V2;
9
13
  export const MAX_SIGNING_KEY_FILE_BYTES = 16 * 1024;
10
14
  const SHA256_RE = /^[a-f0-9]{64}$/u;
11
15
  const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/u;
@@ -23,36 +27,23 @@ function record(value) {
23
27
  : null;
24
28
  }
25
29
  function parseObject(raw, label) {
26
- if (typeof raw !== 'string')
27
- throw new TypeError(`${label} must be UTF-8 JSON text.`);
28
- const bytes = Buffer.byteLength(raw, 'utf8');
29
- if (bytes === 0 || bytes > MAX_OFFLINE_ARTIFACT_BYTES) {
30
- throw new TypeError(`${label} must be between 1 byte and ${MAX_OFFLINE_ARTIFACT_BYTES} bytes.`);
31
- }
32
- let parsed;
33
- try {
34
- parsed = JSON.parse(raw);
35
- }
36
- catch {
37
- throw new TypeError(`${label} must be valid JSON.`);
38
- }
39
- const value = record(parsed);
40
- if (!value)
41
- throw new TypeError(`${label} must contain one JSON object.`);
42
- return value;
30
+ return parseBoundedJsonObject(raw, { label, maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
43
31
  }
44
32
  function hasExactKeys(value, expected) {
45
33
  const keys = Object.keys(value);
46
34
  return keys.length === expected.size && keys.every((key) => expected.has(key));
47
35
  }
48
36
  function timestamp(value) {
49
- if (typeof value !== 'string' || value.length > 64) {
50
- throw new TypeError('Signature time must be a valid timestamp.');
51
- }
52
- const parsed = Date.parse(value);
53
- if (!Number.isFinite(parsed))
54
- throw new TypeError('Signature time must be a valid timestamp.');
55
- return new Date(parsed).toISOString();
37
+ const normalized = normalizeExplicitIsoTimestamp(value);
38
+ if (!normalized)
39
+ throw new TypeError('Signature time must be a valid timestamp with an explicit timezone.');
40
+ return normalized;
41
+ }
42
+ function canonicalTimestamp(value) {
43
+ const normalized = timestamp(value);
44
+ if (value !== normalized)
45
+ throw new TypeError('Signature time must use canonical UTC timestamp text.');
46
+ return normalized;
56
47
  }
57
48
  function importPrivateKey(pem) {
58
49
  if (Buffer.byteLength(pem, 'utf8') > MAX_SIGNING_KEY_FILE_BYTES) {
@@ -96,8 +87,8 @@ function spkiDer(key) {
96
87
  function keyId(publicDer) {
97
88
  return createHash('sha256').update(publicDer).digest('hex');
98
89
  }
99
- function payloadBytes(payload) {
100
- return Buffer.from(canonicalArtifactJson(payload), 'utf8');
90
+ function payloadBytes(payload, canonicalization) {
91
+ return Buffer.from(canonicalArtifactJsonFor(payload, canonicalization), 'utf8');
101
92
  }
102
93
  async function signableArtifact(raw) {
103
94
  const artifact = parseObject(raw, 'Evidence artefact');
@@ -105,6 +96,9 @@ async function signableArtifact(raw) {
105
96
  if (!['case_response_packet', 'investigation_capsule', 'signed_review_artifact'].includes(verification.artifact.kind)) {
106
97
  throw new TypeError('Only reviewed response packets, investigation capsules, and supported review manifests can be signed.');
107
98
  }
99
+ if (!hasVerifiedArtifactStructure(verification) || !hasVerifiedApplicableIntegrity(verification)) {
100
+ throw new TypeError('Evidence signing requires verified structure and a verified applicable integrity contract.');
101
+ }
108
102
  return { artifact, verification };
109
103
  }
110
104
  export async function signEvidencePackage(raw, privateKeyPem, signedAt = new Date().toISOString()) {
@@ -124,7 +118,7 @@ export async function signEvidencePackage(raw, privateKeyPem, signedAt = new Dat
124
118
  canonicalization: EVIDENCE_SIGNATURE_CANONICALIZATION,
125
119
  publicKeySpkiDerBase64: publicDer.toString('base64'),
126
120
  keyIdSha256: keyId(publicDer),
127
- valueBase64: sign(null, payloadBytes(payload), privateKey).toString('base64'),
121
+ valueBase64: sign(null, payloadBytes(payload, SORTED_JSON_V2), privateKey).toString('base64'),
128
122
  }),
129
123
  });
130
124
  }
@@ -139,7 +133,6 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
139
133
  || !signature
140
134
  || !hasExactKeys(signature, SIGNATURE_KEYS)
141
135
  || signature.algorithm !== EVIDENCE_SIGNATURE_ALGORITHM
142
- || signature.canonicalization !== EVIDENCE_SIGNATURE_CANONICALIZATION
143
136
  || typeof signature.publicKeySpkiDerBase64 !== 'string'
144
137
  || signature.publicKeySpkiDerBase64.length > 4096
145
138
  || !BASE64_RE.test(signature.publicKeySpkiDerBase64)
@@ -150,7 +143,10 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
150
143
  || !SHA256_RE.test(signature.keyIdSha256)) {
151
144
  throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
152
145
  }
153
- const signedAt = timestamp(value.signedAt);
146
+ if (signature.canonicalization !== EVIDENCE_SIGNATURE_CANONICALIZATION) {
147
+ throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
148
+ }
149
+ const signedAt = canonicalTimestamp(value.signedAt);
154
150
  const publicDer = Buffer.from(signature.publicKeySpkiDerBase64, 'base64');
155
151
  if (keyId(publicDer) !== signature.keyIdSha256) {
156
152
  throw new TypeError('Signed evidence package public-key identifier does not match its embedded key.');
@@ -164,10 +160,14 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
164
160
  };
165
161
  const signatureBytes = Buffer.from(signature.valueBase64, 'base64');
166
162
  if (signatureBytes.length !== 64
167
- || !verify(null, payloadBytes(payload), embeddedPublicKey, signatureBytes)) {
163
+ || !verify(null, payloadBytes(payload, EVIDENCE_SIGNATURE_CANONICALIZATION), embeddedPublicKey, signatureBytes)) {
168
164
  throw new TypeError('Signed evidence package failed Ed25519 verification.');
169
165
  }
170
- const artifactVerification = await verifyOfflineArtifact(JSON.stringify(artifact));
166
+ let artifactVerification = null;
167
+ try {
168
+ artifactVerification = await verifyOfflineArtifact(JSON.stringify(artifact));
169
+ }
170
+ catch { /* Signature validity remains independently reportable. */ }
171
171
  let publicKeyMatched = null;
172
172
  if (trustedPublicKeyPem) {
173
173
  const trustedDer = spkiDer(importPublicKey(trustedPublicKeyPem, 'Trusted public key file'));
@@ -176,24 +176,41 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
176
176
  throw new TypeError('Signed evidence package does not match the trusted public key.');
177
177
  }
178
178
  return Object.freeze({
179
- schema: 'whoisleuth.evidence-signature-verification',
180
- version: 1,
181
- valid: true,
179
+ schema: EVIDENCE_SIGNATURE_VERIFICATION_SCHEMA,
180
+ version: EVIDENCE_SIGNATURE_VERIFICATION_VERSION,
182
181
  state: 'signature_valid',
183
- signerTrust: trustedPublicKeyPem ? 'trusted_key' : 'embedded_key_only',
184
- signedAt,
185
- keyIdSha256: signature.keyIdSha256,
186
- publicKeyMatched,
182
+ signature: Object.freeze({
183
+ state: 'valid',
184
+ signerTrust: trustedPublicKeyPem ? 'trusted_key' : 'embedded_key_only',
185
+ signedAt,
186
+ keyIdSha256: signature.keyIdSha256,
187
+ publicKeyMatched,
188
+ }),
187
189
  artifact: Object.freeze({
188
- schema: artifactVerification.artifact.schema,
189
- version: artifactVerification.artifact.version,
190
- kind: artifactVerification.artifact.kind,
190
+ schema: artifactVerification?.artifact.schema
191
+ ?? (typeof artifact.schema === 'string' && /^[a-z0-9.-]{1,160}$/u.test(artifact.schema) ? artifact.schema : null),
192
+ version: artifactVerification?.artifact.version
193
+ ?? (Number.isSafeInteger(artifact.version ?? artifact.schemaVersion)
194
+ && Number(artifact.version ?? artifact.schemaVersion) >= 1
195
+ && Number(artifact.version ?? artifact.schemaVersion) <= 1_000
196
+ ? Number(artifact.version ?? artifact.schemaVersion)
197
+ : null),
198
+ kind: artifactVerification?.artifact.kind ?? 'unverified_artifact',
199
+ assurance: Object.freeze({
200
+ state: artifactVerification?.state ?? 'not_verified',
201
+ structure: artifactVerification?.checks.structure ?? 'not_checked',
202
+ contentIntegrity: artifactVerification?.checks.contentIntegrity ?? 'not_checked',
203
+ contentIntegrityScope: artifactVerification?.checks.contentIntegrityScope ?? 'not_checked',
204
+ }),
191
205
  }),
192
206
  limitations: Object.freeze([
193
207
  trustedPublicKeyPem
194
208
  ? 'The signature and supplied public key match; recipient trust still depends on obtaining that public key through an authenticated channel.'
195
209
  : 'The signature is internally valid for the embedded public key, but signer identity was not authenticated because no trusted public key was supplied.',
196
210
  'The signature authenticates the canonical sorted-JSON content, not the original whitespace, indentation, or object-key order of the serialised file.',
211
+ ...(artifactVerification
212
+ ? [`Embedded-artifact assurance is reported separately as ${artifactVerification.state}; it does not change the cryptographic signature result.`]
213
+ : ['The embedded artefact did not pass a supported local assurance contract. The cryptographic signature remains valid for its bytes, but no structure or inner-integrity claim is made.']),
197
214
  'A valid signature proves the canonical package content has not changed since signing; it does not establish that the retained observations or analyst statements are accurate.',
198
215
  'WHOISleuth does not generate, store, recover, rotate, or publish signing keys.',
199
216
  ]),
@@ -203,11 +220,12 @@ export function formatEvidenceSignatureVerification(report) {
203
220
  const lines = [
204
221
  'WHOISleuth evidence signature verification',
205
222
  `State: ${report.state}`,
206
- `Signer trust: ${report.signerTrust}`,
207
- `Artifact: ${report.artifact.kind} · ${report.artifact.schema} v${report.artifact.version}`,
208
- `Signed: ${report.signedAt}`,
209
- `Key ID: sha256:${report.keyIdSha256}`,
210
- `Trusted public key: ${report.publicKeyMatched === null ? 'not supplied' : 'matched'}`,
223
+ `Signer trust: ${report.signature.signerTrust}`,
224
+ `Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'unrecognised'} v${report.artifact.version ?? 'unrecognised'}`,
225
+ `Artifact assurance: ${report.artifact.assurance.state}`,
226
+ `Signed: ${report.signature.signedAt}`,
227
+ `Key ID: sha256:${report.signature.keyIdSha256}`,
228
+ `Trusted public key: ${report.signature.publicKeyMatched === null ? 'not supplied' : 'matched'}`,
211
229
  ];
212
230
  for (const limitation of report.limitations)
213
231
  lines.push(`Limitation: ${limitation}`);
@@ -1,8 +1,9 @@
1
- import { createRequire } from 'node:module';
2
1
  import { projectCliLookupComparisonInput } from "./compare.mjs";
3
2
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
4
- const require = createRequire(import.meta.url);
5
- const { version: APPLICATION_VERSION } = require('../package.json');
3
+ import { validateLookupEvidenceArtifactStructure } from "./artifact-structure.mjs";
4
+ import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
5
+ import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, serializeLookupEvidence, } from "../lib/evidence-export.mjs";
6
+ const APPLICATION_VERSION = WHOISLEUTH_APPLICATION_VERSION;
6
7
  function objectOrNull(value) {
7
8
  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
8
9
  }
@@ -27,9 +28,15 @@ function buildCliEvidenceExport(text, evidenceModule, generatedAt = new Date().t
27
28
  || result.schemaVersion !== dependency.LOOKUP_EVIDENCE_SCHEMA_VERSION) {
28
29
  throw new TypeError('Lookup evidence builder returned an unsupported report contract.');
29
30
  }
31
+ validateLookupEvidenceArtifactStructure(result);
32
+ serializeLookupEvidence(result);
30
33
  return result;
31
34
  }
32
35
  function formatCliEvidenceExport(document, compact = false) {
33
- return `${JSON.stringify(document, null, compact ? 0 : 2)}\n`;
36
+ const output = `${serializeLookupEvidence(document, !compact)}\n`;
37
+ if (new TextEncoder().encode(output).byteLength > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
38
+ throw new TypeError('Lookup evidence exceeds the 5 MiB portable file limit.');
39
+ }
40
+ return output;
34
41
  }
35
42
  export { APPLICATION_VERSION, buildCliEvidenceExport, formatCliEvidenceExport };
@@ -1,6 +1,6 @@
1
1
  import { domainToASCII } from 'node:url';
2
2
  import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
- import { EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../frontend/src/lib/analysis/external-findings-import.js";
3
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../packages/interchange/external-findings-import.mjs";
4
4
  export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
5
5
  export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
6
6
  export const MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES = 4 * 1024 * 1024;
@@ -135,8 +135,8 @@ export function mapExternalObservations(inputRaw) {
135
135
  }
136
136
  const truncated = selected.length < seen.size;
137
137
  const document = {
138
- schema: 'whoisleuth.external-findings',
139
- schemaVersion: 3,
138
+ schema: EXTERNAL_FINDINGS_SCHEMA,
139
+ schemaVersion: EXTERNAL_FINDINGS_VERSION,
140
140
  source,
141
141
  findings: selected.map((finding) => ({
142
142
  ...finding,
@@ -26,8 +26,8 @@ function renderRegistrarComparison(comparison) {
26
26
  : '';
27
27
  return `<div class="table-scroll"><table><caption>Normalized registry and registrar RDAP publication comparison</caption><thead><tr><th scope="col">Field</th><th scope="col">Result</th><th scope="col">Registry RDAP</th><th scope="col">Registrar RDAP</th></tr></thead><tbody>${rows}</tbody></table></div>${omitted}`;
28
28
  }
29
- function formatLookupEvidenceHtml(document) {
30
- const report = buildLookupEvidenceReport(document);
29
+ function formatLookupEvidenceHtml(document, options = {}) {
30
+ const report = buildLookupEvidenceReport(document, options);
31
31
  const title = `Lookup evidence report: ${report.title}`;
32
32
  return `<!doctype html>
33
33
  <html lang="en">
@@ -60,7 +60,7 @@ function formatLookupEvidenceHtml(document) {
60
60
  <section class="block"><h2>Network evidence</h2>${renderGroups(report.networkGroups)}</section>
61
61
  <section class="block"><h2>Collection diagnostics</h2>${renderFields(report.diagnostics)}</section>
62
62
  <section class="block"><h2>Limitations</h2><ul>${report.limitations.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></section>
63
- <footer>Offline, self-contained report. No scripts, forms, external resources, or active links are included.</footer>
63
+ <footer>Offline, self-contained report. No scripts, forms, external resources, or active links are included.${report.attribution ? `<br>${escapeHtml(report.attribution)}` : ''}</footer>
64
64
  </main>
65
65
  </body>
66
66
  </html>
@@ -1,5 +1,6 @@
1
1
  import { bulkDnsSummary } from "../bulk-output.mjs";
2
- const CLI_LOOKUP_SCHEMA = 'whoisleuth.cli.lookup';
2
+ import { SAVED_LOOKUP_SCHEMA_VERSION, buildCliLookupDocument, } from "../saved-lookup.mjs";
3
+ import { CLI_LOOKUP_SCHEMA } from "../../packages/contracts/cli-lookup.mjs";
3
4
  const CLI_BULK_SCHEMA = 'whoisleuth.cli.bulk';
4
5
  const CLI_BULK_ITEM_SCHEMA = 'whoisleuth.cli.bulk.item';
5
6
  const CLI_CT_SEARCH_SCHEMA = 'whoisleuth.cli.ct-search';
@@ -9,42 +10,29 @@ const CLI_POSTURE_SCHEMA = 'whoisleuth.cli.posture';
9
10
  const CLI_HTTP_SCHEMA = 'whoisleuth.cli.http';
10
11
  const CLI_TLS_SCHEMA = 'whoisleuth.cli.tls';
11
12
  const CLI_COMPARE_SCHEMA = 'whoisleuth.cli.compare';
12
- const CLI_LOOKUP_SCHEMA_VERSION = 1;
13
+ const CLI_LOOKUP_SCHEMA_VERSION = SAVED_LOOKUP_SCHEMA_VERSION;
13
14
  const CLI_BULK_SCHEMA_VERSION = 3;
14
15
  const CLI_CT_SEARCH_SCHEMA_VERSION = 1;
15
16
  const CLI_DISCOVER_SCHEMA_VERSION = 2;
16
17
  const CLI_POSTURE_SCHEMA_VERSION = 1;
17
- const CLI_HTTP_SCHEMA_VERSION = 2;
18
+ const CLI_HTTP_SCHEMA_VERSION = 3;
18
19
  const CLI_TLS_SCHEMA_VERSION = 1;
19
20
  const CLI_COMPARE_SCHEMA_VERSION = 3;
20
- function buildCliLookupDocument(query, classified, result, generatedAt = new Date().toISOString(), mode = 'fast', collectionContext = {}) {
21
- const observerLabel = typeof collectionContext.observerLabel === 'string'
22
- ? collectionContext.observerLabel
23
- : null;
24
- const vantageLabel = typeof collectionContext.vantageLabel === 'string'
25
- ? collectionContext.vantageLabel
26
- : null;
27
- return {
28
- ...result,
29
- schema: CLI_LOOKUP_SCHEMA,
30
- version: CLI_LOOKUP_SCHEMA_VERSION,
31
- generatedAt,
32
- mode: mode === 'deep' ? 'deep' : 'fast',
33
- query,
34
- type: classified.type,
35
- inputHostname: classified.inputHostname,
36
- registrableDomain: classified.registrableDomain,
37
- isSubdomain: classified.isSubdomain,
38
- ...((observerLabel || vantageLabel) ? {
39
- collectionContext: {
40
- ...(observerLabel ? { observerLabel } : {}),
41
- ...(vantageLabel ? { vantageLabel } : {}),
42
- },
43
- } : {}),
44
- };
21
+ const TERMINAL_UNSAFE_JSON_RE = /[\u007f-\u009f\u2028\u2029]|\p{Default_Ignorable_Code_Point}/gu;
22
+ function jsonUnicodeEscape(value) {
23
+ const codePoint = value.codePointAt(0);
24
+ if (codePoint <= 0xffff)
25
+ return `\\u${codePoint.toString(16).padStart(4, '0')}`;
26
+ const offset = codePoint - 0x10000;
27
+ const high = 0xd800 + (offset >> 10);
28
+ const low = 0xdc00 + (offset & 0x3ff);
29
+ return `\\u${high.toString(16).padStart(4, '0')}\\u${low.toString(16).padStart(4, '0')}`;
30
+ }
31
+ function terminalSafeJson(value, space) {
32
+ return JSON.stringify(value, null, space).replace(TERMINAL_UNSAFE_JSON_RE, jsonUnicodeEscape);
45
33
  }
46
34
  function formatJsonDocument(document) {
47
- return `${JSON.stringify(document, null, 2)}\n`;
35
+ return `${terminalSafeJson(document, 2)}\n`;
48
36
  }
49
37
  function buildCliCtSearchDocument(keyword, result, generatedAt = new Date().toISOString()) {
50
38
  return { ...result, schema: CLI_CT_SEARCH_SCHEMA, version: CLI_CT_SEARCH_SCHEMA_VERSION, generatedAt, keyword };
@@ -83,7 +71,7 @@ function discoverJsonItem(candidate, metadata) {
83
71
  function formatDiscoverJsonLines(candidates, metadata) {
84
72
  if (!candidates.length)
85
73
  return '';
86
- return `${candidates.map((candidate) => JSON.stringify(discoverJsonItem(candidate, metadata))).join('\n')}\n`;
74
+ return `${candidates.map((candidate) => terminalSafeJson(discoverJsonItem(candidate, metadata))).join('\n')}\n`;
87
75
  }
88
76
  function formatDiscoverDomainList(candidates) {
89
77
  const domains = [...new Set(candidates.flatMap((candidate) => (typeof candidate.domain === 'string' && candidate.domain ? [candidate.domain] : [])))];
@@ -149,7 +137,7 @@ function buildCliBulkDocument(items, metadata) {
149
137
  }
150
138
  function formatJsonLines(items, metadata) {
151
139
  return items.length
152
- ? `${items.map((item) => JSON.stringify(bulkJsonItem(item, metadata))).join('\n')}\n`
140
+ ? `${items.map((item) => terminalSafeJson(bulkJsonItem(item, metadata))).join('\n')}\n`
153
141
  : '';
154
142
  }
155
- export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, };
143
+ export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, terminalSafeJson, };