@slicedearth/whoisleuth-cli 2.3.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 +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- 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 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -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 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- 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-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- 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/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- 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-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -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-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -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/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- 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 +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- 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-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- 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/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import {
|
|
3
|
+
import { 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
4
|
import { dispositionLabel, isValidDisposition, isValidStatus, statusLabel, } from "./case-record-decisions.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { PUBLISHED_V2_3_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
7
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
7
8
|
// Forward-version policy (two distinct guarantees):
|
|
8
9
|
// - A locally-stored envelope that declares a version greater than this is
|
|
9
10
|
// still read best-effort on load (known fields kept, unknown dropped), but
|
|
@@ -163,7 +164,7 @@ export function caseTimestampOrNull(value, sourceVersion) {
|
|
|
163
164
|
return explicit;
|
|
164
165
|
return sourceVersion !== undefined
|
|
165
166
|
&& sourceVersion !== null
|
|
166
|
-
&& sourceVersion <
|
|
167
|
+
&& sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION
|
|
167
168
|
? normalizeLegacyIsoTimestamp(value)
|
|
168
169
|
: null;
|
|
169
170
|
}
|
|
@@ -1,103 +1,24 @@
|
|
|
1
1
|
// Pure, framework-neutral analyst-case records, evidence histories, bounded
|
|
2
2
|
// record normalization, and analyst updates.
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeCaseObjective, parseIncidentUrlContext, } from "./case-incident-context.mjs";
|
|
4
|
+
import { selectExistingCase } from "./case-selection.mjs";
|
|
5
|
+
import { emptyCaseWorkflowMetadata, updateCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
|
|
6
|
+
import { readCaseWorkflowFields } from "./case-workflow-migration.mjs";
|
|
4
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";
|
|
5
|
-
import {
|
|
8
|
+
import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
|
|
9
|
+
import { MAX_CASE_OBJECTIVE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
|
|
10
|
+
import { caseRecordVersionInput } from "./case-record-version-input.mjs";
|
|
11
|
+
import { readCaseAttachments } from "./case-attachment-model.mjs";
|
|
12
|
+
import { readCaseEvidenceLinks, appendCaseEvidenceLink, withdrawCaseEvidenceLink } from "./case-evidence-links.mjs";
|
|
13
|
+
import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
|
|
6
14
|
import { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, } from "./case-record-decisions.mjs";
|
|
7
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";
|
|
8
16
|
import { assertCaseBrandProfileIds, normalizeCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
|
|
9
17
|
import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
|
|
10
18
|
import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
|
|
11
19
|
export { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, };
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
export const INCIDENT_CONTEXT_STATEMENT_PREFIX = 'Investigate incident URL: ';
|
|
15
|
-
const OBJECTIVE_PREFIX = 'Objective: ';
|
|
16
|
-
const RETENTION_SEPARATOR = ' | URL retained: ';
|
|
17
|
-
function boundedContextText(value, maximum) {
|
|
18
|
-
return typeof value === 'string'
|
|
19
|
-
? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
20
|
-
: '';
|
|
21
|
-
}
|
|
22
|
-
export function normalizeCaseObjective(value) {
|
|
23
|
-
return boundedContextText(value, MAX_CASE_OBJECTIVE_LENGTH);
|
|
24
|
-
}
|
|
25
|
-
export function parseIncidentUrlContext(value) {
|
|
26
|
-
if (typeof value !== 'string' || !value || value.length > MAX_CASE_INCIDENT_URL_LENGTH)
|
|
27
|
-
return null;
|
|
28
|
-
if (/[\u0000-\u001f\u007f]/u.test(value) || value.trim() !== value)
|
|
29
|
-
return null;
|
|
30
|
-
let parsed;
|
|
31
|
-
try {
|
|
32
|
-
parsed = new URL(value);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname)
|
|
38
|
-
return null;
|
|
39
|
-
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
40
|
-
const registrableDomain = canonicalRegistrableDomain(hostname);
|
|
41
|
-
if (!registrableDomain)
|
|
42
|
-
return null;
|
|
43
|
-
const exactUrl = parsed.toString();
|
|
44
|
-
if (exactUrl.length > MAX_CASE_INCIDENT_URL_LENGTH)
|
|
45
|
-
return null;
|
|
46
|
-
return Object.freeze({
|
|
47
|
-
exactUrl,
|
|
48
|
-
hostname,
|
|
49
|
-
registrableDomain,
|
|
50
|
-
originUrl: parsed.origin,
|
|
51
|
-
hasPath: parsed.pathname !== '/',
|
|
52
|
-
hasQuery: Boolean(parsed.search),
|
|
53
|
-
hasFragment: Boolean(parsed.hash),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
export function caseInvestigationContext(record) {
|
|
57
|
-
if (!record)
|
|
58
|
-
return null;
|
|
59
|
-
for (const assertion of [...record.assertions].reverse()) {
|
|
60
|
-
if (assertion.kind !== 'next_step' || assertion.state !== 'open'
|
|
61
|
-
|| !assertion.statement.startsWith(INCIDENT_CONTEXT_STATEMENT_PREFIX))
|
|
62
|
-
continue;
|
|
63
|
-
const incidentUrl = assertion.statement.slice(INCIDENT_CONTEXT_STATEMENT_PREFIX.length);
|
|
64
|
-
const parsed = parseIncidentUrlContext(incidentUrl);
|
|
65
|
-
if (!parsed || parsed.registrableDomain !== record.domain)
|
|
66
|
-
continue;
|
|
67
|
-
const rationale = assertion.rationale ?? '';
|
|
68
|
-
const separatorIndex = rationale.lastIndexOf(RETENTION_SEPARATOR);
|
|
69
|
-
const objective = separatorIndex > OBJECTIVE_PREFIX.length && rationale.startsWith(OBJECTIVE_PREFIX)
|
|
70
|
-
? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, separatorIndex))
|
|
71
|
-
: '';
|
|
72
|
-
const retention = separatorIndex >= 0 ? rationale.slice(separatorIndex + RETENTION_SEPARATOR.length) : '';
|
|
73
|
-
if (!objective || (retention !== 'exact' && retention !== 'origin_only'))
|
|
74
|
-
continue;
|
|
75
|
-
return Object.freeze({
|
|
76
|
-
objective,
|
|
77
|
-
incidentUrl: parsed.exactUrl,
|
|
78
|
-
urlRetention: retention,
|
|
79
|
-
assertionId: assertion.id,
|
|
80
|
-
updatedAt: assertion.updatedAt,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
export function caseInvestigationContextAssertion(input) {
|
|
86
|
-
const objective = normalizeCaseObjective(input.objective);
|
|
87
|
-
if (!objective)
|
|
88
|
-
throw new Error('Enter the investigation objective before retaining Incident context.');
|
|
89
|
-
const parsed = parseIncidentUrlContext(input.incidentUrl);
|
|
90
|
-
if (!parsed)
|
|
91
|
-
throw new Error(`Enter one absolute HTTP(S) Incident URL of at most ${MAX_CASE_INCIDENT_URL_LENGTH} characters without credentials.`);
|
|
92
|
-
const retention = input.retainExactUrl ? 'exact' : 'origin_only';
|
|
93
|
-
const retainedUrl = input.retainExactUrl ? parsed.exactUrl : parsed.originUrl;
|
|
94
|
-
return Object.freeze({
|
|
95
|
-
statement: `${INCIDENT_CONTEXT_STATEMENT_PREFIX}${retainedUrl}`,
|
|
96
|
-
rationale: `${OBJECTIVE_PREFIX}${objective}${RETENTION_SEPARATOR}${retention}`,
|
|
97
|
-
retainedUrl,
|
|
98
|
-
retention,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
20
|
+
export { MAX_CASE_OBJECTIVE_LENGTH };
|
|
21
|
+
export * from "./case-incident-context.mjs";
|
|
101
22
|
// ---------------------------------------------------------------------------
|
|
102
23
|
// Case normalization
|
|
103
24
|
// ---------------------------------------------------------------------------
|
|
@@ -135,29 +56,29 @@ function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, source
|
|
|
135
56
|
*/
|
|
136
57
|
export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
137
58
|
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
138
|
-
const
|
|
139
|
-
const domain = normalizeDomain(existing ? existing.domain :
|
|
59
|
+
const input = objectRecord(raw);
|
|
60
|
+
const domain = normalizeDomain(existing ? existing.domain : input.domain);
|
|
140
61
|
if (!domain)
|
|
141
62
|
return null;
|
|
63
|
+
const { record, timestampOptions } = caseRecordVersionInput(input, sourceVersion);
|
|
142
64
|
const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
|
|
143
65
|
const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
|
|
144
|
-
const timestampOptions = {
|
|
145
|
-
legacyTimestamps: sourceVersion != null && sourceVersion < CASE_SCHEMA_VERSION,
|
|
146
|
-
...(sourceVersion === undefined ? {} : { sourceVersion }),
|
|
147
|
-
};
|
|
148
66
|
const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
|
|
149
67
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
150
68
|
const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
151
69
|
const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
|
|
152
70
|
const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
|
|
153
71
|
const sightingIds = new Set(sightings.map((item) => item.id));
|
|
154
|
-
const observedEffects = normalizeCaseObservedEffectHistory(
|
|
155
|
-
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));
|
|
156
74
|
const normalizedStatus = normalizeStatus(record.status);
|
|
157
75
|
const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
|
|
76
|
+
const attachments = readCaseAttachments(record.attachments);
|
|
77
|
+
const evidenceLinks = readCaseEvidenceLinks(record.evidenceLinks);
|
|
158
78
|
return {
|
|
159
79
|
id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
|
|
160
80
|
domain,
|
|
81
|
+
title: normalizeCaseObjective(record.title),
|
|
161
82
|
status: caseStatusRequiresClosure(normalizedStatus)
|
|
162
83
|
&& closures.records.length === 0 && !closures.preV13HistoryUnavailable
|
|
163
84
|
? 'reviewing'
|
|
@@ -165,7 +86,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
165
86
|
disposition: normalizeDisposition(record.disposition),
|
|
166
87
|
reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
|
|
167
88
|
brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
|
|
168
|
-
|
|
89
|
+
...readCaseWorkflowFields(record, domain, assertions, sourceVersion),
|
|
169
90
|
notes: normalizeNotes(record.notes, now, sourceVersion),
|
|
170
91
|
source: normalizeSource(record.source),
|
|
171
92
|
evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
|
|
@@ -178,6 +99,8 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
178
99
|
observedEffects,
|
|
179
100
|
closures,
|
|
180
101
|
branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
|
|
102
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
103
|
+
...(evidenceLinks === undefined ? {} : { evidenceLinks }),
|
|
181
104
|
createdAt,
|
|
182
105
|
updatedAt,
|
|
183
106
|
};
|
|
@@ -213,6 +136,12 @@ export function createCase(input, nowIso) {
|
|
|
213
136
|
const observedEffects = input.observedEffectReview !== undefined
|
|
214
137
|
? appendCaseObservedEffectReview(normalizeCaseObservedEffectHistory(undefined, now), input.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)))
|
|
215
138
|
: normalizeCaseObservedEffectHistory(undefined, now);
|
|
139
|
+
for (const review of observedEffects.reviews) {
|
|
140
|
+
if (!review.recheck)
|
|
141
|
+
continue;
|
|
142
|
+
assertCurrentRecheckQuestion(review.recheck, assertions);
|
|
143
|
+
assertRecheckNonReproduction(review.state, review.recheck, review.completeness, evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), review.observedAt);
|
|
144
|
+
}
|
|
216
145
|
const closures = input.closure !== undefined
|
|
217
146
|
? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions)
|
|
218
147
|
: normalizeCaseClosureHistory(undefined, now);
|
|
@@ -222,11 +151,13 @@ export function createCase(input, nowIso) {
|
|
|
222
151
|
return {
|
|
223
152
|
id: makeId(),
|
|
224
153
|
domain,
|
|
154
|
+
title: normalizeCaseObjective(input.title),
|
|
225
155
|
status: input.closure !== undefined ? 'resolved' : normalizeStatus(input.status),
|
|
226
156
|
disposition: normalizeDisposition(input.disposition),
|
|
227
157
|
reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
|
|
228
158
|
brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
|
|
229
159
|
tags: normalizeTags(input.tags),
|
|
160
|
+
workflowMetadata: updateCaseWorkflowMetadata(emptyCaseWorkflowMetadata(), input, domain, now),
|
|
230
161
|
notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
|
|
231
162
|
source,
|
|
232
163
|
evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
|
|
@@ -238,6 +169,7 @@ export function createCase(input, nowIso) {
|
|
|
238
169
|
decisions: input.decision !== undefined
|
|
239
170
|
? appendCaseDecision([], input.decision, now)
|
|
240
171
|
: [],
|
|
172
|
+
...(input.evidenceLink === undefined ? {} : { evidenceLinks: appendCaseEvidenceLink([], input.evidenceLink, evidencePins, now) }),
|
|
241
173
|
actions,
|
|
242
174
|
assertions,
|
|
243
175
|
manualTrail: input.trailEvent !== undefined
|
|
@@ -254,25 +186,46 @@ export function createCase(input, nowIso) {
|
|
|
254
186
|
};
|
|
255
187
|
}
|
|
256
188
|
/**
|
|
257
|
-
* Opens
|
|
189
|
+
* Opens an explicitly selected or unambiguous Case, or creates one. Returns a new array
|
|
258
190
|
* (callers persist it) plus the resolved record and whether it was created.
|
|
259
191
|
* @param {CaseRecord[]} cases
|
|
260
192
|
* @param {{ domain: unknown, status?: unknown, disposition?: unknown, source?: unknown, tags?: unknown, evidence?: unknown, note?: unknown }} input
|
|
261
193
|
* @param {string} [nowIso]
|
|
262
194
|
* @returns {{ cases: CaseRecord[], record: CaseRecord, created: boolean }}
|
|
263
195
|
*/
|
|
264
|
-
export function openOrCreateCase(cases, input, nowIso) {
|
|
196
|
+
export function openOrCreateCase(cases, input, nowIso, selection = {}) {
|
|
265
197
|
const domain = normalizeDomain(input.domain);
|
|
266
198
|
if (!domain)
|
|
267
199
|
throw new Error('A valid domain is required to open a case.');
|
|
268
|
-
const existing = cases
|
|
200
|
+
const existing = selectExistingCase(cases, domain, selection);
|
|
269
201
|
if (existing)
|
|
270
202
|
return { cases, record: existing, created: false };
|
|
271
203
|
if (cases.length >= MAX_CASES)
|
|
272
204
|
throw new Error(`Cases are limited to ${MAX_CASES}. Delete or export some first.`);
|
|
273
205
|
const record = createCase({ ...input, domain }, nowIso);
|
|
206
|
+
if (cases.some(existingCase => existingCase.id === record.id))
|
|
207
|
+
throw new Error('A unique Case ID could not be allocated. No data was changed; try again.');
|
|
274
208
|
return { cases: [record, ...cases], record, created: true };
|
|
275
209
|
}
|
|
210
|
+
/** Start a separate incident; reuse only the deliberately selected observation. */
|
|
211
|
+
export function createCaseIncident(cases, input, nowIso) {
|
|
212
|
+
const domain = normalizeDomain(input.domain);
|
|
213
|
+
const title = normalizeCaseObjective(input.title);
|
|
214
|
+
if (!domain)
|
|
215
|
+
throw new Error('Enter a valid domain for the incident.');
|
|
216
|
+
if (!title)
|
|
217
|
+
throw new Error('Enter a title that distinguishes this incident.');
|
|
218
|
+
if (typeof input.title === 'string' && input.title.trim().length > MAX_CASE_OBJECTIVE_LENGTH)
|
|
219
|
+
throw new Error(`Incident titles are limited to ${MAX_CASE_OBJECTIVE_LENGTH} characters.`);
|
|
220
|
+
let evidence;
|
|
221
|
+
if (input.reuse) {
|
|
222
|
+
const source = cases.find(record => record.id === input.reuse?.caseId && record.domain === domain);
|
|
223
|
+
evidence = source?.evidenceHistory.find(snapshot => snapshot.id === input.reuse?.snapshotId);
|
|
224
|
+
if (!evidence)
|
|
225
|
+
throw new Error('The selected source observation is no longer available for this domain. No Case was created.');
|
|
226
|
+
}
|
|
227
|
+
return openOrCreateCase(cases, { domain, title, ...(evidence ? { evidence: structuredClone(evidence) } : {}) }, nowIso, { newIncident: true });
|
|
228
|
+
}
|
|
276
229
|
/**
|
|
277
230
|
* Applies a partial update to one case by id, bumping updatedAt. A `note` in
|
|
278
231
|
* the patch is appended (respecting the per-case note bound); a `tags` array
|
|
@@ -293,6 +246,21 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
293
246
|
const current = cases[index];
|
|
294
247
|
if (!current)
|
|
295
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.');
|
|
251
|
+
if (patch.title !== undefined && patch.expectedTitle !== undefined && patch.expectedTitle !== (current.title ?? '')) {
|
|
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.');
|
|
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
|
+
}
|
|
296
264
|
let notes = current.notes;
|
|
297
265
|
if (patch.note !== undefined) {
|
|
298
266
|
const body = normalizeNoteBody(patch.note);
|
|
@@ -352,6 +320,12 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
352
320
|
}
|
|
353
321
|
let observedEffects = normalizeCaseObservedEffectHistory(current.observedEffects, current.updatedAt, pinIds, new Set(sightings.map((item) => item.id)));
|
|
354
322
|
if (patch.observedEffectReview !== undefined) {
|
|
323
|
+
const review = objectRecord(patch.observedEffectReview);
|
|
324
|
+
const context = readCaseRecheckAnswerContext(review.recheck);
|
|
325
|
+
if (context) {
|
|
326
|
+
assertCurrentRecheckQuestion(context, assertions);
|
|
327
|
+
assertRecheckNonReproduction(review.state, context, String(review.completeness), evidencePins, evidencePins.find(pin => pin.id === review.evidencePinId), typeof review.observedAt === 'string' ? review.observedAt : null);
|
|
328
|
+
}
|
|
355
329
|
observedEffects = appendCaseObservedEffectReview(observedEffects, patch.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)));
|
|
356
330
|
}
|
|
357
331
|
let closures = normalizeCaseClosureHistory(current.closures, current.updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), {}, buildCaseClosureLinkContext(observedEffects, actions));
|
|
@@ -363,6 +337,9 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
363
337
|
}
|
|
364
338
|
const record = {
|
|
365
339
|
...current,
|
|
340
|
+
workflowMetadata: updateCaseWorkflowMetadata(current.workflowMetadata
|
|
341
|
+
?? readCaseWorkflowFields(current, current.domain, current.assertions).workflowMetadata, patch, current.domain, now),
|
|
342
|
+
title: patch.title === undefined ? current.title ?? '' : normalizeCaseObjective(patch.title),
|
|
366
343
|
status: patch.closure !== undefined
|
|
367
344
|
? 'resolved'
|
|
368
345
|
: patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
|
|
@@ -377,6 +354,11 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
377
354
|
source,
|
|
378
355
|
evidenceHistory,
|
|
379
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
|
+
} : {}),
|
|
380
362
|
decisions,
|
|
381
363
|
actions,
|
|
382
364
|
assertions,
|
|
@@ -394,8 +376,8 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
394
376
|
}
|
|
395
377
|
/**
|
|
396
378
|
* Records one reviewed conclusion as a single pure Case mutation. The selected
|
|
397
|
-
* observations become bounded Case pins
|
|
398
|
-
* decision cannot point at absent evidence. Counterevidence is retained as a
|
|
379
|
+
* observations become bounded Case pins or reference existing retained pins,
|
|
380
|
+
* so the decision cannot point at absent evidence. Counterevidence is retained as a
|
|
399
381
|
* separate resolved contradiction assertion rather than being hidden inside a
|
|
400
382
|
* favourable disposition.
|
|
401
383
|
*/
|
|
@@ -422,14 +404,36 @@ export function recordCaseConclusion(cases, id, input, nowIso) {
|
|
|
422
404
|
if (!current)
|
|
423
405
|
throw new Error('That case no longer exists.');
|
|
424
406
|
const existingPinIds = new Set(current.evidencePins.map((pin) => pin.id));
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
407
|
+
const selectedExisting = new Set();
|
|
408
|
+
for (const item of input.evidence) {
|
|
409
|
+
if (Object.hasOwn(item, 'pin') === Object.hasOwn(item, 'pinId'))
|
|
410
|
+
throw new Error('Select one new pin or one retained pin ID for each conclusion fact.');
|
|
411
|
+
if (Object.hasOwn(item, 'pinId')) {
|
|
412
|
+
if (typeof item.pinId !== 'string' || !existingPinIds.has(item.pinId))
|
|
413
|
+
throw new Error('The selected conclusion pin is no longer retained in this Case.');
|
|
414
|
+
if (selectedExisting.has(item.pinId))
|
|
415
|
+
throw new Error('Select each retained conclusion pin once.');
|
|
416
|
+
selectedExisting.add(item.pinId);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const newEvidence = input.evidence.filter(item => Object.hasOwn(item, 'pin'));
|
|
420
|
+
const withPins = newEvidence.length
|
|
421
|
+
? updateCase(cases, id, { evidencePins: newEvidence.map(item => item.pin) }, now)
|
|
422
|
+
: { cases, record: current };
|
|
428
423
|
const addedPins = withPins.record.evidencePins.filter((pin) => !existingPinIds.has(pin.id));
|
|
429
|
-
if (addedPins.length !==
|
|
424
|
+
if (addedPins.length !== newEvidence.length) {
|
|
430
425
|
throw new Error('The selected conclusion evidence could not be retained completely.');
|
|
431
426
|
}
|
|
432
|
-
|
|
427
|
+
let nextAdded = 0;
|
|
428
|
+
const selectedPins = input.evidence.map(item => {
|
|
429
|
+
const pin = Object.hasOwn(item, 'pinId')
|
|
430
|
+
? withPins.record.evidencePins.find(pin => pin.id === item.pinId)
|
|
431
|
+
: addedPins[nextAdded++];
|
|
432
|
+
if (!pin)
|
|
433
|
+
throw new Error('The selected conclusion evidence could not be retained completely.');
|
|
434
|
+
return pin;
|
|
435
|
+
});
|
|
436
|
+
const supportingPinIds = selectedPins.flatMap((pin, index) => (input.evidence[index]?.stance === 'supports' ? [pin.id] : []));
|
|
433
437
|
let concluded = updateCase(withPins.cases, id, {
|
|
434
438
|
disposition,
|
|
435
439
|
reviewReasonCode,
|
|
@@ -439,7 +443,7 @@ export function recordCaseConclusion(cases, id, input, nowIso) {
|
|
|
439
443
|
evidencePinIds: supportingPinIds,
|
|
440
444
|
},
|
|
441
445
|
}, now);
|
|
442
|
-
const contradictionRelations =
|
|
446
|
+
const contradictionRelations = selectedPins.flatMap((pin, index) => (input.evidence[index]?.stance === 'contradicts'
|
|
443
447
|
? [{ evidencePinId: pin.id, stance: 'contradicts' }]
|
|
444
448
|
: []));
|
|
445
449
|
if (contradictionRelations.length) {
|
|
@@ -454,7 +458,7 @@ export function recordCaseConclusion(cases, id, input, nowIso) {
|
|
|
454
458
|
},
|
|
455
459
|
}, now);
|
|
456
460
|
}
|
|
457
|
-
const unresolvedRelations =
|
|
461
|
+
const unresolvedRelations = selectedPins.flatMap((pin, index) => (input.evidence[index]?.stance === 'unresolved'
|
|
458
462
|
? [{ evidencePinId: pin.id, stance: 'unresolved' }]
|
|
459
463
|
: []));
|
|
460
464
|
if (unresolvedRelations.length) {
|
|
@@ -479,32 +483,15 @@ export function recordCaseInvestigationContext(cases, id, input, nowIso) {
|
|
|
479
483
|
if (!parsed || parsed.registrableDomain !== current.domain) {
|
|
480
484
|
throw new Error(`The Incident URL must belong to the Case domain ${current.domain}.`);
|
|
481
485
|
}
|
|
482
|
-
|
|
483
|
-
const existing = caseInvestigationContext(current);
|
|
484
|
-
return updateCase(cases, id, existing
|
|
485
|
-
? {
|
|
486
|
-
assertionUpdate: {
|
|
487
|
-
id: existing.assertionId,
|
|
488
|
-
statement: context.statement,
|
|
489
|
-
rationale: context.rationale,
|
|
490
|
-
state: 'open',
|
|
491
|
-
},
|
|
492
|
-
}
|
|
493
|
-
: {
|
|
494
|
-
assertion: {
|
|
495
|
-
kind: 'next_step',
|
|
496
|
-
statement: context.statement,
|
|
497
|
-
rationale: context.rationale,
|
|
498
|
-
evidenceRelations: [],
|
|
499
|
-
state: 'open',
|
|
500
|
-
},
|
|
501
|
-
}, nowIso);
|
|
486
|
+
return updateCase(cases, id, { investigationContext: input }, nowIso);
|
|
502
487
|
}
|
|
503
488
|
export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
504
489
|
const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
|
|
505
490
|
const current = cases.find((item) => item.id === id);
|
|
506
491
|
if (!current)
|
|
507
492
|
throw new Error('That case no longer exists.');
|
|
493
|
+
if (input.recheck && input.observationHostname !== input.recheck.targetHostname)
|
|
494
|
+
throw new Error('This recheck concerns a different or unknown hostname. Recollect the saved question target before recording its answer.');
|
|
508
495
|
const beforePinIds = new Set(current.evidencePins.map((pin) => pin.id));
|
|
509
496
|
const withPin = updateCase(cases, id, {
|
|
510
497
|
evidencePin: {
|
|
@@ -516,8 +503,10 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
|
516
503
|
sourceState: 'reviewed',
|
|
517
504
|
observedAt: input.observedAt,
|
|
518
505
|
collectionDepth: input.collectionDepth,
|
|
506
|
+
observationHostname: input.observationHostname,
|
|
519
507
|
completeness: input.completeness,
|
|
520
|
-
truncated:
|
|
508
|
+
truncated: input.comparisonTruncated === true
|
|
509
|
+
|| (typeof input.comparisonSummary === 'string' && input.comparisonSummary.length > MAX_RESPONSE_VALUE_LENGTH),
|
|
521
510
|
limitations: input.limitations,
|
|
522
511
|
},
|
|
523
512
|
}, now);
|
|
@@ -534,6 +523,7 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
|
|
|
534
523
|
evidencePinId: comparisonPin.id,
|
|
535
524
|
followUpAt: input.followUpAt,
|
|
536
525
|
limitations: input.limitations,
|
|
526
|
+
...(input.recheck ? { recheck: input.recheck } : {}),
|
|
537
527
|
},
|
|
538
528
|
}, now);
|
|
539
529
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PUBLIC_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',
|
|
@@ -71,6 +71,12 @@ function publicClosureHistory(preV13HistoryUnavailable) {
|
|
|
71
71
|
const CASE_FIELD_RULES = Object.freeze({
|
|
72
72
|
id: preservedField('id'),
|
|
73
73
|
domain: preservedField('domain'),
|
|
74
|
+
title: fieldRule('title', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? '' : record.title ?? ''), {
|
|
75
|
+
audienceExclusions: {
|
|
76
|
+
trusted: { label: 'Case titles', order: 1, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
77
|
+
public: { label: 'Case titles', order: 1, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
74
80
|
status: fieldRule('status', PUBLIC_TRANSFORM, (record, profile) => (profile === 'public' && caseStatusIsClosed(record.status) && record.closures.records.length
|
|
75
81
|
? 'reviewing'
|
|
76
82
|
: record.status)),
|
|
@@ -80,6 +86,12 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
80
86
|
audienceExclusions: { public: { label: 'Brand Profile references', order: 2 } },
|
|
81
87
|
}),
|
|
82
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
|
+
}),
|
|
83
95
|
notes: fieldRule('notes', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public'
|
|
84
96
|
? []
|
|
85
97
|
: record.notes.map((item) => ({ ...item }))), {
|
|
@@ -91,6 +103,7 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
91
103
|
source: preservedField('source'),
|
|
92
104
|
evidenceHistory: fieldRule('evidenceHistory', PRESERVE, (record) => structuredClone(record.evidenceHistory)),
|
|
93
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 } } }),
|
|
94
107
|
decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
|
|
95
108
|
actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
|
|
96
109
|
? []
|
|
@@ -133,6 +146,12 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
133
146
|
branches: fieldRule('branches', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? [] : structuredClone(record.branches ?? [])), {
|
|
134
147
|
audienceExclusions: { public: { label: 'Investigation branches', order: 5 } },
|
|
135
148
|
}),
|
|
149
|
+
attachments: fieldRule('attachments', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? undefined : structuredClone(record.attachments)), {
|
|
150
|
+
audienceExclusions: {
|
|
151
|
+
trusted: { label: 'Retained file references and provenance', order: 5, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
152
|
+
public: { label: 'Retained file references and provenance', order: 5, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
136
155
|
createdAt: preservedField('createdAt'),
|
|
137
156
|
updatedAt: preservedField('updatedAt'),
|
|
138
157
|
});
|
|
@@ -140,10 +159,13 @@ function projectCase(record, profile) {
|
|
|
140
159
|
const projected = {};
|
|
141
160
|
for (const key of Object.keys(CASE_FIELD_RULES)) {
|
|
142
161
|
const rule = CASE_FIELD_RULES[key];
|
|
162
|
+
const value = rule.value(record, profile);
|
|
163
|
+
if (value === undefined)
|
|
164
|
+
continue;
|
|
143
165
|
Object.defineProperty(projected, key, {
|
|
144
166
|
configurable: true,
|
|
145
167
|
enumerable: true,
|
|
146
|
-
value
|
|
168
|
+
value,
|
|
147
169
|
writable: true,
|
|
148
170
|
});
|
|
149
171
|
}
|
|
@@ -167,38 +189,26 @@ const OUTSIDE_SCHEMA_EXCLUSIONS = Object.freeze({
|
|
|
167
189
|
trusted: Object.freeze({ label: 'Raw upstream payloads and credentials', order: 4 }),
|
|
168
190
|
public: Object.freeze({ label: 'Raw upstream payloads and credentials', order: 7 }),
|
|
169
191
|
});
|
|
170
|
-
function currentAudienceExclusions(audience) {
|
|
192
|
+
function currentAudienceExclusions(audience, caseVersion) {
|
|
171
193
|
const entries = [
|
|
172
194
|
...Object.values(CASE_FIELD_RULES).flatMap((rule) => (rule.audienceExclusions[audience] ? [rule.audienceExclusions[audience]] : [])),
|
|
173
195
|
OUTSIDE_SCHEMA_EXCLUSIONS[audience],
|
|
174
196
|
];
|
|
175
197
|
const orderByLabel = new Map();
|
|
176
|
-
const labelByOrder = new Map();
|
|
177
198
|
for (const entry of entries) {
|
|
199
|
+
if (entry.sinceVersion !== undefined && caseVersion < entry.sinceVersion)
|
|
200
|
+
continue;
|
|
178
201
|
if (!entry.label || entry.label.length > 160 || entry.label.trim() !== entry.label
|
|
179
202
|
|| !Number.isSafeInteger(entry.order) || entry.order < 1 || entry.order > 64) {
|
|
180
203
|
throw new TypeError('Case audience exclusion metadata is malformed or exceeds its bound.');
|
|
181
204
|
}
|
|
182
205
|
const prior = orderByLabel.get(entry.label);
|
|
183
|
-
|
|
184
|
-
throw new TypeError(`Case audience exclusion ${entry.label} has conflicting presentation order.`);
|
|
185
|
-
}
|
|
186
|
-
const priorLabel = labelByOrder.get(entry.order);
|
|
187
|
-
if (priorLabel !== undefined && priorLabel !== entry.label) {
|
|
188
|
-
throw new TypeError(`Case audience exclusions ${priorLabel} and ${entry.label} share a presentation order.`);
|
|
189
|
-
}
|
|
190
|
-
orderByLabel.set(entry.label, entry.order);
|
|
191
|
-
labelByOrder.set(entry.order, entry.label);
|
|
206
|
+
orderByLabel.set(entry.label, Math.min(prior ?? entry.order, entry.order));
|
|
192
207
|
}
|
|
193
208
|
return Object.freeze([...orderByLabel]
|
|
194
|
-
.sort((left, right) => left[1] - right[1])
|
|
209
|
+
.sort((left, right) => left[1] - right[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0))
|
|
195
210
|
.map(([label]) => label));
|
|
196
211
|
}
|
|
197
|
-
const CURRENT_AUDIENCE_EXCLUSIONS = Object.freeze({
|
|
198
|
-
internal: currentAudienceExclusions('internal'),
|
|
199
|
-
trusted: currentAudienceExclusions('trusted'),
|
|
200
|
-
public: currentAudienceExclusions('public'),
|
|
201
|
-
});
|
|
202
212
|
// Schema 12 is an immutable public compatibility contract and deliberately
|
|
203
213
|
// remains independent of the current field-policy projection.
|
|
204
214
|
const PUBLIC_V12_EXCLUSIONS = Object.freeze([
|
|
@@ -213,6 +223,6 @@ const PUBLIC_V12_EXCLUSIONS = Object.freeze([
|
|
|
213
223
|
export function caseAudienceExclusions(audience, caseVersion) {
|
|
214
224
|
return audience === 'public' && caseVersion === PUBLIC_CASE_SCHEMA_VERSION
|
|
215
225
|
? PUBLIC_V12_EXCLUSIONS
|
|
216
|
-
:
|
|
226
|
+
: currentAudienceExclusions(audience, caseVersion);
|
|
217
227
|
}
|
|
218
228
|
export { CASE_FIELD_RULES };
|
|
@@ -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
|
+
}
|