@slicedearth/whoisleuth-cli 1.47.4 → 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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- 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 +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- 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 +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -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 +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- 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-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- 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-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- 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 +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- 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/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- 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 -21
- 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} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- 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/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -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/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- 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} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- 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/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- 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} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -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} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- 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} +13 -13
- 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} +110 -89
- package/third-party-notices.txt +14 -14
- 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 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- 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 -255
- 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 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /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,37 +1,106 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { domainToASCII } from 'node:url';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { canonicalArtifactJsonFor, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
+
import { DOMAIN_CONTROL_MANIFEST_CURRENT_CANONICALIZATION, DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_INPUT_RECORDS, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH, } from "../packages/contracts/domain-control-manifest.mjs";
|
|
5
|
+
import { DOMAIN_CONTROL_REVIEW_FIELDS, DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS, DOMAIN_CONTROL_REVIEW_COUNT_KEYS, DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS, DOMAIN_CONTROL_REVIEW_INPUT_KEYS, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_LIMITATIONS, DOMAIN_CONTROL_REVIEW_MANIFEST_SUMMARY_KEYS, DOMAIN_CONTROL_REVIEW_OBSERVATION_FIELD_KEYS, DOMAIN_CONTROL_REVIEW_OBSERVATION_KEYS, DOMAIN_CONTROL_REVIEW_ROOT_KEYS, DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_OBSERVATIONS, MAX_DOMAIN_CONTROL_REVIEW_SOURCE_LENGTH, MAX_DOMAIN_CONTROL_REVIEW_TEXT_LENGTH, } from "../packages/contracts/domain-control-review.mjs";
|
|
6
|
+
import { assertDomainControlManifestByteBudget, buildUnsignedDomainControlManifest, canonicalDomainControlRecordList, normalizeDomainControlManifestDocument, } from "../packages/evidence/domain-control-runtime.mjs";
|
|
6
7
|
import { exactKeys } from "./bounded-contract-normalizers.mjs";
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
8
|
+
import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
|
+
export { DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION, };
|
|
10
|
+
export const MAX_DOMAIN_CONTROL_ENTRIES = MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES;
|
|
11
|
+
export const MAX_DOMAIN_CONTROL_RECORDS = MAX_CANONICAL_DOMAIN_CONTROL_RECORDS;
|
|
12
|
+
const REVIEW_INPUT_KEYS = new Set(DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
|
|
13
|
+
const OBSERVATION_KEYS = new Set(DOMAIN_CONTROL_REVIEW_OBSERVATION_KEYS);
|
|
14
|
+
const OBSERVATION_FIELDS = new Set(DOMAIN_CONTROL_REVIEW_FIELDS);
|
|
15
|
+
const OBSERVATION_FIELD_KEYS = new Set(DOMAIN_CONTROL_REVIEW_OBSERVATION_FIELD_KEYS);
|
|
16
|
+
const REVIEW_ROOT_KEYS = new Set(DOMAIN_CONTROL_REVIEW_ROOT_KEYS);
|
|
17
|
+
const REVIEW_MANIFEST_KEYS = new Set(DOMAIN_CONTROL_REVIEW_MANIFEST_SUMMARY_KEYS);
|
|
18
|
+
const REVIEW_COUNT_KEYS = new Set(DOMAIN_CONTROL_REVIEW_COUNT_KEYS);
|
|
19
|
+
const REVIEW_DOMAIN_KEYS = new Set(DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS);
|
|
20
|
+
const REVIEW_COMPARISON_KEYS = new Set(DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS);
|
|
19
21
|
function record(value) {
|
|
20
22
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
21
23
|
? value
|
|
22
24
|
: null;
|
|
23
25
|
}
|
|
26
|
+
function exactReviewRecord(value, keys, label) {
|
|
27
|
+
try {
|
|
28
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
29
|
+
throw new TypeError(`${label} must be an object.`);
|
|
30
|
+
}
|
|
31
|
+
const prototype = Object.getPrototypeOf(value);
|
|
32
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
33
|
+
throw new TypeError(`${label} must be an ordinary object.`);
|
|
34
|
+
}
|
|
35
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
36
|
+
if (ownKeys.length !== keys.size
|
|
37
|
+
|| ownKeys.some((key) => typeof key !== 'string' || !keys.has(key))) {
|
|
38
|
+
throw new TypeError(`${label} must use its exact registered fields.`);
|
|
39
|
+
}
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
43
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
44
|
+
throw new TypeError(`${label}.${key} must be an enumerable data field.`);
|
|
45
|
+
}
|
|
46
|
+
result[key] = descriptor.value;
|
|
47
|
+
}
|
|
48
|
+
return Object.freeze(result);
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
if (cause instanceof TypeError && cause.message.startsWith(label))
|
|
52
|
+
throw cause;
|
|
53
|
+
throw new TypeError(`${label} must be an ordinary object.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function exactReviewText(value, maximum, label) {
|
|
57
|
+
const normalized = boundedText(value, maximum);
|
|
58
|
+
if (!normalized || normalized !== value) {
|
|
59
|
+
throw new TypeError(`${label} must be a canonical bounded string.`);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
function exactReviewTimestamp(value, label) {
|
|
64
|
+
const normalized = timestamp(value);
|
|
65
|
+
if (!normalized || normalized !== value)
|
|
66
|
+
throw new TypeError(`${label} must be a canonical timestamp.`);
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
function exactReviewInteger(value, maximum, label) {
|
|
70
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0 || Number(value) > maximum) {
|
|
71
|
+
throw new TypeError(`${label} must be a bounded non-negative integer.`);
|
|
72
|
+
}
|
|
73
|
+
return Number(value);
|
|
74
|
+
}
|
|
75
|
+
function exactReviewStringArray(value, maximumItems, maximumCharacters, label) {
|
|
76
|
+
const input = boundedDataArray(value, maximumItems);
|
|
77
|
+
if (!input)
|
|
78
|
+
throw new TypeError(`${label} must be a bounded array.`);
|
|
79
|
+
const output = input.map((item, index) => exactReviewText(item, maximumCharacters, `${label}[${index}]`));
|
|
80
|
+
if (new Set(output).size !== output.length
|
|
81
|
+
|| output.some((item, index) => index > 0 && output[index - 1] >= item)) {
|
|
82
|
+
throw new TypeError(`${label} must contain unique canonically ordered values.`);
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze(output);
|
|
85
|
+
}
|
|
24
86
|
function boundedText(value, maximum = 300) {
|
|
25
|
-
if (typeof value !== 'string'
|
|
87
|
+
if (typeof value !== 'string'
|
|
88
|
+
|| value.length > maximum * DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR
|
|
89
|
+
|| /[\u0000-\u001f\u007f]/u.test(value))
|
|
26
90
|
return null;
|
|
27
91
|
return value.replace(/\s+/gu, ' ').trim().slice(0, maximum) || null;
|
|
28
92
|
}
|
|
29
|
-
function timestamp(value) {
|
|
30
|
-
|
|
31
|
-
|
|
93
|
+
function timestamp(value, legacy = false) {
|
|
94
|
+
if (typeof value !== 'string'
|
|
95
|
+
|| value.length > MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH * DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR
|
|
96
|
+
|| /[\u0000-\u001f\u007f]/u.test(value))
|
|
32
97
|
return null;
|
|
33
|
-
const
|
|
34
|
-
|
|
98
|
+
const text = value.replace(/\s+/gu, ' ').trim();
|
|
99
|
+
if (!text || text.length > MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH)
|
|
100
|
+
return null;
|
|
101
|
+
return text
|
|
102
|
+
? normalizeExplicitIsoTimestamp(text) ?? (legacy ? normalizeLegacyIsoTimestamp(text) : null)
|
|
103
|
+
: null;
|
|
35
104
|
}
|
|
36
105
|
function domain(value) {
|
|
37
106
|
const text = boundedText(value, 253)?.toLowerCase().replace(/\.$/u, '');
|
|
@@ -43,59 +112,110 @@ function domain(value) {
|
|
|
43
112
|
return ascii;
|
|
44
113
|
}
|
|
45
114
|
function hostnameList(value) {
|
|
46
|
-
|
|
47
|
-
return [];
|
|
48
|
-
return [...new Set(value.slice(0, MAX_DOMAIN_CONTROL_RECORDS * 4)
|
|
115
|
+
return [...new Set(value
|
|
49
116
|
.map(domain)
|
|
50
117
|
.filter((item) => item !== null))]
|
|
51
118
|
.sort()
|
|
52
119
|
.slice(0, MAX_DOMAIN_CONTROL_RECORDS);
|
|
53
120
|
}
|
|
54
121
|
function textList(value, maximum = 300) {
|
|
55
|
-
|
|
56
|
-
return [];
|
|
57
|
-
return [...new Set(value.slice(0, MAX_DOMAIN_CONTROL_RECORDS * 4)
|
|
122
|
+
return [...new Set(value
|
|
58
123
|
.map((item) => boundedText(item, maximum)?.toLowerCase() ?? null)
|
|
59
124
|
.filter((item) => item !== null))]
|
|
60
125
|
.sort()
|
|
61
126
|
.slice(0, MAX_DOMAIN_CONTROL_RECORDS);
|
|
62
127
|
}
|
|
63
128
|
function spkiFingerprint(value) {
|
|
64
|
-
|
|
129
|
+
if (typeof value !== 'string'
|
|
130
|
+
|| value.length > 64 * DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR
|
|
131
|
+
|| /[\u0000-\u001f\u007f]/u.test(value))
|
|
132
|
+
return null;
|
|
133
|
+
const text = value.trim().toLowerCase();
|
|
65
134
|
return text && /^[a-f0-9]{64}$/u.test(text) ? text : null;
|
|
66
135
|
}
|
|
67
136
|
export function buildDomainControlManifest(input, generatedAtValue = new Date().toISOString()) {
|
|
68
|
-
const unsigned =
|
|
69
|
-
|
|
137
|
+
const unsigned = buildUnsignedDomainControlManifest(input, generatedAtValue);
|
|
138
|
+
const manifest = Object.freeze({
|
|
70
139
|
...unsigned,
|
|
71
140
|
integrity: Object.freeze({
|
|
72
141
|
algorithm: 'SHA-256',
|
|
73
|
-
canonicalization:
|
|
74
|
-
digestSha256: `sha256:${createHash('sha256').update(
|
|
142
|
+
canonicalization: DOMAIN_CONTROL_MANIFEST_CURRENT_CANONICALIZATION,
|
|
143
|
+
digestSha256: `sha256:${createHash('sha256').update(canonicalArtifactJsonFor(unsigned, DOMAIN_CONTROL_MANIFEST_CURRENT_CANONICALIZATION)).digest('hex')}`,
|
|
75
144
|
}),
|
|
76
145
|
});
|
|
146
|
+
assertDomainControlManifestByteBudget(manifest);
|
|
147
|
+
return manifest;
|
|
77
148
|
}
|
|
78
149
|
export function verifyDomainControlManifest(value) {
|
|
79
|
-
const normalized =
|
|
150
|
+
const normalized = normalizeDomainControlManifestDocument(value);
|
|
80
151
|
if (`sha256:${createHash('sha256').update(normalized.canonicalUnsigned).digest('hex')}` !== normalized.manifest.integrity.digestSha256) {
|
|
81
152
|
throw new TypeError('Domain control manifest failed its SHA-256 integrity check.');
|
|
82
153
|
}
|
|
83
154
|
return normalized.manifest;
|
|
84
155
|
}
|
|
156
|
+
function boundedDataArray(value, maximum) {
|
|
157
|
+
try {
|
|
158
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype)
|
|
159
|
+
return null;
|
|
160
|
+
if (Object.getOwnPropertyDescriptor(value, Symbol.iterator)
|
|
161
|
+
|| Object.getOwnPropertyDescriptor(value, 'toJSON'))
|
|
162
|
+
return null;
|
|
163
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
164
|
+
const length = lengthDescriptor && !lengthDescriptor.enumerable && 'value' in lengthDescriptor
|
|
165
|
+
? lengthDescriptor.value
|
|
166
|
+
: null;
|
|
167
|
+
if (!Number.isSafeInteger(length)
|
|
168
|
+
|| Number(length) < 0
|
|
169
|
+
|| Number(length) > maximum)
|
|
170
|
+
return null;
|
|
171
|
+
const boundedLength = Number(length);
|
|
172
|
+
const keys = Reflect.ownKeys(value);
|
|
173
|
+
if (keys.length !== boundedLength + 1 || !keys.includes('length'))
|
|
174
|
+
return null;
|
|
175
|
+
const result = [];
|
|
176
|
+
for (let index = 0; index < boundedLength; index += 1) {
|
|
177
|
+
const key = String(index);
|
|
178
|
+
if (!keys.includes(key))
|
|
179
|
+
return null;
|
|
180
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
181
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor))
|
|
182
|
+
return null;
|
|
183
|
+
result.push(descriptor.value);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
85
191
|
function fieldValues(field, value) {
|
|
86
|
-
if (field === 'nameservers')
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
192
|
+
if (field === 'nameservers') {
|
|
193
|
+
const normalized = value.map(domain);
|
|
194
|
+
return normalized.some((item) => item === null)
|
|
195
|
+
? null
|
|
196
|
+
: hostnameList(normalized);
|
|
197
|
+
}
|
|
198
|
+
if (field === 'mx' || field === 'caa' || field === 'ds') {
|
|
199
|
+
const normalized = value.flatMap((item) => canonicalDomainControlRecordList([item], field));
|
|
200
|
+
return normalized.length === value.length
|
|
201
|
+
? [...new Set(normalized)].sort().slice(0, MAX_DOMAIN_CONTROL_RECORDS)
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
90
204
|
if (field === 'tlsSpkiSha256') {
|
|
91
|
-
const normalized =
|
|
92
|
-
return
|
|
205
|
+
const normalized = value.map(spkiFingerprint);
|
|
206
|
+
return normalized.some((item) => item === null)
|
|
207
|
+
? null
|
|
208
|
+
: [...new Set(normalized)].sort().slice(0, MAX_DOMAIN_CONTROL_RECORDS);
|
|
93
209
|
}
|
|
94
210
|
if (field === 'registrarLock') {
|
|
95
|
-
|
|
96
|
-
|
|
211
|
+
return value.every((item) => item === 'required' || item === 'not_required')
|
|
212
|
+
? [...new Set(value)].sort()
|
|
213
|
+
: null;
|
|
97
214
|
}
|
|
98
|
-
|
|
215
|
+
const normalized = value.map((item) => boundedText(item, MAX_DOMAIN_CONTROL_REVIEW_TEXT_LENGTH)?.toLowerCase() ?? null);
|
|
216
|
+
return normalized.some((item) => item === null)
|
|
217
|
+
? null
|
|
218
|
+
: textList(normalized, MAX_DOMAIN_CONTROL_REVIEW_TEXT_LENGTH);
|
|
99
219
|
}
|
|
100
220
|
function normalizeObservation(value) {
|
|
101
221
|
const source = record(value);
|
|
@@ -106,22 +226,31 @@ function normalizeObservation(value) {
|
|
|
106
226
|
exactKeys(source, OBSERVATION_KEYS, 'Domain control observation');
|
|
107
227
|
exactKeys(rawFields, OBSERVATION_FIELDS, 'Domain control observation fields');
|
|
108
228
|
const fields = {};
|
|
109
|
-
for (const field of
|
|
229
|
+
for (const field of DOMAIN_CONTROL_REVIEW_FIELDS) {
|
|
230
|
+
if (!Object.hasOwn(rawFields, field))
|
|
231
|
+
continue;
|
|
110
232
|
const raw = record(rawFields[field]);
|
|
111
233
|
if (!raw)
|
|
112
|
-
|
|
234
|
+
return null;
|
|
113
235
|
exactKeys(raw, OBSERVATION_FIELD_KEYS, `Domain control observation field ${field}`);
|
|
236
|
+
if (!DOMAIN_CONTROL_REVIEW_OBSERVATION_FIELD_KEYS.every((key) => Object.hasOwn(raw, key)))
|
|
237
|
+
return null;
|
|
114
238
|
const state = raw.state;
|
|
115
239
|
if (state !== 'observed' && state !== 'partial' && state !== 'unavailable' && state !== 'unsupported')
|
|
116
240
|
return null;
|
|
117
|
-
const sourceLabel = boundedText(raw.source,
|
|
118
|
-
|
|
241
|
+
const sourceLabel = boundedText(raw.source, MAX_DOMAIN_CONTROL_REVIEW_SOURCE_LENGTH);
|
|
242
|
+
const values = boundedDataArray(raw.values, MAX_DOMAIN_CONTROL_INPUT_RECORDS);
|
|
243
|
+
if (!sourceLabel || !values)
|
|
244
|
+
return null;
|
|
245
|
+
const normalizedValues = fieldValues(field, values);
|
|
246
|
+
const observedAt = raw.observedAt === null ? null : timestamp(raw.observedAt, true);
|
|
247
|
+
if (!normalizedValues || (raw.observedAt !== null && !observedAt))
|
|
119
248
|
return null;
|
|
120
249
|
fields[field] = Object.freeze({
|
|
121
250
|
state,
|
|
122
|
-
values: Object.freeze(
|
|
251
|
+
values: Object.freeze(normalizedValues),
|
|
123
252
|
source: sourceLabel,
|
|
124
|
-
observedAt
|
|
253
|
+
observedAt,
|
|
125
254
|
});
|
|
126
255
|
}
|
|
127
256
|
return Object.freeze({ domain: normalizedDomain, fields: Object.freeze(fields) });
|
|
@@ -187,18 +316,19 @@ function renewalComparison(entry, now) {
|
|
|
187
316
|
}
|
|
188
317
|
export function reviewDomainControlManifest(input, generatedAtValue = new Date().toISOString()) {
|
|
189
318
|
const source = record(input);
|
|
190
|
-
if (!source || source.schema !== DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA || source.version !==
|
|
191
|
-
throw new TypeError(`Domain control review input must use ${DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version
|
|
319
|
+
if (!source || source.schema !== DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA || source.version !== DOMAIN_CONTROL_REVIEW_VERSION) {
|
|
320
|
+
throw new TypeError(`Domain control review input must use ${DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${DOMAIN_CONTROL_REVIEW_VERSION}.`);
|
|
192
321
|
}
|
|
193
322
|
exactKeys(source, REVIEW_INPUT_KEYS, 'Domain control review input');
|
|
194
323
|
const generatedAt = timestamp(generatedAtValue);
|
|
195
324
|
if (!generatedAt)
|
|
196
|
-
throw new TypeError('Domain control review time
|
|
325
|
+
throw new TypeError('Domain control review time must be valid and include an explicit timezone.');
|
|
197
326
|
const manifest = verifyDomainControlManifest(source.manifest);
|
|
198
|
-
|
|
327
|
+
const rawObservations = boundedDataArray(source.observations, MAX_DOMAIN_CONTROL_REVIEW_OBSERVATIONS);
|
|
328
|
+
if (!rawObservations) {
|
|
199
329
|
throw new TypeError('Domain control observations must be a bounded array.');
|
|
200
330
|
}
|
|
201
|
-
const observations =
|
|
331
|
+
const observations = rawObservations.map(normalizeObservation);
|
|
202
332
|
if (observations.some((item) => item === null))
|
|
203
333
|
throw new TypeError('Domain control review contains an invalid observation.');
|
|
204
334
|
const normalizedObservations = observations;
|
|
@@ -206,7 +336,7 @@ export function reviewDomainControlManifest(input, generatedAtValue = new Date()
|
|
|
206
336
|
throw new TypeError('Domain control observations must use unique domains.');
|
|
207
337
|
}
|
|
208
338
|
const observationMap = new Map(normalizedObservations.map((item) => [item.domain, item]));
|
|
209
|
-
const fields =
|
|
339
|
+
const fields = DOMAIN_CONTROL_REVIEW_FIELDS;
|
|
210
340
|
const domains = manifest.entries.map((entry) => {
|
|
211
341
|
const observation = observationMap.get(entry.domain) ?? null;
|
|
212
342
|
const comparisons = [...fields.map((field) => compareField(entry, observation, field)), renewalComparison(entry, generatedAt)];
|
|
@@ -235,11 +365,136 @@ export function reviewDomainControlManifest(input, generatedAtValue = new Date()
|
|
|
235
365
|
counts: Object.freeze(counts),
|
|
236
366
|
domains: Object.freeze(domains),
|
|
237
367
|
ignoredObservationCount: normalizedObservations.filter((item) => !manifest.entries.some((entry) => entry.domain === item.domain)).length,
|
|
238
|
-
limitations:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
368
|
+
limitations: DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
export function validateDomainControlReviewDocument(value) {
|
|
372
|
+
const root = exactReviewRecord(value, REVIEW_ROOT_KEYS, 'Domain control review document');
|
|
373
|
+
if (root.schema !== DOMAIN_CONTROL_REVIEW_SCHEMA || root.version !== DOMAIN_CONTROL_REVIEW_VERSION) {
|
|
374
|
+
throw new TypeError(`Domain control review document must use ${DOMAIN_CONTROL_REVIEW_SCHEMA} version ${DOMAIN_CONTROL_REVIEW_VERSION}.`);
|
|
375
|
+
}
|
|
376
|
+
const generatedAt = exactReviewTimestamp(root.generatedAt, 'Domain control review document generatedAt');
|
|
377
|
+
const manifestInput = exactReviewRecord(root.manifest, REVIEW_MANIFEST_KEYS, 'Domain control review document manifest');
|
|
378
|
+
const manifestGeneratedAt = exactReviewTimestamp(manifestInput.generatedAt, 'Domain control review document manifest generatedAt');
|
|
379
|
+
const expiresAt = exactReviewTimestamp(manifestInput.expiresAt, 'Domain control review document manifest expiresAt');
|
|
380
|
+
if (typeof manifestInput.digestSha256 !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test(manifestInput.digestSha256)) {
|
|
381
|
+
throw new TypeError('Domain control review document manifest digest is invalid.');
|
|
382
|
+
}
|
|
383
|
+
const expired = Date.parse(expiresAt) <= Date.parse(generatedAt);
|
|
384
|
+
if (manifestInput.expired !== expired) {
|
|
385
|
+
throw new TypeError('Domain control review document manifest currentness is inconsistent.');
|
|
386
|
+
}
|
|
387
|
+
const manifest = Object.freeze({
|
|
388
|
+
generatedAt: manifestGeneratedAt,
|
|
389
|
+
expiresAt,
|
|
390
|
+
digestSha256: manifestInput.digestSha256,
|
|
391
|
+
expired,
|
|
392
|
+
});
|
|
393
|
+
const countInput = exactReviewRecord(root.counts, REVIEW_COUNT_KEYS, 'Domain control review document counts');
|
|
394
|
+
const maximumComparisons = MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES * (DOMAIN_CONTROL_REVIEW_FIELDS.length + 1);
|
|
395
|
+
const suppliedCounts = Object.fromEntries(DOMAIN_CONTROL_REVIEW_COUNT_KEYS.map((state) => [
|
|
396
|
+
state,
|
|
397
|
+
exactReviewInteger(countInput[state], maximumComparisons, `Domain control review document count ${state}`),
|
|
398
|
+
]));
|
|
399
|
+
const domainInput = boundedDataArray(root.domains, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES);
|
|
400
|
+
if (!domainInput || domainInput.length < 1) {
|
|
401
|
+
throw new TypeError('Domain control review document domains must be a bounded non-empty array.');
|
|
402
|
+
}
|
|
403
|
+
const comparisonFields = [...DOMAIN_CONTROL_REVIEW_FIELDS, 'renewalReviewAt'];
|
|
404
|
+
const comparisonStates = new Set(DOMAIN_CONTROL_REVIEW_COUNT_KEYS);
|
|
405
|
+
const calculatedCounts = Object.fromEntries(DOMAIN_CONTROL_REVIEW_COUNT_KEYS.map((state) => [state, 0]));
|
|
406
|
+
const domains = domainInput.map((domainValue, domainIndex) => {
|
|
407
|
+
const item = exactReviewRecord(domainValue, REVIEW_DOMAIN_KEYS, `Domain control review document domain ${domainIndex + 1}`);
|
|
408
|
+
const normalizedDomain = domain(item.domain);
|
|
409
|
+
if (!normalizedDomain || normalizedDomain !== item.domain) {
|
|
410
|
+
throw new TypeError(`Domain control review document domain ${domainIndex + 1} is invalid.`);
|
|
411
|
+
}
|
|
412
|
+
const comparisonsInput = boundedDataArray(item.comparisons, comparisonFields.length);
|
|
413
|
+
if (!comparisonsInput || comparisonsInput.length !== comparisonFields.length) {
|
|
414
|
+
throw new TypeError(`Domain control review document domain ${domainIndex + 1} comparisons are incomplete.`);
|
|
415
|
+
}
|
|
416
|
+
const comparisons = comparisonsInput.map((comparisonValue, comparisonIndex) => {
|
|
417
|
+
const comparison = exactReviewRecord(comparisonValue, REVIEW_COMPARISON_KEYS, `Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1}`);
|
|
418
|
+
const expectedField = comparisonFields[comparisonIndex];
|
|
419
|
+
if (comparison.field !== expectedField
|
|
420
|
+
|| typeof comparison.state !== 'string'
|
|
421
|
+
|| !comparisonStates.has(comparison.state)) {
|
|
422
|
+
throw new TypeError(`Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1} is unsupported.`);
|
|
423
|
+
}
|
|
424
|
+
const state = comparison.state;
|
|
425
|
+
calculatedCounts[state] += 1;
|
|
426
|
+
const maximumValueCharacters = expectedField === 'ds'
|
|
427
|
+
? MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH
|
|
428
|
+
: expectedField === 'caa'
|
|
429
|
+
? MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH
|
|
430
|
+
: MAX_DOMAIN_CONTROL_REVIEW_TEXT_LENGTH;
|
|
431
|
+
const desired = exactReviewStringArray(comparison.desired, MAX_DOMAIN_CONTROL_RECORDS, maximumValueCharacters, `Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1} desired values`);
|
|
432
|
+
const observed = exactReviewStringArray(comparison.observed, MAX_DOMAIN_CONTROL_RECORDS, maximumValueCharacters, `Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1} observed values`);
|
|
433
|
+
const source = comparison.source === null
|
|
434
|
+
? null
|
|
435
|
+
: exactReviewText(comparison.source, MAX_DOMAIN_CONTROL_REVIEW_SOURCE_LENGTH, `Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1} source`);
|
|
436
|
+
const observedAt = comparison.observedAt === null
|
|
437
|
+
? null
|
|
438
|
+
: exactReviewTimestamp(comparison.observedAt, `Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1} observedAt`);
|
|
439
|
+
if (expectedField === 'renewalReviewAt' && (observed.length || source !== null || observedAt !== null)) {
|
|
440
|
+
throw new TypeError(`Domain control review document domain ${domainIndex + 1} renewal comparison is inconsistent.`);
|
|
441
|
+
}
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
field: expectedField,
|
|
444
|
+
state,
|
|
445
|
+
desired,
|
|
446
|
+
observed,
|
|
447
|
+
source,
|
|
448
|
+
observedAt,
|
|
449
|
+
explanation: exactReviewText(comparison.explanation, MAX_DOMAIN_CONTROL_REVIEW_TEXT_LENGTH, `Domain control review document domain ${domainIndex + 1} comparison ${comparisonIndex + 1} explanation`),
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
const expectedState = comparisons.some((comparison) => comparison.state === 'drift' || comparison.state === 'due')
|
|
453
|
+
? 'drift'
|
|
454
|
+
: comparisons.some((comparison) => comparison.state === 'partial'
|
|
455
|
+
|| comparison.state === 'unavailable'
|
|
456
|
+
|| comparison.state === 'unsupported')
|
|
457
|
+
? 'partial'
|
|
458
|
+
: 'aligned';
|
|
459
|
+
if (item.state !== expectedState) {
|
|
460
|
+
throw new TypeError(`Domain control review document domain ${domainIndex + 1} state is inconsistent.`);
|
|
461
|
+
}
|
|
462
|
+
return Object.freeze({ domain: normalizedDomain, state: expectedState, comparisons: Object.freeze(comparisons) });
|
|
463
|
+
});
|
|
464
|
+
if (new Set(domains.map((item) => item.domain)).size !== domains.length) {
|
|
465
|
+
throw new TypeError('Domain control review document domains must be unique.');
|
|
466
|
+
}
|
|
467
|
+
for (const state of DOMAIN_CONTROL_REVIEW_COUNT_KEYS) {
|
|
468
|
+
if (suppliedCounts[state] !== calculatedCounts[state]) {
|
|
469
|
+
throw new TypeError('Domain control review document counts are inconsistent.');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const expectedState = expired
|
|
473
|
+
? 'expired'
|
|
474
|
+
: domains.some((item) => item.state === 'drift')
|
|
475
|
+
? 'drift'
|
|
476
|
+
: domains.some((item) => item.state === 'partial')
|
|
477
|
+
? 'partial'
|
|
478
|
+
: 'aligned';
|
|
479
|
+
if (root.state !== expectedState)
|
|
480
|
+
throw new TypeError('Domain control review document state is inconsistent.');
|
|
481
|
+
const ignoredObservationCount = exactReviewInteger(root.ignoredObservationCount, MAX_DOMAIN_CONTROL_REVIEW_OBSERVATIONS, 'Domain control review document ignored observation count');
|
|
482
|
+
const limitationsInput = boundedDataArray(root.limitations, DOMAIN_CONTROL_REVIEW_LIMITATIONS.length);
|
|
483
|
+
if (!limitationsInput
|
|
484
|
+
|| limitationsInput.length !== DOMAIN_CONTROL_REVIEW_LIMITATIONS.length
|
|
485
|
+
|| limitationsInput.some((item, index) => item !== DOMAIN_CONTROL_REVIEW_LIMITATIONS[index])) {
|
|
486
|
+
throw new TypeError('Domain control review document limitations are invalid.');
|
|
487
|
+
}
|
|
488
|
+
return Object.freeze({
|
|
489
|
+
schema: DOMAIN_CONTROL_REVIEW_SCHEMA,
|
|
490
|
+
version: DOMAIN_CONTROL_REVIEW_VERSION,
|
|
491
|
+
generatedAt,
|
|
492
|
+
state: expectedState,
|
|
493
|
+
manifest,
|
|
494
|
+
counts: Object.freeze(suppliedCounts),
|
|
495
|
+
domains: Object.freeze(domains),
|
|
496
|
+
ignoredObservationCount,
|
|
497
|
+
limitations: DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
243
498
|
});
|
|
244
499
|
}
|
|
245
500
|
export function formatDomainControlResult(value) {
|
|
@@ -10,10 +10,13 @@ const SPF_MAX_BRANCHES = 32;
|
|
|
10
10
|
const MAX_DMARC_DESTINATIONS = 10;
|
|
11
11
|
const MAX_DEPENDENCIES = 64;
|
|
12
12
|
function strictHostname(value) {
|
|
13
|
-
const
|
|
13
|
+
const input = String(value || '').trim().replace(/\.+$/u, '');
|
|
14
|
+
if (/[^\x00-\x7f]/u.test(input))
|
|
15
|
+
return null;
|
|
16
|
+
const raw = input.toLowerCase();
|
|
14
17
|
if (!raw || raw.length > 253 || raw.includes('%'))
|
|
15
18
|
return null;
|
|
16
|
-
if (!raw.split('.').every((label) => /^(?:_?[a-z0-9](?:[a-z0-9_-]{0,61}[a-z0-9])?)$/
|
|
19
|
+
if (!raw.split('.').every((label) => /^(?:_?[a-z0-9](?:[a-z0-9_-]{0,61}[a-z0-9])?)$/u.test(label)))
|
|
17
20
|
return null;
|
|
18
21
|
return raw.includes('.') ? raw : null;
|
|
19
22
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
1
2
|
const ENCRYPTED_DNS_CONTRACT_SCHEMA = 'whoisleuth.encrypted-dns-adapter';
|
|
2
3
|
const ENCRYPTED_DNS_CONTRACT_VERSION = 1;
|
|
3
4
|
const DNS_TYPES = new Set(['A', 'AAAA', 'CAA', 'CNAME', 'DNSKEY', 'DS', 'HTTPS', 'MX', 'NS', 'RRSIG', 'SOA', 'SVCB', 'TLSA', 'TXT']);
|
|
@@ -28,9 +29,7 @@ function normalizeEncryptedDnsAdapter(value) {
|
|
|
28
29
|
const endpoint = httpsUrl(source.endpoint);
|
|
29
30
|
const termsUrl = httpsUrl(source.termsUrl);
|
|
30
31
|
const privacyUrl = httpsUrl(source.privacyUrl);
|
|
31
|
-
const reviewedAt =
|
|
32
|
-
? new Date(source.reviewedAt).toISOString()
|
|
33
|
-
: null;
|
|
32
|
+
const reviewedAt = normalizeExplicitIsoTimestamp(source.reviewedAt);
|
|
34
33
|
const method = source.method === 'GET' || source.method === 'POST' ? source.method : null;
|
|
35
34
|
const representation = source.representation === 'dns-json' || source.representation === 'dns-wire'
|
|
36
35
|
? source.representation
|