@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
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
// Shared HTTP response boundary for the unified Lookup API.
|
|
2
|
-
//
|
|
3
|
-
// The backend owns the evidence collectors, but an HTTP response is still an
|
|
4
|
-
// untrusted value when it reaches the browser. This module keeps the response
|
|
5
|
-
// envelope typed and validates the small set of fields the Lookup controller
|
|
6
|
-
// relies on before any evidence is derived or rendered. Nested source records
|
|
7
|
-
// stay separately attributed and additive; their own normalizers remain the
|
|
8
|
-
// authority for source-specific fields.
|
|
9
|
-
const INVALID_LOOKUP_RESPONSE = 'INVALID_LOOKUP_RESPONSE';
|
|
10
|
-
const INVALID_LOOKUP_RESPONSE_MESSAGE = 'Lookup returned an invalid response.';
|
|
11
|
-
const INVALID_COMPACT_LOOKUP_RESPONSE = 'INVALID_COMPACT_LOOKUP_RESPONSE';
|
|
12
|
-
const INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE = 'Bulk lookup returned an invalid response.';
|
|
13
|
-
const MAX_LOOKUP_RESPONSE_QUERY_LENGTH = 4096;
|
|
14
|
-
const MAX_LOOKUP_RESPONSE_HOST_LENGTH = 253;
|
|
15
|
-
const MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 32;
|
|
16
|
-
const MAX_LOOKUP_RESPONSE_ERROR_LENGTH = 240;
|
|
17
|
-
const MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 8;
|
|
18
|
-
const MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS = 128;
|
|
19
|
-
const MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS = 16;
|
|
20
|
-
const MAX_COMPACT_BULK_TECHNOLOGY_IDS = 12;
|
|
21
|
-
const MAX_THREAT_INTELLIGENCE_PROVIDERS = 10;
|
|
22
|
-
const MAX_LOOKUP_TIMING_MS = 120_000;
|
|
23
|
-
const MAX_LOOKUP_TIMING_SOURCES = 10;
|
|
24
|
-
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/u;
|
|
25
|
-
const QUERY_TYPES = new Set(['domain', 'ipv4', 'ipv6', 'asn']);
|
|
26
|
-
const COMPACT_AVAILABILITY_STATES = new Set([
|
|
27
|
-
'available',
|
|
28
|
-
'expiring',
|
|
29
|
-
'for_sale',
|
|
30
|
-
'registered',
|
|
31
|
-
'unknown',
|
|
32
|
-
]);
|
|
33
|
-
const COMPACT_CONFIDENCE_LEVELS = new Set(['high', 'low', 'medium']);
|
|
34
|
-
const COMPACT_AVAILABILITY_DIAGNOSTIC_STATES = new Set(['complete', 'disabled', 'error']);
|
|
35
|
-
const COMPACT_BULK_COMPARISON_STATES = new Set([
|
|
36
|
-
'error',
|
|
37
|
-
'not_found',
|
|
38
|
-
'partial',
|
|
39
|
-
'success',
|
|
40
|
-
'unavailable',
|
|
41
|
-
]);
|
|
42
|
-
const COMPACT_BULK_TECHNOLOGY_ID_RE = /^[a-z0-9][a-z0-9_-]{0,79}$/u;
|
|
43
|
-
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
44
|
-
const LOOKUP_TIMING_SOURCES = new Set([
|
|
45
|
-
'rdap',
|
|
46
|
-
'whois',
|
|
47
|
-
'domain_evidence',
|
|
48
|
-
'reverse_dns',
|
|
49
|
-
'registrar_rdap',
|
|
50
|
-
'network_context',
|
|
51
|
-
'security_txt',
|
|
52
|
-
'external_intelligence',
|
|
53
|
-
'malware_host_intelligence',
|
|
54
|
-
'malware_ioc_intelligence',
|
|
55
|
-
]);
|
|
56
|
-
const LOOKUP_TIMING_OUTCOMES = new Set(['fulfilled', 'rejected']);
|
|
57
|
-
const EMPTY_RECORD = Object.freeze({});
|
|
58
|
-
function isJsonObject(value) {
|
|
59
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
function record(value) {
|
|
62
|
-
return isJsonObject(value) ? value : EMPTY_RECORD;
|
|
63
|
-
}
|
|
64
|
-
function optionalBoundedText(value, maxLength) {
|
|
65
|
-
return value === undefined || (typeof value === 'string'
|
|
66
|
-
&& value.length <= maxLength
|
|
67
|
-
&& !CONTROL_CHAR_RE.test(value));
|
|
68
|
-
}
|
|
69
|
-
function normalizedDomain(value) {
|
|
70
|
-
if (typeof value !== 'string'
|
|
71
|
-
|| !value.trim()
|
|
72
|
-
|| value.length > MAX_LOOKUP_RESPONSE_HOST_LENGTH
|
|
73
|
-
|| CONTROL_CHAR_RE.test(value)
|
|
74
|
-
|| /[\s/?#@\\:]/u.test(value)) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const parsed = new URL(`https://${value.trim().replace(/\.$/u, '')}/`);
|
|
79
|
-
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
80
|
-
const labels = hostname.split('.');
|
|
81
|
-
return labels.length >= 2
|
|
82
|
-
&& hostname.length <= MAX_LOOKUP_RESPONSE_HOST_LENGTH
|
|
83
|
-
&& labels.every((label) => (label.length <= 63
|
|
84
|
-
&& /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)))
|
|
85
|
-
? hostname
|
|
86
|
-
: null;
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function compactDomainMatches(value, expectedDomain) {
|
|
93
|
-
const domain = normalizedDomain(value);
|
|
94
|
-
const expected = normalizedDomain(expectedDomain);
|
|
95
|
-
return Boolean(domain && expected && (domain === expected || expected.endsWith(`.${domain}`)));
|
|
96
|
-
}
|
|
97
|
-
function validCompactBulkComparison(value) {
|
|
98
|
-
if (!isJsonObject(value)
|
|
99
|
-
|| Object.keys(value).length > 3
|
|
100
|
-
|| value.version !== 1
|
|
101
|
-
|| !isJsonObject(value.technology)
|
|
102
|
-
|| Object.keys(value.technology).length > 3
|
|
103
|
-
|| typeof value.technology.state !== 'string'
|
|
104
|
-
|| !COMPACT_BULK_COMPARISON_STATES.has(value.technology.state)
|
|
105
|
-
|| !Array.isArray(value.technology.ids)
|
|
106
|
-
|| value.technology.ids.length > MAX_COMPACT_BULK_TECHNOLOGY_IDS
|
|
107
|
-
|| typeof value.technology.truncated !== 'boolean'
|
|
108
|
-
|| !isJsonObject(value.tls)
|
|
109
|
-
|| Object.keys(value.tls).length > 3
|
|
110
|
-
|| typeof value.tls.state !== 'string'
|
|
111
|
-
|| !COMPACT_BULK_COMPARISON_STATES.has(value.tls.state)
|
|
112
|
-
|| (value.tls.issuerLabel !== null && !optionalBoundedText(value.tls.issuerLabel, 240))
|
|
113
|
-
|| (value.tls.spkiSha256 !== null
|
|
114
|
-
&& (typeof value.tls.spkiSha256 !== 'string' || !SHA256_RE.test(value.tls.spkiSha256)))) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
const ids = value.technology.ids;
|
|
118
|
-
return ids.every((id) => typeof id === 'string' && COMPACT_BULK_TECHNOLOGY_ID_RE.test(id))
|
|
119
|
-
&& new Set(ids).size === ids.length;
|
|
120
|
-
}
|
|
121
|
-
function invalidLookupResponse() {
|
|
122
|
-
return {
|
|
123
|
-
ok: false,
|
|
124
|
-
errorCode: INVALID_LOOKUP_RESPONSE,
|
|
125
|
-
error: INVALID_LOOKUP_RESPONSE_MESSAGE,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function invalidCompactLookupResponse() {
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
errorCode: INVALID_COMPACT_LOOKUP_RESPONSE,
|
|
132
|
-
error: INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function parseLookupHttpResponse(value) {
|
|
136
|
-
if (!isJsonObject(value) || Object.keys(value).length > MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
|
|
137
|
-
return invalidLookupResponse();
|
|
138
|
-
}
|
|
139
|
-
if (typeof value.query !== 'string'
|
|
140
|
-
|| !value.query.trim()
|
|
141
|
-
|| value.query.length > MAX_LOOKUP_RESPONSE_QUERY_LENGTH
|
|
142
|
-
|| CONTROL_CHAR_RE.test(value.query)
|
|
143
|
-
|| typeof value.type !== 'string'
|
|
144
|
-
|| !QUERY_TYPES.has(value.type)
|
|
145
|
-
|| !isJsonObject(value.rdap)
|
|
146
|
-
|| !isJsonObject(value.whois)
|
|
147
|
-
|| !isJsonObject(value.availability)
|
|
148
|
-
|| !isJsonObject(value.diagnostics)
|
|
149
|
-
|| !optionalBoundedText(value.inputHostname, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
|
|
150
|
-
|| !optionalBoundedText(value.registrableDomain, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
|
|
151
|
-
|| (value.isSubdomain !== undefined && typeof value.isSubdomain !== 'boolean')) {
|
|
152
|
-
return invalidLookupResponse();
|
|
153
|
-
}
|
|
154
|
-
for (const key of ['reverseDns', 'networkContext', 'securityTxt', 'sslbl', 'threatIntelligence', 'registryInsights']) {
|
|
155
|
-
const section = value[key];
|
|
156
|
-
if (section !== undefined && !isJsonObject(section))
|
|
157
|
-
return invalidLookupResponse();
|
|
158
|
-
}
|
|
159
|
-
return { ok: true, value: value };
|
|
160
|
-
}
|
|
161
|
-
function parseCompactLookupHttpResponse(value, expectedDomain) {
|
|
162
|
-
if (!isJsonObject(value) || Object.keys(value).length > MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
|
|
163
|
-
return invalidCompactLookupResponse();
|
|
164
|
-
}
|
|
165
|
-
const availability = value.availability;
|
|
166
|
-
const diagnostics = value.diagnostics;
|
|
167
|
-
if (!isJsonObject(availability)
|
|
168
|
-
|| Object.keys(availability).length > MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS
|
|
169
|
-
|| availability.applicable !== true
|
|
170
|
-
|| !compactDomainMatches(availability.domain, expectedDomain)
|
|
171
|
-
|| typeof availability.state !== 'string'
|
|
172
|
-
|| !COMPACT_AVAILABILITY_STATES.has(availability.state)
|
|
173
|
-
|| typeof availability.confidence !== 'string'
|
|
174
|
-
|| !COMPACT_CONFIDENCE_LEVELS.has(availability.confidence)
|
|
175
|
-
|| (availability.deepScanComplete !== undefined && typeof availability.deepScanComplete !== 'boolean')
|
|
176
|
-
|| (availability.bulkComparison !== undefined
|
|
177
|
-
&& !validCompactBulkComparison(availability.bulkComparison))
|
|
178
|
-
|| (value.query !== undefined && !compactDomainMatches(value.query, expectedDomain))
|
|
179
|
-
|| (value.type !== undefined && value.type !== 'domain')
|
|
180
|
-
|| (value.inputHostname !== undefined && !compactDomainMatches(value.inputHostname, expectedDomain))
|
|
181
|
-
|| (value.registrableDomain !== undefined
|
|
182
|
-
&& normalizedDomain(value.registrableDomain) !== normalizedDomain(availability.domain))
|
|
183
|
-
|| (value.isSubdomain !== undefined && typeof value.isSubdomain !== 'boolean')
|
|
184
|
-
|| !isJsonObject(diagnostics)
|
|
185
|
-
|| Object.keys(diagnostics).length > MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS
|
|
186
|
-
|| diagnostics.version !== 7
|
|
187
|
-
|| !isJsonObject(diagnostics.rdap)
|
|
188
|
-
|| !isJsonObject(diagnostics.whois)
|
|
189
|
-
|| !isJsonObject(diagnostics.availability)
|
|
190
|
-
|| typeof diagnostics.availability.status !== 'string'
|
|
191
|
-
|| !COMPACT_AVAILABILITY_DIAGNOSTIC_STATES.has(diagnostics.availability.status)) {
|
|
192
|
-
return invalidCompactLookupResponse();
|
|
193
|
-
}
|
|
194
|
-
return { ok: true, value: value };
|
|
195
|
-
}
|
|
196
|
-
function lookupHttpErrorMessage(value, status) {
|
|
197
|
-
const source = record(value);
|
|
198
|
-
const message = typeof source.error === 'string'
|
|
199
|
-
? source.error
|
|
200
|
-
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
201
|
-
.replace(/\s+/gu, ' ')
|
|
202
|
-
.trim()
|
|
203
|
-
.slice(0, MAX_LOOKUP_RESPONSE_ERROR_LENGTH)
|
|
204
|
-
: '';
|
|
205
|
-
return message || `Lookup failed (${status})`;
|
|
206
|
-
}
|
|
207
|
-
function isBoundedTimingMs(value, totalMs) {
|
|
208
|
-
return Number.isInteger(value)
|
|
209
|
-
&& Number(value) >= 0
|
|
210
|
-
&& Number(value) <= totalMs
|
|
211
|
-
&& Number(value) <= MAX_LOOKUP_TIMING_MS;
|
|
212
|
-
}
|
|
213
|
-
function normalizeLookupTiming(value) {
|
|
214
|
-
if (!isJsonObject(value)
|
|
215
|
-
|| value.version !== 1
|
|
216
|
-
|| !Number.isInteger(value.totalMs)
|
|
217
|
-
|| Number(value.totalMs) < 0
|
|
218
|
-
|| Number(value.totalMs) > MAX_LOOKUP_TIMING_MS
|
|
219
|
-
|| !Array.isArray(value.sources)
|
|
220
|
-
|| value.sources.length > MAX_LOOKUP_TIMING_SOURCES) {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
const totalMs = Number(value.totalMs);
|
|
224
|
-
const seen = new Set();
|
|
225
|
-
const sources = [];
|
|
226
|
-
for (const candidate of value.sources) {
|
|
227
|
-
if (!isJsonObject(candidate)
|
|
228
|
-
|| typeof candidate.source !== 'string'
|
|
229
|
-
|| !LOOKUP_TIMING_SOURCES.has(candidate.source)
|
|
230
|
-
|| seen.has(candidate.source)
|
|
231
|
-
|| typeof candidate.outcome !== 'string'
|
|
232
|
-
|| !LOOKUP_TIMING_OUTCOMES.has(candidate.outcome)
|
|
233
|
-
|| !isBoundedTimingMs(candidate.durationMs, totalMs)
|
|
234
|
-
|| !isBoundedTimingMs(candidate.completedAfterMs, totalMs)
|
|
235
|
-
|| Number(candidate.durationMs) > Number(candidate.completedAfterMs)) {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
const source = candidate.source;
|
|
239
|
-
seen.add(source);
|
|
240
|
-
sources.push({
|
|
241
|
-
source,
|
|
242
|
-
outcome: candidate.outcome,
|
|
243
|
-
durationMs: Number(candidate.durationMs),
|
|
244
|
-
completedAfterMs: Number(candidate.completedAfterMs),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
return {
|
|
248
|
-
version: 1,
|
|
249
|
-
totalMs,
|
|
250
|
-
sources,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function createLookupHttpResponse(query, classified, result) {
|
|
254
|
-
return {
|
|
255
|
-
...result,
|
|
256
|
-
query,
|
|
257
|
-
type: classified.type,
|
|
258
|
-
inputHostname: classified.inputHostname,
|
|
259
|
-
registrableDomain: classified.registrableDomain,
|
|
260
|
-
isSubdomain: classified.isSubdomain,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
function createLookupViewModel(response) {
|
|
264
|
-
const availability = record(response?.availability);
|
|
265
|
-
const rdap = record(response?.rdap);
|
|
266
|
-
const registrarRdap = record(rdap.registrarRdap);
|
|
267
|
-
const whois = record(response?.whois);
|
|
268
|
-
const diagnostics = record(response?.diagnostics);
|
|
269
|
-
const reverseDns = record(response?.reverseDns);
|
|
270
|
-
const observedNetworkContext = record(response?.networkContext);
|
|
271
|
-
const registryInsights = record(response?.registryInsights);
|
|
272
|
-
const securityTxt = record(response?.securityTxt);
|
|
273
|
-
const sslbl = record(response?.sslbl);
|
|
274
|
-
const threatIntelligence = record(response?.threatIntelligence);
|
|
275
|
-
const providers = Array.isArray(threatIntelligence.providers)
|
|
276
|
-
? threatIntelligence.providers
|
|
277
|
-
.filter(isJsonObject)
|
|
278
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS)
|
|
279
|
-
: [];
|
|
280
|
-
const dnsEvidence = record(availability.dns);
|
|
281
|
-
const httpEvidence = record(availability.http);
|
|
282
|
-
const httpResponse = record(httpEvidence.response);
|
|
283
|
-
const tlsEvidence = record(availability.tls);
|
|
284
|
-
const tlsCertificate = record(tlsEvidence.certificate);
|
|
285
|
-
const pageIdentity = record(availability.pageIdentity);
|
|
286
|
-
const pageOpenGraph = record(pageIdentity.openGraph);
|
|
287
|
-
const pageResources = record(pageIdentity.resources);
|
|
288
|
-
const securityPosture = record(availability.securityPosture);
|
|
289
|
-
return {
|
|
290
|
-
availability,
|
|
291
|
-
rdap,
|
|
292
|
-
registrarRdap,
|
|
293
|
-
registrarRdapParsed: record(registrarRdap.parsed),
|
|
294
|
-
whois,
|
|
295
|
-
rdapParsed: record(rdap.parsed),
|
|
296
|
-
whoisParsed: record(whois.parsed),
|
|
297
|
-
diagnostics,
|
|
298
|
-
timing: normalizeLookupTiming(diagnostics.timing),
|
|
299
|
-
registryAccess: record(diagnostics.registryAccess),
|
|
300
|
-
registryInsights,
|
|
301
|
-
reverseDns,
|
|
302
|
-
reverseDnsRecords: record(reverseDns.records),
|
|
303
|
-
observedNetworkContext,
|
|
304
|
-
observedNetworkEndpoint: record(observedNetworkContext.endpoint),
|
|
305
|
-
observedNetworkRdap: record(observedNetworkContext.rdap),
|
|
306
|
-
observedNetwork: record(observedNetworkContext.network),
|
|
307
|
-
securityTxt,
|
|
308
|
-
sslbl,
|
|
309
|
-
threatIntelligence,
|
|
310
|
-
threatIntelligenceProviders: providers,
|
|
311
|
-
dnsEvidence,
|
|
312
|
-
dnsRecords: record(dnsEvidence.records),
|
|
313
|
-
httpEvidence,
|
|
314
|
-
httpResponse,
|
|
315
|
-
httpSecurityHeaders: record(httpResponse.securityHeaders),
|
|
316
|
-
tlsEvidence,
|
|
317
|
-
tlsCertificate,
|
|
318
|
-
tlsSubject: record(tlsCertificate.subject),
|
|
319
|
-
tlsIssuer: record(tlsCertificate.issuer),
|
|
320
|
-
tlsAltNames: record(tlsCertificate.subjectAltNames),
|
|
321
|
-
tlsPublicKey: record(tlsCertificate.publicKey),
|
|
322
|
-
tlsCipher: record(tlsEvidence.cipher),
|
|
323
|
-
tlsAuthorization: record(tlsEvidence.authorization),
|
|
324
|
-
tlsHostname: record(tlsEvidence.hostname),
|
|
325
|
-
tlsValidity: record(tlsEvidence.validity),
|
|
326
|
-
tlsDiagnostics: record(tlsEvidence.diagnostics),
|
|
327
|
-
pageIdentity,
|
|
328
|
-
pageCanonical: record(pageIdentity.canonical),
|
|
329
|
-
pageMetaRefresh: record(pageIdentity.metaRefresh),
|
|
330
|
-
pageOpenGraph,
|
|
331
|
-
pageOpenGraphUrl: record(pageOpenGraph.url),
|
|
332
|
-
pageForms: record(pageIdentity.forms),
|
|
333
|
-
pageResources,
|
|
334
|
-
pageResourceTypes: record(pageResources.byType),
|
|
335
|
-
pageDownloads: record(pageIdentity.downloads),
|
|
336
|
-
pageFingerprints: record(pageIdentity.fingerprints),
|
|
337
|
-
credentialSurfaceProfile: record(availability.credentialSurfaceProfile),
|
|
338
|
-
structuredDataIdentity: record(availability.structuredDataIdentity),
|
|
339
|
-
technologyProfile: record(availability.technologyProfile),
|
|
340
|
-
pageRoleProfile: record(availability.pageRoleProfile),
|
|
341
|
-
clientBehaviorProfile: record(availability.clientBehaviorProfile),
|
|
342
|
-
securityPosture,
|
|
343
|
-
securityPostureSummary: record(securityPosture.summary),
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
export { INVALID_COMPACT_LOOKUP_RESPONSE, INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE, INVALID_LOOKUP_RESPONSE, INVALID_LOOKUP_RESPONSE_MESSAGE, MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS, MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS, MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_RESPONSE_ERROR_LENGTH, MAX_LOOKUP_RESPONSE_HOST_LENGTH, MAX_LOOKUP_RESPONSE_QUERY_LENGTH, MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_TIMING_MS, MAX_LOOKUP_TIMING_SOURCES, MAX_THREAT_INTELLIGENCE_PROVIDERS, createLookupHttpResponse, createLookupViewModel, isJsonObject, lookupHttpErrorMessage, normalizeLookupTiming, parseCompactLookupHttpResponse, parseLookupHttpResponse, record as lookupRecord, };
|
package/lib/observation.mjs
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// Compact provenance envelope for network-derived evidence. Type-specific
|
|
2
|
-
// payloads remain beside this object; the envelope only standardizes 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_LIMITATIONS = 10;
|
|
16
|
-
const MAX_LIMITATION_LENGTH = 300;
|
|
17
|
-
const MAX_DIAGNOSTICS = 20;
|
|
18
|
-
const MAX_DIAGNOSTIC_KEY = 40;
|
|
19
|
-
const MAX_DIAGNOSTIC_STRING = 240;
|
|
20
|
-
const MAX_DURATION_MS = 120_000;
|
|
21
|
-
function isObservationStatus(value) {
|
|
22
|
-
return typeof value === 'string' && STATUSES.has(value);
|
|
23
|
-
}
|
|
24
|
-
function isScanMode(value) {
|
|
25
|
-
return typeof value === 'string' && SCAN_MODES.has(value);
|
|
26
|
-
}
|
|
27
|
-
function safeString(value, maxLength) {
|
|
28
|
-
if (typeof value !== 'string')
|
|
29
|
-
return null;
|
|
30
|
-
const normalized = value.trim().replace(/[\u0000-\u001f\u007f]/g, ' ');
|
|
31
|
-
return normalized ? normalized.slice(0, maxLength) : null;
|
|
32
|
-
}
|
|
33
|
-
function isoTimestamp(value) {
|
|
34
|
-
if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/.test(value))
|
|
35
|
-
return null;
|
|
36
|
-
const time = Date.parse(value);
|
|
37
|
-
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
38
|
-
}
|
|
39
|
-
function normalizeDiagnosticValue(value) {
|
|
40
|
-
if (typeof value === 'boolean')
|
|
41
|
-
return value;
|
|
42
|
-
if (typeof value === 'number')
|
|
43
|
-
return Number.isFinite(value) ? value : null;
|
|
44
|
-
if (typeof value === 'string')
|
|
45
|
-
return safeString(value, MAX_DIAGNOSTIC_STRING);
|
|
46
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
47
|
-
return null;
|
|
48
|
-
const record = value;
|
|
49
|
-
const output = {};
|
|
50
|
-
for (const field of ['status', 'error', 'detail', 'truncated', 'discarded', 'count']) {
|
|
51
|
-
if (!Object.prototype.hasOwnProperty.call(record, field))
|
|
52
|
-
continue;
|
|
53
|
-
const normalized = normalizeDiagnosticValue(record[field]);
|
|
54
|
-
if (normalized !== null)
|
|
55
|
-
output[field] = normalized;
|
|
56
|
-
}
|
|
57
|
-
return Object.keys(output).length ? output : null;
|
|
58
|
-
}
|
|
59
|
-
function normalizeDiagnostics(value) {
|
|
60
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
61
|
-
return {};
|
|
62
|
-
const record = value;
|
|
63
|
-
const output = {};
|
|
64
|
-
for (const key of Object.keys(record).sort().slice(0, MAX_DIAGNOSTICS)) {
|
|
65
|
-
if (!key || key.length > MAX_DIAGNOSTIC_KEY || !/^[a-z0-9_-]+$/i.test(key))
|
|
66
|
-
continue;
|
|
67
|
-
const normalized = normalizeDiagnosticValue(record[key]);
|
|
68
|
-
if (normalized !== null)
|
|
69
|
-
output[key] = normalized;
|
|
70
|
-
}
|
|
71
|
-
return output;
|
|
72
|
-
}
|
|
73
|
-
function createObservation(input = {}) {
|
|
74
|
-
const status = isObservationStatus(input.status) ? input.status : 'error';
|
|
75
|
-
const source = safeString(input.source, 40) || 'unknown';
|
|
76
|
-
const observedAt = isoTimestamp(input.observedAt) || new Date().toISOString();
|
|
77
|
-
const duration = Number(input.durationMs);
|
|
78
|
-
const limitations = [...new Set((Array.isArray(input.limitations) ? input.limitations : [])
|
|
79
|
-
.map((item) => safeString(item, MAX_LIMITATION_LENGTH))
|
|
80
|
-
.filter((item) => item !== null))]
|
|
81
|
-
.slice(0, MAX_LIMITATIONS);
|
|
82
|
-
return {
|
|
83
|
-
version: OBSERVATION_VERSION,
|
|
84
|
-
status,
|
|
85
|
-
observedAt,
|
|
86
|
-
scanMode: isScanMode(input.scanMode) ? input.scanMode : null,
|
|
87
|
-
source,
|
|
88
|
-
durationMs: Number.isFinite(duration) ? Math.max(0, Math.min(MAX_DURATION_MS, Math.round(duration))) : null,
|
|
89
|
-
complete: input.complete === true,
|
|
90
|
-
truncated: input.truncated === true,
|
|
91
|
-
limitations,
|
|
92
|
-
diagnostics: normalizeDiagnostics(input.diagnostics),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function readObservationEnvelope(value) {
|
|
96
|
-
if (value === undefined || value === null)
|
|
97
|
-
return { state: 'absent', observation: null };
|
|
98
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
99
|
-
return { state: 'invalid', observation: null };
|
|
100
|
-
const record = value;
|
|
101
|
-
if (typeof record.version === 'number'
|
|
102
|
-
&& Number.isInteger(record.version)
|
|
103
|
-
&& record.version > OBSERVATION_VERSION) {
|
|
104
|
-
return { state: 'unsupported', observation: null };
|
|
105
|
-
}
|
|
106
|
-
if (record.version !== OBSERVATION_VERSION
|
|
107
|
-
|| !isObservationStatus(record.status)
|
|
108
|
-
|| !safeString(record.source, 40)
|
|
109
|
-
|| !isoTimestamp(record.observedAt)) {
|
|
110
|
-
return { state: 'invalid', observation: null };
|
|
111
|
-
}
|
|
112
|
-
return { state: 'supported', observation: createObservation(record) };
|
|
113
|
-
}
|
|
114
|
-
export { OBSERVATION_VERSION, createObservation, readObservationEnvelope, };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|