@slicedearth/whoisleuth-cli 1.34.0 → 1.39.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 (131) hide show
  1. package/README.md +19 -2
  2. package/bin/whoisleuth.mjs +41 -24
  3. package/cli/arguments.mjs +326 -3
  4. package/cli/artifact-verify.mjs +60 -14
  5. package/cli/bulk.mjs +1 -2
  6. package/cli/compare.mjs +1 -1
  7. package/cli/completion.mjs +36 -2
  8. package/cli/doctor.mjs +11 -5
  9. package/cli/evidence-command-arguments.mjs +1 -1
  10. package/cli/evidence-command-runner.mjs +2 -2
  11. package/cli/evidence-signing.mjs +4 -4
  12. package/cli/export-evidence.mjs +9 -2
  13. package/cli/formatters/html.mjs +1 -1
  14. package/cli/formatters/json.mjs +13 -1
  15. package/cli/formatters/terminal.mjs +14 -4
  16. package/cli/investigation-plan.mjs +112 -0
  17. package/cli/local-mmdb-review.mjs +76 -0
  18. package/cli/lookalike-calibration.mjs +121 -0
  19. package/cli/lookup-command-runner.mjs +4 -1
  20. package/cli/lookup-plan.mjs +3 -3
  21. package/cli/lookup-reconcile.mjs +141 -0
  22. package/cli/lookup-timeline.mjs +1 -1
  23. package/cli/mail-review.mjs +48 -3
  24. package/cli/offline-evidence-review.mjs +159 -0
  25. package/cli/registry-doctor.mjs +125 -0
  26. package/cli/registry-support.mjs +1 -4
  27. package/cli/risk-calibration.mjs +117 -9
  28. package/cli/runner.mjs +342 -15
  29. package/cli/saved-lookup.mjs +2 -2
  30. package/cli/sharing-review.mjs +202 -0
  31. package/cli/source-reliability.mjs +1 -1
  32. package/docs/cli-reference.md +1110 -0
  33. package/docs/cli.md +224 -847
  34. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  35. package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
  36. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  37. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  38. package/frontend/src/lib/analysis/bulk-retry-plan.js +3 -10
  39. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  40. package/frontend/src/lib/analysis/bulk-source-coverage.js +76 -0
  41. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  42. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  43. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  44. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  45. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  46. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  47. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  48. package/frontend/src/lib/analysis/http-summary.js +2 -1
  49. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  50. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  51. package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
  52. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  53. package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
  54. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  55. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  56. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  57. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  58. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  59. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  60. package/frontend/src/lib/analysis/scoring.js +8 -88
  61. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  62. package/frontend/src/lib/analysis/utils.js +7 -40
  63. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  64. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  65. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  66. package/frontend/src/lib/analysis/visualization-models.js +370 -0
  67. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  68. package/frontend/src/lib/analysis/website-snapshot-model.js +57 -3
  69. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  70. package/frontend/src/lib/analysis/workspace-archive.js +2 -2
  71. package/lib/analyst-taxonomy.mjs +40 -0
  72. package/lib/availability.mjs +30 -10
  73. package/lib/browser-library-profile.mjs +15 -2
  74. package/lib/bulk-limits.mjs +13 -0
  75. package/lib/certificate-extension-profile.mjs +71 -0
  76. package/lib/credential-surface-profile.mjs +1 -1
  77. package/lib/ct-response-bounds.mjs +9 -0
  78. package/lib/ct-search.mjs +66 -12
  79. package/lib/dns-delegation-health.mjs +56 -5
  80. package/lib/dnssec-evidence-validation.mjs +235 -0
  81. package/lib/domain-assurance.mjs +368 -0
  82. package/lib/domain-control-manifest.mjs +382 -0
  83. package/lib/domain-posture.mjs +11 -10
  84. package/lib/encrypted-dns-contract.mjs +81 -0
  85. package/lib/evidence-export.mjs +17 -3
  86. package/lib/evidence-report-markdown.mjs +1 -1
  87. package/lib/evidence-report.mjs +4 -1
  88. package/lib/favicon.mjs +3 -8
  89. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  90. package/lib/html-signals.mjs +4 -0
  91. package/lib/http-evidence-bounds.mjs +4 -0
  92. package/lib/http-intelligence.mjs +2 -1
  93. package/lib/ip-prefix.mjs +87 -0
  94. package/lib/local-geoip-evidence.mjs +89 -0
  95. package/lib/lookup-response-contract.mjs +346 -0
  96. package/lib/observed-network-context.mjs +1 -1
  97. package/lib/opportunity-scoring.mjs +107 -0
  98. package/lib/outbound-identity.mjs +9 -0
  99. package/lib/outbound-request-bounds.mjs +9 -0
  100. package/lib/page-fingerprints.mjs +1 -1
  101. package/lib/perceptual-hash-comparison.mjs +34 -0
  102. package/lib/perceptual-hash.mjs +3 -31
  103. package/lib/project-metadata.mjs +8 -0
  104. package/lib/provider-policy-admission.mjs +49 -0
  105. package/lib/rdap-capabilities.mjs +6 -6
  106. package/lib/rdap-search-workbench.mjs +247 -0
  107. package/lib/rdap.mjs +7 -2
  108. package/lib/registry-access.mjs +1 -1
  109. package/lib/registry-capabilities.mjs +31 -1
  110. package/lib/registry-capability-catalogue.mjs +15 -14
  111. package/lib/registry-insights.mjs +6 -6
  112. package/lib/response-policy.mjs +81 -6
  113. package/lib/risk-scoring.mjs +206 -83
  114. package/lib/rpki-evidence.mjs +105 -0
  115. package/lib/safe-fetch.mjs +2 -1
  116. package/lib/scoring-evidence-quality.mjs +107 -0
  117. package/lib/security-txt.mjs +3 -3
  118. package/lib/semantic-version.mjs +54 -0
  119. package/lib/static-html-analysis.mjs +27 -0
  120. package/lib/threat-intelligence-manifests.mjs +1 -1
  121. package/lib/threatfox-intelligence.mjs +11 -6
  122. package/lib/tls-intelligence.mjs +13 -3
  123. package/lib/tlsa-evidence.mjs +194 -0
  124. package/lib/urlhaus-intelligence.mjs +11 -6
  125. package/lib/urlscan-intelligence.mjs +10 -5
  126. package/lib/website-security-posture.mjs +16 -9
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-chain.mjs +14 -152
  129. package/lib/whois.mjs +2 -2
  130. package/package.json +6 -2
  131. package/third-party-notices.txt +911 -19
@@ -0,0 +1,202 @@
1
+ import { UnsupportedOfflineArtifactError, verifyOfflineArtifact, } from "./artifact-verify.mjs";
2
+ const SHARING_REVIEW_SCHEMA = 'whoisleuth.cli.sharing-review';
3
+ const SHARING_REVIEW_VERSION = 1;
4
+ const MAX_SHARING_REVIEW_BYTES = 15 * 1024 * 1024;
5
+ const TLP_MARKINGS = ['clear', 'green', 'amber', 'amber-strict', 'red'];
6
+ const RECIPIENT_SCOPES = ['public', 'community', 'organization', 'named-recipients'];
7
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
8
+ const RISKY_KEYS = new Set([
9
+ 'authorization', 'cookie', 'cookies', 'credential', 'credentials', 'email', 'emails',
10
+ 'entities', 'password', 'phone', 'raw', 'rawrdap', 'rawwhois', 'registrant', 'session', 'token',
11
+ ]);
12
+ const MARKING_KEYS = new Set([
13
+ 'informationmarking', 'marking', 'markings', 'sharingmarking',
14
+ 'tlp', 'tlplabel', 'tlpmarking', 'trafficlightprotocol',
15
+ ]);
16
+ function record(value) {
17
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
18
+ }
19
+ function normalizedKey(value) {
20
+ return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
21
+ }
22
+ function boundedMetadataText(value) {
23
+ if (typeof value !== 'string' || value.length > 120 || CONTROL_RE.test(value))
24
+ return null;
25
+ const trimmed = value.trim();
26
+ return trimmed || null;
27
+ }
28
+ function markingFromText(value) {
29
+ const normalized = value.toUpperCase().replace(/\s+/gu, '').replace('TLP:', '').replace('+STRICT', '-STRICT').toLowerCase();
30
+ return TLP_MARKINGS.includes(normalized) ? normalized : null;
31
+ }
32
+ function scanArtifact(root) {
33
+ const stack = [{ value: root, depth: 0, key: null }];
34
+ const markings = new Set();
35
+ let riskyKeyCount = 0;
36
+ let visited = 0;
37
+ let truncated = false;
38
+ while (stack.length && visited < 20_000) {
39
+ const next = stack.pop();
40
+ visited += 1;
41
+ if (next.key && RISKY_KEYS.has(normalizedKey(next.key)))
42
+ riskyKeyCount += 1;
43
+ if (typeof next.value === 'string') {
44
+ const marking = markingFromText(next.value);
45
+ if (marking && (next.key === null || MARKING_KEYS.has(normalizedKey(next.key))))
46
+ markings.add(marking);
47
+ continue;
48
+ }
49
+ if (next.depth >= 12) {
50
+ if (next.value && typeof next.value === 'object')
51
+ truncated = true;
52
+ continue;
53
+ }
54
+ if (Array.isArray(next.value)) {
55
+ for (const item of next.value.slice(0, 500))
56
+ stack.push({ value: item, depth: next.depth + 1, key: next.key });
57
+ if (next.value.length > 500)
58
+ truncated = true;
59
+ continue;
60
+ }
61
+ const valueRecord = record(next.value);
62
+ if (!valueRecord)
63
+ continue;
64
+ const entries = Object.entries(valueRecord);
65
+ for (const [key, value] of entries.slice(0, 500))
66
+ stack.push({ value, depth: next.depth + 1, key });
67
+ if (entries.length > 500)
68
+ truncated = true;
69
+ }
70
+ if (stack.length)
71
+ truncated = true;
72
+ return { importedMarkings: [...markings], riskyKeyCount, truncated };
73
+ }
74
+ function expectedScope(marking) {
75
+ if (marking === 'clear')
76
+ return 'public';
77
+ if (marking === 'green')
78
+ return 'community';
79
+ if (marking === 'red')
80
+ return 'named-recipients';
81
+ return 'organization';
82
+ }
83
+ function tlpLabel(marking) {
84
+ return `TLP:${marking === 'amber-strict' ? 'AMBER+STRICT' : marking.toUpperCase()}`;
85
+ }
86
+ async function buildSharingReview(raw, options, generatedAt = new Date().toISOString()) {
87
+ let artifactValue;
88
+ try {
89
+ artifactValue = JSON.parse(raw);
90
+ }
91
+ catch {
92
+ throw new TypeError('Sharing review input must be valid JSON.');
93
+ }
94
+ const artifact = record(artifactValue);
95
+ if (!artifact)
96
+ throw new TypeError('Sharing review input must contain one JSON object.');
97
+ const scan = scanArtifact(artifact);
98
+ const imported = [...scan.importedMarkings].sort((left, right) => TLP_MARKINGS.indexOf(right) - TLP_MARKINGS.indexOf(left));
99
+ const strictestImported = imported[0] || null;
100
+ const requestedRank = TLP_MARKINGS.indexOf(options.marking);
101
+ const importedRank = strictestImported === null ? -1 : TLP_MARKINGS.indexOf(strictestImported);
102
+ const effective = importedRank > requestedRank ? strictestImported : options.marking;
103
+ let integrity = 'unsupported';
104
+ try {
105
+ const verification = await verifyOfflineArtifact(raw);
106
+ integrity = verification.checks.contentIntegrity === 'verified' ? 'verified' : 'structure_only';
107
+ }
108
+ catch (error) {
109
+ integrity = error instanceof UnsupportedOfflineArtifactError ? 'unsupported' : 'failed';
110
+ }
111
+ const findings = [];
112
+ const add = (id, state, label, detail) => findings.push({ id, state, label, detail });
113
+ add('integrity', integrity === 'failed' ? 'block' : integrity === 'verified' ? 'pass' : 'caution', 'Artefact integrity', integrity === 'verified'
114
+ ? 'The artefact passed its supported local integrity contract.'
115
+ : integrity === 'structure_only'
116
+ ? 'The artefact passed its structural contract but has no verified content digest.'
117
+ : integrity === 'unsupported'
118
+ ? 'This artefact schema has no supported local integrity verifier; review its producer and digest contract manually.'
119
+ : 'The artefact failed its supported structure or integrity contract.');
120
+ add('human-review', options.humanReviewed ? 'pass' : 'block', 'Human review', options.humanReviewed
121
+ ? 'A deliberate human review was recorded for this sharing decision.'
122
+ : 'Complete a deliberate human review before sharing.');
123
+ add('personal-data', options.personalDataReviewed ? 'pass' : scan.riskyKeyCount ? 'block' : 'caution', 'Personal-data review', options.personalDataReviewed
124
+ ? 'A personal-data necessity review was recorded.'
125
+ : scan.riskyKeyCount
126
+ ? `The bounded key scan found ${scan.riskyKeyCount} field names that can carry raw, contact, credential, or session material.`
127
+ : 'No personal-data review was recorded; a bounded key scan cannot prove personal data is absent.');
128
+ add('redactions', options.redactionsConfirmed ? 'pass' : scan.riskyKeyCount ? 'block' : 'caution', 'Redaction review', options.redactionsConfirmed
129
+ ? 'A deliberate redaction review was recorded.'
130
+ : 'Confirm that secrets, unnecessary contacts, query strings, fragments, credentials, and unrelated evidence have been removed.');
131
+ add('marking', importedRank > requestedRank ? 'block' : 'pass', 'Sharing marking', importedRank > requestedRank
132
+ ? `${tlpLabel(options.marking)} is less restrictive than imported ${tlpLabel(strictestImported)} evidence.`
133
+ : strictestImported
134
+ ? `The requested marking preserves the strictest imported marking (${tlpLabel(strictestImported)}).`
135
+ : 'No recognised imported TLP 2.0 marking was found in the bounded scan.');
136
+ add('recipient-scope', options.recipientScope === expectedScope(effective) ? 'pass' : 'block', 'Recipient scope', options.recipientScope === expectedScope(effective)
137
+ ? `The recipient scope matches the effective ${tlpLabel(effective)} sharing boundary.`
138
+ : `${tlpLabel(effective)} requires the ${expectedScope(effective)} recipient scope in this local policy.`);
139
+ if (scan.truncated)
140
+ add('scan-bounds', 'caution', 'Bounded content scan', 'The defensive key and marking scan reached a traversal bound; review the artefact manually.');
141
+ const counts = {
142
+ block: findings.filter((finding) => finding.state === 'block').length,
143
+ caution: findings.filter((finding) => finding.state === 'caution').length,
144
+ pass: findings.filter((finding) => finding.state === 'pass').length,
145
+ };
146
+ const artifactSchema = boundedMetadataText(artifact.schema);
147
+ const rawArtifactVersion = artifact.version ?? artifact.schemaVersion;
148
+ const artifactVersion = Number.isSafeInteger(rawArtifactVersion)
149
+ && Number(rawArtifactVersion) >= 1
150
+ && Number(rawArtifactVersion) <= 10_000
151
+ ? Number(rawArtifactVersion)
152
+ : null;
153
+ const artifactMetadataFieldsEmitted = (Number(artifactSchema !== null) + Number(artifactVersion !== null));
154
+ return {
155
+ schema: SHARING_REVIEW_SCHEMA,
156
+ version: SHARING_REVIEW_VERSION,
157
+ generatedAt,
158
+ artifact: {
159
+ schema: artifactSchema,
160
+ version: artifactVersion,
161
+ integrity,
162
+ },
163
+ sharing: {
164
+ requestedMarking: tlpLabel(options.marking),
165
+ strictestImportedMarking: strictestImported ? tlpLabel(strictestImported) : null,
166
+ effectiveMarking: tlpLabel(effective),
167
+ recipientScope: options.recipientScope,
168
+ purposeRecorded: true,
169
+ },
170
+ findings,
171
+ summary: {
172
+ ...counts,
173
+ status: counts.block ? 'blocked' : counts.caution ? 'review_cautions' : 'ready',
174
+ },
175
+ privacy: { artifactMetadataFieldsEmitted, contentValuesEmitted: 0, rawEvidenceCopied: 0 },
176
+ limitations: [
177
+ 'This is a local pre-sharing lint, not legal advice, recipient authorisation, or proof that the artefact is accurate, current, complete, or safe to disclose.',
178
+ 'TLP 2.0 labels describe sharing boundaries; they do not replace source-specific terms, privacy obligations, confidentiality agreements, or recipient policy.',
179
+ 'The personal-data scan is bounded and key-based. It can miss sensitive meaning and therefore never replaces deliberate human review.',
180
+ 'The recorded purpose is required for analyst accountability but is intentionally omitted from machine output to avoid copying potentially sensitive case context.',
181
+ ],
182
+ };
183
+ }
184
+ function formatSharingReview(document) {
185
+ const output = [
186
+ 'Evidence sharing review',
187
+ `Status ${document.summary.status.replaceAll('_', ' ')}`,
188
+ `Effective marking ${document.sharing.effectiveMarking}`,
189
+ `Recipient scope ${document.sharing.recipientScope.replaceAll('-', ' ')}`,
190
+ `Integrity ${document.artifact.integrity.replaceAll('_', ' ')}`,
191
+ '',
192
+ ];
193
+ for (const finding of document.findings) {
194
+ output.push(`${finding.label} [${finding.state}]`);
195
+ output.push(` ${finding.detail}`);
196
+ }
197
+ output.push('', 'Limitations:');
198
+ for (const limitation of document.limitations)
199
+ output.push(` - ${limitation}`);
200
+ return `${output.join('\n')}\n`;
201
+ }
202
+ export { MAX_SHARING_REVIEW_BYTES, RECIPIENT_SCOPES, SHARING_REVIEW_SCHEMA, SHARING_REVIEW_VERSION, TLP_MARKINGS, buildSharingReview, formatSharingReview, };
@@ -91,7 +91,7 @@ const TIMING_SOURCES = new Set([
91
91
  'malware_ioc_intelligence',
92
92
  ]);
93
93
  const BASE_REPORT_LIMITATIONS = Object.freeze([
94
- 'This local report summarizes only source-health metadata present in the supplied files and does not contact or rank providers.',
94
+ 'This local report summarises only source-health metadata present in the supplied files and does not contact or rank providers.',
95
95
  'Timing measurements from concurrent Lookup sources overlap and must not be added together as total wall-clock time.',
96
96
  'Provider result states and their observation-health states are counted separately because they describe different contracts.',
97
97
  'Missing timing or observation envelopes remain unmeasured rather than being treated as fast, complete, or reliable.',