@slicedearth/whoisleuth-cli 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +16 -9
- package/docs/cli.md +4 -2
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +225 -61
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Shared strict primitives for the Lookup response envelope and its owned
|
|
2
|
+
// child-profile contracts. These checks validate without normalising or
|
|
3
|
+
// copying untrusted values.
|
|
4
|
+
import { MAX_OBSERVATION_DIAGNOSTICS } from "../packages/evidence/observation.mjs";
|
|
5
|
+
import { MAX_HTTP_PROVENANCE_URL } from "./http-evidence-bounds.mjs";
|
|
6
|
+
const LOOKUP_CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
7
|
+
const MAX_LOOKUP_CONTRACT_HOST_LENGTH = 253;
|
|
8
|
+
function isJsonObject(value) {
|
|
9
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function validBoundedString(value, maximum, allowEmpty = false) {
|
|
12
|
+
return typeof value === 'string'
|
|
13
|
+
&& (allowEmpty || Boolean(value))
|
|
14
|
+
&& value.length <= maximum
|
|
15
|
+
&& !LOOKUP_CONTROL_CHAR_RE.test(value);
|
|
16
|
+
}
|
|
17
|
+
function validUint(value, maximum) {
|
|
18
|
+
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum;
|
|
19
|
+
}
|
|
20
|
+
function validStringArray(value, maximumItems, maximumLength) {
|
|
21
|
+
return Array.isArray(value)
|
|
22
|
+
&& value.length <= maximumItems
|
|
23
|
+
&& value.every((item) => validBoundedString(item, maximumLength));
|
|
24
|
+
}
|
|
25
|
+
function validOptionalNullableText(value, maximumLength) {
|
|
26
|
+
return value === undefined || value === null || validBoundedString(value, maximumLength);
|
|
27
|
+
}
|
|
28
|
+
function validHttpProvenanceUrl(value) {
|
|
29
|
+
if (typeof value !== 'string'
|
|
30
|
+
|| !value
|
|
31
|
+
|| value.length > MAX_HTTP_PROVENANCE_URL
|
|
32
|
+
|| LOOKUP_CONTROL_CHAR_RE.test(value))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(value);
|
|
36
|
+
return ['http:', 'https:'].includes(parsed.protocol)
|
|
37
|
+
&& Boolean(parsed.hostname)
|
|
38
|
+
&& !parsed.username
|
|
39
|
+
&& !parsed.password
|
|
40
|
+
&& !parsed.search
|
|
41
|
+
&& !parsed.hash
|
|
42
|
+
&& parsed.href === value;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function normalizedDomain(value) {
|
|
49
|
+
if (typeof value !== 'string'
|
|
50
|
+
|| !value.trim()
|
|
51
|
+
|| value.length > MAX_LOOKUP_CONTRACT_HOST_LENGTH
|
|
52
|
+
|| LOOKUP_CONTROL_CHAR_RE.test(value)
|
|
53
|
+
|| /[\s/?#@\\:]/u.test(value)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(`https://${value.trim().replace(/\.$/u, '')}/`);
|
|
58
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
59
|
+
const labels = hostname.split('.');
|
|
60
|
+
return labels.length >= 2
|
|
61
|
+
&& hostname.length <= MAX_LOOKUP_CONTRACT_HOST_LENGTH
|
|
62
|
+
&& labels.every((label) => (label.length <= 63
|
|
63
|
+
&& /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)))
|
|
64
|
+
? hostname
|
|
65
|
+
: null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function validObservationDiagnosticValue(value, depth = 0) {
|
|
72
|
+
if (typeof value === 'boolean')
|
|
73
|
+
return true;
|
|
74
|
+
if (typeof value === 'number')
|
|
75
|
+
return Number.isFinite(value);
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
return value.length <= 240 && !LOOKUP_CONTROL_CHAR_RE.test(value);
|
|
78
|
+
}
|
|
79
|
+
if (!isJsonObject(value) || depth >= 2)
|
|
80
|
+
return false;
|
|
81
|
+
const keys = Object.keys(value);
|
|
82
|
+
return keys.length <= 6
|
|
83
|
+
&& keys.every((key) => ['status', 'error', 'detail', 'truncated', 'discarded', 'count'].includes(key))
|
|
84
|
+
&& keys.every((key) => validObservationDiagnosticValue(value[key], depth + 1));
|
|
85
|
+
}
|
|
86
|
+
function validObservationDiagnostics(value) {
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
return true;
|
|
89
|
+
if (!isJsonObject(value))
|
|
90
|
+
return false;
|
|
91
|
+
const entries = Object.entries(value);
|
|
92
|
+
return entries.length <= MAX_OBSERVATION_DIAGNOSTICS
|
|
93
|
+
&& entries.every(([key, item]) => (Boolean(key)
|
|
94
|
+
&& key.length <= 40
|
|
95
|
+
&& /^[a-z0-9_-]+$/iu.test(key)
|
|
96
|
+
&& validObservationDiagnosticValue(item)));
|
|
97
|
+
}
|
|
98
|
+
export { LOOKUP_CONTROL_CHAR_RE, isJsonObject, normalizedDomain, validBoundedString, validHttpProvenanceUrl, validObservationDiagnostics, validOptionalNullableText, validStringArray, validUint, };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Lookup diagnostics, bounded source errors and optional deep-lookup timing.
|
|
2
|
+
import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
|
|
3
|
+
import { FEATURE_DISABLED_ERROR_CODE } from "./feature-policy.mjs";
|
|
4
|
+
const LOOKUP_DIAGNOSTICS_VERSION = 8;
|
|
5
|
+
const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
|
|
6
|
+
const LOOKUP_TIMING_VERSION = 1;
|
|
7
|
+
const MAX_LOOKUP_TIMING_MS = 120_000;
|
|
8
|
+
const LOOKUP_TIMING_SOURCE_ORDER = Object.freeze([
|
|
9
|
+
'rdap',
|
|
10
|
+
'whois',
|
|
11
|
+
'domain_evidence',
|
|
12
|
+
'reverse_dns',
|
|
13
|
+
'registrar_rdap',
|
|
14
|
+
'network_context',
|
|
15
|
+
'security_txt',
|
|
16
|
+
'external_intelligence',
|
|
17
|
+
'malware_host_intelligence',
|
|
18
|
+
'malware_ioc_intelligence',
|
|
19
|
+
]);
|
|
20
|
+
const LOOKUP_ERROR_CODES = Object.freeze({
|
|
21
|
+
AUTH_REQUIRED: 'AUTH_REQUIRED',
|
|
22
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
23
|
+
CONCURRENCY_LIMITED: OPERATION_BUDGET_ERROR_CODE,
|
|
24
|
+
FEATURE_DISABLED: FEATURE_DISABLED_ERROR_CODE,
|
|
25
|
+
MISSING_QUERY: 'MISSING_QUERY',
|
|
26
|
+
INVALID_QUERY: 'INVALID_QUERY',
|
|
27
|
+
LOOKUP_FAILED: 'LOOKUP_FAILED',
|
|
28
|
+
RDAP_UPSTREAM_FAILED: 'RDAP_UPSTREAM_FAILED',
|
|
29
|
+
RDAP_UNSUPPORTED: 'RDAP_UNSUPPORTED',
|
|
30
|
+
WHOIS_UPSTREAM_FAILED: 'WHOIS_UPSTREAM_FAILED',
|
|
31
|
+
AVAILABILITY_CHECK_FAILED: 'AVAILABILITY_CHECK_FAILED',
|
|
32
|
+
});
|
|
33
|
+
function errorMessage(err, fallback) {
|
|
34
|
+
const message = err && typeof err === 'object' && 'message' in err
|
|
35
|
+
? err.message
|
|
36
|
+
: null;
|
|
37
|
+
return String(message || fallback);
|
|
38
|
+
}
|
|
39
|
+
function boundedSourceDetail(err, fallback) {
|
|
40
|
+
return errorMessage(err, fallback)
|
|
41
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.trim()
|
|
44
|
+
.slice(0, 240) || fallback;
|
|
45
|
+
}
|
|
46
|
+
function boundedTimingMs(value) {
|
|
47
|
+
if (!Number.isFinite(value))
|
|
48
|
+
return 0;
|
|
49
|
+
return Math.min(MAX_LOOKUP_TIMING_MS, Math.max(0, Math.round(value)));
|
|
50
|
+
}
|
|
51
|
+
function createLookupTimingTracker(enabled, now) {
|
|
52
|
+
const lookupStartedAt = now();
|
|
53
|
+
const entries = new Map();
|
|
54
|
+
function measure(source, operation) {
|
|
55
|
+
const sourceStartedAt = now();
|
|
56
|
+
return Promise.resolve()
|
|
57
|
+
.then(operation)
|
|
58
|
+
.then((value) => {
|
|
59
|
+
const finishedAt = now();
|
|
60
|
+
entries.set(source, {
|
|
61
|
+
source,
|
|
62
|
+
outcome: 'fulfilled',
|
|
63
|
+
durationMs: boundedTimingMs(finishedAt - sourceStartedAt),
|
|
64
|
+
completedAfterMs: boundedTimingMs(finishedAt - lookupStartedAt),
|
|
65
|
+
});
|
|
66
|
+
return value;
|
|
67
|
+
}, (error) => {
|
|
68
|
+
const finishedAt = now();
|
|
69
|
+
entries.set(source, {
|
|
70
|
+
source,
|
|
71
|
+
outcome: 'rejected',
|
|
72
|
+
durationMs: boundedTimingMs(finishedAt - sourceStartedAt),
|
|
73
|
+
completedAfterMs: boundedTimingMs(finishedAt - lookupStartedAt),
|
|
74
|
+
});
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
measure(source, operation) {
|
|
80
|
+
return enabled ? measure(source, operation) : Promise.resolve().then(operation);
|
|
81
|
+
},
|
|
82
|
+
snapshot() {
|
|
83
|
+
if (!enabled)
|
|
84
|
+
return null;
|
|
85
|
+
const totalMs = boundedTimingMs(now() - lookupStartedAt);
|
|
86
|
+
return {
|
|
87
|
+
version: LOOKUP_TIMING_VERSION,
|
|
88
|
+
totalMs,
|
|
89
|
+
sources: LOOKUP_TIMING_SOURCE_ORDER
|
|
90
|
+
.map((source) => entries.get(source))
|
|
91
|
+
.filter((entry) => Boolean(entry)),
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, boundedSourceDetail, createLookupTimingTracker, errorMessage, };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// the Lookup response trust boundary, and the display projection.
|
|
3
3
|
export const MAX_LOOKUP_DNS_RECORDS_PER_TYPE = 16;
|
|
4
4
|
export const MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS = 8;
|
|
5
|
+
export const TLS_PROFILE_VERSION = 3;
|
|
5
6
|
export const MAX_LOOKUP_TLS_CHAIN_CERTIFICATES = 8;
|
|
6
7
|
export const MAX_LOOKUP_TLS_ALT_NAMES = 50;
|
|
7
8
|
export const MAX_LOOKUP_TLS_NAME_VALUES = 4;
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// Lookup result shaping after the source plan has settled. This module starts no
|
|
2
|
+
// collection and preserves compact, diagnostics and enrichment response shapes.
|
|
3
|
+
import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
|
|
4
|
+
import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
|
|
5
|
+
import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, errorMessage, } from "./lookup-diagnostics.mjs";
|
|
6
|
+
import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
|
|
7
|
+
import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
|
|
8
|
+
import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
|
|
9
|
+
import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
|
|
10
|
+
import { buildRegistryInsights } from "./registry-insights.mjs";
|
|
11
|
+
import { buildRegistrarStanding } from "./registrar-standing.mjs";
|
|
12
|
+
import { resolveRegistrarIanaId } from "./registrar-standing-contract.mjs";
|
|
13
|
+
import { securityTxtUnavailable } from "./security-txt.mjs";
|
|
14
|
+
import { parseWhoisChain } from "./whois.mjs";
|
|
15
|
+
function withoutNestedPublicationMetadata(value) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
|
+
return value;
|
|
18
|
+
const output = { ...value };
|
|
19
|
+
delete output.publicationMetadata;
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
function withoutNestedDeliveryMetadata(value) {
|
|
23
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
24
|
+
return value;
|
|
25
|
+
const output = { ...value };
|
|
26
|
+
if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
|
|
27
|
+
const response = { ...output.response };
|
|
28
|
+
delete response.deliveryMetadata;
|
|
29
|
+
output.response = response;
|
|
30
|
+
}
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
async function buildUnifiedLookupResponse(context) {
|
|
34
|
+
const { availabilityEnabled, availabilityPromise, classified, compact, dnsIntelligenceEnabled, fast, inspectCertificateWarningData, networkContextPromise, options, rdapEnabled, rdapPromise, registrarRdapPromise, reverseDnsEligible, reverseDnsPromise, securityTxtPromise, skipWhois, threatfoxIntelligencePromise, timing, urlhausIntelligencePromise, urlscanIntelligencePromise, whoisEnabled, whoisPromise, } = context;
|
|
35
|
+
const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
|
|
36
|
+
rdapPromise,
|
|
37
|
+
whoisPromise,
|
|
38
|
+
availabilityPromise,
|
|
39
|
+
reverseDnsPromise,
|
|
40
|
+
registrarRdapPromise,
|
|
41
|
+
networkContextPromise,
|
|
42
|
+
securityTxtPromise,
|
|
43
|
+
urlscanIntelligencePromise,
|
|
44
|
+
urlhausIntelligencePromise,
|
|
45
|
+
threatfoxIntelligencePromise,
|
|
46
|
+
]);
|
|
47
|
+
options.signal?.throwIfAborted();
|
|
48
|
+
const rdapRecord = rdapResult.status === 'fulfilled' ? rdapResult.value : null;
|
|
49
|
+
const whoisChain = whoisResult.status === 'fulfilled' ? whoisResult.value : null;
|
|
50
|
+
const rdapAttempts = rdapRecord && Array.isArray(rdapRecord.attempts)
|
|
51
|
+
? rdapRecord.attempts
|
|
52
|
+
: rdapResult.status === 'rejected' && Array.isArray(rdapResult.reason?.attempts)
|
|
53
|
+
? rdapResult.reason.attempts
|
|
54
|
+
: [];
|
|
55
|
+
let registrarRdap = null;
|
|
56
|
+
const registryRdapUsable = rdapRecord && rdapRecord.upstreamStatus === 200 && rdapRecord.parsed;
|
|
57
|
+
if (classified.type === 'domain' && rdapEnabled && !compact && registryRdapUsable) {
|
|
58
|
+
if (fast) {
|
|
59
|
+
registrarRdap = {
|
|
60
|
+
status: 'skipped',
|
|
61
|
+
detail: 'Registrar RDAP is omitted in fast RDAP-only mode.',
|
|
62
|
+
endpoint: null,
|
|
63
|
+
transportSecurity: null,
|
|
64
|
+
upstreamStatus: null,
|
|
65
|
+
fetchedAt: null,
|
|
66
|
+
attempt: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
else if (registrarRdapResult.status === 'fulfilled') {
|
|
70
|
+
registrarRdap = registrarRdapResult.value;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
registrarRdap = registrarRdapResult.reason?.registrarRdap || {
|
|
74
|
+
status: 'error',
|
|
75
|
+
detail: boundedSourceDetail(registrarRdapResult.reason, 'Registrar RDAP lookup failed'),
|
|
76
|
+
endpoint: null,
|
|
77
|
+
transportSecurity: null,
|
|
78
|
+
upstreamStatus: null,
|
|
79
|
+
fetchedAt: null,
|
|
80
|
+
attempt: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const rdap = !rdapEnabled
|
|
85
|
+
? { skipped: true, detail: 'RDAP is disabled by deployment policy.' }
|
|
86
|
+
: rdapRecord
|
|
87
|
+
? { ...rdapRecord, ...(registrarRdap ? { registrarRdap } : {}) }
|
|
88
|
+
: {
|
|
89
|
+
error: rdapResult.status === 'rejected'
|
|
90
|
+
? boundedSourceDetail(rdapResult.reason, 'RDAP lookup failed')
|
|
91
|
+
: 'No RDAP registry found for this query via IANA bootstrap',
|
|
92
|
+
attempts: rdapAttempts,
|
|
93
|
+
};
|
|
94
|
+
let whois;
|
|
95
|
+
if (!whoisEnabled) {
|
|
96
|
+
whois = { skipped: true, detail: 'WHOIS is disabled by deployment policy.' };
|
|
97
|
+
}
|
|
98
|
+
else if (skipWhois) {
|
|
99
|
+
whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
|
|
100
|
+
}
|
|
101
|
+
else if (Array.isArray(whoisChain)) {
|
|
102
|
+
whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
whois = {
|
|
106
|
+
error: whoisResult.status === 'rejected'
|
|
107
|
+
? boundedSourceDetail(whoisResult.reason, 'WHOIS lookup failed')
|
|
108
|
+
: 'WHOIS returned no referral chain',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
let availability = { applicable: false, type: classified.type };
|
|
112
|
+
if (classified.type === 'domain') {
|
|
113
|
+
if (!availabilityEnabled) {
|
|
114
|
+
availability = {
|
|
115
|
+
applicable: true,
|
|
116
|
+
domain: classified.value,
|
|
117
|
+
state: 'unknown',
|
|
118
|
+
confidence: 'low',
|
|
119
|
+
disabled: true,
|
|
120
|
+
detail: 'Availability analysis is disabled by deployment policy.',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
else if (availabilityResult.status === 'fulfilled') {
|
|
124
|
+
const result = availabilityResult.value;
|
|
125
|
+
availability = {
|
|
126
|
+
applicable: true,
|
|
127
|
+
domain: classified.value,
|
|
128
|
+
inputHostname: classified.inputHostname,
|
|
129
|
+
registrableDomain: classified.registrableDomain,
|
|
130
|
+
isSubdomain: classified.isSubdomain,
|
|
131
|
+
...result,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
availability = {
|
|
136
|
+
applicable: true,
|
|
137
|
+
domain: classified.value,
|
|
138
|
+
state: 'unknown',
|
|
139
|
+
confidence: 'low',
|
|
140
|
+
detail: errorMessage(availabilityResult.reason, 'Availability lookup failed'),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const rdapStatus = !rdapEnabled
|
|
145
|
+
? 'disabled'
|
|
146
|
+
: rdapResult.status === 'rejected'
|
|
147
|
+
? 'error'
|
|
148
|
+
: !rdapRecord
|
|
149
|
+
? 'unsupported'
|
|
150
|
+
: rdapRecord.upstreamStatus === 404 ? 'not_found' : 'success';
|
|
151
|
+
const whoisStatus = !whoisEnabled
|
|
152
|
+
? 'disabled'
|
|
153
|
+
: skipWhois
|
|
154
|
+
? 'skipped'
|
|
155
|
+
: whoisResult.status === 'rejected' || !Array.isArray(whoisChain)
|
|
156
|
+
? 'error'
|
|
157
|
+
: whoisChain.length === 1
|
|
158
|
+
? 'unsupported'
|
|
159
|
+
: whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
|
|
160
|
+
const availabilityStatus = classified.type !== 'domain'
|
|
161
|
+
? 'not_applicable'
|
|
162
|
+
: !availabilityEnabled ? 'disabled'
|
|
163
|
+
: availabilityResult.status === 'rejected' ? 'error' : 'complete';
|
|
164
|
+
const reverseDns = reverseDnsEligible
|
|
165
|
+
? !dnsIntelligenceEnabled
|
|
166
|
+
? skippedReverseDnsIntelligence()
|
|
167
|
+
: reverseDnsResult.status === 'fulfilled' && reverseDnsResult.value
|
|
168
|
+
? reverseDnsResult.value
|
|
169
|
+
: failedReverseDnsIntelligence(reverseDnsResult.status === 'rejected'
|
|
170
|
+
? reverseDnsResult.reason
|
|
171
|
+
: 'Reverse DNS lookup returned no result')
|
|
172
|
+
: null;
|
|
173
|
+
// This is static access-policy context only. It performs no network work and
|
|
174
|
+
// is deliberately excluded from compact Bulk responses and from every
|
|
175
|
+
// availability input.
|
|
176
|
+
const registryAccess = classified.type === 'domain' && !compact
|
|
177
|
+
? registryAccessDiagnosticFor(classified.value)
|
|
178
|
+
: null;
|
|
179
|
+
const networkContext = networkContextPromise && networkContextResult.status === 'fulfilled'
|
|
180
|
+
? networkContextResult.value
|
|
181
|
+
: null;
|
|
182
|
+
const securityTxt = securityTxtPromise
|
|
183
|
+
? securityTxtResult.status === 'fulfilled'
|
|
184
|
+
? securityTxtResult.value
|
|
185
|
+
: securityTxtUnavailable(classified.inputHostname, securityTxtResult.reason)
|
|
186
|
+
: null;
|
|
187
|
+
// SSLBL comparison is an exact, local, deep-only enrichment over the leaf
|
|
188
|
+
// certificate already observed by the bounded TLS collector. It performs no
|
|
189
|
+
// request, never contributes to availability, and is omitted from compact
|
|
190
|
+
// Bulk and monitoring responses.
|
|
191
|
+
const sslbl = classified.type === 'domain' && !fast && !compact
|
|
192
|
+
? inspectCertificateWarningData(availability && typeof availability.tls === 'object' ? availability.tls : null, {
|
|
193
|
+
...(options.sslblSnapshot === undefined ? {} : { snapshot: options.sslblSnapshot }),
|
|
194
|
+
...(options.sslblNow === undefined ? {} : { now: options.sslblNow }),
|
|
195
|
+
})
|
|
196
|
+
: null;
|
|
197
|
+
const networkContextRdap = networkContext && typeof networkContext.rdap === 'object' && networkContext.rdap
|
|
198
|
+
? networkContext.rdap
|
|
199
|
+
: null;
|
|
200
|
+
const networkContextEndpoint = networkContext && typeof networkContext.endpoint === 'object' && networkContext.endpoint
|
|
201
|
+
? networkContext.endpoint
|
|
202
|
+
: null;
|
|
203
|
+
const lookupTiming = timing.snapshot();
|
|
204
|
+
const diagnostics = {
|
|
205
|
+
version: lookupTiming ? LOOKUP_DIAGNOSTICS_VERSION : LOOKUP_LEGACY_DIAGNOSTICS_VERSION,
|
|
206
|
+
...(lookupTiming ? { timing: lookupTiming } : {}),
|
|
207
|
+
...(registryAccess ? { registryAccess } : {}),
|
|
208
|
+
rdap: {
|
|
209
|
+
status: rdapStatus,
|
|
210
|
+
errorCode: rdapStatus === 'disabled'
|
|
211
|
+
? LOOKUP_ERROR_CODES.FEATURE_DISABLED
|
|
212
|
+
: rdapStatus === 'error'
|
|
213
|
+
? LOOKUP_ERROR_CODES.RDAP_UPSTREAM_FAILED
|
|
214
|
+
: rdapStatus === 'unsupported' ? LOOKUP_ERROR_CODES.RDAP_UNSUPPORTED : null,
|
|
215
|
+
endpoint: rdapRecord ? rdapRecord.rdapServer || null : null,
|
|
216
|
+
transportSecurity: rdapRecord ? rdapRecord.transportSecurity || null : null,
|
|
217
|
+
httpStatus: rdapRecord ? rdapRecord.upstreamStatus ?? null : null,
|
|
218
|
+
fetchedAt: rdapRecord ? rdapRecord.fetchedAt || null : null,
|
|
219
|
+
attempts: rdapAttempts,
|
|
220
|
+
...(registrarRdap ? {
|
|
221
|
+
registrar: {
|
|
222
|
+
status: registrarRdap.status,
|
|
223
|
+
endpoint: registrarRdap.endpoint || null,
|
|
224
|
+
transportSecurity: registrarRdap.transportSecurity || null,
|
|
225
|
+
httpStatus: registrarRdap.upstreamStatus ?? null,
|
|
226
|
+
fetchedAt: registrarRdap.fetchedAt || null,
|
|
227
|
+
attempt: registrarRdap.attempt || null,
|
|
228
|
+
},
|
|
229
|
+
} : {}),
|
|
230
|
+
},
|
|
231
|
+
whois: {
|
|
232
|
+
status: whoisStatus,
|
|
233
|
+
errorCode: whoisStatus === 'disabled'
|
|
234
|
+
? LOOKUP_ERROR_CODES.FEATURE_DISABLED
|
|
235
|
+
: whoisStatus === 'error' ? LOOKUP_ERROR_CODES.WHOIS_UPSTREAM_FAILED : null,
|
|
236
|
+
queriedAt: Array.isArray(whoisChain) && whoisChain[0] ? whoisChain[0].queriedAt || null : null,
|
|
237
|
+
authoritativeHop: whois.parsed ? whois.parsed.authoritativeHop || null : null,
|
|
238
|
+
failedHop: whois.parsed ? whois.parsed.failedHop || null : null,
|
|
239
|
+
conflictingHop: whois.parsed ? whois.parsed.conflictingHop || null : null,
|
|
240
|
+
},
|
|
241
|
+
availability: {
|
|
242
|
+
status: availabilityStatus,
|
|
243
|
+
errorCode: availabilityStatus === 'disabled'
|
|
244
|
+
? LOOKUP_ERROR_CODES.FEATURE_DISABLED
|
|
245
|
+
: availabilityStatus === 'error' ? LOOKUP_ERROR_CODES.AVAILABILITY_CHECK_FAILED : null,
|
|
246
|
+
resultState: availability.applicable === true ? availability.state || 'unknown' : null,
|
|
247
|
+
},
|
|
248
|
+
...(reverseDns ? {
|
|
249
|
+
reverseDns: {
|
|
250
|
+
status: reverseDns.status,
|
|
251
|
+
observedAt: reverseDns.observedAt,
|
|
252
|
+
complete: reverseDns.complete,
|
|
253
|
+
truncated: reverseDns.truncated,
|
|
254
|
+
},
|
|
255
|
+
} : {}),
|
|
256
|
+
...(networkContext ? {
|
|
257
|
+
network: {
|
|
258
|
+
status: networkContext.status,
|
|
259
|
+
address: networkContextEndpoint?.address || null,
|
|
260
|
+
family: networkContextEndpoint?.family || null,
|
|
261
|
+
addressSource: networkContextEndpoint?.selectedFrom || null,
|
|
262
|
+
endpoint: networkContextRdap?.endpoint || null,
|
|
263
|
+
transportSecurity: networkContextRdap?.transportSecurity || null,
|
|
264
|
+
httpStatus: networkContextRdap?.httpStatus ?? null,
|
|
265
|
+
fetchedAt: networkContextRdap?.fetchedAt || null,
|
|
266
|
+
attempts: networkContextRdap?.attempts || [],
|
|
267
|
+
},
|
|
268
|
+
} : {}),
|
|
269
|
+
...(securityTxt ? {
|
|
270
|
+
securityTxt: {
|
|
271
|
+
status: securityTxt.status,
|
|
272
|
+
state: securityTxt.state,
|
|
273
|
+
endpoint: securityTxt.finalUrl,
|
|
274
|
+
httpStatus: securityTxt.httpStatus,
|
|
275
|
+
observedAt: securityTxt.observedAt,
|
|
276
|
+
complete: securityTxt.complete,
|
|
277
|
+
truncated: securityTxt.truncated,
|
|
278
|
+
},
|
|
279
|
+
} : {}),
|
|
280
|
+
...(sslbl ? {
|
|
281
|
+
sslbl: {
|
|
282
|
+
status: sslbl.status,
|
|
283
|
+
verdict: sslbl.verdict,
|
|
284
|
+
observedAt: sslbl.observedAt,
|
|
285
|
+
complete: sslbl.complete,
|
|
286
|
+
snapshotUpdatedAt: sslbl.snapshot.sourceUpdatedAt,
|
|
287
|
+
},
|
|
288
|
+
} : {}),
|
|
289
|
+
};
|
|
290
|
+
// Bulk triage only consumes the derived availability evidence and source
|
|
291
|
+
// diagnostics. Omitting raw RDAP JSON and multi-hop WHOIS bodies from that
|
|
292
|
+
// opt-in response prevents large scans from downloading and retaining the
|
|
293
|
+
// same registry payloads the backend already used to build `availability`.
|
|
294
|
+
if (compact) {
|
|
295
|
+
const bulkComparison = !fast
|
|
296
|
+
? buildBulkComparisonEvidence(availability)
|
|
297
|
+
: null;
|
|
298
|
+
const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, pageIdentity: richPageIdentity, http: richHttp, ...compactAvailability } = availability;
|
|
299
|
+
return {
|
|
300
|
+
availability: {
|
|
301
|
+
...compactAvailability,
|
|
302
|
+
...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
|
|
303
|
+
...(richHttp !== undefined ? { http: withoutNestedDeliveryMetadata(richHttp) } : {}),
|
|
304
|
+
...(bulkComparison ? { bulkComparison } : {}),
|
|
305
|
+
},
|
|
306
|
+
diagnostics,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const targetDomain = classified.registrableDomain || classified.value;
|
|
310
|
+
const threatIntelligenceProviders = [];
|
|
311
|
+
if (urlscanIntelligencePromise) {
|
|
312
|
+
threatIntelligenceProviders.push(urlscanIntelligenceResult.status === 'fulfilled' && urlscanIntelligenceResult.value
|
|
313
|
+
? urlscanIntelligenceResult.value
|
|
314
|
+
: createThreatIntelligenceResult(URLSCAN_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Archived provider intelligence could not be completed.' }));
|
|
315
|
+
}
|
|
316
|
+
if (urlhausIntelligencePromise) {
|
|
317
|
+
threatIntelligenceProviders.push(urlhausIntelligenceResult.status === 'fulfilled' && urlhausIntelligenceResult.value
|
|
318
|
+
? urlhausIntelligenceResult.value
|
|
319
|
+
: createThreatIntelligenceResult(URLHAUS_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-host intelligence could not be completed.' }));
|
|
320
|
+
}
|
|
321
|
+
if (threatfoxIntelligencePromise) {
|
|
322
|
+
threatIntelligenceProviders.push(threatfoxIntelligenceResult.status === 'fulfilled' && threatfoxIntelligenceResult.value
|
|
323
|
+
? threatfoxIntelligenceResult.value
|
|
324
|
+
: createThreatIntelligenceResult(THREATFOX_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-IOC intelligence could not be completed.' }));
|
|
325
|
+
}
|
|
326
|
+
const threatIntelligence = threatIntelligenceProviders.length
|
|
327
|
+
? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers: threatIntelligenceProviders }
|
|
328
|
+
: null;
|
|
329
|
+
const registrarRdapParsed = registrarRdap
|
|
330
|
+
&& typeof registrarRdap.parsed === 'object'
|
|
331
|
+
&& registrarRdap.parsed
|
|
332
|
+
&& !Array.isArray(registrarRdap.parsed)
|
|
333
|
+
? registrarRdap.parsed
|
|
334
|
+
: null;
|
|
335
|
+
const registryInsights = classified.type === 'domain' && !fast && !compact
|
|
336
|
+
? buildRegistryInsights({
|
|
337
|
+
rdapParsed: rdapRecord?.parsed,
|
|
338
|
+
rdapStatus,
|
|
339
|
+
rdapFetchedAt: rdapRecord?.fetchedAt,
|
|
340
|
+
whoisParsed: whois.parsed,
|
|
341
|
+
whoisStatus,
|
|
342
|
+
whoisQueriedAt: Array.isArray(whoisChain) && whoisChain[0] ? whoisChain[0].queriedAt : null,
|
|
343
|
+
registrarRdapParsed,
|
|
344
|
+
registrarRdapStatus: registrarRdap?.status,
|
|
345
|
+
registrarRdapFetchedAt: registrarRdap?.fetchedAt,
|
|
346
|
+
})
|
|
347
|
+
: null;
|
|
348
|
+
const registrarStanding = classified.type === 'domain' && !compact
|
|
349
|
+
? buildRegistrarStanding({
|
|
350
|
+
registrarIanaId: resolveRegistrarIanaId(rdapRecord?.parsed, whois.parsed),
|
|
351
|
+
})
|
|
352
|
+
: null;
|
|
353
|
+
return {
|
|
354
|
+
rdap,
|
|
355
|
+
whois,
|
|
356
|
+
availability,
|
|
357
|
+
diagnostics,
|
|
358
|
+
...(registryInsights ? { registryInsights } : {}),
|
|
359
|
+
...(registrarStanding ? { registrarStanding } : {}),
|
|
360
|
+
...(reverseDns ? { reverseDns } : {}),
|
|
361
|
+
...(networkContext ? { networkContext } : {}),
|
|
362
|
+
...(securityTxt ? { securityTxt } : {}),
|
|
363
|
+
...(sslbl ? { sslbl } : {}),
|
|
364
|
+
...(threatIntelligence ? { threatIntelligence } : {}),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export { buildUnifiedLookupResponse };
|