@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1
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 +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +21 -6
- package/cli/artifact-validation/lookup-evidence.mjs +3 -3
- package/cli/artifact-validation/signed-review.mjs +59 -13
- package/cli/artifact-verify.mjs +2 -0
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -2
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +20 -3
- package/cli/case-pack.mjs +12 -2
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +3 -3
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +1 -1
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +18 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +68 -158
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +8 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +223 -2
- package/docs/contextual-reviews.md +155 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +12 -8
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +9 -7
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +22 -5
- package/lib/domain-posture.mjs +16 -15
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +22 -15
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +4 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +7 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +69 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +27 -0
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +141 -139
- package/lib/static-html-analysis.mjs +1 -1
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +388 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +13 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +78 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-recheck-model.mjs +90 -32
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +40 -112
- package/packages/cases/case-record-projection.mjs +8 -1
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +63 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +103 -1
- package/packages/cases/case-response-model.mjs +9 -1
- package/packages/cases/case-response-outcomes.mjs +29 -8
- package/packages/cases/case-response-packet-render.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +25 -11
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +14 -3
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +28 -10
- package/packages/contracts/case-portability.mjs +118 -25
- package/packages/contracts/cli-command-semantics.mjs +2 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +63 -17
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +4 -12
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-manifest.mjs +2 -2
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/workspace/brand-profile-model.mjs +2 -2
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +23 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +28 -12
- package/packages/workspace/watchlist-store.mjs +17 -5
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -1,31 +1,9 @@
|
|
|
1
1
|
// Lookup diagnostics, bounded source errors and optional deep-lookup timing.
|
|
2
2
|
import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
|
|
3
3
|
import { FEATURE_DISABLED_ERROR_CODE } from "./feature-policy.mjs";
|
|
4
|
+
import { LOOKUP_SOURCE_STATES, lookupDiagnosticStates } from "./lookup-progress.mjs";
|
|
4
5
|
const LOOKUP_DIAGNOSTICS_VERSION = 8;
|
|
5
6
|
const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
|
|
6
|
-
const LOOKUP_SOURCE_STATES = Object.freeze([
|
|
7
|
-
'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
|
|
8
|
-
'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
|
|
9
|
-
]);
|
|
10
|
-
const SOURCE_STATES = new Set(LOOKUP_SOURCE_STATES);
|
|
11
|
-
function lookupDiagnosticStates(value) {
|
|
12
|
-
const record = (input) => input && typeof input === 'object' && !Array.isArray(input)
|
|
13
|
-
? input : {};
|
|
14
|
-
const diagnostics = record(value);
|
|
15
|
-
const candidates = {
|
|
16
|
-
rdap: record(diagnostics.rdap).status,
|
|
17
|
-
registrar_rdap: record(record(diagnostics.rdap).registrar).status,
|
|
18
|
-
whois: record(diagnostics.whois).status,
|
|
19
|
-
availability: record(diagnostics.availability).status,
|
|
20
|
-
reverse_dns: record(diagnostics.reverseDns).status,
|
|
21
|
-
network_context: record(diagnostics.network).status,
|
|
22
|
-
security_txt: record(diagnostics.securityTxt).status,
|
|
23
|
-
sslbl: record(diagnostics.sslbl).status,
|
|
24
|
-
};
|
|
25
|
-
return Object.freeze(Object.fromEntries(Object.entries(candidates).map(([source, state]) => [
|
|
26
|
-
source, typeof state === 'string' && SOURCE_STATES.has(state) ? state : null,
|
|
27
|
-
])));
|
|
28
|
-
}
|
|
29
7
|
const LOOKUP_TIMING_VERSION = 1;
|
|
30
8
|
const MAX_LOOKUP_TIMING_MS = 120_000;
|
|
31
9
|
const LOOKUP_TIMING_SOURCE_ORDER = Object.freeze([
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// the Lookup response trust boundary, and the display projection.
|
|
3
|
-
export const MAX_LOOKUP_DNS_RECORDS_PER_TYPE = 16;
|
|
4
|
-
export const MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS = 8;
|
|
5
|
-
export const TLS_PROFILE_VERSION = 3;
|
|
6
|
-
export const MAX_LOOKUP_TLS_CHAIN_CERTIFICATES = 8;
|
|
7
|
-
export const MAX_LOOKUP_TLS_ALT_NAMES = 50;
|
|
8
|
-
export const MAX_LOOKUP_TLS_NAME_VALUES = 4;
|
|
9
|
-
export const MAX_LOOKUP_TLS_FINDINGS = 5;
|
|
10
|
-
export const MAX_LOOKUP_TLS_CERTIFICATE_POLICIES = 16;
|
|
1
|
+
export * from "../packages/analysis/lookup-network-evidence-bounds.mjs";
|
package/lib/lookup-progress.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Portable bounded NDJSON Lookup progress contract. Partial events are
|
|
2
2
|
// presentation-only; only a validated final ordinary Lookup result may cross
|
|
3
3
|
// the persistence boundary.
|
|
4
|
-
import { scanBoundedJson, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES } from "
|
|
4
|
+
import { scanBoundedJson, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES } from "../packages/analysis/bounded-json.mjs";
|
|
5
5
|
export const LOOKUP_PROGRESS_SCHEMA = 'whoisleuth.lookup-progress';
|
|
6
6
|
export const LOOKUP_PROGRESS_VERSION = 1;
|
|
7
7
|
export const MAX_LOOKUP_PROGRESS_SOURCES = 16;
|
|
@@ -32,7 +32,56 @@ const SOURCE_STATES = new Set([
|
|
|
32
32
|
'unavailable',
|
|
33
33
|
'rate_limited',
|
|
34
34
|
]);
|
|
35
|
+
// Final diagnostic states are distinct from the provisional progress subset.
|
|
36
|
+
export const LOOKUP_SOURCE_STATES = Object.freeze([
|
|
37
|
+
'success', 'partial', 'not_found', 'skipped', 'error', 'unsupported',
|
|
38
|
+
'not_applicable', 'unavailable', 'rate_limited', 'complete', 'disabled', 'stale',
|
|
39
|
+
]);
|
|
40
|
+
const FINAL_SOURCE_STATES = new Set(LOOKUP_SOURCE_STATES);
|
|
41
|
+
export function lookupDiagnosticStates(value) {
|
|
42
|
+
const record = (input) => input && typeof input === 'object' && !Array.isArray(input)
|
|
43
|
+
? input : {};
|
|
44
|
+
const diagnostics = record(value);
|
|
45
|
+
const candidates = {
|
|
46
|
+
rdap: record(diagnostics.rdap).status,
|
|
47
|
+
registrar_rdap: record(record(diagnostics.rdap).registrar).status,
|
|
48
|
+
whois: record(diagnostics.whois).status,
|
|
49
|
+
availability: record(diagnostics.availability).status,
|
|
50
|
+
reverse_dns: record(diagnostics.reverseDns).status,
|
|
51
|
+
network_context: record(diagnostics.network).status,
|
|
52
|
+
security_txt: record(diagnostics.securityTxt).status,
|
|
53
|
+
sslbl: record(diagnostics.sslbl).status,
|
|
54
|
+
};
|
|
55
|
+
return Object.freeze(Object.fromEntries(Object.entries(candidates).map(([source, state]) => [
|
|
56
|
+
source, typeof state === 'string' && FINAL_SOURCE_STATES.has(state) ? state : null,
|
|
57
|
+
])));
|
|
58
|
+
}
|
|
35
59
|
const SOURCE_ID_RE = /^[a-z][a-z0-9_]{0,39}$/u;
|
|
60
|
+
/** The existing Lookup recipe, shared by offline plans and source progress.
|
|
61
|
+
* This describes admitted families, not provider health or request counts. */
|
|
62
|
+
export function plannedLookupSources(targetType, mode, options = {}) {
|
|
63
|
+
const sources = ['rdap'];
|
|
64
|
+
if (mode === 'fast') {
|
|
65
|
+
if (targetType === 'domain')
|
|
66
|
+
sources.push('domain_evidence');
|
|
67
|
+
return Object.freeze(sources);
|
|
68
|
+
}
|
|
69
|
+
sources.push('whois');
|
|
70
|
+
if (targetType === 'domain') {
|
|
71
|
+
sources.push('domain_evidence', 'registrar_rdap', 'network_context');
|
|
72
|
+
if (options.securityTxt)
|
|
73
|
+
sources.push('security_txt');
|
|
74
|
+
if (options.externalIntelligence)
|
|
75
|
+
sources.push('external_intelligence');
|
|
76
|
+
if (options.malwareHostIntelligence)
|
|
77
|
+
sources.push('malware_host_intelligence');
|
|
78
|
+
if (options.malwareIocIntelligence)
|
|
79
|
+
sources.push('malware_ioc_intelligence');
|
|
80
|
+
}
|
|
81
|
+
else if (targetType === 'ipv4' || targetType === 'ipv6')
|
|
82
|
+
sources.push('reverse_dns');
|
|
83
|
+
return Object.freeze(sources);
|
|
84
|
+
}
|
|
36
85
|
const TEXT_ENCODER = new TextEncoder();
|
|
37
86
|
function byteLength(value) {
|
|
38
87
|
return TEXT_ENCODER.encode(value).byteLength;
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// relies on before any evidence is derived or rendered. Nested source records
|
|
7
7
|
// stay separately attributed and additive; their own normalizers remain the
|
|
8
8
|
// authority for source-specific fields.
|
|
9
|
-
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "
|
|
10
|
-
import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, } from "
|
|
9
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "../packages/analysis/threat-intelligence-types.mjs";
|
|
10
|
+
import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, } from "../packages/analysis/http-evidence-bounds.mjs";
|
|
11
11
|
import { MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, normalizeExplicitIsoTimestamp, } from "../packages/evidence/observation.mjs";
|
|
12
|
-
import { assertBoundedJsonStructure } from "
|
|
13
|
-
import { canonicalRegistrableDomain } from "
|
|
12
|
+
import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mjs";
|
|
13
|
+
import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
|
|
14
14
|
import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
|
|
15
|
+
import { isWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
|
|
15
16
|
import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
|
|
16
17
|
import { MAX_SECURITY_POSTURE_FINDINGS, sanitizeLookupChildProfiles, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, } from "./lookup-child-profile-contract.mjs";
|
|
17
18
|
import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./homepage-metadata-contract.mjs";
|
|
@@ -136,22 +137,28 @@ function normalizeThreatFinding(value, providerId) {
|
|
|
136
137
|
}
|
|
137
138
|
function normalizeThreatProvider(value, expectedDomain) {
|
|
138
139
|
const input = record(value);
|
|
140
|
+
const identity = record(input.provider);
|
|
141
|
+
const providerId = boundedThreatText(identity.id, 80);
|
|
142
|
+
const providerDefinition = providerId && Object.hasOwn(THREAT_INTELLIGENCE_PROVIDERS, providerId)
|
|
143
|
+
? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
|
|
144
|
+
const withheld = (reason) => ({ provider: null, withheld: {
|
|
145
|
+
providerId: providerDefinition ? providerId : null,
|
|
146
|
+
label: providerDefinition ? `${providerDefinition.label} record` : 'Unrecognised provider record',
|
|
147
|
+
count: 1, reason,
|
|
148
|
+
} });
|
|
139
149
|
if (input.schema !== THREAT_INTELLIGENCE_SCHEMA
|
|
140
150
|
|| input.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION)
|
|
141
|
-
return
|
|
151
|
+
return withheld('Unsupported provider format; evidence was withheld.');
|
|
142
152
|
const target = record(input.target);
|
|
143
153
|
if (target.type !== 'domain'
|
|
144
154
|
|| target.exposure !== 'registrable_domain'
|
|
145
155
|
|| target.value !== expectedDomain)
|
|
146
|
-
return
|
|
147
|
-
const identity = record(input.provider);
|
|
148
|
-
const providerId = boundedThreatText(identity.id, 80);
|
|
149
|
-
const providerDefinition = providerId ? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
|
|
156
|
+
return withheld('The provider target did not match this Lookup; evidence was withheld.');
|
|
150
157
|
if (!providerId || !providerDefinition)
|
|
151
|
-
return
|
|
158
|
+
return withheld('The provider identity was not recognised; evidence was withheld.');
|
|
152
159
|
const state = boundedThreatText(input.state, 32);
|
|
153
160
|
if (!state || !THREAT_INTELLIGENCE_STATES.has(state))
|
|
154
|
-
return
|
|
161
|
+
return withheld('The provider result state was not supported; evidence was withheld.');
|
|
155
162
|
const observationInput = record(input.observation);
|
|
156
163
|
const limitations = Array.isArray(observationInput.limitations)
|
|
157
164
|
? observationInput.limitations
|
|
@@ -177,35 +184,40 @@ function normalizeThreatProvider(value, expectedDomain) {
|
|
|
177
184
|
? `${omitted} invalid or over-limit provider finding${omitted === 1 ? ' was' : 's were'} omitted from this view.`
|
|
178
185
|
: 'Provider findings were not a supported array and were withheld from this view.');
|
|
179
186
|
}
|
|
180
|
-
return {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
187
|
+
return { withheld: null, provider: {
|
|
188
|
+
schema: THREAT_INTELLIGENCE_SCHEMA,
|
|
189
|
+
version: THREAT_INTELLIGENCE_CONTRACT_VERSION,
|
|
190
|
+
provider: {
|
|
191
|
+
id: providerId,
|
|
192
|
+
label: providerDefinition.label,
|
|
193
|
+
},
|
|
194
|
+
target: {
|
|
195
|
+
type: 'domain',
|
|
196
|
+
value: expectedDomain,
|
|
197
|
+
exposure: 'registrable_domain',
|
|
198
|
+
},
|
|
199
|
+
state: projectionIncomplete && ['success', 'not_found'].includes(state) ? 'partial' : state,
|
|
200
|
+
detail: boundedThreatText(input.detail),
|
|
201
|
+
findings,
|
|
202
|
+
observation: {
|
|
203
|
+
observedAt: threatTimestamp(observationInput.observedAt),
|
|
204
|
+
limitations: limitations.slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS),
|
|
205
|
+
complete: projectionIncomplete ? false : typeof observationInput.complete === 'boolean' ? observationInput.complete : null,
|
|
206
|
+
truncated: projectionTruncated ? true : typeof observationInput.truncated === 'boolean' ? observationInput.truncated : null,
|
|
207
|
+
},
|
|
208
|
+
} };
|
|
202
209
|
}
|
|
203
210
|
function projectThreatProviders(values, expectedDomain) {
|
|
204
211
|
const grouped = new Map();
|
|
212
|
+
const withheld = [];
|
|
213
|
+
const unexamined = Math.max(0, values.length - MAX_THREAT_INTELLIGENCE_PROVIDERS * 2);
|
|
205
214
|
for (const value of values.slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS * 2)) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
215
|
+
const projection = normalizeThreatProvider(value, expectedDomain);
|
|
216
|
+
if (projection.withheld) {
|
|
217
|
+
withheld.push(projection.withheld);
|
|
208
218
|
continue;
|
|
219
|
+
}
|
|
220
|
+
const provider = projection.provider;
|
|
209
221
|
const providerId = String(record(provider.provider).id || '');
|
|
210
222
|
if (!providerId)
|
|
211
223
|
continue;
|
|
@@ -218,20 +230,25 @@ function projectThreatProviders(values, expectedDomain) {
|
|
|
218
230
|
grouped.set(providerId, { first: provider, signatures: new Set([signature]) });
|
|
219
231
|
}
|
|
220
232
|
}
|
|
221
|
-
|
|
233
|
+
if (unexamined)
|
|
234
|
+
withheld.push({ providerId: null, label: 'Additional provider records', count: unexamined,
|
|
235
|
+
reason: 'The provider-record review bound was exceeded; these records were not examined.' });
|
|
236
|
+
const withheldIds = new Set(withheld.map((item) => item.providerId).filter(Boolean));
|
|
237
|
+
const providers = [...grouped.values()].slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS).map(({ first, signatures }) => (signatures.size === 1 && !withheldIds.has(String(record(first.provider).id)) && unexamined === 0
|
|
222
238
|
? first
|
|
223
239
|
: {
|
|
224
240
|
...first,
|
|
225
241
|
state: 'partial',
|
|
226
|
-
detail: 'Conflicting
|
|
242
|
+
detail: 'Conflicting or unvalidated provider records prevent a conclusive provider projection.',
|
|
227
243
|
findings: [],
|
|
228
244
|
observation: {
|
|
229
245
|
observedAt: null,
|
|
230
|
-
limitations: ['Conflicting
|
|
246
|
+
limitations: ['Conflicting or unvalidated records were withheld rather than resolved by array order.'],
|
|
231
247
|
complete: false,
|
|
232
248
|
truncated: null,
|
|
233
249
|
},
|
|
234
250
|
}));
|
|
251
|
+
return { providers, withheld };
|
|
235
252
|
}
|
|
236
253
|
function optionalBoundedText(value, maxLength) {
|
|
237
254
|
return value === undefined || (typeof value === 'string'
|
|
@@ -624,6 +641,7 @@ function validAvailabilityScalars(value) {
|
|
|
624
641
|
&& validOptionalNullableText(value.pageTitle, MAX_LOOKUP_RESPONSE_PAGE_TITLE_LENGTH)
|
|
625
642
|
&& validOptionalNullableText(value.dnssec, 40)
|
|
626
643
|
&& (value.deepScanComplete === undefined || typeof value.deepScanComplete === 'boolean')
|
|
644
|
+
&& (value.webCollectionQuality === undefined || isWebCollectionQuality(value.webCollectionQuality))
|
|
627
645
|
&& (value.hasMx === undefined || value.hasMx === null || typeof value.hasMx === 'boolean')
|
|
628
646
|
&& (value.hasNullMx === undefined || value.hasNullMx === null || typeof value.hasNullMx === 'boolean')
|
|
629
647
|
&& (value.hasSpf === undefined || value.hasSpf === null || typeof value.hasSpf === 'boolean')
|
|
@@ -860,6 +878,7 @@ function parseCompactLookupHttpResponse(value, expectedDomain) {
|
|
|
860
878
|
|| typeof availability.confidence !== 'string'
|
|
861
879
|
|| !COMPACT_CONFIDENCE_LEVELS.has(availability.confidence)
|
|
862
880
|
|| (availability.deepScanComplete !== undefined && typeof availability.deepScanComplete !== 'boolean')
|
|
881
|
+
|| (availability.webCollectionQuality !== undefined && !isWebCollectionQuality(availability.webCollectionQuality))
|
|
863
882
|
|| (availability.bulkComparison !== undefined
|
|
864
883
|
&& !validCompactBulkComparison(availability.bulkComparison))
|
|
865
884
|
|| compactPageIdentity !== null && Object.hasOwn(compactPageIdentity, 'publicationMetadata')
|
|
@@ -968,11 +987,20 @@ function createLookupViewModel(response) {
|
|
|
968
987
|
? canonicalRegistrableDomain(response.registrableDomain)
|
|
969
988
|
?? canonicalRegistrableDomain(availability.domain)
|
|
970
989
|
: null;
|
|
971
|
-
const
|
|
990
|
+
const threatProjection = expectedThreatDomain
|
|
972
991
|
&& rawThreatIntelligence.version === THREAT_INTELLIGENCE_ENVELOPE_VERSION
|
|
973
992
|
&& Array.isArray(rawThreatIntelligence.providers)
|
|
974
993
|
? projectThreatProviders(rawThreatIntelligence.providers, expectedThreatDomain)
|
|
975
|
-
: []
|
|
994
|
+
: { providers: [], withheld: Object.keys(rawThreatIntelligence).length ? [{
|
|
995
|
+
providerId: null,
|
|
996
|
+
label: 'External-intelligence data',
|
|
997
|
+
count: Array.isArray(rawThreatIntelligence.providers) ? rawThreatIntelligence.providers.length : null,
|
|
998
|
+
reason: !expectedThreatDomain ? 'External intelligence was not bound to a domain Lookup and was withheld.'
|
|
999
|
+
: rawThreatIntelligence.version !== THREAT_INTELLIGENCE_ENVELOPE_VERSION
|
|
1000
|
+
? 'The external-intelligence envelope uses an unsupported version; its records were withheld.'
|
|
1001
|
+
: 'External-intelligence records were not a supported array and were withheld.',
|
|
1002
|
+
}] : [] };
|
|
1003
|
+
const { providers, withheld } = threatProjection;
|
|
976
1004
|
const threatIntelligence = providers.length
|
|
977
1005
|
? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers }
|
|
978
1006
|
: {};
|
|
@@ -1008,6 +1036,7 @@ function createLookupViewModel(response) {
|
|
|
1008
1036
|
sslbl,
|
|
1009
1037
|
threatIntelligence,
|
|
1010
1038
|
threatIntelligenceProviders: providers,
|
|
1039
|
+
threatIntelligenceWithheld: withheld,
|
|
1011
1040
|
dnsEvidence,
|
|
1012
1041
|
dnsRecords: record(dnsEvidence.records),
|
|
1013
1042
|
httpEvidence,
|
package/lib/lookup-response.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Lookup result shaping after the source plan has settled. This module starts no
|
|
2
2
|
// collection and preserves compact, diagnostics and enrichment response shapes.
|
|
3
|
+
import { withoutHttpDeliveryMetadata } from "./http-intelligence.mjs";
|
|
3
4
|
import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
|
|
4
5
|
import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
|
|
5
6
|
import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, } from "./lookup-diagnostics.mjs";
|
|
6
7
|
import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
|
|
7
8
|
import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
|
|
8
|
-
import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "
|
|
9
|
+
import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "../packages/analysis/threat-intelligence-types.mjs";
|
|
9
10
|
import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
|
|
10
11
|
import { buildRegistryInsights } from "./registry-insights.mjs";
|
|
11
12
|
import { buildRegistrarStanding } from "./registrar-standing.mjs";
|
|
@@ -20,17 +21,6 @@ function withoutNestedPublicationMetadata(value) {
|
|
|
20
21
|
delete output.publicationMetadata;
|
|
21
22
|
return output;
|
|
22
23
|
}
|
|
23
|
-
function withoutNestedDeliveryMetadata(value) {
|
|
24
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
25
|
-
return value;
|
|
26
|
-
const output = { ...value };
|
|
27
|
-
if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
|
|
28
|
-
const response = { ...output.response };
|
|
29
|
-
delete response.deliveryMetadata;
|
|
30
|
-
output.response = response;
|
|
31
|
-
}
|
|
32
|
-
return output;
|
|
33
|
-
}
|
|
34
24
|
async function buildUnifiedLookupResponse(context) {
|
|
35
25
|
const { availabilityEnabled, availabilityPromise, classified, compact, dnsIntelligenceEnabled, fast, inspectCertificateWarningData, networkContextPromise, options, rdapEnabled, rdapPromise, registrarRdapPromise, reverseDnsEligible, reverseDnsPromise, securityTxtPromise, skipWhois, threatfoxIntelligencePromise, timing, urlhausIntelligencePromise, urlscanIntelligencePromise, whoisEnabled, whoisPromise, } = context;
|
|
36
26
|
const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
|
|
@@ -100,7 +90,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
100
90
|
whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
|
|
101
91
|
}
|
|
102
92
|
else if (Array.isArray(whoisChain)) {
|
|
103
|
-
whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
|
|
93
|
+
whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined) };
|
|
104
94
|
}
|
|
105
95
|
else {
|
|
106
96
|
whois = {
|
|
@@ -154,7 +144,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
154
144
|
? 'skipped'
|
|
155
145
|
: whoisResult.status === 'rejected'
|
|
156
146
|
? 'error'
|
|
157
|
-
: whoisCollectionStatus(whoisChain);
|
|
147
|
+
: whoisCollectionStatus(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
|
|
158
148
|
const availabilityStatus = classified.type !== 'domain'
|
|
159
149
|
? 'not_applicable'
|
|
160
150
|
: !availabilityEnabled ? 'disabled'
|
|
@@ -298,7 +288,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
298
288
|
availability: {
|
|
299
289
|
...compactAvailability,
|
|
300
290
|
...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
|
|
301
|
-
...(richHttp !== undefined ? { http:
|
|
291
|
+
...(richHttp !== undefined ? { http: withoutHttpDeliveryMetadata(richHttp) } : {}),
|
|
302
292
|
...(bulkComparison ? { bulkComparison } : {}),
|
|
303
293
|
},
|
|
304
294
|
diagnostics,
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// provider payloads, and request errors. The ordinary final Lookup response
|
|
4
4
|
// remains the only authoritative and persistable result.
|
|
5
5
|
import { whoisCollectionStatus } from "./whois-authority.mjs";
|
|
6
|
-
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "
|
|
6
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "../packages/analysis/threat-intelligence-types.mjs";
|
|
7
|
+
import { plannedLookupSources } from "./lookup-progress.mjs";
|
|
7
8
|
const RESULT_STATES = new Set([
|
|
8
9
|
'available',
|
|
9
10
|
'expiring',
|
|
@@ -38,28 +39,13 @@ function sourceTruncated(value) {
|
|
|
38
39
|
.some(([key, field]) => field === true && (key === 'truncated' || key.endsWith('Truncated'))));
|
|
39
40
|
}
|
|
40
41
|
function plannedLookupProgressSources(classified, options = {}) {
|
|
41
|
-
|
|
42
|
-
if (classified.type === 'domain') {
|
|
43
|
-
sources.push('domain_evidence', 'registrar_rdap', 'network_context');
|
|
44
|
-
if (options.securityTxt)
|
|
45
|
-
sources.push('security_txt');
|
|
46
|
-
if (options.externalIntelligence)
|
|
47
|
-
sources.push('external_intelligence');
|
|
48
|
-
if (options.malwareHostIntelligence)
|
|
49
|
-
sources.push('malware_host_intelligence');
|
|
50
|
-
if (options.malwareIocIntelligence)
|
|
51
|
-
sources.push('malware_ioc_intelligence');
|
|
52
|
-
}
|
|
53
|
-
else if (classified.type === 'ipv4' || classified.type === 'ipv6') {
|
|
54
|
-
sources.push('reverse_dns');
|
|
55
|
-
}
|
|
56
|
-
return Object.freeze(sources);
|
|
42
|
+
return plannedLookupSources(classified.type, 'deep', options);
|
|
57
43
|
}
|
|
58
|
-
function normalizedState(source, outcome, value) {
|
|
44
|
+
function normalizedState(source, outcome, value, requestedDomain, requested = true) {
|
|
59
45
|
if (outcome === 'rejected')
|
|
60
46
|
return 'error';
|
|
61
47
|
if (value === null || value === undefined)
|
|
62
|
-
return 'skipped';
|
|
48
|
+
return source === 'rdap' && requested ? 'unsupported' : 'skipped';
|
|
63
49
|
if (source === 'rdap') {
|
|
64
50
|
const rdap = record(value);
|
|
65
51
|
if (rdap.upstreamStatus === 404)
|
|
@@ -69,7 +55,7 @@ function normalizedState(source, outcome, value) {
|
|
|
69
55
|
return sourceTruncated(rdap) ? 'partial' : 'success';
|
|
70
56
|
}
|
|
71
57
|
if (source === 'whois') {
|
|
72
|
-
const status = whoisCollectionStatus(value);
|
|
58
|
+
const status = whoisCollectionStatus(value, requestedDomain);
|
|
73
59
|
return status === 'complete' ? 'success' : status;
|
|
74
60
|
}
|
|
75
61
|
if (source === 'domain_evidence') {
|
|
@@ -94,8 +80,8 @@ function normalizedState(source, outcome, value) {
|
|
|
94
80
|
return 'skipped';
|
|
95
81
|
return 'error';
|
|
96
82
|
}
|
|
97
|
-
function normalizeLookupSourceSettlement(source, outcome, value) {
|
|
98
|
-
const state = normalizedState(source, outcome, value);
|
|
83
|
+
function normalizeLookupSourceSettlement(source, outcome, value, requestedDomain, requested = true) {
|
|
84
|
+
const state = normalizedState(source, outcome, value, requestedDomain, requested);
|
|
99
85
|
const sourceRecord = record(value);
|
|
100
86
|
const threatObservation = THREAT_INTELLIGENCE_SOURCES.has(source)
|
|
101
87
|
? record(sourceRecord.observation)
|
package/lib/lookup.mjs
CHANGED
|
@@ -170,7 +170,7 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
170
170
|
]);
|
|
171
171
|
const notify = (source, outcome, value) => {
|
|
172
172
|
try {
|
|
173
|
-
options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value));
|
|
173
|
+
options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined, source !== 'rdap' || rdapEnabled));
|
|
174
174
|
}
|
|
175
175
|
catch {
|
|
176
176
|
// Presentation callbacks must never change evidence collection.
|
|
@@ -1,107 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
export const OPPORTUNITY_REVIEW_THRESHOLD = 70;
|
|
4
|
-
const STATE_BASE = Object.freeze({
|
|
5
|
-
available: 82,
|
|
6
|
-
for_sale: 70,
|
|
7
|
-
expiring: 52,
|
|
8
|
-
registered: 22,
|
|
9
|
-
});
|
|
10
|
-
const STATE_LABELS = Object.freeze({
|
|
11
|
-
available: 'available',
|
|
12
|
-
for_sale: 'registered with a sale signal',
|
|
13
|
-
expiring: 'registered in a lifecycle transition',
|
|
14
|
-
registered: 'registered',
|
|
15
|
-
});
|
|
16
|
-
function finite(value) {
|
|
17
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
18
|
-
}
|
|
19
|
-
function formatAge(days) {
|
|
20
|
-
if (days < 60)
|
|
21
|
-
return `${Math.max(0, Math.round(days))}d old`;
|
|
22
|
-
const years = days / 365.25;
|
|
23
|
-
return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
|
|
24
|
-
}
|
|
25
|
-
function addFactor(factors, dimensions, family, label, delta) {
|
|
26
|
-
factors.push({ family, label, delta });
|
|
27
|
-
dimensions.set(family, (dimensions.get(family) ?? 0) + delta);
|
|
28
|
-
}
|
|
29
|
-
export function normalizeOpportunityModelVersion(value) {
|
|
30
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
|
|
31
|
-
}
|
|
32
|
-
// Opportunity is acquisition readiness, not price, value, eligibility, or a
|
|
33
|
-
// prediction that a registration will lapse. It uses only bounded evidence
|
|
34
|
-
// already collected for the current lookup.
|
|
35
|
-
export function explainOpportunityScore(value) {
|
|
36
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
-
return null;
|
|
38
|
-
const input = value;
|
|
39
|
-
const state = input.availability ?? input.state;
|
|
40
|
-
if (typeof state !== 'string' || !Object.hasOwn(STATE_BASE, state))
|
|
41
|
-
return null;
|
|
42
|
-
const factors = [];
|
|
43
|
-
const dimensions = new Map();
|
|
44
|
-
addFactor(factors, dimensions, 'registration', `Base readiness for “${STATE_LABELS[state] ?? state}”`, STATE_BASE[state] ?? 0);
|
|
45
|
-
if (input.confidence === 'high')
|
|
46
|
-
addFactor(factors, dimensions, 'registration', 'High-confidence registration decision', 8);
|
|
47
|
-
else if (input.confidence === 'medium')
|
|
48
|
-
addFactor(factors, dimensions, 'registration', 'Medium-confidence registration decision', 3);
|
|
49
|
-
else if (input.confidence === 'low')
|
|
50
|
-
addFactor(factors, dimensions, 'registration', 'Low-confidence registration decision', -8);
|
|
51
|
-
if (state === 'for_sale')
|
|
52
|
-
addFactor(factors, dimensions, 'listing', 'Bounded sale signal observed; listing remains unverified', 6);
|
|
53
|
-
else if (input.activityStatus === 'parked')
|
|
54
|
-
addFactor(factors, dimensions, 'listing', 'Parked page observed without a verified sale offer', 3);
|
|
55
|
-
else if (state === 'registered' && input.activityStatus === 'active') {
|
|
56
|
-
addFactor(factors, dimensions, 'listing', 'Active website suggests an in-use registration', -12);
|
|
57
|
-
}
|
|
58
|
-
if (input.hasPublicRegistrantContact === true) {
|
|
59
|
-
addFactor(factors, dimensions, 'contactability', 'Public registrant contact route observed; acquisition suitability is not established', 10);
|
|
60
|
-
}
|
|
61
|
-
const expiresInDays = finite(input.expiresInDays);
|
|
62
|
-
if (state === 'registered' && expiresInDays !== null && expiresInDays >= 0 && expiresInDays < 30) {
|
|
63
|
-
addFactor(factors, dimensions, 'lifecycle', 'Published expiry is within 30 days; release is not implied', 4);
|
|
64
|
-
}
|
|
65
|
-
if (typeof input.privacyProtected === 'boolean') {
|
|
66
|
-
addFactor(factors, dimensions, 'contactability', 'Registration privacy is neutral; it does not establish contactability', 0);
|
|
67
|
-
}
|
|
68
|
-
const ageDays = finite(input.domainAgeDays);
|
|
69
|
-
if (ageDays !== null && ageDays >= 0) {
|
|
70
|
-
addFactor(factors, dimensions, 'registration', `Domain age (${formatAge(ageDays)}) is context only`, 0);
|
|
71
|
-
}
|
|
72
|
-
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
73
|
-
const score = Math.max(0, Math.min(100, rawScore));
|
|
74
|
-
const dimensionLabels = {
|
|
75
|
-
registration: 'Registration certainty',
|
|
76
|
-
listing: 'Listing readiness',
|
|
77
|
-
contactability: 'Contactability',
|
|
78
|
-
lifecycle: 'Lifecycle timing',
|
|
79
|
-
};
|
|
80
|
-
const observedFamilies = [...dimensions.keys()];
|
|
81
|
-
const evidenceQuality = buildScoreEvidenceQuality(input, {
|
|
82
|
-
expectedDepth: 'fast',
|
|
83
|
-
observedFamilies,
|
|
84
|
-
expectedFamilies: ['registration'],
|
|
85
|
-
});
|
|
86
|
-
return {
|
|
87
|
-
modelVersion: OPPORTUNITY_MODEL_VERSION,
|
|
88
|
-
score,
|
|
89
|
-
rawScore,
|
|
90
|
-
capped: score !== rawScore,
|
|
91
|
-
factors,
|
|
92
|
-
dimensions: [...dimensions.entries()].map(([id, contribution]) => ({ id, label: dimensionLabels[id], contribution })),
|
|
93
|
-
evidenceQuality,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
export function computeOpportunityScore(value) {
|
|
97
|
-
return explainOpportunityScore(value)?.score ?? null;
|
|
98
|
-
}
|
|
99
|
-
export function opportunityTone(score) {
|
|
100
|
-
if (score === null)
|
|
101
|
-
return 'neutral';
|
|
102
|
-
if (score >= OPPORTUNITY_REVIEW_THRESHOLD)
|
|
103
|
-
return 'good';
|
|
104
|
-
if (score >= 40)
|
|
105
|
-
return 'neutral';
|
|
106
|
-
return 'warn';
|
|
107
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/opportunity-scoring.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
|
|
2
|
-
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "
|
|
2
|
+
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "../packages/analysis/project-metadata.mjs";
|
|
3
3
|
export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
|
|
4
4
|
export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
|
|
5
5
|
export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
|
|
@@ -64,7 +64,24 @@ function reduceDynamicValues(value) {
|
|
|
64
64
|
.replace(/\b\d{4}-\d{1,2}-\d{1,2}(?:[t\s]\d{1,2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:z|[+-]\d{2}:?\d{2})?)?\b/gi, '<time>')
|
|
65
65
|
.replace(/\b\d{10,13}\b/g, '<time>')
|
|
66
66
|
.replace(/\b[a-f0-9]{16,}\b/gi, '<id>')
|
|
67
|
-
.replace(
|
|
67
|
+
.replace(/[a-z0-9_-]+/gi, (token) => {
|
|
68
|
+
// Visit each maximal token once. Repeated lookaheads at word boundaries
|
|
69
|
+
// rescan hyphenated text quadratically when a required digit is absent.
|
|
70
|
+
// Only hyphens are outside the former ASCII word-boundary match;
|
|
71
|
+
// underscores remain part of the identifier, including at either end.
|
|
72
|
+
let start = 0;
|
|
73
|
+
let end = token.length;
|
|
74
|
+
while (start < end && token[start] === '-')
|
|
75
|
+
start += 1;
|
|
76
|
+
while (end > start && token[end - 1] === '-')
|
|
77
|
+
end -= 1;
|
|
78
|
+
if (end - start < 20)
|
|
79
|
+
return token;
|
|
80
|
+
const identifier = token.slice(start, end);
|
|
81
|
+
return /[a-z]/i.test(identifier) && /\d/.test(identifier)
|
|
82
|
+
? `${token.slice(0, start)}<id>${token.slice(end)}`
|
|
83
|
+
: token;
|
|
84
|
+
});
|
|
68
85
|
}
|
|
69
86
|
function normalizeText(value, maxLength = 512) {
|
|
70
87
|
const normalized = reduceDynamicValues(String(value || ''))
|
|
@@ -1,34 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// prevents Node and browser consumers from drifting into different matches.
|
|
4
|
-
export const PERCEPTUAL_HASH_HEX_RE = /^[0-9a-f]{16}$/u;
|
|
5
|
-
export const MIN_INFORMATIVE_HASH_BITS = 10;
|
|
6
|
-
const POPCOUNT = new Uint8Array(16);
|
|
7
|
-
for (let index = 0; index < POPCOUNT.length; index += 1) {
|
|
8
|
-
POPCOUNT[index] = (index & 1) + ((index >> 1) & 1) + ((index >> 2) & 1) + ((index >> 3) & 1);
|
|
9
|
-
}
|
|
10
|
-
export function isPerceptualHash(value) {
|
|
11
|
-
return typeof value === 'string' && PERCEPTUAL_HASH_HEX_RE.test(value);
|
|
12
|
-
}
|
|
13
|
-
function popcountHex(hex) {
|
|
14
|
-
let bits = 0;
|
|
15
|
-
for (let index = 0; index < hex.length; index += 1) {
|
|
16
|
-
bits += POPCOUNT[parseInt(hex.charAt(index), 16)] ?? 0;
|
|
17
|
-
}
|
|
18
|
-
return bits;
|
|
19
|
-
}
|
|
20
|
-
export function isInformativePerceptualHash(value) {
|
|
21
|
-
if (!isPerceptualHash(value))
|
|
22
|
-
return false;
|
|
23
|
-
const bits = popcountHex(value);
|
|
24
|
-
return bits >= MIN_INFORMATIVE_HASH_BITS && bits <= 64 - MIN_INFORMATIVE_HASH_BITS;
|
|
25
|
-
}
|
|
26
|
-
export function hammingDistanceHex(left, right) {
|
|
27
|
-
if (!isPerceptualHash(left) || !isPerceptualHash(right))
|
|
28
|
-
return null;
|
|
29
|
-
let distance = 0;
|
|
30
|
-
for (let index = 0; index < 16; index += 1) {
|
|
31
|
-
distance += POPCOUNT[parseInt(left.charAt(index), 16) ^ parseInt(right.charAt(index), 16)] ?? 0;
|
|
32
|
-
}
|
|
33
|
-
return distance;
|
|
34
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/perceptual-hash-comparison.mjs";
|