@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/lib/lookup-response.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// collection and preserves compact, diagnostics and enrichment response shapes.
|
|
3
3
|
import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
|
|
4
4
|
import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
|
|
5
|
-
import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail,
|
|
5
|
+
import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, } from "./lookup-diagnostics.mjs";
|
|
6
6
|
import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
|
|
7
7
|
import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
|
|
8
8
|
import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
|
|
@@ -12,6 +12,7 @@ import { buildRegistrarStanding } from "./registrar-standing.mjs";
|
|
|
12
12
|
import { resolveRegistrarIanaId } from "./registrar-standing-contract.mjs";
|
|
13
13
|
import { securityTxtUnavailable } from "./security-txt.mjs";
|
|
14
14
|
import { parseWhoisChain } from "./whois.mjs";
|
|
15
|
+
import { whoisCollectionStatus } from "./whois-authority.mjs";
|
|
15
16
|
function withoutNestedPublicationMetadata(value) {
|
|
16
17
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
18
|
return value;
|
|
@@ -124,11 +125,10 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
124
125
|
const result = availabilityResult.value;
|
|
125
126
|
availability = {
|
|
126
127
|
applicable: true,
|
|
127
|
-
domain: classified.value,
|
|
128
|
-
inputHostname: classified.inputHostname,
|
|
129
|
-
registrableDomain: classified.registrableDomain,
|
|
130
|
-
isSubdomain: classified.isSubdomain,
|
|
131
128
|
...result,
|
|
129
|
+
// Registration authority retains its registrable target. Deep source
|
|
130
|
+
// observations declare their independently collected hostname.
|
|
131
|
+
domain: classified.value,
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
else {
|
|
@@ -137,7 +137,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
137
137
|
domain: classified.value,
|
|
138
138
|
state: 'unknown',
|
|
139
139
|
confidence: 'low',
|
|
140
|
-
detail:
|
|
140
|
+
detail: boundedSourceDetail(availabilityResult.reason, 'Availability lookup failed'),
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
}
|
|
@@ -152,11 +152,9 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
152
152
|
? 'disabled'
|
|
153
153
|
: skipWhois
|
|
154
154
|
? 'skipped'
|
|
155
|
-
: whoisResult.status === 'rejected'
|
|
155
|
+
: whoisResult.status === 'rejected'
|
|
156
156
|
? 'error'
|
|
157
|
-
: whoisChain
|
|
158
|
-
? 'unsupported'
|
|
159
|
-
: whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
|
|
157
|
+
: whoisCollectionStatus(whoisChain);
|
|
160
158
|
const availabilityStatus = classified.type !== 'domain'
|
|
161
159
|
? 'not_applicable'
|
|
162
160
|
: !availabilityEnabled ? 'disabled'
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Lookup. These deliberately exclude raw RDAP/WHOIS bodies, contacts, URLs,
|
|
3
3
|
// provider payloads, and request errors. The ordinary final Lookup response
|
|
4
4
|
// remains the only authoritative and persistable result.
|
|
5
|
+
import { whoisCollectionStatus } from "./whois-authority.mjs";
|
|
6
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
|
|
5
7
|
const RESULT_STATES = new Set([
|
|
6
8
|
'available',
|
|
7
9
|
'expiring',
|
|
@@ -19,11 +21,22 @@ const DIRECT_STATES = new Set([
|
|
|
19
21
|
'unavailable',
|
|
20
22
|
'rate_limited',
|
|
21
23
|
]);
|
|
24
|
+
const THREAT_INTELLIGENCE_SOURCES = new Set([
|
|
25
|
+
'external_intelligence',
|
|
26
|
+
'malware_host_intelligence',
|
|
27
|
+
'malware_ioc_intelligence',
|
|
28
|
+
]);
|
|
29
|
+
const THREAT_INTELLIGENCE_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
|
|
22
30
|
function record(value) {
|
|
23
31
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
24
32
|
? value
|
|
25
33
|
: {};
|
|
26
34
|
}
|
|
35
|
+
function sourceTruncated(value) {
|
|
36
|
+
// RDAP's normalised *Truncated fields describe loss independently of HTTP success.
|
|
37
|
+
return [record(value), record(record(value).parsed)].some(part => Object.entries(part)
|
|
38
|
+
.some(([key, field]) => field === true && (key === 'truncated' || key.endsWith('Truncated'))));
|
|
39
|
+
}
|
|
27
40
|
function plannedLookupProgressSources(classified, options = {}) {
|
|
28
41
|
const sources = ['rdap', 'whois'];
|
|
29
42
|
if (classified.type === 'domain') {
|
|
@@ -51,40 +64,53 @@ function normalizedState(source, outcome, value) {
|
|
|
51
64
|
const rdap = record(value);
|
|
52
65
|
if (rdap.upstreamStatus === 404)
|
|
53
66
|
return 'not_found';
|
|
54
|
-
|
|
67
|
+
if (rdap.upstreamStatus !== 200 || !Object.keys(record(rdap.parsed)).length)
|
|
68
|
+
return 'error';
|
|
69
|
+
return sourceTruncated(rdap) ? 'partial' : 'success';
|
|
55
70
|
}
|
|
56
71
|
if (source === 'whois') {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (value.length <= 1)
|
|
60
|
-
return 'unsupported';
|
|
61
|
-
return 'success';
|
|
72
|
+
const status = whoisCollectionStatus(value);
|
|
73
|
+
return status === 'complete' ? 'success' : status;
|
|
62
74
|
}
|
|
63
75
|
if (source === 'domain_evidence') {
|
|
64
|
-
return record(value).deepScanComplete ===
|
|
76
|
+
return record(value).deepScanComplete === true ? 'success' : 'partial';
|
|
77
|
+
}
|
|
78
|
+
if (THREAT_INTELLIGENCE_SOURCES.has(source)) {
|
|
79
|
+
const intelligence = record(value);
|
|
80
|
+
return intelligence.schema === THREAT_INTELLIGENCE_SCHEMA
|
|
81
|
+
&& intelligence.version === THREAT_INTELLIGENCE_CONTRACT_VERSION
|
|
82
|
+
&& typeof intelligence.state === 'string'
|
|
83
|
+
&& THREAT_INTELLIGENCE_STATES.has(intelligence.state)
|
|
84
|
+
? intelligence.state
|
|
85
|
+
: 'error';
|
|
65
86
|
}
|
|
66
87
|
const status = record(value).status;
|
|
67
88
|
if (typeof status === 'string' && DIRECT_STATES.has(status)) {
|
|
89
|
+
if (status === 'success' && (sourceTruncated(value) || record(value).complete === false))
|
|
90
|
+
return 'partial';
|
|
68
91
|
return status;
|
|
69
92
|
}
|
|
70
|
-
if (status === 'complete' || status === 'observed' || status === 'found')
|
|
71
|
-
return 'success';
|
|
72
93
|
if (status === 'not_applicable' || status === 'disabled')
|
|
73
94
|
return 'skipped';
|
|
74
|
-
return '
|
|
95
|
+
return 'error';
|
|
75
96
|
}
|
|
76
97
|
function normalizeLookupSourceSettlement(source, outcome, value) {
|
|
77
98
|
const state = normalizedState(source, outcome, value);
|
|
78
99
|
const sourceRecord = record(value);
|
|
100
|
+
const threatObservation = THREAT_INTELLIGENCE_SOURCES.has(source)
|
|
101
|
+
? record(sourceRecord.observation)
|
|
102
|
+
: null;
|
|
79
103
|
const resultState = source === 'domain_evidence'
|
|
80
104
|
&& typeof sourceRecord.state === 'string'
|
|
81
105
|
&& RESULT_STATES.has(sourceRecord.state)
|
|
82
106
|
? sourceRecord.state
|
|
83
107
|
: null;
|
|
84
|
-
const complete =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
const complete = threatObservation
|
|
109
|
+
? state !== 'error' && threatObservation.complete === true
|
|
110
|
+
: ['success', 'not_found'].includes(state) && sourceRecord.complete !== false;
|
|
111
|
+
const truncated = threatObservation
|
|
112
|
+
? threatObservation.truncated === true
|
|
113
|
+
: sourceTruncated(value);
|
|
88
114
|
const limitation = outcome === 'rejected'
|
|
89
115
|
? 'This source did not complete. No absence or safety conclusion was inferred.'
|
|
90
116
|
: state === 'skipped'
|
package/lib/lookup.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
|
|
|
21
21
|
import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
|
|
22
22
|
import { buildUnifiedLookupResponse } from "./lookup-response.mjs";
|
|
23
23
|
import { createLookupTimingTracker } from "./lookup-diagnostics.mjs";
|
|
24
|
+
import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
|
|
24
25
|
async function runUnifiedLookup(classified, options = {}) {
|
|
25
26
|
const fetchRdap = options.fetchRdapRecord || fetchRdapRecord;
|
|
26
27
|
const fetchRegistrarRdap = options.fetchRegistrarRdapRecord || fetchRegistrarRdapRecord;
|
|
@@ -44,19 +45,30 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
44
45
|
const securityTxtRequested = options.securityTxt === true;
|
|
45
46
|
const featurePolicy = options.featurePolicy || networkFeaturePolicy();
|
|
46
47
|
const timing = createLookupTimingTracker(!fast && !compact, options.now || Date.now);
|
|
47
|
-
const
|
|
48
|
+
const startedCollectors = [];
|
|
49
|
+
const measure = (source, operation) => (timing.measure(source, () => abortable(() => {
|
|
50
|
+
const running = Promise.resolve(operation());
|
|
51
|
+
if (options.waitForStartedCollectors)
|
|
52
|
+
startedCollectors.push(running.catch(() => { }));
|
|
53
|
+
return running;
|
|
54
|
+
}, options.signal)));
|
|
48
55
|
const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
|
|
49
56
|
const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
|
|
50
57
|
const availabilityEnabled = featureDecision('availability', featurePolicy).enabled;
|
|
51
58
|
const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
|
|
52
59
|
const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
|
|
53
60
|
const skipWhois = fast || !whoisEnabled;
|
|
61
|
+
const selectedUrl = options.selectedUrl === undefined ? undefined
|
|
62
|
+
: prepareSelectedLookupUrl(options.selectedUrl, classified.inputHostname ?? classified.value);
|
|
63
|
+
if (selectedUrl && (classified.type !== 'domain' || fast || compact || !availabilityEnabled || !websiteProbeEnabled)) {
|
|
64
|
+
throw new TypeError('Selected URL collection requires an enabled full Deep domain lookup.');
|
|
65
|
+
}
|
|
54
66
|
const rdapPromise = rdapEnabled
|
|
55
|
-
? measure('rdap', () => fetchRdap(classified.type, classified.value))
|
|
67
|
+
? measure('rdap', () => fetchRdap(classified.type, classified.value, options.signal ? { signal: options.signal } : {}))
|
|
56
68
|
: Promise.resolve(null);
|
|
57
69
|
const whoisPromise = skipWhois
|
|
58
70
|
? Promise.resolve(null)
|
|
59
|
-
: measure('whois', () => fetchWhois(classified.value));
|
|
71
|
+
: measure('whois', () => fetchWhois(classified.value, options.signal ? { signal: options.signal } : {}));
|
|
60
72
|
// Registrar RDAP is a separately attributed deep-lookup enrichment. It may
|
|
61
73
|
// overlap the WHOIS chain, but it never joins the promises used to decide
|
|
62
74
|
// availability and can add up to its own bounded timeout to a deep lookup.
|
|
@@ -76,7 +88,10 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
76
88
|
includeStructuredDataIdentity: !fast && !compact,
|
|
77
89
|
includeTechnologyProfile: !fast,
|
|
78
90
|
includeSecurityPosture: !compact,
|
|
91
|
+
...(!fast && !compact ? { observationHostname: classified.inputHostname } : {}),
|
|
92
|
+
...(selectedUrl ? { selectedUrl } : {}),
|
|
79
93
|
featurePolicy,
|
|
94
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
80
95
|
rdapRecordPromise: rdapPromise,
|
|
81
96
|
whoisChainPromise: whoisPromise,
|
|
82
97
|
...(selectedDnsResolvers ? {
|
|
@@ -171,30 +186,36 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
171
186
|
void Promise.resolve(sourcePromise).then((value) => notify(source, 'fulfilled', value), (error) => notify(source, 'rejected', error));
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
189
|
+
try {
|
|
190
|
+
return await buildUnifiedLookupResponse({
|
|
191
|
+
availabilityEnabled,
|
|
192
|
+
availabilityPromise,
|
|
193
|
+
classified,
|
|
194
|
+
compact,
|
|
195
|
+
dnsIntelligenceEnabled,
|
|
196
|
+
fast,
|
|
197
|
+
inspectCertificateWarningData,
|
|
198
|
+
networkContextPromise,
|
|
199
|
+
options,
|
|
200
|
+
rdapEnabled,
|
|
201
|
+
rdapPromise,
|
|
202
|
+
registrarRdapPromise,
|
|
203
|
+
reverseDnsEligible,
|
|
204
|
+
reverseDnsPromise,
|
|
205
|
+
securityTxtPromise,
|
|
206
|
+
skipWhois,
|
|
207
|
+
threatfoxIntelligencePromise,
|
|
208
|
+
timing,
|
|
209
|
+
urlhausIntelligencePromise,
|
|
210
|
+
urlscanIntelligencePromise,
|
|
211
|
+
whoisEnabled,
|
|
212
|
+
whoisPromise,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (options.waitForStartedCollectors)
|
|
217
|
+
await Promise.all(startedCollectors);
|
|
218
|
+
}
|
|
198
219
|
}
|
|
199
220
|
export { runUnifiedLookup };
|
|
200
221
|
export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, } from "./lookup-diagnostics.mjs";
|
|
@@ -116,6 +116,7 @@ export function reviewNameserverPreflight(inputRaw, generatedAtValue = new Date(
|
|
|
116
116
|
&& authorityState === 'authoritative'
|
|
117
117
|
&& servedSetState === 'aligned'
|
|
118
118
|
&& soaState === 'observed'
|
|
119
|
+
&& addressState !== 'non_public'
|
|
119
120
|
&& (!inBailiwick || addressState === 'ready');
|
|
120
121
|
return Object.freeze({
|
|
121
122
|
nameserver,
|
|
@@ -11,6 +11,7 @@ const OBSERVED_NETWORK_CONTEXT_VERSION = 1;
|
|
|
11
11
|
const MAX_NETWORK_CIDRS = 16;
|
|
12
12
|
const MAX_NETWORK_ATTEMPTS = 3;
|
|
13
13
|
const MAX_NETWORK_ABUSE_ROUTES = 6;
|
|
14
|
+
const NETWORK_ABUSE_ROUTE_LIMITATION = 'The network abuse-route projection reached its local retention limit; additional published routes may exist.';
|
|
14
15
|
const MAX_ENDPOINT_LENGTH = 2048;
|
|
15
16
|
const MAX_DETAIL_LENGTH = 240;
|
|
16
17
|
const MAX_NAME_LENGTH = 300;
|
|
@@ -144,20 +145,25 @@ function networkAbuseEntities(parsed) {
|
|
|
144
145
|
const fallback = parsed.abuse && typeof parsed.abuse === 'object' && !Array.isArray(parsed.abuse)
|
|
145
146
|
? [parsed.abuse]
|
|
146
147
|
: [];
|
|
147
|
-
return
|
|
148
|
-
.
|
|
149
|
-
|
|
148
|
+
return {
|
|
149
|
+
entities: [...roleEntities.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1), ...fallback]
|
|
150
|
+
.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item)),
|
|
151
|
+
truncated: roleEntities.length > MAX_NETWORK_ABUSE_ROUTES + 1,
|
|
152
|
+
};
|
|
150
153
|
}
|
|
151
154
|
function projectNetworkAbuseRoutes(parsed, provenance) {
|
|
152
155
|
const routes = new Map();
|
|
153
|
-
|
|
156
|
+
const inventory = networkAbuseEntities(parsed);
|
|
157
|
+
let truncated = inventory.truncated;
|
|
158
|
+
for (const entity of inventory.entities) {
|
|
154
159
|
const emails = Array.isArray(entity.emails) ? entity.emails : [];
|
|
155
160
|
const phones = Array.isArray(entity.phones) ? entity.phones : [];
|
|
161
|
+
truncated ||= emails.length > MAX_NETWORK_ABUSE_ROUTES + 1 || phones.length > MAX_NETWORK_ABUSE_ROUTES + 1;
|
|
156
162
|
const candidates = [
|
|
157
163
|
{ channel: 'email', value: entity.email },
|
|
158
|
-
...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'email', value })),
|
|
164
|
+
...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'email', value })),
|
|
159
165
|
{ channel: 'phone', value: entity.phone },
|
|
160
|
-
...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'phone', value })),
|
|
166
|
+
...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'phone', value })),
|
|
161
167
|
];
|
|
162
168
|
for (const candidate of candidates) {
|
|
163
169
|
const contact = normalizedAbuseContact(candidate.value, candidate.channel);
|
|
@@ -165,6 +171,10 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
|
|
|
165
171
|
continue;
|
|
166
172
|
const key = `${candidate.channel}:${contact.toLowerCase()}`;
|
|
167
173
|
if (!routes.has(key)) {
|
|
174
|
+
if (routes.size >= MAX_NETWORK_ABUSE_ROUTES) {
|
|
175
|
+
truncated = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
168
178
|
routes.set(key, {
|
|
169
179
|
kind: 'network_hosting',
|
|
170
180
|
channel: candidate.channel,
|
|
@@ -186,11 +196,14 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
|
|
|
186
196
|
],
|
|
187
197
|
});
|
|
188
198
|
}
|
|
189
|
-
if (routes.size >= MAX_NETWORK_ABUSE_ROUTES)
|
|
190
|
-
return [...routes.values()];
|
|
191
199
|
}
|
|
192
200
|
}
|
|
193
|
-
return
|
|
201
|
+
return {
|
|
202
|
+
routes: [...routes.values()].map((route) => truncated
|
|
203
|
+
? { ...route, complete: false, truncated: true, limitations: [...route.limitations, NETWORK_ABUSE_ROUTE_LIMITATION] }
|
|
204
|
+
: route),
|
|
205
|
+
truncated,
|
|
206
|
+
};
|
|
194
207
|
}
|
|
195
208
|
function networkSummary(parsedValue, family) {
|
|
196
209
|
const parsed = record(parsedValue);
|
|
@@ -204,10 +217,12 @@ function networkSummary(parsedValue, family) {
|
|
|
204
217
|
const startAddress = publicAddress(parsed.startAddress, family);
|
|
205
218
|
const endAddress = publicAddress(parsed.endAddress, family);
|
|
206
219
|
const country = boundedString(parsed.country, 2);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
const limitations = [
|
|
221
|
+
...(parsed.serverTruncated === true ? ['The RDAP server declared that part of its response was truncated.'] : []),
|
|
222
|
+
...(parsed.entitiesTruncated === true ? ['Some IP RDAP contact records or fields were omitted during bounded normalisation.'] : []),
|
|
223
|
+
...(parsed.cidrsTruncated === true ? ['Some IP RDAP CIDR entries were invalid or exceeded the normalisation limit.'] : []),
|
|
224
|
+
...(sourceCidrs.length > MAX_NETWORK_CIDRS ? ['The network CIDR summary reached its local retention limit.'] : []),
|
|
225
|
+
];
|
|
211
226
|
return {
|
|
212
227
|
value: {
|
|
213
228
|
handle: boundedString(parsed.handle, MAX_NAME_LENGTH),
|
|
@@ -220,8 +235,8 @@ function networkSummary(parsedValue, family) {
|
|
|
220
235
|
networkType: boundedString(parsed.networkType, 160),
|
|
221
236
|
databaseUpdatedAt: isoTimestamp(lifecycle.databaseUpdatedDateIso || lifecycle.databaseUpdatedDate),
|
|
222
237
|
},
|
|
223
|
-
truncated,
|
|
224
|
-
|
|
238
|
+
truncated: limitations.length > 0,
|
|
239
|
+
limitations,
|
|
225
240
|
};
|
|
226
241
|
}
|
|
227
242
|
function baseContext(selection, input) {
|
|
@@ -299,28 +314,33 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
|
|
|
299
314
|
if (rdap.httpStatus !== 200 || !Object.keys(parsed).length)
|
|
300
315
|
throw new Error('IP RDAP returned no usable normalised object');
|
|
301
316
|
const summary = networkSummary(parsed, selection.family);
|
|
302
|
-
const partial = summary.truncated;
|
|
303
317
|
const sourceObservedAt = rdap.fetchedAt || observedAt();
|
|
304
|
-
const
|
|
318
|
+
const routeProjection = projectNetworkAbuseRoutes(parsed, {
|
|
305
319
|
endpoint: rdap.endpoint,
|
|
306
320
|
observedAt: sourceObservedAt,
|
|
307
321
|
selection,
|
|
308
|
-
complete: !
|
|
309
|
-
truncated:
|
|
322
|
+
complete: !summary.truncated,
|
|
323
|
+
truncated: summary.truncated,
|
|
310
324
|
});
|
|
325
|
+
const incompleteReasons = [
|
|
326
|
+
...summary.limitations,
|
|
327
|
+
...(routeProjection.truncated ? [NETWORK_ABUSE_ROUTE_LIMITATION] : []),
|
|
328
|
+
];
|
|
329
|
+
const partial = incompleteReasons.length > 0;
|
|
311
330
|
return baseContext(selection, {
|
|
312
331
|
status: partial ? 'partial' : 'success',
|
|
313
332
|
observedAt: sourceObservedAt,
|
|
314
333
|
durationMs,
|
|
315
334
|
complete: !partial,
|
|
316
335
|
truncated: partial,
|
|
317
|
-
detail:
|
|
336
|
+
detail: partial
|
|
337
|
+
? `Network registration was retained, but the source record is incomplete. ${incompleteReasons[0]}`
|
|
338
|
+
: 'The selected public endpoint address was mapped to its separately attributed IP RDAP registration.',
|
|
318
339
|
limitations: [
|
|
340
|
+
...incompleteReasons,
|
|
319
341
|
'This identifies the registered network for one point-in-time endpoint address, not a definitive origin host or hosting provider.',
|
|
320
342
|
'CDNs, reverse proxies, load balancers, shared hosting, and location-dependent DNS can present different networks.',
|
|
321
343
|
'Network registration is an investigative lead and does not prove control, ownership, intent, or maliciousness.',
|
|
322
|
-
...(summary.serverTruncated ? ['The RDAP server declared that part of its response was truncated.'] : []),
|
|
323
|
-
...(sourceCidrsWereCapped(parsed) ? ['The network CIDR summary reached its local retention limit.'] : []),
|
|
324
344
|
],
|
|
325
345
|
diagnostics: {
|
|
326
346
|
requestCount: 1,
|
|
@@ -330,7 +350,7 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
|
|
|
330
350
|
},
|
|
331
351
|
rdap,
|
|
332
352
|
network: summary.value,
|
|
333
|
-
abuseRouting,
|
|
353
|
+
abuseRouting: routeProjection.routes,
|
|
334
354
|
});
|
|
335
355
|
}
|
|
336
356
|
catch (error) {
|
|
@@ -348,8 +368,4 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
|
|
|
348
368
|
});
|
|
349
369
|
}
|
|
350
370
|
}
|
|
351
|
-
function sourceCidrsWereCapped(parsed) {
|
|
352
|
-
return parsed.cidrsTruncated === true
|
|
353
|
-
|| (Array.isArray(parsed.cidrs) && parsed.cidrs.length > MAX_NETWORK_CIDRS);
|
|
354
|
-
}
|
|
355
371
|
export { MAX_NETWORK_ATTEMPTS, MAX_NETWORK_ABUSE_ROUTES, MAX_NETWORK_CIDRS, OBSERVED_NETWORK_CONTEXT_VERSION, collectObservedNetworkContext, selectObservedNetworkAddress, };
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// disclosures cannot drift from the enforced request boundaries.
|
|
4
4
|
export const MAX_OUTBOUND_REDIRECTS = 5;
|
|
5
5
|
export const HOMEPAGE_FETCH_TIMEOUT_MS = 6_000;
|
|
6
|
-
|
|
6
|
+
// One MiB admits large static application pages while bounding bytes retained
|
|
7
|
+
// from each response. Parsing, derived outputs and request concurrency retain
|
|
8
|
+
// separate work limits; this is not a claim that larger pages lack evidence.
|
|
9
|
+
export const MAX_HOMEPAGE_BYTES = 1024 * 1024;
|
|
7
10
|
export const FAVICON_FETCH_TIMEOUT_MS = 5_000;
|
|
8
11
|
export const MAX_FAVICON_BYTES = 200_000;
|
|
9
12
|
export const MAX_FAVICON_CANDIDATES = 4;
|