@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.
- package/README.md +32 -2
- package/bin/whoisleuth.mjs +52 -24
- package/cli/archive-inspect.mjs +37 -2
- package/cli/arguments.mjs +493 -157
- package/cli/artifact-verify.mjs +91 -15
- package/cli/bulk-checkpoint.mjs +7 -11
- package/cli/bulk-command-runner.mjs +19 -2
- package/cli/case-pack.mjs +136 -0
- package/cli/ci-report.mjs +73 -0
- package/cli/collection-preflight.mjs +59 -0
- package/cli/command-reference.mjs +395 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +49 -11
- package/cli/config-profile.mjs +180 -0
- package/cli/ct-event-intake.mjs +159 -0
- package/cli/discovery-scan-command-runner.mjs +15 -1
- package/cli/doctor.mjs +11 -5
- package/cli/domain-control-monitor.mjs +104 -0
- package/cli/domain-control-observations.mjs +235 -0
- package/cli/evidence-command-arguments.mjs +12 -1
- package/cli/evidence-command-runner.mjs +3 -2
- package/cli/evidence-signing.mjs +4 -4
- package/cli/external-observation-mapping.mjs +162 -0
- package/cli/fail-policy.mjs +64 -0
- package/cli/formatters/html.mjs +2 -2
- package/cli/formatters/json.mjs +13 -1
- package/cli/formatters/terminal.mjs +7 -3
- package/cli/investigation-manifest.mjs +113 -0
- package/cli/investigation-plan.mjs +2 -2
- package/cli/investigation-run.mjs +112 -0
- package/cli/local-mmdb-review.mjs +76 -0
- package/cli/lookalike-calibration.mjs +121 -0
- package/cli/lookup-brief.mjs +116 -0
- package/cli/lookup-command-runner.mjs +12 -2
- package/cli/lookup-diff.mjs +17 -19
- package/cli/lookup-plan.mjs +3 -3
- package/cli/lookup-reconcile.mjs +141 -0
- package/cli/lookup-timeline.mjs +1 -1
- package/cli/mail-review.mjs +1 -1
- package/cli/network-command-runner.mjs +4 -1
- package/cli/offline-evidence-review.mjs +125 -10
- package/cli/open-asset-model-bridge.mjs +169 -0
- package/cli/registry-cohort.mjs +121 -0
- package/cli/registry-doctor.mjs +220 -0
- package/cli/registry-fixture-scaffold.mjs +73 -0
- package/cli/risk-calibration.mjs +117 -9
- package/cli/runner.mjs +561 -280
- package/cli/saved-lookup.mjs +2 -2
- package/cli/sharing-review.mjs +202 -0
- package/cli/source-reliability.mjs +1 -1
- package/docs/cli-reference.md +1336 -0
- package/docs/cli.md +259 -940
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +7 -7
- package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
- package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
- package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
- package/frontend/src/lib/analysis/case-migration-model.js +6 -1
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +5 -1
- package/frontend/src/lib/analysis/case-record-operations.js +6 -1
- package/frontend/src/lib/analysis/case-report.js +573 -0
- package/frontend/src/lib/analysis/case-response-model.js +30 -1
- package/frontend/src/lib/analysis/case-response-packet.js +7 -7
- package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
- package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
- package/frontend/src/lib/analysis/external-findings-import.js +320 -0
- package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
- package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +487 -0
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +64 -0
- package/frontend/src/lib/analysis/lookup-response.js +2 -0
- package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
- package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
- package/frontend/src/lib/analysis/page-similarity.js +1 -1
- package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
- package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
- package/frontend/src/lib/analysis/scoring.js +9 -89
- package/frontend/src/lib/analysis/shortlist-model.js +8 -4
- package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
- package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
- package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
- package/frontend/src/lib/analysis/visualization-models.js +369 -0
- package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
- package/frontend/src/lib/analysis/workspace-archive.js +4 -2
- package/lib/abusech-auth.mjs +1 -1
- package/lib/analyst-taxonomy.mjs +40 -0
- package/lib/availability.mjs +2 -2
- package/lib/bounded-contract-normalizers.mjs +33 -0
- package/lib/browser-library-profile.mjs +15 -2
- package/lib/certificate-extension-profile.mjs +71 -0
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/dns-convergence-review.mjs +183 -0
- package/lib/dns-delegation-health.mjs +196 -3
- package/lib/domain-assurance.mjs +368 -0
- package/lib/domain-change-packet.mjs +133 -0
- package/lib/domain-change-review.mjs +307 -0
- package/lib/domain-control-flight-recorder.mjs +268 -0
- package/lib/domain-control-manifest.mjs +50 -7
- package/lib/domain-portfolio-review.mjs +151 -0
- package/lib/domain-posture.mjs +9 -9
- package/lib/encrypted-dns-contract.mjs +1 -1
- package/lib/evidence-export.mjs +1 -1
- package/lib/evidence-report-markdown.mjs +1 -1
- package/lib/evidence-report.mjs +1 -1
- package/lib/generated/cisa-kev-catalog.mjs +1668 -0
- package/lib/html-signals.mjs +5 -11
- package/lib/ip-prefix.mjs +34 -1
- package/lib/local-geoip-evidence.mjs +2 -2
- package/lib/lookup-response-contract.mjs +346 -0
- package/lib/nameserver-preflight-review.mjs +159 -0
- package/lib/observed-network-context.mjs +1 -1
- package/lib/opportunity-scoring.mjs +107 -0
- package/lib/page-fingerprints.mjs +1 -1
- package/lib/page-language-signals.mjs +124 -0
- package/lib/rdap-capabilities.mjs +6 -6
- package/lib/rdap-search-workbench.mjs +96 -6
- package/lib/registry-access.mjs +1 -1
- package/lib/registry-capability-catalogue.mjs +4 -4
- package/lib/registry-insights.mjs +6 -6
- package/lib/risk-scoring.mjs +247 -83
- package/lib/scoring-evidence-quality.mjs +107 -0
- package/lib/security-txt.mjs +3 -3
- package/lib/static-html-analysis.mjs +29 -1
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threatfox-intelligence.mjs +1 -1
- package/lib/tls-intelligence.mjs +13 -3
- package/lib/trust-store-comparison.mjs +146 -0
- package/lib/urlhaus-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +5 -5
- package/lib/website-technology.mjs +25 -11
- package/lib/whois-chain.mjs +1 -1
- package/lib/zone-intent-review.mjs +534 -0
- package/package.json +5 -1
- 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
|
-
|
|
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
|
|
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
|
|
package/bin/whoisleuth.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
process.exit(130);
|
|
19
|
+
process.stdout.write(`${metadata.version}\n`);
|
|
21
20
|
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
package/cli/archive-inspect.mjs
CHANGED
|
@@ -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 =
|
|
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.
|
|
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`);
|