@slicedearth/whoisleuth-cli 1.47.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/lib/tls-intelligence.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Bounded, one-connection TLS and certificate
|
|
1
|
+
// Bounded, one-connection TLS and certificate evidence for deep scans.
|
|
2
2
|
//
|
|
3
3
|
// The collector resolves the submitted hostname once through the same public-
|
|
4
4
|
// address guard used by safe-fetch, validates every returned address again at
|
|
@@ -10,7 +10,7 @@ import * as crypto from 'node:crypto';
|
|
|
10
10
|
import * as net from 'node:net';
|
|
11
11
|
import * as tls from 'node:tls';
|
|
12
12
|
import { domainToASCII } from 'node:url';
|
|
13
|
-
import { createObservation } from "
|
|
13
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
14
14
|
import { MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
|
|
15
15
|
import { isPrivateAddress, resolvePublicAddresses } from "./safe-fetch.mjs";
|
|
16
16
|
import { parseCertificateExtensionProfile, } from "./certificate-extension-profile.mjs";
|
|
@@ -671,7 +671,7 @@ function failedTlsObservation(error, options = {}) {
|
|
|
671
671
|
source: 'tls',
|
|
672
672
|
durationMs: options.durationMs,
|
|
673
673
|
complete: false,
|
|
674
|
-
limitations: ['TLS
|
|
674
|
+
limitations: ['TLS evidence collection was inconclusive; DNS, connection, timeout, policy, or handshake failure is not proof that no TLS service exists.'],
|
|
675
675
|
diagnostics: { connectionAttempts: options.connectionAttempts || 0, error: detail },
|
|
676
676
|
}),
|
|
677
677
|
profileVersion: TLS_PROFILE_VERSION,
|
|
@@ -692,7 +692,7 @@ function failedTlsObservation(error, options = {}) {
|
|
|
692
692
|
findings: [],
|
|
693
693
|
};
|
|
694
694
|
}
|
|
695
|
-
function skippedTlsObservation(detail = 'TLS
|
|
695
|
+
function skippedTlsObservation(detail = 'TLS evidence collection is disabled by deployment policy.') {
|
|
696
696
|
return {
|
|
697
697
|
...failedTlsObservation(detail, { connectionAttempts: 0 }),
|
|
698
698
|
...createObservation({
|
|
@@ -9,6 +9,7 @@ import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
|
|
|
9
9
|
import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
10
10
|
import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
|
|
11
11
|
import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
|
|
12
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
12
13
|
const URLHAUS_HOST_ENDPOINT = 'https://urlhaus-api.abuse.ch/v1/host/';
|
|
13
14
|
const URLHAUS_MAX_RESULTS = 20;
|
|
14
15
|
const URLHAUS_MAX_INPUT_RESULTS = 100;
|
|
@@ -82,8 +83,7 @@ function isoTimestamp(value) {
|
|
|
82
83
|
const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?: UTC)?$/u.test(trimmed)
|
|
83
84
|
? `${trimmed.slice(0, 10)}T${trimmed.slice(11, 19)}Z`
|
|
84
85
|
: trimmed;
|
|
85
|
-
|
|
86
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
86
|
+
return normalizeExplicitIsoTimestamp(normalized);
|
|
87
87
|
}
|
|
88
88
|
function retryAfterSeconds(response) {
|
|
89
89
|
const value = response.headers.get('retry-after');
|
|
@@ -151,6 +151,8 @@ function normalizeHostFinding(value, targetDomain) {
|
|
|
151
151
|
|| exactDomainFromUrl(value.url) !== targetDomain)
|
|
152
152
|
return null;
|
|
153
153
|
const observedAt = isoTimestamp(value.date_added);
|
|
154
|
+
if (value.date_added !== undefined && value.date_added !== null && !observedAt)
|
|
155
|
+
return null;
|
|
154
156
|
const tags = normalizedTags(value.tags);
|
|
155
157
|
tags.push(`url-status:${status}`);
|
|
156
158
|
return {
|
|
@@ -7,6 +7,7 @@ import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
|
|
|
7
7
|
import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
8
8
|
import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
|
|
9
9
|
import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
|
|
10
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
10
11
|
const URLSCAN_SEARCH_ENDPOINT = 'https://urlscan.io/api/v1/search/';
|
|
11
12
|
const URLSCAN_SEARCH_DAYS = 90;
|
|
12
13
|
const URLSCAN_MAX_RESULTS = 20;
|
|
@@ -80,10 +81,7 @@ function boundedText(value, maxLength) {
|
|
|
80
81
|
return normalized ? normalized.slice(0, maxLength) : null;
|
|
81
82
|
}
|
|
82
83
|
function isoTimestamp(value) {
|
|
83
|
-
|
|
84
|
-
return null;
|
|
85
|
-
const timestamp = Date.parse(value);
|
|
86
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
84
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
87
85
|
}
|
|
88
86
|
function retryAfterSeconds(response) {
|
|
89
87
|
for (const name of ['retry-after', 'x-rate-limit-reset-after']) {
|
|
@@ -148,7 +146,10 @@ function normalizeSearchFinding(value, targetDomain) {
|
|
|
148
146
|
if (verdicts.malicious !== true)
|
|
149
147
|
return undefined;
|
|
150
148
|
const categories = normalizedCategories(verdicts.categories);
|
|
151
|
-
const
|
|
149
|
+
const rawObservedAt = task.time ?? value.date;
|
|
150
|
+
const observedAt = isoTimestamp(rawObservedAt);
|
|
151
|
+
if (rawObservedAt !== undefined && rawObservedAt !== null && !observedAt)
|
|
152
|
+
return null;
|
|
152
153
|
const title = boundedText(page.title, 180);
|
|
153
154
|
return {
|
|
154
155
|
id,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// HTTP, static page, TLS, and DNS evidence already collected by a deep Lookup.
|
|
3
3
|
// Findings use fixed explanatory text and bounded counts; upstream header
|
|
4
4
|
// values, certificate errors, URLs, and DNS record contents are not copied.
|
|
5
|
-
import { createObservation } from "
|
|
5
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
6
6
|
const WEBSITE_SECURITY_POSTURE_VERSION = 2;
|
|
7
7
|
const MAX_SECURITY_POSTURE_FINDINGS = 32;
|
|
8
8
|
const MAX_RETAINED_ORIGINS = 30;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// markup, header values, URL paths, and arbitrary upstream strings are never
|
|
6
6
|
// retained.
|
|
7
7
|
import { analyzeBrowserLibraries } from "./browser-library-profile.mjs";
|
|
8
|
-
import { createObservation } from "
|
|
8
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
9
9
|
import { MAX_STATIC_HTML_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
10
|
const TECHNOLOGY_PROFILE_VERSION = 10;
|
|
11
11
|
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
package/lib/whois-authority.mjs
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
// Authority-aware interpretation of WHOIS referral chains.
|
|
2
|
-
//
|
|
3
|
-
// diagnostic and cannot
|
|
1
|
+
// Authority-aware interpretation of WHOIS referral chains. Only the first
|
|
2
|
+
// registry response referred by IANA can decide existence; later registrar
|
|
3
|
+
// output is diagnostic and cannot replace an inconclusive registry result.
|
|
4
4
|
import { normalizeWhoisChain } from "./whois-normalization.mjs";
|
|
5
5
|
import { parseIndentedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
|
|
6
|
-
const NOT_FOUND_RE = /no match for|no match\b|not found|no entries found|domain not found|no object found|not registered|status\s*:\s*(?:available|free)\b|registered\s*:\s*(?:no|false)\b|is available for registration/i;
|
|
7
6
|
const LINE_NOT_FOUND_PATTERNS = Object.freeze([
|
|
7
|
+
/^[ \t]*(?:%{1,2}[ \t]*)?(?:error(?::\d+)?[ \t:.-]*)?no match(?:[ \t]+for(?:[ \t]+domain)?(?:[ \t]+["']?[a-z0-9.-]{1,253}["']?)?)?[.!]?[ \t]*$/im,
|
|
8
|
+
/^[ \t]*(?:%{1,2}[ \t]*)?(?:error(?::\d+)?[ \t:.-]*)?no entries found(?:[ \t]+(?:for(?:[ \t]+(?:the[ \t]+)?(?:selected source\(s\)|this query|query[ \t]+["']?[a-z0-9.-]{1,253}["']?))|in[ \t]+the[ \t]+\.[a-z0-9-]{1,63}[ \t]+database))?[.!]?[ \t]*$/im,
|
|
9
|
+
/^[ \t]*[a-z0-9.-]{1,253}[ \t]*:[ \t]*no entries found\.?[ \t]*$/im,
|
|
10
|
+
/^[ \t]*(?:%{1,2}[ \t]*)?(?:domain[ \t]+)?not found(?::[ \t]*[a-z0-9.-]{1,253})?[.!]?[ \t]*$/im,
|
|
11
|
+
/^[ \t]*(?:the[ \t]+)?domain[ \t]+["']?[a-z0-9.-]{1,253}["']?[ \t]+(?:was[ \t]+)?not found\.?[ \t]*$/im,
|
|
12
|
+
/^[ \t]*(?:domain[ \t]+status|the queried object does not exist)[ \t]*:[ \t]*(?:domain[ \t]+not found|no object found)[.!]?[ \t]*$/im,
|
|
13
|
+
/^[ \t]*no object found!?[ \t]*$/im,
|
|
14
|
+
/^[ \t]*(?:%{1,2}[ \t]*)?(?:error(?::\d+)?[ \t:.-]*)?(?:the[ \t]+)?domain(?:[ \t]+is|[ \t]+has)?[ \t]+not[ \t]+(?:been[ \t]+)?registered\.?[ \t]*$/im,
|
|
15
|
+
/^[ \t]*(?:domain[ \t]+)?status[ \t]*:[ \t]*(?:available|free)[ \t]*$/im,
|
|
16
|
+
/^[ \t]*registration[ \t]+status[ \t]*:[ \t]*available[ \t]*$/im,
|
|
17
|
+
/^[ \t]*registered[ \t]*:[ \t]*(?:no|false)[ \t]*$/im,
|
|
18
|
+
/^[ \t]*[a-z0-9.-]{1,253}[ \t]+is[ \t]+(?:available[ \t]+for[ \t]+(?:purchase|registration)|free)[.!]?[ \t]*$/im,
|
|
8
19
|
/^[ \t]*%[ \t]*nothing found[ \t]*$/im,
|
|
9
20
|
/^[ \t]*[a-z0-9](?:[a-z0-9.-]{0,252})[ \t]+is free[ \t]*$/im,
|
|
10
21
|
/^[ \t]*el dominio no se encuentra registrado en nic argentina[ \t]*$/im,
|
|
@@ -48,8 +59,7 @@ function classifyHopEvidence(hop, index) {
|
|
|
48
59
|
return 'rate_limited';
|
|
49
60
|
if (NZ_NOT_FOUND_RE.test(text))
|
|
50
61
|
return 'negative';
|
|
51
|
-
if (
|
|
52
|
-
|| LINE_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
62
|
+
if (LINE_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
53
63
|
return 'negative';
|
|
54
64
|
}
|
|
55
65
|
if (index > 0
|
|
@@ -62,6 +72,26 @@ function classifyHopEvidence(hop, index) {
|
|
|
62
72
|
}
|
|
63
73
|
return 'inconclusive';
|
|
64
74
|
}
|
|
75
|
+
function normalizedWhoisServer(value) {
|
|
76
|
+
if (typeof value !== 'string')
|
|
77
|
+
return null;
|
|
78
|
+
const normalized = value.trim().toLowerCase().replace(/\.+$/u, '');
|
|
79
|
+
return /^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/u.test(normalized) ? normalized : null;
|
|
80
|
+
}
|
|
81
|
+
function ianaRegistryReferral(hop) {
|
|
82
|
+
if (!hop || normalizedWhoisServer(hop.server) !== 'whois.iana.org' || typeof hop.response !== 'string')
|
|
83
|
+
return null;
|
|
84
|
+
for (const pattern of [
|
|
85
|
+
/^[ \t]*refer:[ \t]*([a-zA-Z0-9.\-]+)/mi,
|
|
86
|
+
/^[ \t]*ReferralServer:[ \t]*whois:\/\/([a-zA-Z0-9.\-]+)/mi,
|
|
87
|
+
/^[ \t]*whois:[ \t]*([a-zA-Z0-9.\-]+)/mi,
|
|
88
|
+
]) {
|
|
89
|
+
const referral = normalizedWhoisServer(hop.response.match(pattern)?.[1]);
|
|
90
|
+
if (referral)
|
|
91
|
+
return referral;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
65
95
|
export function analyzeWhoisChainAuthority(chain) {
|
|
66
96
|
const source = normalizeWhoisChain(chain);
|
|
67
97
|
const evidence = source.map((hop, index) => ({
|
|
@@ -70,8 +100,13 @@ export function analyzeWhoisChainAuthority(chain) {
|
|
|
70
100
|
kind: classifyHopEvidence(hop, index),
|
|
71
101
|
}));
|
|
72
102
|
const failed = evidence.filter((item) => item.kind === 'error' || item.kind === 'rate_limited');
|
|
73
|
-
const
|
|
74
|
-
|
|
103
|
+
const registryReferral = ianaRegistryReferral(source[0]);
|
|
104
|
+
const registryServer = normalizedWhoisServer(source[1]?.server);
|
|
105
|
+
const registryEvidence = registryReferral && registryServer === registryReferral ? evidence[1] : null;
|
|
106
|
+
const authoritative = registryEvidence
|
|
107
|
+
&& (registryEvidence.kind === 'positive' || registryEvidence.kind === 'negative')
|
|
108
|
+
? registryEvidence
|
|
109
|
+
: null;
|
|
75
110
|
const conflict = authoritative
|
|
76
111
|
? evidence.find((item) => item.index > authoritative.index
|
|
77
112
|
&& (item.kind === 'positive' || item.kind === 'negative')
|
package/lib/whois-chain.mjs
CHANGED
|
@@ -8,6 +8,17 @@ import { whoisQuery } from "./whois-transport.mjs";
|
|
|
8
8
|
const IANA_WHOIS = 'whois.iana.org';
|
|
9
9
|
const WHOIS_HOP_DEADLINE_MS = 12_000;
|
|
10
10
|
const WHOIS_CHAIN_DEADLINE_MS = 25_000;
|
|
11
|
+
const MAX_WHOIS_QUERY_HOPS = 6;
|
|
12
|
+
function incompleteReferralHop(server, reason) {
|
|
13
|
+
return {
|
|
14
|
+
server,
|
|
15
|
+
queryProfile: 'not-issued',
|
|
16
|
+
responseEncoding: 'utf-8',
|
|
17
|
+
error: reason === 'hop_limit'
|
|
18
|
+
? 'WHOIS referral chain reached the bounded hop limit; this referral was not queried.'
|
|
19
|
+
: 'WHOIS referral chain repeated a previously queried server; this referral was not queried.',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
11
22
|
const WHOIS_QUERY_FORMATTERS = {
|
|
12
23
|
'plain-domain': (domain) => domain,
|
|
13
24
|
'denic-domain-ace': (domain) => `-T dn,ace ${domain}`,
|
|
@@ -65,9 +76,11 @@ export async function buildWhoisChainUncached(query, options = {}) {
|
|
|
65
76
|
const visited = new Set();
|
|
66
77
|
let currentServer = IANA_WHOIS;
|
|
67
78
|
const startedAt = now();
|
|
68
|
-
for (let hop = 0; hop <
|
|
69
|
-
if (visited.has(currentServer.toLowerCase()))
|
|
79
|
+
for (let hop = 0; hop < MAX_WHOIS_QUERY_HOPS; hop += 1) {
|
|
80
|
+
if (visited.has(currentServer.toLowerCase())) {
|
|
81
|
+
chain.push(incompleteReferralHop(currentServer, 'referral_loop'));
|
|
70
82
|
break;
|
|
83
|
+
}
|
|
71
84
|
visited.add(currentServer.toLowerCase());
|
|
72
85
|
const transport = whoisTransportForHop(query, hop);
|
|
73
86
|
const remainingMs = chainDeadlineMs - (now() - startedAt);
|
|
@@ -112,8 +125,17 @@ export async function buildWhoisChainUncached(query, options = {}) {
|
|
|
112
125
|
response: responseText,
|
|
113
126
|
});
|
|
114
127
|
const referral = extractReferral(responseText);
|
|
115
|
-
if (!referral
|
|
128
|
+
if (!referral)
|
|
116
129
|
break;
|
|
130
|
+
const referralKey = referral.toLowerCase();
|
|
131
|
+
if (visited.has(referralKey)) {
|
|
132
|
+
chain.push(incompleteReferralHop(referral, 'referral_loop'));
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (hop + 1 >= MAX_WHOIS_QUERY_HOPS) {
|
|
136
|
+
chain.push(incompleteReferralHop(referral, 'hop_limit'));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
117
139
|
currentServer = referral;
|
|
118
140
|
}
|
|
119
141
|
return chain;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Keeping this separate lets authority analysis and field parsing share the
|
|
3
3
|
// exact same defensive input boundary.
|
|
4
4
|
import { MAX_WHOIS_BYTES } from "./whois-transport.mjs";
|
|
5
|
-
|
|
5
|
+
// Collection performs at most six network queries and may retain one seventh,
|
|
6
|
+
// non-issued hop explaining a loop or hop-limit termination.
|
|
7
|
+
const MAX_WHOIS_HOPS = 7;
|
|
6
8
|
export function normalizeWhoisChain(value) {
|
|
7
9
|
if (!Array.isArray(value))
|
|
8
10
|
return [];
|
package/lib/whois-transport.mjs
CHANGED
|
@@ -47,9 +47,18 @@ function queryWhoisAddress(address, server, query, { port = 43, timeoutMs = 1000
|
|
|
47
47
|
}
|
|
48
48
|
chunks.push(buffer);
|
|
49
49
|
});
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const settleResponse = () => {
|
|
51
|
+
try {
|
|
52
|
+
const responseText = new TextDecoder('utf-8', { fatal: true })
|
|
53
|
+
.decode(Buffer.concat(chunks, totalBytes));
|
|
54
|
+
settle(resolve, responseText);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
settle(reject, new Error(`WHOIS response from ${server} was not valid UTF-8`));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
socket.on('end', settleResponse);
|
|
61
|
+
socket.on('close', settleResponse);
|
|
53
62
|
socket.on('timeout', () => {
|
|
54
63
|
socket.destroy();
|
|
55
64
|
settle(reject, new Error(`WHOIS request to ${server} timed out`));
|
|
@@ -175,7 +175,7 @@ export function normaliseRdata(type, rawValue, origin, nameMode = 'absolute') {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
if (type === 'CDNSKEY') {
|
|
178
|
-
if (tokens.length !== 4 || !/^[
|
|
178
|
+
if (tokens.length !== 4 || !/^[A-Za-z0-9+/=]{1,4096}$/u.test(tokens[3] ?? ''))
|
|
179
179
|
throw new TypeError('CDNSKEY data is invalid.');
|
|
180
180
|
return {
|
|
181
181
|
value: `${integerToken(tokens[0], 0, 65_535, 'CDNSKEY flags')} ${integerToken(tokens[1], 0, 255, 'CDNSKEY protocol')} ${integerToken(tokens[2], 0, 255, 'CDNSKEY algorithm')} ${tokens[3]}`,
|
package/package.json
CHANGED
|
@@ -33,20 +33,28 @@
|
|
|
33
33
|
"bugs": {
|
|
34
34
|
"url": "https://github.com/slicedearth/whoisleuth/issues"
|
|
35
35
|
},
|
|
36
|
-
"version": "
|
|
36
|
+
"version": "2.0.0",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@peculiar/x509": "2.0.0",
|
|
39
39
|
"maxmind": "5.0.7",
|
|
40
40
|
"parse5": "8.0.1",
|
|
41
41
|
"reflect-metadata": "0.2.2",
|
|
42
|
-
"tldts": "7.4.
|
|
43
|
-
"undici": "8.
|
|
42
|
+
"tldts": "7.4.11",
|
|
43
|
+
"undici": "8.10.0"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"bin/**/*.mjs",
|
|
47
47
|
"cli/**/*.mjs",
|
|
48
48
|
"lib/**/*.mjs",
|
|
49
|
-
"frontend/src/lib
|
|
49
|
+
"frontend/src/lib/**/*.js",
|
|
50
|
+
"packages/cases/**/*.mjs",
|
|
51
|
+
"packages/comparison/**/*.mjs",
|
|
52
|
+
"packages/contracts/**/*.mjs",
|
|
53
|
+
"packages/evidence/**/*.mjs",
|
|
54
|
+
"packages/interchange/**/*.mjs",
|
|
55
|
+
"packages/investigation/**/*.mjs",
|
|
56
|
+
"packages/monitoring/**/*.mjs",
|
|
57
|
+
"packages/workspace/**/*.mjs",
|
|
50
58
|
"docs/cli.md",
|
|
51
59
|
"docs/cli-reference.md",
|
|
52
60
|
"DISCLOSURE",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, } from "./case-record-contracts.
|
|
2
|
-
import { normalizeOpaqueReferenceId } from "./opaque-reference-id.
|
|
1
|
+
import { MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, } from "./case-record-contracts.mjs";
|
|
2
|
+
import { normalizeOpaqueReferenceId } from "./opaque-reference-id.mjs";
|
|
3
3
|
/**
|
|
4
4
|
* Reads an untrusted stored or imported reference list without repairing any
|
|
5
5
|
* identifier. At most 32 candidates are inspected and at most eight unique,
|
package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs}
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import { normalizeHttpSummary } from "./http-summary.
|
|
4
|
-
import { normalizeOpportunityModelVersion
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { normalizeHttpSummary } from "./http-summary.mjs";
|
|
4
|
+
import { normalizeOpportunityModelVersion } from "../../lib/opportunity-scoring.mjs";
|
|
5
|
+
import { normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
|
|
6
|
+
import { MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, } from "./case-record-contracts.mjs";
|
|
7
|
+
import { CONCLUSIVE_AVAILABILITY, DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_RANK, EVIDENCE_SOURCE_SET, REGISTERED_LIKE, caseTimestampOrNull, hashString, isoOrNull, normalizeEvidenceHostnameForCase, objectRecord, safeId, } from "./case-record-core.mjs";
|
|
7
8
|
function evidenceString(value, max = MAX_EVIDENCE_STRING_LENGTH) {
|
|
8
9
|
if (value == null)
|
|
9
10
|
return null;
|
|
@@ -81,6 +82,12 @@ function dayOf(value) {
|
|
|
81
82
|
const iso = isoOrNull(value);
|
|
82
83
|
return iso ? iso.slice(0, 10) : null;
|
|
83
84
|
}
|
|
85
|
+
function lifecycleTimestamp(value, sourceVersion) {
|
|
86
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/u.test(value)) {
|
|
87
|
+
return caseTimestampOrNull(`${value}T00:00:00Z`);
|
|
88
|
+
}
|
|
89
|
+
return caseTimestampOrNull(value, sourceVersion);
|
|
90
|
+
}
|
|
84
91
|
// Capture completeness is recorded explicitly, never inferred from whether a
|
|
85
92
|
// boolean happens to be false. 'fast' captures skip the DNS/site/HTML probes;
|
|
86
93
|
// 'deep' evaluates them; 'unknown' is for migrated/imported evidence whose depth
|
|
@@ -104,6 +111,7 @@ const DEEP_SIGNAL_FIELDS = [
|
|
|
104
111
|
// included so captures of differing completeness can never be confused for one
|
|
105
112
|
// another. Deterministic ordering here is what makes the fingerprint stable.
|
|
106
113
|
const MATERIAL_FIELD_ORDER = [
|
|
114
|
+
'inputHostname',
|
|
107
115
|
'scanDepth',
|
|
108
116
|
'availability', 'confidence', 'riskModelVersion', 'riskScore', 'opportunityModelVersion', 'opportunityScore',
|
|
109
117
|
'riskFactors', 'opportunityFactors',
|
|
@@ -157,7 +165,7 @@ function isEmptyMaterial(value) {
|
|
|
157
165
|
}
|
|
158
166
|
// Fields that describe the capture rather than assert evidence, so they never
|
|
159
167
|
// on their own keep an otherwise-empty snapshot alive.
|
|
160
|
-
const NON_EVIDENCE_MATERIAL = new Set(['scanDepth', 'confidence']);
|
|
168
|
+
const NON_EVIDENCE_MATERIAL = new Set(['inputHostname', 'scanDepth', 'confidence']);
|
|
161
169
|
// A snapshot with no material evidence (only timestamps/source/depth, or only a
|
|
162
170
|
// bare confidence/unknown-availability) is dropped rather than added to a
|
|
163
171
|
// timeline.
|
|
@@ -173,8 +181,15 @@ function hasMaterialEvidence(snapshot) {
|
|
|
173
181
|
// Deterministic string form of the material identity, keys in fixed order.
|
|
174
182
|
function canonicalMaterialString(snapshot) {
|
|
175
183
|
const canonical = {};
|
|
176
|
-
for (const field of MATERIAL_FIELD_ORDER)
|
|
177
|
-
|
|
184
|
+
for (const field of MATERIAL_FIELD_ORDER) {
|
|
185
|
+
const value = materialValue(field, snapshot);
|
|
186
|
+
// Preserve historical fingerprints when the new v14 observation-context
|
|
187
|
+
// field is absent. A retained hostname is still material and therefore
|
|
188
|
+
// separates otherwise-identical captures.
|
|
189
|
+
if (field === 'inputHostname' && value === null)
|
|
190
|
+
continue;
|
|
191
|
+
canonical[field] = value;
|
|
192
|
+
}
|
|
178
193
|
return JSON.stringify(canonical);
|
|
179
194
|
}
|
|
180
195
|
/**
|
|
@@ -186,7 +201,7 @@ function canonicalMaterialString(snapshot) {
|
|
|
186
201
|
* pass a real fallback; the import path deliberately passes an older case
|
|
187
202
|
* timestamp (never "now") so malformed imported evidence can't appear newest.
|
|
188
203
|
* @param {unknown} raw
|
|
189
|
-
* @param {{ source?: string, fallback?: string | null }} [options]
|
|
204
|
+
* @param {{ source?: string, fallback?: string | null, sourceVersion?: number | null, caseDomain?: string | null }} [options]
|
|
190
205
|
* @returns {CaseEvidenceSnapshot | null}
|
|
191
206
|
*/
|
|
192
207
|
export function normalizeSnapshot(raw, options = {}) {
|
|
@@ -201,7 +216,11 @@ function buildSnapshot(raw, options) {
|
|
|
201
216
|
const scanDepth = normalizeScanDepth(record.scanDepth);
|
|
202
217
|
const httpSummary = normalizeHttpSummary(record);
|
|
203
218
|
const acceptsProfileContext = options.sourceVersion === undefined || Number(options.sourceVersion) >= 12;
|
|
219
|
+
const acceptsInputHostname = options.sourceVersion === undefined || Number(options.sourceVersion) >= 13;
|
|
204
220
|
const fields = {
|
|
221
|
+
inputHostname: acceptsInputHostname
|
|
222
|
+
? normalizeEvidenceHostnameForCase(record.inputHostname, options.caseDomain)
|
|
223
|
+
: null,
|
|
205
224
|
scanDepth,
|
|
206
225
|
availability: evidenceString(record.availability),
|
|
207
226
|
confidence: evidenceString(record.confidence),
|
|
@@ -212,8 +231,8 @@ function buildSnapshot(raw, options) {
|
|
|
212
231
|
riskFactors: normalizeFactors(record.riskFactors),
|
|
213
232
|
opportunityFactors: normalizeFactors(record.opportunityFactors),
|
|
214
233
|
registrar: evidenceString(record.registrar),
|
|
215
|
-
createdDate:
|
|
216
|
-
expiryDate:
|
|
234
|
+
createdDate: lifecycleTimestamp(record.createdDate, options.sourceVersion),
|
|
235
|
+
expiryDate: lifecycleTimestamp(record.expiryDate, options.sourceVersion),
|
|
217
236
|
nameservers: normalizeNameserverList(record.nameservers),
|
|
218
237
|
hasMx: boolOrNull(record.hasMx),
|
|
219
238
|
hasSpf: boolOrNull(record.hasSpf),
|
|
@@ -286,10 +305,10 @@ function buildSnapshot(raw, options) {
|
|
|
286
305
|
}
|
|
287
306
|
if (!hasMaterialEvidence(fields))
|
|
288
307
|
return null;
|
|
289
|
-
const capturedAt =
|
|
308
|
+
const capturedAt = caseTimestampOrNull(record.capturedAt, options.sourceVersion) || options.fallback || null;
|
|
290
309
|
if (!capturedAt)
|
|
291
310
|
return null; // an evidence entry with no placeable time is skipped
|
|
292
|
-
let firstCapturedAt =
|
|
311
|
+
let firstCapturedAt = caseTimestampOrNull(record.firstCapturedAt, options.sourceVersion) || capturedAt;
|
|
293
312
|
if (Date.parse(firstCapturedAt) > Date.parse(capturedAt))
|
|
294
313
|
firstCapturedAt = capturedAt;
|
|
295
314
|
const source = typeof record.source === 'string' && EVIDENCE_SOURCE_SET.has(record.source)
|
|
@@ -351,7 +370,7 @@ function compareSnapshotChrono(a, b) {
|
|
|
351
370
|
* snapshots are retained up to the per-case bound; ids are made unique within
|
|
352
371
|
* the case.
|
|
353
372
|
* @param {unknown} rawList
|
|
354
|
-
* @param {{ source?: string, fallback?: string | null }} [options]
|
|
373
|
+
* @param {{ source?: string, fallback?: string | null, sourceVersion?: number | null, caseDomain?: string | null }} [options]
|
|
355
374
|
* @returns {CaseEvidenceSnapshot[]}
|
|
356
375
|
*/
|
|
357
376
|
export function normalizeEvidenceHistory(rawList, options = {}) {
|
|
@@ -461,12 +480,14 @@ function valuesMateriallyEqual(field, previous, current) {
|
|
|
461
480
|
* in the same observation produced an ordinary material change.
|
|
462
481
|
* @param {CaseEvidenceSnapshot | null | undefined} previous
|
|
463
482
|
* @param {CaseEvidenceSnapshot | null | undefined} current
|
|
464
|
-
* @returns {Array<'scan-depth' | 'risk-model'>}
|
|
483
|
+
* @returns {Array<'observation-context' | 'opportunity-model' | 'scan-depth' | 'risk-model'>}
|
|
465
484
|
*/
|
|
466
485
|
export function caseEvidenceIncomparableReasons(previous, current) {
|
|
467
486
|
if (!previous || !current || previous.fingerprint === current.fingerprint)
|
|
468
487
|
return [];
|
|
469
488
|
const reasons = [];
|
|
489
|
+
if (previous.inputHostname !== current.inputHostname)
|
|
490
|
+
reasons.push('observation-context');
|
|
470
491
|
const hasRiskEvidence = previous.riskScore !== null || current.riskScore !== null
|
|
471
492
|
|| previous.riskFactors.length > 0 || current.riskFactors.length > 0;
|
|
472
493
|
if (hasRiskEvidence && !riskModelComparable(previous, current))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
1
2
|
export const MAX_CASE_INVESTIGATION_BRANCHES = 8;
|
|
2
3
|
export const MAX_CASE_BRANCH_NAME_LENGTH = 80;
|
|
3
4
|
export const MAX_CASE_BRANCH_REFERENCES = 12;
|
|
@@ -12,13 +13,10 @@ function text(value, maximum) {
|
|
|
12
13
|
? value.replace(CONTROL_REPLACE_RE, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
13
14
|
: '';
|
|
14
15
|
}
|
|
15
|
-
function iso(value, fallback) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return new Date(parsed).toISOString();
|
|
20
|
-
}
|
|
21
|
-
return fallback;
|
|
16
|
+
function iso(value, fallback, options = {}) {
|
|
17
|
+
return normalizeExplicitIsoTimestamp(value)
|
|
18
|
+
?? (options.legacyTimestamps ? normalizeLegacyIsoTimestamp(value) : null)
|
|
19
|
+
?? fallback;
|
|
22
20
|
}
|
|
23
21
|
function hash(value) {
|
|
24
22
|
let output = 2166136261 >>> 0;
|
|
@@ -48,12 +46,12 @@ function ids(value, valid) {
|
|
|
48
46
|
}
|
|
49
47
|
return [...output];
|
|
50
48
|
}
|
|
51
|
-
function normalizeBranch(value, fallback, references) {
|
|
49
|
+
function normalizeBranch(value, fallback, references, options = {}) {
|
|
52
50
|
const item = record(value);
|
|
53
51
|
const name = text(item.name, MAX_CASE_BRANCH_NAME_LENGTH);
|
|
54
52
|
if (!name)
|
|
55
53
|
return null;
|
|
56
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
54
|
+
const createdAt = iso(item.createdAt, fallback, options);
|
|
57
55
|
const branch = Object.freeze({
|
|
58
56
|
id: safeId(item.id, { name, createdAt }),
|
|
59
57
|
name,
|
|
@@ -63,7 +61,7 @@ function normalizeBranch(value, fallback, references) {
|
|
|
63
61
|
assertionIds: Object.freeze(ids(item.assertionIds, references.assertionIds)),
|
|
64
62
|
actionIds: Object.freeze(ids(item.actionIds, references.actionIds)),
|
|
65
63
|
createdAt,
|
|
66
|
-
updatedAt: iso(item.updatedAt, createdAt),
|
|
64
|
+
updatedAt: iso(item.updatedAt, createdAt, options),
|
|
67
65
|
});
|
|
68
66
|
return branch.evidencePinIds.length || branch.checkpointIds.length || branch.assertionIds.length || branch.actionIds.length
|
|
69
67
|
? branch
|
|
@@ -77,12 +75,12 @@ export function caseInvestigationBranchReferences(input) {
|
|
|
77
75
|
actionIds: new Set(input.actions.map((item) => item.id)),
|
|
78
76
|
});
|
|
79
77
|
}
|
|
80
|
-
export function normalizeCaseInvestigationBranches(value, fallback, references) {
|
|
78
|
+
export function normalizeCaseInvestigationBranches(value, fallback, references, options = {}) {
|
|
81
79
|
if (!Array.isArray(value))
|
|
82
80
|
return [];
|
|
83
81
|
const byId = new Map();
|
|
84
82
|
for (const item of value.slice(0, MAX_CASE_INVESTIGATION_BRANCHES * 2)) {
|
|
85
|
-
const normalized = normalizeBranch(item, fallback, references);
|
|
83
|
+
const normalized = normalizeBranch(item, fallback, references, options);
|
|
86
84
|
if (!normalized)
|
|
87
85
|
continue;
|
|
88
86
|
const current = byId.get(normalized.id);
|