@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
|
@@ -8,18 +8,23 @@
|
|
|
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 {
|
|
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 { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../analysis/portable-generator.mjs";
|
|
15
18
|
import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
|
|
16
19
|
import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
|
|
17
|
-
import {
|
|
20
|
+
import { CASE_RECHECK_CONDITIONS } from "./case-recheck-model.mjs";
|
|
21
|
+
import { escapeCaseMarkdownInline as escapeMarkdownInline } from "./case-markdown.mjs";
|
|
22
|
+
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";
|
|
18
23
|
// ---------------------------------------------------------------------------
|
|
19
24
|
// Constants
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
21
26
|
export { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION };
|
|
22
|
-
const
|
|
27
|
+
const PUBLISHED_V2_3_LIMITATIONS = Object.freeze([
|
|
23
28
|
'This report contains normalised browser-local observations from WHOISleuth analyst cases.',
|
|
24
29
|
'It is not a live lookup and does not contain raw WHOIS, RDAP, DNS, HTML, or responses collected during website checks.',
|
|
25
30
|
'Absence of a signal (e.g. no MX record observed) does not prove nonexistence. It may not have been evaluated.',
|
|
@@ -28,17 +33,12 @@ const LIMITATIONS_TEXT = [
|
|
|
28
33
|
'Brand Profile references record an explicit analyst-selected association only; they do not establish ownership, attribution, intent, safety, or maliciousness.',
|
|
29
34
|
'Provider workflow outcomes and independently observed technical effects remain separately attributed. A provider outcome does not establish independent remediation, absence, or safety; neither evidence family establishes legal sufficiency or provider performance.',
|
|
30
35
|
'Generated locally in the browser. Review the package before sharing it.',
|
|
36
|
+
]);
|
|
37
|
+
const LIMITATIONS_TEXT = [
|
|
38
|
+
...PUBLISHED_V2_3_LIMITATIONS.slice(0, 2),
|
|
39
|
+
'Snapshot hostnames are excluded; supporting DNS, TLS and web observations need not concern the Case registration domain.',
|
|
40
|
+
...PUBLISHED_V2_3_LIMITATIONS.slice(2),
|
|
31
41
|
].join(' ');
|
|
32
|
-
function escapeMarkdownInline(text) {
|
|
33
|
-
return String(text)
|
|
34
|
-
.replace(/[\r\n\u2028\u2029]+/g, ' ')
|
|
35
|
-
.replace(/([\\`*_{}\[\]<>()#+!|~])/g, '\\$1')
|
|
36
|
-
// GFM autolinks bare URLs independently of ordinary link syntax. Break
|
|
37
|
-
// the scheme or the first `www.` separator without making normal domains,
|
|
38
|
-
// dates, and prose noisy in the report source.
|
|
39
|
-
.replace(/\b([a-z][a-z0-9+.-]{1,31}):(?=\/\/)/gi, '$1\\:')
|
|
40
|
-
.replace(/\bwww\.(?=\S)/gi, 'www\\.');
|
|
41
|
-
}
|
|
42
42
|
/**
|
|
43
43
|
* Escapes a multiline note body for safe inclusion in a Markdown blockquote.
|
|
44
44
|
* Each line is individually escaped and prefixed with `> `.
|
|
@@ -84,6 +84,12 @@ function formatReportValue(value, field) {
|
|
|
84
84
|
return String(value);
|
|
85
85
|
return String(value);
|
|
86
86
|
}
|
|
87
|
+
function readableRiskScore(snapshot) {
|
|
88
|
+
const value = escapeMarkdownInline(formatReportValue(snapshot.riskScore));
|
|
89
|
+
const limitation = snapshot.riskScore === null ? null
|
|
90
|
+
: webCollectionScoreLimitation(snapshot.webCollectionQuality, snapshot.scanDepth);
|
|
91
|
+
return limitation ? `${value} — ${escapeMarkdownInline(limitation)}` : value;
|
|
92
|
+
}
|
|
87
93
|
/**
|
|
88
94
|
* Returns a shallow clone of a snapshot containing only known schema fields.
|
|
89
95
|
* Unknown/imported keys are never included.
|
|
@@ -100,10 +106,12 @@ function pickKnownSnapshotFields(snapshot) {
|
|
|
100
106
|
return {
|
|
101
107
|
id: snapshot.id,
|
|
102
108
|
fingerprint: snapshot.fingerprint,
|
|
109
|
+
...(snapshot.factorOrder ? { factorOrder: snapshot.factorOrder } : {}),
|
|
103
110
|
firstCapturedAt: snapshot.firstCapturedAt,
|
|
104
111
|
capturedAt: snapshot.capturedAt,
|
|
105
112
|
source: snapshot.source,
|
|
106
113
|
scanDepth: snapshot.scanDepth,
|
|
114
|
+
...(snapshot.webCollectionQuality ? { webCollectionQuality: { ...snapshot.webCollectionQuality } } : {}),
|
|
107
115
|
availability: snapshot.availability,
|
|
108
116
|
confidence: snapshot.confidence,
|
|
109
117
|
riskModelVersion: snapshot.riskModelVersion,
|
|
@@ -165,51 +173,10 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
165
173
|
const now = generatedAt || new Date().toISOString();
|
|
166
174
|
const generator = buildPortableGeneratorMetadata(options.applicationVersion);
|
|
167
175
|
// --- Build JSON report ---
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const snapshot = chronological[i];
|
|
173
|
-
if (!snapshot)
|
|
174
|
-
continue;
|
|
175
|
-
const isBaseline = i === 0;
|
|
176
|
-
const hasRepeatedObservation = snapshot.firstCapturedAt !== snapshot.capturedAt;
|
|
177
|
-
let changes = null;
|
|
178
|
-
let hasIncomparableChange = false;
|
|
179
|
-
let incomparableReasons = [];
|
|
180
|
-
if (!isBaseline) {
|
|
181
|
-
const previous = chronological[i - 1];
|
|
182
|
-
if (!previous)
|
|
183
|
-
continue;
|
|
184
|
-
const rawChanges = compareCaseEvidence(previous, snapshot);
|
|
185
|
-
incomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
|
|
186
|
-
const hostnameContextChanged = incomparableReasons.includes('observation-context');
|
|
187
|
-
if (rawChanges.length > 0 && !hostnameContextChanged) {
|
|
188
|
-
changes = rawChanges.map((change) => ({
|
|
189
|
-
field: change.field,
|
|
190
|
-
label: change.label,
|
|
191
|
-
before: change.before,
|
|
192
|
-
after: change.after,
|
|
193
|
-
tone: change.tone,
|
|
194
|
-
}));
|
|
195
|
-
}
|
|
196
|
-
else if (snapshot.fingerprint !== previous.fingerprint
|
|
197
|
-
&& incomparableReasons.length === 0) {
|
|
198
|
-
incomparableReasons = ['other'];
|
|
199
|
-
}
|
|
200
|
-
hasIncomparableChange = incomparableReasons.length > 0;
|
|
201
|
-
}
|
|
202
|
-
timelineEntries.push({
|
|
203
|
-
snapshot: pickKnownSnapshotFields(snapshot),
|
|
204
|
-
isBaseline,
|
|
205
|
-
hasRepeatedObservation,
|
|
206
|
-
changes,
|
|
207
|
-
hasIncomparableChange,
|
|
208
|
-
incomparableReasons,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const latest = latestCaseEvidence({ evidenceHistory: caseRecord.evidenceHistory ?? undefined });
|
|
176
|
+
const timeline = caseEvidenceTimeline(caseRecord.evidenceHistory);
|
|
177
|
+
const timelineEntries = projectReportTimeline(timeline);
|
|
178
|
+
const selection = currentCaseEvidence(caseRecord);
|
|
179
|
+
const latest = selection.snapshot;
|
|
213
180
|
const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
|
|
214
181
|
const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
|
|
215
182
|
const observedEffects = caseRecord.observedEffects ?? {
|
|
@@ -231,6 +198,7 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
231
198
|
application: generator,
|
|
232
199
|
case: {
|
|
233
200
|
id: caseRecord.id,
|
|
201
|
+
title: caseRecord.title ?? '',
|
|
234
202
|
domain: caseRecord.domain,
|
|
235
203
|
status: caseRecord.status,
|
|
236
204
|
disposition: caseRecord.disposition,
|
|
@@ -247,7 +215,9 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
247
215
|
currentAssessment,
|
|
248
216
|
evidenceTimeline: timelineEntries,
|
|
249
217
|
analystResponse: {
|
|
218
|
+
...(caseRecord.workflowMetadata === undefined ? {} : { workflowMetadata: structuredClone(caseRecord.workflowMetadata) }),
|
|
250
219
|
evidencePins: caseRecord.evidencePins.map((item) => ({ ...item, limitations: [...item.limitations] })),
|
|
220
|
+
...(caseRecord.evidenceLinks === undefined ? {} : { evidenceLinks: structuredClone(caseRecord.evidenceLinks) }),
|
|
251
221
|
decisions: caseRecord.decisions.map((item) => ({ ...item, evidencePinIds: [...item.evidencePinIds] })),
|
|
252
222
|
actions: caseRecord.actions.map((item) => ({
|
|
253
223
|
...item,
|
|
@@ -290,12 +260,42 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
290
260
|
})),
|
|
291
261
|
},
|
|
292
262
|
responseLifecycle,
|
|
293
|
-
limitations: LIMITATIONS_TEXT,
|
|
263
|
+
limitations: [LIMITATIONS_TEXT, selection.limitation, ...new Set(timeline.map((entry) => entry.orderingLimitation))].filter(Boolean).join(' '),
|
|
294
264
|
};
|
|
295
265
|
// --- Build Markdown ---
|
|
296
266
|
const md = buildMarkdown(json, options.includeAttribution !== false);
|
|
297
267
|
return { json, markdown: md };
|
|
298
268
|
}
|
|
269
|
+
function projectReportTimeline(timeline) {
|
|
270
|
+
return timeline.map(entry => ({
|
|
271
|
+
snapshot: pickKnownSnapshotFields(entry.snapshot), isBaseline: entry.isBaseline,
|
|
272
|
+
hasRepeatedObservation: entry.hasRepeatedObservation,
|
|
273
|
+
changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
|
|
274
|
+
hasIncomparableChange: entry.hasIncomparableChange, incomparableReasons: [...entry.incomparableReasons],
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/** Expected strict reader projection; historical disclosure wording is immutable. */
|
|
278
|
+
export function buildCaseReportVerificationProjection(caseRecord, options, schemaVersion) {
|
|
279
|
+
const generated = buildCaseReport(caseRecord, options).json;
|
|
280
|
+
if (schemaVersion === CASE_REPORT_SCHEMA_VERSION)
|
|
281
|
+
return generated;
|
|
282
|
+
if (caseRecord.evidenceHistory.some(snapshot => snapshot.webCollectionQuality !== undefined)) {
|
|
283
|
+
throw new TypeError('Published report formats cannot declare newer collection-quality fields.');
|
|
284
|
+
}
|
|
285
|
+
const current = { ...generated, evidenceTimeline: projectReportTimeline(publishedCaseEvidenceTimelineForVerification(caseRecord.evidenceHistory)) };
|
|
286
|
+
if (schemaVersion === LATEST_PUBLIC_CASE_REPORT_SCHEMA_VERSION)
|
|
287
|
+
return { ...current, schemaVersion };
|
|
288
|
+
if (schemaVersion !== PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION) {
|
|
289
|
+
throw new TypeError('No strict Case report projection is defined for this version.');
|
|
290
|
+
}
|
|
291
|
+
const { title: _title, ...publishedCase } = current.case;
|
|
292
|
+
return {
|
|
293
|
+
...current,
|
|
294
|
+
case: publishedCase,
|
|
295
|
+
schemaVersion,
|
|
296
|
+
limitations: PUBLISHED_V2_3_LIMITATIONS.join(' ') + current.limitations.slice(LIMITATIONS_TEXT.length),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
299
|
// ---------------------------------------------------------------------------
|
|
300
300
|
// Markdown generation
|
|
301
301
|
// ---------------------------------------------------------------------------
|
|
@@ -310,6 +310,11 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
310
310
|
const domain = escapeMarkdownInline(report.case.domain || 'unknown');
|
|
311
311
|
lines.push(`# Case Report: ${domain}`);
|
|
312
312
|
lines.push('');
|
|
313
|
+
if (report.case.title) {
|
|
314
|
+
lines.push(`**Incident:** ${escapeMarkdownInline(report.case.title)}`);
|
|
315
|
+
lines.push('');
|
|
316
|
+
}
|
|
317
|
+
lines.push(`**Case ID:** ${escapeMarkdownInline(report.case.id)}`);
|
|
313
318
|
// Metadata
|
|
314
319
|
lines.push(`**Generated:** ${escapeMarkdownInline(report.generatedAt)}`);
|
|
315
320
|
lines.push(`**Report schema:** ${escapeMarkdownInline(report.schema)} v${report.schemaVersion}`);
|
|
@@ -330,6 +335,17 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
330
335
|
if (report.case.tags.length > 0) {
|
|
331
336
|
lines.push(`| Tags | ${escapeMarkdownInline(report.case.tags.join(', '))} |`);
|
|
332
337
|
}
|
|
338
|
+
const caseTypes = caseTypeSummary(report.analystResponse.workflowMetadata ? { workflowMetadata: report.analystResponse.workflowMetadata } : {});
|
|
339
|
+
if (caseTypes)
|
|
340
|
+
lines.push(`| Case types | ${escapeMarkdownInline(caseTypes)} |`);
|
|
341
|
+
const context = report.analystResponse.workflowMetadata?.investigationContext;
|
|
342
|
+
if (context) {
|
|
343
|
+
lines.push(`| Investigation objective | ${escapeMarkdownInline(context.objective)} |`);
|
|
344
|
+
lines.push(`| Incident URL (${context.urlRetention === 'exact' ? 'exact' : 'origin only'}) | ${escapeMarkdownInline(context.incidentUrl)} |`);
|
|
345
|
+
}
|
|
346
|
+
for (const target of report.analystResponse.workflowMetadata?.incidentTargets ?? []) {
|
|
347
|
+
lines.push(`| Incident link (${target.state}) | ${escapeMarkdownInline(target.url)} |`);
|
|
348
|
+
}
|
|
333
349
|
lines.push(`| Notes included | ${report.case.notesIncluded ? 'Yes' : 'No'} |`);
|
|
334
350
|
lines.push('');
|
|
335
351
|
// Current assessment
|
|
@@ -338,7 +354,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
338
354
|
if (report.currentAssessment) {
|
|
339
355
|
const a = report.currentAssessment;
|
|
340
356
|
lines.push(`- **Availability:** ${escapeMarkdownInline(formatReportValue(a.availability))}`);
|
|
341
|
-
lines.push(`- **Risk score:** ${
|
|
357
|
+
lines.push(`- **Risk score:** ${readableRiskScore(a)}`);
|
|
342
358
|
lines.push(`- **Risk model:** ${a.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.riskModelVersion))}`}`);
|
|
343
359
|
lines.push(`- **Brand Profile context:** ${escapeMarkdownInline(formatReportValue(a.profileContextState))}`);
|
|
344
360
|
if (a.profileContextLimitation)
|
|
@@ -355,7 +371,9 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
355
371
|
lines.push(`- **Source:** ${escapeMarkdownInline(formatReportValue(a.source))}`);
|
|
356
372
|
}
|
|
357
373
|
else {
|
|
358
|
-
lines.push(
|
|
374
|
+
lines.push(report.evidenceTimeline.length
|
|
375
|
+
? 'Retained snapshots have no unique latest assessment. Review the timeline and limitations.'
|
|
376
|
+
: 'No evidence captured.');
|
|
359
377
|
}
|
|
360
378
|
lines.push('');
|
|
361
379
|
// Evidence timeline
|
|
@@ -391,7 +409,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
391
409
|
lines.push('**Key evidence:**');
|
|
392
410
|
lines.push('');
|
|
393
411
|
lines.push(`- Availability: ${escapeMarkdownInline(formatReportValue(snap.availability))}`);
|
|
394
|
-
lines.push(`- Risk score: ${
|
|
412
|
+
lines.push(`- Risk score: ${readableRiskScore(snap)}`);
|
|
395
413
|
lines.push(`- Risk model: ${snap.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(snap.riskModelVersion))}`}`);
|
|
396
414
|
lines.push(`- Brand Profile context: ${escapeMarkdownInline(formatReportValue(snap.profileContextState))}`);
|
|
397
415
|
if (snap.profileContextLimitation)
|
|
@@ -458,6 +476,8 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
458
476
|
lines.push('> Opportunity scores and factors use different or unversioned models, so their numeric difference is not treated as a domain change.');
|
|
459
477
|
if (reasons.includes('scan-depth'))
|
|
460
478
|
lines.push('> Capture depths differ, so unevaluated deep signals are not treated as additions or removals.');
|
|
479
|
+
if (reasons.includes('collection-quality'))
|
|
480
|
+
lines.push('> Missing or incomplete observations prevent comparison of affected registration, page, favicon or score fields.');
|
|
461
481
|
if (reasons.length === 0 || reasons.includes('other'))
|
|
462
482
|
lines.push('> The observations differ materially, but no reliable field-level comparison is available.');
|
|
463
483
|
lines.push('');
|
|
@@ -467,7 +487,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
467
487
|
lines.push('## Analyst Decision Packet');
|
|
468
488
|
lines.push('');
|
|
469
489
|
const response = report.analystResponse;
|
|
470
|
-
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) {
|
|
490
|
+
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) {
|
|
471
491
|
lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, response actions, independent effect reviews, closures, or manual investigation steps recorded.');
|
|
472
492
|
lines.push('');
|
|
473
493
|
}
|
|
@@ -478,7 +498,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
478
498
|
lines.push('No evidence pins recorded.');
|
|
479
499
|
for (const pin of response.evidencePins) {
|
|
480
500
|
lines.push(`- **${escapeMarkdownInline(pin.label)}:** ${escapeMarkdownInline(pin.value)}`);
|
|
481
|
-
lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt)}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
|
|
501
|
+
lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt ?? 'Time unavailable')}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
|
|
482
502
|
if (pin.limitations.length)
|
|
483
503
|
lines.push(` Limitations: ${escapeMarkdownInline(pin.limitations.join('; '))}`);
|
|
484
504
|
}
|
|
@@ -488,7 +508,7 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
488
508
|
if (!response.sightings.length)
|
|
489
509
|
lines.push('No source-qualified sightings recorded.');
|
|
490
510
|
for (const sighting of response.sightings) {
|
|
491
|
-
lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt)})`);
|
|
511
|
+
lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt ?? 'Observation time unavailable')})`);
|
|
492
512
|
lines.push(` Source: ${escapeMarkdownInline(sighting.source)}; class ${escapeMarkdownInline(sighting.sourceClass)}; completeness ${escapeMarkdownInline(sighting.completeness)}.`);
|
|
493
513
|
if (sighting.evidencePinId)
|
|
494
514
|
lines.push(` Evidence pin: ${escapeMarkdownInline(sighting.evidencePinId)}`);
|
|
@@ -512,6 +532,11 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
512
532
|
lines.push(`- **${escapeMarkdownInline(assertion.state)}:** ${escapeMarkdownInline(assertion.statement)}`);
|
|
513
533
|
if (assertion.rationale)
|
|
514
534
|
lines.push(` ${escapeMarkdownInline(assertion.rationale)}`);
|
|
535
|
+
if (assertion.recheck) {
|
|
536
|
+
lines.push(` Recheck target: ${escapeMarkdownInline(assertion.recheck.targetHostname)}; comparison conditions: ${escapeMarkdownInline(assertion.recheck.conditions)}`);
|
|
537
|
+
if (assertion.recheck.baselinePinId)
|
|
538
|
+
lines.push(` Baseline evidence pin: ${escapeMarkdownInline(assertion.recheck.baselinePinId)}`);
|
|
539
|
+
}
|
|
515
540
|
if (assertion.provenance) {
|
|
516
541
|
lines.push(` External provenance: ${escapeMarkdownInline(assertion.provenance.format.toUpperCase())}; source ${escapeMarkdownInline(assertion.provenance.sourceName)}; file SHA-256 ${escapeMarkdownInline(assertion.provenance.sourceDigestSha256)}.`);
|
|
517
542
|
if (assertion.provenance.publisher)
|
|
@@ -537,6 +562,12 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
537
562
|
}
|
|
538
563
|
lines.push('');
|
|
539
564
|
}
|
|
565
|
+
if (response.evidenceLinks?.length) {
|
|
566
|
+
lines.push('### Analyst-declared evidence relationships', '');
|
|
567
|
+
for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(response.evidenceLinks, response.evidencePins))
|
|
568
|
+
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.'] : []));
|
|
569
|
+
lines.push('These relationships record analyst attribution, not independent corroboration or added confidence.', '');
|
|
570
|
+
}
|
|
540
571
|
lines.push('### Decisions');
|
|
541
572
|
lines.push('');
|
|
542
573
|
if (!response.decisions.length)
|
|
@@ -562,6 +593,13 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
562
593
|
lines.push(` Route review due: ${escapeMarkdownInline(action.routeReviewAfter)}`);
|
|
563
594
|
if (action.originActionId)
|
|
564
595
|
lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
|
|
596
|
+
if (action.amendment)
|
|
597
|
+
lines.push(` Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}; request events: ${action.amendment.requestEventIds.map(escapeMarkdownInline).join(', ')}`);
|
|
598
|
+
for (const event of action.history)
|
|
599
|
+
if (event.evidenceRequest) {
|
|
600
|
+
const request = event.evidenceRequest;
|
|
601
|
+
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)}` : ''}`);
|
|
602
|
+
}
|
|
565
603
|
if (action.dueAt)
|
|
566
604
|
lines.push(` Due: ${escapeMarkdownInline(action.dueAt)}`);
|
|
567
605
|
if (action.followUpAt)
|
|
@@ -623,10 +661,19 @@ function buildMarkdown(report, includeAttribution) {
|
|
|
623
661
|
: `- Latest independently observed change time: Withheld because the independent change state is ${escapeMarkdownInline(report.responseLifecycle.observedChangeState)}.`);
|
|
624
662
|
if (observedEffect)
|
|
625
663
|
lines.push(`- Latest independent review: ${escapeMarkdownInline(observedEffect.state.replaceAll('_', ' '))} at ${escapeMarkdownInline(observedEffect.observedAt)} from ${escapeMarkdownInline(observedEffect.source)}.`);
|
|
664
|
+
else if (response.observedEffects.reviews.length)
|
|
665
|
+
lines.push('- A single latest independent review cannot be selected from the retained observation times. Review the individual records below.');
|
|
626
666
|
if (!response.observedEffects.reviews.length)
|
|
627
667
|
lines.push('- No independent observed-effect review recorded.');
|
|
628
668
|
for (const review of response.observedEffects.reviews) {
|
|
629
669
|
lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
|
|
670
|
+
if (review.recheck) {
|
|
671
|
+
lines.push(` Question ${escapeMarkdownInline(review.recheck.questionId)}: ${escapeMarkdownInline(review.recheck.question)}`);
|
|
672
|
+
lines.push(` Recheck target: ${escapeMarkdownInline(review.recheck.targetHostname)}; retained comparison conditions: ${escapeMarkdownInline(review.recheck.conditions)}`);
|
|
673
|
+
lines.push(` Condition comparison: ${escapeMarkdownInline(CASE_RECHECK_CONDITIONS[review.recheck.conditionsMatch])}`);
|
|
674
|
+
if (review.recheck.baselinePinId)
|
|
675
|
+
lines.push(` Baseline evidence pin: ${escapeMarkdownInline(review.recheck.baselinePinId)}`);
|
|
676
|
+
}
|
|
630
677
|
if (review.evidencePinId)
|
|
631
678
|
lines.push(` Evidence pin: ${escapeMarkdownInline(review.evidencePinId)}`);
|
|
632
679
|
if (review.sightingId)
|
|
@@ -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 && event.previousState === 'authorised' && 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
|
+
}
|