@slicedearth/whoisleuth-cli 2.3.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 +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/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- 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 +83 -61
- 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-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- 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 +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -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 +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -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/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-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 +45 -0
- package/cli/runner.mjs +55 -6
- 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 +76 -30
- package/docs/cli.md +289 -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 +43 -3
- 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-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- 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 +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- 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 +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- 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-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 +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -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 +117 -146
- 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 +111 -71
- 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/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 +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- 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 +38 -587
- 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 +25 -0
- 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/page-similarity.mjs +2 -1
- 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 +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -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 +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 +160 -225
- 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 +155 -27
- 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 +89 -49
- 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 +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/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- 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 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- 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,
|
|
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',
|
|
@@ -224,15 +224,15 @@ function validateCurrentActionSummary(value, label) {
|
|
|
224
224
|
}
|
|
225
225
|
return total;
|
|
226
226
|
}
|
|
227
|
-
function
|
|
228
|
-
const
|
|
229
|
-
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;
|
|
230
230
|
const root = exact(value, [
|
|
231
231
|
'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
|
|
232
232
|
'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
|
|
233
233
|
'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
|
|
234
234
|
'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
|
|
235
|
-
...(
|
|
235
|
+
...(hasActionBinding ? ['recipientRoute', 'actionBinding'] : []),
|
|
236
236
|
], `Case-response packet v${version}`);
|
|
237
237
|
if (root.schemaVersion !== version)
|
|
238
238
|
fail(`Case-response packet v${version}`);
|
|
@@ -240,14 +240,14 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
240
240
|
if (root.reviewRequired !== true || root.submissionPerformed !== false)
|
|
241
241
|
fail('Case-response packet review state');
|
|
242
242
|
const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
|
|
243
|
-
const profileId = enumeration(profile.id,
|
|
243
|
+
const profileId = enumeration(profile.id, hasPlatformRoutes ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
|
|
244
244
|
text(profile.label, 'Case-response profile label', 200);
|
|
245
245
|
text(profile.audience, 'Case-response profile audience', 300);
|
|
246
246
|
text(profile.subject, 'Case-response profile subject', 500);
|
|
247
247
|
for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
|
|
248
248
|
strings(profile[key], `Case-response profile ${key}`, 24, 500);
|
|
249
249
|
const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
|
|
250
|
-
const expectedRedactions =
|
|
250
|
+
const expectedRedactions = hasActionBinding ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
|
|
251
251
|
if (!selectedProfile
|
|
252
252
|
|| profile.label !== selectedProfile.label
|
|
253
253
|
|| profile.audience !== selectedProfile.audience
|
|
@@ -287,9 +287,9 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
287
287
|
const contactKeys = new Set();
|
|
288
288
|
for (const candidate of contacts) {
|
|
289
289
|
const contact = exact(candidate, [
|
|
290
|
-
'kind', 'contact', 'source', 'observedAt', ...(
|
|
290
|
+
'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
291
291
|
], 'Case-response contact');
|
|
292
|
-
const kind = enumeration(contact.kind,
|
|
292
|
+
const kind = enumeration(contact.kind, hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
|
|
293
293
|
const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
294
294
|
const key = `${kind}\u0000${contactValue.toLowerCase()}`;
|
|
295
295
|
if (contactKeys.has(key))
|
|
@@ -297,13 +297,13 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
297
297
|
contactKeys.add(key);
|
|
298
298
|
text(contact.source, 'Case-response contact source', 120);
|
|
299
299
|
iso(contact.observedAt, 'Case-response contact observedAt', true);
|
|
300
|
-
if (
|
|
300
|
+
if (hasPlatformRoutes)
|
|
301
301
|
iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
|
|
302
302
|
const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
|
|
303
303
|
const routeAge = contact.observedAt === null
|
|
304
304
|
? null
|
|
305
305
|
: Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
|
|
306
|
-
const reviewExpired =
|
|
306
|
+
const reviewExpired = hasPlatformRoutes && contact.reviewAfter !== null
|
|
307
307
|
&& Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
|
|
308
308
|
const expectedFreshness = routeAge === null
|
|
309
309
|
? 'unknown'
|
|
@@ -319,23 +319,23 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
319
319
|
let selectedActionId = null;
|
|
320
320
|
let lineageActionIds = [];
|
|
321
321
|
let lineageComplete = true;
|
|
322
|
-
if (
|
|
322
|
+
if (hasActionBinding) {
|
|
323
323
|
if (root.recipientRoute !== null) {
|
|
324
324
|
recipientRoute = exact(root.recipientRoute, [
|
|
325
|
-
'actionId', 'kind', 'contact', 'source', 'observedAt', ...(
|
|
325
|
+
'actionId', 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
326
326
|
], 'Case-response recipient route');
|
|
327
327
|
text(recipientRoute.actionId, 'Case-response recipient action id', 64);
|
|
328
|
-
enumeration(recipientRoute.kind, [...(
|
|
328
|
+
enumeration(recipientRoute.kind, [...(hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
|
|
329
329
|
text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
330
330
|
text(recipientRoute.source, 'Case-response recipient source', 120);
|
|
331
331
|
iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
|
|
332
|
-
if (
|
|
332
|
+
if (hasPlatformRoutes)
|
|
333
333
|
iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
|
|
334
334
|
const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
|
|
335
335
|
const routeAge = recipientRoute.observedAt === null
|
|
336
336
|
? null
|
|
337
337
|
: Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
|
|
338
|
-
const reviewExpired =
|
|
338
|
+
const reviewExpired = hasPlatformRoutes && recipientRoute.reviewAfter !== null
|
|
339
339
|
&& Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
|
|
340
340
|
const expectedFreshness = routeAge === null
|
|
341
341
|
? 'unknown'
|
|
@@ -370,7 +370,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
370
370
|
|| selectedContact.contact !== recipientRoute.contact
|
|
371
371
|
|| selectedContact.source !== recipientRoute.source
|
|
372
372
|
|| selectedContact.observedAt !== recipientRoute.observedAt
|
|
373
|
-
|| (
|
|
373
|
+
|| (hasPlatformRoutes && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
|
|
374
374
|
|| selectedContact.freshness !== recipientRoute.freshness)
|
|
375
375
|
fail('Case-response selected contact projection');
|
|
376
376
|
}
|
|
@@ -378,14 +378,16 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
378
378
|
const evidence = array(root.selectedEvidence, 'Case-response selected evidence', MAX_RESPONSE_SELECTED_EVIDENCE);
|
|
379
379
|
const evidenceIds = new Set();
|
|
380
380
|
for (const candidate of evidence) {
|
|
381
|
-
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');
|
|
382
382
|
const id = text(item.id, 'Case-response evidence id', 64);
|
|
383
383
|
if (evidenceIds.has(id))
|
|
384
384
|
fail('Case-response selected evidence');
|
|
385
385
|
evidenceIds.add(id);
|
|
386
386
|
text(item.label, 'Case-response evidence label', 80);
|
|
387
387
|
text(item.source, 'Case-response evidence source', 120);
|
|
388
|
-
|
|
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);
|
|
389
391
|
enumeration(item.completeness, ['complete', 'partial', 'inconclusive', 'unknown'], 'Case-response evidence completeness');
|
|
390
392
|
strings(item.limitations, 'Case-response evidence limitations', 8, 240);
|
|
391
393
|
}
|
|
@@ -412,7 +414,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
412
414
|
...(profileId === 'internal_soc' ? [] : ['recipient_route']),
|
|
413
415
|
...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
|
|
414
416
|
]);
|
|
415
|
-
const requiredContact =
|
|
417
|
+
const requiredContact = hasActionBinding
|
|
416
418
|
? recipientRoute ?? undefined
|
|
417
419
|
: selectedProfile?.requiredContactKind
|
|
418
420
|
? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
|
|
@@ -421,7 +423,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
421
423
|
observed_behaviour: 'complete',
|
|
422
424
|
exact_url: 'complete',
|
|
423
425
|
observation_time: expectedObservationAge(incident.observedAt, root.generatedAt).refreshRecommended ? 'stale' : 'complete',
|
|
424
|
-
capture_provenance: evidence.length ? 'complete' : '
|
|
426
|
+
capture_provenance: !evidence.length ? 'unavailable' : evidence.every((candidate) => candidate.observedAt !== null) ? 'complete' : 'partial',
|
|
425
427
|
recipient_route: !requiredContact
|
|
426
428
|
? 'not_provided'
|
|
427
429
|
: requiredContact.freshness === 'stale'
|
|
@@ -507,8 +509,8 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
507
509
|
|| (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
|
|
508
510
|
fail('Case-response authorisation');
|
|
509
511
|
const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
|
|
510
|
-
const preflightIds =
|
|
511
|
-
if (preflight.version !== (
|
|
512
|
+
const preflightIds = hasActionBinding ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
|
|
513
|
+
if (preflight.version !== (hasActionBinding ? 3 : 2))
|
|
512
514
|
fail('Case-response preflight');
|
|
513
515
|
const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
|
|
514
516
|
const actualCounts = { block: 0, caution: 0, pass: 0 };
|
|
@@ -532,8 +534,8 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
532
534
|
const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
|
|
533
535
|
const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
|
|
534
536
|
const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
|
|
535
|
-
if ((!
|
|
536
|
-
|| (
|
|
537
|
+
if ((!hasActionBinding && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
|
|
538
|
+
|| (hasActionBinding && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
|
|
537
539
|
|| escalationHistoryOmitted !== actionTotal - history.length
|
|
538
540
|
|| (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
|
|
539
541
|
fail('Case-response action-history bounds');
|
|
@@ -545,20 +547,20 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
545
547
|
'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
|
|
546
548
|
'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
|
|
547
549
|
'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
|
|
548
|
-
...(
|
|
549
|
-
...(
|
|
550
|
+
...(hasActionBinding ? ['routeObservedAt'] : []),
|
|
551
|
+
...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
|
|
550
552
|
], 'Case-response escalation action');
|
|
551
553
|
const actionId = text(action.actionId, 'Case-response action id', 64);
|
|
552
554
|
if (actionIds.has(actionId))
|
|
553
555
|
fail('Case-response action identity');
|
|
554
556
|
actionIds.add(actionId);
|
|
555
557
|
actionIdOrder.push(actionId);
|
|
556
|
-
enumeration(action.type,
|
|
558
|
+
enumeration(action.type, hasPlatformRoutes ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
|
|
557
559
|
text(action.recipient, 'Case-response action recipient', 320);
|
|
558
560
|
text(action.contactSource, 'Case-response action source', 120);
|
|
559
|
-
if (
|
|
561
|
+
if (hasActionBinding)
|
|
560
562
|
iso(action.routeObservedAt, 'Case-response action route observedAt', true);
|
|
561
|
-
if (
|
|
563
|
+
if (hasPlatformRoutes)
|
|
562
564
|
iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
|
|
563
565
|
enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
|
|
564
566
|
optionalText(action.reference, 'Case-response action reference', 500);
|
|
@@ -658,7 +660,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
658
660
|
iso(action.createdAt, 'Case-response action createdAt');
|
|
659
661
|
iso(action.updatedAt, 'Case-response action updatedAt');
|
|
660
662
|
}
|
|
661
|
-
if (
|
|
663
|
+
if (hasActionBinding) {
|
|
662
664
|
if (actionIds.size !== lineageActionIds.length
|
|
663
665
|
|| actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
|
|
664
666
|
fail('Case-response action lineage');
|
|
@@ -690,11 +692,11 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
690
692
|
const latestProviderEvents = latestProviderTime === null
|
|
691
693
|
? []
|
|
692
694
|
: providerEvents.filter((event) => event.occurredAt === latestProviderTime);
|
|
693
|
-
const expectedLatestProvider = (
|
|
695
|
+
const expectedLatestProvider = (hasActionBinding ? lineageComplete : escalationHistoryOmitted === 0)
|
|
694
696
|
&& latestProviderEvents.length === 1 && latestProviderEvents[0].applied
|
|
695
697
|
? latestProviderEvents[0]
|
|
696
698
|
: null;
|
|
697
|
-
const expectedProviderState = (
|
|
699
|
+
const expectedProviderState = (hasActionBinding ? !lineageComplete : escalationHistoryOmitted > 0)
|
|
698
700
|
? 'ambiguous'
|
|
699
701
|
: providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
|
|
700
702
|
if (providerOutcomeState !== expectedProviderState
|
|
@@ -727,9 +729,10 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
727
729
|
strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
|
|
728
730
|
const reviewedInputs = {
|
|
729
731
|
contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
|
|
730
|
-
version:
|
|
732
|
+
version: version === CASE_RESPONSE_PACKET_VERSION
|
|
731
733
|
? CASE_RESPONSE_REVIEW_INPUTS_VERSION
|
|
732
|
-
:
|
|
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,
|
|
733
736
|
profile: {
|
|
734
737
|
id: profile.id,
|
|
735
738
|
label: profile.label,
|
|
@@ -743,7 +746,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
|
|
|
743
746
|
case: root.case,
|
|
744
747
|
incident: root.incident,
|
|
745
748
|
contacts: root.contacts,
|
|
746
|
-
...(
|
|
749
|
+
...(hasActionBinding ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
|
|
747
750
|
selectedEvidence: root.selectedEvidence,
|
|
748
751
|
contradictions: root.contradictions,
|
|
749
752
|
readiness: root.readiness,
|
|
@@ -781,10 +784,13 @@ export function validateCaseResponsePacket(value) {
|
|
|
781
784
|
if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
|
|
782
785
|
return validateCaseResponsePacketV6(value);
|
|
783
786
|
if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
|
|
784
|
-
return
|
|
787
|
+
return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
|
|
785
788
|
}
|
|
786
|
-
if (value.schemaVersion ===
|
|
787
|
-
return
|
|
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);
|
|
788
794
|
}
|
|
789
795
|
if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
|
|
790
796
|
throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
|
|
@@ -792,5 +798,5 @@ export function validateCaseResponsePacket(value) {
|
|
|
792
798
|
if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
|
|
793
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.`);
|
|
794
800
|
}
|
|
795
|
-
|
|
801
|
+
validateVersionedCaseResponsePacket(value, CASE_RESPONSE_PACKET_VERSION);
|
|
796
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');
|