@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- 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 +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- 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 +330 -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 +7 -9
- 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/domain-feed.mjs +61 -0
- 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/json.mjs +1 -13
- 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 +32 -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 +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- 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 +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -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 +85 -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 +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -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 +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -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/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- 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 +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- 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 +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- 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 +13 -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-child-profile-contract.mjs +28 -11
- 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 +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- 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 +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- 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/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -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 +348 -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-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -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-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- 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 +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -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 +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -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/external-observation-interchange.mjs +96 -28
- 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/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -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/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -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 +79 -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 +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -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/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -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 +235 -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/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- 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 +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- 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
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
// contains raw RDAP/WHOIS material. This module deliberately excludes all raw
|
|
9
9
|
// registry/web responses, contacts, cookies, screenshots, and authentication
|
|
10
10
|
// data. Reports contain only the normalized case record.
|
|
11
|
-
import { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence } from "./case-model.mjs";
|
|
11
|
+
import { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence, publishedCaseEvidenceTimelineForVerification } from "./case-evidence-model.mjs";
|
|
12
12
|
import { httpSecurityHeaderLabel } from "./http-summary.mjs";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { webCollectionScoreLimitation } from "../evidence/collection-quality.mjs";
|
|
14
|
+
import { analystInteroperabilityTags } from "../analysis/analyst-taxonomy.mjs";
|
|
15
|
+
import { caseEvidenceLinkIssues } from "./case-evidence-links.mjs";
|
|
16
|
+
import { caseTypeSummary } from "./case-workflow-metadata.mjs";
|
|
17
|
+
import { caseClosureHistoryQualification } from "./case-response-outcomes.mjs";
|
|
18
|
+
import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../analysis/portable-generator.mjs";
|
|
15
19
|
import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
|
|
16
20
|
import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
|
|
21
|
+
import { EVIDENCE_FOLLOW_UP_CASE_REPORT_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
22
|
+
import { OBJECT_RESPONSE_CASE_REPORT_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
23
|
+
import { CASE_DELIVERY_RECEIPT_LIMITATION } from "./case-packet-correction.mjs";
|
|
17
24
|
import { CASE_RECHECK_CONDITIONS } from "./case-recheck-model.mjs";
|
|
18
|
-
import {
|
|
25
|
+
import { escapeCaseMarkdownInline as escapeMarkdownInline } from "./case-markdown.mjs";
|
|
26
|
+
import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, LATEST_PUBLIC_CASE_REPORT_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
|
|
19
27
|
// ---------------------------------------------------------------------------
|
|
20
28
|
// Constants
|
|
21
29
|
// ---------------------------------------------------------------------------
|
|
@@ -35,16 +43,6 @@ const LIMITATIONS_TEXT = [
|
|
|
35
43
|
'Snapshot hostnames are excluded; supporting DNS, TLS and web observations need not concern the Case registration domain.',
|
|
36
44
|
...PUBLISHED_V2_3_LIMITATIONS.slice(2),
|
|
37
45
|
].join(' ');
|
|
38
|
-
function escapeMarkdownInline(text) {
|
|
39
|
-
return String(text)
|
|
40
|
-
.replace(/[\r\n\u2028\u2029]+/g, ' ')
|
|
41
|
-
.replace(/([\\`*_{}\[\]<>()#+!|~])/g, '\\$1')
|
|
42
|
-
// GFM autolinks bare URLs independently of ordinary link syntax. Break
|
|
43
|
-
// the scheme or the first `www.` separator without making normal domains,
|
|
44
|
-
// dates, and prose noisy in the report source.
|
|
45
|
-
.replace(/\b([a-z][a-z0-9+.-]{1,31}):(?=\/\/)/gi, '$1\\:')
|
|
46
|
-
.replace(/\bwww\.(?=\S)/gi, 'www\\.');
|
|
47
|
-
}
|
|
48
46
|
/**
|
|
49
47
|
* Escapes a multiline note body for safe inclusion in a Markdown blockquote.
|
|
50
48
|
* Each line is individually escaped and prefixed with `> `.
|
|
@@ -90,6 +88,12 @@ function formatReportValue(value, field) {
|
|
|
90
88
|
return String(value);
|
|
91
89
|
return String(value);
|
|
92
90
|
}
|
|
91
|
+
function readableRiskScore(snapshot) {
|
|
92
|
+
const value = escapeMarkdownInline(formatReportValue(snapshot.riskScore));
|
|
93
|
+
const limitation = snapshot.riskScore === null ? null
|
|
94
|
+
: webCollectionScoreLimitation(snapshot.webCollectionQuality, snapshot.scanDepth);
|
|
95
|
+
return limitation ? `${value} — ${escapeMarkdownInline(limitation)}` : value;
|
|
96
|
+
}
|
|
93
97
|
/**
|
|
94
98
|
* Returns a shallow clone of a snapshot containing only known schema fields.
|
|
95
99
|
* Unknown/imported keys are never included.
|
|
@@ -111,6 +115,7 @@ function pickKnownSnapshotFields(snapshot) {
|
|
|
111
115
|
capturedAt: snapshot.capturedAt,
|
|
112
116
|
source: snapshot.source,
|
|
113
117
|
scanDepth: snapshot.scanDepth,
|
|
118
|
+
...(snapshot.webCollectionQuality ? { webCollectionQuality: { ...snapshot.webCollectionQuality } } : {}),
|
|
114
119
|
availability: snapshot.availability,
|
|
115
120
|
confidence: snapshot.confidence,
|
|
116
121
|
riskModelVersion: snapshot.riskModelVersion,
|
|
@@ -144,6 +149,7 @@ function pickKnownSnapshotFields(snapshot) {
|
|
|
144
149
|
reusesOfficialAssets: snapshot.reusesOfficialAssets,
|
|
145
150
|
hasPasswordField: snapshot.hasPasswordField,
|
|
146
151
|
hasExternalFormAction: snapshot.hasExternalFormAction,
|
|
152
|
+
...(snapshot.hasExternalPasswordForm === undefined ? {} : { hasExternalPasswordForm: snapshot.hasExternalPasswordForm }),
|
|
147
153
|
phishingLanguageMatch: snapshot.phishingLanguageMatch,
|
|
148
154
|
privacyProtected: snapshot.privacyProtected ?? null,
|
|
149
155
|
idnReferenceMatch: snapshot.idnReferenceMatch ?? null,
|
|
@@ -173,14 +179,7 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
173
179
|
const generator = buildPortableGeneratorMetadata(options.applicationVersion);
|
|
174
180
|
// --- Build JSON report ---
|
|
175
181
|
const timeline = caseEvidenceTimeline(caseRecord.evidenceHistory);
|
|
176
|
-
const timelineEntries = timeline
|
|
177
|
-
snapshot: pickKnownSnapshotFields(entry.snapshot),
|
|
178
|
-
isBaseline: entry.isBaseline,
|
|
179
|
-
hasRepeatedObservation: entry.hasRepeatedObservation,
|
|
180
|
-
changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
|
|
181
|
-
hasIncomparableChange: entry.hasIncomparableChange,
|
|
182
|
-
incomparableReasons: [...entry.incomparableReasons],
|
|
183
|
-
}));
|
|
182
|
+
const timelineEntries = projectReportTimeline(timeline);
|
|
184
183
|
const selection = currentCaseEvidence(caseRecord);
|
|
185
184
|
const latest = selection.snapshot;
|
|
186
185
|
const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
|
|
@@ -221,7 +220,9 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
221
220
|
currentAssessment,
|
|
222
221
|
evidenceTimeline: timelineEntries,
|
|
223
222
|
analystResponse: {
|
|
223
|
+
...(caseRecord.workflowMetadata === undefined ? {} : { workflowMetadata: structuredClone(caseRecord.workflowMetadata) }),
|
|
224
224
|
evidencePins: caseRecord.evidencePins.map((item) => ({ ...item, limitations: [...item.limitations] })),
|
|
225
|
+
...(caseRecord.evidenceLinks === undefined ? {} : { evidenceLinks: structuredClone(caseRecord.evidenceLinks) }),
|
|
225
226
|
decisions: caseRecord.decisions.map((item) => ({ ...item, evidencePinIds: [...item.evidencePinIds] })),
|
|
226
227
|
actions: caseRecord.actions.map((item) => ({
|
|
227
228
|
...item,
|
|
@@ -270,11 +271,29 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
270
271
|
const md = buildMarkdown(json, options.includeAttribution !== false);
|
|
271
272
|
return { json, markdown: md };
|
|
272
273
|
}
|
|
274
|
+
function projectReportTimeline(timeline) {
|
|
275
|
+
return timeline.map(entry => ({
|
|
276
|
+
snapshot: pickKnownSnapshotFields(entry.snapshot), isBaseline: entry.isBaseline,
|
|
277
|
+
hasRepeatedObservation: entry.hasRepeatedObservation,
|
|
278
|
+
changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
|
|
279
|
+
hasIncomparableChange: entry.hasIncomparableChange, incomparableReasons: [...entry.incomparableReasons],
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
273
282
|
/** Expected strict reader projection; historical disclosure wording is immutable. */
|
|
274
283
|
export function buildCaseReportVerificationProjection(caseRecord, options, schemaVersion) {
|
|
275
|
-
const
|
|
284
|
+
const generated = buildCaseReport(caseRecord, options).json;
|
|
276
285
|
if (schemaVersion === CASE_REPORT_SCHEMA_VERSION)
|
|
277
|
-
return
|
|
286
|
+
return generated;
|
|
287
|
+
if (schemaVersion === OBJECT_RESPONSE_CASE_REPORT_SCHEMA_VERSION)
|
|
288
|
+
return { ...generated, schemaVersion };
|
|
289
|
+
if (schemaVersion === EVIDENCE_FOLLOW_UP_CASE_REPORT_SCHEMA_VERSION)
|
|
290
|
+
return { ...generated, schemaVersion };
|
|
291
|
+
if (caseRecord.evidenceHistory.some(snapshot => snapshot.webCollectionQuality !== undefined)) {
|
|
292
|
+
throw new TypeError('Published report formats cannot declare newer collection-quality fields.');
|
|
293
|
+
}
|
|
294
|
+
const current = { ...generated, evidenceTimeline: projectReportTimeline(publishedCaseEvidenceTimelineForVerification(caseRecord.evidenceHistory)) };
|
|
295
|
+
if (schemaVersion === LATEST_PUBLIC_CASE_REPORT_SCHEMA_VERSION)
|
|
296
|
+
return { ...current, schemaVersion };
|
|
278
297
|
if (schemaVersion !== PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION) {
|
|
279
298
|
throw new TypeError('No strict Case report projection is defined for this version.');
|
|
280
299
|
}
|
|
@@ -325,6 +344,17 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
325
344
|
if (report.case.tags.length > 0) {
|
|
326
345
|
lines.push(`| Tags | ${escapeMarkdownInline(report.case.tags.join(', '))} |`);
|
|
327
346
|
}
|
|
347
|
+
const caseTypes = caseTypeSummary(report.analystResponse.workflowMetadata ? { workflowMetadata: report.analystResponse.workflowMetadata } : {});
|
|
348
|
+
if (caseTypes)
|
|
349
|
+
lines.push(`| Case types | ${escapeMarkdownInline(caseTypes)} |`);
|
|
350
|
+
const context = report.analystResponse.workflowMetadata?.investigationContext;
|
|
351
|
+
if (context) {
|
|
352
|
+
lines.push(`| Investigation objective | ${escapeMarkdownInline(context.objective)} |`);
|
|
353
|
+
lines.push(`| Incident URL (${context.urlRetention === 'exact' ? 'exact' : 'origin only'}) | ${escapeMarkdownInline(context.incidentUrl)} |`);
|
|
354
|
+
}
|
|
355
|
+
for (const target of report.analystResponse.workflowMetadata?.incidentTargets ?? []) {
|
|
356
|
+
lines.push(`| Incident link (${target.state}) | ${escapeMarkdownInline(target.url)} |`);
|
|
357
|
+
}
|
|
328
358
|
lines.push(`| Notes included | ${report.case.notesIncluded ? 'Yes' : 'No'} |`);
|
|
329
359
|
lines.push('');
|
|
330
360
|
// Current assessment
|
|
@@ -333,7 +363,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
333
363
|
if (report.currentAssessment) {
|
|
334
364
|
const a = report.currentAssessment;
|
|
335
365
|
lines.push(`- **Availability:** ${escapeMarkdownInline(formatReportValue(a.availability))}`);
|
|
336
|
-
lines.push(`- **Risk score:** ${
|
|
366
|
+
lines.push(`- **Risk score:** ${readableRiskScore(a)}`);
|
|
337
367
|
lines.push(`- **Risk model:** ${a.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.riskModelVersion))}`}`);
|
|
338
368
|
lines.push(`- **Brand Profile context:** ${escapeMarkdownInline(formatReportValue(a.profileContextState))}`);
|
|
339
369
|
if (a.profileContextLimitation)
|
|
@@ -388,7 +418,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
388
418
|
lines.push('**Key evidence:**');
|
|
389
419
|
lines.push('');
|
|
390
420
|
lines.push(`- Availability: ${escapeMarkdownInline(formatReportValue(snap.availability))}`);
|
|
391
|
-
lines.push(`- Risk score: ${
|
|
421
|
+
lines.push(`- Risk score: ${readableRiskScore(snap)}`);
|
|
392
422
|
lines.push(`- Risk model: ${snap.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(snap.riskModelVersion))}`}`);
|
|
393
423
|
lines.push(`- Brand Profile context: ${escapeMarkdownInline(formatReportValue(snap.profileContextState))}`);
|
|
394
424
|
if (snap.profileContextLimitation)
|
|
@@ -455,6 +485,8 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
455
485
|
lines.push('> Opportunity scores and factors use different or unversioned models, so their numeric difference is not treated as a domain change.');
|
|
456
486
|
if (reasons.includes('scan-depth'))
|
|
457
487
|
lines.push('> Capture depths differ, so unevaluated deep signals are not treated as additions or removals.');
|
|
488
|
+
if (reasons.includes('collection-quality'))
|
|
489
|
+
lines.push('> Missing or incomplete observations prevent comparison of affected registration, page, favicon or score fields.');
|
|
458
490
|
if (reasons.length === 0 || reasons.includes('other'))
|
|
459
491
|
lines.push('> The observations differ materially, but no reliable field-level comparison is available.');
|
|
460
492
|
lines.push('');
|
|
@@ -464,7 +496,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
464
496
|
lines.push('## Analyst Decision Packet');
|
|
465
497
|
lines.push('');
|
|
466
498
|
const response = report.analystResponse;
|
|
467
|
-
if (!response.evidencePins.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length && !response.observedEffects.reviews.length && !response.closures.records.length && !response.branches.length) {
|
|
499
|
+
if (!response.evidencePins.length && !response.evidenceLinks?.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length && !response.observedEffects.reviews.length && !response.closures.records.length && !response.branches.length) {
|
|
468
500
|
lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, response actions, independent effect reviews, closures, or manual investigation steps recorded.');
|
|
469
501
|
lines.push('');
|
|
470
502
|
}
|
|
@@ -476,6 +508,8 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
476
508
|
for (const pin of response.evidencePins) {
|
|
477
509
|
lines.push(`- **${escapeMarkdownInline(pin.label)}:** ${escapeMarkdownInline(pin.value)}`);
|
|
478
510
|
lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt ?? 'Time unavailable')}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
|
|
511
|
+
if (pin.responseObject)
|
|
512
|
+
lines.push(` Exact object: ${escapeMarkdownInline(pin.responseObject.kind)} · ${escapeMarkdownInline(pin.responseObject.identifier)}`);
|
|
479
513
|
if (pin.limitations.length)
|
|
480
514
|
lines.push(` Limitations: ${escapeMarkdownInline(pin.limitations.join('; '))}`);
|
|
481
515
|
}
|
|
@@ -539,6 +573,12 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
539
573
|
}
|
|
540
574
|
lines.push('');
|
|
541
575
|
}
|
|
576
|
+
if (response.evidenceLinks?.length) {
|
|
577
|
+
lines.push('### Analyst-declared evidence relationships', '');
|
|
578
|
+
for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(response.evidenceLinks, response.evidencePins))
|
|
579
|
+
lines.push(`- ${escapeMarkdownInline(link.fromPinId)} ${link.kind === 'derived_from' ? 'derived from' : 'shares a source with'} ${escapeMarkdownInline(link.toPinId)} · recorded ${link.createdAt}`, ` Basis: ${escapeMarkdownInline(link.basis)}`, ...(link.withdrawal ? [` Withdrawn ${link.withdrawal.at}: ${escapeMarkdownInline(link.withdrawal.reason)}`] : []), ...(missingPinIds.length ? [` Referenced pins not retained: ${missingPinIds.map(escapeMarkdownInline).join(', ')}`] : []), ...(cyclic ? [' Conflicting imported derivation cycle; no order is inferred.'] : []));
|
|
580
|
+
lines.push('These relationships record analyst attribution, not independent corroboration or added confidence.', '');
|
|
581
|
+
}
|
|
542
582
|
lines.push('### Decisions');
|
|
543
583
|
lines.push('');
|
|
544
584
|
if (!response.decisions.length)
|
|
@@ -558,12 +598,22 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
558
598
|
for (const action of response.actions) {
|
|
559
599
|
lines.push(`- **${escapeMarkdownInline(action.type.replaceAll('_', ' '))}:** ${escapeMarkdownInline(action.recipient)} (${escapeMarkdownInline(action.state)})`);
|
|
560
600
|
lines.push(` Contact source: ${escapeMarkdownInline(action.contactSource)}; updated ${escapeMarkdownInline(action.updatedAt)}.`);
|
|
601
|
+
lines.push(` Object binding: ${action.responseObjects?.length ? action.responseObjects.map(object => `${escapeMarkdownInline(object.kind)} · ${escapeMarkdownInline(object.identifier)}`).join('; ') : 'Unknown; historical or unbound actions do not establish exact-object coverage.'}`);
|
|
561
602
|
if (action.routeObservedAt)
|
|
562
603
|
lines.push(` Route reviewed: ${escapeMarkdownInline(action.routeObservedAt)}`);
|
|
563
604
|
if (action.routeReviewAfter)
|
|
564
605
|
lines.push(` Route review due: ${escapeMarkdownInline(action.routeReviewAfter)}`);
|
|
565
606
|
if (action.originActionId)
|
|
566
607
|
lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
|
|
608
|
+
if (action.amendment)
|
|
609
|
+
lines.push(` Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}; request events: ${action.amendment.requestEventIds.map(escapeMarkdownInline).join(', ')}`);
|
|
610
|
+
if (action.correction)
|
|
611
|
+
lines.push(` ${action.correction.purpose === 'correction' ? 'Correction request' : 'Retraction request'} for delivery event ${escapeMarkdownInline(action.correction.deliveryEventId)}; packet v${action.correction.packetVersion} SHA-256: ${action.correction.packetDigestSha256}`, ` Reason: ${escapeMarkdownInline(action.correction.reason)}`, ` Previous statement: ${escapeMarkdownInline(action.correction.previousStatement)}`, ` Corrected statement: ${escapeMarkdownInline(action.correction.correctedStatement || 'Request to retract the previous statement')}`, ` Corrected evidence pins: ${action.correction.evidencePinIds.map(escapeMarkdownInline).join(', ')}`, ' Preparation is not delivery, recipient acceptance, restoration or independent recheck.');
|
|
612
|
+
for (const event of action.history)
|
|
613
|
+
if (event.evidenceRequest) {
|
|
614
|
+
const request = event.evidenceRequest;
|
|
615
|
+
lines.push(` Requested evidence [${request.state}]: ${escapeMarkdownInline(request.summary)}; deadline: ${request.dueAt ?? 'not stated'}; original packet: ${request.packetDigestSha256}${event.applied ? '' : ' (retained conflict)'}`, ` Prepared pins: ${request.evidencePinIds.map(escapeMarkdownInline).join(', ') || 'none'}${request.rationale ? `; ${escapeMarkdownInline(request.rationale)}` : ''}`);
|
|
616
|
+
}
|
|
567
617
|
if (action.dueAt)
|
|
568
618
|
lines.push(` Due: ${escapeMarkdownInline(action.dueAt)}`);
|
|
569
619
|
if (action.followUpAt)
|
|
@@ -580,8 +630,14 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
580
630
|
lines.push(` - ${escapeMarkdownInline(event.occurredAt)} · ${escapeMarkdownInline(event.previousState ?? 'none')} → ${escapeMarkdownInline(event.nextState)} · ${escapeMarkdownInline(event.sourceClass)} · ${event.applied ? 'applied' : 'retained conflict'}`);
|
|
581
631
|
if (event.providerOutcome)
|
|
582
632
|
lines.push(` Provider outcome at this time: ${escapeMarkdownInline(event.providerOutcome.replaceAll('_', ' '))}`);
|
|
633
|
+
if (event.responseObjects?.length)
|
|
634
|
+
lines.push(` Objects at this event: ${event.responseObjects.map(object => `${escapeMarkdownInline(object.kind)} · ${escapeMarkdownInline(object.identifier)}`).join('; ')}`);
|
|
635
|
+
if (event.objectOutcome)
|
|
636
|
+
lines.push(` Source-reported object outcome: ${escapeMarkdownInline(event.objectOutcome)}; not independent verification or causation.`);
|
|
583
637
|
if (event.reference)
|
|
584
638
|
lines.push(` Event reference: ${escapeMarkdownInline(event.reference)}`);
|
|
639
|
+
if (event.packetReceipt)
|
|
640
|
+
lines.push(` Retained packet v${event.packetReceipt.packetVersion}: ${event.packetReceipt.packetDigestSha256}; recipient ${escapeMarkdownInline(event.packetReceipt.recipient)}; audience ${escapeMarkdownInline(event.packetReceipt.profile)}; generated ${event.packetReceipt.packetGeneratedAt}`, ` ${CASE_DELIVERY_RECEIPT_LIMITATION}`);
|
|
585
641
|
if (event.evidencePinId)
|
|
586
642
|
lines.push(` Evidence pin: ${escapeMarkdownInline(event.evidencePinId)}`);
|
|
587
643
|
if (event.limitations.length)
|
|
@@ -631,6 +687,10 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
631
687
|
lines.push('- No independent observed-effect review recorded.');
|
|
632
688
|
for (const review of response.observedEffects.reviews) {
|
|
633
689
|
lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
|
|
690
|
+
if (review.responseObject)
|
|
691
|
+
lines.push(` Exact object: ${escapeMarkdownInline(review.responseObject.kind)} · ${escapeMarkdownInline(review.responseObject.identifier)}`);
|
|
692
|
+
if (review.objectOutcome)
|
|
693
|
+
lines.push(` Independently recorded object outcome: ${escapeMarkdownInline(review.objectOutcome)}; point-in-time observation, not report causation or recurrence.`);
|
|
634
694
|
if (review.recheck) {
|
|
635
695
|
lines.push(` Question ${escapeMarkdownInline(review.recheck.questionId)}: ${escapeMarkdownInline(review.recheck.question)}`);
|
|
636
696
|
lines.push(` Recheck target: ${escapeMarkdownInline(review.recheck.targetHostname)}; retained comparison conditions: ${escapeMarkdownInline(review.recheck.conditions)}`);
|
|
@@ -653,13 +713,19 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
653
713
|
lines.push('- No deliberate analyst closure recorded.');
|
|
654
714
|
for (const closure of response.closures.records) {
|
|
655
715
|
lines.push(`- **Closure · ${escapeMarkdownInline(closure.reason.replaceAll('_', ' '))}** (${escapeMarkdownInline(closure.createdAt)}): ${escapeMarkdownInline(closure.summary)}`);
|
|
716
|
+
lines.push(` Scope: ${closure.responseObject ? `${escapeMarkdownInline(closure.responseObject.kind)} · ${escapeMarkdownInline(closure.responseObject.identifier)}; other objects and the Case remain independent.` : 'Whole Case analyst decision.'}`);
|
|
656
717
|
if (closure.observedEffectReviewId)
|
|
657
718
|
lines.push(` Independent review: ${escapeMarkdownInline(closure.observedEffectReviewId)}`);
|
|
658
719
|
if (closure.actionId)
|
|
659
720
|
lines.push(` Provider action: ${escapeMarkdownInline(closure.actionId)}`);
|
|
721
|
+
const qualification = caseClosureHistoryQualification(closure, response.actions);
|
|
722
|
+
if (qualification)
|
|
723
|
+
lines.push(` History qualification: ${escapeMarkdownInline(qualification)}`);
|
|
660
724
|
if (closure.limitations.length)
|
|
661
725
|
lines.push(` Limitations: ${escapeMarkdownInline(closure.limitations.join('; '))}`);
|
|
662
726
|
}
|
|
727
|
+
if (response.closures.omitted)
|
|
728
|
+
lines.push(`- Closure records not retained: ${response.closures.omitted}. ${escapeMarkdownInline(response.closures.limitations.join('; '))}`);
|
|
663
729
|
lines.push('');
|
|
664
730
|
// Analyst notes (only when included)
|
|
665
731
|
if (report.case.notesIncluded && Array.isArray(report.case.notes) && report.case.notes.length > 0) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { array, enumeration, exact, HEX_DIGEST_RE, iso, text } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { assertWorkspaceInputGraph } from "../workspace/hostile-input.mjs";
|
|
3
|
+
import { MAX_DECISION_PIN_REFERENCES, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
|
|
4
|
+
import { SAFE_ID_RE } from "./case-response-values.mjs";
|
|
5
|
+
function identifier(value, label) {
|
|
6
|
+
const result = text(value, label, 64);
|
|
7
|
+
if (!SAFE_ID_RE.test(result))
|
|
8
|
+
throw new TypeError(`${label} is invalid.`);
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
function identifiers(value, label) {
|
|
12
|
+
const result = array(value, label, MAX_DECISION_PIN_REFERENCES).map(item => identifier(item, label));
|
|
13
|
+
if (new Set(result).size !== result.length)
|
|
14
|
+
throw new TypeError(`${label} contains duplicate identities.`);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function packetDigest(value) {
|
|
18
|
+
if (typeof value !== 'string' || !HEX_DIGEST_RE.test(value))
|
|
19
|
+
throw new TypeError('Select a recorded submitted-packet digest.');
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export function readCaseEvidenceRequest(raw) {
|
|
23
|
+
if (raw === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
assertWorkspaceInputGraph(raw, 'Requested evidence');
|
|
26
|
+
const row = exact(raw, ['id', 'packetDigestSha256', 'summary', 'dueAt', 'state', 'evidencePinIds', 'rationale', 'previousEventIds'], 'Requested evidence');
|
|
27
|
+
iso(row.dueAt, 'Requested evidence deadline', true);
|
|
28
|
+
const summary = text(row.summary, 'Requested evidence summary', MAX_RESPONSE_VALUE_LENGTH).trim();
|
|
29
|
+
const rationale = text(row.rationale, 'Requested evidence rationale', MAX_RESPONSE_RATIONALE_LENGTH, true).trim();
|
|
30
|
+
const state = enumeration(row.state, ['requested', 'prepared', 'unavailable'], 'Requested evidence state');
|
|
31
|
+
const evidencePinIds = identifiers(row.evidencePinIds, 'Requested evidence pins');
|
|
32
|
+
if (!summary || (state === 'prepared' && !evidencePinIds.length) || (state === 'unavailable' && !rationale)) {
|
|
33
|
+
throw new TypeError('Prepared evidence needs retained pins; unavailable evidence needs an explanation.');
|
|
34
|
+
}
|
|
35
|
+
return { id: identifier(row.id, 'Evidence request identity'), packetDigestSha256: packetDigest(row.packetDigestSha256),
|
|
36
|
+
summary, dueAt: row.dueAt, state, evidencePinIds, rationale,
|
|
37
|
+
previousEventIds: identifiers(row.previousEventIds, 'Prior evidence request events') };
|
|
38
|
+
}
|
|
39
|
+
export function readCasePacketAmendment(raw) {
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
assertWorkspaceInputGraph(raw, 'Packet amendment');
|
|
43
|
+
const row = exact(raw, ['packetDigestSha256', 'requestEventIds'], 'Packet amendment');
|
|
44
|
+
const requestEventIds = identifiers(row.requestEventIds, 'Amendment request events');
|
|
45
|
+
if (!requestEventIds.length)
|
|
46
|
+
throw new TypeError('Select at least one prepared evidence request.');
|
|
47
|
+
return { packetDigestSha256: packetDigest(row.packetDigestSha256), requestEventIds };
|
|
48
|
+
}
|
|
49
|
+
export function assertEvidenceRequestEvent(request, event) {
|
|
50
|
+
if (request && (event.nextState !== 'acknowledged' || !['submitted', 'acknowledged'].includes(String(event.previousState))
|
|
51
|
+
|| (request.state === 'requested'
|
|
52
|
+
? event.sourceClass !== 'provider' || event.providerOutcome !== 'more_information_requested' || request.evidencePinIds.length > 0 || Boolean(request.rationale) || request.previousEventIds.length > 0
|
|
53
|
+
: event.sourceClass !== 'analyst' || request.previousEventIds.length === 0)))
|
|
54
|
+
throw new TypeError('Requested evidence has incompatible event provenance.');
|
|
55
|
+
}
|
|
56
|
+
/** A digest belongs to the explicit delivery event, never the latest mutable reference. */
|
|
57
|
+
export function submittedPacketReceipts(action) {
|
|
58
|
+
return action.history.flatMap(event => {
|
|
59
|
+
const match = /^response-packet-sha256:([a-f0-9]{64})$/u.exec(event.reference ?? '');
|
|
60
|
+
return event.applied && ['authorised', 'submitted'].includes(event.previousState ?? '') && event.nextState === 'submitted'
|
|
61
|
+
&& event.sourceClass === 'analyst' && match
|
|
62
|
+
? [{ eventId: event.id, digestSha256: match[1], occurredAt: event.occurredAt }] : [];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function latestEvidenceRequests(action) {
|
|
66
|
+
const events = action.history.filter((event) => event.applied && Boolean(event.evidenceRequest));
|
|
67
|
+
const replaced = new Set(events.flatMap(event => event.evidenceRequest.previousEventIds
|
|
68
|
+
.filter(id => events.some(parent => parent.id === id && parent.evidenceRequest.id === event.evidenceRequest.id))));
|
|
69
|
+
return events.filter(event => !replaced.has(event.id));
|
|
70
|
+
}
|
|
71
|
+
/** Causal links order same-time reviews and retain concurrent branches for explicit reconciliation. */
|
|
72
|
+
export function assertEvidenceRequestHistory(events, missingHistory) {
|
|
73
|
+
const byId = new Map(events.map(event => [event.id, event]));
|
|
74
|
+
const complete = new Set(), visiting = new Set();
|
|
75
|
+
const visit = (id) => {
|
|
76
|
+
if (complete.has(id))
|
|
77
|
+
return;
|
|
78
|
+
if (visiting.has(id))
|
|
79
|
+
throw new TypeError('Requested evidence history contains a cycle.');
|
|
80
|
+
visiting.add(id);
|
|
81
|
+
const request = byId.get(id)?.evidenceRequest;
|
|
82
|
+
for (const parentId of request?.previousEventIds ?? []) {
|
|
83
|
+
const parent = byId.get(parentId);
|
|
84
|
+
if (!parent && missingHistory)
|
|
85
|
+
continue;
|
|
86
|
+
if (!parent?.evidenceRequest || parent.evidenceRequest.id !== request.id
|
|
87
|
+
|| parent.evidenceRequest.summary !== request.summary || parent.evidenceRequest.dueAt !== request.dueAt
|
|
88
|
+
|| parent.evidenceRequest.packetDigestSha256 !== request.packetDigestSha256)
|
|
89
|
+
throw new TypeError('Requested evidence history does not preserve its original request.');
|
|
90
|
+
visit(parentId);
|
|
91
|
+
}
|
|
92
|
+
visiting.delete(id);
|
|
93
|
+
complete.add(id);
|
|
94
|
+
};
|
|
95
|
+
for (const event of events)
|
|
96
|
+
visit(event.id);
|
|
97
|
+
}
|
|
98
|
+
export function assertEvidenceRequestTransition(action, request, sourceClass, providerOutcome, validPinIds) {
|
|
99
|
+
if (!['submitted', 'acknowledged'].includes(action.state)
|
|
100
|
+
|| !submittedPacketReceipts(action).some(receipt => receipt.digestSha256 === request.packetDigestSha256)) {
|
|
101
|
+
throw new Error('Requested evidence must refer to an explicitly recorded packet delivery on an open action.');
|
|
102
|
+
}
|
|
103
|
+
const previousEvents = latestEvidenceRequests(action).filter(event => event.evidenceRequest.id === request.id);
|
|
104
|
+
if (!previousEvents.length) {
|
|
105
|
+
if (request.state !== 'requested' || sourceClass !== 'provider' || providerOutcome !== 'more_information_requested'
|
|
106
|
+
|| request.evidencePinIds.length || request.rationale || request.previousEventIds.length)
|
|
107
|
+
throw new Error('A new evidence request must record the provider request before preparation.');
|
|
108
|
+
}
|
|
109
|
+
else if (sourceClass !== 'analyst' || request.state === 'requested'
|
|
110
|
+
|| previousEvents.some(({ evidenceRequest: previous }) => request.summary !== previous.summary
|
|
111
|
+
|| request.dueAt !== previous.dueAt || request.packetDigestSha256 !== previous.packetDigestSha256)) {
|
|
112
|
+
throw new Error('Keep the original provider request unchanged; append a separate request for changed requirements.');
|
|
113
|
+
}
|
|
114
|
+
if (JSON.stringify(request.previousEventIds.slice().sort()) !== JSON.stringify(previousEvents.map(event => event.id).sort())) {
|
|
115
|
+
throw new Error('The evidence preparation changed. Review all current request events before saving.');
|
|
116
|
+
}
|
|
117
|
+
if (validPinIds && request.evidencePinIds.some(id => !validPinIds.has(id)))
|
|
118
|
+
throw new Error('Requested evidence refers to a pin no longer retained in this Case.');
|
|
119
|
+
}
|
|
120
|
+
export function assertPacketAmendment(actions, originActionId, amendment) {
|
|
121
|
+
const original = actions.find(action => action.id === originActionId);
|
|
122
|
+
if (!original || !submittedPacketReceipts(original).some(receipt => receipt.digestSha256 === amendment.packetDigestSha256)) {
|
|
123
|
+
throw new Error('An amendment requires the exact recorded packet delivery from its originating action.');
|
|
124
|
+
}
|
|
125
|
+
const latest = latestEvidenceRequests(original);
|
|
126
|
+
if (amendment.requestEventIds.some(id => !latest.some(event => event.id === id
|
|
127
|
+
&& latest.filter(other => other.evidenceRequest.id === event.evidenceRequest.id).length === 1
|
|
128
|
+
&& event.evidenceRequest.state === 'prepared' && event.evidenceRequest.packetDigestSha256 === amendment.packetDigestSha256))) {
|
|
129
|
+
throw new Error('Review the current prepared evidence requests before creating or submitting an amendment.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function assertPacketAmendmentSelection(actions, selectedActionId, selectedPinIds) {
|
|
133
|
+
const action = actions.find(item => item.id === selectedActionId);
|
|
134
|
+
if (!action?.amendment)
|
|
135
|
+
return;
|
|
136
|
+
assertPacketAmendment(actions, action.originActionId, action.amendment);
|
|
137
|
+
const original = actions.find(item => item.id === action.originActionId);
|
|
138
|
+
const required = latestEvidenceRequests(original).filter(event => action.amendment.requestEventIds.includes(event.id))
|
|
139
|
+
.flatMap(event => event.evidenceRequest.evidencePinIds);
|
|
140
|
+
if (required.some(id => !selectedPinIds.has(id)))
|
|
141
|
+
throw new TypeError('Select every prepared request pin for this amendment; the original packet is not regenerated.');
|
|
142
|
+
}
|
|
143
|
+
export function evidenceRequestDelivery(actions, actionId, eventId) {
|
|
144
|
+
return actions.flatMap(action => action.originActionId === actionId && action.amendment?.requestEventIds.includes(eventId)
|
|
145
|
+
? submittedPacketReceipts(action).map(receipt => ({ actionId: action.id, ...receipt })) : []);
|
|
146
|
+
}
|