@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- 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 +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- 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 +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- 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 +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- 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 +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- 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-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -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 +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -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 +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -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 +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -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/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- 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 +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -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/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- 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/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- 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/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalArtifactJsonV2 } from "../../packages/evidence/artifact-integrity.mjs";
|
|
3
|
-
import { CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
|
|
3
|
+
import { CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
|
|
4
4
|
import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../../packages/cases/case-response-model.mjs";
|
|
5
|
-
import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
|
|
5
|
+
import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
|
|
6
6
|
const CASE_RESPONSE_PREFLIGHT_IDS_V6 = [
|
|
7
7
|
'required_incident_fields',
|
|
8
8
|
'evidence_pins',
|
|
@@ -21,6 +21,9 @@ const CASE_RESPONSE_PREFLIGHT_IDS_V8 = [
|
|
|
21
21
|
'action_tracking',
|
|
22
22
|
];
|
|
23
23
|
const PUBLIC_CASE_ACTION_STATES = ['planned', 'ready_for_review', 'submitted', 'acknowledged', 'resolved', 'closed'];
|
|
24
|
+
const PRE_PLATFORM_PROFILE_IDS = RESPONSE_PACKET_PROFILE_IDS.filter((id) => id !== 'application_platform');
|
|
25
|
+
const PRE_PLATFORM_CONTACT_KINDS = RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform');
|
|
26
|
+
const PRE_PLATFORM_ACTION_TYPES = CASE_ACTION_TYPES.filter((type) => type !== 'platform_report');
|
|
24
27
|
function legacyProfileRedactions(profileId) {
|
|
25
28
|
if (profileId === 'registrar' || profileId === 'registry') {
|
|
26
29
|
return ['Remove unnecessary personal registration data', 'Remove credentials, tokens, and URL fragments'];
|
|
@@ -68,7 +71,7 @@ function validateCaseResponsePacketV6(value) {
|
|
|
68
71
|
if (root.reviewRequired !== true || root.submissionPerformed !== false)
|
|
69
72
|
fail('Case-response packet review state');
|
|
70
73
|
const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
|
|
71
|
-
const profileId = enumeration(profile.id,
|
|
74
|
+
const profileId = enumeration(profile.id, PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
|
|
72
75
|
text(profile.label, 'Case-response profile label', 200);
|
|
73
76
|
text(profile.audience, 'Case-response profile audience', 300);
|
|
74
77
|
text(profile.subject, 'Case-response profile subject', 500);
|
|
@@ -124,7 +127,7 @@ function validateCaseResponsePacketV6(value) {
|
|
|
124
127
|
const contactKeys = new Set();
|
|
125
128
|
for (const candidate of contacts) {
|
|
126
129
|
const contact = exact(candidate, ['kind', 'contact', 'source', 'limitations'], 'Case-response contact');
|
|
127
|
-
const kind = enumeration(contact.kind,
|
|
130
|
+
const kind = enumeration(contact.kind, PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
|
|
128
131
|
const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
129
132
|
const contactKey = `${kind}\u0000${contactValue.toLowerCase()}`;
|
|
130
133
|
if (contactKeys.has(contactKey))
|
|
@@ -160,7 +163,7 @@ function validateCaseResponsePacketV6(value) {
|
|
|
160
163
|
const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
|
|
161
164
|
for (const candidate of history) {
|
|
162
165
|
const action = exact(candidate, ['type', 'recipient', 'contactSource', 'state', 'reference', 'outcome', 'createdAt', 'updatedAt'], 'Case-response escalation action');
|
|
163
|
-
enumeration(action.type,
|
|
166
|
+
enumeration(action.type, PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
|
|
164
167
|
text(action.recipient, 'Case-response action recipient', 320, true);
|
|
165
168
|
text(action.contactSource, 'Case-response action source', 120, true);
|
|
166
169
|
enumeration(action.state, PUBLIC_CASE_ACTION_STATES, 'Case-response action state');
|
|
@@ -221,14 +224,15 @@ function validateCurrentActionSummary(value, label) {
|
|
|
221
224
|
}
|
|
222
225
|
return total;
|
|
223
226
|
}
|
|
224
|
-
function
|
|
225
|
-
const
|
|
227
|
+
function validateVersionedCaseResponsePacket(value, version) {
|
|
228
|
+
const hasPlatformRoutes = version >= PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION;
|
|
229
|
+
const hasActionBinding = version >= PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION;
|
|
226
230
|
const root = exact(value, [
|
|
227
231
|
'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
|
|
228
232
|
'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
|
|
229
233
|
'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
|
|
230
234
|
'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
|
|
231
|
-
...(
|
|
235
|
+
...(hasActionBinding ? ['recipientRoute', 'actionBinding'] : []),
|
|
232
236
|
], `Case-response packet v${version}`);
|
|
233
237
|
if (root.schemaVersion !== version)
|
|
234
238
|
fail(`Case-response packet v${version}`);
|
|
@@ -236,14 +240,14 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
236
240
|
if (root.reviewRequired !== true || root.submissionPerformed !== false)
|
|
237
241
|
fail('Case-response packet review state');
|
|
238
242
|
const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
|
|
239
|
-
const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
|
|
243
|
+
const profileId = enumeration(profile.id, hasPlatformRoutes ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
|
|
240
244
|
text(profile.label, 'Case-response profile label', 200);
|
|
241
245
|
text(profile.audience, 'Case-response profile audience', 300);
|
|
242
246
|
text(profile.subject, 'Case-response profile subject', 500);
|
|
243
247
|
for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
|
|
244
248
|
strings(profile[key], `Case-response profile ${key}`, 24, 500);
|
|
245
249
|
const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
|
|
246
|
-
const expectedRedactions =
|
|
250
|
+
const expectedRedactions = hasActionBinding ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
|
|
247
251
|
if (!selectedProfile
|
|
248
252
|
|| profile.label !== selectedProfile.label
|
|
249
253
|
|| profile.audience !== selectedProfile.audience
|
|
@@ -282,8 +286,10 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
282
286
|
const contacts = array(root.contacts, 'Case-response contacts', MAX_RESPONSE_CONTACTS);
|
|
283
287
|
const contactKeys = new Set();
|
|
284
288
|
for (const candidate of contacts) {
|
|
285
|
-
const contact = exact(candidate, [
|
|
286
|
-
|
|
289
|
+
const contact = exact(candidate, [
|
|
290
|
+
'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
291
|
+
], 'Case-response contact');
|
|
292
|
+
const kind = enumeration(contact.kind, hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
|
|
287
293
|
const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
288
294
|
const key = `${kind}\u0000${contactValue.toLowerCase()}`;
|
|
289
295
|
if (contactKeys.has(key))
|
|
@@ -291,13 +297,18 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
291
297
|
contactKeys.add(key);
|
|
292
298
|
text(contact.source, 'Case-response contact source', 120);
|
|
293
299
|
iso(contact.observedAt, 'Case-response contact observedAt', true);
|
|
300
|
+
if (hasPlatformRoutes)
|
|
301
|
+
iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
|
|
294
302
|
const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
|
|
295
303
|
const routeAge = contact.observedAt === null
|
|
296
304
|
? null
|
|
297
305
|
: Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
|
|
306
|
+
const reviewExpired = hasPlatformRoutes && contact.reviewAfter !== null
|
|
307
|
+
&& Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
|
|
298
308
|
const expectedFreshness = routeAge === null
|
|
299
309
|
? 'unknown'
|
|
300
|
-
: routeAge < -300_000 ||
|
|
310
|
+
: routeAge < -300_000 || reviewExpired
|
|
311
|
+
|| (contact.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000)
|
|
301
312
|
? 'stale'
|
|
302
313
|
: 'current';
|
|
303
314
|
if (freshness !== expectedFreshness)
|
|
@@ -308,24 +319,29 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
308
319
|
let selectedActionId = null;
|
|
309
320
|
let lineageActionIds = [];
|
|
310
321
|
let lineageComplete = true;
|
|
311
|
-
if (
|
|
322
|
+
if (hasActionBinding) {
|
|
312
323
|
if (root.recipientRoute !== null) {
|
|
313
324
|
recipientRoute = exact(root.recipientRoute, [
|
|
314
|
-
'actionId', 'kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations',
|
|
325
|
+
'actionId', 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
315
326
|
], 'Case-response recipient route');
|
|
316
327
|
text(recipientRoute.actionId, 'Case-response recipient action id', 64);
|
|
317
|
-
enumeration(recipientRoute.kind, [...RESPONSE_CONTACT_KINDS, 'manual'], 'Case-response recipient kind');
|
|
328
|
+
enumeration(recipientRoute.kind, [...(hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
|
|
318
329
|
text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
319
330
|
text(recipientRoute.source, 'Case-response recipient source', 120);
|
|
320
331
|
iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
|
|
332
|
+
if (hasPlatformRoutes)
|
|
333
|
+
iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
|
|
321
334
|
const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
|
|
322
335
|
const routeAge = recipientRoute.observedAt === null
|
|
323
336
|
? null
|
|
324
337
|
: Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
|
|
338
|
+
const reviewExpired = hasPlatformRoutes && recipientRoute.reviewAfter !== null
|
|
339
|
+
&& Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
|
|
325
340
|
const expectedFreshness = routeAge === null
|
|
326
341
|
? 'unknown'
|
|
327
342
|
: routeAge < -MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS
|
|
328
|
-
||
|
|
343
|
+
|| reviewExpired
|
|
344
|
+
|| (recipientRoute.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000) ? 'stale' : 'current';
|
|
329
345
|
if (freshness !== expectedFreshness)
|
|
330
346
|
fail('Case-response recipient freshness');
|
|
331
347
|
strings(recipientRoute.limitations, 'Case-response recipient limitations', MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH);
|
|
@@ -354,6 +370,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
354
370
|
|| selectedContact.contact !== recipientRoute.contact
|
|
355
371
|
|| selectedContact.source !== recipientRoute.source
|
|
356
372
|
|| selectedContact.observedAt !== recipientRoute.observedAt
|
|
373
|
+
|| (hasPlatformRoutes && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
|
|
357
374
|
|| selectedContact.freshness !== recipientRoute.freshness)
|
|
358
375
|
fail('Case-response selected contact projection');
|
|
359
376
|
}
|
|
@@ -361,14 +378,16 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
361
378
|
const evidence = array(root.selectedEvidence, 'Case-response selected evidence', MAX_RESPONSE_SELECTED_EVIDENCE);
|
|
362
379
|
const evidenceIds = new Set();
|
|
363
380
|
for (const candidate of evidence) {
|
|
364
|
-
const item =
|
|
381
|
+
const item = exactOptional(candidate, ['id', 'label', 'source', 'observedAt', 'completeness', 'limitations'], version > PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION ? ['observationHostname'] : [], 'Case-response selected evidence');
|
|
365
382
|
const id = text(item.id, 'Case-response evidence id', 64);
|
|
366
383
|
if (evidenceIds.has(id))
|
|
367
384
|
fail('Case-response selected evidence');
|
|
368
385
|
evidenceIds.add(id);
|
|
369
386
|
text(item.label, 'Case-response evidence label', 80);
|
|
370
387
|
text(item.source, 'Case-response evidence source', 120);
|
|
371
|
-
|
|
388
|
+
if (item.observationHostname !== undefined)
|
|
389
|
+
domain(item.observationHostname, 'Case-response evidence hostname');
|
|
390
|
+
iso(item.observedAt, 'Case-response evidence observedAt', version > PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION);
|
|
372
391
|
enumeration(item.completeness, ['complete', 'partial', 'inconclusive', 'unknown'], 'Case-response evidence completeness');
|
|
373
392
|
strings(item.limitations, 'Case-response evidence limitations', 8, 240);
|
|
374
393
|
}
|
|
@@ -395,7 +414,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
395
414
|
...(profileId === 'internal_soc' ? [] : ['recipient_route']),
|
|
396
415
|
...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
|
|
397
416
|
]);
|
|
398
|
-
const requiredContact =
|
|
417
|
+
const requiredContact = hasActionBinding
|
|
399
418
|
? recipientRoute ?? undefined
|
|
400
419
|
: selectedProfile?.requiredContactKind
|
|
401
420
|
? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
|
|
@@ -404,7 +423,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
404
423
|
observed_behaviour: 'complete',
|
|
405
424
|
exact_url: 'complete',
|
|
406
425
|
observation_time: expectedObservationAge(incident.observedAt, root.generatedAt).refreshRecommended ? 'stale' : 'complete',
|
|
407
|
-
capture_provenance: evidence.length ? 'complete' : '
|
|
426
|
+
capture_provenance: !evidence.length ? 'unavailable' : evidence.every((candidate) => candidate.observedAt !== null) ? 'complete' : 'partial',
|
|
408
427
|
recipient_route: !requiredContact
|
|
409
428
|
? 'not_provided'
|
|
410
429
|
: requiredContact.freshness === 'stale'
|
|
@@ -490,8 +509,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
490
509
|
|| (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
|
|
491
510
|
fail('Case-response authorisation');
|
|
492
511
|
const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
|
|
493
|
-
const preflightIds =
|
|
494
|
-
if (preflight.version !== (
|
|
512
|
+
const preflightIds = hasActionBinding ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
|
|
513
|
+
if (preflight.version !== (hasActionBinding ? 3 : 2))
|
|
495
514
|
fail('Case-response preflight');
|
|
496
515
|
const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
|
|
497
516
|
const actualCounts = { block: 0, caution: 0, pass: 0 };
|
|
@@ -515,8 +534,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
515
534
|
const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
|
|
516
535
|
const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
|
|
517
536
|
const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
|
|
518
|
-
if ((!
|
|
519
|
-
|| (
|
|
537
|
+
if ((!hasActionBinding && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
|
|
538
|
+
|| (hasActionBinding && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
|
|
520
539
|
|| escalationHistoryOmitted !== actionTotal - history.length
|
|
521
540
|
|| (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
|
|
522
541
|
fail('Case-response action-history bounds');
|
|
@@ -528,18 +547,21 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
528
547
|
'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
|
|
529
548
|
'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
|
|
530
549
|
'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
|
|
531
|
-
...(
|
|
550
|
+
...(hasActionBinding ? ['routeObservedAt'] : []),
|
|
551
|
+
...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
|
|
532
552
|
], 'Case-response escalation action');
|
|
533
553
|
const actionId = text(action.actionId, 'Case-response action id', 64);
|
|
534
554
|
if (actionIds.has(actionId))
|
|
535
555
|
fail('Case-response action identity');
|
|
536
556
|
actionIds.add(actionId);
|
|
537
557
|
actionIdOrder.push(actionId);
|
|
538
|
-
enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
|
|
558
|
+
enumeration(action.type, hasPlatformRoutes ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
|
|
539
559
|
text(action.recipient, 'Case-response action recipient', 320);
|
|
540
560
|
text(action.contactSource, 'Case-response action source', 120);
|
|
541
|
-
if (
|
|
561
|
+
if (hasActionBinding)
|
|
542
562
|
iso(action.routeObservedAt, 'Case-response action route observedAt', true);
|
|
563
|
+
if (hasPlatformRoutes)
|
|
564
|
+
iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
|
|
543
565
|
enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
|
|
544
566
|
optionalText(action.reference, 'Case-response action reference', 500);
|
|
545
567
|
if (action.providerOutcome !== null)
|
|
@@ -638,7 +660,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
638
660
|
iso(action.createdAt, 'Case-response action createdAt');
|
|
639
661
|
iso(action.updatedAt, 'Case-response action updatedAt');
|
|
640
662
|
}
|
|
641
|
-
if (
|
|
663
|
+
if (hasActionBinding) {
|
|
642
664
|
if (actionIds.size !== lineageActionIds.length
|
|
643
665
|
|| actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
|
|
644
666
|
fail('Case-response action lineage');
|
|
@@ -670,11 +692,11 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
670
692
|
const latestProviderEvents = latestProviderTime === null
|
|
671
693
|
? []
|
|
672
694
|
: providerEvents.filter((event) => event.occurredAt === latestProviderTime);
|
|
673
|
-
const expectedLatestProvider = (
|
|
695
|
+
const expectedLatestProvider = (hasActionBinding ? lineageComplete : escalationHistoryOmitted === 0)
|
|
674
696
|
&& latestProviderEvents.length === 1 && latestProviderEvents[0].applied
|
|
675
697
|
? latestProviderEvents[0]
|
|
676
698
|
: null;
|
|
677
|
-
const expectedProviderState = (
|
|
699
|
+
const expectedProviderState = (hasActionBinding ? !lineageComplete : escalationHistoryOmitted > 0)
|
|
678
700
|
? 'ambiguous'
|
|
679
701
|
: providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
|
|
680
702
|
if (providerOutcomeState !== expectedProviderState
|
|
@@ -707,7 +729,10 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
707
729
|
strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
|
|
708
730
|
const reviewedInputs = {
|
|
709
731
|
contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
|
|
710
|
-
version:
|
|
732
|
+
version: version === CASE_RESPONSE_PACKET_VERSION
|
|
733
|
+
? CASE_RESPONSE_REVIEW_INPUTS_VERSION
|
|
734
|
+
: hasPlatformRoutes ? PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION
|
|
735
|
+
: hasActionBinding ? PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
|
|
711
736
|
profile: {
|
|
712
737
|
id: profile.id,
|
|
713
738
|
label: profile.label,
|
|
@@ -721,7 +746,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
721
746
|
case: root.case,
|
|
722
747
|
incident: root.incident,
|
|
723
748
|
contacts: root.contacts,
|
|
724
|
-
...(
|
|
749
|
+
...(hasActionBinding ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
|
|
725
750
|
selectedEvidence: root.selectedEvidence,
|
|
726
751
|
contradictions: root.contradictions,
|
|
727
752
|
readiness: root.readiness,
|
|
@@ -759,7 +784,13 @@ export function validateCaseResponsePacket(value) {
|
|
|
759
784
|
if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
|
|
760
785
|
return validateCaseResponsePacketV6(value);
|
|
761
786
|
if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
|
|
762
|
-
return
|
|
787
|
+
return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
|
|
788
|
+
}
|
|
789
|
+
if (value.schemaVersion === PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION) {
|
|
790
|
+
return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION);
|
|
791
|
+
}
|
|
792
|
+
if (value.schemaVersion === PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION) {
|
|
793
|
+
return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION);
|
|
763
794
|
}
|
|
764
795
|
if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
|
|
765
796
|
throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
|
|
@@ -767,5 +798,5 @@ export function validateCaseResponsePacket(value) {
|
|
|
767
798
|
if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
|
|
768
799
|
throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is newer than the supported version ${CASE_RESPONSE_PACKET_VERSION}; no data was changed.`);
|
|
769
800
|
}
|
|
770
|
-
|
|
801
|
+
validateVersionedCaseResponsePacket(value, CASE_RESPONSE_PACKET_VERSION);
|
|
771
802
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { investigationCapsuleContracts, MAX_LOOKUP_ASSET_NODES, MAX_LOOKUP_ASSET_EDGES, MAX_LOOKUP_ASSET_LIMITATIONS, MAX_LOOKUP_ASSET_INPUT_ROWS } from "../../packages/contracts/investigation-portability.mjs";
|
|
2
|
+
import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, investigationCapsuleProjectionsMatch, } from "../../packages/investigation/investigation-capsule.mjs";
|
|
2
3
|
import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../../packages/investigation/lookup-asset-graph.mjs";
|
|
3
|
-
import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA,
|
|
4
|
+
import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../../packages/investigation/lookup-investigation-brief.mjs";
|
|
4
5
|
import { DECISION_FACT_CONSISTENCY_STATES, DECISION_FACT_EVIDENCE_STATES, DECISION_FACT_FRESHNESS_STATES, DECISION_FACT_PROVENANCE_STATES, DECISION_FACT_PROJECTION_VERSION, DECISION_FACT_VERSION, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_BYTES, MAX_DECISION_FACT_PROJECTION_FACTS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCES, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACTS, decisionFactCompleteness, } from "../../packages/evidence/decision-fact.mjs";
|
|
5
6
|
import { MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_DECISION_PIN_REFERENCES, } from "../../packages/cases/case-response-model.mjs";
|
|
6
7
|
import { array, boolean, digest, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
|
|
@@ -221,10 +222,10 @@ function validateDecisionFactProjection(value) {
|
|
|
221
222
|
fail('Investigation capsule Decision Fact projection byte limit');
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
|
-
function validateCurrentBrief(value) {
|
|
225
|
+
function validateCurrentBrief(value, expectedVersion) {
|
|
225
226
|
const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'decisionFacts', 'relationships', 'limitations'], 'Investigation capsule brief');
|
|
226
227
|
if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
|
|
227
|
-
|| brief.schemaVersion !==
|
|
228
|
+
|| brief.schemaVersion !== expectedVersion)
|
|
228
229
|
fail('Investigation capsule brief');
|
|
229
230
|
iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
|
|
230
231
|
text(brief.target, 'Investigation capsule brief target', 253);
|
|
@@ -250,21 +251,22 @@ function validateCurrentBrief(value) {
|
|
|
250
251
|
integer(thresholds[key], 'Investigation capsule freshness threshold', 0, 3650);
|
|
251
252
|
validateDecisionFactProjection(brief.decisionFacts);
|
|
252
253
|
const relationships = exact(brief.relationships, ['nodes', 'edges', 'truncated', 'kinds'], 'Investigation capsule relationship summary');
|
|
253
|
-
integer(relationships.nodes, 'Investigation capsule relationship node count', 0, 72);
|
|
254
|
-
integer(relationships.edges, 'Investigation capsule relationship edge count', 0, 120);
|
|
254
|
+
integer(relationships.nodes, 'Investigation capsule relationship node count', 0, expectedVersion === 2 ? 72 : MAX_LOOKUP_ASSET_NODES);
|
|
255
|
+
integer(relationships.edges, 'Investigation capsule relationship edge count', 0, expectedVersion === 2 ? 120 : MAX_LOOKUP_ASSET_EDGES);
|
|
255
256
|
boolean(relationships.truncated, 'Investigation capsule relationship truncation');
|
|
256
|
-
strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
|
|
257
|
+
strings(relationships.kinds, 'Investigation capsule relationship kinds', expectedVersion === 2 ? 12 : 64, 320);
|
|
257
258
|
strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
|
|
258
259
|
if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
|
|
259
260
|
fail('Investigation capsule brief byte limit');
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
|
-
function validateGraph(value) {
|
|
263
|
-
const
|
|
264
|
-
|
|
263
|
+
function validateGraph(value, expectedVersion) {
|
|
264
|
+
const current = expectedVersion === LOOKUP_ASSET_GRAPH_VERSION;
|
|
265
|
+
const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations', ...(current ? ['coverage'] : [])], 'Investigation capsule graph');
|
|
266
|
+
if (graph.version !== expectedVersion)
|
|
265
267
|
fail('Investigation capsule graph');
|
|
266
268
|
text(graph.targetId, 'Investigation capsule graph target', 160);
|
|
267
|
-
const nodes = array(graph.nodes, 'Investigation capsule graph nodes', 72, 1);
|
|
269
|
+
const nodes = array(graph.nodes, 'Investigation capsule graph nodes', current ? MAX_LOOKUP_ASSET_NODES : 72, 1);
|
|
268
270
|
const nodeIds = new Set();
|
|
269
271
|
for (const candidate of nodes) {
|
|
270
272
|
const node = exact(candidate, ['id', 'label', 'kind', 'detail'], 'Investigation capsule graph node');
|
|
@@ -279,7 +281,7 @@ function validateGraph(value) {
|
|
|
279
281
|
if (!nodeIds.has(graph.targetId))
|
|
280
282
|
fail('Investigation capsule graph target');
|
|
281
283
|
const sourceIds = new Set();
|
|
282
|
-
for (const candidate of array(graph.sources, 'Investigation capsule graph sources', 32)) {
|
|
284
|
+
for (const candidate of array(graph.sources, 'Investigation capsule graph sources', current ? 128 : 32)) {
|
|
283
285
|
const source = exact(candidate, ['id', 'label', 'href', 'observedAt', 'completeness', 'limitations'], 'Investigation capsule graph source');
|
|
284
286
|
const id = text(source.id, 'Investigation capsule graph source id', 160);
|
|
285
287
|
if (sourceIds.has(id))
|
|
@@ -290,9 +292,9 @@ function validateGraph(value) {
|
|
|
290
292
|
fail('Investigation capsule graph source href');
|
|
291
293
|
iso(source.observedAt, 'Investigation capsule graph source observedAt', true);
|
|
292
294
|
enumeration(source.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph source completeness');
|
|
293
|
-
strings(source.limitations, 'Investigation capsule graph source limitations', 5, 320);
|
|
295
|
+
strings(source.limitations, 'Investigation capsule graph source limitations', current ? MAX_LOOKUP_ASSET_LIMITATIONS : 5, 320);
|
|
294
296
|
}
|
|
295
|
-
const edges = array(graph.edges, 'Investigation capsule graph edges', 120);
|
|
297
|
+
const edges = array(graph.edges, 'Investigation capsule graph edges', current ? MAX_LOOKUP_ASSET_EDGES : 120);
|
|
296
298
|
const edgeIds = new Set();
|
|
297
299
|
for (const candidate of edges) {
|
|
298
300
|
const edge = exactOptional(candidate, ['id', 'sourceId', 'source', 'target', 'kind', 'label', 'sourceLabel', 'observedAt', 'completeness', 'limitations', 'lenses', 'href'], ['boundary'], 'Investigation capsule graph edge');
|
|
@@ -310,13 +312,36 @@ function validateGraph(value) {
|
|
|
310
312
|
text(edge.sourceLabel, 'Investigation capsule graph edge source label', 320);
|
|
311
313
|
iso(edge.observedAt, 'Investigation capsule graph edge observedAt', true);
|
|
312
314
|
enumeration(edge.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph edge completeness');
|
|
313
|
-
strings(edge.limitations, 'Investigation capsule graph edge limitations', 5, 320);
|
|
315
|
+
strings(edge.limitations, 'Investigation capsule graph edge limitations', current ? MAX_LOOKUP_ASSET_LIMITATIONS : 5, 320);
|
|
314
316
|
strings(edge.lenses, 'Investigation capsule graph edge lenses', 4, 40).forEach((lens) => enumeration(lens, ['all', 'identity', 'delegation', 'certificate'], 'Investigation capsule graph edge lens'));
|
|
315
317
|
if (typeof edge.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(edge.href))
|
|
316
318
|
fail('Investigation capsule graph edge href');
|
|
317
319
|
if (edge.boundary !== undefined)
|
|
318
320
|
enumeration(edge.boundary, ['external', 'reviewed_profile', 'same_origin', 'same_registrable_domain', 'unresolved'], 'Investigation capsule graph edge boundary');
|
|
319
321
|
}
|
|
322
|
+
if (current) {
|
|
323
|
+
const coverage = exact(graph.coverage, ['inputs'], 'Investigation capsule graph coverage');
|
|
324
|
+
const ids = new Set();
|
|
325
|
+
let incomplete = false;
|
|
326
|
+
for (const candidate of array(coverage.inputs, 'Investigation capsule graph input coverage', MAX_LOOKUP_ASSET_INPUT_ROWS)) {
|
|
327
|
+
const row = exact(candidate, ['id', 'supplied', 'inspected', 'admitted', 'invalid', 'duplicates', 'omitted'], 'Investigation capsule graph input coverage row');
|
|
328
|
+
const id = text(row.id, 'Investigation capsule graph input id', 160);
|
|
329
|
+
if (ids.has(id))
|
|
330
|
+
fail('Investigation capsule graph input ids');
|
|
331
|
+
ids.add(id);
|
|
332
|
+
const supplied = integer(row.supplied, 'Investigation capsule graph supplied input', 0, 0xffff_ffff);
|
|
333
|
+
const inspected = integer(row.inspected, 'Investigation capsule graph inspected input', 0, 800);
|
|
334
|
+
const admitted = integer(row.admitted, 'Investigation capsule graph admitted input', 0, inspected);
|
|
335
|
+
const invalid = integer(row.invalid, 'Investigation capsule graph invalid input', 0, inspected);
|
|
336
|
+
const duplicates = integer(row.duplicates, 'Investigation capsule graph duplicate input', 0, inspected);
|
|
337
|
+
const omitted = integer(row.omitted, 'Investigation capsule graph omitted input', 0, inspected);
|
|
338
|
+
if (inspected > supplied || admitted + invalid + duplicates + omitted !== inspected)
|
|
339
|
+
fail('Investigation capsule graph input accounting');
|
|
340
|
+
incomplete ||= supplied > inspected || invalid > 0 || omitted > 0;
|
|
341
|
+
}
|
|
342
|
+
if (graph.truncated !== incomplete)
|
|
343
|
+
fail('Investigation capsule graph coverage linkage');
|
|
344
|
+
}
|
|
320
345
|
boolean(graph.truncated, 'Investigation capsule graph truncation');
|
|
321
346
|
strings(graph.limitations, 'Investigation capsule graph limitations', 20, 320);
|
|
322
347
|
}
|
|
@@ -349,8 +374,8 @@ function validateAnalystRecords(value) {
|
|
|
349
374
|
}
|
|
350
375
|
export function validateInvestigationCapsuleStructure(value) {
|
|
351
376
|
const root = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'application', 'target', 'sourceContracts', 'investigationBrief', 'graphSnapshot', 'analystRecords', 'integrity', 'limitations'], 'Investigation capsule');
|
|
352
|
-
|
|
353
|
-
|
|
377
|
+
const expected = investigationCapsuleContracts(root.schemaVersion);
|
|
378
|
+
if (!expected)
|
|
354
379
|
fail('Investigation capsule');
|
|
355
380
|
iso(root.generatedAt, 'Investigation capsule generatedAt');
|
|
356
381
|
const application = exact(root.application, ['name', 'version'], 'Investigation capsule application');
|
|
@@ -376,9 +401,7 @@ export function validateInvestigationCapsuleStructure(value) {
|
|
|
376
401
|
const expectedContractIds = root.analystRecords === null
|
|
377
402
|
? ['lookup-evidence', 'investigation-brief', 'asset-graph']
|
|
378
403
|
: ['lookup-evidence', 'investigation-brief', 'asset-graph', 'analyst-records'];
|
|
379
|
-
const expectedBriefVersion =
|
|
380
|
-
? LOOKUP_INVESTIGATION_BRIEF_VERSION
|
|
381
|
-
: PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION;
|
|
404
|
+
const expectedBriefVersion = expected.brief;
|
|
382
405
|
if (!sameValues(contracts.map((candidate) => candidate.id), expectedContractIds))
|
|
383
406
|
fail('Investigation capsule source contracts');
|
|
384
407
|
if (!byId.has('lookup-evidence') || !byId.has('investigation-brief') || !byId.has('asset-graph')
|
|
@@ -387,25 +410,23 @@ export function validateInvestigationCapsuleStructure(value) {
|
|
|
387
410
|
|| byId.get('investigation-brief')?.version !== expectedBriefVersion
|
|
388
411
|
|| byId.get('investigation-brief')?.embedded !== true
|
|
389
412
|
|| byId.get('asset-graph')?.schema !== LOOKUP_ASSET_GRAPH_SCHEMA
|
|
390
|
-
|| byId.get('asset-graph')?.version !==
|
|
413
|
+
|| byId.get('asset-graph')?.version !== expected.graph
|
|
391
414
|
|| byId.get('asset-graph')?.embedded !== true
|
|
392
415
|
|| (byId.has('analyst-records') && (byId.get('analyst-records')?.schema !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA
|
|
393
416
|
|| byId.get('analyst-records')?.version !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION
|
|
394
417
|
|| byId.get('analyst-records')?.embedded !== true)))
|
|
395
418
|
fail('Investigation capsule source contracts');
|
|
396
|
-
if (
|
|
397
|
-
validateCurrentBrief(root.investigationBrief);
|
|
398
|
-
else
|
|
419
|
+
if (expectedBriefVersion === 1)
|
|
399
420
|
validatePublicBrief(root.investigationBrief);
|
|
400
|
-
|
|
421
|
+
else
|
|
422
|
+
validateCurrentBrief(root.investigationBrief, expectedBriefVersion);
|
|
423
|
+
validateGraph(root.graphSnapshot, expected.graph);
|
|
401
424
|
validateAnalystRecords(root.analystRecords);
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
if (target
|
|
405
|
-
|| graph.targetId === undefined
|
|
406
|
-
|| brief.relationships.nodes !== graph.nodes.length
|
|
407
|
-
|| brief.relationships.edges !== graph.edges.length)
|
|
425
|
+
// The version-specific validators above independently establish these common fields.
|
|
426
|
+
const projections = root;
|
|
427
|
+
if (!investigationCapsuleProjectionsMatch(projections.target, projections.investigationBrief, projections.graphSnapshot)) {
|
|
408
428
|
fail('Investigation capsule projection linkage');
|
|
429
|
+
}
|
|
409
430
|
const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'briefDigest', 'graphDigest', 'analystRecordsDigest', 'digestSha256'], 'Investigation capsule integrity');
|
|
410
431
|
if (integrity.algorithm !== 'SHA-256')
|
|
411
432
|
fail('Investigation capsule integrity');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../../lib/evidence-export.mjs";
|
|
2
|
+
import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION, PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION, REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../../lib/evidence-export.mjs";
|
|
3
|
+
import { validLookupObservationScope } from "../../packages/evidence/lookup-target.mjs";
|
|
3
4
|
import { registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "../../lib/registrar-standing-contract.mjs";
|
|
4
5
|
import { compareRegistrySources } from "../../lib/registry-comparison.mjs";
|
|
5
6
|
import { buildRegistryInsights } from "../../lib/registry-insights.mjs";
|
|
@@ -649,24 +650,29 @@ export function validateLookupEvidenceArtifactStructure(value) {
|
|
|
649
650
|
}
|
|
650
651
|
validateLookupEvidenceSecurityTxt(sources.securityTxt);
|
|
651
652
|
validateLookupEvidenceSslbl(sources.sslbl);
|
|
652
|
-
const analysis = exact(root.analysis, version >=
|
|
653
|
+
const analysis = exact(root.analysis, version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
|
|
653
654
|
? ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison', 'registrarStanding']
|
|
654
655
|
: ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison'], 'Lookup evidence analysis');
|
|
655
|
-
if (version >=
|
|
656
|
+
if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
|
|
656
657
|
&& analysis.registrarStanding !== null
|
|
657
658
|
&& !validRegistrarStanding(analysis.registrarStanding))
|
|
658
659
|
fail('Lookup evidence registrar standing');
|
|
659
|
-
if (version >=
|
|
660
|
+
if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
|
|
660
661
|
&& analysis.registrarStanding !== null
|
|
661
662
|
&& !registrarStandingObservedBy(analysis.registrarStanding, root.generatedAt)) {
|
|
662
663
|
fail('Lookup evidence registrar standing observation time');
|
|
663
664
|
}
|
|
664
665
|
if (analysis.availability !== null) {
|
|
665
666
|
const currentAvailability = version >= PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION;
|
|
666
|
-
const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability
|
|
667
|
+
const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability
|
|
668
|
+
? [...LOOKUP_AVAILABILITY_ANALYSIS_KEYS, ...(version >= HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION ? ['observationHostname', 'webObservationMode'] : [])]
|
|
669
|
+
: PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
|
|
667
670
|
if (currentAvailability && availability.registryContactsExcluded !== true) {
|
|
668
671
|
fail('Lookup evidence availability contact exclusion');
|
|
669
672
|
}
|
|
673
|
+
if (!validLookupObservationScope(availability, query)) {
|
|
674
|
+
fail('Lookup evidence observation hostname binding');
|
|
675
|
+
}
|
|
670
676
|
validateLookupEvidenceHomepageMetadata(availability, version);
|
|
671
677
|
if (!isDeepStrictEqual(analysis.availability, currentAvailability
|
|
672
678
|
? projectLookupEvidenceAvailability(analysis.availability)
|
|
@@ -684,7 +690,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
|
|
|
684
690
|
const whois = record(sources.whois, 'Lookup evidence WHOIS source');
|
|
685
691
|
const rdapParsed = ['success', 'partial'].includes(rdapDiagnosticStatus) ? rdap.parsed : null;
|
|
686
692
|
const whoisParsed = ['complete', 'partial'].includes(whoisDiagnosticStatus) ? whois.parsed : null;
|
|
687
|
-
if (version >=
|
|
693
|
+
if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
|
|
688
694
|
&& validRegistrarStanding(analysis.registrarStanding)
|
|
689
695
|
&& analysis.registrarStanding.ianaId !== resolveRegistrarIanaId(rdapParsed, whoisParsed)) {
|
|
690
696
|
fail('Lookup evidence registrar standing source binding');
|