@slicedearth/whoisleuth-cli 1.47.4 → 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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- 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 +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- 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 +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -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 +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- 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-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- 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-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- 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 +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- 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/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- 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 -21
- 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} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- 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/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -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/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- 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} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- 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/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- 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} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -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} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- 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} +13 -13
- 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} +110 -89
- package/third-party-notices.txt +14 -14
- 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 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- 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 -255
- 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 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /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,17 +1,28 @@
|
|
|
1
1
|
import { classifyQuery } from "../lib/classify.mjs";
|
|
2
2
|
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
3
|
-
import { buildDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
|
|
4
|
-
import {
|
|
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";
|
|
5
9
|
import { buildCliLookupDocument } from "./formatters/json.mjs";
|
|
6
10
|
import { buildCliDomainControlReview, } from "./domain-control-observations.mjs";
|
|
7
|
-
import {
|
|
8
|
-
export
|
|
9
|
-
export const
|
|
10
|
-
export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = 20;
|
|
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;
|
|
11
14
|
function parseMonitorJson(input, label) {
|
|
12
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
|
+
}
|
|
13
19
|
try {
|
|
14
|
-
scanBoundedJson(normalized
|
|
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
|
+
});
|
|
15
26
|
return JSON.parse(normalized);
|
|
16
27
|
}
|
|
17
28
|
catch (cause) {
|
|
@@ -25,32 +36,187 @@ function parseMonitorJson(input, label) {
|
|
|
25
36
|
throw new CliUsageError(`${label} must satisfy the bounded JSON structure contract.`);
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
|
-
function
|
|
29
|
-
if (!
|
|
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)
|
|
30
58
|
return { observations: [] };
|
|
31
59
|
const parsed = parseMonitorJson(input, 'Previous monitor snapshot');
|
|
32
|
-
const root = parsed
|
|
33
|
-
if (root.schema !== CLI_DOMAIN_CONTROL_MONITOR_SCHEMA || root.version !== CLI_DOMAIN_CONTROL_MONITOR_VERSION
|
|
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) {
|
|
34
62
|
throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} version ${CLI_DOMAIN_CONTROL_MONITOR_VERSION}.`);
|
|
35
63
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
let previousGeneratedAt;
|
|
65
|
+
try {
|
|
66
|
+
previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new CliUsageError('Previous monitor snapshot generatedAt must be a valid ISO 8601 timestamp.');
|
|
70
|
+
}
|
|
71
|
+
if (Date.parse(previousGeneratedAt) >= Date.parse(currentGeneratedAt)) {
|
|
72
|
+
throw new CliUsageError('Previous monitor snapshot must precede the current monitor run.');
|
|
73
|
+
}
|
|
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];
|
|
41
180
|
}
|
|
42
181
|
export async function runDomainControlMonitor(manifestInput, previousInput, options) {
|
|
43
|
-
|
|
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
|
+
}
|
|
44
199
|
const rawManifest = parseMonitorJson(manifestInput, 'Domain-control manifest');
|
|
45
|
-
|
|
46
|
-
|
|
200
|
+
let manifest;
|
|
201
|
+
try {
|
|
202
|
+
manifest = verifyDomainControlManifest(rawManifest);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
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.');
|
|
210
|
+
}
|
|
211
|
+
const previous = previousSnapshot(previousInput, generatedAt);
|
|
47
212
|
const entries = manifest.entries.slice(0, Math.min(options.limit, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS));
|
|
48
213
|
const lookups = new Array(entries.length);
|
|
49
|
-
const
|
|
214
|
+
const failuresByIndex = new Array(entries.length);
|
|
50
215
|
let cursor = 0;
|
|
51
216
|
let completed = 0;
|
|
52
217
|
const worker = async () => {
|
|
53
218
|
while (cursor < entries.length) {
|
|
219
|
+
options.signal?.throwIfAborted();
|
|
54
220
|
const index = cursor++;
|
|
55
221
|
const entry = entries[index];
|
|
56
222
|
if (!entry)
|
|
@@ -58,10 +224,14 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
58
224
|
try {
|
|
59
225
|
const classified = classifyQuery(entry.domain);
|
|
60
226
|
const result = await options.executeLookup(classified, { fast: false, compact: false, ...(options.signal ? { signal: options.signal } : {}) });
|
|
227
|
+
options.signal?.throwIfAborted();
|
|
61
228
|
lookups[index] = buildCliLookupDocument(entry.domain, classified, result, generatedAt, 'deep');
|
|
62
229
|
}
|
|
63
|
-
catch
|
|
64
|
-
|
|
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() });
|
|
65
235
|
}
|
|
66
236
|
finally {
|
|
67
237
|
completed += 1;
|
|
@@ -70,16 +240,23 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
70
240
|
}
|
|
71
241
|
};
|
|
72
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));
|
|
73
245
|
const successful = lookups.filter(Boolean);
|
|
74
246
|
if (!successful.length)
|
|
75
247
|
throw new Error('No domain-control monitor lookup completed successfully.');
|
|
76
248
|
const review = buildCliDomainControlReview(JSON.stringify({
|
|
77
|
-
schema:
|
|
249
|
+
schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
250
|
+
version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
|
|
251
|
+
manifest,
|
|
252
|
+
lookups: successful,
|
|
78
253
|
}), generatedAt);
|
|
79
254
|
const observations = review.observations;
|
|
80
255
|
const flightRecorder = buildDomainControlFlightRecorder({
|
|
81
|
-
schema:
|
|
82
|
-
|
|
256
|
+
schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
|
|
257
|
+
version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
|
|
258
|
+
observations: [...previous.observations, ...observations],
|
|
259
|
+
approvedWindows: [],
|
|
83
260
|
}, generatedAt);
|
|
84
261
|
return Object.freeze({
|
|
85
262
|
schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA,
|
|
@@ -90,11 +267,7 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
|
|
|
90
267
|
observations,
|
|
91
268
|
review: review.review,
|
|
92
269
|
flightRecorder,
|
|
93
|
-
limitations:
|
|
94
|
-
'This is one bounded run, not a daemon. Scheduling and secure checkpoint retention remain operator-controlled.',
|
|
95
|
-
'The output retains compact normalised observations and errors, not raw RDAP, WHOIS, DNS, HTTP, TLS or page payloads.',
|
|
96
|
-
'A failed source or lookup remains incomplete and does not establish that a previously observed value disappeared.',
|
|
97
|
-
]),
|
|
270
|
+
limitations: DOMAIN_CONTROL_MONITOR_LIMITATIONS,
|
|
98
271
|
});
|
|
99
272
|
}
|
|
100
273
|
export function formatDomainControlMonitor(document) {
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
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";
|
|
3
4
|
import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, } from "../lib/domain-control-flight-recorder.mjs";
|
|
4
5
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
5
6
|
import { CliUsageError } from "./errors.mjs";
|
|
6
7
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
|
|
10
|
-
export const MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES = 32 * 1024 * 1024;
|
|
11
|
-
export const MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS = 100;
|
|
12
|
-
export const MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH = MAX_BOUNDED_JSON_DEPTH + 2;
|
|
13
|
-
export const MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS = MAX_BOUNDED_JSON_KEYS * (MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS + 1);
|
|
14
|
-
export const MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES = MAX_BOUNDED_JSON_VALUES * (MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS + 1);
|
|
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);
|
|
15
10
|
function record(value) {
|
|
16
11
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
17
12
|
}
|
|
@@ -165,14 +160,17 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
165
160
|
throw new CliUsageError('Domain-control review input must be valid JSON.');
|
|
166
161
|
}
|
|
167
162
|
const input = record(parsed);
|
|
168
|
-
if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
169
|
-
|
|
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}.`);
|
|
170
167
|
}
|
|
171
|
-
if (Object.keys(input).some((key) => !
|
|
168
|
+
if (Object.keys(input).some((key) => !CLI_REVIEW_INPUT_KEY_SET.has(key))) {
|
|
172
169
|
throw new CliUsageError('Domain-control review input contains an unsupported field.');
|
|
173
170
|
}
|
|
174
|
-
if (input.lookups.length <
|
|
175
|
-
|
|
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.`);
|
|
176
174
|
}
|
|
177
175
|
const manifest = verifyDomainControlManifest(input.manifest);
|
|
178
176
|
const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
|
|
@@ -185,7 +183,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
185
183
|
const observations = [...latest.values()].map(domainControlObservationFromSavedLookup);
|
|
186
184
|
const review = reviewDomainControlManifest({
|
|
187
185
|
schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
188
|
-
version:
|
|
186
|
+
version: DOMAIN_CONTROL_REVIEW_VERSION,
|
|
189
187
|
manifest,
|
|
190
188
|
observations: observations.map((item) => ({
|
|
191
189
|
domain: item.domain,
|
|
@@ -225,17 +223,13 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
225
223
|
latestDomainObservations: observations.length,
|
|
226
224
|
ignoredHistoricalLookups: lookups.length - observations.length,
|
|
227
225
|
}),
|
|
228
|
-
limitations:
|
|
229
|
-
'The review uses only supplied saved Lookup documents and performs no request.',
|
|
230
|
-
'The newest supplied observation per domain is used for desired-state comparison; all supplied observations remain available to a separate flight-recorder review.',
|
|
231
|
-
'Raw RDAP, WHOIS, HTTP, TLS, and page payloads are not copied into this output.',
|
|
232
|
-
]),
|
|
226
|
+
limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
233
227
|
});
|
|
234
228
|
}
|
|
235
229
|
export function buildControlReviewInput(manifest, lookupDocuments) {
|
|
236
230
|
return Object.freeze({
|
|
237
231
|
schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
238
|
-
version:
|
|
232
|
+
version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
|
|
239
233
|
manifest,
|
|
240
234
|
lookups: Object.freeze(lookupDocuments),
|
|
241
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, };
|
package/cli/evidence-signing.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash, createPrivateKey, createPublicKey, sign, verify, } from 'node:crypto';
|
|
3
|
-
import { canonicalArtifactJsonFor,
|
|
3
|
+
import { canonicalArtifactJsonFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
4
|
import { hasVerifiedApplicableIntegrity, hasVerifiedArtifactStructure, MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
5
5
|
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
6
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
6
7
|
export const SIGNED_EVIDENCE_PACKAGE_SCHEMA = 'whoisleuth.signed-evidence-package';
|
|
7
8
|
export const SIGNED_EVIDENCE_PACKAGE_VERSION = 2;
|
|
8
|
-
export const LEGACY_SIGNED_EVIDENCE_PACKAGE_VERSION = 1;
|
|
9
9
|
export const EVIDENCE_SIGNATURE_VERIFICATION_SCHEMA = 'whoisleuth.evidence-signature-verification';
|
|
10
10
|
export const EVIDENCE_SIGNATURE_VERIFICATION_VERSION = 2;
|
|
11
11
|
export const EVIDENCE_SIGNATURE_ALGORITHM = 'Ed25519';
|
|
@@ -34,13 +34,10 @@ function hasExactKeys(value, expected) {
|
|
|
34
34
|
return keys.length === expected.size && keys.every((key) => expected.has(key));
|
|
35
35
|
}
|
|
36
36
|
function timestamp(value) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!Number.isFinite(parsed))
|
|
42
|
-
throw new TypeError('Signature time must be a valid timestamp.');
|
|
43
|
-
return new Date(parsed).toISOString();
|
|
37
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
38
|
+
if (!normalized)
|
|
39
|
+
throw new TypeError('Signature time must be a valid timestamp with an explicit timezone.');
|
|
40
|
+
return normalized;
|
|
44
41
|
}
|
|
45
42
|
function canonicalTimestamp(value) {
|
|
46
43
|
const normalized = timestamp(value);
|
|
@@ -131,7 +128,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
131
128
|
const artifact = record(value.artifact);
|
|
132
129
|
if (!hasExactKeys(value, PACKAGE_KEYS)
|
|
133
130
|
|| value.schema !== SIGNED_EVIDENCE_PACKAGE_SCHEMA
|
|
134
|
-
||
|
|
131
|
+
|| value.version !== SIGNED_EVIDENCE_PACKAGE_VERSION
|
|
135
132
|
|| !artifact
|
|
136
133
|
|| !signature
|
|
137
134
|
|| !hasExactKeys(signature, SIGNATURE_KEYS)
|
|
@@ -146,14 +143,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
146
143
|
|| !SHA256_RE.test(signature.keyIdSha256)) {
|
|
147
144
|
throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
|
|
148
145
|
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
canonicalization = resolveArtifactCanonicalization(value.version, signature.canonicalization, [
|
|
152
|
-
{ version: LEGACY_SIGNED_EVIDENCE_PACKAGE_VERSION, canonicalization: SORTED_JSON_V1, explicit: true },
|
|
153
|
-
{ version: SIGNED_EVIDENCE_PACKAGE_VERSION, canonicalization: SORTED_JSON_V2, explicit: true },
|
|
154
|
-
], 'Signed evidence package');
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
146
|
+
if (signature.canonicalization !== EVIDENCE_SIGNATURE_CANONICALIZATION) {
|
|
157
147
|
throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
|
|
158
148
|
}
|
|
159
149
|
const signedAt = canonicalTimestamp(value.signedAt);
|
|
@@ -164,13 +154,13 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
164
154
|
const embeddedPublicKey = importPublicKey(publicDer, 'Embedded evidence-signing public key');
|
|
165
155
|
const payload = {
|
|
166
156
|
schema: SIGNED_EVIDENCE_PACKAGE_SCHEMA,
|
|
167
|
-
version:
|
|
157
|
+
version: SIGNED_EVIDENCE_PACKAGE_VERSION,
|
|
168
158
|
signedAt,
|
|
169
159
|
artifact,
|
|
170
160
|
};
|
|
171
161
|
const signatureBytes = Buffer.from(signature.valueBase64, 'base64');
|
|
172
162
|
if (signatureBytes.length !== 64
|
|
173
|
-
|| !verify(null, payloadBytes(payload,
|
|
163
|
+
|| !verify(null, payloadBytes(payload, EVIDENCE_SIGNATURE_CANONICALIZATION), embeddedPublicKey, signatureBytes)) {
|
|
174
164
|
throw new TypeError('Signed evidence package failed Ed25519 verification.');
|
|
175
165
|
}
|
|
176
166
|
let artifactVerification = null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { domainToASCII } from 'node:url';
|
|
2
2
|
import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
|
-
import { EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../
|
|
3
|
+
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../packages/interchange/external-findings-import.mjs";
|
|
4
4
|
export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
|
|
5
5
|
export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
|
|
6
6
|
export const MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES = 4 * 1024 * 1024;
|
|
@@ -135,8 +135,8 @@ export function mapExternalObservations(inputRaw) {
|
|
|
135
135
|
}
|
|
136
136
|
const truncated = selected.length < seen.size;
|
|
137
137
|
const document = {
|
|
138
|
-
schema:
|
|
139
|
-
schemaVersion:
|
|
138
|
+
schema: EXTERNAL_FINDINGS_SCHEMA,
|
|
139
|
+
schemaVersion: EXTERNAL_FINDINGS_VERSION,
|
|
140
140
|
source,
|
|
141
141
|
findings: selected.map((finding) => ({
|
|
142
142
|
...finding,
|
package/cli/formatters/json.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bulkDnsSummary } from "../bulk-output.mjs";
|
|
2
|
-
|
|
2
|
+
import { SAVED_LOOKUP_SCHEMA_VERSION, buildCliLookupDocument, } from "../saved-lookup.mjs";
|
|
3
|
+
import { CLI_LOOKUP_SCHEMA } from "../../packages/contracts/cli-lookup.mjs";
|
|
3
4
|
const CLI_BULK_SCHEMA = 'whoisleuth.cli.bulk';
|
|
4
5
|
const CLI_BULK_ITEM_SCHEMA = 'whoisleuth.cli.bulk.item';
|
|
5
6
|
const CLI_CT_SEARCH_SCHEMA = 'whoisleuth.cli.ct-search';
|
|
@@ -9,42 +10,29 @@ const CLI_POSTURE_SCHEMA = 'whoisleuth.cli.posture';
|
|
|
9
10
|
const CLI_HTTP_SCHEMA = 'whoisleuth.cli.http';
|
|
10
11
|
const CLI_TLS_SCHEMA = 'whoisleuth.cli.tls';
|
|
11
12
|
const CLI_COMPARE_SCHEMA = 'whoisleuth.cli.compare';
|
|
12
|
-
const CLI_LOOKUP_SCHEMA_VERSION =
|
|
13
|
+
const CLI_LOOKUP_SCHEMA_VERSION = SAVED_LOOKUP_SCHEMA_VERSION;
|
|
13
14
|
const CLI_BULK_SCHEMA_VERSION = 3;
|
|
14
15
|
const CLI_CT_SEARCH_SCHEMA_VERSION = 1;
|
|
15
16
|
const CLI_DISCOVER_SCHEMA_VERSION = 2;
|
|
16
17
|
const CLI_POSTURE_SCHEMA_VERSION = 1;
|
|
17
|
-
const CLI_HTTP_SCHEMA_VERSION =
|
|
18
|
+
const CLI_HTTP_SCHEMA_VERSION = 3;
|
|
18
19
|
const CLI_TLS_SCHEMA_VERSION = 1;
|
|
19
20
|
const CLI_COMPARE_SCHEMA_VERSION = 3;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
mode: mode === 'deep' ? 'deep' : 'fast',
|
|
33
|
-
query,
|
|
34
|
-
type: classified.type,
|
|
35
|
-
inputHostname: classified.inputHostname,
|
|
36
|
-
registrableDomain: classified.registrableDomain,
|
|
37
|
-
isSubdomain: classified.isSubdomain,
|
|
38
|
-
...((observerLabel || vantageLabel) ? {
|
|
39
|
-
collectionContext: {
|
|
40
|
-
...(observerLabel ? { observerLabel } : {}),
|
|
41
|
-
...(vantageLabel ? { vantageLabel } : {}),
|
|
42
|
-
},
|
|
43
|
-
} : {}),
|
|
44
|
-
};
|
|
21
|
+
const TERMINAL_UNSAFE_JSON_RE = /[\u007f-\u009f\u2028\u2029]|\p{Default_Ignorable_Code_Point}/gu;
|
|
22
|
+
function jsonUnicodeEscape(value) {
|
|
23
|
+
const codePoint = value.codePointAt(0);
|
|
24
|
+
if (codePoint <= 0xffff)
|
|
25
|
+
return `\\u${codePoint.toString(16).padStart(4, '0')}`;
|
|
26
|
+
const offset = codePoint - 0x10000;
|
|
27
|
+
const high = 0xd800 + (offset >> 10);
|
|
28
|
+
const low = 0xdc00 + (offset & 0x3ff);
|
|
29
|
+
return `\\u${high.toString(16).padStart(4, '0')}\\u${low.toString(16).padStart(4, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
function terminalSafeJson(value, space) {
|
|
32
|
+
return JSON.stringify(value, null, space).replace(TERMINAL_UNSAFE_JSON_RE, jsonUnicodeEscape);
|
|
45
33
|
}
|
|
46
34
|
function formatJsonDocument(document) {
|
|
47
|
-
return `${
|
|
35
|
+
return `${terminalSafeJson(document, 2)}\n`;
|
|
48
36
|
}
|
|
49
37
|
function buildCliCtSearchDocument(keyword, result, generatedAt = new Date().toISOString()) {
|
|
50
38
|
return { ...result, schema: CLI_CT_SEARCH_SCHEMA, version: CLI_CT_SEARCH_SCHEMA_VERSION, generatedAt, keyword };
|
|
@@ -83,7 +71,7 @@ function discoverJsonItem(candidate, metadata) {
|
|
|
83
71
|
function formatDiscoverJsonLines(candidates, metadata) {
|
|
84
72
|
if (!candidates.length)
|
|
85
73
|
return '';
|
|
86
|
-
return `${candidates.map((candidate) =>
|
|
74
|
+
return `${candidates.map((candidate) => terminalSafeJson(discoverJsonItem(candidate, metadata))).join('\n')}\n`;
|
|
87
75
|
}
|
|
88
76
|
function formatDiscoverDomainList(candidates) {
|
|
89
77
|
const domains = [...new Set(candidates.flatMap((candidate) => (typeof candidate.domain === 'string' && candidate.domain ? [candidate.domain] : [])))];
|
|
@@ -149,7 +137,7 @@ function buildCliBulkDocument(items, metadata) {
|
|
|
149
137
|
}
|
|
150
138
|
function formatJsonLines(items, metadata) {
|
|
151
139
|
return items.length
|
|
152
|
-
? `${items.map((item) =>
|
|
140
|
+
? `${items.map((item) => terminalSafeJson(bulkJsonItem(item, metadata))).join('\n')}\n`
|
|
153
141
|
: '';
|
|
154
142
|
}
|
|
155
|
-
export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, };
|
|
143
|
+
export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, terminalSafeJson, };
|