@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Compact provenance envelope for network-derived evidence. Type-specific
|
|
2
|
+
// payloads remain beside this object; the envelope only standardises source
|
|
3
|
+
// health, timing, completeness, truncation, and bounded limitations.
|
|
4
|
+
const OBSERVATION_VERSION = 1;
|
|
5
|
+
const STATUSES = new Set([
|
|
6
|
+
'success',
|
|
7
|
+
'partial',
|
|
8
|
+
'not_found',
|
|
9
|
+
'skipped',
|
|
10
|
+
'error',
|
|
11
|
+
'unsupported',
|
|
12
|
+
'not_applicable',
|
|
13
|
+
]);
|
|
14
|
+
const SCAN_MODES = new Set(['fast', 'deep']);
|
|
15
|
+
const MAX_OBSERVATION_LIMITATIONS = 10;
|
|
16
|
+
const MAX_OBSERVATION_LIMITATION_LENGTH = 300;
|
|
17
|
+
const MAX_OBSERVATION_DIAGNOSTICS = 20;
|
|
18
|
+
const MAX_DIAGNOSTICS = MAX_OBSERVATION_DIAGNOSTICS;
|
|
19
|
+
const MAX_DIAGNOSTIC_KEY = 40;
|
|
20
|
+
const MAX_DIAGNOSTIC_STRING = 240;
|
|
21
|
+
const MAX_DURATION_MS = 120_000;
|
|
22
|
+
const MAX_OBSERVATION_INPUT_LIMITATIONS = MAX_OBSERVATION_LIMITATIONS * 4;
|
|
23
|
+
const MAX_DIAGNOSTIC_DEPTH = 4;
|
|
24
|
+
const MAX_DIAGNOSTIC_VALUES_PER_ENTRY = 32;
|
|
25
|
+
const OBSERVATION_INPUT_FIELDS = [
|
|
26
|
+
'version',
|
|
27
|
+
'status',
|
|
28
|
+
'observedAt',
|
|
29
|
+
'scanMode',
|
|
30
|
+
'source',
|
|
31
|
+
'durationMs',
|
|
32
|
+
'complete',
|
|
33
|
+
'truncated',
|
|
34
|
+
'limitations',
|
|
35
|
+
'diagnostics',
|
|
36
|
+
];
|
|
37
|
+
// Observation v1 diagnostics use a closed, source-owned vocabulary. Reading
|
|
38
|
+
// only these descriptors keeps normalisation independent of an untrusted
|
|
39
|
+
// object's total key count and preserves the deployed producer fields.
|
|
40
|
+
const REGISTERED_DIAGNOSTIC_KEYS = [
|
|
41
|
+
'a',
|
|
42
|
+
'aaaa',
|
|
43
|
+
'addressSource',
|
|
44
|
+
'advisoryMatches',
|
|
45
|
+
'arrayItemsExamined',
|
|
46
|
+
'attemptCount',
|
|
47
|
+
'authorityCount',
|
|
48
|
+
'caa',
|
|
49
|
+
'caa_policy',
|
|
50
|
+
'catalogComponents',
|
|
51
|
+
'certificateGroups',
|
|
52
|
+
'certificateGroupsTruncated',
|
|
53
|
+
'certificateRows',
|
|
54
|
+
'charactersExamined',
|
|
55
|
+
'cidrCount',
|
|
56
|
+
'classifiedInputs',
|
|
57
|
+
'cname',
|
|
58
|
+
'connectionAttempts',
|
|
59
|
+
'delegation',
|
|
60
|
+
'discarded',
|
|
61
|
+
'discardedAuthorityRecordValueCount',
|
|
62
|
+
'discardedFields',
|
|
63
|
+
'discardedProperties',
|
|
64
|
+
'discardedUrls',
|
|
65
|
+
'discarded_attributes',
|
|
66
|
+
'discarded_entities',
|
|
67
|
+
'discarded_metadata',
|
|
68
|
+
'discarded_relationships',
|
|
69
|
+
'dmarc',
|
|
70
|
+
'documentsParsed',
|
|
71
|
+
'entities',
|
|
72
|
+
'error',
|
|
73
|
+
'externalScriptsSkipped',
|
|
74
|
+
'findings',
|
|
75
|
+
'formsObserved',
|
|
76
|
+
'generatorEvaluated',
|
|
77
|
+
'htmlEvaluated',
|
|
78
|
+
'httpStatus',
|
|
79
|
+
'https',
|
|
80
|
+
'ignoredCount',
|
|
81
|
+
'indicatorsObserved',
|
|
82
|
+
'inlineCharactersExamined',
|
|
83
|
+
'inlineScriptsExamined',
|
|
84
|
+
'inlineSignatureCharactersExamined',
|
|
85
|
+
'inlineSignatureTimedOut',
|
|
86
|
+
'inlineSignatureUnavailable',
|
|
87
|
+
'inputsObserved',
|
|
88
|
+
'knownExploitedMatches',
|
|
89
|
+
'lameAuthorityCount',
|
|
90
|
+
'malformedCount',
|
|
91
|
+
'malformedScripts',
|
|
92
|
+
'matches',
|
|
93
|
+
'mx',
|
|
94
|
+
'namesExamined',
|
|
95
|
+
'ns',
|
|
96
|
+
'objectsExamined',
|
|
97
|
+
'observed',
|
|
98
|
+
'observedAbsence',
|
|
99
|
+
'partialAuthorityCount',
|
|
100
|
+
'passiveHeadersEvaluated',
|
|
101
|
+
'potentialExposure',
|
|
102
|
+
'ptr',
|
|
103
|
+
'queriedAddressCount',
|
|
104
|
+
'redirectCount',
|
|
105
|
+
'referencesExamined',
|
|
106
|
+
'rejectedRows',
|
|
107
|
+
'relationshipTagsExamined',
|
|
108
|
+
'relationshipUrlsDiscarded',
|
|
109
|
+
'relationships',
|
|
110
|
+
'requestCount',
|
|
111
|
+
'resolvedAddressCount',
|
|
112
|
+
'resourceOriginsEvaluated',
|
|
113
|
+
'rolesObserved',
|
|
114
|
+
'scriptElementsExamined',
|
|
115
|
+
'scriptsExamined',
|
|
116
|
+
'scriptsObserved',
|
|
117
|
+
'serverEvaluated',
|
|
118
|
+
'soa',
|
|
119
|
+
'spf',
|
|
120
|
+
'successfulAuthorityCount',
|
|
121
|
+
'tagLimitReached',
|
|
122
|
+
'tagsExamined',
|
|
123
|
+
'trackingIdentifiersTruncated',
|
|
124
|
+
'tree',
|
|
125
|
+
'truncatedAuthorityRecordSetCount',
|
|
126
|
+
'truncatedNames',
|
|
127
|
+
'truncatedSameAsLists',
|
|
128
|
+
'truncatedTypeLists',
|
|
129
|
+
'unavailable',
|
|
130
|
+
'unclassifiedActions',
|
|
131
|
+
'unreachableAuthorityCount',
|
|
132
|
+
'workTruncated',
|
|
133
|
+
];
|
|
134
|
+
const ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})$/iu;
|
|
135
|
+
const LEGACY_ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})?$/iu;
|
|
136
|
+
const CT_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})?$/iu;
|
|
137
|
+
function isObservationStatus(value) {
|
|
138
|
+
return typeof value === 'string' && STATUSES.has(value);
|
|
139
|
+
}
|
|
140
|
+
function isScanMode(value) {
|
|
141
|
+
return typeof value === 'string' && SCAN_MODES.has(value);
|
|
142
|
+
}
|
|
143
|
+
function safeString(value, maxLength) {
|
|
144
|
+
if (typeof value !== 'string' || value.length > maxLength * 4)
|
|
145
|
+
return null;
|
|
146
|
+
const normalised = value.trim().replace(/[\u0000-\u001f\u007f]/g, ' ');
|
|
147
|
+
return normalised ? normalised.slice(0, maxLength) : null;
|
|
148
|
+
}
|
|
149
|
+
function ownDataValue(value, key) {
|
|
150
|
+
try {
|
|
151
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
152
|
+
return descriptor && 'value' in descriptor ? descriptor.value : undefined;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function snapshotObservationInput(value) {
|
|
159
|
+
const snapshot = {};
|
|
160
|
+
for (const field of OBSERVATION_INPUT_FIELDS) {
|
|
161
|
+
const fieldValue = ownDataValue(value, field);
|
|
162
|
+
if (fieldValue === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
Object.defineProperty(snapshot, field, {
|
|
165
|
+
configurable: true,
|
|
166
|
+
enumerable: true,
|
|
167
|
+
value: fieldValue,
|
|
168
|
+
writable: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return snapshot;
|
|
172
|
+
}
|
|
173
|
+
function safelyIsArray(value) {
|
|
174
|
+
try {
|
|
175
|
+
return Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function boundedArrayDataValues(value, maximum) {
|
|
182
|
+
if (!safelyIsArray(value))
|
|
183
|
+
return [];
|
|
184
|
+
const output = [];
|
|
185
|
+
const rawLength = ownDataValue(value, 'length');
|
|
186
|
+
const length = typeof rawLength === 'number' && Number.isSafeInteger(rawLength) && rawLength >= 0
|
|
187
|
+
? Math.min(rawLength, maximum)
|
|
188
|
+
: 0;
|
|
189
|
+
for (let index = 0; index < length; index += 1) {
|
|
190
|
+
const item = ownDataValue(value, String(index));
|
|
191
|
+
if (item !== undefined)
|
|
192
|
+
output.push(item);
|
|
193
|
+
}
|
|
194
|
+
return output;
|
|
195
|
+
}
|
|
196
|
+
function validCalendarParts(parts) {
|
|
197
|
+
const year = Number(parts[1]);
|
|
198
|
+
const month = Number(parts[2]);
|
|
199
|
+
const day = Number(parts[3]);
|
|
200
|
+
const hour = Number(parts[4]);
|
|
201
|
+
const minute = Number(parts[5]);
|
|
202
|
+
const second = Number(parts[6]);
|
|
203
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59)
|
|
204
|
+
return false;
|
|
205
|
+
const date = new Date(0);
|
|
206
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
207
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
208
|
+
return date.getUTCFullYear() === year
|
|
209
|
+
&& date.getUTCMonth() === month - 1
|
|
210
|
+
&& date.getUTCDate() === day
|
|
211
|
+
&& date.getUTCHours() === hour
|
|
212
|
+
&& date.getUTCMinutes() === minute
|
|
213
|
+
&& date.getUTCSeconds() === second;
|
|
214
|
+
}
|
|
215
|
+
function canonicalTimestamp(value, expression, assignUtcWhenMissing) {
|
|
216
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value))
|
|
217
|
+
return null;
|
|
218
|
+
const match = expression.exec(value);
|
|
219
|
+
if (!match || !validCalendarParts(match))
|
|
220
|
+
return null;
|
|
221
|
+
const zone = match[8] || (assignUtcWhenMissing ? 'Z' : '');
|
|
222
|
+
if (!zone)
|
|
223
|
+
return null;
|
|
224
|
+
if (zone !== 'Z' && zone.toUpperCase() !== 'Z') {
|
|
225
|
+
const hours = Number(zone.slice(1, 3));
|
|
226
|
+
const minutes = Number(zone.slice(4, 6));
|
|
227
|
+
if (hours > 23 || minutes > 59)
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const normalised = `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}${match[7] || ''}${zone.toUpperCase() === 'Z' ? 'Z' : zone}`;
|
|
231
|
+
const timestamp = Date.parse(normalised);
|
|
232
|
+
if (!Number.isFinite(timestamp))
|
|
233
|
+
return null;
|
|
234
|
+
const date = new Date(timestamp);
|
|
235
|
+
const utcYear = date.getUTCFullYear();
|
|
236
|
+
return utcYear >= 1 && utcYear <= 9_999 ? date.toISOString() : null;
|
|
237
|
+
}
|
|
238
|
+
function normalizeExplicitIsoTimestamp(value) {
|
|
239
|
+
return canonicalTimestamp(value, ISO_DATE_TIME_RE, false);
|
|
240
|
+
}
|
|
241
|
+
// Frozen legacy schemas accepted zone-less ISO date-times. Preserve that
|
|
242
|
+
// migration path deterministically by assigning UTC instead of consulting the
|
|
243
|
+
// host timezone. Current schemas must use normalizeExplicitIsoTimestamp.
|
|
244
|
+
function normalizeLegacyIsoTimestamp(value) {
|
|
245
|
+
return canonicalTimestamp(value, LEGACY_ISO_DATE_TIME_RE, true);
|
|
246
|
+
}
|
|
247
|
+
function normalizeCtTimestamp(value) {
|
|
248
|
+
return canonicalTimestamp(value, CT_DATE_TIME_RE, true);
|
|
249
|
+
}
|
|
250
|
+
function isoTimestamp(value) {
|
|
251
|
+
return normalizeExplicitIsoTimestamp(value);
|
|
252
|
+
}
|
|
253
|
+
function normalizeDiagnosticValue(value, budget, depth = 0) {
|
|
254
|
+
budget.remaining -= 1;
|
|
255
|
+
if (budget.remaining < 0) {
|
|
256
|
+
budget.exhausted = true;
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (depth > MAX_DIAGNOSTIC_DEPTH)
|
|
260
|
+
return null;
|
|
261
|
+
if (typeof value === 'boolean')
|
|
262
|
+
return value;
|
|
263
|
+
if (typeof value === 'number')
|
|
264
|
+
return Number.isFinite(value) ? value : null;
|
|
265
|
+
if (typeof value === 'string')
|
|
266
|
+
return safeString(value, MAX_DIAGNOSTIC_STRING);
|
|
267
|
+
if (!value || typeof value !== 'object' || safelyIsArray(value))
|
|
268
|
+
return null;
|
|
269
|
+
const objectValue = value;
|
|
270
|
+
if (budget.active.has(objectValue))
|
|
271
|
+
return null;
|
|
272
|
+
const record = value;
|
|
273
|
+
const output = {};
|
|
274
|
+
budget.active.add(objectValue);
|
|
275
|
+
try {
|
|
276
|
+
for (const field of ['status', 'error', 'detail', 'truncated', 'discarded', 'count']) {
|
|
277
|
+
const fieldValue = ownDataValue(record, field);
|
|
278
|
+
if (fieldValue === undefined)
|
|
279
|
+
continue;
|
|
280
|
+
const normalised = normalizeDiagnosticValue(fieldValue, budget, depth + 1);
|
|
281
|
+
if (normalised !== null) {
|
|
282
|
+
Object.defineProperty(output, field, {
|
|
283
|
+
configurable: true,
|
|
284
|
+
enumerable: true,
|
|
285
|
+
value: normalised,
|
|
286
|
+
writable: true,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
budget.active.delete(objectValue);
|
|
293
|
+
}
|
|
294
|
+
return Object.keys(output).length ? output : null;
|
|
295
|
+
}
|
|
296
|
+
function normalizeDiagnostics(value) {
|
|
297
|
+
if (!value || typeof value !== 'object' || safelyIsArray(value))
|
|
298
|
+
return {};
|
|
299
|
+
const record = value;
|
|
300
|
+
const output = {};
|
|
301
|
+
let retained = 0;
|
|
302
|
+
for (const key of REGISTERED_DIAGNOSTIC_KEYS) {
|
|
303
|
+
if (retained >= MAX_DIAGNOSTICS)
|
|
304
|
+
break;
|
|
305
|
+
if (key.length > MAX_DIAGNOSTIC_KEY)
|
|
306
|
+
continue;
|
|
307
|
+
const entryValue = ownDataValue(record, key);
|
|
308
|
+
if (entryValue === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
const budget = {
|
|
311
|
+
remaining: MAX_DIAGNOSTIC_VALUES_PER_ENTRY,
|
|
312
|
+
active: new Set(),
|
|
313
|
+
exhausted: false,
|
|
314
|
+
};
|
|
315
|
+
const normalised = normalizeDiagnosticValue(entryValue, budget);
|
|
316
|
+
if (normalised !== null && !budget.exhausted) {
|
|
317
|
+
Object.defineProperty(output, key, {
|
|
318
|
+
configurable: true,
|
|
319
|
+
enumerable: true,
|
|
320
|
+
value: normalised,
|
|
321
|
+
writable: true,
|
|
322
|
+
});
|
|
323
|
+
retained += 1;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return output;
|
|
327
|
+
}
|
|
328
|
+
function createObservationFromSnapshot(record) {
|
|
329
|
+
const rawStatus = record.status;
|
|
330
|
+
const rawSource = record.source;
|
|
331
|
+
const rawObservedAt = record.observedAt;
|
|
332
|
+
const rawDuration = record.durationMs;
|
|
333
|
+
const rawScanMode = record.scanMode;
|
|
334
|
+
const status = isObservationStatus(rawStatus) ? rawStatus : 'error';
|
|
335
|
+
const source = safeString(rawSource, 40) || 'unknown';
|
|
336
|
+
const observedAt = isoTimestamp(rawObservedAt) || new Date().toISOString();
|
|
337
|
+
const duration = typeof rawDuration === 'number' ? rawDuration : Number.NaN;
|
|
338
|
+
const limitations = [...new Set(boundedArrayDataValues(record.limitations, MAX_OBSERVATION_INPUT_LIMITATIONS)
|
|
339
|
+
.map((item) => safeString(item, MAX_OBSERVATION_LIMITATION_LENGTH))
|
|
340
|
+
.filter((item) => item !== null))]
|
|
341
|
+
.slice(0, MAX_OBSERVATION_LIMITATIONS);
|
|
342
|
+
return {
|
|
343
|
+
version: OBSERVATION_VERSION,
|
|
344
|
+
status,
|
|
345
|
+
observedAt,
|
|
346
|
+
scanMode: isScanMode(rawScanMode) ? rawScanMode : null,
|
|
347
|
+
source,
|
|
348
|
+
durationMs: Number.isFinite(duration) ? Math.max(0, Math.min(MAX_DURATION_MS, Math.round(duration))) : null,
|
|
349
|
+
complete: record.complete === true,
|
|
350
|
+
truncated: record.truncated === true,
|
|
351
|
+
limitations,
|
|
352
|
+
diagnostics: normalizeDiagnostics(record.diagnostics),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function createObservation(input = {}) {
|
|
356
|
+
const record = input && typeof input === 'object' && !safelyIsArray(input)
|
|
357
|
+
? snapshotObservationInput(input)
|
|
358
|
+
: {};
|
|
359
|
+
return createObservationFromSnapshot(record);
|
|
360
|
+
}
|
|
361
|
+
function readObservationEnvelope(value) {
|
|
362
|
+
if (value === undefined || value === null)
|
|
363
|
+
return { state: 'absent', observation: null };
|
|
364
|
+
if (!value || typeof value !== 'object' || safelyIsArray(value))
|
|
365
|
+
return { state: 'invalid', observation: null };
|
|
366
|
+
const record = snapshotObservationInput(value);
|
|
367
|
+
const version = record.version;
|
|
368
|
+
const status = record.status;
|
|
369
|
+
const source = record.source;
|
|
370
|
+
const observedAt = record.observedAt;
|
|
371
|
+
if (typeof version === 'number'
|
|
372
|
+
&& Number.isInteger(version)
|
|
373
|
+
&& version > OBSERVATION_VERSION) {
|
|
374
|
+
return { state: 'unsupported', observation: null };
|
|
375
|
+
}
|
|
376
|
+
if (version !== OBSERVATION_VERSION
|
|
377
|
+
|| !isObservationStatus(status)
|
|
378
|
+
|| !safeString(source, 40)
|
|
379
|
+
|| !isoTimestamp(observedAt)) {
|
|
380
|
+
return { state: 'invalid', observation: null };
|
|
381
|
+
}
|
|
382
|
+
return { state: 'supported', observation: createObservationFromSnapshot(record) };
|
|
383
|
+
}
|
|
384
|
+
export { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, createObservation, normalizeCtTimestamp, normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, readObservationEnvelope, };
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { normalizeDomain, openOrCreateCase, updateCase, } from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export const MAX_EXTERNAL_FINDINGS_PER_DOMAIN = 20;
|
|
7
|
-
export const MAX_EXTERNAL_FINDING_DOMAINS = 25;
|
|
1
|
+
import { normalizeDomain, openOrCreateCase, updateCase, } from "../cases/case-model.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
|
|
4
|
+
import { hasUnsafeRetainedText } from "./retained-text.mjs";
|
|
5
|
+
export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
|
|
8
6
|
export const EXTERNAL_FINDING_CATEGORIES = [
|
|
9
7
|
'certificate',
|
|
10
8
|
'dns',
|
|
@@ -17,7 +15,6 @@ export const EXTERNAL_FINDING_CATEGORIES = [
|
|
|
17
15
|
];
|
|
18
16
|
const CATEGORIES = new Set(EXTERNAL_FINDING_CATEGORIES);
|
|
19
17
|
const COMPLETENESS = new Set(['complete', 'inconclusive', 'partial', 'unknown']);
|
|
20
|
-
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
21
18
|
const ROOT_KEYS = new Set(['schema', 'schemaVersion', 'source', 'findings']);
|
|
22
19
|
const SOURCE_KEYS = new Set(['name', 'reference', 'collectedAt']);
|
|
23
20
|
const FINDING_KEYS = new Set([
|
|
@@ -38,12 +35,18 @@ const STRUCTURED_OBSERVATION_KEYS = new Set([
|
|
|
38
35
|
'value',
|
|
39
36
|
'issuer',
|
|
40
37
|
'notAfter',
|
|
38
|
+
'eventId',
|
|
39
|
+
'logId',
|
|
40
|
+
'certificateSha256',
|
|
41
|
+
'dnsNameCount',
|
|
42
|
+
'namesComplete',
|
|
41
43
|
]);
|
|
42
44
|
const STRUCTURED_OBSERVATION_SCHEMAS = new Set([
|
|
43
45
|
'whoisleuth.domain-observation-rows',
|
|
44
46
|
'whoisleuth.dns-observation-rows',
|
|
45
47
|
'whoisleuth.certificate-observation-rows',
|
|
46
48
|
]);
|
|
49
|
+
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
47
50
|
function record(value) {
|
|
48
51
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
49
52
|
? value
|
|
@@ -56,8 +59,8 @@ function requiredText(value, maximum, label) {
|
|
|
56
59
|
if (typeof value !== 'string'
|
|
57
60
|
|| !value.trim()
|
|
58
61
|
|| value.length > maximum
|
|
59
|
-
||
|
|
60
|
-
throw new Error(`${label} must be non-empty, bounded text without control characters.`);
|
|
62
|
+
|| hasUnsafeRetainedText(value)) {
|
|
63
|
+
throw new Error(`${label} must be non-empty, bounded text without unsafe control characters or formatting controls.`);
|
|
61
64
|
}
|
|
62
65
|
return value.trim();
|
|
63
66
|
}
|
|
@@ -70,10 +73,10 @@ function iso(value, label, optional = false) {
|
|
|
70
73
|
if (optional && (value === undefined || value === null || value === ''))
|
|
71
74
|
return null;
|
|
72
75
|
const text = requiredText(value, 64, label);
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
throw new Error(`${label} must be a valid date and time.`);
|
|
76
|
-
return
|
|
76
|
+
const normalized = normalizeExplicitIsoTimestamp(text);
|
|
77
|
+
if (!normalized)
|
|
78
|
+
throw new Error(`${label} must be a valid date and time with an explicit timezone.`);
|
|
79
|
+
return normalized;
|
|
77
80
|
}
|
|
78
81
|
function limitations(value, index) {
|
|
79
82
|
if (value === undefined)
|
|
@@ -104,9 +107,44 @@ function structuredObservation(value, index) {
|
|
|
104
107
|
const issuer = optionalText(item.issuer, 160, `Finding ${index + 1} certificate issuer`);
|
|
105
108
|
const notAfter = iso(item.notAfter, `Finding ${index + 1} certificate expiry`, true);
|
|
106
109
|
const certificateSchema = item.sourceSchema === 'whoisleuth.certificate-observation-rows';
|
|
110
|
+
if (certificateSchema && (field === 'certificateSha256' || field === 'fingerprintSha256') && !SHA256_RE.test(observationValue.toLowerCase())) {
|
|
111
|
+
throw new Error(`Finding ${index + 1} certificate observation value must be a SHA-256 hexadecimal digest.`);
|
|
112
|
+
}
|
|
107
113
|
if (!certificateSchema && (issuer !== null || notAfter !== null)) {
|
|
108
114
|
throw new Error(`Finding ${index + 1} non-certificate observation cannot declare certificate metadata.`);
|
|
109
115
|
}
|
|
116
|
+
const declaresEventMetadata = ['eventId', 'logId', 'certificateSha256', 'dnsNameCount', 'namesComplete']
|
|
117
|
+
.some((key) => item[key] !== undefined && item[key] !== null);
|
|
118
|
+
let eventId = null;
|
|
119
|
+
let logId = null;
|
|
120
|
+
let certificateSha256 = null;
|
|
121
|
+
let dnsNameCount = null;
|
|
122
|
+
let namesComplete = null;
|
|
123
|
+
if (declaresEventMetadata) {
|
|
124
|
+
if (!certificateSchema) {
|
|
125
|
+
throw new Error(`Finding ${index + 1} certificate-event metadata requires external-findings schema version 4.`);
|
|
126
|
+
}
|
|
127
|
+
eventId = requiredText(item.eventId, 64, `Finding ${index + 1} certificate event id`);
|
|
128
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/u.test(eventId)) {
|
|
129
|
+
throw new Error(`Finding ${index + 1} certificate event id is invalid.`);
|
|
130
|
+
}
|
|
131
|
+
logId = requiredText(item.logId, 200, `Finding ${index + 1} certificate log id`);
|
|
132
|
+
certificateSha256 = requiredText(item.certificateSha256, 64, `Finding ${index + 1} certificate digest`).toLowerCase();
|
|
133
|
+
if (!SHA256_RE.test(certificateSha256)) {
|
|
134
|
+
throw new Error(`Finding ${index + 1} certificate event digest must be a SHA-256 hexadecimal digest.`);
|
|
135
|
+
}
|
|
136
|
+
if (certificateSha256 !== observationValue.toLowerCase()) {
|
|
137
|
+
throw new Error(`Finding ${index + 1} certificate event digest must match the structured observation value.`);
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isSafeInteger(item.dnsNameCount) || Number(item.dnsNameCount) < 1 || Number(item.dnsNameCount) > 100) {
|
|
140
|
+
throw new Error(`Finding ${index + 1} certificate DNS name count is invalid.`);
|
|
141
|
+
}
|
|
142
|
+
dnsNameCount = Number(item.dnsNameCount);
|
|
143
|
+
if (typeof item.namesComplete !== 'boolean') {
|
|
144
|
+
throw new Error(`Finding ${index + 1} certificate name completeness is invalid.`);
|
|
145
|
+
}
|
|
146
|
+
namesComplete = item.namesComplete;
|
|
147
|
+
}
|
|
110
148
|
return {
|
|
111
149
|
sourceSchema: item.sourceSchema,
|
|
112
150
|
sourceVersion: 1,
|
|
@@ -114,6 +152,11 @@ function structuredObservation(value, index) {
|
|
|
114
152
|
value: observationValue,
|
|
115
153
|
issuer,
|
|
116
154
|
notAfter,
|
|
155
|
+
eventId,
|
|
156
|
+
logId,
|
|
157
|
+
certificateSha256,
|
|
158
|
+
dnsNameCount,
|
|
159
|
+
namesComplete,
|
|
117
160
|
};
|
|
118
161
|
}
|
|
119
162
|
function findingKey(finding, sourceName) {
|
|
@@ -125,6 +168,7 @@ function findingKey(finding, sourceName) {
|
|
|
125
168
|
finding.observedAt,
|
|
126
169
|
finding.completeness,
|
|
127
170
|
sourceName,
|
|
171
|
+
finding.structuredObservation?.eventId ?? '',
|
|
128
172
|
].join('\u0000');
|
|
129
173
|
}
|
|
130
174
|
export function parseExternalFindingsDocument(value) {
|
|
@@ -133,8 +177,8 @@ export function parseExternalFindingsDocument(value) {
|
|
|
133
177
|
throw new Error('External findings must use the documented object shape without additional top-level fields.');
|
|
134
178
|
}
|
|
135
179
|
if (root.schema !== EXTERNAL_FINDINGS_SCHEMA
|
|
136
|
-
||
|
|
137
|
-
throw new Error(`External findings must use ${EXTERNAL_FINDINGS_SCHEMA} schema version
|
|
180
|
+
|| root.schemaVersion !== EXTERNAL_FINDINGS_VERSION) {
|
|
181
|
+
throw new Error(`External findings must use ${EXTERNAL_FINDINGS_SCHEMA} schema version ${EXTERNAL_FINDINGS_VERSION}.`);
|
|
138
182
|
}
|
|
139
183
|
const sourceValue = record(root.source);
|
|
140
184
|
if (!sourceValue || !hasOnlyKeys(sourceValue, SOURCE_KEYS)) {
|
|
@@ -166,9 +210,7 @@ export function parseExternalFindingsDocument(value) {
|
|
|
166
210
|
if (typeof item.completeness !== 'string' || !COMPLETENESS.has(item.completeness)) {
|
|
167
211
|
throw new Error(`Finding ${index + 1} completeness is unsupported.`);
|
|
168
212
|
}
|
|
169
|
-
const evidenceClass = item.evidenceClass
|
|
170
|
-
? 'provider_report'
|
|
171
|
-
: item.evidenceClass;
|
|
213
|
+
const evidenceClass = item.evidenceClass;
|
|
172
214
|
if (evidenceClass !== 'deployment_observation' && evidenceClass !== 'provider_report') {
|
|
173
215
|
throw new Error(`Finding ${index + 1} evidence class is unsupported.`);
|
|
174
216
|
}
|
|
@@ -206,6 +248,13 @@ export function parseExternalFindingsDocument(value) {
|
|
|
206
248
|
findings: normalized,
|
|
207
249
|
};
|
|
208
250
|
}
|
|
251
|
+
export function serializeExternalFindingsDocument(value) {
|
|
252
|
+
const serialised = JSON.stringify(value, null, 2);
|
|
253
|
+
if (typeof serialised !== 'string') {
|
|
254
|
+
throw new TypeError('External findings must be JSON-serialisable.');
|
|
255
|
+
}
|
|
256
|
+
return `${serialised}\n`;
|
|
257
|
+
}
|
|
209
258
|
function pinValue(finding) {
|
|
210
259
|
return finding.reference
|
|
211
260
|
? `${finding.summary} Reference: ${finding.reference}`
|
|
@@ -229,12 +278,28 @@ function existingPinKey(recordValue, finding, sourceName) {
|
|
|
229
278
|
&& (!observation || (pin.field === observation.field
|
|
230
279
|
&& pin.sourceSchema?.collection === 'external_observations'
|
|
231
280
|
&& pin.sourceSchema.schema === observation.sourceSchema
|
|
232
|
-
&& pin.sourceSchema.version === observation.sourceVersion
|
|
281
|
+
&& pin.sourceSchema.version === observation.sourceVersion
|
|
282
|
+
&& (observation.eventId === null || pin.certificateObservation?.eventId === observation.eventId))))) ? findingKey(finding, sourceName) : '';
|
|
233
283
|
}
|
|
234
284
|
function structuredPinFields(finding) {
|
|
235
285
|
const observation = finding.structuredObservation;
|
|
236
286
|
if (!observation)
|
|
237
287
|
return {};
|
|
288
|
+
const certificateObservation = observation.eventId
|
|
289
|
+
&& observation.logId
|
|
290
|
+
&& observation.certificateSha256
|
|
291
|
+
&& observation.dnsNameCount !== null
|
|
292
|
+
&& observation.namesComplete !== null
|
|
293
|
+
? {
|
|
294
|
+
eventId: observation.eventId,
|
|
295
|
+
logId: observation.logId,
|
|
296
|
+
certificateSha256: observation.certificateSha256,
|
|
297
|
+
issuer: observation.issuer,
|
|
298
|
+
notAfter: observation.notAfter,
|
|
299
|
+
dnsNameCount: observation.dnsNameCount,
|
|
300
|
+
namesComplete: observation.namesComplete,
|
|
301
|
+
}
|
|
302
|
+
: null;
|
|
238
303
|
return {
|
|
239
304
|
field: observation.field,
|
|
240
305
|
category: finding.category,
|
|
@@ -243,6 +308,7 @@ function structuredPinFields(finding) {
|
|
|
243
308
|
schema: observation.sourceSchema,
|
|
244
309
|
version: observation.sourceVersion,
|
|
245
310
|
},
|
|
311
|
+
certificateObservation,
|
|
246
312
|
};
|
|
247
313
|
}
|
|
248
314
|
export function mergeExternalFindingsIntoCases(current, document, now = new Date().toISOString()) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const UNSAFE_RETAINED_TEXT_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
2
|
+
const UNSAFE_RETAINED_TEXT_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
3
|
+
export function hasUnsafeRetainedText(value) {
|
|
4
|
+
return UNSAFE_RETAINED_TEXT_RE.test(value);
|
|
5
|
+
}
|
|
6
|
+
export function neutralizeUnsafeRetainedText(value) {
|
|
7
|
+
return value.replace(UNSAFE_RETAINED_TEXT_GLOBAL_RE, ' ');
|
|
8
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
2
|
+
import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../contracts/investigation-portability.mjs";
|
|
3
|
+
import { isValidAsciiDomainName } from "../../lib/hostname.mjs";
|
|
4
|
+
export { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION };
|
|
4
5
|
export const ACQUISITION_DECISIONS = [
|
|
5
6
|
'unresolved',
|
|
6
7
|
'continue_manual_review',
|
|
@@ -28,11 +29,13 @@ function timestamp(value) {
|
|
|
28
29
|
return new Date(value).toISOString();
|
|
29
30
|
}
|
|
30
31
|
function target(value) {
|
|
31
|
-
const
|
|
32
|
-
|
|
32
|
+
const submitted = typeof value === 'string'
|
|
33
|
+
? value.trim().replace(/\.$/u, '')
|
|
34
|
+
: '';
|
|
35
|
+
if (!isValidAsciiDomainName(submitted, { requireDot: true })) {
|
|
33
36
|
throw new Error('A canonical domain is required for an acquisition decision packet.');
|
|
34
37
|
}
|
|
35
|
-
return
|
|
38
|
+
return submitted.toLowerCase();
|
|
36
39
|
}
|
|
37
40
|
function decision(value) {
|
|
38
41
|
return typeof value === 'string' && DECISION_SET.has(value)
|
|
@@ -80,10 +83,10 @@ export async function buildAcquisitionDecisionPacket(input) {
|
|
|
80
83
|
: []),
|
|
81
84
|
],
|
|
82
85
|
};
|
|
83
|
-
const digestSha256 = await
|
|
86
|
+
const digestSha256 = await sha256ArtifactDigestV2(unsigned);
|
|
84
87
|
const document = {
|
|
85
88
|
...unsigned,
|
|
86
|
-
integrity: { algorithm: 'SHA-256', digestSha256 },
|
|
89
|
+
integrity: { algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256 },
|
|
87
90
|
};
|
|
88
91
|
return {
|
|
89
92
|
document,
|