@slicedearth/whoisleuth-cli 2.1.0 → 2.3.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 +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +24 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +63 -13
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +240 -3
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +183 -53
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +115 -46
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, MAX_CASES, MAX_NOTES_PER_CASE, deterministicId, caseTimestampOrNull, normalizeCase, normalizeDomain, normalizeEvidenceHistory, normalizeNotes, normalizeReviewReasonCode, normalizeTags, objectRecord, safeId, isValidDisposition, isValidSource, isValidStatus, } from "./case-record-model.mjs";
|
|
2
2
|
import { CASE_REPORT_SCHEMA, PUBLIC_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, } from "../contracts/case-portability.mjs";
|
|
3
3
|
import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
|
|
4
4
|
import { assertBoundedJsonStructure } from "../../lib/bounded-json.mjs";
|
|
5
5
|
import { inspectCaseBrandProfileIds, unionCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
6
6
|
import { caseInvestigationBranchReferences, mergeCaseInvestigationBranches, normalizeCaseInvestigationBranches, } from "./case-investigation-branch-model.mjs";
|
|
7
|
-
import { buildCaseClosureLinkContext, mergeCaseActions,
|
|
8
|
-
const STATUS_VALUES = new Set(CASE_STATUSES.map((item) => item.value));
|
|
9
|
-
const DISPOSITION_VALUES = new Set(CASE_DISPOSITIONS.map((item) => item.value));
|
|
10
|
-
const SOURCE_VALUES = new Set(CASE_SOURCES.map((item) => item.value));
|
|
7
|
+
import { buildCaseClosureLinkContext, mergeCaseActions, mergeCaseClosureHistories, mergeCaseObservedEffectHistories, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_CASE_MANUAL_TRAIL_EVENTS, MAX_CASE_SIGHTINGS, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, } from "./case-response-model.mjs";
|
|
11
8
|
export const MAX_CASE_INPUT_RECORDS = 2_000;
|
|
12
9
|
function compareCodeUnits(left, right) {
|
|
13
10
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -51,7 +48,7 @@ export function normalizeCaseStore(raw) {
|
|
|
51
48
|
throw new TypeError(`Unversioned Case stores are retired. Export or reset them explicitly before using Case schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
52
49
|
}
|
|
53
50
|
if (!CASE_IMPORT_VERSIONS.includes(sourceVersion) && sourceVersion < CASE_SCHEMA_VERSION) {
|
|
54
|
-
throw new TypeError(`Case schema ${sourceVersion} is not part of the supported compatibility boundary. Exact published schemas
|
|
51
|
+
throw new TypeError(`Case schema ${sourceVersion} is not part of the supported compatibility boundary. Exact published schemas ${CASE_IMPORT_VERSIONS.filter((version) => version < CASE_SCHEMA_VERSION).join(', ')} migrate to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
55
52
|
}
|
|
56
53
|
if (sourceVersion > CASE_SCHEMA_VERSION) {
|
|
57
54
|
throw new TypeError(`Case schema ${sourceVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
@@ -92,6 +89,17 @@ function assertCurrentCaseShape(raw) {
|
|
|
92
89
|
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeObservedAt')) {
|
|
93
90
|
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} response actions must declare their route observation time, including null; unreleased local Case checkpoints are not interpreted as the current format and no data was changed.`);
|
|
94
91
|
}
|
|
92
|
+
if (Object.keys(record).length > 0 && !Object.hasOwn(record, 'routeReviewAfter')) {
|
|
93
|
+
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} response actions must declare their route review deadline, including null; unreleased local Case checkpoints are not interpreted as the current format and no data was changed.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const decisions = itemRecord.decisions;
|
|
97
|
+
for (const decision of Array.isArray(decisions) ? decisions : []) {
|
|
98
|
+
const record = objectRecord(decision);
|
|
99
|
+
if (Object.keys(record).length > 0
|
|
100
|
+
&& (!Object.hasOwn(record, 'confidence') || !Object.hasOwn(record, 'confidenceBasis'))) {
|
|
101
|
+
throw new TypeError(`Case schema ${CASE_SCHEMA_VERSION} analyst decisions must declare confidence and its basis; unreleased local Case checkpoints are not interpreted as the current format and no data was changed.`);
|
|
102
|
+
}
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
}
|
|
@@ -140,9 +148,6 @@ function boundedCaseList(raw) {
|
|
|
140
148
|
* @param {string} [nowIso]
|
|
141
149
|
* @returns {CaseRecord}
|
|
142
150
|
*/
|
|
143
|
-
function importScalar(value, valid) {
|
|
144
|
-
return typeof value === 'string' && valid.has(value) ? value : undefined;
|
|
145
|
-
}
|
|
146
151
|
/**
|
|
147
152
|
* Validates one imported record into a patch. Unlike normalizeCase, absent or
|
|
148
153
|
* invalid scalar fields stay `undefined` (never defaulted) and a missing/invalid
|
|
@@ -174,6 +179,8 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
174
179
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
175
180
|
const actions = normalizeCaseActions(record.actions, normalizedFallback, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
176
181
|
const assertions = normalizeCaseAssertions(record.assertions, normalizedFallback, pinIds, timestampOptions);
|
|
182
|
+
const decisions = normalizeCaseDecisions(record.decisions, normalizedFallback, pinIds, timestampOptions);
|
|
183
|
+
const manualTrail = normalizeCaseManualTrail(record.manualTrail, normalizedFallback, timestampOptions);
|
|
177
184
|
const sightings = normalizeCaseSightings(record.sightings, normalizedFallback, pinIds, timestampOptions);
|
|
178
185
|
const observedEffects = normalizeCaseObservedEffectHistory(importedVersion >= 13 ? record.observedEffects : undefined, normalizedFallback, pinIds, new Set(sightings.map((item) => item.id)), timestampOptions);
|
|
179
186
|
const closures = normalizeCaseClosureHistory(importedVersion >= 13 ? record.closures : undefined, normalizedFallback, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
|
|
@@ -181,17 +188,26 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
181
188
|
const brandProfileReferences = importedVersion >= 12
|
|
182
189
|
? inspectCaseBrandProfileIds(record.brandProfileIds)
|
|
183
190
|
: { ids: [], omitted: 0 };
|
|
191
|
+
const authoredHistoryOmitted = [
|
|
192
|
+
[record.evidencePins, evidencePins],
|
|
193
|
+
[record.decisions, decisions],
|
|
194
|
+
[record.assertions, assertions],
|
|
195
|
+
[record.manualTrail, manualTrail],
|
|
196
|
+
[record.sightings, sightings],
|
|
197
|
+
].reduce((total, [candidates, retained]) => total
|
|
198
|
+
+ Math.max(0, (Array.isArray(candidates) ? candidates.length : 0) - retained.length), 0);
|
|
184
199
|
return {
|
|
185
200
|
domain,
|
|
186
201
|
rawId: typeof record.id === 'string' ? record.id : null,
|
|
187
|
-
status:
|
|
188
|
-
disposition:
|
|
202
|
+
status: isValidStatus(record.status) ? record.status : undefined,
|
|
203
|
+
disposition: isValidDisposition(record.disposition) ? record.disposition : undefined,
|
|
189
204
|
reviewReasonCode: Object.hasOwn(record, 'reviewReasonCode')
|
|
190
205
|
? normalizeReviewReasonCode(record.reviewReasonCode)
|
|
191
206
|
: undefined,
|
|
192
207
|
brandProfileIds: brandProfileReferences.ids,
|
|
193
208
|
brandProfileReferencesOmitted: brandProfileReferences.omitted,
|
|
194
|
-
|
|
209
|
+
authoredHistoryOmitted,
|
|
210
|
+
source: isValidSource(record.source) ? record.source : undefined,
|
|
195
211
|
evidenceHistory: normalizeEvidenceHistory(rawEvidence, {
|
|
196
212
|
source: 'import',
|
|
197
213
|
fallback: importFallback,
|
|
@@ -199,10 +215,10 @@ function extractImportPatch(raw, importedVersion) {
|
|
|
199
215
|
caseDomain: domain,
|
|
200
216
|
}),
|
|
201
217
|
evidencePins,
|
|
202
|
-
decisions
|
|
218
|
+
decisions,
|
|
203
219
|
actions,
|
|
204
220
|
assertions,
|
|
205
|
-
manualTrail
|
|
221
|
+
manualTrail,
|
|
206
222
|
sightings,
|
|
207
223
|
observedEffects,
|
|
208
224
|
closures,
|
|
@@ -229,6 +245,22 @@ function unionNotes(a, b) {
|
|
|
229
245
|
const notes = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
230
246
|
return notes.slice(Math.max(0, notes.length - MAX_NOTES_PER_CASE));
|
|
231
247
|
}
|
|
248
|
+
function retainLocalAuthoredRecords(local, imported, maximum) {
|
|
249
|
+
const records = [...local];
|
|
250
|
+
const retainedIds = new Set(records.map((item) => item.id));
|
|
251
|
+
let omitted = 0;
|
|
252
|
+
for (const item of imported) {
|
|
253
|
+
if (retainedIds.has(item.id))
|
|
254
|
+
continue;
|
|
255
|
+
if (records.length >= maximum) {
|
|
256
|
+
omitted += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
records.push(item);
|
|
260
|
+
retainedIds.add(item.id);
|
|
261
|
+
}
|
|
262
|
+
return { records, omitted };
|
|
263
|
+
}
|
|
232
264
|
// Additive, deduplicated union of two evidence histories. Identical material
|
|
233
265
|
// collapses (earliest firstCapturedAt, latest capturedAt), distinct snapshots
|
|
234
266
|
// are retained subject to the per-case bound, and an older import can never
|
|
@@ -274,16 +306,29 @@ function caseFromPatch(patch, now) {
|
|
|
274
306
|
function applyImportPatch(local, patch) {
|
|
275
307
|
const importNewer = patch.updatedAt !== null && Date.parse(patch.updatedAt) > Date.parse(local.updatedAt);
|
|
276
308
|
const fallback = patch.updatedAt || local.updatedAt;
|
|
277
|
-
const
|
|
309
|
+
const pinSelection = retainLocalAuthoredRecords(local.evidencePins, patch.evidencePins, MAX_CASE_EVIDENCE_PINS);
|
|
310
|
+
const evidencePins = normalizeCaseEvidencePins(pinSelection.records, fallback);
|
|
278
311
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
279
312
|
const actions = mergeCaseActions(local.actions, patch.actions, fallback, pinIds);
|
|
280
|
-
const
|
|
313
|
+
const assertionSelection = retainLocalAuthoredRecords(local.assertions, patch.assertions, MAX_CASE_ASSERTIONS);
|
|
314
|
+
const assertions = normalizeCaseAssertions(assertionSelection.records, fallback, pinIds);
|
|
281
315
|
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
282
|
-
const
|
|
316
|
+
const sightingSelection = retainLocalAuthoredRecords(local.sightings, patch.sightings, MAX_CASE_SIGHTINGS);
|
|
317
|
+
const sightings = normalizeCaseSightings(sightingSelection.records, fallback, pinIds);
|
|
283
318
|
const sightingIds = new Set(sightings.map((item) => item.id));
|
|
284
319
|
const observedEffects = mergeCaseObservedEffectHistories(local.observedEffects, patch.observedEffects, fallback, pinIds, sightingIds);
|
|
285
320
|
const closures = mergeCaseClosureHistories(local.closures, patch.closures, fallback, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), buildCaseClosureLinkContext(observedEffects, actions));
|
|
286
321
|
const brandProfileReferences = unionCaseBrandProfileIds(local.brandProfileIds, patch.brandProfileIds);
|
|
322
|
+
const decisionSelection = retainLocalAuthoredRecords(local.decisions, patch.decisions, MAX_CASE_DECISIONS);
|
|
323
|
+
const decisions = normalizeCaseDecisions(decisionSelection.records, fallback, pinIds);
|
|
324
|
+
const trailSelection = retainLocalAuthoredRecords(local.manualTrail, patch.manualTrail, MAX_CASE_MANUAL_TRAIL_EVENTS);
|
|
325
|
+
const manualTrail = normalizeCaseManualTrail(trailSelection.records, fallback);
|
|
326
|
+
const authoredHistoryOmitted = patch.authoredHistoryOmitted
|
|
327
|
+
+ pinSelection.omitted
|
|
328
|
+
+ decisionSelection.omitted
|
|
329
|
+
+ assertionSelection.omitted
|
|
330
|
+
+ trailSelection.omitted
|
|
331
|
+
+ sightingSelection.omitted;
|
|
287
332
|
return { record: {
|
|
288
333
|
...local,
|
|
289
334
|
status: patch.status !== undefined && importNewer ? patch.status : local.status,
|
|
@@ -293,10 +338,10 @@ function applyImportPatch(local, patch) {
|
|
|
293
338
|
source: patch.source !== undefined && importNewer ? patch.source : local.source,
|
|
294
339
|
evidenceHistory: mergeEvidenceHistories(local.evidenceHistory, patch.evidenceHistory, local.domain),
|
|
295
340
|
evidencePins,
|
|
296
|
-
decisions
|
|
341
|
+
decisions,
|
|
297
342
|
actions,
|
|
298
343
|
assertions,
|
|
299
|
-
manualTrail
|
|
344
|
+
manualTrail,
|
|
300
345
|
sightings,
|
|
301
346
|
observedEffects,
|
|
302
347
|
closures,
|
|
@@ -305,7 +350,7 @@ function applyImportPatch(local, patch) {
|
|
|
305
350
|
notes: unionNotes(local.notes, patch.notes),
|
|
306
351
|
createdAt: patch.createdAt && Date.parse(patch.createdAt) < Date.parse(local.createdAt) ? patch.createdAt : local.createdAt,
|
|
307
352
|
updatedAt: importNewer ? (patch.updatedAt ?? local.updatedAt) : local.updatedAt,
|
|
308
|
-
}, brandProfileReferencesOmitted: brandProfileReferences.omitted };
|
|
353
|
+
}, brandProfileReferencesOmitted: brandProfileReferences.omitted, authoredHistoryOmitted };
|
|
309
354
|
}
|
|
310
355
|
function pickFreeId(preferred, domain, used) {
|
|
311
356
|
const wanted = safeId(preferred);
|
|
@@ -418,7 +463,7 @@ function caseCollectionImportEnvelope(importedRaw) {
|
|
|
418
463
|
* reinterpreted.
|
|
419
464
|
* @param {CaseRecord[]} localCases
|
|
420
465
|
* @param {unknown} importedRaw
|
|
421
|
-
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number }}
|
|
466
|
+
* @returns {{ cases: CaseRecord[], added: number, updated: number, skipped: number, brandProfileReferencesOmitted: number, authoredHistoryOmitted: number }}
|
|
422
467
|
*/
|
|
423
468
|
export function mergeCases(localCases, importedRaw) {
|
|
424
469
|
assertBoundedJsonStructure(importedRaw, 'Case import', CASE_INPUT_JSON_LIMITS);
|
|
@@ -426,7 +471,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
426
471
|
const importedVersion = parseStoreVersion(importedEnvelope);
|
|
427
472
|
if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion < CASE_SCHEMA_VERSION
|
|
428
473
|
&& !CASE_IMPORT_VERSIONS.includes(importedVersion)) {
|
|
429
|
-
throw new Error(`Case schema ${importedVersion} is not part of the supported compatibility boundary. Exact published schemas
|
|
474
|
+
throw new Error(`Case schema ${importedVersion} is not part of the supported compatibility boundary. Exact published schemas ${CASE_IMPORT_VERSIONS.filter((version) => version < CASE_SCHEMA_VERSION).join(', ')} migrate to schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
430
475
|
}
|
|
431
476
|
if (importedVersion !== null && Number.isSafeInteger(importedVersion) && importedVersion > CASE_SCHEMA_VERSION) {
|
|
432
477
|
throw new Error(`Case schema ${importedVersion} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
@@ -446,6 +491,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
446
491
|
const imported = boundedCaseList(importedEnvelope);
|
|
447
492
|
let skipped = imported.omitted;
|
|
448
493
|
let brandProfileReferencesOmitted = 0;
|
|
494
|
+
let authoredHistoryOmitted = 0;
|
|
449
495
|
const fallback = new Date(0).toISOString();
|
|
450
496
|
for (const item of imported.items) {
|
|
451
497
|
const patch = extractImportPatch(item, supportedImportedVersion);
|
|
@@ -458,6 +504,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
458
504
|
const merged = applyImportPatch(existing, patch);
|
|
459
505
|
byDomain.set(patch.domain, merged.record);
|
|
460
506
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted + merged.brandProfileReferencesOmitted;
|
|
507
|
+
authoredHistoryOmitted += merged.authoredHistoryOmitted;
|
|
461
508
|
updated += 1;
|
|
462
509
|
}
|
|
463
510
|
else if (byDomain.size < MAX_CASES) {
|
|
@@ -466,6 +513,7 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
466
513
|
usedIds.add(record.id);
|
|
467
514
|
byDomain.set(patch.domain, record);
|
|
468
515
|
brandProfileReferencesOmitted += patch.brandProfileReferencesOmitted;
|
|
516
|
+
authoredHistoryOmitted += patch.authoredHistoryOmitted;
|
|
469
517
|
added += 1;
|
|
470
518
|
}
|
|
471
519
|
else {
|
|
@@ -478,5 +526,6 @@ export function mergeCases(localCases, importedRaw) {
|
|
|
478
526
|
updated,
|
|
479
527
|
skipped,
|
|
480
528
|
brandProfileReferencesOmitted,
|
|
529
|
+
authoredHistoryOmitted,
|
|
481
530
|
};
|
|
482
531
|
}
|
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
import { ANALYST_REVIEW_REASONS } from "../../lib/analyst-taxonomy.mjs";
|
|
2
2
|
import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, 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, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, } from "../contracts/case-portability.mjs";
|
|
3
|
+
import { CASE_DISPOSITIONS, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_STATUS, } from "./case-record-decisions.mjs";
|
|
3
4
|
export { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, 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, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, };
|
|
4
|
-
export
|
|
5
|
-
{ value: 'new', label: 'New' },
|
|
6
|
-
{ value: 'reviewing', label: 'Reviewing' },
|
|
7
|
-
{ value: 'monitoring', label: 'Monitoring' },
|
|
8
|
-
{ value: 'escalated', label: 'Escalated' },
|
|
9
|
-
{ value: 'resolved', label: 'Resolved' },
|
|
10
|
-
];
|
|
11
|
-
export const CASE_DISPOSITIONS = [
|
|
12
|
-
{ value: 'unreviewed', label: 'Unreviewed' },
|
|
13
|
-
{ value: 'suspicious', label: 'Suspicious' },
|
|
14
|
-
{ value: 'confirmed_abuse', label: 'Confirmed abuse' },
|
|
15
|
-
{ value: 'false_positive', label: 'False positive' },
|
|
16
|
-
{ value: 'expected', label: 'Expected' },
|
|
17
|
-
{ value: 'closed_no_action', label: 'Closed without action' },
|
|
18
|
-
];
|
|
5
|
+
export { CASE_DISPOSITIONS, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_STATUS, };
|
|
19
6
|
export const CASE_REVIEW_REASONS = ANALYST_REVIEW_REASONS.map((item) => ({ ...item }));
|
|
20
|
-
export const CASE_SOURCES = [
|
|
7
|
+
export const CASE_SOURCES = Object.freeze([
|
|
21
8
|
{ value: 'lookup', label: 'Lookup' },
|
|
22
9
|
{ value: 'bulk', label: 'Bulk' },
|
|
23
10
|
{ value: 'monitor', label: 'Monitor' },
|
|
24
11
|
{ value: 'manual', label: 'Manual' },
|
|
25
12
|
{ value: 'unknown', label: 'Unknown' },
|
|
26
|
-
];
|
|
27
|
-
export const EVIDENCE_SOURCES = ['lookup', 'bulk', 'monitor', 'import', 'unknown'];
|
|
28
|
-
export const DEFAULT_STATUS = 'new';
|
|
29
|
-
export const DEFAULT_DISPOSITION = 'unreviewed';
|
|
13
|
+
]);
|
|
14
|
+
export const EVIDENCE_SOURCES = Object.freeze(['lookup', 'bulk', 'monitor', 'import', 'unknown']);
|
|
30
15
|
export const DEFAULT_SOURCE = 'unknown';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import {
|
|
3
|
+
import { CASE_SCHEMA_VERSION, CASE_REVIEW_REASONS, CASE_SOURCES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, EVIDENCE_SOURCES, MAX_DOMAIN_LENGTH, MAX_NOTES_PER_CASE, MAX_NOTE_LENGTH, MAX_TAGS_PER_CASE, MAX_TAG_LENGTH, } from "./case-record-contracts.mjs";
|
|
4
|
+
import { dispositionLabel, isValidDisposition, isValidStatus, statusLabel, } from "./case-record-decisions.mjs";
|
|
4
5
|
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
5
6
|
import { canonicalRegistrableDomain } from "../../lib/registrable-domain.mjs";
|
|
6
7
|
// Forward-version policy (two distinct guarantees):
|
|
@@ -26,13 +27,9 @@ export const EVIDENCE_SOURCE_RANK = {
|
|
|
26
27
|
import: 1,
|
|
27
28
|
unknown: 0,
|
|
28
29
|
};
|
|
29
|
-
const STATUS_VALUES = new Set(CASE_STATUSES.map((item) => item.value));
|
|
30
|
-
const DISPOSITION_VALUES = new Set(CASE_DISPOSITIONS.map((item) => item.value));
|
|
31
30
|
const REVIEW_REASON_VALUES = new Set(CASE_REVIEW_REASONS.map((item) => item.value).filter(Boolean));
|
|
32
31
|
const SOURCE_VALUES = new Set(CASE_SOURCES.map((item) => item.value));
|
|
33
|
-
const
|
|
34
|
-
const DISPOSITION_LABELS = Object.fromEntries(CASE_DISPOSITIONS.map((item) => [item.value, item.label]));
|
|
35
|
-
const SOURCE_LABELS = Object.fromEntries(CASE_SOURCES.map((item) => [item.value, item.label]));
|
|
32
|
+
const SOURCE_LABELS = new Map(CASE_SOURCES.map((item) => [item.value, item.label]));
|
|
36
33
|
// Availability tokens that actually assert something about the domain. Anything
|
|
37
34
|
// else ('unknown', 'error', empty) is not, on its own, material evidence.
|
|
38
35
|
export const CONCLUSIVE_AVAILABILITY = new Set([
|
|
@@ -59,21 +56,13 @@ export function objectRecord(value) {
|
|
|
59
56
|
? value
|
|
60
57
|
: {};
|
|
61
58
|
}
|
|
62
|
-
export function statusLabel(value) {
|
|
63
|
-
return (typeof value === 'string' ? STATUS_LABELS[value] : '') || String(value || '');
|
|
64
|
-
}
|
|
65
|
-
export function dispositionLabel(value) {
|
|
66
|
-
return (typeof value === 'string' ? DISPOSITION_LABELS[value] : '') || String(value || '');
|
|
67
|
-
}
|
|
68
59
|
export function sourceLabel(value) {
|
|
69
|
-
return (
|
|
70
|
-
}
|
|
71
|
-
export function isValidStatus(value) {
|
|
72
|
-
return typeof value === 'string' && STATUS_VALUES.has(value);
|
|
60
|
+
return isValidSource(value) ? SOURCE_LABELS.get(value) : String(value || '');
|
|
73
61
|
}
|
|
74
|
-
export function
|
|
75
|
-
return typeof value === 'string' &&
|
|
62
|
+
export function isValidSource(value) {
|
|
63
|
+
return typeof value === 'string' && SOURCE_VALUES.has(value);
|
|
76
64
|
}
|
|
65
|
+
export { dispositionLabel, isValidDisposition, isValidStatus, statusLabel, };
|
|
77
66
|
/** Fresh, safe, effectively-unique id for a brand-new local record. */
|
|
78
67
|
export function makeId() {
|
|
79
68
|
if (typeof globalThis.crypto?.randomUUID === 'function')
|
|
@@ -147,16 +136,16 @@ export function normalizeEvidenceHostnameForCase(value, caseDomain) {
|
|
|
147
136
|
return canonicalRegistrableDomain(hostname) === canonicalCase ? hostname : null;
|
|
148
137
|
}
|
|
149
138
|
export function normalizeStatus(value) {
|
|
150
|
-
return
|
|
139
|
+
return isValidStatus(value) ? value : DEFAULT_STATUS;
|
|
151
140
|
}
|
|
152
141
|
export function normalizeDisposition(value) {
|
|
153
|
-
return
|
|
142
|
+
return isValidDisposition(value) ? value : DEFAULT_DISPOSITION;
|
|
154
143
|
}
|
|
155
144
|
export function normalizeReviewReasonCode(value) {
|
|
156
145
|
return typeof value === 'string' && REVIEW_REASON_VALUES.has(value) ? value : null;
|
|
157
146
|
}
|
|
158
147
|
export function normalizeSource(value) {
|
|
159
|
-
return
|
|
148
|
+
return isValidSource(value) ? value : DEFAULT_SOURCE;
|
|
160
149
|
}
|
|
161
150
|
/** Parsed ISO string, or null when missing/invalid (used for import ordering). */
|
|
162
151
|
export function isoOrNull(value) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Lightweight canonical Case status and disposition definitions. Persistence,
|
|
2
|
+
// portability and presentation consumers derive these identities and policies
|
|
3
|
+
// without importing the wider Case record or serialisation graph.
|
|
4
|
+
export const CASE_STATUSES = Object.freeze([
|
|
5
|
+
{ value: 'new', label: 'New' },
|
|
6
|
+
{ value: 'reviewing', label: 'Reviewing' },
|
|
7
|
+
{ value: 'monitoring', label: 'Monitoring' },
|
|
8
|
+
{ value: 'escalated', label: 'Escalated' },
|
|
9
|
+
{ value: 'resolved', label: 'Resolved' },
|
|
10
|
+
]);
|
|
11
|
+
export const CASE_DISPOSITIONS = Object.freeze([
|
|
12
|
+
{ value: 'unreviewed', label: 'Unreviewed' },
|
|
13
|
+
{ value: 'suspicious', label: 'Suspicious' },
|
|
14
|
+
{ value: 'confirmed_abuse', label: 'Confirmed abuse' },
|
|
15
|
+
{ value: 'false_positive', label: 'False positive' },
|
|
16
|
+
{ value: 'expected', label: 'Expected' },
|
|
17
|
+
{ value: 'closed_no_action', label: 'Closed without action' },
|
|
18
|
+
]);
|
|
19
|
+
export const DEFAULT_STATUS = 'new';
|
|
20
|
+
export const DEFAULT_DISPOSITION = 'unreviewed';
|
|
21
|
+
const CASE_STATUS_OPERATION_POLICIES = Object.freeze({
|
|
22
|
+
new: { closed: false, closureRequired: false, directEdit: 'allowed' },
|
|
23
|
+
reviewing: { closed: false, closureRequired: false, directEdit: 'allowed' },
|
|
24
|
+
monitoring: { closed: false, closureRequired: false, directEdit: 'allowed' },
|
|
25
|
+
escalated: { closed: false, closureRequired: false, directEdit: 'allowed' },
|
|
26
|
+
resolved: { closed: true, closureRequired: true, directEdit: 'existing_only' },
|
|
27
|
+
});
|
|
28
|
+
const CASE_DISPOSITION_DECISION_POLICIES = Object.freeze({
|
|
29
|
+
unreviewed: { reviewed: false, supportsDefensiveResponse: false },
|
|
30
|
+
suspicious: { reviewed: true, supportsDefensiveResponse: true },
|
|
31
|
+
confirmed_abuse: { reviewed: true, supportsDefensiveResponse: true },
|
|
32
|
+
false_positive: { reviewed: true, supportsDefensiveResponse: false },
|
|
33
|
+
expected: { reviewed: true, supportsDefensiveResponse: false },
|
|
34
|
+
closed_no_action: { reviewed: true, supportsDefensiveResponse: false },
|
|
35
|
+
});
|
|
36
|
+
function statusOption(value) {
|
|
37
|
+
return CASE_STATUSES.find((option) => option.value === value);
|
|
38
|
+
}
|
|
39
|
+
function dispositionOption(value) {
|
|
40
|
+
return CASE_DISPOSITIONS.find((option) => option.value === value);
|
|
41
|
+
}
|
|
42
|
+
export function isValidStatus(value) {
|
|
43
|
+
return Boolean(statusOption(value));
|
|
44
|
+
}
|
|
45
|
+
export function isValidDisposition(value) {
|
|
46
|
+
return Boolean(dispositionOption(value));
|
|
47
|
+
}
|
|
48
|
+
export function statusLabel(value) {
|
|
49
|
+
return statusOption(value)?.label ?? String(value || '');
|
|
50
|
+
}
|
|
51
|
+
export function dispositionLabel(value) {
|
|
52
|
+
return dispositionOption(value)?.label ?? String(value || '');
|
|
53
|
+
}
|
|
54
|
+
export function caseStatusRequiresClosure(value) {
|
|
55
|
+
const option = statusOption(value);
|
|
56
|
+
return option ? CASE_STATUS_OPERATION_POLICIES[option.value].closureRequired : false;
|
|
57
|
+
}
|
|
58
|
+
export function caseStatusIsClosed(value) {
|
|
59
|
+
const option = statusOption(value);
|
|
60
|
+
return option ? CASE_STATUS_OPERATION_POLICIES[option.value].closed : false;
|
|
61
|
+
}
|
|
62
|
+
export function isReviewedCaseDisposition(value) {
|
|
63
|
+
const option = dispositionOption(value);
|
|
64
|
+
return option ? CASE_DISPOSITION_DECISION_POLICIES[option.value].reviewed : false;
|
|
65
|
+
}
|
|
66
|
+
export function caseDispositionSupportsDefensiveResponse(value) {
|
|
67
|
+
const option = dispositionOption(value);
|
|
68
|
+
return option ? CASE_DISPOSITION_DECISION_POLICIES[option.value].supportsDefensiveResponse : false;
|
|
69
|
+
}
|
|
70
|
+
export function caseStatusOptionsForDirectEdit(currentStatus) {
|
|
71
|
+
return CASE_STATUSES.filter((option) => (CASE_STATUS_OPERATION_POLICIES[option.value].directEdit === 'allowed' || option.value === currentStatus));
|
|
72
|
+
}
|
|
@@ -6,4 +6,5 @@ export * from "./case-record-core.mjs";
|
|
|
6
6
|
export * from "./case-brand-profile-references.mjs";
|
|
7
7
|
export * from "./case-evidence-model.mjs";
|
|
8
8
|
export * from "./case-record-operations.mjs";
|
|
9
|
+
export * from "./case-record-projection.mjs";
|
|
9
10
|
export * from "./case-investigation-branch-model.mjs";
|