@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
|
@@ -1,49 +1,222 @@
|
|
|
1
1
|
import { classifyQuery } from "../lib/classify.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
3
|
+
import { buildDomainControlFlightRecorder, validateDomainControlFlightRecorderDocument, } from "../lib/domain-control-flight-recorder.mjs";
|
|
4
|
+
import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, } from "../packages/contracts/domain-control-flight-recorder.mjs";
|
|
5
|
+
import { DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, } from "../packages/contracts/domain-control-review.mjs";
|
|
6
|
+
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
|
|
7
|
+
import { validateDomainControlReviewDocument, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
8
|
+
import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
9
|
import { buildCliLookupDocument } from "./formatters/json.mjs";
|
|
5
10
|
import { buildCliDomainControlReview, } from "./domain-control-observations.mjs";
|
|
6
|
-
import {
|
|
7
|
-
export
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (
|
|
11
|
+
import { CliUsageError } from "./errors.mjs";
|
|
12
|
+
export { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION };
|
|
13
|
+
export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS;
|
|
14
|
+
function parseMonitorJson(input, label) {
|
|
15
|
+
const normalized = input.replace(/^\uFEFF/u, '');
|
|
16
|
+
if (Buffer.byteLength(input, 'utf8') > MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES) {
|
|
17
|
+
throw new CliUsageError(`${label} exceeds the ${MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES}-byte limit.`);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
scanBoundedJson(normalized, {
|
|
21
|
+
maximumDepth: MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH,
|
|
22
|
+
maximumKeys: MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS,
|
|
23
|
+
maximumValues: MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES,
|
|
24
|
+
maximumContainerItems: MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS,
|
|
25
|
+
});
|
|
26
|
+
return JSON.parse(normalized);
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
const detail = cause instanceof Error ? cause.message : '';
|
|
30
|
+
if (detail === 'Artefact input is not valid JSON.') {
|
|
31
|
+
throw new CliUsageError(`${label} must be valid JSON.`);
|
|
32
|
+
}
|
|
33
|
+
if (detail.startsWith('Artefact JSON ')) {
|
|
34
|
+
throw new CliUsageError(`${label} ${detail.slice('Artefact JSON '.length)}`);
|
|
35
|
+
}
|
|
36
|
+
throw new CliUsageError(`${label} must satisfy the bounded JSON structure contract.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function exactMonitorRecord(value, keys, label) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
41
|
+
throw new CliUsageError(`${label} must use its exact object fields.`);
|
|
42
|
+
}
|
|
43
|
+
const record = value;
|
|
44
|
+
const actualKeys = Object.keys(record);
|
|
45
|
+
if (actualKeys.length !== keys.length || actualKeys.some((key) => !keys.includes(key))) {
|
|
46
|
+
throw new CliUsageError(`${label} must use its exact object fields.`);
|
|
47
|
+
}
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
function monitorInteger(value, minimum, maximum, label) {
|
|
51
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
|
|
52
|
+
throw new CliUsageError(`${label} must be an integer from ${minimum} to ${maximum}.`);
|
|
53
|
+
}
|
|
54
|
+
return Number(value);
|
|
55
|
+
}
|
|
56
|
+
function previousSnapshot(input, currentGeneratedAt) {
|
|
57
|
+
if (input === null)
|
|
12
58
|
return { observations: [] };
|
|
13
|
-
|
|
59
|
+
const parsed = parseMonitorJson(input, 'Previous monitor snapshot');
|
|
60
|
+
const root = exactMonitorRecord(parsed, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, 'Previous monitor snapshot');
|
|
61
|
+
if (root.schema !== CLI_DOMAIN_CONTROL_MONITOR_SCHEMA || root.version !== CLI_DOMAIN_CONTROL_MONITOR_VERSION) {
|
|
62
|
+
throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} version ${CLI_DOMAIN_CONTROL_MONITOR_VERSION}.`);
|
|
63
|
+
}
|
|
64
|
+
let previousGeneratedAt;
|
|
14
65
|
try {
|
|
15
|
-
|
|
66
|
+
previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
|
|
16
67
|
}
|
|
17
68
|
catch {
|
|
18
|
-
throw new CliUsageError('Previous monitor snapshot must be valid
|
|
69
|
+
throw new CliUsageError('Previous monitor snapshot generatedAt must be a valid ISO 8601 timestamp.');
|
|
19
70
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} version ${CLI_DOMAIN_CONTROL_MONITOR_VERSION}.`);
|
|
71
|
+
if (Date.parse(previousGeneratedAt) >= Date.parse(currentGeneratedAt)) {
|
|
72
|
+
throw new CliUsageError('Previous monitor snapshot must precede the current monitor run.');
|
|
23
73
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
74
|
+
const manifest = exactMonitorRecord(root.manifest, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, 'Previous monitor snapshot manifest');
|
|
75
|
+
if (typeof manifest.digestSha256 !== 'string'
|
|
76
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(manifest.digestSha256)
|
|
77
|
+
|| typeof manifest.expiresAt !== 'string') {
|
|
78
|
+
throw new CliUsageError('Previous monitor snapshot manifest is invalid.');
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
requireIsoTimestamp(manifest.expiresAt, 'Previous monitor snapshot manifest expiresAt');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new CliUsageError('Previous monitor snapshot manifest expiresAt must be a valid ISO 8601 timestamp.');
|
|
85
|
+
}
|
|
86
|
+
if (Date.parse(manifest.expiresAt) <= Date.parse(previousGeneratedAt)) {
|
|
87
|
+
throw new CliUsageError('Previous monitor snapshot manifest must have been unexpired when the checkpoint was generated.');
|
|
88
|
+
}
|
|
89
|
+
const collection = exactMonitorRecord(root.collection, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, 'Previous monitor snapshot collection');
|
|
90
|
+
const requested = monitorInteger(collection.requested, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, 'Previous monitor snapshot requested count');
|
|
91
|
+
const succeeded = monitorInteger(collection.succeeded, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, 'Previous monitor snapshot succeeded count');
|
|
92
|
+
const failed = monitorInteger(collection.failed, 0, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, 'Previous monitor snapshot failed count');
|
|
93
|
+
if (requested !== succeeded + failed || !Array.isArray(collection.failures) || collection.failures.length !== failed) {
|
|
94
|
+
throw new CliUsageError('Previous monitor snapshot collection counts are inconsistent.');
|
|
95
|
+
}
|
|
96
|
+
const failureDomains = new Set();
|
|
97
|
+
const failureDomainOrder = [];
|
|
98
|
+
for (const [index, failureValue] of collection.failures.entries()) {
|
|
99
|
+
const failure = exactMonitorRecord(failureValue, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, `Previous monitor snapshot failure ${index + 1}`);
|
|
100
|
+
let failureDomain;
|
|
101
|
+
try {
|
|
102
|
+
failureDomain = requireDomainName(failure.domain, `Previous monitor snapshot failure ${index + 1} domain`);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new CliUsageError(`Previous monitor snapshot failure ${index + 1} domain is invalid.`);
|
|
106
|
+
}
|
|
107
|
+
if (failureDomain !== failure.domain
|
|
108
|
+
|| failureDomains.has(failureDomain)
|
|
109
|
+
|| typeof failure.error !== 'string'
|
|
110
|
+
|| !DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES.includes(failure.error)
|
|
111
|
+
|| failure.error.length > MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH) {
|
|
112
|
+
throw new CliUsageError(`Previous monitor snapshot failure ${index + 1} is invalid.`);
|
|
113
|
+
}
|
|
114
|
+
failureDomains.add(failureDomain);
|
|
115
|
+
failureDomainOrder.push(failureDomain);
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(root.observations)
|
|
118
|
+
|| root.observations.length !== succeeded
|
|
119
|
+
|| root.observations.length < MIN_DOMAIN_CONTROL_MONITOR_DOMAINS
|
|
120
|
+
|| root.observations.length > MAX_DOMAIN_CONTROL_MONITOR_DOMAINS) {
|
|
121
|
+
throw new CliUsageError('Previous monitor snapshot observations are inconsistent with its collection summary.');
|
|
122
|
+
}
|
|
123
|
+
let review;
|
|
124
|
+
let flightRecorder;
|
|
125
|
+
try {
|
|
126
|
+
review = validateDomainControlReviewDocument(root.review);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new CliUsageError(`Previous monitor snapshot review must use the exact ${DOMAIN_CONTROL_REVIEW_SCHEMA} version ${DOMAIN_CONTROL_REVIEW_VERSION} contract.`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
flightRecorder = validateDomainControlFlightRecorderDocument(root.flightRecorder);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new CliUsageError(`Previous monitor snapshot flight recorder must use the exact ${DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA} version ${DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION} contract.`);
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(root.limitations)
|
|
138
|
+
|| root.limitations.length !== DOMAIN_CONTROL_MONITOR_LIMITATIONS.length
|
|
139
|
+
|| root.limitations.some((value, index) => value !== DOMAIN_CONTROL_MONITOR_LIMITATIONS[index])) {
|
|
140
|
+
throw new CliUsageError('Previous monitor snapshot limitations are invalid.');
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
buildDomainControlFlightRecorder({
|
|
144
|
+
schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
|
|
145
|
+
version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
|
|
146
|
+
observations: root.observations,
|
|
147
|
+
approvedWindows: [],
|
|
148
|
+
}, previousGeneratedAt);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw new CliUsageError('Previous monitor snapshot observations must use the exact bounded flight-recorder input contract.');
|
|
152
|
+
}
|
|
153
|
+
const observations = root.observations;
|
|
154
|
+
const observationDomainOrder = observations.map((observation) => observation.domain);
|
|
155
|
+
const observationDomains = new Set(observationDomainOrder);
|
|
156
|
+
const requestedDomains = review.domains.slice(0, requested).map((item) => item.domain);
|
|
157
|
+
const expectedObservationDomains = requestedDomains.filter((domain) => !failureDomains.has(domain));
|
|
158
|
+
const expectedFailureDomains = requestedDomains.filter((domain) => !observationDomains.has(domain));
|
|
159
|
+
if (review.domains.length < requested
|
|
160
|
+
|| observationDomains.size !== observations.length
|
|
161
|
+
|| observationDomainOrder.some((domain, index) => domain !== expectedObservationDomains[index])
|
|
162
|
+
|| observationDomainOrder.length !== expectedObservationDomains.length
|
|
163
|
+
|| failureDomainOrder.some((domain, index) => domain !== expectedFailureDomains[index])
|
|
164
|
+
|| failureDomainOrder.length !== expectedFailureDomains.length
|
|
165
|
+
|| observations.some((observation) => observation.observedAt !== previousGeneratedAt)
|
|
166
|
+
|| review.domains.some((item) => item.comparisons.some((comparison) => comparison.observedAt !== null && comparison.observedAt !== previousGeneratedAt))
|
|
167
|
+
|| review.generatedAt !== previousGeneratedAt
|
|
168
|
+
|| review.manifest.digestSha256 !== manifest.digestSha256
|
|
169
|
+
|| review.manifest.expiresAt !== manifest.expiresAt
|
|
170
|
+
|| [...observationDomains].some((domain) => !review.domains.some((item) => item.domain === domain))
|
|
171
|
+
|| flightRecorder.generatedAt !== previousGeneratedAt
|
|
172
|
+
|| flightRecorder.observationCount < observations.length
|
|
173
|
+
|| [...observationDomains].some((domain) => !flightRecorder.domains.includes(domain))) {
|
|
174
|
+
throw new CliUsageError('Previous monitor snapshot embedded documents are inconsistent with its observation projection.');
|
|
175
|
+
}
|
|
176
|
+
return { observations };
|
|
177
|
+
}
|
|
178
|
+
function monitorFailureMessage() {
|
|
179
|
+
return DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES[0];
|
|
29
180
|
}
|
|
30
181
|
export async function runDomainControlMonitor(manifestInput, previousInput, options) {
|
|
31
|
-
|
|
32
|
-
|
|
182
|
+
if (!Number.isSafeInteger(options.limit)
|
|
183
|
+
|| options.limit < MIN_DOMAIN_CONTROL_MONITOR_DOMAINS
|
|
184
|
+
|| options.limit > MAX_DOMAIN_CONTROL_MONITOR_DOMAINS) {
|
|
185
|
+
throw new CliUsageError(`Domain-control monitor limit must be from ${MIN_DOMAIN_CONTROL_MONITOR_DOMAINS} to ${MAX_DOMAIN_CONTROL_MONITOR_DOMAINS}.`);
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isSafeInteger(options.concurrency)
|
|
188
|
+
|| options.concurrency < MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY
|
|
189
|
+
|| options.concurrency > MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY) {
|
|
190
|
+
throw new CliUsageError(`Domain-control monitor concurrency must be from ${MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY} to ${MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY}.`);
|
|
191
|
+
}
|
|
192
|
+
let generatedAt;
|
|
193
|
+
try {
|
|
194
|
+
generatedAt = requireIsoTimestamp(options.now(), 'Domain-control monitor time');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new CliUsageError('Domain-control monitor time must be a valid ISO 8601 timestamp.');
|
|
198
|
+
}
|
|
199
|
+
const rawManifest = parseMonitorJson(manifestInput, 'Domain-control manifest');
|
|
200
|
+
let manifest;
|
|
33
201
|
try {
|
|
34
|
-
|
|
202
|
+
manifest = verifyDomainControlManifest(rawManifest);
|
|
35
203
|
}
|
|
36
204
|
catch {
|
|
37
|
-
throw new CliUsageError('Domain-control manifest must
|
|
205
|
+
throw new CliUsageError('Domain-control manifest must satisfy its supported bounded integrity contract.');
|
|
206
|
+
}
|
|
207
|
+
const checkedAt = Date.parse(generatedAt);
|
|
208
|
+
if (Date.parse(manifest.expiresAt) <= checkedAt) {
|
|
209
|
+
throw new CliUsageError('Domain-control monitor requires an unexpired manifest.');
|
|
38
210
|
}
|
|
39
|
-
const
|
|
211
|
+
const previous = previousSnapshot(previousInput, generatedAt);
|
|
40
212
|
const entries = manifest.entries.slice(0, Math.min(options.limit, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS));
|
|
41
213
|
const lookups = new Array(entries.length);
|
|
42
|
-
const
|
|
214
|
+
const failuresByIndex = new Array(entries.length);
|
|
43
215
|
let cursor = 0;
|
|
44
216
|
let completed = 0;
|
|
45
217
|
const worker = async () => {
|
|
46
218
|
while (cursor < entries.length) {
|
|
219
|
+
options.signal?.throwIfAborted();
|
|
47
220
|
const index = cursor++;
|
|
48
221
|
const entry = entries[index];
|
|
49
222
|
if (!entry)
|
|
@@ -51,10 +224,14 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
51
224
|
try {
|
|
52
225
|
const classified = classifyQuery(entry.domain);
|
|
53
226
|
const result = await options.executeLookup(classified, { fast: false, compact: false, ...(options.signal ? { signal: options.signal } : {}) });
|
|
227
|
+
options.signal?.throwIfAborted();
|
|
54
228
|
lookups[index] = buildCliLookupDocument(entry.domain, classified, result, generatedAt, 'deep');
|
|
55
229
|
}
|
|
56
|
-
catch
|
|
57
|
-
|
|
230
|
+
catch {
|
|
231
|
+
if (options.signal?.aborted) {
|
|
232
|
+
throw options.signal.reason || new DOMException('Aborted', 'AbortError');
|
|
233
|
+
}
|
|
234
|
+
failuresByIndex[index] = Object.freeze({ domain: entry.domain, error: monitorFailureMessage() });
|
|
58
235
|
}
|
|
59
236
|
finally {
|
|
60
237
|
completed += 1;
|
|
@@ -63,17 +240,23 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
63
240
|
}
|
|
64
241
|
};
|
|
65
242
|
await Promise.all(Array.from({ length: Math.min(options.concurrency, Math.max(entries.length, 1)) }, worker));
|
|
243
|
+
options.signal?.throwIfAborted();
|
|
244
|
+
const failures = failuresByIndex.filter((failure) => Boolean(failure));
|
|
66
245
|
const successful = lookups.filter(Boolean);
|
|
67
246
|
if (!successful.length)
|
|
68
247
|
throw new Error('No domain-control monitor lookup completed successfully.');
|
|
69
248
|
const review = buildCliDomainControlReview(JSON.stringify({
|
|
70
|
-
schema:
|
|
249
|
+
schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
250
|
+
version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
|
|
251
|
+
manifest,
|
|
252
|
+
lookups: successful,
|
|
71
253
|
}), generatedAt);
|
|
72
|
-
const previous = previousSnapshot(previousInput);
|
|
73
254
|
const observations = review.observations;
|
|
74
255
|
const flightRecorder = buildDomainControlFlightRecorder({
|
|
75
|
-
schema:
|
|
76
|
-
|
|
256
|
+
schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
|
|
257
|
+
version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
|
|
258
|
+
observations: [...previous.observations, ...observations],
|
|
259
|
+
approvedWindows: [],
|
|
77
260
|
}, generatedAt);
|
|
78
261
|
return Object.freeze({
|
|
79
262
|
schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA,
|
|
@@ -84,11 +267,7 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
84
267
|
observations,
|
|
85
268
|
review: review.review,
|
|
86
269
|
flightRecorder,
|
|
87
|
-
limitations:
|
|
88
|
-
'This is one bounded run, not a daemon. Scheduling and secure checkpoint retention remain operator-controlled.',
|
|
89
|
-
'The output retains compact normalised observations and errors, not raw RDAP, WHOIS, DNS, HTTP, TLS or page payloads.',
|
|
90
|
-
'A failed source or lookup remains incomplete and does not establish that a previously observed value disappeared.',
|
|
91
|
-
]),
|
|
270
|
+
limitations: DOMAIN_CONTROL_MONITOR_LIMITATIONS,
|
|
92
271
|
});
|
|
93
272
|
}
|
|
94
273
|
export function formatDomainControlMonitor(document) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { scanBoundedJson, } from "../lib/bounded-json.mjs";
|
|
3
|
+
import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
|
|
2
4
|
import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, } from "../lib/domain-control-flight-recorder.mjs";
|
|
3
5
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
4
6
|
import { CliUsageError } from "./errors.mjs";
|
|
5
7
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
|
|
9
|
-
export const MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES = 32 * 1024 * 1024;
|
|
8
|
+
export { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, };
|
|
9
|
+
const CLI_REVIEW_INPUT_KEY_SET = new Set(CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
|
|
10
10
|
function record(value) {
|
|
11
11
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
12
12
|
}
|
|
@@ -134,22 +134,43 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
134
134
|
if (Buffer.byteLength(inputText, 'utf8') > MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES) {
|
|
135
135
|
throw new CliUsageError(`Domain-control review input is limited to ${MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES} bytes.`);
|
|
136
136
|
}
|
|
137
|
+
const normalizedInput = inputText.replace(/^\uFEFF/u, '');
|
|
138
|
+
try {
|
|
139
|
+
scanBoundedJson(normalizedInput, {
|
|
140
|
+
maximumDepth: MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH,
|
|
141
|
+
maximumKeys: MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS,
|
|
142
|
+
maximumValues: MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (cause) {
|
|
146
|
+
const detail = cause instanceof Error ? cause.message : '';
|
|
147
|
+
if (detail === 'Artefact input is not valid JSON.') {
|
|
148
|
+
throw new CliUsageError('Domain-control review input must be valid JSON.');
|
|
149
|
+
}
|
|
150
|
+
const boundedDetail = detail.replace(/^Artefact JSON /u, '');
|
|
151
|
+
throw new CliUsageError(boundedDetail
|
|
152
|
+
? `Domain-control review input ${boundedDetail}`
|
|
153
|
+
: 'Domain-control review input must be valid JSON.');
|
|
154
|
+
}
|
|
137
155
|
let parsed;
|
|
138
156
|
try {
|
|
139
|
-
parsed = JSON.parse(
|
|
157
|
+
parsed = JSON.parse(normalizedInput);
|
|
140
158
|
}
|
|
141
159
|
catch {
|
|
142
160
|
throw new CliUsageError('Domain-control review input must be valid JSON.');
|
|
143
161
|
}
|
|
144
162
|
const input = record(parsed);
|
|
145
|
-
if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
146
|
-
|
|
163
|
+
if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
164
|
+
|| input.version !== CLI_DOMAIN_CONTROL_REVIEW_VERSION
|
|
165
|
+
|| !Array.isArray(input.lookups)) {
|
|
166
|
+
throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${CLI_DOMAIN_CONTROL_REVIEW_VERSION}.`);
|
|
147
167
|
}
|
|
148
|
-
if (Object.keys(input).some((key) => !
|
|
168
|
+
if (Object.keys(input).some((key) => !CLI_REVIEW_INPUT_KEY_SET.has(key))) {
|
|
149
169
|
throw new CliUsageError('Domain-control review input contains an unsupported field.');
|
|
150
170
|
}
|
|
151
|
-
if (input.lookups.length <
|
|
152
|
-
|
|
171
|
+
if (input.lookups.length < MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS
|
|
172
|
+
|| input.lookups.length > MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS) {
|
|
173
|
+
throw new CliUsageError(`Domain-control review requires from ${MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS} to ${MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS} saved Lookup documents.`);
|
|
153
174
|
}
|
|
154
175
|
const manifest = verifyDomainControlManifest(input.manifest);
|
|
155
176
|
const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
|
|
@@ -162,7 +183,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
162
183
|
const observations = [...latest.values()].map(domainControlObservationFromSavedLookup);
|
|
163
184
|
const review = reviewDomainControlManifest({
|
|
164
185
|
schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
165
|
-
version:
|
|
186
|
+
version: DOMAIN_CONTROL_REVIEW_VERSION,
|
|
166
187
|
manifest,
|
|
167
188
|
observations: observations.map((item) => ({
|
|
168
189
|
domain: item.domain,
|
|
@@ -202,17 +223,13 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
202
223
|
latestDomainObservations: observations.length,
|
|
203
224
|
ignoredHistoricalLookups: lookups.length - observations.length,
|
|
204
225
|
}),
|
|
205
|
-
limitations:
|
|
206
|
-
'The review uses only supplied saved Lookup documents and performs no request.',
|
|
207
|
-
'The newest supplied observation per domain is used for desired-state comparison; all supplied observations remain available to a separate flight-recorder review.',
|
|
208
|
-
'Raw RDAP, WHOIS, HTTP, TLS, and page payloads are not copied into this output.',
|
|
209
|
-
]),
|
|
226
|
+
limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
210
227
|
});
|
|
211
228
|
}
|
|
212
229
|
export function buildControlReviewInput(manifest, lookupDocuments) {
|
|
213
230
|
return Object.freeze({
|
|
214
231
|
schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
215
|
-
version:
|
|
232
|
+
version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
|
|
216
233
|
manifest,
|
|
217
234
|
lookups: Object.freeze(lookupDocuments),
|
|
218
235
|
});
|
package/cli/errors.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
|
|
2
|
+
const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
|
|
3
|
+
const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
2
4
|
class CliUsageError extends Error {
|
|
3
5
|
constructor(message) {
|
|
4
6
|
super(message);
|
|
@@ -12,16 +14,19 @@ function errorMessage(error) {
|
|
|
12
14
|
}
|
|
13
15
|
function boundedCliErrorMessage(error, fallback = 'Unexpected command failure') {
|
|
14
16
|
return String(errorMessage(error) || error || fallback)
|
|
15
|
-
.replace(/[\x00-\x1f\x7f]+/g, ' ')
|
|
16
|
-
.replace(
|
|
17
|
+
.replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ')
|
|
18
|
+
.replace(CLI_DEFAULT_IGNORABLE_GLOBAL_RE, '')
|
|
17
19
|
.replace(/\s+/g, ' ')
|
|
18
20
|
.trim()
|
|
19
21
|
.slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
|
|
20
22
|
}
|
|
23
|
+
function hasUnsafeCliText(value) {
|
|
24
|
+
return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
|
|
25
|
+
}
|
|
21
26
|
function boundedCliInputError(error, label) {
|
|
22
27
|
if (error instanceof CliUsageError)
|
|
23
28
|
return error;
|
|
24
29
|
const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
|
|
25
30
|
return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`);
|
|
26
31
|
}
|
|
27
|
-
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, };
|
|
32
|
+
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, hasUnsafeCliText, };
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { createReadStream } from 'node:fs';
|
|
2
1
|
import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, } from "./artifact-verify.mjs";
|
|
3
2
|
import { formatArchiveInspection, inspectWorkspaceArchive, } from "./archive-inspect.mjs";
|
|
4
3
|
import { MAX_SIGNING_KEY_FILE_BYTES, formatEvidenceSignatureVerification, signEvidencePackage, verifyEvidencePackageSignature, } from "./evidence-signing.mjs";
|
|
5
4
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
6
5
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
7
6
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
8
|
-
import {
|
|
7
|
+
import { readCliTextInput } from "./input.mjs";
|
|
9
8
|
export function isEvidenceCommand(args) {
|
|
10
9
|
return args.action === 'inspect-archive'
|
|
11
10
|
|| args.action === 'sign-artifact'
|
|
@@ -22,11 +21,10 @@ async function readArtifact(source, label, dependencies) {
|
|
|
22
21
|
try {
|
|
23
22
|
return dependencies.readArtifactInput
|
|
24
23
|
? await dependencies.readArtifactInput(source)
|
|
25
|
-
: await
|
|
26
|
-
|
|
27
|
-
: dependencies.stdin, {
|
|
28
|
-
limit: MAX_OFFLINE_ARTIFACT_BYTES,
|
|
24
|
+
: await readCliTextInput(source, dependencies.stdin, {
|
|
25
|
+
maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES,
|
|
29
26
|
label,
|
|
27
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
30
28
|
});
|
|
31
29
|
}
|
|
32
30
|
catch (error) {
|
|
@@ -35,11 +33,15 @@ async function readArtifact(source, label, dependencies) {
|
|
|
35
33
|
throw new CliUsageError(`Could not read ${label.toLowerCase()}: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
|
-
async function readKey(source, label, injected) {
|
|
36
|
+
async function readKey(source, label, injected, dependencies) {
|
|
39
37
|
try {
|
|
40
38
|
return injected
|
|
41
39
|
? await injected(source)
|
|
42
|
-
: await
|
|
40
|
+
: await readCliTextInput(source, dependencies.stdin, {
|
|
41
|
+
maximumBytes: MAX_SIGNING_KEY_FILE_BYTES,
|
|
42
|
+
label,
|
|
43
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
46
|
catch (error) {
|
|
45
47
|
if (error instanceof CliUsageError)
|
|
@@ -50,7 +52,11 @@ async function readKey(source, label, injected) {
|
|
|
50
52
|
async function readPassphrase(source, dependencies) {
|
|
51
53
|
const passphraseText = dependencies.readPassphraseFile
|
|
52
54
|
? await dependencies.readPassphraseFile(source)
|
|
53
|
-
: await
|
|
55
|
+
: await readCliTextInput(source, dependencies.stdin, {
|
|
56
|
+
maximumBytes: MAX_OFFLINE_PASSPHRASE_FILE_BYTES,
|
|
57
|
+
label: 'Passphrase file',
|
|
58
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
59
|
+
});
|
|
54
60
|
const passphrase = passphraseText.replace(/\r?\n$/u, '');
|
|
55
61
|
if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
|
|
56
62
|
throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
|
|
@@ -85,7 +91,7 @@ export async function runEvidenceCommand(args, dependencies) {
|
|
|
85
91
|
if (!input.trim()) {
|
|
86
92
|
throw new CliUsageError('sign-artifact requires one reviewed artefact file or an artefact on stdin.');
|
|
87
93
|
}
|
|
88
|
-
const privateKey = await readKey(args.privateKeySource, 'Private key file', dependencies.readPrivateKeyFile);
|
|
94
|
+
const privateKey = await readKey(args.privateKeySource, 'Private key file', dependencies.readPrivateKeyFile, dependencies);
|
|
89
95
|
const signed = await signEvidencePackage(input, privateKey, dependencies.now ? dependencies.now() : new Date().toISOString());
|
|
90
96
|
dependencies.stdout.write(formatJsonDocument(signed));
|
|
91
97
|
return EXIT_CODES.SUCCESS;
|
|
@@ -95,7 +101,7 @@ export async function runEvidenceCommand(args, dependencies) {
|
|
|
95
101
|
throw new CliUsageError('verify-signature requires one signed package file or a package on stdin.');
|
|
96
102
|
}
|
|
97
103
|
const publicKey = args.publicKeySource
|
|
98
|
-
? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile)
|
|
104
|
+
? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile, dependencies)
|
|
99
105
|
: null;
|
|
100
106
|
const report = await verifyEvidencePackageSignature(input, publicKey);
|
|
101
107
|
if (!args.quiet) {
|