@slicedearth/whoisleuth-cli 2.0.0 → 2.1.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/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +17 -10
- package/docs/cli.md +5 -3
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/project-metadata.mjs +1 -1
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +3 -3
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +227 -63
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/brand-profile-model.mjs +1 -1
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
2
|
+
const DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
|
|
3
|
+
const DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
|
|
4
|
+
export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/u;
|
|
5
|
+
export const SEMANTIC_VERSION_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
6
|
+
export function fail(label) {
|
|
7
|
+
throw new TypeError(`${label} has an unsupported or malformed structure.`);
|
|
8
|
+
}
|
|
9
|
+
export function record(value, label) {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
fail(label);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function exact(value, keys, label) {
|
|
15
|
+
const source = record(value, label);
|
|
16
|
+
const actual = Object.keys(source);
|
|
17
|
+
if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
|
|
18
|
+
fail(label);
|
|
19
|
+
return source;
|
|
20
|
+
}
|
|
21
|
+
export function exactOptional(value, required, optional, label) {
|
|
22
|
+
const source = record(value, label);
|
|
23
|
+
const actual = Object.keys(source);
|
|
24
|
+
if (required.some((key) => !actual.includes(key))
|
|
25
|
+
|| actual.some((key) => !required.includes(key) && !optional.includes(key)))
|
|
26
|
+
fail(label);
|
|
27
|
+
return source;
|
|
28
|
+
}
|
|
29
|
+
export function text(value, label, maximum = 2_000, allowEmpty = false) {
|
|
30
|
+
if (typeof value !== 'string' || value.length > maximum || CONTROL_RE.test(value) || (!allowEmpty && !value))
|
|
31
|
+
fail(label);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function optionalText(value, label, maximum = 2_000) {
|
|
35
|
+
if (value !== null)
|
|
36
|
+
text(value, label, maximum);
|
|
37
|
+
}
|
|
38
|
+
export function iso(value, label, nullable = false) {
|
|
39
|
+
if (nullable && value === null)
|
|
40
|
+
return;
|
|
41
|
+
const candidate = text(value, label, 64);
|
|
42
|
+
const parsed = Date.parse(candidate);
|
|
43
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== candidate)
|
|
44
|
+
fail(label);
|
|
45
|
+
}
|
|
46
|
+
export function integer(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
|
|
47
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum)
|
|
48
|
+
fail(label);
|
|
49
|
+
return Number(value);
|
|
50
|
+
}
|
|
51
|
+
export function boolean(value, label) {
|
|
52
|
+
if (typeof value !== 'boolean')
|
|
53
|
+
fail(label);
|
|
54
|
+
}
|
|
55
|
+
export function enumeration(value, allowed, label) {
|
|
56
|
+
if (typeof value !== 'string' || !allowed.includes(value))
|
|
57
|
+
fail(label);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
export function array(value, label, maximum, minimum = 0) {
|
|
61
|
+
if (!Array.isArray(value) || value.length < minimum || value.length > maximum)
|
|
62
|
+
fail(label);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
export function strings(value, label, maximum, textMaximum = 2_000) {
|
|
66
|
+
const values = array(value, label, maximum);
|
|
67
|
+
for (const item of values)
|
|
68
|
+
text(item, label, textMaximum);
|
|
69
|
+
return values;
|
|
70
|
+
}
|
|
71
|
+
export function digest(value, label) {
|
|
72
|
+
if (typeof value !== 'string' || !DIGEST_RE.test(value))
|
|
73
|
+
fail(label);
|
|
74
|
+
}
|
|
75
|
+
export function sameValues(left, right) {
|
|
76
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
77
|
+
}
|
|
78
|
+
export function domain(value, label) {
|
|
79
|
+
if (typeof value !== 'string' || !DOMAIN_RE.test(value))
|
|
80
|
+
fail(label);
|
|
81
|
+
}
|
|
82
|
+
export function absoluteUrl(value, label, protocols) {
|
|
83
|
+
const candidate = text(value, label, 2_048);
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(candidate);
|
|
86
|
+
if (!protocols.includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
87
|
+
fail(label);
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return fail(label);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function nullableRecord(value, label) {
|
|
95
|
+
if (value !== null)
|
|
96
|
+
record(value, label);
|
|
97
|
+
}
|
|
98
|
+
export function validateIntegrity(value, label, version, currentVersion) {
|
|
99
|
+
if (version !== currentVersion)
|
|
100
|
+
fail(label);
|
|
101
|
+
const integrity = exact(value, ['algorithm', 'canonicalization', 'digestSha256'], label);
|
|
102
|
+
if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2')
|
|
103
|
+
fail(label);
|
|
104
|
+
digest(integrity.digestSha256, label);
|
|
105
|
+
}
|
package/cli/artifact-verify.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
4
|
-
import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./artifact-
|
|
4
|
+
import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./offline-artifact-validation.mjs";
|
|
5
5
|
import { verifyCaseResponsePacketIntegrity, } from "../packages/cases/case-response-packet.mjs";
|
|
6
6
|
import { CASE_PORTABILITY_VERIFIER_DISPATCH, CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_PACKET_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_SCHEMA, } from "../packages/contracts/case-portability.mjs";
|
|
7
7
|
import { ACQUISITION_DECISION_PACKET_SCHEMA, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_MAIL_EXPOSURE_SCHEMA, BULK_REVIEW_MANIFEST_SCHEMA, LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_VERSION, SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS, SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS, SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../packages/contracts/investigation-portability.mjs";
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
3
|
+
import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
|
|
4
|
+
import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
|
|
5
|
+
import { DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, buildDomainControlFlightRecorder, formatDomainControlFlightRecorder, serializeDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
|
|
6
|
+
import { DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, buildDomainControlManifest, formatDomainControlResult, reviewDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
7
|
+
import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, buildCliDomainControlReview, formatCliDomainControlReview, } from "./domain-control-observations.mjs";
|
|
8
|
+
import { serializeDomainControlManifest } from "../packages/evidence/domain-control-runtime.mjs";
|
|
9
|
+
import { MAX_DOMAIN_CONTROL_REVIEW_COMMAND_BYTES } from "../packages/contracts/domain-control-review.mjs";
|
|
10
|
+
import { MAX_CT_EVENT_INPUT_BYTES, buildCtEventFindings, formatCtEventFindings, } from "./ct-event-intake.mjs";
|
|
11
|
+
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
12
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
13
|
+
import { MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, formatExternalObservationMapping, mapExternalObservations, } from "./external-observation-mapping.mjs";
|
|
14
|
+
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
15
|
+
import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
|
|
16
|
+
import { buildOpenAssetModelBridge, formatOpenAssetModelBridge, } from "./open-asset-model-bridge.mjs";
|
|
17
|
+
import { ASSURANCE_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
18
|
+
import { MAX_SHARING_REVIEW_BYTES, buildSharingReview, formatSharingReview, } from "./sharing-review.mjs";
|
|
19
|
+
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
20
|
+
async function runManifestCommand(args, dependencies, context) {
|
|
21
|
+
context.setFailureLabel('Investigation manifest');
|
|
22
|
+
const artifacts = [];
|
|
23
|
+
let totalBytes = 0;
|
|
24
|
+
try {
|
|
25
|
+
for (const source of args.sources) {
|
|
26
|
+
const content = dependencies.readDiffInput
|
|
27
|
+
? await dependencies.readDiffInput(source)
|
|
28
|
+
: await context.readInput(source, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, 'Manifest artefact input');
|
|
29
|
+
totalBytes += Buffer.byteLength(content, 'utf8');
|
|
30
|
+
if (totalBytes > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) {
|
|
31
|
+
throw new CliUsageError(`Manifest artefacts exceed the ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES}-byte combined limit.`);
|
|
32
|
+
}
|
|
33
|
+
artifacts.push({ content });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof CliUsageError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw new CliUsageError(`Could not read manifest artefact input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
40
|
+
}
|
|
41
|
+
let document;
|
|
42
|
+
try {
|
|
43
|
+
document = await buildInvestigationManifest({
|
|
44
|
+
workflow: args.workflow,
|
|
45
|
+
configurationDigestSha256: args.configurationDigestSha256,
|
|
46
|
+
artifacts,
|
|
47
|
+
}, context.now(), context.packageVersion);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Investigation manifest input is invalid'));
|
|
51
|
+
}
|
|
52
|
+
if (!args.quiet) {
|
|
53
|
+
context.writeStdout(args.output === 'json'
|
|
54
|
+
? formatJsonDocument(document)
|
|
55
|
+
: context.terminal(formatInvestigationManifest(document), args.color));
|
|
56
|
+
}
|
|
57
|
+
return EXIT_CODES.SUCCESS;
|
|
58
|
+
}
|
|
59
|
+
async function runObservationBridgeCommand(args, dependencies, context) {
|
|
60
|
+
const mapping = args.action === 'map-observations';
|
|
61
|
+
context.setFailureLabel(mapping ? 'External observation mapping' : 'Open Asset Model export');
|
|
62
|
+
let input;
|
|
63
|
+
try {
|
|
64
|
+
input = dependencies.readArtifactInput
|
|
65
|
+
? await dependencies.readArtifactInput(args.source)
|
|
66
|
+
: await context.readInput(args.source, MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapping ? 'External observation mapping input' : 'Open Asset Model bridge input');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof CliUsageError)
|
|
70
|
+
throw error;
|
|
71
|
+
throw new CliUsageError(`Could not read ${mapping ? 'observation mapping' : 'asset bridge'} input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
72
|
+
}
|
|
73
|
+
if (!input.trim())
|
|
74
|
+
throw new CliUsageError(`${args.action} requires one versioned JSON file or a document on stdin.`);
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
scanBoundedJson(input);
|
|
78
|
+
parsed = JSON.parse(input);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new CliUsageError(`${mapping ? 'External observation mapping' : 'Open Asset Model bridge'} input is not valid bounded JSON without duplicate keys.`);
|
|
82
|
+
}
|
|
83
|
+
let document;
|
|
84
|
+
try {
|
|
85
|
+
document = mapping
|
|
86
|
+
? mapExternalObservations(parsed)
|
|
87
|
+
: buildOpenAssetModelBridge(parsed, context.now());
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new CliUsageError(boundedCliErrorMessage(error, `${mapping ? 'External observation mapping' : 'Open Asset Model bridge'} input is invalid`));
|
|
91
|
+
}
|
|
92
|
+
if (!args.quiet) {
|
|
93
|
+
context.writeStdout(args.output === 'json'
|
|
94
|
+
? formatJsonDocument(document)
|
|
95
|
+
: context.terminal(mapping
|
|
96
|
+
? formatExternalObservationMapping(document)
|
|
97
|
+
: formatOpenAssetModelBridge(document), args.color));
|
|
98
|
+
}
|
|
99
|
+
return EXIT_CODES.SUCCESS;
|
|
100
|
+
}
|
|
101
|
+
async function runDomainControlCommand(args, dependencies, context) {
|
|
102
|
+
context.setFailureLabel('Domain control review');
|
|
103
|
+
let input;
|
|
104
|
+
try {
|
|
105
|
+
input = dependencies.readArtifactInput
|
|
106
|
+
? await dependencies.readArtifactInput(args.source)
|
|
107
|
+
: await context.readInput(args.source, MAX_DOMAIN_CONTROL_REVIEW_COMMAND_BYTES, 'Domain control input');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof CliUsageError)
|
|
111
|
+
throw error;
|
|
112
|
+
throw new CliUsageError(`Could not read domain control input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
113
|
+
}
|
|
114
|
+
if (!input.trim())
|
|
115
|
+
throw new CliUsageError('domain-control requires one JSON file or a document on stdin.');
|
|
116
|
+
const normalisedInput = input.replace(/^\uFEFF/u, '');
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
scanBoundedJson(normalisedInput, {
|
|
120
|
+
maximumDepth: MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH,
|
|
121
|
+
maximumKeys: MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS,
|
|
122
|
+
maximumValues: MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES,
|
|
123
|
+
});
|
|
124
|
+
parsed = JSON.parse(normalisedInput);
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
const detail = cause instanceof Error ? cause.message : '';
|
|
128
|
+
if (detail.startsWith('Artefact JSON ')) {
|
|
129
|
+
throw new CliUsageError(`Domain control input ${detail.slice('Artefact JSON '.length)}`);
|
|
130
|
+
}
|
|
131
|
+
throw new CliUsageError('Domain control input is not valid JSON.');
|
|
132
|
+
}
|
|
133
|
+
const schema = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
134
|
+
? parsed.schema
|
|
135
|
+
: null;
|
|
136
|
+
const document = schema === DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA
|
|
137
|
+
? buildDomainControlManifest(parsed, context.now())
|
|
138
|
+
: schema === DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
139
|
+
? reviewDomainControlManifest(parsed, context.now())
|
|
140
|
+
: schema === CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
141
|
+
? buildCliDomainControlReview(input, context.now())
|
|
142
|
+
: schema === DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
|
|
143
|
+
? buildDomainControlFlightRecorder(parsed, context.now())
|
|
144
|
+
: null;
|
|
145
|
+
if (!document) {
|
|
146
|
+
throw new CliUsageError('Domain control input must use a supported manifest, review, saved-Lookup review, or flight-recorder schema.');
|
|
147
|
+
}
|
|
148
|
+
const terminalDocument = schema === CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
149
|
+
? formatCliDomainControlReview(document)
|
|
150
|
+
: schema === DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
|
|
151
|
+
? formatDomainControlFlightRecorder(document)
|
|
152
|
+
: formatDomainControlResult(document);
|
|
153
|
+
if (!args.quiet) {
|
|
154
|
+
context.writeStdout(args.output === 'json'
|
|
155
|
+
? schema === DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA
|
|
156
|
+
? serializeDomainControlManifest(document)
|
|
157
|
+
: schema === DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
|
|
158
|
+
? serializeDomainControlFlightRecorder(document)
|
|
159
|
+
: formatJsonDocument(document)
|
|
160
|
+
: context.terminal(terminalDocument, args.color));
|
|
161
|
+
}
|
|
162
|
+
return EXIT_CODES.SUCCESS;
|
|
163
|
+
}
|
|
164
|
+
async function runAssuranceReviewCommand(args, dependencies, context) {
|
|
165
|
+
context.setFailureLabel('Domain assurance review');
|
|
166
|
+
let input;
|
|
167
|
+
try {
|
|
168
|
+
input = dependencies.readArtifactInput
|
|
169
|
+
? await dependencies.readArtifactInput(args.source)
|
|
170
|
+
: await context.readInput(args.source, MAX_ASSURANCE_INPUT_BYTES, 'Domain assurance input');
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error instanceof CliUsageError)
|
|
174
|
+
throw error;
|
|
175
|
+
throw new CliUsageError(`Could not read domain assurance input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
176
|
+
}
|
|
177
|
+
if (!input.trim())
|
|
178
|
+
throw new CliUsageError('assurance requires one versioned JSON file or a document on stdin.');
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
scanBoundedJson(input);
|
|
182
|
+
parsed = JSON.parse(input);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new CliUsageError('Domain assurance input is not valid bounded JSON without duplicate keys.');
|
|
186
|
+
}
|
|
187
|
+
let document;
|
|
188
|
+
try {
|
|
189
|
+
document = buildDomainAssurance(parsed, context.now());
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Domain assurance input is invalid'));
|
|
193
|
+
}
|
|
194
|
+
if (!args.quiet) {
|
|
195
|
+
context.writeStdout(args.output === 'json'
|
|
196
|
+
? formatJsonDocument(document)
|
|
197
|
+
: context.terminal(formatDomainAssurance(document), args.color));
|
|
198
|
+
}
|
|
199
|
+
return EXIT_CODES.SUCCESS;
|
|
200
|
+
}
|
|
201
|
+
async function runChangePacketCommand(args, dependencies, context) {
|
|
202
|
+
context.setFailureLabel('Domain change packet');
|
|
203
|
+
let input;
|
|
204
|
+
try {
|
|
205
|
+
input = dependencies.readArtifactInput
|
|
206
|
+
? await dependencies.readArtifactInput(args.source)
|
|
207
|
+
: await context.readInput(args.source, MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, 'Domain change packet input');
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error instanceof CliUsageError)
|
|
211
|
+
throw error;
|
|
212
|
+
throw new CliUsageError(`Could not read domain change packet input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
213
|
+
}
|
|
214
|
+
if (!input.trim())
|
|
215
|
+
throw new CliUsageError('change-packet requires one versioned JSON file or a document on stdin.');
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
scanBoundedJson(input);
|
|
219
|
+
parsed = JSON.parse(input);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new CliUsageError('Domain change packet input is not valid bounded JSON without duplicate keys.');
|
|
223
|
+
}
|
|
224
|
+
let document;
|
|
225
|
+
try {
|
|
226
|
+
document = await buildDomainChangePacket(parsed, context.now());
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Domain change packet input is invalid'));
|
|
230
|
+
}
|
|
231
|
+
if (!args.quiet) {
|
|
232
|
+
context.writeStdout(args.output === 'json'
|
|
233
|
+
? formatJsonDocument(document)
|
|
234
|
+
: context.terminal(formatDomainChangePacket(document), args.color));
|
|
235
|
+
}
|
|
236
|
+
return document.gate.pass ? EXIT_CODES.SUCCESS : EXIT_CODES.PARTIAL_FAILURE;
|
|
237
|
+
}
|
|
238
|
+
async function runSharingReviewCommand(args, dependencies, context) {
|
|
239
|
+
context.setFailureLabel('Evidence sharing review');
|
|
240
|
+
let input;
|
|
241
|
+
try {
|
|
242
|
+
input = dependencies.readArtifactInput
|
|
243
|
+
? await dependencies.readArtifactInput(args.source)
|
|
244
|
+
: await context.readInput(args.source, MAX_SHARING_REVIEW_BYTES, 'Sharing review input');
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error instanceof CliUsageError)
|
|
248
|
+
throw error;
|
|
249
|
+
throw new CliUsageError(`Could not read sharing review input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
250
|
+
}
|
|
251
|
+
if (!input.trim())
|
|
252
|
+
throw new CliUsageError('sharing-review requires one artefact JSON file or a document on stdin.');
|
|
253
|
+
let document;
|
|
254
|
+
try {
|
|
255
|
+
document = await buildSharingReview(input, {
|
|
256
|
+
marking: args.marking,
|
|
257
|
+
recipientScope: args.recipientScope,
|
|
258
|
+
purpose: args.purpose,
|
|
259
|
+
humanReviewed: args.humanReviewed,
|
|
260
|
+
personalDataReviewed: args.personalDataReviewed,
|
|
261
|
+
redactionsConfirmed: args.redactionsConfirmed,
|
|
262
|
+
}, context.now());
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Sharing review input is invalid'));
|
|
266
|
+
}
|
|
267
|
+
if (!args.quiet) {
|
|
268
|
+
context.writeStdout(args.output === 'json'
|
|
269
|
+
? formatJsonDocument(document)
|
|
270
|
+
: context.terminal(formatSharingReview(document), args.color));
|
|
271
|
+
}
|
|
272
|
+
return document.summary.status === 'blocked' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
273
|
+
}
|
|
274
|
+
async function runCertificateIntakeCommand(args, dependencies, context) {
|
|
275
|
+
context.setFailureLabel('Certificate event intake');
|
|
276
|
+
let input;
|
|
277
|
+
try {
|
|
278
|
+
input = dependencies.readArtifactInput
|
|
279
|
+
? await dependencies.readArtifactInput(args.source)
|
|
280
|
+
: await context.readInput(args.source, MAX_CT_EVENT_INPUT_BYTES, 'Certificate event input');
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (error instanceof CliUsageError)
|
|
284
|
+
throw error;
|
|
285
|
+
throw new CliUsageError(`Could not read certificate event input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
286
|
+
}
|
|
287
|
+
if (!input.trim())
|
|
288
|
+
throw new CliUsageError('ct-intake requires one versioned JSON file or a document on stdin.');
|
|
289
|
+
let parsed;
|
|
290
|
+
try {
|
|
291
|
+
scanBoundedJson(input);
|
|
292
|
+
parsed = JSON.parse(input);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
throw new CliUsageError('Certificate event input is not valid bounded JSON without duplicate keys.');
|
|
296
|
+
}
|
|
297
|
+
let document;
|
|
298
|
+
try {
|
|
299
|
+
document = buildCtEventFindings(parsed);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Certificate event input is invalid'));
|
|
303
|
+
}
|
|
304
|
+
if (!args.quiet) {
|
|
305
|
+
context.writeStdout(args.output === 'json'
|
|
306
|
+
? formatJsonDocument(document)
|
|
307
|
+
: context.terminal(formatCtEventFindings(document), args.color));
|
|
308
|
+
}
|
|
309
|
+
return EXIT_CODES.SUCCESS;
|
|
310
|
+
}
|
|
311
|
+
const ASSURANCE_COMMAND_HANDLERS = Object.freeze({
|
|
312
|
+
'manifest': runManifestCommand,
|
|
313
|
+
'map-observations': runObservationBridgeCommand,
|
|
314
|
+
'oam-export': runObservationBridgeCommand,
|
|
315
|
+
'domain-control': runDomainControlCommand,
|
|
316
|
+
'assurance': runAssuranceReviewCommand,
|
|
317
|
+
'change-packet': runChangePacketCommand,
|
|
318
|
+
'sharing-review': runSharingReviewCommand,
|
|
319
|
+
'ct-intake': runCertificateIntakeCommand,
|
|
320
|
+
});
|
|
321
|
+
function runAssuranceCommand(args, dependencies, context) {
|
|
322
|
+
return runDiscriminatedCommandHandler(ASSURANCE_COMMAND_HANDLERS, args, dependencies, context);
|
|
323
|
+
}
|
|
324
|
+
export { ASSURANCE_COMMAND_HANDLERS, runAssuranceCommand };
|