@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
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { CASE_DISPOSITIONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, MAX_CASES, MAX_NOTES_PER_CASE, deterministicId, isoOrNull, normalizeCase, normalizeDomain, normalizeEvidenceHistory, normalizeNotes, normalizeReviewReasonCode, normalizeTags, objectRecord, safeId, } from "./case-record-model.js";
|
|
2
|
-
import { inspectCaseBrandProfileIds, unionCaseBrandProfileIds, } from "./case-brand-profile-references.js";
|
|
3
|
-
import { caseInvestigationBranchReferences, mergeCaseInvestigationBranches, normalizeCaseInvestigationBranches, } from "./case-investigation-branch-model.js";
|
|
4
|
-
import { mergeCaseActions, mergeCaseAssertions, mergeCaseDecisions, mergeCaseEvidencePins, mergeCaseManualTrail, mergeCaseSightings, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseSightings, } from "./case-response-model.js";
|
|
5
|
-
const STATUS_VALUES = new Set(CASE_STATUSES.map((item) => item.value));
|
|
6
|
-
const DISPOSITION_VALUES = new Set(CASE_DISPOSITIONS.map((item) => item.value));
|
|
7
|
-
const SOURCE_VALUES = new Set(CASE_SOURCES.map((item) => item.value));
|
|
8
|
-
export const MAX_CASE_INPUT_RECORDS = 2_000;
|
|
9
|
-
function assignUniqueIds(cases) {
|
|
10
|
-
const used = new Set();
|
|
11
|
-
for (const record of [...cases].sort((a, b) => a.domain.localeCompare(b.domain))) {
|
|
12
|
-
let id = safeId(record.id) || deterministicId(record.domain);
|
|
13
|
-
if (used.has(id)) {
|
|
14
|
-
const base = deterministicId(record.domain);
|
|
15
|
-
id = base;
|
|
16
|
-
let suffix = 2;
|
|
17
|
-
while (used.has(id))
|
|
18
|
-
id = `${base}-${suffix++}`;
|
|
19
|
-
}
|
|
20
|
-
used.add(id);
|
|
21
|
-
record.id = id;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Recovers a clean, bounded store from an arbitrary parsed value. Accepts the
|
|
26
|
-
* versioned envelope or a bare array, drops malformed records, keeps a single
|
|
27
|
-
* case per domain (most recently updated wins), caps to MAX_CASES by recency,
|
|
28
|
-
* and guarantees globally unique safe ids. Never throws.
|
|
29
|
-
* @param {unknown} raw
|
|
30
|
-
* @returns {CaseStore}
|
|
31
|
-
*/
|
|
32
|
-
export function normalizeCaseStore(raw) {
|
|
33
|
-
const now = new Date().toISOString();
|
|
34
|
-
const sourceVersion = parseStoreVersion(raw);
|
|
35
|
-
const acceptsBrandProfileIds = Array.isArray(raw)
|
|
36
|
-
|| sourceVersion === CASE_SCHEMA_VERSION;
|
|
37
|
-
const byDomain = new Map();
|
|
38
|
-
for (const item of boundedCaseList(raw).items) {
|
|
39
|
-
const normalized = normalizeCase(item, undefined, now, Array.isArray(raw) ? CASE_SCHEMA_VERSION : sourceVersion);
|
|
40
|
-
if (!normalized)
|
|
41
|
-
continue;
|
|
42
|
-
if (!acceptsBrandProfileIds)
|
|
43
|
-
normalized.brandProfileIds = [];
|
|
44
|
-
const existing = byDomain.get(normalized.domain);
|
|
45
|
-
if (!existing || Date.parse(normalized.updatedAt) >= Date.parse(existing.updatedAt)) {
|
|
46
|
-
byDomain.set(normalized.domain, normalized);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const cases = [...byDomain.values()]
|
|
50
|
-
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
51
|
-
.slice(0, MAX_CASES);
|
|
52
|
-
assignUniqueIds(cases);
|
|
53
|
-
return { version: CASE_SCHEMA_VERSION, cases };
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* The schema version declared by a stored/parsed value, or null. The storage
|
|
57
|
-
* wrapper uses this to refuse overwriting data written by a newer, unsupported
|
|
58
|
-
* version instead of silently downgrading it.
|
|
59
|
-
* @param {unknown} raw
|
|
60
|
-
* @returns {number | null}
|
|
61
|
-
*/
|
|
62
|
-
export function parseStoreVersion(raw) {
|
|
63
|
-
const value = objectRecord(raw).version;
|
|
64
|
-
return typeof value === 'number' ? value : null;
|
|
65
|
-
}
|
|
66
|
-
/** @param {unknown} raw @returns {unknown[]} */
|
|
67
|
-
function asCaseList(raw) {
|
|
68
|
-
if (Array.isArray(raw))
|
|
69
|
-
return raw;
|
|
70
|
-
const cases = objectRecord(raw).cases;
|
|
71
|
-
if (Array.isArray(cases))
|
|
72
|
-
return cases;
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
function boundedCaseList(raw) {
|
|
76
|
-
const source = asCaseList(raw);
|
|
77
|
-
return {
|
|
78
|
-
items: source.slice(0, MAX_CASE_INPUT_RECORDS),
|
|
79
|
-
omitted: Math.max(0, source.length - MAX_CASE_INPUT_RECORDS),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* @param {{ domain: unknown, status?: unknown, disposition?: unknown, source?: unknown, tags?: unknown, evidence?: unknown, note?: unknown }} input
|
|
84
|
-
* @param {string} [nowIso]
|
|
85
|
-
* @returns {CaseRecord}
|
|
86
|
-
*/
|
|
87
|
-
function importScalar(value, valid) {
|
|
88
|
-
return typeof value === 'string' && valid.has(value) ? value : undefined;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Validates one imported record into a patch. Unlike normalizeCase, absent or
|
|
92
|
-
* invalid scalar fields stay `undefined` (never defaulted) and a missing/invalid
|
|
93
|
-
* updatedAt stays `null` (treated as older than any real local timestamp), so an
|
|
94
|
-
* incomplete import can never win a merge over valid local data. Imported
|
|
95
|
-
* evidence is normalized additively; a snapshot with no captured time falls back
|
|
96
|
-
* only to the imported record's own (older) timestamps, never to "now".
|
|
97
|
-
* @param {unknown} raw
|
|
98
|
-
* @returns {ImportPatch | null}
|
|
99
|
-
*/
|
|
100
|
-
function extractImportPatch(raw, importedVersion) {
|
|
101
|
-
const record = objectRecord(raw);
|
|
102
|
-
const domain = normalizeDomain(record.domain);
|
|
103
|
-
if (!domain)
|
|
104
|
-
return null;
|
|
105
|
-
const importFallback = isoOrNull(record.updatedAt) || isoOrNull(record.createdAt) || null;
|
|
106
|
-
const normalizedFallback = importFallback || '1970-01-01T00:00:00.000Z';
|
|
107
|
-
const rawEvidence = Array.isArray(record.evidenceHistory) ? record.evidenceHistory : [];
|
|
108
|
-
const evidencePins = normalizeCaseEvidencePins(record.evidencePins, normalizedFallback, {
|
|
109
|
-
allowCertificateObservation: importedVersion >= 11,
|
|
110
|
-
});
|
|
111
|
-
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
112
|
-
const actions = normalizeCaseActions(record.actions, normalizedFallback);
|
|
113
|
-
const assertions = normalizeCaseAssertions(record.assertions, normalizedFallback, pinIds);
|
|
114
|
-
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
115
|
-
const brandProfileReferences = importedVersion >= 12
|
|
116
|
-
? inspectCaseBrandProfileIds(record.brandProfileIds)
|
|
117
|
-
: { ids: [], omitted: 0 };
|
|
118
|
-
return {
|
|
119
|
-
domain,
|
|
120
|
-
rawId: typeof record.id === 'string' ? record.id : null,
|
|
121
|
-
status: importScalar(record.status, STATUS_VALUES),
|
|
122
|
-
disposition: importScalar(record.disposition, DISPOSITION_VALUES),
|
|
123
|
-
reviewReasonCode: Object.hasOwn(record, 'reviewReasonCode')
|
|
124
|
-
? normalizeReviewReasonCode(record.reviewReasonCode)
|
|
125
|
-
: undefined,
|
|
126
|
-
brandProfileIds: brandProfileReferences.ids,
|
|
127
|
-
brandProfileReferencesOmitted: brandProfileReferences.omitted,
|
|
128
|
-
source: importScalar(record.source, SOURCE_VALUES),
|
|
129
|
-
evidenceHistory: normalizeEvidenceHistory(rawEvidence, {
|
|
130
|
-
source: 'import',
|
|
131
|
-
fallback: importFallback,
|
|
132
|
-
sourceVersion: importedVersion,
|
|
133
|
-
}),
|
|
134
|
-
evidencePins,
|
|
135
|
-
decisions: normalizeCaseDecisions(record.decisions, normalizedFallback, pinIds),
|
|
136
|
-
actions,
|
|
137
|
-
assertions,
|
|
138
|
-
manualTrail: normalizeCaseManualTrail(record.manualTrail, normalizedFallback),
|
|
139
|
-
sightings: normalizeCaseSightings(record.sightings, normalizedFallback, pinIds),
|
|
140
|
-
branches: importedVersion >= 11
|
|
141
|
-
? normalizeCaseInvestigationBranches(record.branches, normalizedFallback, branchReferences)
|
|
142
|
-
: [],
|
|
143
|
-
tags: normalizeTags(record.tags),
|
|
144
|
-
// Imported notes fall back only to the imported record's own timestamps
|
|
145
|
-
// (never "now"), so a timestamp-less note gets a stable, deterministic time
|
|
146
|
-
// and id, and re-importing the same file cannot manufacture a duplicate or a
|
|
147
|
-
// spuriously-newer note.
|
|
148
|
-
notes: normalizeNotes(record.notes, importFallback),
|
|
149
|
-
createdAt: isoOrNull(record.createdAt),
|
|
150
|
-
updatedAt: isoOrNull(record.updatedAt),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
/** @param {CaseNote[]} a @param {CaseNote[]} b @returns {CaseNote[]} */
|
|
154
|
-
function unionNotes(a, b) {
|
|
155
|
-
const byId = new Map();
|
|
156
|
-
for (const note of [...a, ...b]) {
|
|
157
|
-
if (!byId.has(note.id))
|
|
158
|
-
byId.set(note.id, note);
|
|
159
|
-
}
|
|
160
|
-
const notes = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
161
|
-
return notes.slice(Math.max(0, notes.length - MAX_NOTES_PER_CASE));
|
|
162
|
-
}
|
|
163
|
-
// Additive, deduplicated union of two evidence histories. Identical material
|
|
164
|
-
// collapses (earliest firstCapturedAt, latest capturedAt), distinct snapshots
|
|
165
|
-
// are retained subject to the per-case bound, and an older import can never
|
|
166
|
-
// move an existing observation backwards.
|
|
167
|
-
function mergeEvidenceHistories(local, imported) {
|
|
168
|
-
return normalizeEvidenceHistory([...local, ...imported], { source: 'import', fallback: null });
|
|
169
|
-
}
|
|
170
|
-
/** @param {ImportPatch} patch @param {string} now @returns {CaseRecord} */
|
|
171
|
-
function caseFromPatch(patch, now) {
|
|
172
|
-
return {
|
|
173
|
-
id: '', // assigned by mergeCases so it can guarantee uniqueness against locals
|
|
174
|
-
domain: patch.domain,
|
|
175
|
-
status: patch.status ?? DEFAULT_STATUS,
|
|
176
|
-
disposition: patch.disposition ?? DEFAULT_DISPOSITION,
|
|
177
|
-
reviewReasonCode: patch.reviewReasonCode ?? null,
|
|
178
|
-
brandProfileIds: patch.brandProfileIds,
|
|
179
|
-
tags: patch.tags,
|
|
180
|
-
notes: patch.notes,
|
|
181
|
-
source: patch.source ?? DEFAULT_SOURCE,
|
|
182
|
-
evidenceHistory: patch.evidenceHistory,
|
|
183
|
-
evidencePins: patch.evidencePins,
|
|
184
|
-
decisions: patch.decisions,
|
|
185
|
-
actions: patch.actions,
|
|
186
|
-
assertions: patch.assertions,
|
|
187
|
-
manualTrail: patch.manualTrail,
|
|
188
|
-
sightings: patch.sightings,
|
|
189
|
-
branches: patch.branches,
|
|
190
|
-
createdAt: patch.createdAt || patch.updatedAt || now,
|
|
191
|
-
updatedAt: patch.updatedAt || patch.createdAt || now,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Merges an imported patch into an existing local case. Notes, tags, and
|
|
196
|
-
* evidence history are merged additively (never destructive); a scalar field is
|
|
197
|
-
* only overwritten when the import provided a valid value AND is strictly newer
|
|
198
|
-
* than the local record. A patch with no/invalid updatedAt is never newer.
|
|
199
|
-
* @param {CaseRecord} local
|
|
200
|
-
* @param {ImportPatch} patch
|
|
201
|
-
* @returns {CaseRecord}
|
|
202
|
-
*/
|
|
203
|
-
function applyImportPatch(local, patch) {
|
|
204
|
-
const importNewer = patch.updatedAt !== null && Date.parse(patch.updatedAt) > Date.parse(local.updatedAt);
|
|
205
|
-
const fallback = patch.updatedAt || local.updatedAt;
|
|
206
|
-
const evidencePins = mergeCaseEvidencePins(local.evidencePins, patch.evidencePins, fallback);
|
|
207
|
-
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
208
|
-
const actions = mergeCaseActions(local.actions, patch.actions, fallback);
|
|
209
|
-
const assertions = mergeCaseAssertions(local.assertions, patch.assertions, fallback, pinIds);
|
|
210
|
-
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
211
|
-
const brandProfileReferences = unionCaseBrandProfileIds(local.brandProfileIds, patch.brandProfileIds);
|
|
212
|
-
return { record: {
|
|
213
|
-
...local,
|
|
214
|
-
status: patch.status !== undefined && importNewer ? patch.status : local.status,
|
|
215
|
-
disposition: patch.disposition !== undefined && importNewer ? patch.disposition : local.disposition,
|
|
216
|
-
reviewReasonCode: patch.reviewReasonCode !== undefined && importNewer ? patch.reviewReasonCode : local.reviewReasonCode ?? null,
|
|
217
|
-
brandProfileIds: brandProfileReferences.ids,
|
|
218
|
-
source: patch.source !== undefined && importNewer ? patch.source : local.source,
|
|
219
|
-
evidenceHistory: mergeEvidenceHistories(local.evidenceHistory, patch.evidenceHistory),
|
|
220
|
-
evidencePins,
|
|
221
|
-
decisions: mergeCaseDecisions(local.decisions, patch.decisions, fallback, pinIds),
|
|
222
|
-
actions,
|
|
223
|
-
assertions,
|
|
224
|
-
manualTrail: mergeCaseManualTrail(local.manualTrail, patch.manualTrail, fallback),
|
|
225
|
-
sightings: mergeCaseSightings(local.sightings, patch.sightings, fallback, pinIds),
|
|
226
|
-
branches: mergeCaseInvestigationBranches(local.branches ?? [], patch.branches, fallback, branchReferences),
|
|
227
|
-
tags: normalizeTags([...local.tags, ...patch.tags]),
|
|
228
|
-
notes: unionNotes(local.notes, patch.notes),
|
|
229
|
-
createdAt: patch.createdAt && Date.parse(patch.createdAt) < Date.parse(local.createdAt) ? patch.createdAt : local.createdAt,
|
|
230
|
-
updatedAt: importNewer ? (patch.updatedAt ?? local.updatedAt) : local.updatedAt,
|
|
231
|
-
}, brandProfileReferencesOmitted: brandProfileReferences.omitted };
|
|
232
|
-
}
|
|
233
|
-
function pickFreeId(preferred, domain, used) {
|
|
234
|
-
const wanted = safeId(preferred);
|
|
235
|
-
let id = wanted && !used.has(wanted) ? wanted : deterministicId(domain);
|
|
236
|
-
const base = deterministicId(domain);
|
|
237
|
-
let suffix = 2;
|
|
238
|
-
while (used.has(id))
|
|
239
|
-
id = `${base}-${suffix++}`;
|
|
240
|
-
return id;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Merges an imported (already parsed) value into the local cases. Predictable
|
|
244
|
-
* and idempotent: unknown records are skipped, existing domains merge without
|
|
245
|
-
* losing newer local decisions, and new ones are added until the store bound is
|
|
246
|
-
* reached. An imported envelope that declares a schema version newer than we
|
|
247
|
-
* support is rejected up front (before any local data is touched) rather than
|
|
248
|
-
* reinterpreted.
|
|
249
|
-
* @param {CaseRecord[]} localCases
|
|
250
|
-
* @param {unknown} importedRaw
|
|
251
|
-
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number }}
|
|
252
|
-
*/
|
|
253
|
-
export function mergeCases(localCases, importedRaw) {
|
|
254
|
-
const importedVersion = parseStoreVersion(importedRaw);
|
|
255
|
-
if (importedVersion !== null && Number.isInteger(importedVersion) && importedVersion > CASE_SCHEMA_VERSION) {
|
|
256
|
-
throw new Error(`This case file was exported by a newer version of WHOISleuth (schema ${importedVersion}). Update the app before importing it.`);
|
|
257
|
-
}
|
|
258
|
-
if (!CASE_IMPORT_VERSIONS.includes(importedVersion)
|
|
259
|
-
|| !importedRaw || typeof importedRaw !== 'object'
|
|
260
|
-
|| !Array.isArray(objectRecord(importedRaw).cases)) {
|
|
261
|
-
throw new Error(`Expected a WHOISleuth case export using schema ${CASE_IMPORT_VERSIONS.join(' or ')}.`);
|
|
262
|
-
}
|
|
263
|
-
const local = normalizeCaseStore(localCases).cases;
|
|
264
|
-
const supportedImportedVersion = importedVersion ?? 0;
|
|
265
|
-
const byDomain = new Map(local.map((item) => [item.domain, item]));
|
|
266
|
-
const usedIds = new Set(local.map((item) => item.id));
|
|
267
|
-
let added = 0;
|
|
268
|
-
let updated = 0;
|
|
269
|
-
const imported = boundedCaseList(importedRaw);
|
|
270
|
-
let skipped = imported.omitted;
|
|
271
|
-
let brandProfileReferencesOmitted = 0;
|
|
272
|
-
const now = new Date().toISOString();
|
|
273
|
-
for (const item of imported.items) {
|
|
274
|
-
const patch = extractImportPatch(item, supportedImportedVersion);
|
|
275
|
-
if (!patch) {
|
|
276
|
-
skipped += 1;
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
const existing = byDomain.get(patch.domain);
|
|
280
|
-
if (existing) {
|
|
281
|
-
const merged = applyImportPatch(existing, patch);
|
|
282
|
-
byDomain.set(patch.domain, merged.record);
|
|
283
|
-
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted + merged.brandProfileReferencesOmitted;
|
|
284
|
-
updated += 1;
|
|
285
|
-
}
|
|
286
|
-
else if (byDomain.size < MAX_CASES) {
|
|
287
|
-
const record = caseFromPatch(patch, now);
|
|
288
|
-
record.id = pickFreeId(patch.rawId, patch.domain, usedIds);
|
|
289
|
-
usedIds.add(record.id);
|
|
290
|
-
byDomain.set(patch.domain, record);
|
|
291
|
-
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted;
|
|
292
|
-
added += 1;
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
skipped += 1;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
cases: normalizeCaseStore([...byDomain.values()]).cases,
|
|
300
|
-
added,
|
|
301
|
-
updated,
|
|
302
|
-
skipped,
|
|
303
|
-
brandProfileReferencesOmitted,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Stable case-record facade. Core validation, evidence history/comparison,
|
|
2
|
-
// and analyst-record operations remain framework-neutral and independently
|
|
3
|
-
// testable while existing consumers keep one compatibility import.
|
|
4
|
-
export * from "./case-record-contracts.js";
|
|
5
|
-
export * from "./case-record-core.js";
|
|
6
|
-
export * from "./case-brand-profile-references.js";
|
|
7
|
-
export * from "./case-evidence-model.js";
|
|
8
|
-
export * from "./case-record-operations.js";
|
|
9
|
-
export * from "./case-investigation-branch-model.js";
|