@slicedearth/whoisleuth-cli 1.39.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 (66) hide show
  1. package/README.md +15 -0
  2. package/bin/whoisleuth.mjs +13 -2
  3. package/cli/archive-inspect.mjs +37 -2
  4. package/cli/arguments.mjs +361 -256
  5. package/cli/artifact-verify.mjs +35 -1
  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/completion.mjs +33 -10
  13. package/cli/config-profile.mjs +180 -0
  14. package/cli/ct-event-intake.mjs +159 -0
  15. package/cli/discovery-scan-command-runner.mjs +15 -1
  16. package/cli/domain-control-monitor.mjs +104 -0
  17. package/cli/domain-control-observations.mjs +235 -0
  18. package/cli/evidence-command-arguments.mjs +11 -0
  19. package/cli/evidence-command-runner.mjs +1 -0
  20. package/cli/external-observation-mapping.mjs +162 -0
  21. package/cli/fail-policy.mjs +64 -0
  22. package/cli/formatters/html.mjs +1 -1
  23. package/cli/investigation-manifest.mjs +113 -0
  24. package/cli/investigation-run.mjs +112 -0
  25. package/cli/lookup-brief.mjs +116 -0
  26. package/cli/lookup-command-runner.mjs +8 -1
  27. package/cli/lookup-diff.mjs +17 -19
  28. package/cli/network-command-runner.mjs +4 -1
  29. package/cli/offline-evidence-review.mjs +105 -8
  30. package/cli/open-asset-model-bridge.mjs +169 -0
  31. package/cli/registry-cohort.mjs +121 -0
  32. package/cli/registry-doctor.mjs +106 -11
  33. package/cli/registry-fixture-scaffold.mjs +73 -0
  34. package/cli/runner.mjs +368 -306
  35. package/docs/cli-reference.md +240 -14
  36. package/docs/cli.md +42 -7
  37. package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
  38. package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
  39. package/frontend/src/lib/analysis/case-report.js +573 -0
  40. package/frontend/src/lib/analysis/case-response-model.js +30 -1
  41. package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
  42. package/frontend/src/lib/analysis/external-findings-import.js +320 -0
  43. package/frontend/src/lib/analysis/lookup-decision-support.js +7 -0
  44. package/frontend/src/lib/analysis/lookup-presentation.js +10 -20
  45. package/frontend/src/lib/analysis/scoring.js +1 -1
  46. package/frontend/src/lib/analysis/visualization-models.js +2 -3
  47. package/frontend/src/lib/analysis/workspace-archive.js +2 -0
  48. package/lib/abusech-auth.mjs +1 -1
  49. package/lib/availability.mjs +1 -1
  50. package/lib/bounded-contract-normalizers.mjs +33 -0
  51. package/lib/dns-convergence-review.mjs +183 -0
  52. package/lib/dns-delegation-health.mjs +196 -3
  53. package/lib/domain-change-packet.mjs +133 -0
  54. package/lib/domain-change-review.mjs +307 -0
  55. package/lib/domain-control-flight-recorder.mjs +268 -0
  56. package/lib/domain-portfolio-review.mjs +151 -0
  57. package/lib/evidence-export.mjs +1 -1
  58. package/lib/html-signals.mjs +5 -11
  59. package/lib/nameserver-preflight-review.mjs +159 -0
  60. package/lib/page-language-signals.mjs +124 -0
  61. package/lib/risk-scoring.mjs +42 -1
  62. package/lib/static-html-analysis.mjs +29 -1
  63. package/lib/trust-store-comparison.mjs +146 -0
  64. package/lib/website-technology.mjs +24 -10
  65. package/lib/zone-intent-review.mjs +534 -0
  66. package/package.json +1 -1
package/README.md CHANGED
@@ -68,6 +68,11 @@ node bin/whoisleuth.mts reconcile office.json mobile.json external.json --json
68
68
  node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
69
69
  node bin/whoisleuth.mts registry-support example.test --json
70
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
71
76
  node bin/whoisleuth.mts lookalike-calibrate reviewed-candidates.json --json
72
77
  node bin/whoisleuth.mts doctor
73
78
  node bin/whoisleuth.mts commands --json
@@ -100,6 +105,10 @@ checkpoint. A checkpoint write failure preserves completed output and returns
100
105
  the partial-result exit code. Ctrl-C suppresses partial final output and
101
106
  returns exit code 130; a second interrupt performs best-effort temporary-file
102
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.
103
112
 
104
113
  The offline `timeline` command orders 2 to 20 saved Lookup documents for one
105
114
  domain and compares each adjacent observation. It retains observation times and
@@ -114,6 +123,12 @@ non-comparable evidence separate; the labels are context, not proof of
114
123
  independence. `registry-doctor`, `assurance`, `sharing-review`, and
115
124
  `lookalike-calibrate` add offline compatibility, change/recovery, sharing, and
116
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.
117
132
 
118
133
  Bulk also supports fixed-column CSV, domain-only or exact-query output,
119
134
  registered, inconclusive, and hard-failure output filters, and bounded A, AAAA,
@@ -20,11 +20,21 @@ if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) {
20
20
  }
21
21
  }
22
22
  else {
23
- const [errorsModule, outputModule, runnerModule] = await Promise.all([
23
+ const [configModule, errorsModule, outputModule, runnerModule] = await Promise.all([
24
+ import("../cli/config-profile.mjs"),
24
25
  import("../cli/errors.mjs"),
25
26
  import("../cli/output-file.mjs"),
26
27
  import("../cli/runner.mjs"),
27
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 = [];
37
+ }
28
38
  const cancellation = new AbortController();
29
39
  let interruptionCount = 0;
30
40
  const interrupt = () => {
@@ -37,7 +47,8 @@ else {
37
47
  }
38
48
  };
39
49
  process.on('SIGINT', interrupt);
40
- runnerModule.runCli(argv, { signal: cancellation.signal }).then((code) => {
50
+ const execution = resolvedArgv.length ? runnerModule.runCli(resolvedArgv, { signal: cancellation.signal }) : Promise.resolve(process.exitCode || 64);
51
+ execution.then((code) => {
41
52
  if (code === 130) {
42
53
  process.removeListener('SIGINT', interrupt);
43
54
  process.exit(130);
@@ -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`);