@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,728 +0,0 @@
|
|
|
1
|
-
// Bounded, framework-neutral analyst response records. These records are
|
|
2
|
-
// deliberately separate from collected evidence snapshots: a pin describes a
|
|
3
|
-
// fact selected by an analyst, a decision records analyst reasoning, and an
|
|
4
|
-
// action records a reviewed external or internal follow-up.
|
|
5
|
-
export const MAX_CASE_EVIDENCE_PINS = 40;
|
|
6
|
-
export const MAX_CASE_CHECKPOINT_FACTS = 20;
|
|
7
|
-
export const MAX_CASE_DECISIONS = 30;
|
|
8
|
-
export const MAX_CASE_ACTIONS = 50;
|
|
9
|
-
export const MAX_CASE_ASSERTIONS = 50;
|
|
10
|
-
export const MAX_CASE_MANUAL_TRAIL_EVENTS = 80;
|
|
11
|
-
export const MAX_CASE_SIGHTINGS = 80;
|
|
12
|
-
export const MAX_RESPONSE_LABEL_LENGTH = 80;
|
|
13
|
-
export const MAX_RESPONSE_VALUE_LENGTH = 1000;
|
|
14
|
-
export const MAX_RESPONSE_RATIONALE_LENGTH = 2000;
|
|
15
|
-
export const MAX_RESPONSE_RECIPIENT_LENGTH = 320;
|
|
16
|
-
export const MAX_RESPONSE_REFERENCE_LENGTH = 500;
|
|
17
|
-
export const MAX_RESPONSE_LIMITATIONS = 8;
|
|
18
|
-
export const MAX_RESPONSE_LIMITATION_LENGTH = 240;
|
|
19
|
-
export const MAX_DECISION_PIN_REFERENCES = 20;
|
|
20
|
-
export const MAX_TRAIL_TARGET_LENGTH = 500;
|
|
21
|
-
export const MAX_ASSERTION_PROVENANCE_LABELS = 20;
|
|
22
|
-
export const MAX_ASSERTION_PROVENANCE_MARKINGS = 12;
|
|
23
|
-
export const CASE_EVIDENCE_RELATION_STANCES = ['supports', 'contradicts', 'unresolved'];
|
|
24
|
-
export const CASE_PIN_COMPLETENESS = ['complete', 'partial', 'inconclusive', 'unknown'];
|
|
25
|
-
export const CASE_TRANSITION_EXPECTATIONS = ['preserve', 'change', 'review'];
|
|
26
|
-
export const CASE_ACTION_TYPES = [
|
|
27
|
-
'registrar_report',
|
|
28
|
-
'registry_report',
|
|
29
|
-
'network_hosting_report',
|
|
30
|
-
'security_contact_report',
|
|
31
|
-
'defensive_control',
|
|
32
|
-
'internal_review',
|
|
33
|
-
];
|
|
34
|
-
export const CASE_ACTION_STATES = [
|
|
35
|
-
'planned',
|
|
36
|
-
'ready_for_review',
|
|
37
|
-
'submitted',
|
|
38
|
-
'acknowledged',
|
|
39
|
-
'resolved',
|
|
40
|
-
'closed',
|
|
41
|
-
];
|
|
42
|
-
export const CASE_ASSERTION_KINDS = [
|
|
43
|
-
'verified_fact',
|
|
44
|
-
'hypothesis',
|
|
45
|
-
'unknown',
|
|
46
|
-
'contradiction',
|
|
47
|
-
'next_step',
|
|
48
|
-
];
|
|
49
|
-
export const CASE_ASSERTION_STATES = ['open', 'resolved'];
|
|
50
|
-
export const CASE_ASSERTION_EXTERNAL_FORMATS = ['stix', 'misp'];
|
|
51
|
-
export const CASE_ASSERTION_EXTERNAL_ENTITY_TYPES = [
|
|
52
|
-
'asn',
|
|
53
|
-
'certificate',
|
|
54
|
-
'domain',
|
|
55
|
-
'hostname',
|
|
56
|
-
'ipv4',
|
|
57
|
-
'ipv6',
|
|
58
|
-
'url',
|
|
59
|
-
];
|
|
60
|
-
export const CASE_MANUAL_TRAIL_KINDS = ['pivot', 'review', 'handoff'];
|
|
61
|
-
export const CASE_SIGHTING_STATES = [
|
|
62
|
-
'observed_by_deployment',
|
|
63
|
-
'reported_by_provider',
|
|
64
|
-
'analyst_confirmed',
|
|
65
|
-
'not_reproduced',
|
|
66
|
-
'expired',
|
|
67
|
-
];
|
|
68
|
-
export const CASE_SIGHTING_CATEGORIES = [
|
|
69
|
-
'registration',
|
|
70
|
-
'delegation',
|
|
71
|
-
'certificate',
|
|
72
|
-
'mail',
|
|
73
|
-
'website',
|
|
74
|
-
'infrastructure',
|
|
75
|
-
'other',
|
|
76
|
-
];
|
|
77
|
-
const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,64}$/u;
|
|
78
|
-
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
79
|
-
const CONTROL_REPLACE_RE = /[\u0000-\u001f\u007f]+/gu;
|
|
80
|
-
const COMPLETENESS = new Set(CASE_PIN_COMPLETENESS);
|
|
81
|
-
const TRANSITION_EXPECTATIONS = new Set(CASE_TRANSITION_EXPECTATIONS);
|
|
82
|
-
const ACTION_TYPES = new Set(CASE_ACTION_TYPES);
|
|
83
|
-
const ACTION_STATES = new Set(CASE_ACTION_STATES);
|
|
84
|
-
const ASSERTION_KINDS = new Set(CASE_ASSERTION_KINDS);
|
|
85
|
-
const ASSERTION_STATES = new Set(CASE_ASSERTION_STATES);
|
|
86
|
-
const EVIDENCE_RELATION_STANCES = new Set(CASE_EVIDENCE_RELATION_STANCES);
|
|
87
|
-
const ASSERTION_EXTERNAL_FORMATS = new Set(CASE_ASSERTION_EXTERNAL_FORMATS);
|
|
88
|
-
const ASSERTION_EXTERNAL_ENTITY_TYPES = new Set(CASE_ASSERTION_EXTERNAL_ENTITY_TYPES);
|
|
89
|
-
const TRAIL_KINDS = new Set(CASE_MANUAL_TRAIL_KINDS);
|
|
90
|
-
const SIGHTING_STATES = new Set(CASE_SIGHTING_STATES);
|
|
91
|
-
const SIGHTING_CATEGORIES = new Set(CASE_SIGHTING_CATEGORIES);
|
|
92
|
-
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
93
|
-
function record(value) {
|
|
94
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
95
|
-
? value
|
|
96
|
-
: {};
|
|
97
|
-
}
|
|
98
|
-
function text(value, maximum) {
|
|
99
|
-
if (typeof value !== 'string')
|
|
100
|
-
return '';
|
|
101
|
-
return value.replace(CONTROL_REPLACE_RE, ' ').trim().slice(0, maximum);
|
|
102
|
-
}
|
|
103
|
-
function iso(value, fallback) {
|
|
104
|
-
if (typeof value === 'string' && value.length <= 64 && !CONTROL_RE.test(value)) {
|
|
105
|
-
const parsed = Date.parse(value);
|
|
106
|
-
if (Number.isFinite(parsed))
|
|
107
|
-
return new Date(parsed).toISOString();
|
|
108
|
-
}
|
|
109
|
-
return fallback;
|
|
110
|
-
}
|
|
111
|
-
function optionalIso(value) {
|
|
112
|
-
if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
|
|
113
|
-
return null;
|
|
114
|
-
const parsed = Date.parse(value);
|
|
115
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
116
|
-
}
|
|
117
|
-
function hash(value) {
|
|
118
|
-
let result = 2166136261 >>> 0;
|
|
119
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
120
|
-
result ^= value.charCodeAt(index);
|
|
121
|
-
result = Math.imul(result, 16777619);
|
|
122
|
-
}
|
|
123
|
-
return (result >>> 0).toString(36);
|
|
124
|
-
}
|
|
125
|
-
function deterministicId(prefix, value) {
|
|
126
|
-
return `${prefix}-${hash(JSON.stringify(value))}`;
|
|
127
|
-
}
|
|
128
|
-
function freshId(prefix) {
|
|
129
|
-
if (typeof globalThis.crypto?.randomUUID === 'function')
|
|
130
|
-
return globalThis.crypto.randomUUID();
|
|
131
|
-
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
132
|
-
}
|
|
133
|
-
function safeId(value, prefix, raw) {
|
|
134
|
-
return typeof value === 'string' && SAFE_ID_RE.test(value)
|
|
135
|
-
? value
|
|
136
|
-
: deterministicId(prefix, raw);
|
|
137
|
-
}
|
|
138
|
-
function limitations(value) {
|
|
139
|
-
if (!Array.isArray(value))
|
|
140
|
-
return [];
|
|
141
|
-
const unique = new Set();
|
|
142
|
-
for (const item of value.slice(0, MAX_RESPONSE_LIMITATIONS * 2)) {
|
|
143
|
-
const normalized = text(item, MAX_RESPONSE_LIMITATION_LENGTH);
|
|
144
|
-
if (normalized)
|
|
145
|
-
unique.add(normalized);
|
|
146
|
-
if (unique.size >= MAX_RESPONSE_LIMITATIONS)
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
return [...unique];
|
|
150
|
-
}
|
|
151
|
-
function sourceSchema(value) {
|
|
152
|
-
const item = record(value);
|
|
153
|
-
const collection = text(item.collection, 80);
|
|
154
|
-
const schema = text(item.schema, 120);
|
|
155
|
-
const version = typeof item.version === 'number'
|
|
156
|
-
&& Number.isSafeInteger(item.version)
|
|
157
|
-
&& item.version > 0
|
|
158
|
-
&& item.version <= 10_000
|
|
159
|
-
? item.version
|
|
160
|
-
: null;
|
|
161
|
-
return collection && schema && version !== null ? { collection, schema, version } : null;
|
|
162
|
-
}
|
|
163
|
-
function certificateObservation(value, pin) {
|
|
164
|
-
const item = record(value);
|
|
165
|
-
if (!Object.keys(item).length)
|
|
166
|
-
return null;
|
|
167
|
-
const keys = new Set([
|
|
168
|
-
'eventId',
|
|
169
|
-
'logId',
|
|
170
|
-
'certificateSha256',
|
|
171
|
-
'issuer',
|
|
172
|
-
'notAfter',
|
|
173
|
-
'dnsNameCount',
|
|
174
|
-
'namesComplete',
|
|
175
|
-
]);
|
|
176
|
-
if (Object.keys(item).some((key) => !keys.has(key)))
|
|
177
|
-
return null;
|
|
178
|
-
if (pin.sourceSchema?.collection !== 'external_observations'
|
|
179
|
-
|| pin.sourceSchema.schema !== 'whoisleuth.certificate-observation-rows'
|
|
180
|
-
|| (pin.field !== 'certificateSha256' && pin.field !== 'fingerprintSha256'))
|
|
181
|
-
return null;
|
|
182
|
-
const eventId = text(item.eventId, 64);
|
|
183
|
-
const logId = text(item.logId, 200);
|
|
184
|
-
const certificateSha256 = text(item.certificateSha256, 64).toLowerCase();
|
|
185
|
-
const issuer = item.issuer === null ? null : text(item.issuer, 160) || null;
|
|
186
|
-
const notAfter = item.notAfter === null ? null : optionalIso(item.notAfter);
|
|
187
|
-
const dnsNameCount = typeof item.dnsNameCount === 'number'
|
|
188
|
-
&& Number.isSafeInteger(item.dnsNameCount)
|
|
189
|
-
&& item.dnsNameCount >= 1
|
|
190
|
-
&& item.dnsNameCount <= 100
|
|
191
|
-
? item.dnsNameCount
|
|
192
|
-
: null;
|
|
193
|
-
if (!SAFE_ID_RE.test(eventId)
|
|
194
|
-
|| !logId
|
|
195
|
-
|| !SHA256_RE.test(certificateSha256)
|
|
196
|
-
|| certificateSha256 !== pin.value.toLowerCase()
|
|
197
|
-
|| dnsNameCount === null
|
|
198
|
-
|| typeof item.namesComplete !== 'boolean'
|
|
199
|
-
|| (item.notAfter !== null && notAfter === null))
|
|
200
|
-
return null;
|
|
201
|
-
return {
|
|
202
|
-
eventId,
|
|
203
|
-
logId,
|
|
204
|
-
certificateSha256,
|
|
205
|
-
issuer,
|
|
206
|
-
notAfter,
|
|
207
|
-
dnsNameCount,
|
|
208
|
-
namesComplete: item.namesComplete,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function uniqueIds(value, validIds) {
|
|
212
|
-
if (!Array.isArray(value))
|
|
213
|
-
return [];
|
|
214
|
-
const unique = new Set();
|
|
215
|
-
for (const item of value.slice(0, MAX_DECISION_PIN_REFERENCES * 2)) {
|
|
216
|
-
if (typeof item !== 'string' || !SAFE_ID_RE.test(item) || (validIds && !validIds.has(item)))
|
|
217
|
-
continue;
|
|
218
|
-
unique.add(item);
|
|
219
|
-
if (unique.size >= MAX_DECISION_PIN_REFERENCES)
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
return [...unique];
|
|
223
|
-
}
|
|
224
|
-
function normalizeEvidenceRelations(value, legacyIds, validIds) {
|
|
225
|
-
const output = new Map();
|
|
226
|
-
if (Array.isArray(value)) {
|
|
227
|
-
for (const raw of value.slice(0, MAX_DECISION_PIN_REFERENCES * 2)) {
|
|
228
|
-
const item = record(raw);
|
|
229
|
-
const evidencePinId = typeof item.evidencePinId === 'string' && SAFE_ID_RE.test(item.evidencePinId)
|
|
230
|
-
? item.evidencePinId
|
|
231
|
-
: '';
|
|
232
|
-
if (!evidencePinId || (validIds && !validIds.has(evidencePinId)))
|
|
233
|
-
continue;
|
|
234
|
-
if (typeof item.stance !== 'string' || !EVIDENCE_RELATION_STANCES.has(item.stance))
|
|
235
|
-
continue;
|
|
236
|
-
if (!output.has(evidencePinId))
|
|
237
|
-
output.set(evidencePinId, item.stance);
|
|
238
|
-
if (output.size >= MAX_DECISION_PIN_REFERENCES)
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (!output.size) {
|
|
243
|
-
for (const evidencePinId of legacyIds)
|
|
244
|
-
output.set(evidencePinId, 'supports');
|
|
245
|
-
}
|
|
246
|
-
return [...output].map(([evidencePinId, stance]) => ({ evidencePinId, stance }));
|
|
247
|
-
}
|
|
248
|
-
function normalizePin(raw, fallback, options = {}) {
|
|
249
|
-
const item = record(raw);
|
|
250
|
-
const label = text(item.label, MAX_RESPONSE_LABEL_LENGTH);
|
|
251
|
-
const value = text(item.value, MAX_RESPONSE_VALUE_LENGTH);
|
|
252
|
-
if (!label || !value)
|
|
253
|
-
return null;
|
|
254
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
255
|
-
const normalizedSourceSchema = sourceSchema(item.sourceSchema);
|
|
256
|
-
const normalizedField = text(item.field, 120) || null;
|
|
257
|
-
const normalized = {
|
|
258
|
-
id: safeId(item.id, 'pin', { label, value, createdAt }),
|
|
259
|
-
checkpointId: typeof item.checkpointId === 'string' && SAFE_ID_RE.test(item.checkpointId)
|
|
260
|
-
? item.checkpointId
|
|
261
|
-
: null,
|
|
262
|
-
field: normalizedField,
|
|
263
|
-
category: text(item.category, 80) || null,
|
|
264
|
-
label,
|
|
265
|
-
value,
|
|
266
|
-
source: text(item.source, MAX_RESPONSE_LABEL_LENGTH) || 'analyst_selected',
|
|
267
|
-
sourceState: text(item.sourceState, 40) || null,
|
|
268
|
-
sourceSchema: normalizedSourceSchema,
|
|
269
|
-
observedAt: iso(item.observedAt, createdAt),
|
|
270
|
-
collectionDepth: item.collectionDepth === 'deep' || item.collectionDepth === 'fast'
|
|
271
|
-
? item.collectionDepth
|
|
272
|
-
: 'unknown',
|
|
273
|
-
completeness: typeof item.completeness === 'string' && COMPLETENESS.has(item.completeness)
|
|
274
|
-
? item.completeness
|
|
275
|
-
: 'unknown',
|
|
276
|
-
truncated: typeof item.truncated === 'boolean' ? item.truncated : null,
|
|
277
|
-
transitionExpectation: typeof item.transitionExpectation === 'string'
|
|
278
|
-
&& TRANSITION_EXPECTATIONS.has(item.transitionExpectation)
|
|
279
|
-
? item.transitionExpectation
|
|
280
|
-
: null,
|
|
281
|
-
limitations: limitations(item.limitations),
|
|
282
|
-
createdAt,
|
|
283
|
-
};
|
|
284
|
-
normalized.certificateObservation = options.allowCertificateObservation === false
|
|
285
|
-
? null
|
|
286
|
-
: certificateObservation(item.certificateObservation, {
|
|
287
|
-
field: normalizedField,
|
|
288
|
-
value,
|
|
289
|
-
sourceSchema: normalizedSourceSchema,
|
|
290
|
-
});
|
|
291
|
-
return normalized;
|
|
292
|
-
}
|
|
293
|
-
export function normalizeCaseEvidencePins(raw, fallback, options = {}) {
|
|
294
|
-
if (!Array.isArray(raw))
|
|
295
|
-
return [];
|
|
296
|
-
const byId = new Map();
|
|
297
|
-
for (const item of raw.slice(0, MAX_CASE_EVIDENCE_PINS * 2)) {
|
|
298
|
-
const normalized = normalizePin(item, fallback, options);
|
|
299
|
-
if (normalized && !byId.has(normalized.id))
|
|
300
|
-
byId.set(normalized.id, normalized);
|
|
301
|
-
}
|
|
302
|
-
return [...byId.values()]
|
|
303
|
-
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
|
|
304
|
-
.slice(-MAX_CASE_EVIDENCE_PINS);
|
|
305
|
-
}
|
|
306
|
-
export function appendCaseEvidencePin(current, raw, now) {
|
|
307
|
-
const item = record(raw);
|
|
308
|
-
const created = normalizePin({ ...item, id: freshId('pin'), createdAt: now }, now);
|
|
309
|
-
if (!created)
|
|
310
|
-
throw new Error('An evidence pin requires a label and value.');
|
|
311
|
-
return normalizeCaseEvidencePins([...current, created], now);
|
|
312
|
-
}
|
|
313
|
-
export function appendCaseEvidencePins(current, raw, now) {
|
|
314
|
-
if (!Array.isArray(raw) || !raw.length)
|
|
315
|
-
throw new Error('An evidence checkpoint requires at least one selected fact.');
|
|
316
|
-
let output = [...current];
|
|
317
|
-
let added = 0;
|
|
318
|
-
for (const item of raw.slice(0, MAX_CASE_CHECKPOINT_FACTS)) {
|
|
319
|
-
output = appendCaseEvidencePin(output, item, now);
|
|
320
|
-
added += 1;
|
|
321
|
-
}
|
|
322
|
-
if (!added)
|
|
323
|
-
throw new Error('An evidence checkpoint requires at least one valid selected fact.');
|
|
324
|
-
return output;
|
|
325
|
-
}
|
|
326
|
-
function normalizeDecision(raw, fallback, validPinIds) {
|
|
327
|
-
const item = record(raw);
|
|
328
|
-
const summary = text(item.summary, MAX_RESPONSE_LABEL_LENGTH);
|
|
329
|
-
const rationale = text(item.rationale, MAX_RESPONSE_RATIONALE_LENGTH);
|
|
330
|
-
if (!summary || !rationale)
|
|
331
|
-
return null;
|
|
332
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
333
|
-
return {
|
|
334
|
-
id: safeId(item.id, 'decision', { summary, rationale, createdAt }),
|
|
335
|
-
summary,
|
|
336
|
-
rationale,
|
|
337
|
-
evidencePinIds: uniqueIds(item.evidencePinIds, validPinIds),
|
|
338
|
-
createdAt,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
export function normalizeCaseDecisions(raw, fallback, validPinIds) {
|
|
342
|
-
if (!Array.isArray(raw))
|
|
343
|
-
return [];
|
|
344
|
-
const byId = new Map();
|
|
345
|
-
for (const item of raw.slice(0, MAX_CASE_DECISIONS * 2)) {
|
|
346
|
-
const normalized = normalizeDecision(item, fallback, validPinIds);
|
|
347
|
-
if (normalized && !byId.has(normalized.id))
|
|
348
|
-
byId.set(normalized.id, normalized);
|
|
349
|
-
}
|
|
350
|
-
return [...byId.values()]
|
|
351
|
-
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
|
|
352
|
-
.slice(-MAX_CASE_DECISIONS);
|
|
353
|
-
}
|
|
354
|
-
export function appendCaseDecision(current, raw, now, validPinIds) {
|
|
355
|
-
const item = record(raw);
|
|
356
|
-
const created = normalizeDecision({ ...item, id: freshId('decision'), createdAt: now }, now, validPinIds);
|
|
357
|
-
if (!created)
|
|
358
|
-
throw new Error('A decision requires a summary and rationale.');
|
|
359
|
-
return normalizeCaseDecisions([...current, created], now, validPinIds);
|
|
360
|
-
}
|
|
361
|
-
function normalizeAction(raw, fallback) {
|
|
362
|
-
const item = record(raw);
|
|
363
|
-
const recipient = text(item.recipient, MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
364
|
-
if (!recipient)
|
|
365
|
-
return null;
|
|
366
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
367
|
-
const updatedAt = iso(item.updatedAt, createdAt);
|
|
368
|
-
return {
|
|
369
|
-
id: safeId(item.id, 'action', { recipient, createdAt }),
|
|
370
|
-
type: typeof item.type === 'string' && ACTION_TYPES.has(item.type)
|
|
371
|
-
? item.type
|
|
372
|
-
: 'internal_review',
|
|
373
|
-
recipient,
|
|
374
|
-
contactSource: text(item.contactSource, MAX_RESPONSE_LABEL_LENGTH) || 'analyst_supplied',
|
|
375
|
-
contactLimitations: limitations(item.contactLimitations),
|
|
376
|
-
dueAt: optionalIso(item.dueAt),
|
|
377
|
-
state: typeof item.state === 'string' && ACTION_STATES.has(item.state)
|
|
378
|
-
? item.state
|
|
379
|
-
: 'planned',
|
|
380
|
-
reference: text(item.reference, MAX_RESPONSE_REFERENCE_LENGTH) || null,
|
|
381
|
-
followUpAt: optionalIso(item.followUpAt),
|
|
382
|
-
outcome: text(item.outcome, MAX_RESPONSE_RATIONALE_LENGTH) || null,
|
|
383
|
-
createdAt,
|
|
384
|
-
updatedAt,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
export function normalizeCaseActions(raw, fallback) {
|
|
388
|
-
if (!Array.isArray(raw))
|
|
389
|
-
return [];
|
|
390
|
-
const byId = new Map();
|
|
391
|
-
for (const item of raw.slice(0, MAX_CASE_ACTIONS * 2)) {
|
|
392
|
-
const normalized = normalizeAction(item, fallback);
|
|
393
|
-
if (!normalized)
|
|
394
|
-
continue;
|
|
395
|
-
const existing = byId.get(normalized.id);
|
|
396
|
-
if (!existing || Date.parse(normalized.updatedAt) >= Date.parse(existing.updatedAt)) {
|
|
397
|
-
byId.set(normalized.id, normalized);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return [...byId.values()]
|
|
401
|
-
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
|
|
402
|
-
.slice(-MAX_CASE_ACTIONS);
|
|
403
|
-
}
|
|
404
|
-
export function appendCaseAction(current, raw, now) {
|
|
405
|
-
const item = record(raw);
|
|
406
|
-
const created = normalizeAction({ ...item, id: freshId('action'), createdAt: now, updatedAt: now }, now);
|
|
407
|
-
if (!created)
|
|
408
|
-
throw new Error('An action requires a recipient or internal owner.');
|
|
409
|
-
return normalizeCaseActions([...current, created], now);
|
|
410
|
-
}
|
|
411
|
-
export function updateCaseAction(current, raw, now) {
|
|
412
|
-
const patch = record(raw);
|
|
413
|
-
const id = typeof patch.id === 'string' && SAFE_ID_RE.test(patch.id) ? patch.id : '';
|
|
414
|
-
const existing = current.find((item) => item.id === id);
|
|
415
|
-
if (!existing)
|
|
416
|
-
throw new Error('That case action no longer exists.');
|
|
417
|
-
const updated = normalizeAction({ ...existing, ...patch, id, createdAt: existing.createdAt, updatedAt: now }, now);
|
|
418
|
-
if (!updated)
|
|
419
|
-
throw new Error('An action requires a recipient or internal owner.');
|
|
420
|
-
return normalizeCaseActions(current.map((item) => item.id === id ? updated : item), now);
|
|
421
|
-
}
|
|
422
|
-
export function buildCaseActionOutcomeSummary(actions, nowRaw = new Date().toISOString()) {
|
|
423
|
-
const now = optionalIso(nowRaw) ?? new Date().toISOString();
|
|
424
|
-
const nowTime = Date.parse(now);
|
|
425
|
-
const terminal = new Set(['closed', 'resolved']);
|
|
426
|
-
const active = actions.filter((item) => !terminal.has(item.state));
|
|
427
|
-
const latestOutcomes = actions
|
|
428
|
-
.filter((item) => Boolean(item.outcome))
|
|
429
|
-
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))
|
|
430
|
-
.slice(0, 5)
|
|
431
|
-
.map((item) => ({
|
|
432
|
-
actionId: item.id,
|
|
433
|
-
recipient: item.recipient,
|
|
434
|
-
state: item.state,
|
|
435
|
-
outcome: item.outcome ?? '',
|
|
436
|
-
updatedAt: item.updatedAt,
|
|
437
|
-
}));
|
|
438
|
-
return {
|
|
439
|
-
total: actions.length,
|
|
440
|
-
active: active.length,
|
|
441
|
-
submitted: actions.filter((item) => item.state === 'submitted').length,
|
|
442
|
-
acknowledged: actions.filter((item) => item.state === 'acknowledged').length,
|
|
443
|
-
resolved: actions.filter((item) => item.state === 'resolved').length,
|
|
444
|
-
closed: actions.filter((item) => item.state === 'closed').length,
|
|
445
|
-
overdue: active.filter((item) => item.dueAt && Date.parse(item.dueAt) < nowTime).length,
|
|
446
|
-
followUpDue: active.filter((item) => item.followUpAt && Date.parse(item.followUpAt) <= nowTime).length,
|
|
447
|
-
withOutcome: actions.filter((item) => Boolean(item.outcome)).length,
|
|
448
|
-
latestOutcomes,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
export function mergeCaseEvidencePins(local, imported, fallback) {
|
|
452
|
-
return normalizeCaseEvidencePins([...local, ...imported], fallback);
|
|
453
|
-
}
|
|
454
|
-
export function mergeCaseDecisions(local, imported, fallback, validPinIds) {
|
|
455
|
-
return normalizeCaseDecisions([...local, ...imported], fallback, validPinIds);
|
|
456
|
-
}
|
|
457
|
-
export function mergeCaseActions(local, imported, fallback) {
|
|
458
|
-
return normalizeCaseActions([...local, ...imported], fallback);
|
|
459
|
-
}
|
|
460
|
-
function assertionProvenanceList(value, maximum) {
|
|
461
|
-
if (!Array.isArray(value))
|
|
462
|
-
return [];
|
|
463
|
-
const output = new Set();
|
|
464
|
-
for (const item of value.slice(0, maximum * 2)) {
|
|
465
|
-
const normalized = text(item, MAX_RESPONSE_LIMITATION_LENGTH);
|
|
466
|
-
if (normalized)
|
|
467
|
-
output.add(normalized);
|
|
468
|
-
if (output.size >= maximum)
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
return [...output];
|
|
472
|
-
}
|
|
473
|
-
function normalizeAssertionProvenance(value) {
|
|
474
|
-
const item = record(value);
|
|
475
|
-
const sourceName = text(item.sourceName, 120);
|
|
476
|
-
const sourceDigestSha256 = text(item.sourceDigestSha256, 64).toLowerCase();
|
|
477
|
-
const entityValue = text(item.entityValue, MAX_RESPONSE_VALUE_LENGTH);
|
|
478
|
-
if (item.origin !== 'external_import'
|
|
479
|
-
|| typeof item.format !== 'string'
|
|
480
|
-
|| !ASSERTION_EXTERNAL_FORMATS.has(item.format)
|
|
481
|
-
|| typeof item.entityType !== 'string'
|
|
482
|
-
|| !ASSERTION_EXTERNAL_ENTITY_TYPES.has(item.entityType)
|
|
483
|
-
|| !sourceName
|
|
484
|
-
|| !/^[0-9a-f]{64}$/u.test(sourceDigestSha256)
|
|
485
|
-
|| !entityValue) {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
const confidence = typeof item.confidence === 'number'
|
|
489
|
-
&& Number.isInteger(item.confidence)
|
|
490
|
-
&& item.confidence >= 0
|
|
491
|
-
&& item.confidence <= 100
|
|
492
|
-
? item.confidence
|
|
493
|
-
: null;
|
|
494
|
-
return {
|
|
495
|
-
origin: 'external_import',
|
|
496
|
-
format: item.format,
|
|
497
|
-
sourceName,
|
|
498
|
-
sourceDigestSha256,
|
|
499
|
-
publisher: text(item.publisher, 160) || null,
|
|
500
|
-
externalId: text(item.externalId, 200) || null,
|
|
501
|
-
entityType: item.entityType,
|
|
502
|
-
entityValue,
|
|
503
|
-
observedAt: optionalIso(item.observedAt),
|
|
504
|
-
createdAt: optionalIso(item.createdAt),
|
|
505
|
-
modifiedAt: optionalIso(item.modifiedAt),
|
|
506
|
-
confidence,
|
|
507
|
-
labels: assertionProvenanceList(item.labels, MAX_ASSERTION_PROVENANCE_LABELS),
|
|
508
|
-
markings: assertionProvenanceList(item.markings, MAX_ASSERTION_PROVENANCE_MARKINGS),
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function normalizeAssertion(raw, fallback, validPinIds) {
|
|
512
|
-
const item = record(raw);
|
|
513
|
-
const statement = text(item.statement, MAX_RESPONSE_RATIONALE_LENGTH);
|
|
514
|
-
if (!statement)
|
|
515
|
-
return null;
|
|
516
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
517
|
-
const provenance = normalizeAssertionProvenance(item.provenance);
|
|
518
|
-
const legacyIds = uniqueIds(item.evidencePinIds, validPinIds);
|
|
519
|
-
const evidenceRelations = normalizeEvidenceRelations(item.evidenceRelations, legacyIds, validPinIds);
|
|
520
|
-
return {
|
|
521
|
-
id: safeId(item.id, 'assertion', { statement, createdAt }),
|
|
522
|
-
kind: typeof item.kind === 'string' && ASSERTION_KINDS.has(item.kind)
|
|
523
|
-
? item.kind
|
|
524
|
-
: 'hypothesis',
|
|
525
|
-
statement,
|
|
526
|
-
rationale: text(item.rationale, MAX_RESPONSE_RATIONALE_LENGTH) || null,
|
|
527
|
-
evidencePinIds: evidenceRelations.map((relation) => relation.evidencePinId),
|
|
528
|
-
evidenceRelations,
|
|
529
|
-
state: typeof item.state === 'string' && ASSERTION_STATES.has(item.state)
|
|
530
|
-
? item.state
|
|
531
|
-
: 'open',
|
|
532
|
-
createdAt,
|
|
533
|
-
updatedAt: iso(item.updatedAt, createdAt),
|
|
534
|
-
...(provenance ? { provenance } : {}),
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
export function normalizeCaseAssertions(raw, fallback, validPinIds) {
|
|
538
|
-
if (!Array.isArray(raw))
|
|
539
|
-
return [];
|
|
540
|
-
const byId = new Map();
|
|
541
|
-
for (const item of raw.slice(0, MAX_CASE_ASSERTIONS * 2)) {
|
|
542
|
-
const normalized = normalizeAssertion(item, fallback, validPinIds);
|
|
543
|
-
if (!normalized)
|
|
544
|
-
continue;
|
|
545
|
-
const existing = byId.get(normalized.id);
|
|
546
|
-
if (!existing || Date.parse(normalized.updatedAt) >= Date.parse(existing.updatedAt)) {
|
|
547
|
-
byId.set(normalized.id, normalized);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return [...byId.values()]
|
|
551
|
-
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
|
|
552
|
-
.slice(-MAX_CASE_ASSERTIONS);
|
|
553
|
-
}
|
|
554
|
-
export function appendCaseAssertion(current, raw, now, validPinIds) {
|
|
555
|
-
const item = record(raw);
|
|
556
|
-
const created = normalizeAssertion({
|
|
557
|
-
...item,
|
|
558
|
-
id: freshId('assertion'),
|
|
559
|
-
createdAt: now,
|
|
560
|
-
updatedAt: now,
|
|
561
|
-
}, now, validPinIds);
|
|
562
|
-
if (!created)
|
|
563
|
-
throw new Error('An analyst assertion requires a statement.');
|
|
564
|
-
return normalizeCaseAssertions([...current, created], now, validPinIds);
|
|
565
|
-
}
|
|
566
|
-
export function updateCaseAssertion(current, raw, now, validPinIds) {
|
|
567
|
-
const patch = record(raw);
|
|
568
|
-
const id = typeof patch.id === 'string' && SAFE_ID_RE.test(patch.id) ? patch.id : '';
|
|
569
|
-
const existing = current.find((item) => item.id === id);
|
|
570
|
-
if (!existing)
|
|
571
|
-
throw new Error('That analyst assertion no longer exists.');
|
|
572
|
-
const updated = normalizeAssertion({
|
|
573
|
-
...existing,
|
|
574
|
-
...patch,
|
|
575
|
-
id,
|
|
576
|
-
createdAt: existing.createdAt,
|
|
577
|
-
updatedAt: now,
|
|
578
|
-
}, now, validPinIds);
|
|
579
|
-
if (!updated)
|
|
580
|
-
throw new Error('An analyst assertion requires a statement.');
|
|
581
|
-
return normalizeCaseAssertions(current.map((item) => item.id === id ? updated : item), now, validPinIds);
|
|
582
|
-
}
|
|
583
|
-
function normalizeManualTrailEvent(raw, fallback) {
|
|
584
|
-
const item = record(raw);
|
|
585
|
-
const summary = text(item.summary, MAX_RESPONSE_RATIONALE_LENGTH);
|
|
586
|
-
if (!summary)
|
|
587
|
-
return null;
|
|
588
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
589
|
-
return {
|
|
590
|
-
id: safeId(item.id, 'trail', { summary, createdAt }),
|
|
591
|
-
kind: typeof item.kind === 'string' && TRAIL_KINDS.has(item.kind)
|
|
592
|
-
? item.kind
|
|
593
|
-
: 'review',
|
|
594
|
-
summary,
|
|
595
|
-
target: text(item.target, MAX_TRAIL_TARGET_LENGTH) || null,
|
|
596
|
-
createdAt,
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
export function normalizeCaseManualTrail(raw, fallback) {
|
|
600
|
-
if (!Array.isArray(raw))
|
|
601
|
-
return [];
|
|
602
|
-
const byId = new Map();
|
|
603
|
-
for (const item of raw.slice(0, MAX_CASE_MANUAL_TRAIL_EVENTS * 2)) {
|
|
604
|
-
const normalized = normalizeManualTrailEvent(item, fallback);
|
|
605
|
-
if (normalized && !byId.has(normalized.id))
|
|
606
|
-
byId.set(normalized.id, normalized);
|
|
607
|
-
}
|
|
608
|
-
return [...byId.values()]
|
|
609
|
-
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
|
|
610
|
-
.slice(-MAX_CASE_MANUAL_TRAIL_EVENTS);
|
|
611
|
-
}
|
|
612
|
-
export function appendCaseManualTrailEvent(current, raw, now) {
|
|
613
|
-
const item = record(raw);
|
|
614
|
-
const created = normalizeManualTrailEvent({ ...item, id: freshId('trail'), createdAt: now }, now);
|
|
615
|
-
if (!created)
|
|
616
|
-
throw new Error('An investigation-trail entry requires a summary.');
|
|
617
|
-
return normalizeCaseManualTrail([...current, created], now);
|
|
618
|
-
}
|
|
619
|
-
export function mergeCaseAssertions(local, imported, fallback, validPinIds) {
|
|
620
|
-
return normalizeCaseAssertions([...local, ...imported], fallback, validPinIds);
|
|
621
|
-
}
|
|
622
|
-
export function mergeCaseManualTrail(local, imported, fallback) {
|
|
623
|
-
return normalizeCaseManualTrail([...local, ...imported], fallback);
|
|
624
|
-
}
|
|
625
|
-
function sightingSourceClass(state) {
|
|
626
|
-
if (state === 'observed_by_deployment')
|
|
627
|
-
return 'deployment';
|
|
628
|
-
if (state === 'reported_by_provider')
|
|
629
|
-
return 'provider';
|
|
630
|
-
return 'analyst';
|
|
631
|
-
}
|
|
632
|
-
function normalizeCaseSighting(raw, fallback, validPinIds) {
|
|
633
|
-
const item = record(raw);
|
|
634
|
-
if (typeof item.state !== 'string' || !SIGHTING_STATES.has(item.state))
|
|
635
|
-
return null;
|
|
636
|
-
const state = item.state;
|
|
637
|
-
const source = text(item.source, MAX_RESPONSE_LABEL_LENGTH);
|
|
638
|
-
if (!source)
|
|
639
|
-
return null;
|
|
640
|
-
const createdAt = iso(item.createdAt, fallback);
|
|
641
|
-
const evidencePinId = typeof item.evidencePinId === 'string'
|
|
642
|
-
&& SAFE_ID_RE.test(item.evidencePinId)
|
|
643
|
-
&& (!validPinIds || validPinIds.has(item.evidencePinId))
|
|
644
|
-
? item.evidencePinId
|
|
645
|
-
: null;
|
|
646
|
-
return {
|
|
647
|
-
id: safeId(item.id, 'sighting', { state, source, createdAt }),
|
|
648
|
-
state,
|
|
649
|
-
sourceClass: sightingSourceClass(state),
|
|
650
|
-
category: typeof item.category === 'string' && SIGHTING_CATEGORIES.has(item.category)
|
|
651
|
-
? item.category
|
|
652
|
-
: 'other',
|
|
653
|
-
source,
|
|
654
|
-
observedAt: iso(item.observedAt, createdAt),
|
|
655
|
-
completeness: typeof item.completeness === 'string' && COMPLETENESS.has(item.completeness)
|
|
656
|
-
? item.completeness
|
|
657
|
-
: 'unknown',
|
|
658
|
-
evidencePinId,
|
|
659
|
-
limitations: limitations(item.limitations),
|
|
660
|
-
createdAt,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
export function normalizeCaseSightings(raw, fallback, validPinIds) {
|
|
664
|
-
if (!Array.isArray(raw))
|
|
665
|
-
return [];
|
|
666
|
-
const byId = new Map();
|
|
667
|
-
for (const item of raw.slice(0, MAX_CASE_SIGHTINGS * 2)) {
|
|
668
|
-
const normalized = normalizeCaseSighting(item, fallback, validPinIds);
|
|
669
|
-
if (normalized && !byId.has(normalized.id))
|
|
670
|
-
byId.set(normalized.id, normalized);
|
|
671
|
-
}
|
|
672
|
-
return [...byId.values()]
|
|
673
|
-
.sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt) || left.id.localeCompare(right.id))
|
|
674
|
-
.slice(-MAX_CASE_SIGHTINGS);
|
|
675
|
-
}
|
|
676
|
-
export function appendCaseSighting(current, raw, now, validPinIds) {
|
|
677
|
-
const item = record(raw);
|
|
678
|
-
const created = normalizeCaseSighting({
|
|
679
|
-
...item,
|
|
680
|
-
id: freshId('sighting'),
|
|
681
|
-
createdAt: now,
|
|
682
|
-
}, now, validPinIds);
|
|
683
|
-
if (!created)
|
|
684
|
-
throw new Error('A sighting requires a source and explicit source-qualified state.');
|
|
685
|
-
return normalizeCaseSightings([...current, created], now, validPinIds);
|
|
686
|
-
}
|
|
687
|
-
export function mergeCaseSightings(local, imported, fallback, validPinIds) {
|
|
688
|
-
return normalizeCaseSightings([...local, ...imported], fallback, validPinIds);
|
|
689
|
-
}
|
|
690
|
-
export function buildCaseInvestigationTrail(input) {
|
|
691
|
-
return [
|
|
692
|
-
...(input.assertions ?? []).map((item) => ({
|
|
693
|
-
id: `assertion:${item.id}`,
|
|
694
|
-
kind: 'assertion',
|
|
695
|
-
label: `${item.kind.replaceAll('_', ' ')} · ${item.state}`,
|
|
696
|
-
detail: item.statement,
|
|
697
|
-
createdAt: item.updatedAt,
|
|
698
|
-
})),
|
|
699
|
-
...(input.decisions ?? []).map((item) => ({
|
|
700
|
-
id: `decision:${item.id}`,
|
|
701
|
-
kind: 'decision',
|
|
702
|
-
label: 'analyst decision',
|
|
703
|
-
detail: item.summary,
|
|
704
|
-
createdAt: item.createdAt,
|
|
705
|
-
})),
|
|
706
|
-
...(input.actions ?? []).map((item) => ({
|
|
707
|
-
id: `action:${item.id}`,
|
|
708
|
-
kind: 'action',
|
|
709
|
-
label: `${item.type.replaceAll('_', ' ')} · ${item.state}`,
|
|
710
|
-
detail: item.recipient,
|
|
711
|
-
createdAt: item.updatedAt,
|
|
712
|
-
})),
|
|
713
|
-
...(input.manualTrail ?? []).map((item) => ({
|
|
714
|
-
id: `manual:${item.id}`,
|
|
715
|
-
kind: 'manual',
|
|
716
|
-
label: item.kind.replaceAll('_', ' '),
|
|
717
|
-
detail: item.target ? `${item.summary} · ${item.target}` : item.summary,
|
|
718
|
-
createdAt: item.createdAt,
|
|
719
|
-
})),
|
|
720
|
-
...(input.sightings ?? []).map((item) => ({
|
|
721
|
-
id: `sighting:${item.id}`,
|
|
722
|
-
kind: 'sighting',
|
|
723
|
-
label: `${item.state.replaceAll('_', ' ')} · ${item.category}`,
|
|
724
|
-
detail: `${item.source} · ${item.completeness}`,
|
|
725
|
-
createdAt: item.observedAt,
|
|
726
|
-
})),
|
|
727
|
-
].sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt) || left.id.localeCompare(right.id));
|
|
728
|
-
}
|