@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4
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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +67 -10
- package/bin/whoisleuth.mjs +22 -9
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +67 -31
- package/cli/bulk-command-runner.mjs +16 -6
- package/cli/bulk.mjs +18 -7
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +50 -11
- package/cli/config-profile.mjs +15 -12
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-observation-snapshot.mjs +8 -14
- package/cli/discovery-scan-command-runner.mjs +18 -6
- package/cli/discovery-snapshot.mjs +8 -15
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/errors.mjs +7 -1
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +12 -4
- package/cli/formatters/terminal.mjs +4 -1
- package/cli/http.mjs +2 -1
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/lookup-reconcile.mjs +1 -1
- package/cli/mail-review.mjs +5 -5
- package/cli/manual.mjs +1 -1
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-fixture-scaffold.mjs +4 -3
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +150 -177
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +467 -74
- package/docs/cli.md +25 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +1 -1
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +43 -13
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
- package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
- package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +27 -4
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
- package/frontend/src/lib/analysis/workspace-archive.js +105 -19
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +82 -0
- package/lib/bounded-json-response.mjs +170 -0
- package/lib/bounded-json.mjs +249 -0
- package/lib/browser-library-profile.mjs +167 -38
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +5 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +6 -6
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +96 -42
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +4 -13
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/website-technology.mjs +2 -6
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
package/cli/formatters/json.mjs
CHANGED
|
@@ -10,11 +10,11 @@ const CLI_HTTP_SCHEMA = 'whoisleuth.cli.http';
|
|
|
10
10
|
const CLI_TLS_SCHEMA = 'whoisleuth.cli.tls';
|
|
11
11
|
const CLI_COMPARE_SCHEMA = 'whoisleuth.cli.compare';
|
|
12
12
|
const CLI_LOOKUP_SCHEMA_VERSION = 1;
|
|
13
|
-
const CLI_BULK_SCHEMA_VERSION =
|
|
13
|
+
const CLI_BULK_SCHEMA_VERSION = 3;
|
|
14
14
|
const CLI_CT_SEARCH_SCHEMA_VERSION = 1;
|
|
15
15
|
const CLI_DISCOVER_SCHEMA_VERSION = 2;
|
|
16
16
|
const CLI_POSTURE_SCHEMA_VERSION = 1;
|
|
17
|
-
const CLI_HTTP_SCHEMA_VERSION =
|
|
17
|
+
const CLI_HTTP_SCHEMA_VERSION = 2;
|
|
18
18
|
const CLI_TLS_SCHEMA_VERSION = 1;
|
|
19
19
|
const CLI_COMPARE_SCHEMA_VERSION = 3;
|
|
20
20
|
function buildCliLookupDocument(query, classified, result, generatedAt = new Date().toISOString(), mode = 'fast', collectionContext = {}) {
|
|
@@ -25,6 +25,7 @@ function buildCliLookupDocument(query, classified, result, generatedAt = new Dat
|
|
|
25
25
|
? collectionContext.vantageLabel
|
|
26
26
|
: null;
|
|
27
27
|
return {
|
|
28
|
+
...result,
|
|
28
29
|
schema: CLI_LOOKUP_SCHEMA,
|
|
29
30
|
version: CLI_LOOKUP_SCHEMA_VERSION,
|
|
30
31
|
generatedAt,
|
|
@@ -40,7 +41,6 @@ function buildCliLookupDocument(query, classified, result, generatedAt = new Dat
|
|
|
40
41
|
...(vantageLabel ? { vantageLabel } : {}),
|
|
41
42
|
},
|
|
42
43
|
} : {}),
|
|
43
|
-
...result,
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
function formatJsonDocument(document) {
|
|
@@ -105,15 +105,21 @@ function buildCliCompareDocument(result, generatedAt = new Date().toISOString())
|
|
|
105
105
|
return { ...result, schema: CLI_COMPARE_SCHEMA, version: CLI_COMPARE_SCHEMA_VERSION, generatedAt };
|
|
106
106
|
}
|
|
107
107
|
function bulkJsonItem(item, metadata) {
|
|
108
|
+
const provenance = {
|
|
109
|
+
observedAt: item.observedAt ?? null,
|
|
110
|
+
collectionOrigin: item.collectionOrigin ?? 'current_run',
|
|
111
|
+
};
|
|
108
112
|
if (!item.ok) {
|
|
109
113
|
return {
|
|
110
114
|
schema: CLI_BULK_ITEM_SCHEMA, version: CLI_BULK_SCHEMA_VERSION, generatedAt: metadata.generatedAt,
|
|
115
|
+
...provenance,
|
|
111
116
|
index: item.index, query: item.query, ok: false, error: item.error,
|
|
112
117
|
};
|
|
113
118
|
}
|
|
114
119
|
const result = item.result;
|
|
115
120
|
return {
|
|
116
121
|
schema: CLI_BULK_ITEM_SCHEMA, version: CLI_BULK_SCHEMA_VERSION, generatedAt: metadata.generatedAt,
|
|
122
|
+
...provenance,
|
|
117
123
|
index: item.index, query: item.query, ok: true,
|
|
118
124
|
type: item.classified.type,
|
|
119
125
|
inputHostname: item.classified.inputHostname,
|
|
@@ -142,6 +148,8 @@ function buildCliBulkDocument(items, metadata) {
|
|
|
142
148
|
};
|
|
143
149
|
}
|
|
144
150
|
function formatJsonLines(items, metadata) {
|
|
145
|
-
return
|
|
151
|
+
return items.length
|
|
152
|
+
? `${items.map((item) => JSON.stringify(bulkJsonItem(item, metadata))).join('\n')}\n`
|
|
153
|
+
: '';
|
|
146
154
|
}
|
|
147
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, };
|
|
@@ -271,6 +271,9 @@ function formatTerminalRegistrySupport(document) {
|
|
|
271
271
|
`WHOIS scope ${supportLabel(whois.queryScope)}`,
|
|
272
272
|
`WHOIS encoding ${supportLabel(whois.encodingProfile)}`,
|
|
273
273
|
`WHOIS parser ${supportLabel(whois.parserProfile)}`,
|
|
274
|
+
...(typeof profile.officialLookupUrl === 'string' && profile.officialLookupUrl
|
|
275
|
+
? [`Official lookup ${safeTerminalValue(profile.officialLookupUrl)}`]
|
|
276
|
+
: []),
|
|
274
277
|
`Fixture states ${fixtures.length ? fixtures.map((value) => supportLabel(value)).join(', ') : 'None documented'}`,
|
|
275
278
|
];
|
|
276
279
|
for (const file of files)
|
|
@@ -431,7 +434,7 @@ function formatTerminalHttp(document) {
|
|
|
431
434
|
const lines = [
|
|
432
435
|
`Domain ${safeTerminalValue(document.domain)}`,
|
|
433
436
|
`Probe ${titleCase(document.probeStatus)}`,
|
|
434
|
-
`
|
|
437
|
+
`Assessment ${titleCase(document.assessment ?? document.activityStatus)}`,
|
|
435
438
|
`Evidence ${titleCase(http.status)}`,
|
|
436
439
|
`Final URL ${safeTerminalValue(http.finalUrl)}`,
|
|
437
440
|
`HTTP status ${safeTerminalValue(response.status)}`,
|
package/cli/http.mjs
CHANGED
|
@@ -17,7 +17,8 @@ function buildHttpProbeResult(domain, probe) {
|
|
|
17
17
|
return {
|
|
18
18
|
domain,
|
|
19
19
|
probeStatus: status,
|
|
20
|
-
|
|
20
|
+
assessment: status === 'fetched' || status === 'responded' ? 'active' : 'inconclusive',
|
|
21
|
+
activityStatus: status === 'fetched' || status === 'responded' ? 'active' : null,
|
|
21
22
|
detail: boundedDetail(input.detail),
|
|
22
23
|
http: input.http && typeof input.http === 'object' && !Array.isArray(input.http)
|
|
23
24
|
? input.http
|
package/cli/input.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
3
|
+
import { CliUsageError } from "./errors.mjs";
|
|
4
|
+
function abortError(signal) {
|
|
5
|
+
return signal.reason instanceof Error
|
|
6
|
+
? signal.reason
|
|
7
|
+
: new DOMException('The operation was aborted.', 'AbortError');
|
|
8
|
+
}
|
|
9
|
+
export async function readCliTextInput(source, stdin, options) {
|
|
10
|
+
if (options.signal?.aborted)
|
|
11
|
+
throw abortError(options.signal);
|
|
12
|
+
if (source) {
|
|
13
|
+
try {
|
|
14
|
+
return await readBoundedRegularTextFile(source, {
|
|
15
|
+
maximumBytes: options.maximumBytes,
|
|
16
|
+
label: options.label,
|
|
17
|
+
allowSymbolicLink: true,
|
|
18
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (options.signal?.aborted)
|
|
23
|
+
throw abortError(options.signal);
|
|
24
|
+
if (error instanceof CliUsageError)
|
|
25
|
+
throw error;
|
|
26
|
+
const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
|
|
27
|
+
throw new CliUsageError(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!stdin || stdin.isTTY)
|
|
31
|
+
return '';
|
|
32
|
+
let bytes = 0;
|
|
33
|
+
const chunks = [];
|
|
34
|
+
const onAbort = () => {
|
|
35
|
+
if ('destroy' in stdin && typeof stdin.destroy === 'function')
|
|
36
|
+
stdin.destroy(abortError(options.signal));
|
|
37
|
+
};
|
|
38
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
39
|
+
try {
|
|
40
|
+
for await (const chunk of stdin) {
|
|
41
|
+
if (options.signal?.aborted)
|
|
42
|
+
throw abortError(options.signal);
|
|
43
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
44
|
+
bytes += buffer.length;
|
|
45
|
+
if (bytes > options.maximumBytes) {
|
|
46
|
+
throw new CliUsageError(`${options.label} is limited to ${options.maximumBytes} bytes.`);
|
|
47
|
+
}
|
|
48
|
+
chunks.push(buffer);
|
|
49
|
+
}
|
|
50
|
+
if (options.signal?.aborted)
|
|
51
|
+
throw abortError(options.signal);
|
|
52
|
+
return Buffer.concat(chunks, bytes).toString('utf8');
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { INTERCHANGE_ARTIFACT_CONTRACTS, } from "../lib/interchange-fidelity-registry.mjs";
|
|
3
|
+
import { offlineArtifactSatisfiesAssurance, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
4
|
+
import { mergeBrandProfiles } from "../frontend/src/lib/analysis/brand-profile-model.js";
|
|
5
|
+
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
6
|
+
export const INTERCHANGE_FIDELITY_REPORT_SCHEMA = 'whoisleuth.interchange-fidelity-report';
|
|
7
|
+
export const INTERCHANGE_FIDELITY_REPORT_VERSION = 2;
|
|
8
|
+
export const MAX_INTERCHANGE_REPORT_BYTES = 15 * 1024 * 1024;
|
|
9
|
+
function record(value) {
|
|
10
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
14
|
+
function parseInput(raw) {
|
|
15
|
+
if (typeof raw !== 'string')
|
|
16
|
+
throw new TypeError('Interchange input must be UTF-8 JSON text.');
|
|
17
|
+
const bytes = Buffer.byteLength(raw, 'utf8');
|
|
18
|
+
if (bytes < 1 || bytes > MAX_INTERCHANGE_REPORT_BYTES) {
|
|
19
|
+
throw new TypeError(`Interchange input must be between 1 byte and ${MAX_INTERCHANGE_REPORT_BYTES} bytes.`);
|
|
20
|
+
}
|
|
21
|
+
return parseBoundedJsonObject(raw.replace(/^\uFEFF/u, ''), {
|
|
22
|
+
label: 'Interchange input',
|
|
23
|
+
maximumBytes: MAX_INTERCHANGE_REPORT_BYTES,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function nestedRecord(value, path) {
|
|
27
|
+
let current = value;
|
|
28
|
+
for (const part of path)
|
|
29
|
+
current = record(current?.[part]);
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
function identify(value) {
|
|
33
|
+
for (const contract of INTERCHANGE_ARTIFACT_CONTRACTS) {
|
|
34
|
+
const container = nestedRecord(value, contract.nestedSchemaPath);
|
|
35
|
+
if (container?.schema !== contract.schema)
|
|
36
|
+
continue;
|
|
37
|
+
const declared = container[contract.versionField];
|
|
38
|
+
return {
|
|
39
|
+
contract,
|
|
40
|
+
version: Number.isSafeInteger(declared) && Number(declared) > 0 && Number(declared) <= 1000
|
|
41
|
+
? Number(declared)
|
|
42
|
+
: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function timestamp(value) {
|
|
48
|
+
if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
49
|
+
throw new TypeError('Interchange report time is invalid.');
|
|
50
|
+
}
|
|
51
|
+
const parsed = Date.parse(value);
|
|
52
|
+
if (!Number.isFinite(parsed))
|
|
53
|
+
throw new TypeError('Interchange report time is invalid.');
|
|
54
|
+
return new Date(parsed).toISOString();
|
|
55
|
+
}
|
|
56
|
+
function validateBrandProfileExport(value) {
|
|
57
|
+
const profiles = value.profiles;
|
|
58
|
+
if (!Array.isArray(profiles) || profiles.length > 100) {
|
|
59
|
+
throw new TypeError('Brand Profile export does not contain a bounded profiles collection.');
|
|
60
|
+
}
|
|
61
|
+
if (typeof value.exportedAt !== 'string' || value.exportedAt.length > 64 || !Number.isFinite(Date.parse(value.exportedAt))) {
|
|
62
|
+
throw new TypeError('Brand Profile export time is invalid.');
|
|
63
|
+
}
|
|
64
|
+
let nextId = 0;
|
|
65
|
+
const result = mergeBrandProfiles([], value, {
|
|
66
|
+
nowIso: new Date(Date.parse(value.exportedAt)).toISOString(),
|
|
67
|
+
makeId: () => `interchange-profile-${++nextId}`,
|
|
68
|
+
});
|
|
69
|
+
return Object.freeze({ accepted: result.profiles.length, skipped: result.skipped });
|
|
70
|
+
}
|
|
71
|
+
export async function buildInterchangeFidelityReport(raw, options = {}) {
|
|
72
|
+
const inputBytes = Buffer.byteLength(raw, 'utf8');
|
|
73
|
+
const value = parseInput(raw);
|
|
74
|
+
const identified = identify(value);
|
|
75
|
+
const generatedAt = timestamp(options.generatedAt ?? new Date().toISOString());
|
|
76
|
+
if (!identified)
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
schema: INTERCHANGE_FIDELITY_REPORT_SCHEMA,
|
|
79
|
+
version: INTERCHANGE_FIDELITY_REPORT_VERSION,
|
|
80
|
+
generatedAt,
|
|
81
|
+
recognised: false,
|
|
82
|
+
artifact: Object.freeze({ id: null, schema: null, version: null, versionSupported: false }),
|
|
83
|
+
verification: Object.freeze({ state: 'not_verified', requiredAssurance: null, assuranceSatisfied: false }),
|
|
84
|
+
compatibility: Object.freeze({
|
|
85
|
+
browser: null,
|
|
86
|
+
cli: null,
|
|
87
|
+
fidelity: 'not_verified',
|
|
88
|
+
preservedFieldGroups: Object.freeze([]),
|
|
89
|
+
excludedFieldGroups: Object.freeze([]),
|
|
90
|
+
futureVersionBehaviour: null,
|
|
91
|
+
fullyImportable: null,
|
|
92
|
+
}),
|
|
93
|
+
summary: Object.freeze({
|
|
94
|
+
inputBytes, recordCount: null, sectionCount: null, encryptedContentVerified: null,
|
|
95
|
+
acceptedRecordCount: null, skippedRecordCount: null, unsupportedSectionCount: null,
|
|
96
|
+
blockedSectionCount: null, prunedRecordCount: null,
|
|
97
|
+
}),
|
|
98
|
+
limitations: Object.freeze([
|
|
99
|
+
'The file did not match a registered interchange contract. Its supplied schema text and contents are not echoed.',
|
|
100
|
+
'This report makes no request and does not establish that evidence is accurate, current, complete, or safe to share.',
|
|
101
|
+
]),
|
|
102
|
+
});
|
|
103
|
+
const { contract, version } = identified;
|
|
104
|
+
const supported = version !== null && contract.versions.includes(version);
|
|
105
|
+
let verificationState = supported ? 'not_verified' : 'unsupported_version';
|
|
106
|
+
let assuranceSatisfied = false;
|
|
107
|
+
let recordCount = null;
|
|
108
|
+
let sectionCount = null;
|
|
109
|
+
let encryptedContentVerified = contract.id === 'encrypted_workspace' ? false : null;
|
|
110
|
+
let acceptedRecordCount = null;
|
|
111
|
+
let skippedRecordCount = null;
|
|
112
|
+
let unsupportedSectionCount = null;
|
|
113
|
+
let blockedSectionCount = null;
|
|
114
|
+
let prunedRecordCount = null;
|
|
115
|
+
let fullyImportable = null;
|
|
116
|
+
if (supported && contract.fidelity !== 'unsupported') {
|
|
117
|
+
try {
|
|
118
|
+
if (contract.id === 'brand_profiles') {
|
|
119
|
+
const validation = validateBrandProfileExport(value);
|
|
120
|
+
recordCount = validation.accepted;
|
|
121
|
+
acceptedRecordCount = validation.accepted;
|
|
122
|
+
skippedRecordCount = validation.skipped;
|
|
123
|
+
verificationState = validation.skipped === 0 ? 'structure_valid' : 'not_verified';
|
|
124
|
+
assuranceSatisfied = validation.skipped === 0 && contract.requiredAssurance === 'structure';
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const verification = await verifyOfflineArtifact(raw, { passphrase: options.passphrase ?? null });
|
|
128
|
+
verificationState = verification.state;
|
|
129
|
+
assuranceSatisfied = contract.requiredAssurance === 'whole_integrity'
|
|
130
|
+
? offlineArtifactSatisfiesAssurance(verification, 'whole_integrity')
|
|
131
|
+
: contract.requiredAssurance === 'structure'
|
|
132
|
+
? offlineArtifactSatisfiesAssurance(verification, 'structure')
|
|
133
|
+
: contract.requiredAssurance === 'authenticated_whole_integrity'
|
|
134
|
+
? offlineArtifactSatisfiesAssurance(verification, 'whole_integrity')
|
|
135
|
+
&& verification.checks.authenticatedEncryption === 'verified'
|
|
136
|
+
: false;
|
|
137
|
+
recordCount = verification.summary.recordCount;
|
|
138
|
+
sectionCount = verification.summary.sectionCount;
|
|
139
|
+
const unsupportedSections = verification.summary.unsupportedSectionCount ?? 0;
|
|
140
|
+
unsupportedSectionCount = unsupportedSections;
|
|
141
|
+
blockedSectionCount = verification.summary.blockedSectionCount ?? null;
|
|
142
|
+
skippedRecordCount = verification.summary.skippedRecordCount ?? skippedRecordCount;
|
|
143
|
+
prunedRecordCount = verification.summary.prunedRecordCount ?? null;
|
|
144
|
+
if (verification.summary.fullyImportable !== undefined
|
|
145
|
+
&& verification.summary.fullyImportable !== null) {
|
|
146
|
+
fullyImportable = assuranceSatisfied && verification.summary.fullyImportable;
|
|
147
|
+
}
|
|
148
|
+
if (contract.id === 'encrypted_workspace')
|
|
149
|
+
encryptedContentVerified = verification.state === 'verified'
|
|
150
|
+
&& verification.checks.authenticatedEncryption === 'verified';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
verificationState = 'not_verified';
|
|
155
|
+
assuranceSatisfied = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const fidelityEligible = assuranceSatisfied && fullyImportable !== false;
|
|
159
|
+
const fidelity = !supported
|
|
160
|
+
? 'unsupported'
|
|
161
|
+
: fidelityEligible
|
|
162
|
+
? contract.fidelity
|
|
163
|
+
: contract.fidelity === 'unsupported'
|
|
164
|
+
? 'unsupported'
|
|
165
|
+
: 'not_verified';
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
schema: INTERCHANGE_FIDELITY_REPORT_SCHEMA,
|
|
168
|
+
version: INTERCHANGE_FIDELITY_REPORT_VERSION,
|
|
169
|
+
generatedAt,
|
|
170
|
+
recognised: true,
|
|
171
|
+
artifact: Object.freeze({ id: contract.id, schema: contract.schema, version, versionSupported: supported }),
|
|
172
|
+
verification: Object.freeze({
|
|
173
|
+
state: verificationState,
|
|
174
|
+
requiredAssurance: contract.requiredAssurance,
|
|
175
|
+
assuranceSatisfied,
|
|
176
|
+
}),
|
|
177
|
+
compatibility: Object.freeze({
|
|
178
|
+
browser: contract.browser,
|
|
179
|
+
cli: contract.cli,
|
|
180
|
+
fidelity,
|
|
181
|
+
preservedFieldGroups: contract.preservedFieldGroups,
|
|
182
|
+
excludedFieldGroups: contract.excludedFieldGroups,
|
|
183
|
+
futureVersionBehaviour: contract.futureVersionBehaviour,
|
|
184
|
+
fullyImportable,
|
|
185
|
+
}),
|
|
186
|
+
summary: Object.freeze({
|
|
187
|
+
inputBytes, recordCount, sectionCount, encryptedContentVerified,
|
|
188
|
+
acceptedRecordCount, skippedRecordCount, unsupportedSectionCount,
|
|
189
|
+
blockedSectionCount, prunedRecordCount,
|
|
190
|
+
}),
|
|
191
|
+
limitations: Object.freeze([
|
|
192
|
+
'Compatibility describes declared field groups and supported operations, not byte-for-byte equality or evidence truth.',
|
|
193
|
+
'The report emits only registered metadata, counts, and fixed field-group identifiers. Targets, notes, contacts, passphrases, and evidence values are not included.',
|
|
194
|
+
'A verified integrity envelope detects file changes but does not establish author identity, source accuracy, currency, completeness, or sharing authority.',
|
|
195
|
+
]),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
export function formatInterchangeFidelityReport(report) {
|
|
199
|
+
const browser = report.compatibility.browser;
|
|
200
|
+
const cli = report.compatibility.cli;
|
|
201
|
+
return [
|
|
202
|
+
'Interchange fidelity report',
|
|
203
|
+
`Recognised ${report.recognised ? 'yes' : 'no'}`,
|
|
204
|
+
`Artifact ${report.artifact.id ?? 'unrecognised'}`,
|
|
205
|
+
`Version ${report.artifact.version ?? 'unavailable'}${report.artifact.versionSupported ? '' : ' (unsupported)'}`,
|
|
206
|
+
`Verification ${report.verification.state}`,
|
|
207
|
+
`Assurance ${report.verification.requiredAssurance ?? 'unregistered'} · ${report.verification.assuranceSatisfied ? 'satisfied' : 'withheld'}`,
|
|
208
|
+
`Fidelity ${report.compatibility.fidelity}`,
|
|
209
|
+
...(report.compatibility.fullyImportable === null ? [] : [`Importability ${report.compatibility.fullyImportable ? 'complete' : 'partial'}`]),
|
|
210
|
+
`Browser import ${browser?.import ?? 'unknown'} · export ${browser?.export ?? 'unknown'}`,
|
|
211
|
+
`CLI read ${cli?.read ?? 'unknown'} · write ${cli?.write ?? 'unknown'} · verify ${cli?.verify ?? 'unknown'}`,
|
|
212
|
+
`Records ${report.summary.recordCount ?? 'not disclosed'}`,
|
|
213
|
+
`Sections ${report.summary.sectionCount ?? 'not disclosed'}`,
|
|
214
|
+
...(report.summary.acceptedRecordCount === null ? [] : [`Accepted ${report.summary.acceptedRecordCount}`]),
|
|
215
|
+
...(report.summary.skippedRecordCount === null ? [] : [`Skipped ${report.summary.skippedRecordCount}`]),
|
|
216
|
+
...(report.summary.unsupportedSectionCount === null ? [] : [`Unsupported ${report.summary.unsupportedSectionCount}`]),
|
|
217
|
+
...(report.summary.blockedSectionCount === null ? [] : [`Blocked ${report.summary.blockedSectionCount}`]),
|
|
218
|
+
...(report.summary.prunedRecordCount === null ? [] : [`Pruned ${report.summary.prunedRecordCount}`]),
|
|
219
|
+
'',
|
|
220
|
+
`Preserved ${report.compatibility.preservedFieldGroups.join(', ') || 'none declared'}`,
|
|
221
|
+
`Excluded ${report.compatibility.excludedFieldGroups.join(', ') || 'none declared'}`,
|
|
222
|
+
'',
|
|
223
|
+
...report.limitations.map((item) => `Limitation: ${item}`),
|
|
224
|
+
'',
|
|
225
|
+
].join('\n');
|
|
226
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { canonicalArtifactJsonV2, SORTED_JSON_V2, sha256ArtifactDigestV2, } from "../frontend/src/lib/analysis/artifact-integrity.js";
|
|
4
|
+
import { requireBoundedString, requireIsoTimestamp, } from "../lib/bounded-contract-normalizers.mjs";
|
|
5
5
|
import { normalizeBoundedSemanticVersion } from "../lib/semantic-version.mjs";
|
|
6
|
+
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
6
7
|
export const INVESTIGATION_MANIFEST_SCHEMA = 'whoisleuth.investigation-manifest';
|
|
7
|
-
export const INVESTIGATION_MANIFEST_VERSION =
|
|
8
|
+
export const INVESTIGATION_MANIFEST_VERSION = 2;
|
|
8
9
|
export const MAX_INVESTIGATION_MANIFEST_ARTIFACTS = 16;
|
|
9
10
|
export const MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES = 15 * 1024 * 1024;
|
|
10
11
|
export const MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
@@ -40,20 +41,17 @@ export async function buildInvestigationManifest(input, generatedAtValue, applic
|
|
|
40
41
|
if (byteLength < 1 || byteLength > MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES) {
|
|
41
42
|
throw new TypeError(`Artifact ${index + 1} must be between 1 byte and ${MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES} bytes.`);
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
parsed = JSON.parse(artifact.content);
|
|
44
|
+
if (totalBytes > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) {
|
|
45
|
+
throw new TypeError(`Manifest artefacts exceed the ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES}-byte combined limit.`);
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!value)
|
|
52
|
-
throw new TypeError(`Artifact ${index + 1} must contain one JSON object.`);
|
|
47
|
+
const value = parseBoundedJsonObject(artifact.content, {
|
|
48
|
+
label: `Artifact ${index + 1}`,
|
|
49
|
+
maximumBytes: MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES,
|
|
50
|
+
});
|
|
53
51
|
const schema = value.schema === undefined
|
|
54
52
|
? null
|
|
55
53
|
: requireBoundedString(value.schema, `Artifact ${index + 1} schema`, 160);
|
|
56
|
-
const canonical =
|
|
54
|
+
const canonical = canonicalArtifactJsonV2(value);
|
|
57
55
|
const contentDigestSha256 = digest(artifact.content);
|
|
58
56
|
return Object.freeze({
|
|
59
57
|
sequence: index + 1,
|
|
@@ -65,9 +63,6 @@ export async function buildInvestigationManifest(input, generatedAtValue, applic
|
|
|
65
63
|
canonicalDigestSha256: digest(canonical),
|
|
66
64
|
});
|
|
67
65
|
});
|
|
68
|
-
if (totalBytes > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) {
|
|
69
|
-
throw new TypeError(`Manifest artefacts exceed the ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES}-byte combined limit.`);
|
|
70
|
-
}
|
|
71
66
|
const unsigned = Object.freeze({
|
|
72
67
|
schema: INVESTIGATION_MANIFEST_SCHEMA,
|
|
73
68
|
version: INVESTIGATION_MANIFEST_VERSION,
|
|
@@ -90,7 +85,11 @@ export async function buildInvestigationManifest(input, generatedAtValue, applic
|
|
|
90
85
|
});
|
|
91
86
|
return Object.freeze({
|
|
92
87
|
...unsigned,
|
|
93
|
-
integrity: Object.freeze({
|
|
88
|
+
integrity: Object.freeze({
|
|
89
|
+
algorithm: 'SHA-256',
|
|
90
|
+
canonicalization: SORTED_JSON_V2,
|
|
91
|
+
digestSha256: await sha256ArtifactDigestV2(unsigned),
|
|
92
|
+
}),
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
export function formatInvestigationManifest(manifest) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { buildInvestigationPlan, } from "./investigation-plan.mjs";
|
|
3
3
|
import { CliUsageError } from "./errors.mjs";
|
|
4
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
4
5
|
export const CLI_INVESTIGATION_RUN_SCHEMA = 'whoisleuth.cli.investigation-run';
|
|
5
6
|
export const CLI_INVESTIGATION_RUN_VERSION = 1;
|
|
6
7
|
export const MAX_INVESTIGATION_RUN_BYTES = 24 * 1024 * 1024;
|
|
@@ -9,50 +10,93 @@ function boundedResult(value) {
|
|
|
9
10
|
throw new CliUsageError(`Investigation step output is limited to ${MAX_INVESTIGATION_RUN_BYTES} bytes.`);
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
13
|
+
scanBoundedJson(value);
|
|
12
14
|
return JSON.parse(value);
|
|
13
15
|
}
|
|
14
|
-
catch {
|
|
16
|
+
catch (cause) {
|
|
17
|
+
if (cause instanceof TypeError && cause.message !== 'Artefact input is not valid JSON.') {
|
|
18
|
+
throw new CliUsageError(`Investigation step output ${cause.message.replace(/^Artefact JSON /u, '')}`);
|
|
19
|
+
}
|
|
15
20
|
return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '').slice(0, MAX_INVESTIGATION_RUN_BYTES);
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
|
-
function
|
|
23
|
+
function resultSchema(value) {
|
|
24
|
+
return value && typeof value === 'object' && !Array.isArray(value) && typeof value.schema === 'string'
|
|
25
|
+
? String(value.schema)
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
function sameArguments(left, right) {
|
|
29
|
+
return Array.isArray(left)
|
|
30
|
+
&& left.length === right.length
|
|
31
|
+
&& left.every((value, index) => typeof value === 'string' && value === right[index]);
|
|
32
|
+
}
|
|
33
|
+
function parseResumeState(input, plan) {
|
|
19
34
|
if (!input)
|
|
20
35
|
return [];
|
|
21
36
|
if (Buffer.byteLength(input, 'utf8') > MAX_INVESTIGATION_RUN_BYTES)
|
|
22
37
|
throw new CliUsageError('Investigation resume state exceeds the 24 MiB limit.');
|
|
38
|
+
const normalizedInput = input.replace(/^\uFEFF/u, '');
|
|
39
|
+
try {
|
|
40
|
+
scanBoundedJson(normalizedInput);
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
const detail = cause instanceof Error ? cause.message : '';
|
|
44
|
+
if (detail === 'Artefact input is not valid JSON.')
|
|
45
|
+
throw new CliUsageError('Investigation resume state must be valid JSON.');
|
|
46
|
+
throw new CliUsageError(detail ? `Investigation resume state ${detail.replace(/^Artefact JSON /u, '')}` : 'Investigation resume state must be valid JSON.');
|
|
47
|
+
}
|
|
23
48
|
let parsed;
|
|
24
49
|
try {
|
|
25
|
-
parsed = JSON.parse(
|
|
50
|
+
parsed = JSON.parse(normalizedInput);
|
|
26
51
|
}
|
|
27
52
|
catch {
|
|
28
53
|
throw new CliUsageError('Investigation resume state must be valid JSON.');
|
|
29
54
|
}
|
|
30
55
|
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
31
|
-
if (root.schema !== CLI_INVESTIGATION_RUN_SCHEMA || root.version !== CLI_INVESTIGATION_RUN_VERSION || root.recipe !== recipe || root.subject !== subject || !Array.isArray(root.completedSteps)) {
|
|
56
|
+
if (root.schema !== CLI_INVESTIGATION_RUN_SCHEMA || root.version !== CLI_INVESTIGATION_RUN_VERSION || root.recipe !== plan.recipe.id || root.subject !== plan.subject || !Array.isArray(root.completedSteps)) {
|
|
32
57
|
throw new CliUsageError('Investigation resume state must match this versioned recipe and subject.');
|
|
33
58
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
if (root.completedSteps.length > plan.steps.length) {
|
|
60
|
+
throw new CliUsageError('Investigation resume state contains more steps than the installed fixed recipe.');
|
|
61
|
+
}
|
|
62
|
+
const completed = [];
|
|
63
|
+
for (const [index, value] of root.completedSteps.entries()) {
|
|
64
|
+
const planned = plan.steps[index];
|
|
65
|
+
if (!planned || !value || typeof value !== 'object' || Array.isArray(value)) {
|
|
66
|
+
throw new CliUsageError('Investigation resume state contains a malformed or out-of-order step.');
|
|
67
|
+
}
|
|
37
68
|
const item = value;
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
if (item.id !== planned.id
|
|
70
|
+
|| item.command !== planned.command
|
|
71
|
+
|| item.mode !== planned.mode
|
|
72
|
+
|| !sameArguments(item.arguments, planned.arguments)
|
|
73
|
+
|| !Number.isSafeInteger(item.exitCode)) {
|
|
74
|
+
throw new CliUsageError('Investigation resume state does not match the installed fixed recipe.');
|
|
75
|
+
}
|
|
76
|
+
const exitCode = Number(item.exitCode);
|
|
77
|
+
if (exitCode !== 0 && exitCode !== 2) {
|
|
78
|
+
if (index !== root.completedSteps.length - 1) {
|
|
79
|
+
throw new CliUsageError('A failed investigation step must be the final retained step.');
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (resultSchema(item.result) !== planned.produces) {
|
|
84
|
+
throw new CliUsageError('Investigation resume state contains output from an unexpected command contract.');
|
|
85
|
+
}
|
|
86
|
+
completed.push(Object.freeze({
|
|
87
|
+
id: planned.id,
|
|
88
|
+
command: planned.command,
|
|
89
|
+
arguments: planned.arguments,
|
|
90
|
+
mode: planned.mode,
|
|
91
|
+
exitCode,
|
|
92
|
+
result: item.result,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return completed;
|
|
49
96
|
}
|
|
50
97
|
export async function runInvestigationRecipe(recipe, subjectValue, options) {
|
|
51
98
|
const plan = buildInvestigationPlan(recipe, subjectValue, options.generatedAt);
|
|
52
|
-
const prior = parseResumeState(options.resumeInput,
|
|
53
|
-
const allowedStepIds = new Set(plan.steps.map((step) => step.id));
|
|
54
|
-
if (prior.some((step) => !allowedStepIds.has(step.id)))
|
|
55
|
-
throw new CliUsageError('Investigation resume state contains a step outside the fixed recipe.');
|
|
99
|
+
const prior = parseResumeState(options.resumeInput, plan);
|
|
56
100
|
const completed = [...prior];
|
|
57
101
|
let state = 'complete';
|
|
58
102
|
let currentStep = null;
|
|
@@ -69,13 +113,17 @@ export async function runInvestigationRecipe(recipe, subjectValue, options) {
|
|
|
69
113
|
break;
|
|
70
114
|
}
|
|
71
115
|
const result = await options.execute(step.command, step.arguments);
|
|
116
|
+
const parsedResult = boundedResult(result.stdout);
|
|
117
|
+
if ((result.exitCode === 0 || result.exitCode === 2) && resultSchema(parsedResult) !== step.produces) {
|
|
118
|
+
throw new CliUsageError(`Investigation step ${step.id} returned an unexpected command contract.`);
|
|
119
|
+
}
|
|
72
120
|
completed.push(Object.freeze({
|
|
73
121
|
id: step.id,
|
|
74
122
|
command: step.command,
|
|
75
123
|
arguments: step.arguments,
|
|
76
124
|
mode: step.mode,
|
|
77
125
|
exitCode: result.exitCode,
|
|
78
|
-
result:
|
|
126
|
+
result: parsedResult,
|
|
79
127
|
}));
|
|
80
128
|
if (result.exitCode !== 0 && result.exitCode !== 2) {
|
|
81
129
|
state = 'step_failed';
|