@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/input.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { decodeBoundedUtf8, 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
|
+
try {
|
|
53
|
+
return decodeBoundedUtf8(Buffer.concat(chunks, bytes), options.label);
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : `${options.label} must contain valid UTF-8 text.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { classifyQuery, isDirectLookupTarget } from "../lib/classify.mjs";
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
import { boundedCliErrorMessage, CliUsageError, hasUnsafeCliText } from "./errors.mjs";
|
|
4
|
+
import { INVESTIGATION_PLAN_RECIPE_LABELS, INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
|
|
5
|
+
import { buildCliLookupPlan, formatCliLookupPlan } from "./lookup-plan.mjs";
|
|
6
|
+
import { canBrowseLookup, } from "./lookup-browser.mjs";
|
|
7
|
+
const MAX_INTERACTIVE_ANSWER_SCALARS = 1_024;
|
|
8
|
+
const MAX_INTERACTIVE_ANSWER_BYTES = 4_096;
|
|
9
|
+
function canLaunchInteractiveCli(input, output, environment = process.env) {
|
|
10
|
+
return input?.isTTY === true
|
|
11
|
+
&& canBrowseLookup(input, output, environment);
|
|
12
|
+
}
|
|
13
|
+
function boundedInteractiveAnswer(value) {
|
|
14
|
+
const supplied = typeof value === 'string' ? value : '';
|
|
15
|
+
if (Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES
|
|
16
|
+
|| Array.from(supplied).length > MAX_INTERACTIVE_ANSWER_SCALARS
|
|
17
|
+
|| hasUnsafeCliText(supplied)) {
|
|
18
|
+
throw new CliUsageError('Interactive input must be bounded text without control characters.');
|
|
19
|
+
}
|
|
20
|
+
return supplied.trim();
|
|
21
|
+
}
|
|
22
|
+
function abortError() {
|
|
23
|
+
return new DOMException('Aborted', 'AbortError');
|
|
24
|
+
}
|
|
25
|
+
function readBoundedInteractiveLine(prompt, options) {
|
|
26
|
+
const { input, output, signal } = options;
|
|
27
|
+
if (signal?.aborted)
|
|
28
|
+
return Promise.reject(signal.reason || abortError());
|
|
29
|
+
const previousRaw = input.isRaw === true;
|
|
30
|
+
const wasPaused = input.isPaused?.() === true;
|
|
31
|
+
let receivedBytes = 0;
|
|
32
|
+
let receivedScalars = 0;
|
|
33
|
+
let settled = false;
|
|
34
|
+
let characters = [];
|
|
35
|
+
const decoder = new StringDecoder('utf8');
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const cleanup = () => {
|
|
38
|
+
let failure = null;
|
|
39
|
+
const attempt = (operation) => {
|
|
40
|
+
try {
|
|
41
|
+
operation();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
failure ||= error instanceof Error ? error : new Error('Interactive terminal cleanup failed.');
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
attempt(() => input.off?.('data', onData));
|
|
48
|
+
attempt(() => input.off?.('end', onEnd));
|
|
49
|
+
attempt(() => signal?.removeEventListener('abort', onAbort));
|
|
50
|
+
attempt(() => input.setRawMode?.(previousRaw));
|
|
51
|
+
if (wasPaused)
|
|
52
|
+
attempt(() => input.pause?.());
|
|
53
|
+
return failure;
|
|
54
|
+
};
|
|
55
|
+
const finish = (value, error) => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
const cleanupFailure = cleanup();
|
|
60
|
+
characters = [];
|
|
61
|
+
if (error && cleanupFailure) {
|
|
62
|
+
reject(new AggregateError([error, cleanupFailure], `${error.message}; terminal cleanup also failed: ${cleanupFailure.message}`));
|
|
63
|
+
}
|
|
64
|
+
else if (error || cleanupFailure)
|
|
65
|
+
reject(error || cleanupFailure);
|
|
66
|
+
else
|
|
67
|
+
resolve(value || '');
|
|
68
|
+
};
|
|
69
|
+
const rejectUsage = () => finish(undefined, new CliUsageError('Interactive input must be bounded text without control characters.'));
|
|
70
|
+
const onData = (chunk) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
let rawChunk;
|
|
74
|
+
if (Buffer.isBuffer(chunk)) {
|
|
75
|
+
if (chunk.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
|
|
76
|
+
rejectUsage();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
rawChunk = chunk;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const supplied = String(chunk ?? '');
|
|
83
|
+
if (supplied.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes
|
|
84
|
+
|| Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
|
|
85
|
+
rejectUsage();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
rawChunk = Buffer.from(supplied, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
receivedBytes += rawChunk.length;
|
|
91
|
+
const value = decoder.write(rawChunk);
|
|
92
|
+
receivedScalars += Array.from(value).length;
|
|
93
|
+
if (receivedBytes > MAX_INTERACTIVE_ANSWER_BYTES
|
|
94
|
+
|| receivedScalars > MAX_INTERACTIVE_ANSWER_SCALARS
|
|
95
|
+
|| value.includes('\ufffd')) {
|
|
96
|
+
rejectUsage();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const character of value) {
|
|
100
|
+
if (character === '\r' || character === '\n') {
|
|
101
|
+
try {
|
|
102
|
+
output.write('\n');
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
finish(characters.join(''));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (character === '\u0003' || character === '\u0004' || character === '\u001b') {
|
|
112
|
+
finish(undefined, abortError());
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (character === '\u0008' || character === '\u007f') {
|
|
116
|
+
if (characters.length) {
|
|
117
|
+
characters.pop();
|
|
118
|
+
try {
|
|
119
|
+
output.write('\b \b');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (hasUnsafeCliText(character)) {
|
|
129
|
+
rejectUsage();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
characters.push(character);
|
|
133
|
+
try {
|
|
134
|
+
output.write(character);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const onEnd = () => {
|
|
143
|
+
const remainder = decoder.end();
|
|
144
|
+
if (remainder)
|
|
145
|
+
rejectUsage();
|
|
146
|
+
else
|
|
147
|
+
finish(undefined, abortError());
|
|
148
|
+
};
|
|
149
|
+
const onAbort = () => finish(undefined, signal?.reason instanceof Error ? signal.reason : abortError());
|
|
150
|
+
try {
|
|
151
|
+
output.write(prompt);
|
|
152
|
+
input.on?.('data', onData);
|
|
153
|
+
input.on?.('end', onEnd);
|
|
154
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
155
|
+
if (signal?.aborted) {
|
|
156
|
+
onAbort();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
input.setRawMode?.(true);
|
|
160
|
+
if (settled)
|
|
161
|
+
return;
|
|
162
|
+
input.resume?.();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
finish(undefined, error instanceof Error ? error : new Error('Interactive terminal input failed.'));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function launchInteractiveCli(options) {
|
|
170
|
+
const environment = options.environment || process.env;
|
|
171
|
+
if (!canLaunchInteractiveCli(options.input, options.output, environment)) {
|
|
172
|
+
throw new CliUsageError('Interactive launch requires terminal input and output.');
|
|
173
|
+
}
|
|
174
|
+
if (options.signal?.aborted)
|
|
175
|
+
throw options.signal.reason || new DOMException('Aborted', 'AbortError');
|
|
176
|
+
const question = options.question || ((prompt) => readBoundedInteractiveLine(prompt, options));
|
|
177
|
+
const ask = async (prompt) => boundedInteractiveAnswer(await question(prompt));
|
|
178
|
+
const write = (value) => options.output.write(value);
|
|
179
|
+
write([
|
|
180
|
+
'WHOISleuth interactive launch',
|
|
181
|
+
'',
|
|
182
|
+
' 1 Fast Lookup',
|
|
183
|
+
' 2 Deep Lookup',
|
|
184
|
+
' 3 Build an investigation workflow plan (offline)',
|
|
185
|
+
' 4 List commands',
|
|
186
|
+
' q Exit',
|
|
187
|
+
'',
|
|
188
|
+
].join('\n'));
|
|
189
|
+
const selection = (await ask('Select: ')).toLowerCase();
|
|
190
|
+
if (selection === 'q' || selection === 'quit' || selection === 'exit' || selection === '')
|
|
191
|
+
return null;
|
|
192
|
+
if (selection === '4')
|
|
193
|
+
return ['commands'];
|
|
194
|
+
if (selection === '3') {
|
|
195
|
+
write(`\n${INVESTIGATION_PLAN_RECIPES.map((recipe, index) => ` ${index + 1} ${INVESTIGATION_PLAN_RECIPE_LABELS[recipe]}`).join('\n')}\n\n`);
|
|
196
|
+
const recipeSelection = await ask('Workflow: ');
|
|
197
|
+
const recipe = INVESTIGATION_PLAN_RECIPES[Number(recipeSelection) - 1];
|
|
198
|
+
if (!recipe)
|
|
199
|
+
throw new CliUsageError('Choose one listed workflow number.');
|
|
200
|
+
const subject = await ask('Domain or brand subject: ');
|
|
201
|
+
if (!subject)
|
|
202
|
+
throw new CliUsageError('A workflow plan requires one domain or brand subject.');
|
|
203
|
+
return ['workflow-plan', recipe, subject];
|
|
204
|
+
}
|
|
205
|
+
if (selection !== '1' && selection !== '2')
|
|
206
|
+
throw new CliUsageError('Choose 1, 2, 3, 4, or q.');
|
|
207
|
+
const query = await ask('Domain, IP address, or ASN: ');
|
|
208
|
+
if (!isDirectLookupTarget(query)) {
|
|
209
|
+
throw new CliUsageError('Interactive Lookup requires an unambiguous domain with a recognised public suffix, public IP, or ASN.');
|
|
210
|
+
}
|
|
211
|
+
const deep = selection === '2';
|
|
212
|
+
let classified;
|
|
213
|
+
try {
|
|
214
|
+
classified = classifyQuery(query);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid Lookup target'));
|
|
218
|
+
}
|
|
219
|
+
const plan = buildCliLookupPlan(query, classified, deep);
|
|
220
|
+
write(`\n${formatCliLookupPlan(plan)}\n`);
|
|
221
|
+
const confirmed = (await ask('Start this collection and open the evidence browser? [y/N] ')).toLowerCase();
|
|
222
|
+
if (confirmed !== 'y' && confirmed !== 'yes')
|
|
223
|
+
return null;
|
|
224
|
+
return ['lookup', query, deep ? '--deep' : '--fast', '--browse'];
|
|
225
|
+
}
|
|
226
|
+
export { MAX_INTERACTIVE_ANSWER_BYTES, MAX_INTERACTIVE_ANSWER_SCALARS, boundedInteractiveAnswer, canLaunchInteractiveCli, launchInteractiveCli, readBoundedInteractiveLine, };
|
|
@@ -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 "../packages/workspace/brand-profile-model.mjs";
|
|
5
|
+
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
6
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
7
|
+
export const INTERCHANGE_FIDELITY_REPORT_SCHEMA = 'whoisleuth.interchange-fidelity-report';
|
|
8
|
+
export const INTERCHANGE_FIDELITY_REPORT_VERSION = 2;
|
|
9
|
+
export const MAX_INTERCHANGE_REPORT_BYTES = 15 * 1024 * 1024;
|
|
10
|
+
function record(value) {
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
function parseInput(raw) {
|
|
16
|
+
if (typeof raw !== 'string')
|
|
17
|
+
throw new TypeError('Interchange input must be UTF-8 JSON text.');
|
|
18
|
+
const bytes = Buffer.byteLength(raw, 'utf8');
|
|
19
|
+
if (bytes < 1 || bytes > MAX_INTERCHANGE_REPORT_BYTES) {
|
|
20
|
+
throw new TypeError(`Interchange input must be between 1 byte and ${MAX_INTERCHANGE_REPORT_BYTES} bytes.`);
|
|
21
|
+
}
|
|
22
|
+
return parseBoundedJsonObject(raw.replace(/^\uFEFF/u, ''), {
|
|
23
|
+
label: 'Interchange input',
|
|
24
|
+
maximumBytes: MAX_INTERCHANGE_REPORT_BYTES,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function nestedRecord(value, path) {
|
|
28
|
+
let current = value;
|
|
29
|
+
for (const part of path)
|
|
30
|
+
current = record(current?.[part]);
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
function identify(value) {
|
|
34
|
+
for (const contract of INTERCHANGE_ARTIFACT_CONTRACTS) {
|
|
35
|
+
const container = nestedRecord(value, contract.nestedSchemaPath);
|
|
36
|
+
if (container?.schema !== contract.schema)
|
|
37
|
+
continue;
|
|
38
|
+
const declared = container[contract.versionField];
|
|
39
|
+
return {
|
|
40
|
+
contract,
|
|
41
|
+
version: Number.isSafeInteger(declared) && Number(declared) > 0 && Number(declared) <= 1000
|
|
42
|
+
? Number(declared)
|
|
43
|
+
: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function timestamp(value) {
|
|
49
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
50
|
+
if (!normalized)
|
|
51
|
+
throw new TypeError('Interchange report time is invalid.');
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
function validateBrandProfileExport(value, version) {
|
|
55
|
+
const profiles = value.profiles;
|
|
56
|
+
if (!Array.isArray(profiles) || profiles.length > 100) {
|
|
57
|
+
throw new TypeError('Brand Profile export does not contain a bounded profiles collection.');
|
|
58
|
+
}
|
|
59
|
+
const exportedAt = timestamp(value.exportedAt);
|
|
60
|
+
let nextId = 0;
|
|
61
|
+
const result = mergeBrandProfiles([], value, {
|
|
62
|
+
nowIso: exportedAt,
|
|
63
|
+
makeId: () => `interchange-profile-${++nextId}`,
|
|
64
|
+
});
|
|
65
|
+
return Object.freeze({ accepted: result.profiles.length, skipped: result.skipped });
|
|
66
|
+
}
|
|
67
|
+
export async function buildInterchangeFidelityReport(raw, options = {}) {
|
|
68
|
+
const inputBytes = Buffer.byteLength(raw, 'utf8');
|
|
69
|
+
const value = parseInput(raw);
|
|
70
|
+
const identified = identify(value);
|
|
71
|
+
const generatedAt = timestamp(options.generatedAt ?? new Date().toISOString());
|
|
72
|
+
if (!identified)
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
schema: INTERCHANGE_FIDELITY_REPORT_SCHEMA,
|
|
75
|
+
version: INTERCHANGE_FIDELITY_REPORT_VERSION,
|
|
76
|
+
generatedAt,
|
|
77
|
+
recognised: false,
|
|
78
|
+
artifact: Object.freeze({ id: null, schema: null, version: null, versionSupported: false }),
|
|
79
|
+
verification: Object.freeze({ state: 'not_verified', requiredAssurance: null, assuranceSatisfied: false }),
|
|
80
|
+
compatibility: Object.freeze({
|
|
81
|
+
browser: null,
|
|
82
|
+
cli: null,
|
|
83
|
+
fidelity: 'not_verified',
|
|
84
|
+
preservedFieldGroups: Object.freeze([]),
|
|
85
|
+
excludedFieldGroups: Object.freeze([]),
|
|
86
|
+
futureVersionBehaviour: null,
|
|
87
|
+
fullyImportable: null,
|
|
88
|
+
}),
|
|
89
|
+
summary: Object.freeze({
|
|
90
|
+
inputBytes, recordCount: null, sectionCount: null, encryptedContentVerified: null,
|
|
91
|
+
acceptedRecordCount: null, skippedRecordCount: null, unsupportedSectionCount: null,
|
|
92
|
+
blockedSectionCount: null, prunedRecordCount: null,
|
|
93
|
+
}),
|
|
94
|
+
limitations: Object.freeze([
|
|
95
|
+
'The file did not match a registered interchange contract. Its supplied schema text and contents are not echoed.',
|
|
96
|
+
'This report makes no request and does not establish that evidence is accurate, current, complete, or safe to share.',
|
|
97
|
+
]),
|
|
98
|
+
});
|
|
99
|
+
const { contract, version } = identified;
|
|
100
|
+
const supported = version !== null && contract.versions.includes(version);
|
|
101
|
+
let verificationState = supported ? 'not_verified' : 'unsupported_version';
|
|
102
|
+
let assuranceSatisfied = false;
|
|
103
|
+
let recordCount = null;
|
|
104
|
+
let sectionCount = null;
|
|
105
|
+
let encryptedContentVerified = contract.id === 'encrypted_workspace' ? false : null;
|
|
106
|
+
let acceptedRecordCount = null;
|
|
107
|
+
let skippedRecordCount = null;
|
|
108
|
+
let unsupportedSectionCount = null;
|
|
109
|
+
let blockedSectionCount = null;
|
|
110
|
+
let prunedRecordCount = null;
|
|
111
|
+
let fullyImportable = null;
|
|
112
|
+
if (supported && contract.fidelity !== 'unsupported') {
|
|
113
|
+
try {
|
|
114
|
+
if (contract.id === 'brand_profiles') {
|
|
115
|
+
if (version === null)
|
|
116
|
+
throw new TypeError('Brand Profile export version is invalid.');
|
|
117
|
+
const validation = validateBrandProfileExport(value, version);
|
|
118
|
+
recordCount = validation.accepted;
|
|
119
|
+
acceptedRecordCount = validation.accepted;
|
|
120
|
+
skippedRecordCount = validation.skipped;
|
|
121
|
+
verificationState = validation.skipped === 0 ? 'structure_valid' : 'not_verified';
|
|
122
|
+
assuranceSatisfied = validation.skipped === 0 && contract.requiredAssurance === 'structure';
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const verification = await verifyOfflineArtifact(raw, { passphrase: options.passphrase ?? null });
|
|
126
|
+
verificationState = verification.state;
|
|
127
|
+
assuranceSatisfied = contract.requiredAssurance === 'whole_integrity'
|
|
128
|
+
? offlineArtifactSatisfiesAssurance(verification, 'whole_integrity')
|
|
129
|
+
: contract.requiredAssurance === 'applicable_integrity'
|
|
130
|
+
? offlineArtifactSatisfiesAssurance(verification, 'applicable_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 "../packages/evidence/artifact-integrity.mjs";
|
|
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) {
|