@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/config-profile.mjs
CHANGED
|
@@ -2,12 +2,14 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { CliUsageError } from "./errors.mjs";
|
|
5
|
+
import { cliMetaActionForInvocation } from "./command-reference.mjs";
|
|
5
6
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
7
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
6
8
|
export const CLI_CONFIG_SCHEMA = 'whoisleuth.cli.config';
|
|
7
9
|
export const CLI_CONFIG_VERSION = 1;
|
|
8
10
|
export const MAX_CLI_CONFIG_BYTES = 64 * 1024;
|
|
9
11
|
const SAFE_BOOLEAN_DEFAULTS = new Set(['--no-color', '--summary', '--verbose', '--fast']);
|
|
10
|
-
const SAFE_VALUE_DEFAULTS = new Set(['--concurrency', '--observer', '--vantage']);
|
|
12
|
+
const SAFE_VALUE_DEFAULTS = new Set(['--concurrency', '--observer', '--palette', '--vantage']);
|
|
11
13
|
const OPTION_GROUPS = Object.freeze({
|
|
12
14
|
'--no-color': 'colour',
|
|
13
15
|
'--summary': 'detail',
|
|
@@ -15,6 +17,7 @@ const OPTION_GROUPS = Object.freeze({
|
|
|
15
17
|
'--fast': 'scan',
|
|
16
18
|
'--concurrency': 'concurrency',
|
|
17
19
|
'--observer': 'observer',
|
|
20
|
+
'--palette': 'colour',
|
|
18
21
|
'--vantage': 'vantage',
|
|
19
22
|
});
|
|
20
23
|
function validProfileName(value) {
|
|
@@ -54,6 +57,9 @@ function validateProfileArguments(value) {
|
|
|
54
57
|
if (argument === '--concurrency' && (!/^\d+$/u.test(next) || Number(next) < 1 || Number(next) > 8)) {
|
|
55
58
|
throw new CliUsageError('CLI profile concurrency must be an integer from 1 to 8.');
|
|
56
59
|
}
|
|
60
|
+
if (argument === '--palette' && !['auto', 'light', 'dark'].includes(next)) {
|
|
61
|
+
throw new CliUsageError('CLI profile palette must be auto, light, or dark.');
|
|
62
|
+
}
|
|
57
63
|
groups.add(group);
|
|
58
64
|
validated.push(argument, next);
|
|
59
65
|
}
|
|
@@ -62,12 +68,14 @@ function validateProfileArguments(value) {
|
|
|
62
68
|
function parseProfileDocument(input) {
|
|
63
69
|
if (Buffer.byteLength(input, 'utf8') > MAX_CLI_CONFIG_BYTES)
|
|
64
70
|
throw new CliUsageError('CLI configuration is limited to 64 KiB.');
|
|
71
|
+
const normalized = input.replace(/^\uFEFF/u, '');
|
|
65
72
|
let parsed;
|
|
66
73
|
try {
|
|
67
|
-
|
|
74
|
+
scanBoundedJson(normalized);
|
|
75
|
+
parsed = JSON.parse(normalized);
|
|
68
76
|
}
|
|
69
77
|
catch {
|
|
70
|
-
throw new CliUsageError('CLI configuration must be valid JSON.');
|
|
78
|
+
throw new CliUsageError('CLI configuration must be valid bounded JSON without duplicate keys.');
|
|
71
79
|
}
|
|
72
80
|
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
73
81
|
if (root.schema !== CLI_CONFIG_SCHEMA || root.version !== CLI_CONFIG_VERSION || !root.profiles || typeof root.profiles !== 'object' || Array.isArray(root.profiles)) {
|
|
@@ -109,6 +117,8 @@ function suppliedGroups(argumentsList) {
|
|
|
109
117
|
return groups;
|
|
110
118
|
}
|
|
111
119
|
export async function resolveCliProfileArguments(argv, options = {}) {
|
|
120
|
+
if (cliMetaActionForInvocation(argv))
|
|
121
|
+
return [...argv];
|
|
112
122
|
// registry-scaffold owns --profile as the capability template to generate.
|
|
113
123
|
// Do not reinterpret it as a global CLI-default profile.
|
|
114
124
|
if (argv[0] === 'registry-scaffold') {
|
package/cli/discover.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
2
3
|
import { CliUsageError } from "./arguments.mjs";
|
|
3
4
|
const DEFAULT_DISCOVERY_TLDS = Object.freeze(['com', 'net', 'org']);
|
|
4
5
|
const MAX_DISCOVERY_TLD_TEXT_LENGTH = 1024;
|
|
@@ -16,7 +17,12 @@ async function readDiscoveryDictionaryBounded(stream, limit = MAX_DISCOVERY_DICT
|
|
|
16
17
|
throw new CliUsageError(`Discovery dictionary input is limited to ${limit} bytes.`);
|
|
17
18
|
chunks.push(buffer);
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
try {
|
|
21
|
+
return decodeBoundedUtf8(Buffer.concat(chunks), 'Discovery dictionary input');
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : 'Discovery dictionary input must contain valid UTF-8 text.');
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
27
|
function normalizeDiscoveryTlds(raw, maximum) {
|
|
22
28
|
if (!Number.isSafeInteger(maximum) || maximum < 1) {
|
|
@@ -4,11 +4,12 @@ import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
|
4
4
|
import { writePrivateFile } from "./output-file.mjs";
|
|
5
5
|
import { isValidAsciiDomainName } from "../lib/hostname.mjs";
|
|
6
6
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
7
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
8
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
7
9
|
export const CLI_DISCOVERY_OBSERVATION_SCHEMA = 'whoisleuth.cli.discovery-observation-snapshot';
|
|
8
10
|
export const CLI_DISCOVERY_OBSERVATION_VERSION = 2;
|
|
9
11
|
export const MAX_DISCOVERY_OBSERVATION_BYTES = 8 * 1024 * 1024;
|
|
10
12
|
const MAX_DISCOVERY_OBSERVATIONS = 500;
|
|
11
|
-
const SUPPORTED_DISCOVERY_OBSERVATION_VERSIONS = new Set([1, CLI_DISCOVERY_OBSERVATION_VERSION]);
|
|
12
13
|
function record(value) {
|
|
13
14
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
14
15
|
}
|
|
@@ -25,10 +26,10 @@ function normalizedTimestamp(value, label, optional = false) {
|
|
|
25
26
|
if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
26
27
|
throw new CliUsageError(`${label} is invalid.`);
|
|
27
28
|
}
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
30
|
+
if (normalized)
|
|
31
|
+
return normalized;
|
|
32
|
+
throw new CliUsageError(`${label} is invalid.`);
|
|
32
33
|
}
|
|
33
34
|
function latestObservedAt(registrationObservedAt, dnsObservedAt) {
|
|
34
35
|
if (!registrationObservedAt)
|
|
@@ -64,32 +65,25 @@ function normalizeDns(value) {
|
|
|
64
65
|
hasDmarc: nullableBoolean(dns.hasDmarc),
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
|
-
function normalizeObservation(value
|
|
68
|
+
function normalizeObservation(value) {
|
|
68
69
|
const item = record(value);
|
|
69
70
|
const dns = normalizeDns(item.dns);
|
|
70
71
|
try {
|
|
71
|
-
const validAttemptStates = version === 1 ? ['error', 'success'] : ['error', 'partial', 'success'];
|
|
72
72
|
if (!dns
|
|
73
|
-
|| !
|
|
73
|
+
|| !['error', 'partial', 'success'].includes(String(item.latestAttemptState))
|
|
74
74
|
|| typeof item.availabilityState !== 'string' || !item.availabilityState || item.availabilityState.length > 40
|
|
75
75
|
|| /[\u0000-\u001f\u007f]/u.test(item.availabilityState)
|
|
76
76
|
|| typeof item.confidence !== 'string' || !item.confidence || item.confidence.length > 40
|
|
77
77
|
|| /[\u0000-\u001f\u007f]/u.test(item.confidence))
|
|
78
78
|
return null;
|
|
79
|
-
const
|
|
80
|
-
const registrationObservedAt =
|
|
81
|
-
|
|
82
|
-
: normalizedTimestamp(item.registrationObservedAt, 'Discovery registration observation time', true);
|
|
83
|
-
const dnsObservedAt = version === 1
|
|
84
|
-
? ['partial', 'success'].includes(dns.status) ? legacyObservedAt : null
|
|
85
|
-
: normalizedTimestamp(item.dnsObservedAt, 'Discovery DNS observation time', true);
|
|
79
|
+
const declaredObservedAt = normalizedTimestamp(item.observedAt, 'Discovery observation time', true);
|
|
80
|
+
const registrationObservedAt = normalizedTimestamp(item.registrationObservedAt, 'Discovery registration observation time', true);
|
|
81
|
+
const dnsObservedAt = normalizedTimestamp(item.dnsObservedAt, 'Discovery DNS observation time', true);
|
|
86
82
|
const observedAt = latestObservedAt(registrationObservedAt, dnsObservedAt);
|
|
87
|
-
if (
|
|
83
|
+
if (declaredObservedAt !== observedAt)
|
|
88
84
|
return null;
|
|
89
|
-
const latestRegistrationState =
|
|
90
|
-
|
|
91
|
-
: item.latestRegistrationState;
|
|
92
|
-
const latestDnsState = version === 1 ? dns.status : item.latestDnsState;
|
|
85
|
+
const latestRegistrationState = item.latestRegistrationState;
|
|
86
|
+
const latestDnsState = item.latestDnsState;
|
|
93
87
|
if (!['observed', 'unavailable'].includes(String(latestRegistrationState))
|
|
94
88
|
|| typeof latestDnsState !== 'string' || !latestDnsState || latestDnsState.length > 40
|
|
95
89
|
|| /[\u0000-\u001f\u007f]/u.test(latestDnsState))
|
|
@@ -118,28 +112,30 @@ function parseObservationSnapshot(text, expectedDigest) {
|
|
|
118
112
|
if (Buffer.byteLength(text, 'utf8') > MAX_DISCOVERY_OBSERVATION_BYTES) {
|
|
119
113
|
throw new CliUsageError(`Discovery observation snapshots are limited to ${MAX_DISCOVERY_OBSERVATION_BYTES} bytes.`);
|
|
120
114
|
}
|
|
115
|
+
const normalizedInput = text.replace(/^\uFEFF/u, '');
|
|
121
116
|
let parsed;
|
|
122
117
|
try {
|
|
123
|
-
|
|
118
|
+
scanBoundedJson(normalizedInput);
|
|
119
|
+
parsed = JSON.parse(normalizedInput);
|
|
124
120
|
}
|
|
125
121
|
catch {
|
|
126
|
-
throw new CliUsageError('Discovery observation snapshot must be valid JSON.');
|
|
122
|
+
throw new CliUsageError('Discovery observation snapshot must be valid bounded JSON without duplicate keys.');
|
|
127
123
|
}
|
|
128
124
|
const document = record(parsed);
|
|
129
125
|
const version = typeof document.version === 'number' && Number.isSafeInteger(document.version)
|
|
130
126
|
? document.version
|
|
131
127
|
: 0;
|
|
132
|
-
if (document.schema !== CLI_DISCOVERY_OBSERVATION_SCHEMA ||
|
|
133
|
-
throw new CliUsageError(`Discovery observation snapshot must use ${CLI_DISCOVERY_OBSERVATION_SCHEMA} version
|
|
128
|
+
if (document.schema !== CLI_DISCOVERY_OBSERVATION_SCHEMA || version !== CLI_DISCOVERY_OBSERVATION_VERSION) {
|
|
129
|
+
throw new CliUsageError(`Discovery observation snapshot must use ${CLI_DISCOVERY_OBSERVATION_SCHEMA} version ${CLI_DISCOVERY_OBSERVATION_VERSION}.`);
|
|
134
130
|
}
|
|
135
131
|
if (document.configurationDigestSha256 !== expectedDigest) {
|
|
136
132
|
throw new CliUsageError('Discovery observation snapshot does not match this candidate set, scan mode, or resolver selection.');
|
|
137
133
|
}
|
|
138
|
-
if (
|
|
139
|
-
|| !Array.isArray(document.observations) || document.observations.length > MAX_DISCOVERY_OBSERVATIONS) {
|
|
134
|
+
if (!Array.isArray(document.observations) || document.observations.length > MAX_DISCOVERY_OBSERVATIONS) {
|
|
140
135
|
throw new CliUsageError('Discovery observation snapshot metadata is invalid.');
|
|
141
136
|
}
|
|
142
|
-
const
|
|
137
|
+
const generatedAt = normalizedTimestamp(document.generatedAt, 'Discovery snapshot generation time') ?? '';
|
|
138
|
+
const observations = document.observations.map((item) => normalizeObservation(item));
|
|
143
139
|
if (observations.some((item) => item === null) || document.observationCount !== observations.length) {
|
|
144
140
|
throw new CliUsageError('Discovery observation snapshot contains invalid evidence.');
|
|
145
141
|
}
|
|
@@ -150,7 +146,7 @@ function parseObservationSnapshot(text, expectedDigest) {
|
|
|
150
146
|
return {
|
|
151
147
|
schema: CLI_DISCOVERY_OBSERVATION_SCHEMA,
|
|
152
148
|
version: CLI_DISCOVERY_OBSERVATION_VERSION,
|
|
153
|
-
generatedAt
|
|
149
|
+
generatedAt,
|
|
154
150
|
configurationDigestSha256: expectedDigest,
|
|
155
151
|
observationCount: normalized.length,
|
|
156
152
|
observations: normalized,
|
|
@@ -34,6 +34,15 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
34
34
|
});
|
|
35
35
|
context.setEventProgress(eventProgress);
|
|
36
36
|
eventProgress.emit({ event: 'started' });
|
|
37
|
+
let resolverServers = [];
|
|
38
|
+
if (args.resolverText) {
|
|
39
|
+
try {
|
|
40
|
+
resolverServers = normalizeSelectedDnsResolvers(args.resolverText);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid DNS resolver selection'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
37
46
|
const { metadata: generationMetadata, result } = await generateDiscoveryCandidates(args, dependencies, context);
|
|
38
47
|
const candidates = result.candidates
|
|
39
48
|
.filter((candidate) => typeof candidate.domain === 'string' && candidate.domain.length > 0)
|
|
@@ -51,15 +60,6 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
51
60
|
return EXIT_CODES.SUCCESS;
|
|
52
61
|
}
|
|
53
62
|
const classify = dependencies.classifyQuery || classifyQuery;
|
|
54
|
-
let resolverServers = [];
|
|
55
|
-
if (args.resolverText) {
|
|
56
|
-
try {
|
|
57
|
-
resolverServers = normalizeSelectedDnsResolvers(args.resolverText);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid DNS resolver selection'));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
63
|
const allowlist = await readAllowlist(args.allowlistSource, dependencies, context, classify);
|
|
64
64
|
const checkpointWriter = dependencies.createBulkCheckpointWriter || createBulkCheckpointWriter;
|
|
65
65
|
const checkpoint = args.checkpoint
|
package/cli/discovery-scan.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
3
4
|
import { runBulkLookups } from "./bulk.mjs";
|
|
4
5
|
import { REGISTERED_STATES, availabilityState, bulkDnsSummary } from "./bulk-output.mjs";
|
|
5
6
|
import { cliCsvCell } from "./csv.mjs";
|
|
6
7
|
import { CliUsageError } from "./errors.mjs";
|
|
8
|
+
import { terminalSafeJson } from "./formatters/json.mjs";
|
|
7
9
|
export const CLI_DISCOVERY_SCAN_SCHEMA = 'whoisleuth.cli.discovery-scan';
|
|
8
10
|
export const CLI_DISCOVERY_SCAN_ITEM_SCHEMA = 'whoisleuth.cli.discovery-scan.item';
|
|
9
11
|
export const CLI_DISCOVERY_SCAN_VERSION = 1;
|
|
@@ -30,7 +32,12 @@ async function readDiscoveryScanListBounded(stream, limit = MAX_DISCOVERY_SCAN_A
|
|
|
30
32
|
throw new CliUsageError(`Discovery scan allowlists are limited to ${limit} bytes.`);
|
|
31
33
|
chunks.push(buffer);
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
try {
|
|
36
|
+
return decodeBoundedUtf8(Buffer.concat(chunks), 'Discovery scan allowlist');
|
|
37
|
+
}
|
|
38
|
+
catch (cause) {
|
|
39
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : 'Discovery scan allowlist must contain valid UTF-8 text.');
|
|
40
|
+
}
|
|
34
41
|
}
|
|
35
42
|
function parseDiscoveryScanAllowlist(text, classifyQuery) {
|
|
36
43
|
if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_DISCOVERY_SCAN_ALLOWLIST_BYTES) {
|
|
@@ -232,7 +239,10 @@ function buildDiscoveryScanDocument(candidates, items, metadata, allowlist, snap
|
|
|
232
239
|
collection: {
|
|
233
240
|
chunkSize: metadata.chunkSize,
|
|
234
241
|
concurrency: metadata.concurrency,
|
|
235
|
-
resolver: metadata.resolverServers.length ? '
|
|
242
|
+
resolver: metadata.resolverServers.length ? 'mixed' : 'system_default',
|
|
243
|
+
dnsResolver: metadata.resolverServers.length ? 'analyst_selected' : 'system_default',
|
|
244
|
+
registryEndpointResolution: 'system_default',
|
|
245
|
+
serviceAddressResolution: 'system_default',
|
|
236
246
|
resolverServers: [...metadata.resolverServers],
|
|
237
247
|
},
|
|
238
248
|
filter: metadata.filter,
|
|
@@ -258,6 +268,9 @@ function buildDiscoveryScanDocument(candidates, items, metadata, allowlist, snap
|
|
|
258
268
|
'Failed, partial, rate-limited, and unavailable sources remain inconclusive and are not interpreted as absence or safety.',
|
|
259
269
|
'The allowlist changes review priority only. It does not remove collected evidence or assert that a domain is benign.',
|
|
260
270
|
'Relationship groups use exact bounded DNS observations from this run and can include common shared infrastructure.',
|
|
271
|
+
...(metadata.resolverServers.length
|
|
272
|
+
? ['The analyst-selected resolver applies to DNS record and delegation evidence only. Registry, WHOIS, HTTP, redirect, favicon, and TLS endpoint address resolution remains system-default and is reported separately.']
|
|
273
|
+
: []),
|
|
261
274
|
],
|
|
262
275
|
};
|
|
263
276
|
}
|
|
@@ -286,7 +299,7 @@ async function runDiscoveryScanChunks(queries, options) {
|
|
|
286
299
|
}
|
|
287
300
|
function formatDiscoveryScanJsonLines(document) {
|
|
288
301
|
return document.results.length
|
|
289
|
-
? `${document.results.map((item) =>
|
|
302
|
+
? `${document.results.map((item) => terminalSafeJson({ ...item, generatedAt: document.generatedAt })).join('\n')}\n`
|
|
290
303
|
: '';
|
|
291
304
|
}
|
|
292
305
|
function formatDiscoveryScanCsv(document) {
|
|
@@ -311,6 +324,7 @@ function formatTerminalDiscoveryScan(document) {
|
|
|
311
324
|
`Matched ${document.summary.matched}`,
|
|
312
325
|
`Source failures ${document.summary.failed}`,
|
|
313
326
|
`Relationships ${document.relationships.length}`,
|
|
327
|
+
`Resolution ${document.collection.resolver === 'mixed' ? 'mixed (selected DNS; system service addresses)' : 'system default'}`,
|
|
314
328
|
'',
|
|
315
329
|
'Review queue',
|
|
316
330
|
];
|
|
@@ -3,6 +3,8 @@ import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
|
3
3
|
import { writePrivateFile } from "./output-file.mjs";
|
|
4
4
|
import { isValidAsciiDomainName } from "../lib/hostname.mjs";
|
|
5
5
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
6
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
7
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
6
8
|
export const CLI_DISCOVERY_SNAPSHOT_SCHEMA = 'whoisleuth.cli.discovery-snapshot';
|
|
7
9
|
export const CLI_DISCOVERY_SNAPSHOT_VERSION = 1;
|
|
8
10
|
export const MAX_DISCOVERY_SNAPSHOT_BYTES = 4 * 1024 * 1024;
|
|
@@ -30,16 +32,24 @@ function normalizedDomains(value) {
|
|
|
30
32
|
}
|
|
31
33
|
return [...output].sort();
|
|
32
34
|
}
|
|
35
|
+
function normalizedTimestamp(value) {
|
|
36
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
37
|
+
if (!normalized)
|
|
38
|
+
throw new CliUsageError('Discovery snapshot collection time is invalid.');
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
33
41
|
function parseDiscoverySnapshot(text, expectedConfigurationDigest) {
|
|
34
42
|
if (Buffer.byteLength(text, 'utf8') > MAX_DISCOVERY_SNAPSHOT_BYTES) {
|
|
35
43
|
throw new CliUsageError(`Discovery snapshot input is limited to ${MAX_DISCOVERY_SNAPSHOT_BYTES} bytes.`);
|
|
36
44
|
}
|
|
45
|
+
const normalized = text.replace(/^\uFEFF/u, '');
|
|
37
46
|
let parsed;
|
|
38
47
|
try {
|
|
39
|
-
|
|
48
|
+
scanBoundedJson(normalized);
|
|
49
|
+
parsed = JSON.parse(normalized);
|
|
40
50
|
}
|
|
41
51
|
catch {
|
|
42
|
-
throw new CliUsageError('Discovery snapshot must be valid JSON.');
|
|
52
|
+
throw new CliUsageError('Discovery snapshot must be valid bounded JSON without duplicate keys.');
|
|
43
53
|
}
|
|
44
54
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
45
55
|
throw new CliUsageError('Discovery snapshot must be one JSON object.');
|
|
@@ -51,16 +61,14 @@ function parseDiscoverySnapshot(text, expectedConfigurationDigest) {
|
|
|
51
61
|
if (value.configurationDigestSha256 !== expectedConfigurationDigest) {
|
|
52
62
|
throw new CliUsageError('Discovery snapshot configuration does not match this run. Choose another state file or restore the original discovery controls.');
|
|
53
63
|
}
|
|
54
|
-
|
|
55
|
-
throw new CliUsageError('Discovery snapshot collection time is invalid.');
|
|
56
|
-
}
|
|
64
|
+
const generatedAt = normalizedTimestamp(value.generatedAt);
|
|
57
65
|
const candidates = normalizedDomains(value.candidates);
|
|
58
66
|
if (value.candidateCount !== candidates.length)
|
|
59
67
|
throw new CliUsageError('Discovery snapshot candidate count is inconsistent.');
|
|
60
68
|
return {
|
|
61
69
|
schema: CLI_DISCOVERY_SNAPSHOT_SCHEMA,
|
|
62
70
|
version: CLI_DISCOVERY_SNAPSHOT_VERSION,
|
|
63
|
-
generatedAt
|
|
71
|
+
generatedAt,
|
|
64
72
|
configurationDigestSha256: expectedConfigurationDigest,
|
|
65
73
|
candidateCount: candidates.length,
|
|
66
74
|
candidates,
|
|
@@ -95,7 +103,7 @@ async function updateDiscoverySnapshot(path, candidatesValue, configuration, gen
|
|
|
95
103
|
const document = {
|
|
96
104
|
schema: CLI_DISCOVERY_SNAPSHOT_SCHEMA,
|
|
97
105
|
version: CLI_DISCOVERY_SNAPSHOT_VERSION,
|
|
98
|
-
generatedAt:
|
|
106
|
+
generatedAt: normalizedTimestamp(generatedAt),
|
|
99
107
|
configurationDigestSha256: digest,
|
|
100
108
|
candidateCount: candidates.length,
|
|
101
109
|
candidates,
|