@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1
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/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +21 -6
- package/cli/artifact-validation/lookup-evidence.mjs +3 -3
- package/cli/artifact-validation/signed-review.mjs +59 -13
- package/cli/artifact-verify.mjs +2 -0
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -2
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +20 -3
- package/cli/case-pack.mjs +12 -2
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +3 -3
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +1 -1
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +18 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +68 -158
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +8 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +223 -2
- package/docs/contextual-reviews.md +155 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +12 -8
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +9 -7
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +22 -5
- package/lib/domain-posture.mjs +16 -15
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +22 -15
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +4 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +7 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +69 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +27 -0
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +141 -139
- package/lib/static-html-analysis.mjs +1 -1
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +388 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +13 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +78 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-recheck-model.mjs +90 -32
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +40 -112
- package/packages/cases/case-record-projection.mjs +8 -1
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +63 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +103 -1
- package/packages/cases/case-response-model.mjs +9 -1
- package/packages/cases/case-response-outcomes.mjs +29 -8
- package/packages/cases/case-response-packet-render.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +25 -11
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +14 -3
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +28 -10
- package/packages/contracts/case-portability.mjs +118 -25
- package/packages/contracts/cli-command-semantics.mjs +2 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +63 -17
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +4 -12
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-manifest.mjs +2 -2
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/workspace/brand-profile-model.mjs +2 -2
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +23 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +28 -12
- package/packages/workspace/watchlist-store.mjs +17 -5
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { isValidAsciiHostname } from "
|
|
2
|
-
import { INCIDENT_CASE_SCHEMA_VERSION, MAX_RESPONSE_RATIONALE_LENGTH } from "../contracts/case-portability.mjs";
|
|
1
|
+
import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
|
|
2
|
+
import { INCIDENT_CASE_SCHEMA_VERSION, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
|
|
3
3
|
import { enumeration, exact, text } from "../evidence/artifact-structure.mjs";
|
|
4
|
-
|
|
4
|
+
import { casePinHasCompleteObservation } from "./case-evidence-quality.mjs";
|
|
5
|
+
export const CASE_RECHECK_CONDITIONS = Object.freeze({
|
|
6
|
+
unknown: 'Not established',
|
|
7
|
+
comparable: 'Comparable for this question',
|
|
8
|
+
different: 'Different conditions',
|
|
9
|
+
});
|
|
5
10
|
function reference(value, label) {
|
|
6
11
|
const id = text(value, label, 64);
|
|
7
12
|
if (!/^[A-Za-z0-9_-]+$/u.test(id))
|
|
@@ -16,8 +21,11 @@ function contextFields(item) {
|
|
|
16
21
|
const conditions = text(item.conditions, 'Recheck comparison conditions', MAX_RESPONSE_RATIONALE_LENGTH);
|
|
17
22
|
if (!conditions.trim())
|
|
18
23
|
throw new TypeError('Describe the conditions needed to compare the observations.');
|
|
19
|
-
return Object.freeze({
|
|
20
|
-
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
targetHostname,
|
|
26
|
+
conditions,
|
|
27
|
+
baselinePinId: item.baselinePinId === null ? null : reference(item.baselinePinId, 'Recheck baseline pin'),
|
|
28
|
+
});
|
|
21
29
|
}
|
|
22
30
|
/** Optional current-format context; historical records have no invented question. */
|
|
23
31
|
export function readCaseRecheckContext(value, sourceVersion) {
|
|
@@ -36,56 +44,106 @@ export function readCaseRecheckAnswerContext(value, sourceVersion) {
|
|
|
36
44
|
const question = text(item.question, 'Recheck question', MAX_RESPONSE_RATIONALE_LENGTH);
|
|
37
45
|
if (!question.trim())
|
|
38
46
|
throw new TypeError('A recheck answer requires its original question.');
|
|
39
|
-
return Object.freeze({
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
...contextFields(item),
|
|
49
|
+
questionId: reference(item.questionId, 'Recheck question ID'),
|
|
40
50
|
question,
|
|
41
|
-
conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison')
|
|
51
|
+
conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison'),
|
|
52
|
+
});
|
|
42
53
|
}
|
|
43
54
|
export function caseRecheckQuestions(assertions) {
|
|
44
|
-
return assertions.filter(item => item.kind === 'next_step' && item.recheck && item.state === 'open');
|
|
55
|
+
return assertions.filter((item) => item.kind === 'next_step' && item.recheck && item.state === 'open');
|
|
45
56
|
}
|
|
46
57
|
export function caseRecheckAnswerContext(question, conditionsMatch) {
|
|
47
58
|
if (question.kind !== 'next_step' || !question.recheck)
|
|
48
59
|
throw new TypeError('Select a saved recheck question.');
|
|
49
|
-
return readCaseRecheckAnswerContext({
|
|
60
|
+
return readCaseRecheckAnswerContext({
|
|
61
|
+
...question.recheck,
|
|
62
|
+
questionId: question.id,
|
|
63
|
+
question: question.statement,
|
|
64
|
+
conditionsMatch,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Shared read projection; each form retains its draft and existing save coordinator. */
|
|
68
|
+
export function selectCaseRecheckQuestion(assertions, selection) {
|
|
69
|
+
const questions = caseRecheckQuestions(assertions);
|
|
70
|
+
const question = questions.find((item) => item.id === selection.questionId);
|
|
71
|
+
return {
|
|
72
|
+
questions,
|
|
73
|
+
context: question ? caseRecheckAnswerContext(question, selection.conditionsMatch) : undefined,
|
|
74
|
+
stale: Boolean(selection.questionId) && (!question || question.updatedAt !== selection.questionUpdatedAt),
|
|
75
|
+
};
|
|
50
76
|
}
|
|
51
77
|
/** A saved answer is a snapshot, not a live reference to an editable plan. */
|
|
52
78
|
export function assertCurrentRecheckQuestion(answer, assertions) {
|
|
53
|
-
const question = caseRecheckQuestions(assertions).find(item => item.id === answer.questionId);
|
|
54
|
-
if (!question ||
|
|
79
|
+
const question = caseRecheckQuestions(assertions).find((item) => item.id === answer.questionId);
|
|
80
|
+
if (!question ||
|
|
81
|
+
JSON.stringify(caseRecheckAnswerContext(question, answer.conditionsMatch)) !==
|
|
82
|
+
JSON.stringify(answer)) {
|
|
55
83
|
throw new Error('The recheck question changed or was resolved. Refresh and review its current conditions before saving this answer.');
|
|
56
84
|
}
|
|
57
85
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
const RECHECK_COMPARISON_MESSAGES = {
|
|
87
|
+
different_conditions: 'The comparison conditions differ.',
|
|
88
|
+
unconfirmed_conditions: 'Comparable conditions have not been confirmed.',
|
|
89
|
+
missing_baseline: 'The baseline evidence is no longer available.',
|
|
90
|
+
current_target_mismatch: 'The current evidence concerns a different or unknown hostname.',
|
|
91
|
+
incomplete_current: 'The current source does not establish a complete observation.',
|
|
92
|
+
later_observation_needed: 'A later source observation is needed; reviewing the baseline again is not a recheck.',
|
|
93
|
+
baseline_target_mismatch: 'The baseline concerns a different hostname.',
|
|
94
|
+
different_field_or_source: 'The observations use different fields or sources.',
|
|
95
|
+
};
|
|
96
|
+
/** Admissibility depends on these reasons, not on presentation warnings. */
|
|
97
|
+
export function caseRecheckComparisonBlockers(context, pins, current) {
|
|
98
|
+
const blockers = [];
|
|
99
|
+
if (context.conditionsMatch !== 'comparable') {
|
|
100
|
+
blockers.push(context.conditionsMatch === 'different'
|
|
101
|
+
? 'different_conditions'
|
|
102
|
+
: 'unconfirmed_conditions');
|
|
103
|
+
}
|
|
104
|
+
const baseline = pins.find((pin) => pin.id === context.baselinePinId);
|
|
64
105
|
if (context.baselinePinId && !baseline)
|
|
65
|
-
|
|
106
|
+
blockers.push('missing_baseline');
|
|
66
107
|
if (current) {
|
|
67
108
|
if (current.observationHostname !== context.targetHostname)
|
|
68
|
-
|
|
69
|
-
if (current
|
|
70
|
-
|
|
109
|
+
blockers.push('current_target_mismatch');
|
|
110
|
+
if (!casePinHasCompleteObservation(current))
|
|
111
|
+
blockers.push('incomplete_current');
|
|
71
112
|
if (baseline) {
|
|
72
|
-
|
|
73
|
-
|
|
113
|
+
const sameObservation = baseline.id === current.id;
|
|
114
|
+
const unknownObservationTime = !baseline.observedAt || !current.observedAt;
|
|
115
|
+
const nonLaterObservation = !unknownObservationTime &&
|
|
116
|
+
Date.parse(current.observedAt) <= Date.parse(baseline.observedAt);
|
|
117
|
+
if (sameObservation || unknownObservationTime || nonLaterObservation) {
|
|
118
|
+
blockers.push('later_observation_needed');
|
|
119
|
+
}
|
|
74
120
|
if (baseline.observationHostname && baseline.observationHostname !== context.targetHostname)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
121
|
+
blockers.push('baseline_target_mismatch');
|
|
122
|
+
const differentField = Boolean(baseline.field) && current.field !== baseline.field;
|
|
123
|
+
const differentSource = baseline.source !== current.source;
|
|
124
|
+
if (differentField || differentSource)
|
|
125
|
+
blockers.push('different_field_or_source');
|
|
78
126
|
}
|
|
79
127
|
}
|
|
80
|
-
return
|
|
128
|
+
return blockers;
|
|
129
|
+
}
|
|
130
|
+
export function caseRecheckComparisonWarnings(context, pins, current) {
|
|
131
|
+
return caseRecheckComparisonBlockers(context, pins, current)
|
|
132
|
+
.map((reason) => RECHECK_COMPARISON_MESSAGES[reason]);
|
|
81
133
|
}
|
|
82
134
|
export function assertRecheckNonReproduction(state, context, completeness, pins, current, observedAt) {
|
|
83
135
|
if (state !== 'not_reproduced')
|
|
84
136
|
return;
|
|
85
|
-
const
|
|
86
|
-
const baseline = pins.find(pin => pin.id === context.baselinePinId);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
137
|
+
const blockers = caseRecheckComparisonBlockers(context, pins, current);
|
|
138
|
+
const baseline = pins.find((pin) => pin.id === context.baselinePinId);
|
|
139
|
+
let laterIndependentObservationNeeded = false;
|
|
140
|
+
if (!current && baseline) {
|
|
141
|
+
const missingObservationTime = !baseline.observedAt || !observedAt;
|
|
142
|
+
const invalidObservationTime = observedAt ? !Number.isFinite(Date.parse(observedAt)) : false;
|
|
143
|
+
const nonLaterObservation = !missingObservationTime && Date.parse(observedAt) <= Date.parse(baseline.observedAt);
|
|
144
|
+
laterIndependentObservationNeeded = missingObservationTime || invalidObservationTime || nonLaterObservation;
|
|
145
|
+
}
|
|
146
|
+
if (completeness !== 'complete' || blockers.length || laterIndependentObservationNeeded) {
|
|
90
147
|
throw new Error('Not reproduced requires a complete observation under comparable conditions. Record unavailable or describe the limited observation instead.');
|
|
148
|
+
}
|
|
91
149
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANALYST_REVIEW_REASONS } from "
|
|
1
|
+
import { ANALYST_REVIEW_REASONS } from "../analysis/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
3
|
import { CASE_DISPOSITIONS, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_STATUS, } from "./case-record-decisions.mjs";
|
|
4
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,7 +4,7 @@ import { CASE_REVIEW_REASONS, CASE_SOURCES, DEFAULT_DISPOSITION, DEFAULT_SOURCE,
|
|
|
4
4
|
import { dispositionLabel, isValidDisposition, isValidStatus, statusLabel, } from "./case-record-decisions.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
6
|
import { PUBLISHED_V2_3_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
7
|
-
import { canonicalRegistrableDomain } from "
|
|
7
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
8
8
|
// Forward-version policy (two distinct guarantees):
|
|
9
9
|
// - A locally-stored envelope that declares a version greater than this is
|
|
10
10
|
// still read best-effort on load (known fields kept, unknown dropped), but
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import {
|
|
4
|
-
import { parseCredentialFreeHttpUrl } from "../evidence/lookup-target.mjs";
|
|
3
|
+
import { normalizeCaseObjective, parseIncidentUrlContext, } from "./case-incident-context.mjs";
|
|
5
4
|
import { selectExistingCase } from "./case-selection.mjs";
|
|
5
|
+
import { emptyCaseWorkflowMetadata, updateCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
|
|
6
|
+
import { readCaseWorkflowFields } from "./case-workflow-migration.mjs";
|
|
6
7
|
import { appendCaseAction, appendCaseAssertion, appendCaseClosure, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseObservedEffectReview, appendCaseSighting, buildCaseClosureLinkContext, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.mjs";
|
|
7
8
|
import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
|
|
8
|
-
import {
|
|
9
|
+
import { MAX_CASE_OBJECTIVE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
|
|
10
|
+
import { caseRecordVersionInput } from "./case-record-version-input.mjs";
|
|
9
11
|
import { readCaseAttachments } from "./case-attachment-model.mjs";
|
|
12
|
+
import { readCaseEvidenceLinks, appendCaseEvidenceLink, withdrawCaseEvidenceLink } from "./case-evidence-links.mjs";
|
|
10
13
|
import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
|
|
11
14
|
import { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, } from "./case-record-decisions.mjs";
|
|
12
15
|
import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, caseTimestampOrNull, deterministicId, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.mjs";
|
|
@@ -15,84 +18,7 @@ import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
|
|
|
15
18
|
import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
|
|
16
19
|
export { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, };
|
|
17
20
|
export { MAX_CASE_OBJECTIVE_LENGTH };
|
|
18
|
-
export
|
|
19
|
-
export const INCIDENT_CONTEXT_STATEMENT_PREFIX = 'Investigate incident URL: ';
|
|
20
|
-
const OBJECTIVE_PREFIX = 'Objective: ';
|
|
21
|
-
const RETENTION_SEPARATOR = ' | URL retained: ';
|
|
22
|
-
function boundedContextText(value, maximum) {
|
|
23
|
-
return typeof value === 'string'
|
|
24
|
-
? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
25
|
-
: '';
|
|
26
|
-
}
|
|
27
|
-
export function normalizeCaseObjective(value) {
|
|
28
|
-
return boundedContextText(value, MAX_CASE_OBJECTIVE_LENGTH);
|
|
29
|
-
}
|
|
30
|
-
export function parseIncidentUrlContext(value) {
|
|
31
|
-
const parsed = parseCredentialFreeHttpUrl(value, MAX_CASE_INCIDENT_URL_LENGTH);
|
|
32
|
-
if (!parsed)
|
|
33
|
-
return null;
|
|
34
|
-
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
35
|
-
const registrableDomain = canonicalRegistrableDomain(hostname);
|
|
36
|
-
if (!registrableDomain)
|
|
37
|
-
return null;
|
|
38
|
-
const exactUrl = parsed.toString();
|
|
39
|
-
if (exactUrl.length > MAX_CASE_INCIDENT_URL_LENGTH)
|
|
40
|
-
return null;
|
|
41
|
-
return Object.freeze({
|
|
42
|
-
exactUrl,
|
|
43
|
-
hostname,
|
|
44
|
-
registrableDomain,
|
|
45
|
-
originUrl: parsed.origin,
|
|
46
|
-
hasPath: parsed.pathname !== '/',
|
|
47
|
-
hasQuery: Boolean(parsed.search),
|
|
48
|
-
hasFragment: Boolean(parsed.hash),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
export function caseInvestigationContext(record) {
|
|
52
|
-
if (!record)
|
|
53
|
-
return null;
|
|
54
|
-
for (const assertion of [...record.assertions].reverse()) {
|
|
55
|
-
if (assertion.kind !== 'next_step' || assertion.state !== 'open'
|
|
56
|
-
|| !assertion.statement.startsWith(INCIDENT_CONTEXT_STATEMENT_PREFIX))
|
|
57
|
-
continue;
|
|
58
|
-
const incidentUrl = assertion.statement.slice(INCIDENT_CONTEXT_STATEMENT_PREFIX.length);
|
|
59
|
-
const parsed = parseIncidentUrlContext(incidentUrl);
|
|
60
|
-
if (!parsed || parsed.registrableDomain !== record.domain)
|
|
61
|
-
continue;
|
|
62
|
-
const rationale = assertion.rationale ?? '';
|
|
63
|
-
const separatorIndex = rationale.lastIndexOf(RETENTION_SEPARATOR);
|
|
64
|
-
const objective = separatorIndex > OBJECTIVE_PREFIX.length && rationale.startsWith(OBJECTIVE_PREFIX)
|
|
65
|
-
? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, separatorIndex))
|
|
66
|
-
: '';
|
|
67
|
-
const retention = separatorIndex >= 0 ? rationale.slice(separatorIndex + RETENTION_SEPARATOR.length) : '';
|
|
68
|
-
if (!objective || (retention !== 'exact' && retention !== 'origin_only'))
|
|
69
|
-
continue;
|
|
70
|
-
return Object.freeze({
|
|
71
|
-
objective,
|
|
72
|
-
incidentUrl: parsed.exactUrl,
|
|
73
|
-
urlRetention: retention,
|
|
74
|
-
assertionId: assertion.id,
|
|
75
|
-
updatedAt: assertion.updatedAt,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
export function caseInvestigationContextAssertion(input) {
|
|
81
|
-
const objective = normalizeCaseObjective(input.objective);
|
|
82
|
-
if (!objective)
|
|
83
|
-
throw new Error('Enter the investigation objective before retaining Incident context.');
|
|
84
|
-
const parsed = parseIncidentUrlContext(input.incidentUrl);
|
|
85
|
-
if (!parsed)
|
|
86
|
-
throw new Error(`Enter one absolute HTTP(S) Incident URL of at most ${MAX_CASE_INCIDENT_URL_LENGTH} characters without credentials.`);
|
|
87
|
-
const retention = input.retainExactUrl ? 'exact' : 'origin_only';
|
|
88
|
-
const retainedUrl = input.retainExactUrl ? parsed.exactUrl : parsed.originUrl;
|
|
89
|
-
return Object.freeze({
|
|
90
|
-
statement: `${INCIDENT_CONTEXT_STATEMENT_PREFIX}${retainedUrl}`,
|
|
91
|
-
rationale: `${OBJECTIVE_PREFIX}${objective}${RETENTION_SEPARATOR}${retention}`,
|
|
92
|
-
retainedUrl,
|
|
93
|
-
retention,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
21
|
+
export * from "./case-incident-context.mjs";
|
|
96
22
|
// ---------------------------------------------------------------------------
|
|
97
23
|
// Case normalization
|
|
98
24
|
// ---------------------------------------------------------------------------
|
|
@@ -130,31 +56,29 @@ function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, source
|
|
|
130
56
|
*/
|
|
131
57
|
export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
132
58
|
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
133
|
-
const
|
|
134
|
-
const domain = normalizeDomain(existing ? existing.domain :
|
|
59
|
+
const input = objectRecord(raw);
|
|
60
|
+
const domain = normalizeDomain(existing ? existing.domain : input.domain);
|
|
135
61
|
if (!domain)
|
|
136
62
|
return null;
|
|
63
|
+
const { record, timestampOptions } = caseRecordVersionInput(input, sourceVersion);
|
|
137
64
|
const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
|
|
138
65
|
const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
|
|
139
|
-
const timestampOptions = {
|
|
140
|
-
legacyTimestamps: sourceVersion != null && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
|
|
141
|
-
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
142
|
-
};
|
|
143
66
|
const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
|
|
144
67
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
145
68
|
const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
146
69
|
const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
|
|
147
70
|
const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
|
|
148
71
|
const sightingIds = new Set(sightings.map((item) => item.id));
|
|
149
|
-
const observedEffects = normalizeCaseObservedEffectHistory(
|
|
150
|
-
const closures = normalizeCaseClosureHistory(
|
|
72
|
+
const observedEffects = normalizeCaseObservedEffectHistory(record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
|
|
73
|
+
const closures = normalizeCaseClosureHistory(record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
|
|
151
74
|
const normalizedStatus = normalizeStatus(record.status);
|
|
152
75
|
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
153
76
|
const attachments = readCaseAttachments(record.attachments);
|
|
77
|
+
const evidenceLinks = readCaseEvidenceLinks(record.evidenceLinks);
|
|
154
78
|
return {
|
|
155
79
|
id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
|
|
156
80
|
domain,
|
|
157
|
-
title:
|
|
81
|
+
title: normalizeCaseObjective(record.title),
|
|
158
82
|
status: caseStatusRequiresClosure(normalizedStatus)
|
|
159
83
|
&& closures.records.length === 0 && !closures.preV13HistoryUnavailable
|
|
160
84
|
? 'reviewing'
|
|
@@ -162,7 +86,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
162
86
|
disposition: normalizeDisposition(record.disposition),
|
|
163
87
|
reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
|
|
164
88
|
brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
|
|
165
|
-
|
|
89
|
+
...readCaseWorkflowFields(record, domain, assertions, sourceVersion),
|
|
166
90
|
notes: normalizeNotes(record.notes, now, sourceVersion),
|
|
167
91
|
source: normalizeSource(record.source),
|
|
168
92
|
evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
|
|
@@ -176,6 +100,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
176
100
|
closures,
|
|
177
101
|
branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
|
|
178
102
|
...(attachments === undefined ? {} : { attachments }),
|
|
103
|
+
...(evidenceLinks === undefined ? {} : { evidenceLinks }),
|
|
179
104
|
createdAt,
|
|
180
105
|
updatedAt,
|
|
181
106
|
};
|
|
@@ -232,6 +157,7 @@ export function createCase(input, nowIso) {
|
|
|
232
157
|
reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
|
|
233
158
|
brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
|
|
234
159
|
tags: normalizeTags(input.tags),
|
|
160
|
+
workflowMetadata: updateCaseWorkflowMetadata(emptyCaseWorkflowMetadata(), input, domain, now),
|
|
235
161
|
notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
|
|
236
162
|
source,
|
|
237
163
|
evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
|
|
@@ -243,6 +169,7 @@ export function createCase(input, nowIso) {
|
|
|
243
169
|
decisions: input.decision !== undefined
|
|
244
170
|
? appendCaseDecision([], input.decision, now)
|
|
245
171
|
: [],
|
|
172
|
+
...(input.evidenceLink === undefined ? {} : { evidenceLinks: appendCaseEvidenceLink([], input.evidenceLink, evidencePins, now) }),
|
|
246
173
|
actions,
|
|
247
174
|
assertions,
|
|
248
175
|
manualTrail: input.trailEvent !== undefined
|
|
@@ -319,9 +246,21 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
319
246
|
const current = cases[index];
|
|
320
247
|
if (!current)
|
|
321
248
|
throw new Error('That case no longer exists.');
|
|
249
|
+
if (patch.evidenceLink !== undefined && patch.evidenceLinkWithdrawal !== undefined)
|
|
250
|
+
throw new TypeError('Record or withdraw one evidence relationship at a time.');
|
|
322
251
|
if (patch.title !== undefined && patch.expectedTitle !== undefined && patch.expectedTitle !== (current.title ?? '')) {
|
|
323
252
|
throw new Error('The incident title changed after this draft was started. Reload and review the current title before saving; your draft has not been applied.');
|
|
324
253
|
}
|
|
254
|
+
for (const [value, expected, actual, label] of [
|
|
255
|
+
[patch.status, patch.expectedStatus, current.status, 'status'],
|
|
256
|
+
[patch.disposition, patch.expectedDisposition, current.disposition, 'disposition'],
|
|
257
|
+
[patch.reviewReasonCode, patch.expectedReviewReasonCode, current.reviewReasonCode ?? null, 'review reason'],
|
|
258
|
+
[patch.tags, patch.expectedTags, current.tags, 'tags'],
|
|
259
|
+
]) {
|
|
260
|
+
if (value !== undefined && expected !== undefined && JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
261
|
+
throw new Error(`The Case ${label} changed after this edit was started. Reopen the Case to review the current value; your edit has not been applied.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
325
264
|
let notes = current.notes;
|
|
326
265
|
if (patch.note !== undefined) {
|
|
327
266
|
const body = normalizeNoteBody(patch.note);
|
|
@@ -398,6 +337,8 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
398
337
|
}
|
|
399
338
|
const record = {
|
|
400
339
|
...current,
|
|
340
|
+
workflowMetadata: updateCaseWorkflowMetadata(current.workflowMetadata
|
|
341
|
+
?? readCaseWorkflowFields(current, current.domain, current.assertions).workflowMetadata, patch, current.domain, now),
|
|
401
342
|
title: patch.title === undefined ? current.title ?? '' : normalizeCaseObjective(patch.title),
|
|
402
343
|
status: patch.closure !== undefined
|
|
403
344
|
? 'resolved'
|
|
@@ -413,6 +354,11 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
413
354
|
source,
|
|
414
355
|
evidenceHistory,
|
|
415
356
|
evidencePins,
|
|
357
|
+
...((patch.evidenceLink !== undefined || patch.evidenceLinkWithdrawal !== undefined) ? {
|
|
358
|
+
evidenceLinks: patch.evidenceLink !== undefined
|
|
359
|
+
? appendCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLink, evidencePins, now)
|
|
360
|
+
: withdrawCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLinkWithdrawal, now),
|
|
361
|
+
} : {}),
|
|
416
362
|
decisions,
|
|
417
363
|
actions,
|
|
418
364
|
assertions,
|
|
@@ -537,26 +483,7 @@ export function recordCaseInvestigationContext(cases, id, input, nowIso) {
|
|
|
537
483
|
if (!parsed || parsed.registrableDomain !== current.domain) {
|
|
538
484
|
throw new Error(`The Incident URL must belong to the Case domain ${current.domain}.`);
|
|
539
485
|
}
|
|
540
|
-
|
|
541
|
-
const existing = caseInvestigationContext(current);
|
|
542
|
-
return updateCase(cases, id, existing
|
|
543
|
-
? {
|
|
544
|
-
assertionUpdate: {
|
|
545
|
-
id: existing.assertionId,
|
|
546
|
-
statement: context.statement,
|
|
547
|
-
rationale: context.rationale,
|
|
548
|
-
state: 'open',
|
|
549
|
-
},
|
|
550
|
-
}
|
|
551
|
-
: {
|
|
552
|
-
assertion: {
|
|
553
|
-
kind: 'next_step',
|
|
554
|
-
statement: context.statement,
|
|
555
|
-
rationale: context.rationale,
|
|
556
|
-
evidenceRelations: [],
|
|
557
|
-
state: 'open',
|
|
558
|
-
},
|
|
559
|
-
}, nowIso);
|
|
486
|
+
return updateCase(cases, id, { investigationContext: input }, nowIso);
|
|
560
487
|
}
|
|
561
488
|
export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
562
489
|
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
@@ -578,7 +505,8 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
|
578
505
|
collectionDepth: input.collectionDepth,
|
|
579
506
|
observationHostname: input.observationHostname,
|
|
580
507
|
completeness: input.completeness,
|
|
581
|
-
truncated:
|
|
508
|
+
truncated: input.comparisonTruncated === true
|
|
509
|
+
|| (typeof input.comparisonSummary === 'string' && input.comparisonSummary.length > MAX_RESPONSE_VALUE_LENGTH),
|
|
582
510
|
limitations: input.limitations,
|
|
583
511
|
},
|
|
584
512
|
}, now);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
1
|
+
import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
2
2
|
import { caseStatusIsClosed } from "./case-record-decisions.mjs";
|
|
3
3
|
const PRESERVE = Object.freeze({
|
|
4
4
|
durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
|
|
@@ -86,6 +86,12 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
86
86
|
audienceExclusions: { public: { label: 'Brand Profile references', order: 2 } },
|
|
87
87
|
}),
|
|
88
88
|
tags: fieldRule('tags', PRESERVE, (record) => [...record.tags]),
|
|
89
|
+
workflowMetadata: fieldRule('workflowMetadata', PUBLIC_REDACT, (record, profile) => (record.workflowMetadata === undefined ? undefined : profile === 'public'
|
|
90
|
+
? { types: [...record.workflowMetadata.types], incidentTargets: [], investigationContext: null }
|
|
91
|
+
: structuredClone(record.workflowMetadata)), {
|
|
92
|
+
nestedSensitiveFields: ['incidentTargets', 'investigationContext'],
|
|
93
|
+
audienceExclusions: { public: { label: 'Private incident links and investigation context', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } },
|
|
94
|
+
}),
|
|
89
95
|
notes: fieldRule('notes', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public'
|
|
90
96
|
? []
|
|
91
97
|
: record.notes.map((item) => ({ ...item }))), {
|
|
@@ -97,6 +103,7 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
97
103
|
source: preservedField('source'),
|
|
98
104
|
evidenceHistory: fieldRule('evidenceHistory', PRESERVE, (record) => structuredClone(record.evidenceHistory)),
|
|
99
105
|
evidencePins: fieldRule('evidencePins', PRESERVE, (record) => structuredClone(record.evidencePins)),
|
|
106
|
+
evidenceLinks: fieldRule('evidenceLinks', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? undefined : structuredClone(record.evidenceLinks)), { audienceExclusions: { public: { label: 'Analyst evidence relationships', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } } }),
|
|
100
107
|
decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
|
|
101
108
|
actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
|
|
102
109
|
? []
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, PUBLISHED_V2_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
|
|
2
|
+
/** Adapt declared historical fields before ordinary current-record recovery. */
|
|
3
|
+
export function caseRecordVersionInput(record, sourceVersion) {
|
|
4
|
+
const declared = sourceVersion != null;
|
|
5
|
+
if (record.evidenceLinks !== undefined && declared && sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION) {
|
|
6
|
+
throw new TypeError('Evidence relationships require the current Case schema.');
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
record: declared && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION ? {
|
|
10
|
+
...record,
|
|
11
|
+
...(sourceVersion < PUBLISHED_V2_CASE_SCHEMA_VERSION ? { observedEffects: undefined, closures: undefined } : {}),
|
|
12
|
+
title: '',
|
|
13
|
+
} : record,
|
|
14
|
+
timestampOptions: {
|
|
15
|
+
legacyTimestamps: declared && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
|
|
16
|
+
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|