@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
package/cli/saved-lookup.mjs
CHANGED
|
@@ -1,54 +1,368 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { types as utilTypes } from 'node:util';
|
|
2
3
|
import { classifyQuery } from "../lib/classify.mjs";
|
|
3
|
-
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
4
|
+
import { isSafeJsonObjectKey, scanBoundedJson, } from "../lib/bounded-json.mjs";
|
|
5
|
+
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
4
6
|
import { CliUsageError } from "./arguments.mjs";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../lib/homepage-metadata-contract.mjs";
|
|
8
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
|
+
import { CLI_LOOKUP_SCHEMA as SAVED_LOOKUP_SCHEMA, CLI_LOOKUP_VERSION as SAVED_LOOKUP_SCHEMA_VERSION, PUBLIC_CLI_LOOKUP_VERSION as PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION, MAX_CLI_LOOKUP_BYTES as MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_CLI_LOOKUP_JSON_CONTAINER_ITEMS as MAX_BOUNDED_JSON_CONTAINER_ITEMS, MAX_CLI_LOOKUP_JSON_DEPTH as MAX_BOUNDED_JSON_DEPTH, MAX_CLI_LOOKUP_JSON_KEYS as MAX_BOUNDED_JSON_KEYS, MAX_CLI_LOOKUP_JSON_VALUES as MAX_BOUNDED_JSON_VALUES, MAX_CLI_LOOKUP_STRING_LENGTH as MAX_SAVED_LOOKUP_STRING_LENGTH, SUPPORTED_CLI_LOOKUP_VERSIONS as SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, } from "../packages/contracts/cli-lookup.mjs";
|
|
10
|
+
const RDAP_STATUSES = new Set([
|
|
11
|
+
'success', 'complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
12
|
+
'unavailable', 'rate_limited', 'not_applicable', 'stale',
|
|
13
|
+
]);
|
|
14
|
+
const WHOIS_STATUSES = new Set([
|
|
15
|
+
'success', 'complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
16
|
+
'unavailable', 'rate_limited', 'not_applicable', 'stale',
|
|
17
|
+
]);
|
|
18
|
+
const QUERY_TYPES = new Set(['domain', 'ipv4', 'ipv6', 'asn']);
|
|
11
19
|
function objectOrNull(value) {
|
|
12
20
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
13
21
|
}
|
|
22
|
+
function snapshotLookupResultRoot(value) {
|
|
23
|
+
if (utilTypes.isProxy(value)
|
|
24
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
25
|
+
throw new TypeError('Lookup result must be one ordinary object.');
|
|
26
|
+
}
|
|
27
|
+
const keys = Reflect.ownKeys(value);
|
|
28
|
+
if (keys.length > MAX_BOUNDED_JSON_CONTAINER_ITEMS) {
|
|
29
|
+
throw new TypeError(`Lookup result contains more than ${MAX_BOUNDED_JSON_CONTAINER_ITEMS} fields.`);
|
|
30
|
+
}
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
if (typeof key !== 'string' || !isSafeJsonObjectKey(key)) {
|
|
34
|
+
throw new TypeError('Lookup result contains an unsafe object key.');
|
|
35
|
+
}
|
|
36
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
37
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
38
|
+
throw new TypeError('Lookup result contains a non-ordinary object field.');
|
|
39
|
+
}
|
|
40
|
+
result[key] = descriptor.value;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function snapshotKnownDataFields(value, label, allowedKeys) {
|
|
45
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || utilTypes.isProxy(value)) {
|
|
46
|
+
throw new TypeError(`${label} must be one ordinary object.`);
|
|
47
|
+
}
|
|
48
|
+
const prototype = Object.getPrototypeOf(value);
|
|
49
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
50
|
+
throw new TypeError(`${label} must be one ordinary object.`);
|
|
51
|
+
}
|
|
52
|
+
const keys = Reflect.ownKeys(value);
|
|
53
|
+
if (keys.some((key) => typeof key !== 'string' || !allowedKeys.has(key))) {
|
|
54
|
+
throw new TypeError(`${label} contains an unsupported field.`);
|
|
55
|
+
}
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
59
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
60
|
+
throw new TypeError(`${label} contains a non-ordinary field.`);
|
|
61
|
+
}
|
|
62
|
+
result[key] = descriptor.value;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function addJsonBytes(budget, compactBytes, prettyBytes, label) {
|
|
67
|
+
budget.compactBytes += compactBytes;
|
|
68
|
+
budget.prettyBytes += prettyBytes;
|
|
69
|
+
if (budget.compactBytes > MAX_SAVED_LOOKUP_INPUT_BYTES
|
|
70
|
+
|| budget.prettyBytes > MAX_SAVED_LOOKUP_INPUT_BYTES) {
|
|
71
|
+
throw new TypeError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function jsonStringCodePointBytes(value, index) {
|
|
75
|
+
const code = value.charCodeAt(index);
|
|
76
|
+
if (code === 0x22 || code === 0x5c
|
|
77
|
+
|| code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
|
|
78
|
+
return { bytes: 2, advance: 1 };
|
|
79
|
+
}
|
|
80
|
+
if (code <= 0x1f)
|
|
81
|
+
return { bytes: 6, advance: 1 };
|
|
82
|
+
if (code <= 0x7f)
|
|
83
|
+
return { bytes: 1, advance: 1 };
|
|
84
|
+
if (code <= 0x7ff)
|
|
85
|
+
return { bytes: 2, advance: 1 };
|
|
86
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
87
|
+
const next = value.charCodeAt(index + 1);
|
|
88
|
+
return next >= 0xdc00 && next <= 0xdfff
|
|
89
|
+
? { bytes: 4, advance: 2 }
|
|
90
|
+
: { bytes: 6, advance: 1 };
|
|
91
|
+
}
|
|
92
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
93
|
+
return { bytes: 6, advance: 1 };
|
|
94
|
+
return { bytes: 3, advance: 1 };
|
|
95
|
+
}
|
|
96
|
+
function addJsonStringBytes(budget, value, label) {
|
|
97
|
+
addJsonBytes(budget, 2, 2, label);
|
|
98
|
+
for (let index = 0; index < value.length;) {
|
|
99
|
+
const item = jsonStringCodePointBytes(value, index);
|
|
100
|
+
addJsonBytes(budget, item.bytes, item.bytes, label);
|
|
101
|
+
index += item.advance;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function jsonStringBytes(value) {
|
|
105
|
+
let bytes = 2;
|
|
106
|
+
for (let index = 0; index < value.length;) {
|
|
107
|
+
const item = jsonStringCodePointBytes(value, index);
|
|
108
|
+
bytes += item.bytes;
|
|
109
|
+
index += item.advance;
|
|
110
|
+
}
|
|
111
|
+
return bytes;
|
|
112
|
+
}
|
|
113
|
+
function jsonNonStringScalarBytes(value) {
|
|
114
|
+
if (value === null)
|
|
115
|
+
return 4;
|
|
116
|
+
if (typeof value === 'boolean')
|
|
117
|
+
return value ? 4 : 5;
|
|
118
|
+
return Buffer.byteLength(String(value), 'utf8');
|
|
119
|
+
}
|
|
120
|
+
function snapshotBoundedJson(value, label) {
|
|
121
|
+
// Reserve the one terminal LF used by the portable pretty-JSON document.
|
|
122
|
+
const budget = { keys: 0, values: 0, compactBytes: 0, prettyBytes: 1 };
|
|
123
|
+
const ancestors = new Set();
|
|
124
|
+
const copy = (current, depth) => {
|
|
125
|
+
budget.values += 1;
|
|
126
|
+
if (budget.values > MAX_BOUNDED_JSON_VALUES) {
|
|
127
|
+
throw new TypeError(`${label} exceeds the ${MAX_BOUNDED_JSON_VALUES}-value limit.`);
|
|
128
|
+
}
|
|
129
|
+
if (depth > MAX_BOUNDED_JSON_DEPTH) {
|
|
130
|
+
throw new TypeError(`${label} exceeds the ${MAX_BOUNDED_JSON_DEPTH}-level nesting limit.`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof current === 'string') {
|
|
133
|
+
addJsonStringBytes(budget, current, label);
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
if (current === null || typeof current === 'boolean') {
|
|
137
|
+
const bytes = jsonNonStringScalarBytes(current);
|
|
138
|
+
addJsonBytes(budget, bytes, bytes, label);
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
if (typeof current === 'number') {
|
|
142
|
+
if (!Number.isFinite(current))
|
|
143
|
+
throw new TypeError(`${label} contains a non-finite number.`);
|
|
144
|
+
const bytes = jsonNonStringScalarBytes(current);
|
|
145
|
+
addJsonBytes(budget, bytes, bytes, label);
|
|
146
|
+
return current;
|
|
147
|
+
}
|
|
148
|
+
if (!current || typeof current !== 'object') {
|
|
149
|
+
throw new TypeError(`${label} contains a non-JSON value.`);
|
|
150
|
+
}
|
|
151
|
+
if (utilTypes.isProxy(current)) {
|
|
152
|
+
throw new TypeError(`${label} contains a non-ordinary object.`);
|
|
153
|
+
}
|
|
154
|
+
if (ancestors.has(current))
|
|
155
|
+
throw new TypeError(`${label} contains a cyclic object reference.`);
|
|
156
|
+
ancestors.add(current);
|
|
157
|
+
try {
|
|
158
|
+
if (Array.isArray(current)) {
|
|
159
|
+
if (Object.getPrototypeOf(current) !== Array.prototype
|
|
160
|
+
|| Object.prototype.hasOwnProperty.call(current, Symbol.iterator)) {
|
|
161
|
+
throw new TypeError(`${label} contains a non-ordinary array.`);
|
|
162
|
+
}
|
|
163
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(current, 'length');
|
|
164
|
+
const length = lengthDescriptor && 'value' in lengthDescriptor ? lengthDescriptor.value : null;
|
|
165
|
+
if (!Number.isSafeInteger(length)
|
|
166
|
+
|| Number(length) < 0
|
|
167
|
+
|| Number(length) > MAX_BOUNDED_JSON_CONTAINER_ITEMS) {
|
|
168
|
+
throw new TypeError(`${label} contains a container with more than ${MAX_BOUNDED_JSON_CONTAINER_ITEMS} items.`);
|
|
169
|
+
}
|
|
170
|
+
const keys = Reflect.ownKeys(current);
|
|
171
|
+
if (keys.length !== Number(length) + 1
|
|
172
|
+
|| keys.at(-1) !== 'length'
|
|
173
|
+
|| keys.slice(0, -1).some((key, index) => key !== String(index))) {
|
|
174
|
+
throw new TypeError(`${label} contains a sparse or extended array.`);
|
|
175
|
+
}
|
|
176
|
+
if (Number(length) === 0)
|
|
177
|
+
addJsonBytes(budget, 2, 2, label);
|
|
178
|
+
else
|
|
179
|
+
addJsonBytes(budget, 1, 2, label);
|
|
180
|
+
const result = [];
|
|
181
|
+
for (let index = 0; index < Number(length); index += 1) {
|
|
182
|
+
if (index > 0)
|
|
183
|
+
addJsonBytes(budget, 1, 2, label);
|
|
184
|
+
addJsonBytes(budget, 0, (depth + 1) * 2, label);
|
|
185
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
|
|
186
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
187
|
+
throw new TypeError(`${label} contains a non-ordinary array entry.`);
|
|
188
|
+
}
|
|
189
|
+
result.push(copy(descriptor.value, depth + 1));
|
|
190
|
+
}
|
|
191
|
+
if (Number(length) > 0)
|
|
192
|
+
addJsonBytes(budget, 1, 2 + (depth * 2), label);
|
|
193
|
+
return Object.freeze(result);
|
|
194
|
+
}
|
|
195
|
+
const prototype = Object.getPrototypeOf(current);
|
|
196
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
197
|
+
throw new TypeError(`${label} contains a non-ordinary object.`);
|
|
198
|
+
}
|
|
199
|
+
const keys = Reflect.ownKeys(current);
|
|
200
|
+
if (keys.length > MAX_BOUNDED_JSON_CONTAINER_ITEMS) {
|
|
201
|
+
throw new TypeError(`${label} contains a container with more than ${MAX_BOUNDED_JSON_CONTAINER_ITEMS} items.`);
|
|
202
|
+
}
|
|
203
|
+
budget.keys += keys.length;
|
|
204
|
+
if (budget.keys > MAX_BOUNDED_JSON_KEYS) {
|
|
205
|
+
throw new TypeError(`${label} exceeds the ${MAX_BOUNDED_JSON_KEYS}-key limit.`);
|
|
206
|
+
}
|
|
207
|
+
if (keys.length === 0)
|
|
208
|
+
addJsonBytes(budget, 2, 2, label);
|
|
209
|
+
else
|
|
210
|
+
addJsonBytes(budget, 1, 2, label);
|
|
211
|
+
const result = {};
|
|
212
|
+
for (const [index, key] of keys.entries()) {
|
|
213
|
+
if (typeof key !== 'string' || !isSafeJsonObjectKey(key)) {
|
|
214
|
+
throw new TypeError(`${label} contains an unsafe object key.`);
|
|
215
|
+
}
|
|
216
|
+
if (index > 0)
|
|
217
|
+
addJsonBytes(budget, 1, 2, label);
|
|
218
|
+
addJsonBytes(budget, 1, ((depth + 1) * 2) + 2, label);
|
|
219
|
+
addJsonStringBytes(budget, key, label);
|
|
220
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
221
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
222
|
+
throw new TypeError(`${label} contains a non-ordinary object field.`);
|
|
223
|
+
}
|
|
224
|
+
result[key] = copy(descriptor.value, depth + 1);
|
|
225
|
+
}
|
|
226
|
+
if (keys.length > 0)
|
|
227
|
+
addJsonBytes(budget, 1, 2 + (depth * 2), label);
|
|
228
|
+
return Object.freeze(result);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
ancestors.delete(current);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const copied = copy(value, 0);
|
|
235
|
+
return Object.freeze({
|
|
236
|
+
value: copied,
|
|
237
|
+
compactBytes: budget.compactBytes,
|
|
238
|
+
prettyBytes: budget.prettyBytes,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
14
241
|
function requiredBoundedString(value, field) {
|
|
15
242
|
if (typeof value !== 'string' || !value.trim())
|
|
16
|
-
throw new
|
|
243
|
+
throw new TypeError(`${field} is missing.`);
|
|
17
244
|
if (value.length > MAX_SAVED_LOOKUP_STRING_LENGTH) {
|
|
18
|
-
throw new
|
|
245
|
+
throw new TypeError(`${field} exceeds the saved-document value limit.`);
|
|
19
246
|
}
|
|
20
247
|
return value;
|
|
21
248
|
}
|
|
22
249
|
function requiredStatus(value, allowed, field) {
|
|
23
250
|
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
24
|
-
throw new
|
|
251
|
+
throw new TypeError(`${field} is missing or unsupported.`);
|
|
25
252
|
}
|
|
26
253
|
return value;
|
|
27
254
|
}
|
|
28
|
-
|
|
29
|
-
|
|
255
|
+
function assertLookupQueryIdentity(document, query, type) {
|
|
256
|
+
let classified;
|
|
257
|
+
try {
|
|
258
|
+
classified = classifyQuery(query);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw new TypeError('query must identify the declared lookup type.');
|
|
262
|
+
}
|
|
263
|
+
if (classified.type !== type)
|
|
264
|
+
throw new TypeError('query must identify the declared lookup type.');
|
|
265
|
+
if (type !== 'domain' || classified.type !== 'domain') {
|
|
266
|
+
if (document.inputHostname !== undefined
|
|
267
|
+
|| document.registrableDomain !== undefined
|
|
268
|
+
|| document.isSubdomain !== undefined) {
|
|
269
|
+
throw new TypeError('Non-domain lookup documents cannot contain domain identity fields.');
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const registrableDomain = requiredBoundedString(document.registrableDomain, 'registrableDomain');
|
|
274
|
+
if (classified.registrableDomain !== registrableDomain.toLowerCase().replace(/\.$/u, '')) {
|
|
275
|
+
throw new TypeError('query must identify the declared registrable domain.');
|
|
276
|
+
}
|
|
277
|
+
if (document.inputHostname !== undefined
|
|
278
|
+
&& requiredBoundedString(document.inputHostname, 'inputHostname') !== classified.inputHostname) {
|
|
279
|
+
throw new TypeError('inputHostname must match the classified query.');
|
|
280
|
+
}
|
|
281
|
+
if (document.isSubdomain !== undefined && document.isSubdomain !== classified.isSubdomain) {
|
|
282
|
+
throw new TypeError('isSubdomain must match the classified query.');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function assertLookupEnvelope(document, label) {
|
|
286
|
+
if (document.schema !== SAVED_LOOKUP_SCHEMA
|
|
287
|
+
|| typeof document.version !== 'number'
|
|
288
|
+
|| !Number.isSafeInteger(document.version)
|
|
289
|
+
|| !SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS.some((version) => version === document.version)) {
|
|
290
|
+
throw new TypeError(`${label} must use ${SAVED_LOOKUP_SCHEMA} version ${SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS.join(' or ')}.`);
|
|
291
|
+
}
|
|
292
|
+
if (typeof document.type !== 'string' || !QUERY_TYPES.has(document.type)) {
|
|
293
|
+
throw new TypeError(`${label} has an unsupported lookup type.`);
|
|
294
|
+
}
|
|
295
|
+
if (document.mode !== 'fast' && document.mode !== 'deep') {
|
|
296
|
+
throw new TypeError(`${label} has an unsupported lookup mode.`);
|
|
297
|
+
}
|
|
298
|
+
const query = requiredBoundedString(document.query, 'query');
|
|
299
|
+
assertLookupQueryIdentity(document, query, document.type);
|
|
300
|
+
const rawGeneratedAt = requiredBoundedString(document.generatedAt, 'generatedAt');
|
|
301
|
+
const generatedAt = normalizeExplicitIsoTimestamp(rawGeneratedAt);
|
|
302
|
+
if (!generatedAt) {
|
|
303
|
+
throw new TypeError('generatedAt must be a valid timestamp with an explicit timezone.');
|
|
304
|
+
}
|
|
305
|
+
const diagnostics = objectOrNull(document.diagnostics);
|
|
306
|
+
const rdapDiagnostics = objectOrNull(diagnostics?.rdap);
|
|
307
|
+
const whoisDiagnostics = objectOrNull(diagnostics?.whois);
|
|
308
|
+
const rdapStatus = requiredStatus(rdapDiagnostics?.status, RDAP_STATUSES, 'diagnostics.rdap.status');
|
|
309
|
+
const whoisStatus = requiredStatus(whoisDiagnostics?.status, WHOIS_STATUSES, 'diagnostics.whois.status');
|
|
310
|
+
const rdap = objectOrNull(document.rdap);
|
|
311
|
+
const whois = objectOrNull(document.whois);
|
|
312
|
+
const availability = objectOrNull(document.availability);
|
|
313
|
+
const pageIdentity = objectOrNull(availability?.pageIdentity);
|
|
314
|
+
const http = objectOrNull(availability?.http);
|
|
315
|
+
const httpResponse = objectOrNull(http?.response);
|
|
316
|
+
const publicationMetadata = pageIdentity?.publicationMetadata;
|
|
317
|
+
const deliveryMetadata = httpResponse?.deliveryMetadata;
|
|
318
|
+
if (document.version === PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION
|
|
319
|
+
&& (publicationMetadata !== undefined || deliveryMetadata !== undefined)) {
|
|
320
|
+
throw new TypeError(`${label} version ${PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION} cannot contain version ${SAVED_LOOKUP_SCHEMA_VERSION} homepage metadata.`);
|
|
321
|
+
}
|
|
322
|
+
if (document.version === SAVED_LOOKUP_SCHEMA_VERSION
|
|
323
|
+
&& (publicationMetadata !== undefined && (!['success', 'partial'].includes(String(pageIdentity?.status))
|
|
324
|
+
|| !validPagePublicationMetadata(publicationMetadata))
|
|
325
|
+
|| deliveryMetadata !== undefined && (!['success', 'partial'].includes(String(http?.status))
|
|
326
|
+
|| !validHttpDeliveryMetadata(deliveryMetadata)))) {
|
|
327
|
+
throw new TypeError(`${label} contains invalid homepage metadata.`);
|
|
328
|
+
}
|
|
329
|
+
const rdapParsed = objectOrNull(rdap?.parsed);
|
|
330
|
+
const whoisParsed = objectOrNull(whois?.parsed);
|
|
331
|
+
if (rdapStatus === 'success' && !rdapParsed) {
|
|
332
|
+
throw new TypeError('Successful RDAP input is missing normalised parsed data.');
|
|
333
|
+
}
|
|
334
|
+
if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
|
|
335
|
+
throw new TypeError('Successful WHOIS input is missing normalised parsed data.');
|
|
336
|
+
}
|
|
337
|
+
return generatedAt;
|
|
338
|
+
}
|
|
339
|
+
function normalizeCliLookupDocument(value, options = {}) {
|
|
30
340
|
const label = options.label || 'Saved lookup input';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
341
|
+
const snapshot = snapshotBoundedJson(value, label);
|
|
342
|
+
const document = objectOrNull(snapshot.value);
|
|
343
|
+
if (!document)
|
|
344
|
+
throw new TypeError(`${label} must be one JSON object.`);
|
|
345
|
+
const generatedAt = assertLookupEnvelope(document, label);
|
|
346
|
+
const generatedAtByteDelta = jsonStringBytes(generatedAt)
|
|
347
|
+
- jsonStringBytes(document.generatedAt);
|
|
348
|
+
if (snapshot.compactBytes + generatedAtByteDelta > MAX_SAVED_LOOKUP_INPUT_BYTES
|
|
349
|
+
|| snapshot.prettyBytes + generatedAtByteDelta > MAX_SAVED_LOOKUP_INPUT_BYTES) {
|
|
350
|
+
throw new TypeError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
|
|
41
351
|
}
|
|
42
|
-
|
|
352
|
+
const normalized = {};
|
|
353
|
+
for (const [key, item] of Object.entries(document)) {
|
|
354
|
+
normalized[key] = key === 'generatedAt' ? generatedAt : item;
|
|
355
|
+
}
|
|
356
|
+
return Object.freeze(normalized);
|
|
43
357
|
}
|
|
44
|
-
function
|
|
358
|
+
function parseCliLookupDocument(text, options = {}) {
|
|
45
359
|
const label = options.label || 'Saved lookup input';
|
|
46
360
|
if (typeof text !== 'string')
|
|
47
361
|
throw new CliUsageError(`${label} must be a JSON document.`);
|
|
48
362
|
if (Buffer.byteLength(text, 'utf8') > MAX_SAVED_LOOKUP_INPUT_BYTES) {
|
|
49
363
|
throw new CliUsageError(`${label} is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
|
|
50
364
|
}
|
|
51
|
-
const normalized = text.replace(/^\uFEFF
|
|
365
|
+
const normalized = text.replace(/^\uFEFF/u, '');
|
|
52
366
|
try {
|
|
53
367
|
scanBoundedJson(normalized);
|
|
54
368
|
}
|
|
@@ -67,44 +381,97 @@ function parseSavedLookupDocument(text, options = {}) {
|
|
|
67
381
|
catch {
|
|
68
382
|
throw new CliUsageError(`${label} must be valid JSON.`);
|
|
69
383
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
throw new CliUsageError(
|
|
384
|
+
try {
|
|
385
|
+
return normalizeCliLookupDocument(parsed, { label });
|
|
386
|
+
}
|
|
387
|
+
catch (cause) {
|
|
388
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : `${label} is invalid.`);
|
|
75
389
|
}
|
|
390
|
+
}
|
|
391
|
+
function parseSavedLookupDocument(text, options = {}) {
|
|
392
|
+
const label = options.label || 'Saved lookup input';
|
|
393
|
+
const document = parseCliLookupDocument(text, { label });
|
|
76
394
|
if (document.type !== 'domain')
|
|
77
395
|
throw new CliUsageError(`${label} supports domain lookup documents only.`);
|
|
78
|
-
|
|
79
|
-
|
|
396
|
+
return document;
|
|
397
|
+
}
|
|
398
|
+
function serialiseNormalizedCliLookupDocument(document) {
|
|
399
|
+
const output = `${JSON.stringify(document, null, 2)}\n`;
|
|
400
|
+
if (Buffer.byteLength(output, 'utf8') > MAX_SAVED_LOOKUP_INPUT_BYTES) {
|
|
401
|
+
throw new TypeError(`Lookup document output is limited to ${MAX_SAVED_LOOKUP_INPUT_BYTES} bytes.`);
|
|
80
402
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
403
|
+
return output;
|
|
404
|
+
}
|
|
405
|
+
function serializeCliLookupDocument(value) {
|
|
406
|
+
return serialiseNormalizedCliLookupDocument(normalizeCliLookupDocument(value, {
|
|
407
|
+
label: 'Lookup document output',
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
function buildCliLookupDocument(query, classified, result, generatedAt = new Date().toISOString(), mode = 'fast', collectionContext = {}) {
|
|
411
|
+
const normalizedGeneratedAt = normalizeExplicitIsoTimestamp(generatedAt);
|
|
412
|
+
if (!normalizedGeneratedAt) {
|
|
413
|
+
throw new TypeError('Lookup document generation time must be valid and include an explicit timezone.');
|
|
88
414
|
}
|
|
89
|
-
|
|
90
|
-
|
|
415
|
+
const classifiedSnapshot = snapshotKnownDataFields(classified, 'Lookup classification', new Set(['type', 'value', 'inputHostname', 'registrableDomain', 'isSubdomain']));
|
|
416
|
+
const contextSnapshot = snapshotKnownDataFields(collectionContext, 'Lookup collection context', new Set(['observerLabel', 'vantageLabel']));
|
|
417
|
+
const observerLabel = typeof contextSnapshot.observerLabel === 'string'
|
|
418
|
+
? contextSnapshot.observerLabel
|
|
419
|
+
: null;
|
|
420
|
+
const vantageLabel = typeof contextSnapshot.vantageLabel === 'string'
|
|
421
|
+
? contextSnapshot.vantageLabel
|
|
422
|
+
: null;
|
|
423
|
+
const resultSnapshot = snapshotLookupResultRoot(result);
|
|
424
|
+
delete resultSnapshot.collectionContext;
|
|
425
|
+
const classifiedType = classifiedSnapshot.type;
|
|
426
|
+
if (typeof classifiedType !== 'string' || !QUERY_TYPES.has(classifiedType)) {
|
|
427
|
+
throw new TypeError('Lookup classification has an unsupported query type.');
|
|
91
428
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
429
|
+
const candidate = {
|
|
430
|
+
...resultSnapshot,
|
|
431
|
+
schema: SAVED_LOOKUP_SCHEMA,
|
|
432
|
+
version: SAVED_LOOKUP_SCHEMA_VERSION,
|
|
433
|
+
generatedAt: normalizedGeneratedAt,
|
|
434
|
+
mode: mode === 'deep' ? 'deep' : 'fast',
|
|
435
|
+
query,
|
|
436
|
+
type: classifiedType,
|
|
437
|
+
...(classifiedType === 'domain' ? {
|
|
438
|
+
inputHostname: classifiedSnapshot.inputHostname,
|
|
439
|
+
registrableDomain: classifiedSnapshot.registrableDomain,
|
|
440
|
+
isSubdomain: classifiedSnapshot.isSubdomain,
|
|
441
|
+
} : {}),
|
|
442
|
+
...((observerLabel || vantageLabel) ? {
|
|
443
|
+
collectionContext: {
|
|
444
|
+
...(observerLabel ? { observerLabel } : {}),
|
|
445
|
+
...(vantageLabel ? { vantageLabel } : {}),
|
|
446
|
+
},
|
|
447
|
+
} : {}),
|
|
448
|
+
};
|
|
449
|
+
if (classifiedType !== 'domain') {
|
|
450
|
+
delete candidate.inputHostname;
|
|
451
|
+
delete candidate.registrableDomain;
|
|
452
|
+
delete candidate.isSubdomain;
|
|
104
453
|
}
|
|
105
|
-
|
|
106
|
-
|
|
454
|
+
return normalizeCliLookupDocument(candidate, { label: 'Lookup document output' });
|
|
455
|
+
}
|
|
456
|
+
async function readSavedLookupInputBounded(stream, options = {}) {
|
|
457
|
+
const limit = options.limit || MAX_SAVED_LOOKUP_INPUT_BYTES;
|
|
458
|
+
const label = options.label || 'Saved lookup input';
|
|
459
|
+
if (!stream || stream.isTTY)
|
|
460
|
+
return '';
|
|
461
|
+
const chunks = [];
|
|
462
|
+
let total = 0;
|
|
463
|
+
for await (const chunk of stream) {
|
|
464
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
465
|
+
total += buffer.length;
|
|
466
|
+
if (total > limit)
|
|
467
|
+
throw new CliUsageError(`${label} is limited to ${limit} bytes.`);
|
|
468
|
+
chunks.push(buffer);
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
return decodeBoundedUtf8(Buffer.concat(chunks), label);
|
|
472
|
+
}
|
|
473
|
+
catch (cause) {
|
|
474
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : `${label} must contain valid UTF-8 text.`);
|
|
107
475
|
}
|
|
108
|
-
return document;
|
|
109
476
|
}
|
|
110
|
-
export { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, readSavedLookupInputBounded, };
|
|
477
|
+
export { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION, SAVED_LOOKUP_SCHEMA_VERSION, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, buildCliLookupDocument, normalizeCliLookupDocument, parseCliLookupDocument, parseSavedLookupDocument, readSavedLookupInputBounded, serializeCliLookupDocument, };
|
package/cli/sharing-review.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasVerifiedWholeArtifactIntegrity, UnsupportedOfflineArtifactError, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
2
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
2
3
|
const SHARING_REVIEW_SCHEMA = 'whoisleuth.cli.sharing-review';
|
|
3
4
|
const SHARING_REVIEW_VERSION = 2;
|
|
4
5
|
const MAX_SHARING_REVIEW_BYTES = 15 * 1024 * 1024;
|
|
@@ -84,12 +85,16 @@ function tlpLabel(marking) {
|
|
|
84
85
|
return `TLP:${marking === 'amber-strict' ? 'AMBER+STRICT' : marking.toUpperCase()}`;
|
|
85
86
|
}
|
|
86
87
|
async function buildSharingReview(raw, options, generatedAt = new Date().toISOString()) {
|
|
88
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_SHARING_REVIEW_BYTES) {
|
|
89
|
+
throw new TypeError(`Sharing review input is limited to ${MAX_SHARING_REVIEW_BYTES} bytes.`);
|
|
90
|
+
}
|
|
87
91
|
let artifactValue;
|
|
88
92
|
try {
|
|
93
|
+
scanBoundedJson(raw);
|
|
89
94
|
artifactValue = JSON.parse(raw);
|
|
90
95
|
}
|
|
91
96
|
catch {
|
|
92
|
-
throw new TypeError('Sharing review input must be valid JSON.');
|
|
97
|
+
throw new TypeError('Sharing review input must be valid bounded JSON without duplicate keys.');
|
|
93
98
|
}
|
|
94
99
|
const artifact = record(artifactValue);
|
|
95
100
|
if (!artifact)
|