@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export const LOOKUP_PRESENTATION_STORAGE_KEY = 'whoisleuth:lookup-presentation:v1';
|
|
2
|
-
export const MAX_LOOKUP_PRESENTATION_SERIALIZED_BYTES = 1_024;
|
|
3
|
-
export const LOOKUP_TASK_VIEWS = Object.freeze([
|
|
4
|
-
Object.freeze({ id: 'general', label: 'General investigation' }),
|
|
5
|
-
Object.freeze({ id: 'acquisition', label: 'Acquisition review' }),
|
|
6
|
-
Object.freeze({ id: 'brand', label: 'Brand review' }),
|
|
7
|
-
Object.freeze({ id: 'incident', label: 'Incident response' }),
|
|
8
|
-
Object.freeze({ id: 'owned', label: 'Owned-domain posture' }),
|
|
9
|
-
]);
|
|
10
|
-
const TASKS = new Set(LOOKUP_TASK_VIEWS.map((option) => option.id));
|
|
11
|
-
export function normalizeLookupTaskView(value) {
|
|
12
|
-
return typeof value === 'string' && TASKS.has(value)
|
|
13
|
-
? value
|
|
14
|
-
: 'general';
|
|
15
|
-
}
|
|
16
|
-
export function readLookupPresentation(storage) {
|
|
17
|
-
try {
|
|
18
|
-
const serialized = storage.getItem(LOOKUP_PRESENTATION_STORAGE_KEY);
|
|
19
|
-
if (serialized === null
|
|
20
|
-
|| serialized.length > MAX_LOOKUP_PRESENTATION_SERIALIZED_BYTES
|
|
21
|
-
|| new TextEncoder().encode(serialized).byteLength > MAX_LOOKUP_PRESENTATION_SERIALIZED_BYTES) {
|
|
22
|
-
return { task: 'general' };
|
|
23
|
-
}
|
|
24
|
-
const stored = JSON.parse(serialized);
|
|
25
|
-
if (stored?.version === 1) {
|
|
26
|
-
return {
|
|
27
|
-
task: normalizeLookupTaskView(stored.task),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Invalid or unavailable browser storage falls back to the stable defaults.
|
|
33
|
-
}
|
|
34
|
-
return { task: 'general' };
|
|
35
|
-
}
|
|
36
|
-
export function writeLookupPresentation(storage, state) {
|
|
37
|
-
try {
|
|
38
|
-
storage.setItem(LOOKUP_PRESENTATION_STORAGE_KEY, JSON.stringify({
|
|
39
|
-
version: 1,
|
|
40
|
-
task: normalizeLookupTaskView(state.task),
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// The caller's in-memory selection remains valid when storage is unavailable.
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const PRIORITY = Object.freeze({
|
|
48
|
-
general: Object.freeze(['overview', 'registry', 'web-evidence', 'relationships-history', 'source-quality', 'case-response', 'advanced-evidence']),
|
|
49
|
-
acquisition: Object.freeze(['overview', 'registry', 'relationships-history', 'web-evidence', 'source-quality', 'case-response', 'advanced-evidence']),
|
|
50
|
-
brand: Object.freeze(['overview', 'web-evidence', 'relationships-history', 'registry', 'source-quality', 'case-response', 'advanced-evidence']),
|
|
51
|
-
incident: Object.freeze(['overview', 'web-evidence', 'relationships-history', 'advanced-evidence', 'registry', 'source-quality', 'case-response']),
|
|
52
|
-
owned: Object.freeze(['overview', 'registry', 'web-evidence', 'relationships-history', 'source-quality', 'case-response', 'advanced-evidence']),
|
|
53
|
-
});
|
|
54
|
-
export function prioritizeLookupSectionLinks(links, task) {
|
|
55
|
-
const order = PRIORITY[normalizeLookupTaskView(task)];
|
|
56
|
-
const rank = new Map(order.map((id, index) => [`#${id}`, index]));
|
|
57
|
-
return [...links].sort((left, right) => ((rank.get(left.href) ?? order.length) - (rank.get(right.href) ?? order.length)));
|
|
58
|
-
}
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { BoundedJsonResponseError, requestJsonCapped, } from "../bounded-json-response.js";
|
|
2
|
-
export const LOOKUP_SOURCE_REFRESH_VERSION = 1;
|
|
3
|
-
export const LOOKUP_SOURCE_STALE_AFTER_DAYS = 7;
|
|
4
|
-
export const LOOKUP_FRESHNESS_POLICY_VERSION = 1;
|
|
5
|
-
export const LOOKUP_SOURCE_REFRESH_TIMEOUT_MS = 40_000;
|
|
6
|
-
export const MAX_LOOKUP_SOURCE_REFRESH_KEYS = 512;
|
|
7
|
-
export const MAX_LOOKUP_SOURCE_REFRESH_BYTES = 2 * 1024 * 1024;
|
|
8
|
-
export const MAX_LOOKUP_SOURCE_REFRESH_HISTORY = 12;
|
|
9
|
-
const DOMAIN_EVIDENCE_IDS = new Set([
|
|
10
|
-
'availability',
|
|
11
|
-
'client-behavior',
|
|
12
|
-
'dns',
|
|
13
|
-
'http',
|
|
14
|
-
'page-identity',
|
|
15
|
-
'page-role',
|
|
16
|
-
'security-posture',
|
|
17
|
-
'technology',
|
|
18
|
-
'tls',
|
|
19
|
-
]);
|
|
20
|
-
const NETWORK_EVIDENCE_IDS = new Set(['availability', 'dns', 'reverse-dns', 'network-context']);
|
|
21
|
-
const WEB_EVIDENCE_IDS = new Set(['http', 'tls', 'page-identity', 'page-role', 'client-behavior', 'security-posture', 'technology']);
|
|
22
|
-
const TASK_FRESHNESS_THRESHOLDS = Object.freeze({
|
|
23
|
-
general: Object.freeze({ registration: 30, network: 7, web: 3 }),
|
|
24
|
-
acquisition: Object.freeze({ registration: 7, network: 3, web: 3 }),
|
|
25
|
-
brand: Object.freeze({ registration: 30, network: 3, web: 1 }),
|
|
26
|
-
incident: Object.freeze({ registration: 14, network: 1, web: 1 }),
|
|
27
|
-
owned: Object.freeze({ registration: 30, network: 7, web: 3 }),
|
|
28
|
-
});
|
|
29
|
-
function freshnessDays(value, fallback) {
|
|
30
|
-
const parsed = Number(value);
|
|
31
|
-
return Number.isFinite(parsed) ? Math.max(1, Math.min(365, Math.round(parsed))) : fallback;
|
|
32
|
-
}
|
|
33
|
-
export function buildLookupFreshnessPolicy(task, input) {
|
|
34
|
-
const defaults = TASK_FRESHNESS_THRESHOLDS[task];
|
|
35
|
-
const custom = input?.id === 'analyst-custom';
|
|
36
|
-
return {
|
|
37
|
-
version: LOOKUP_FRESHNESS_POLICY_VERSION,
|
|
38
|
-
id: custom ? 'analyst-custom' : 'task-default',
|
|
39
|
-
task,
|
|
40
|
-
thresholdsDays: custom ? {
|
|
41
|
-
registration: freshnessDays(input?.thresholdsDays?.registration, defaults.registration),
|
|
42
|
-
network: freshnessDays(input?.thresholdsDays?.network, defaults.network),
|
|
43
|
-
web: freshnessDays(input?.thresholdsDays?.web, defaults.web),
|
|
44
|
-
} : { ...defaults },
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function observedAgeDays(observedAt, now) {
|
|
48
|
-
if (typeof observedAt !== 'string' || typeof now !== 'string')
|
|
49
|
-
return null;
|
|
50
|
-
const observedMs = Date.parse(observedAt);
|
|
51
|
-
const nowMs = Date.parse(now);
|
|
52
|
-
if (!Number.isFinite(observedMs) || !Number.isFinite(nowMs))
|
|
53
|
-
return null;
|
|
54
|
-
return Math.max(0, Math.floor((nowMs - observedMs) / 86_400_000));
|
|
55
|
-
}
|
|
56
|
-
function limited(entries, ids) {
|
|
57
|
-
return entries.some((entry) => ids.has(entry.id) && entry.manualReviewSuggested);
|
|
58
|
-
}
|
|
59
|
-
function availableIds(entries, ids) {
|
|
60
|
-
return entries.filter((entry) => ids.has(entry.id)).map((entry) => entry.id).slice(0, 12);
|
|
61
|
-
}
|
|
62
|
-
export function buildLookupSourceRefreshPlan(ledger, observedAt, now = new Date().toISOString(), options = {}) {
|
|
63
|
-
const ageDays = observedAgeDays(observedAt, now);
|
|
64
|
-
const freshnessPolicy = buildLookupFreshnessPolicy(options.task ?? 'general', options.freshnessPolicy);
|
|
65
|
-
const entries = ledger.entries.slice(0, 24);
|
|
66
|
-
const plans = [];
|
|
67
|
-
const groups = [
|
|
68
|
-
{
|
|
69
|
-
id: 'rdap',
|
|
70
|
-
label: 'Registry RDAP',
|
|
71
|
-
endpoint: '/api/rdap',
|
|
72
|
-
ids: new Set(['rdap']),
|
|
73
|
-
disclosure: 'Starts one bounded registry RDAP operation for this target.',
|
|
74
|
-
threshold: 'registration',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: 'whois',
|
|
78
|
-
label: 'WHOIS',
|
|
79
|
-
endpoint: '/api/whois',
|
|
80
|
-
ids: new Set(['whois']),
|
|
81
|
-
disclosure: 'Starts one bounded referral-aware WHOIS operation for this target.',
|
|
82
|
-
threshold: 'registration',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 'availability',
|
|
86
|
-
label: 'Domain evidence',
|
|
87
|
-
endpoint: '/api/availability',
|
|
88
|
-
ids: DOMAIN_EVIDENCE_IDS,
|
|
89
|
-
disclosure: 'Repeats the bounded domain-evidence branch, including eligible DNS, HTTP, page, and TLS work for the selected depth.',
|
|
90
|
-
threshold: 'network',
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
for (const group of groups) {
|
|
94
|
-
const evidenceIds = availableIds(entries, group.ids);
|
|
95
|
-
if (!evidenceIds.length)
|
|
96
|
-
continue;
|
|
97
|
-
const isLimited = limited(entries, group.ids);
|
|
98
|
-
const thresholds = evidenceIds.map((id) => WEB_EVIDENCE_IDS.has(id)
|
|
99
|
-
? freshnessPolicy.thresholdsDays.web
|
|
100
|
-
: NETWORK_EVIDENCE_IDS.has(id)
|
|
101
|
-
? freshnessPolicy.thresholdsDays.network
|
|
102
|
-
: freshnessPolicy.thresholdsDays[group.threshold]);
|
|
103
|
-
const staleAfterDays = Math.min(...thresholds);
|
|
104
|
-
const evidenceAges = evidenceIds
|
|
105
|
-
.map((id) => observedAgeDays(options.observedAtByEvidence?.[id] ?? observedAt, now))
|
|
106
|
-
.filter((value) => value !== null);
|
|
107
|
-
const groupAgeDays = evidenceAges.length ? Math.max(...evidenceAges) : ageDays;
|
|
108
|
-
const groupStale = groupAgeDays !== null && groupAgeDays >= staleAfterDays;
|
|
109
|
-
if (!isLimited && !groupStale)
|
|
110
|
-
continue;
|
|
111
|
-
plans.push({
|
|
112
|
-
id: group.id,
|
|
113
|
-
label: group.label,
|
|
114
|
-
endpoint: group.endpoint,
|
|
115
|
-
evidenceIds,
|
|
116
|
-
reason: isLimited ? 'limited' : 'stale',
|
|
117
|
-
requestDisclosure: group.disclosure,
|
|
118
|
-
supersedesObservedAt: typeof observedAt === 'string' && Number.isFinite(Date.parse(observedAt))
|
|
119
|
-
? new Date(observedAt).toISOString()
|
|
120
|
-
: null,
|
|
121
|
-
ageDays: groupAgeDays,
|
|
122
|
-
staleAfterDays,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
const stale = plans.some((item) => item.reason === 'stale');
|
|
126
|
-
return {
|
|
127
|
-
version: LOOKUP_SOURCE_REFRESH_VERSION,
|
|
128
|
-
stale,
|
|
129
|
-
ageDays,
|
|
130
|
-
freshnessPolicy,
|
|
131
|
-
items: plans,
|
|
132
|
-
limitations: [
|
|
133
|
-
'A source refresh is displayed separately and never merged into the original unified Lookup envelope.',
|
|
134
|
-
'Run a complete Lookup before saving, comparing, or exporting replacement evidence collected at one review time.',
|
|
135
|
-
'A retry can remain partial or unavailable and never proves that missing evidence is absent.',
|
|
136
|
-
'Freshness thresholds organise review only. They do not make an older observation false or a newer observation complete.',
|
|
137
|
-
],
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
function record(value) {
|
|
141
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
142
|
-
? value
|
|
143
|
-
: {};
|
|
144
|
-
}
|
|
145
|
-
function text(value, maximum = 180) {
|
|
146
|
-
return String(value ?? '')
|
|
147
|
-
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
148
|
-
.replace(/\s+/gu, ' ')
|
|
149
|
-
.trim()
|
|
150
|
-
.slice(0, maximum);
|
|
151
|
-
}
|
|
152
|
-
function summarizeSource(plan, body, observedAt, depth, supersedesObservedAt) {
|
|
153
|
-
const base = {
|
|
154
|
-
version: LOOKUP_SOURCE_REFRESH_VERSION,
|
|
155
|
-
id: plan.id,
|
|
156
|
-
observedAt,
|
|
157
|
-
attemptedAt: observedAt,
|
|
158
|
-
reason: plan.reason,
|
|
159
|
-
evidenceIds: plan.evidenceIds.slice(0, 12),
|
|
160
|
-
supersedesObservedAt,
|
|
161
|
-
};
|
|
162
|
-
if (plan.id === 'rdap') {
|
|
163
|
-
const upstreamStatus = Number(body.upstreamStatus);
|
|
164
|
-
const parsed = record(body.parsed);
|
|
165
|
-
const complete = upstreamStatus === 200 && Object.keys(parsed).length > 0;
|
|
166
|
-
return {
|
|
167
|
-
...base,
|
|
168
|
-
state: complete ? 'complete' : 'limited',
|
|
169
|
-
detail: complete
|
|
170
|
-
? `Registry RDAP returned a validated ${upstreamStatus} response.`
|
|
171
|
-
: `Registry RDAP returned ${Number.isFinite(upstreamStatus) ? `HTTP ${upstreamStatus}` : 'no complete structured record'}.`,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (plan.id === 'whois') {
|
|
175
|
-
const chain = Array.isArray(body.chain) ? body.chain : [];
|
|
176
|
-
const chainStatus = text(record(body.parsed).chainStatus, 40);
|
|
177
|
-
const complete = chain.length > 0 && chainStatus === 'complete';
|
|
178
|
-
return {
|
|
179
|
-
...base,
|
|
180
|
-
state: complete ? 'complete' : 'limited',
|
|
181
|
-
detail: complete
|
|
182
|
-
? `WHOIS returned a complete ${chain.length}-hop referral chain.`
|
|
183
|
-
: `WHOIS returned ${chain.length} hop${chain.length === 1 ? '' : 's'} with ${chainStatus || 'unknown'} chain status.`,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
const state = text(body.state, 40) || 'unknown';
|
|
187
|
-
const complete = body.deepScanComplete === true
|
|
188
|
-
|| (depth === 'fast' && state !== 'unknown');
|
|
189
|
-
const sourceStates = ['dns', 'http', 'tls']
|
|
190
|
-
.map((key) => text(record(body[key]).status, 40))
|
|
191
|
-
.filter(Boolean);
|
|
192
|
-
return {
|
|
193
|
-
...base,
|
|
194
|
-
state: complete ? 'complete' : 'limited',
|
|
195
|
-
detail: `Domain evidence returned ${state}${sourceStates.length ? `; DNS, HTTP, and TLS states: ${sourceStates.join(', ')}` : ''}.`,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, detail) {
|
|
199
|
-
return {
|
|
200
|
-
version: LOOKUP_SOURCE_REFRESH_VERSION,
|
|
201
|
-
id: plan.id,
|
|
202
|
-
state: 'unavailable',
|
|
203
|
-
detail,
|
|
204
|
-
observedAt: null,
|
|
205
|
-
attemptedAt,
|
|
206
|
-
reason: plan.reason,
|
|
207
|
-
evidenceIds: plan.evidenceIds.slice(0, 12),
|
|
208
|
-
supersedesObservedAt,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
export async function requestLookupSourceRefresh(plan, query, depth, options = {}) {
|
|
212
|
-
const normalizedQuery = text(query, 4096);
|
|
213
|
-
if (!normalizedQuery)
|
|
214
|
-
return { ok: false, message: 'A valid Lookup target is required.' };
|
|
215
|
-
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
216
|
-
? Math.max(1, Math.min(LOOKUP_SOURCE_REFRESH_TIMEOUT_MS, Math.round(Number(options.timeoutMs))))
|
|
217
|
-
: LOOKUP_SOURCE_REFRESH_TIMEOUT_MS;
|
|
218
|
-
const attemptedAt = options.now?.() ?? new Date().toISOString();
|
|
219
|
-
const supersedesObservedAt = options.supersedesObservedAt === undefined
|
|
220
|
-
? plan.supersedesObservedAt
|
|
221
|
-
: options.supersedesObservedAt;
|
|
222
|
-
try {
|
|
223
|
-
const url = `${plan.endpoint}?q=${encodeURIComponent(normalizedQuery)}${plan.id === 'availability' && depth === 'fast' ? '&fast=true' : ''}`;
|
|
224
|
-
const { response, body: rawBody } = await requestJsonCapped(url, {
|
|
225
|
-
credentials: 'same-origin',
|
|
226
|
-
}, {
|
|
227
|
-
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
228
|
-
maximumBytes: MAX_LOOKUP_SOURCE_REFRESH_BYTES,
|
|
229
|
-
timeoutMs,
|
|
230
|
-
});
|
|
231
|
-
const body = record(rawBody);
|
|
232
|
-
if (!response.ok) {
|
|
233
|
-
return {
|
|
234
|
-
ok: true,
|
|
235
|
-
value: unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, text(body.error, 240) || `The source returned HTTP ${response.status}; no new observation was recorded.`),
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
if (Object.keys(body).length > MAX_LOOKUP_SOURCE_REFRESH_KEYS) {
|
|
239
|
-
return { ok: false, message: 'Source refresh returned an oversized record.' };
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
ok: true,
|
|
243
|
-
value: summarizeSource(plan, body, attemptedAt, depth, supersedesObservedAt),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
catch (cause) {
|
|
247
|
-
if (cause instanceof BoundedJsonResponseError && cause.code === 'response_too_large') {
|
|
248
|
-
return { ok: true, value: unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, 'The source response exceeded the local limit; no new observation was recorded.') };
|
|
249
|
-
}
|
|
250
|
-
return {
|
|
251
|
-
ok: true,
|
|
252
|
-
value: unavailableRefreshResult(plan, attemptedAt, supersedesObservedAt, cause instanceof BoundedJsonResponseError && cause.code === 'timeout'
|
|
253
|
-
? 'The source refresh timed out; no new observation was recorded.'
|
|
254
|
-
: 'The source refresh could not be completed; no new observation was recorded.'),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
export function mergeLookupSourceRefreshLedger(current, result) {
|
|
259
|
-
const candidates = [
|
|
260
|
-
...(current?.version === LOOKUP_SOURCE_REFRESH_VERSION ? current.entries : []),
|
|
261
|
-
result,
|
|
262
|
-
];
|
|
263
|
-
const byObservation = new Map();
|
|
264
|
-
for (const entry of candidates.slice(-MAX_LOOKUP_SOURCE_REFRESH_HISTORY * 4)) {
|
|
265
|
-
byObservation.set(`${entry.id}:${entry.attemptedAt}`, entry);
|
|
266
|
-
}
|
|
267
|
-
const ordered = [...byObservation.values()]
|
|
268
|
-
.sort((left, right) => Date.parse(left.attemptedAt) - Date.parse(right.attemptedAt));
|
|
269
|
-
const truncated = Boolean(current?.truncated)
|
|
270
|
-
|| ordered.length > MAX_LOOKUP_SOURCE_REFRESH_HISTORY
|
|
271
|
-
|| candidates.length > MAX_LOOKUP_SOURCE_REFRESH_HISTORY;
|
|
272
|
-
return {
|
|
273
|
-
version: LOOKUP_SOURCE_REFRESH_VERSION,
|
|
274
|
-
entries: ordered.slice(-MAX_LOOKUP_SOURCE_REFRESH_HISTORY),
|
|
275
|
-
truncated,
|
|
276
|
-
};
|
|
277
|
-
}
|