@slicedearth/whoisleuth-cli 1.36.1 → 1.43.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 (139) hide show
  1. package/README.md +32 -2
  2. package/bin/whoisleuth.mjs +52 -24
  3. package/cli/archive-inspect.mjs +37 -2
  4. package/cli/arguments.mjs +493 -157
  5. package/cli/artifact-verify.mjs +91 -15
  6. package/cli/bulk-checkpoint.mjs +7 -11
  7. package/cli/bulk-command-runner.mjs +19 -2
  8. package/cli/case-pack.mjs +136 -0
  9. package/cli/ci-report.mjs +73 -0
  10. package/cli/collection-preflight.mjs +59 -0
  11. package/cli/command-reference.mjs +395 -0
  12. package/cli/compare.mjs +1 -1
  13. package/cli/completion.mjs +49 -11
  14. package/cli/config-profile.mjs +180 -0
  15. package/cli/ct-event-intake.mjs +159 -0
  16. package/cli/discovery-scan-command-runner.mjs +15 -1
  17. package/cli/doctor.mjs +11 -5
  18. package/cli/domain-control-monitor.mjs +104 -0
  19. package/cli/domain-control-observations.mjs +235 -0
  20. package/cli/evidence-command-arguments.mjs +12 -1
  21. package/cli/evidence-command-runner.mjs +3 -2
  22. package/cli/evidence-signing.mjs +4 -4
  23. package/cli/external-observation-mapping.mjs +162 -0
  24. package/cli/fail-policy.mjs +64 -0
  25. package/cli/formatters/html.mjs +2 -2
  26. package/cli/formatters/json.mjs +13 -1
  27. package/cli/formatters/terminal.mjs +7 -3
  28. package/cli/investigation-manifest.mjs +113 -0
  29. package/cli/investigation-plan.mjs +2 -2
  30. package/cli/investigation-run.mjs +112 -0
  31. package/cli/local-mmdb-review.mjs +76 -0
  32. package/cli/lookalike-calibration.mjs +121 -0
  33. package/cli/lookup-brief.mjs +116 -0
  34. package/cli/lookup-command-runner.mjs +12 -2
  35. package/cli/lookup-diff.mjs +17 -19
  36. package/cli/lookup-plan.mjs +3 -3
  37. package/cli/lookup-reconcile.mjs +141 -0
  38. package/cli/lookup-timeline.mjs +1 -1
  39. package/cli/mail-review.mjs +1 -1
  40. package/cli/network-command-runner.mjs +4 -1
  41. package/cli/offline-evidence-review.mjs +125 -10
  42. package/cli/open-asset-model-bridge.mjs +169 -0
  43. package/cli/registry-cohort.mjs +121 -0
  44. package/cli/registry-doctor.mjs +220 -0
  45. package/cli/registry-fixture-scaffold.mjs +73 -0
  46. package/cli/risk-calibration.mjs +117 -9
  47. package/cli/runner.mjs +561 -280
  48. package/cli/saved-lookup.mjs +2 -2
  49. package/cli/sharing-review.mjs +202 -0
  50. package/cli/source-reliability.mjs +1 -1
  51. package/docs/cli-reference.md +1336 -0
  52. package/docs/cli.md +259 -940
  53. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  54. package/frontend/src/lib/analysis/acquisition-due-diligence.js +7 -7
  55. package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
  56. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  57. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  58. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  59. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  60. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  61. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  62. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  63. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  64. package/frontend/src/lib/analysis/case-report.js +573 -0
  65. package/frontend/src/lib/analysis/case-response-model.js +30 -1
  66. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  67. package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
  68. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  69. package/frontend/src/lib/analysis/external-findings-import.js +320 -0
  70. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  71. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  72. package/frontend/src/lib/analysis/lookup-decision-support.js +487 -0
  73. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  74. package/frontend/src/lib/analysis/lookup-presentation.js +64 -0
  75. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  76. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  77. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  78. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  79. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  80. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  81. package/frontend/src/lib/analysis/scoring.js +9 -89
  82. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  83. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  84. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  85. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  86. package/frontend/src/lib/analysis/visualization-models.js +369 -0
  87. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  88. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  89. package/frontend/src/lib/analysis/workspace-archive.js +4 -2
  90. package/lib/abusech-auth.mjs +1 -1
  91. package/lib/analyst-taxonomy.mjs +40 -0
  92. package/lib/availability.mjs +2 -2
  93. package/lib/bounded-contract-normalizers.mjs +33 -0
  94. package/lib/browser-library-profile.mjs +15 -2
  95. package/lib/certificate-extension-profile.mjs +71 -0
  96. package/lib/credential-surface-profile.mjs +1 -1
  97. package/lib/dns-convergence-review.mjs +183 -0
  98. package/lib/dns-delegation-health.mjs +196 -3
  99. package/lib/domain-assurance.mjs +368 -0
  100. package/lib/domain-change-packet.mjs +133 -0
  101. package/lib/domain-change-review.mjs +307 -0
  102. package/lib/domain-control-flight-recorder.mjs +268 -0
  103. package/lib/domain-control-manifest.mjs +50 -7
  104. package/lib/domain-portfolio-review.mjs +151 -0
  105. package/lib/domain-posture.mjs +9 -9
  106. package/lib/encrypted-dns-contract.mjs +1 -1
  107. package/lib/evidence-export.mjs +1 -1
  108. package/lib/evidence-report-markdown.mjs +1 -1
  109. package/lib/evidence-report.mjs +1 -1
  110. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  111. package/lib/html-signals.mjs +5 -11
  112. package/lib/ip-prefix.mjs +34 -1
  113. package/lib/local-geoip-evidence.mjs +2 -2
  114. package/lib/lookup-response-contract.mjs +346 -0
  115. package/lib/nameserver-preflight-review.mjs +159 -0
  116. package/lib/observed-network-context.mjs +1 -1
  117. package/lib/opportunity-scoring.mjs +107 -0
  118. package/lib/page-fingerprints.mjs +1 -1
  119. package/lib/page-language-signals.mjs +124 -0
  120. package/lib/rdap-capabilities.mjs +6 -6
  121. package/lib/rdap-search-workbench.mjs +96 -6
  122. package/lib/registry-access.mjs +1 -1
  123. package/lib/registry-capability-catalogue.mjs +4 -4
  124. package/lib/registry-insights.mjs +6 -6
  125. package/lib/risk-scoring.mjs +247 -83
  126. package/lib/scoring-evidence-quality.mjs +107 -0
  127. package/lib/security-txt.mjs +3 -3
  128. package/lib/static-html-analysis.mjs +29 -1
  129. package/lib/threat-intelligence-manifests.mjs +1 -1
  130. package/lib/threatfox-intelligence.mjs +1 -1
  131. package/lib/tls-intelligence.mjs +13 -3
  132. package/lib/trust-store-comparison.mjs +146 -0
  133. package/lib/urlhaus-intelligence.mjs +1 -1
  134. package/lib/website-security-posture.mjs +5 -5
  135. package/lib/website-technology.mjs +25 -11
  136. package/lib/whois-chain.mjs +1 -1
  137. package/lib/zone-intent-review.mjs +534 -0
  138. package/package.json +5 -1
  139. package/third-party-notices.txt +911 -19
package/README.md CHANGED
@@ -50,6 +50,7 @@ node bin/whoisleuth.mts --help
50
50
  node bin/whoisleuth.mts lookup example.test
51
51
  node bin/whoisleuth.mts lookup example.test --deep --summary
52
52
  node bin/whoisleuth.mts lookup example.test --deep --plan --json
53
+ node bin/whoisleuth.mts lookup example.test --deep --observer workstation-a --vantage office-egress --json
53
54
  node bin/whoisleuth.mts lookup example.test --deep --markdown --output lookup.md
54
55
  node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json
55
56
  node bin/whoisleuth.mts bulk domains.txt --csv --registered-only
@@ -59,10 +60,20 @@ node bin/whoisleuth.mts discover-scan example.test --scan-limit 50 --checkpoint
59
60
  node bin/whoisleuth.mts page-compare official.json candidate.json --json
60
61
  node bin/whoisleuth.mts mail-review bulk.json --json
61
62
  node bin/whoisleuth.mts domain-control domain-control-input.json --json
63
+ node bin/whoisleuth.mts assurance domain-assurance-input.json --json
64
+ node bin/whoisleuth.mts sharing-review response-packet.json --marking amber --recipient-scope organization --purpose 'Reviewed handoff' --human-reviewed --personal-data-reviewed --redactions-confirmed --json
62
65
  node bin/whoisleuth.mts workflow-plan domain-triage example.test --json
63
66
  node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
67
+ node bin/whoisleuth.mts reconcile office.json mobile.json external.json --json
64
68
  node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
65
69
  node bin/whoisleuth.mts registry-support example.test --json
70
+ node bin/whoisleuth.mts registry-doctor lookup.json --json
71
+ node bin/whoisleuth.mts registry-cohort saved-lookups.jsonl --json
72
+ node bin/whoisleuth.mts brief lookup.json
73
+ node bin/whoisleuth.mts case-pack cases.json --audience trusted --reviewed --json
74
+ node bin/whoisleuth.mts monitor-once control-manifest.json --json
75
+ node bin/whoisleuth.mts workflow-run domain-triage example.test --approve-network --json --output run.json
76
+ node bin/whoisleuth.mts lookalike-calibrate reviewed-candidates.json --json
66
77
  node bin/whoisleuth.mts doctor
67
78
  node bin/whoisleuth.mts commands --json
68
79
  node bin/whoisleuth.mts completion zsh
@@ -94,13 +105,31 @@ checkpoint. A checkpoint write failure preserves completed output and returns
94
105
  the partial-result exit code. Ctrl-C suppresses partial final output and
95
106
  returns exit code 130; a second interrupt performs best-effort temporary-file
96
107
  cleanup before exiting immediately.
108
+ Strict local profiles can supply only safe presentation, Fast-mode,
109
+ concurrency, and observer defaults; command-line options override them.
110
+ Supported automation commands can emit JUnit or SARIF and apply explicit
111
+ failure policies without changing their collection result.
97
112
 
98
113
  The offline `timeline` command orders 2 to 20 saved Lookup documents for one
99
114
  domain and compares each adjacent observation. It retains observation times and
100
- normalized field states but no input filenames or raw registry payloads.
115
+ normalised field states but no input filenames or raw registry payloads.
101
116
  Observed differences remain separate from missing, unavailable, and changed
102
117
  collection conditions.
103
118
 
119
+ Optional bounded observer and vantage labels let analysts distinguish saved
120
+ collection contexts without changing collection. `reconcile` compares 2 to 5
121
+ same-domain observations while keeping agreement, disagreement, and
122
+ non-comparable evidence separate; the labels are context, not proof of
123
+ independence. `registry-doctor`, `assurance`, `sharing-review`, and
124
+ `lookalike-calibrate` add offline compatibility, change/recovery, sharing, and
125
+ review-yield workflows without contacting the hosted application.
126
+ `registry-cohort` aggregates target-free publication-quality counts from saved
127
+ Lookups, while `registry-scaffold` produces a sanitised synthetic contributor
128
+ fixture. `brief` and `case-pack` create bounded local handoffs, `monitor-once`
129
+ runs one explicit control review rather than a daemon, and `workflow-run`
130
+ executes only installed fixed-recipe steps with per-run network approval and
131
+ analyst-selection pauses.
132
+
104
133
  Bulk also supports fixed-column CSV, domain-only or exact-query output,
105
134
  registered, inconclusive, and hard-failure output filters, and bounded A, AAAA,
106
135
  NS, and MX summaries. Discover
@@ -115,7 +144,8 @@ lookups can be compared across static page, parse5-tokenized DOM structure,
115
144
  favicon, technology, and TLS evidence, while saved Bulk output can be reviewed
116
145
  for passive MX, null MX, SPF, DMARC, and shared-provider context.
117
146
 
118
- See `docs/cli.md` in the package for the complete command, privacy, output, and
147
+ See `docs/cli.md` in the package for installation and first use, and
148
+ `docs/cli-reference.md` for the complete command, privacy, output, and
119
149
  evidence-contract reference. Report suspected vulnerabilities through the
120
150
  private channel described in `SECURITY.md`.
121
151
 
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { boundedCliErrorMessage } from "../cli/errors.mjs";
3
- import { cleanupPendingOutputFilesSync } from "../cli/output-file.mjs";
4
- import { runCli } from "../cli/runner.mjs";
2
+ import { createRequire } from 'node:module';
5
3
  for (const stream of [process.stdout, process.stderr]) {
6
4
  stream.on('error', (error) => {
7
5
  if (error.code === 'EPIPE')
@@ -9,27 +7,57 @@ for (const stream of [process.stdout, process.stderr]) {
9
7
  throw error;
10
8
  });
11
9
  }
12
- const cancellation = new AbortController();
13
- let interruptionCount = 0;
14
- const interrupt = () => {
15
- interruptionCount += 1;
16
- if (interruptionCount === 1)
17
- cancellation.abort(new DOMException('Aborted', 'AbortError'));
10
+ const argv = process.argv.slice(2);
11
+ if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) {
12
+ const require = createRequire(import.meta.url);
13
+ const metadata = require('../package.json');
14
+ if (typeof metadata.version !== 'string' || !metadata.version) {
15
+ process.stderr.write('Internal CLI error: Package version is unavailable.\n');
16
+ process.exitCode = 70;
17
+ }
18
18
  else {
19
- cleanupPendingOutputFilesSync();
20
- process.exit(130);
19
+ process.stdout.write(`${metadata.version}\n`);
21
20
  }
22
- };
23
- process.on('SIGINT', interrupt);
24
- runCli(process.argv.slice(2), { signal: cancellation.signal }).then((code) => {
25
- if (code === 130) {
26
- process.removeListener('SIGINT', interrupt);
27
- process.exit(130);
21
+ }
22
+ else {
23
+ const [configModule, errorsModule, outputModule, runnerModule] = await Promise.all([
24
+ import("../cli/config-profile.mjs"),
25
+ import("../cli/errors.mjs"),
26
+ import("../cli/output-file.mjs"),
27
+ import("../cli/runner.mjs"),
28
+ ]);
29
+ let resolvedArgv;
30
+ try {
31
+ resolvedArgv = await configModule.resolveCliProfileArguments(argv);
32
+ }
33
+ catch (error) {
34
+ process.stderr.write(`Usage error: ${errorsModule.boundedCliErrorMessage(error, 'Invalid CLI profile')}\n`);
35
+ process.exitCode = 64;
36
+ resolvedArgv = [];
28
37
  }
29
- process.exitCode = code;
30
- }).catch((error) => {
31
- process.stderr.write(`Internal CLI error: ${boundedCliErrorMessage(error)}\n`);
32
- process.exitCode = 70;
33
- }).finally(() => {
34
- process.removeListener('SIGINT', interrupt);
35
- });
38
+ const cancellation = new AbortController();
39
+ let interruptionCount = 0;
40
+ const interrupt = () => {
41
+ interruptionCount += 1;
42
+ if (interruptionCount === 1)
43
+ cancellation.abort(new DOMException('Aborted', 'AbortError'));
44
+ else {
45
+ outputModule.cleanupPendingOutputFilesSync();
46
+ process.exit(130);
47
+ }
48
+ };
49
+ process.on('SIGINT', interrupt);
50
+ const execution = resolvedArgv.length ? runnerModule.runCli(resolvedArgv, { signal: cancellation.signal }) : Promise.resolve(process.exitCode || 64);
51
+ execution.then((code) => {
52
+ if (code === 130) {
53
+ process.removeListener('SIGINT', interrupt);
54
+ process.exit(130);
55
+ }
56
+ process.exitCode = code;
57
+ }).catch((error) => {
58
+ process.stderr.write(`Internal CLI error: ${errorsModule.boundedCliErrorMessage(error)}\n`);
59
+ process.exitCode = 70;
60
+ }).finally(() => {
61
+ process.removeListener('SIGINT', interrupt);
62
+ });
63
+ }
@@ -3,9 +3,10 @@ import { createHash } from 'node:crypto';
3
3
  import { domainToASCII } from 'node:url';
4
4
  import { decryptWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive-crypto.js";
5
5
  import { WORKSPACE_ARCHIVE_SCHEMA, readWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive.js";
6
+ import { canonicalArtifactJson } from "../frontend/src/lib/analysis/artifact-integrity.js";
6
7
  import { MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
7
8
  export const ARCHIVE_INSPECTION_SCHEMA = 'whoisleuth.workspace-archive-inspection';
8
- export const ARCHIVE_INSPECTION_VERSION = 1;
9
+ export const ARCHIVE_INSPECTION_VERSION = 2;
9
10
  export const MAX_ARCHIVE_SEARCH_LENGTH = 253;
10
11
  export const MAX_ARCHIVE_SEARCH_MATCHES = 100;
11
12
  export const MAX_ARCHIVE_SEARCH_NODES = 100_000;
@@ -19,6 +20,7 @@ const SEARCHABLE_FIELDS = new Set([
19
20
  'target',
20
21
  ]);
21
22
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
23
+ const SHA256_DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
22
24
  function record(value) {
23
25
  return value && typeof value === 'object' && !Array.isArray(value)
24
26
  ? value
@@ -66,6 +68,30 @@ function canonicalSearchValue(value) {
66
68
  function valueDigest(value) {
67
69
  return createHash('sha256').update(value, 'utf8').digest('hex');
68
70
  }
71
+ function archiveContentDigest(sections) {
72
+ const identity = sections.map((section) => ({
73
+ id: section.id,
74
+ schema: section.schema,
75
+ version: section.version,
76
+ recordCount: section.recordCount,
77
+ content: (() => {
78
+ if (!section.data || typeof section.data !== 'object' || Array.isArray(section.data)) {
79
+ return section.data;
80
+ }
81
+ const { exportedAt: _exportedAt, generatedAt: _generatedAt, ...content } = section.data;
82
+ return content;
83
+ })(),
84
+ }));
85
+ return `sha256:${createHash('sha256').update(canonicalArtifactJson(identity), 'utf8').digest('hex')}`;
86
+ }
87
+ function expectedContentDigest(value) {
88
+ if (value === null || value === undefined || value === '')
89
+ return null;
90
+ if (typeof value !== 'string' || !SHA256_DIGEST_RE.test(value)) {
91
+ throw new TypeError('Expected archive content digest must use the form sha256 followed by 64 lowercase hexadecimal characters.');
92
+ }
93
+ return value;
94
+ }
69
95
  function searchSection(section, value, search, reveal, results, state) {
70
96
  const stack = [{ value, depth: 0 }];
71
97
  while (stack.length && results.length < MAX_ARCHIVE_SEARCH_MATCHES) {
@@ -120,6 +146,11 @@ export async function inspectWorkspaceArchive(raw, options = {}) {
120
146
  ? await decryptWorkspaceArchive(parsed, options.passphrase)
121
147
  : parsed;
122
148
  const archive = await readWorkspaceArchive(archiveValue);
149
+ const contentDigestSha256 = archiveContentDigest(archive.sections);
150
+ const expectedDigest = expectedContentDigest(options.expectedContentDigest);
151
+ if (expectedDigest && expectedDigest !== contentDigestSha256) {
152
+ throw new TypeError('Workspace archive content digest did not match the expected value.');
153
+ }
123
154
  const search = normalizeSearch(options.search);
124
155
  const reveal = options.reveal === true;
125
156
  if (reveal && !search)
@@ -143,12 +174,14 @@ export async function inspectWorkspaceArchive(raw, options = {}) {
143
174
  archive: Object.freeze({
144
175
  encrypted,
145
176
  schema: WORKSPACE_ARCHIVE_SCHEMA,
146
- version: archive.version,
177
+ version: archive.sourceVersion,
178
+ readerVersion: archive.version,
147
179
  }),
148
180
  summary: Object.freeze({
149
181
  inputBytes: Buffer.byteLength(raw, 'utf8'),
150
182
  sectionCount: archive.sections.length,
151
183
  recordCount: archive.sections.reduce((sum, section) => sum + section.recordCount, 0),
184
+ contentDigestSha256,
152
185
  }),
153
186
  sections: Object.freeze(archive.sections.map((section) => Object.freeze({
154
187
  id: section.id,
@@ -169,6 +202,7 @@ export async function inspectWorkspaceArchive(raw, options = {}) {
169
202
  }),
170
203
  limitations: Object.freeze([
171
204
  'Inspection validates the archive and reports bounded section metadata without printing retained evidence by default.',
205
+ 'The content digest covers ordered section identities and normalised content after excluding each section export timestamp. Equality detects matching retained archive content but does not authenticate its source or accuracy.',
172
206
  'Search checks exact values in a small allowlist of target fields and never searches analyst notes, contacts, or raw evidence.',
173
207
  ...(reveal
174
208
  ? ['Search values were revealed only because the operator supplied --reveal; handle the output as sensitive evidence.']
@@ -182,6 +216,7 @@ export function formatArchiveInspection(report) {
182
216
  `Archive: ${report.archive.encrypted ? 'encrypted' : 'plain'} · v${report.archive.version}`,
183
217
  `Sections: ${report.summary.sectionCount}`,
184
218
  `Records: ${report.summary.recordCount}`,
219
+ `Content digest: ${report.summary.contentDigestSha256}`,
185
220
  ];
186
221
  for (const section of report.sections) {
187
222
  lines.push(`${section.label}: ${section.recordCount} records · ${section.status} · ${section.bytes} bytes`);