@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4
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 +67 -10
- package/bin/whoisleuth.mjs +22 -9
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +67 -31
- package/cli/bulk-command-runner.mjs +16 -6
- package/cli/bulk.mjs +18 -7
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +50 -11
- package/cli/config-profile.mjs +15 -12
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-observation-snapshot.mjs +8 -14
- package/cli/discovery-scan-command-runner.mjs +18 -6
- package/cli/discovery-snapshot.mjs +8 -15
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/errors.mjs +7 -1
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +12 -4
- package/cli/formatters/terminal.mjs +4 -1
- package/cli/http.mjs +2 -1
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/lookup-reconcile.mjs +1 -1
- package/cli/mail-review.mjs +5 -5
- package/cli/manual.mjs +1 -1
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-fixture-scaffold.mjs +4 -3
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +150 -177
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +467 -74
- package/docs/cli.md +25 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +1 -1
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +43 -13
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
- package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
- package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +27 -4
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
- package/frontend/src/lib/analysis/workspace-archive.js +105 -19
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +82 -0
- package/lib/bounded-json-response.mjs +170 -0
- package/lib/bounded-json.mjs +249 -0
- package/lib/browser-library-profile.mjs +167 -38
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +5 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +6 -6
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +96 -42
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +4 -13
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/website-technology.mjs +2 -6
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { safeComparisonLedgerHref, stableComparisonLedgerId, } from "./comparison-ledger-serialization.js";
|
|
2
|
+
import { projectComparisonLedgerJson } from "./comparison-ledger-json.js";
|
|
3
|
+
export { stableComparisonLedgerJson } from "./comparison-ledger-json.js";
|
|
4
|
+
export { safeComparisonLedgerHref, stableComparisonLedgerId } from "./comparison-ledger-serialization.js";
|
|
5
|
+
export const COMPARISON_LEDGER_MODES = Object.freeze([
|
|
6
|
+
'publication',
|
|
7
|
+
'entity',
|
|
8
|
+
'temporal',
|
|
9
|
+
'expectation',
|
|
10
|
+
'membership',
|
|
11
|
+
'reconciliation',
|
|
12
|
+
]);
|
|
13
|
+
export const COMPARISON_LEDGER_STATES = Object.freeze([
|
|
14
|
+
'equivalent',
|
|
15
|
+
'added',
|
|
16
|
+
'removed',
|
|
17
|
+
'different',
|
|
18
|
+
'conflict',
|
|
19
|
+
'collection_changed',
|
|
20
|
+
'model_changed',
|
|
21
|
+
'incomplete',
|
|
22
|
+
'unavailable',
|
|
23
|
+
'unsupported',
|
|
24
|
+
'not_compared',
|
|
25
|
+
]);
|
|
26
|
+
export const MAX_COMPARISON_LEDGER_INDEX_ITEMS = 2_000;
|
|
27
|
+
export const MAX_COMPARISON_LEDGER_ENTITIES_PER_REQUEST = 20;
|
|
28
|
+
export const MAX_COMPARISON_LEDGER_DETAIL_ROWS = 256;
|
|
29
|
+
export const MAX_COMPARISON_LEDGER_LIMITATIONS = 8;
|
|
30
|
+
export const MAX_COMPARISON_LEDGER_STRING_LENGTH = 500;
|
|
31
|
+
export const MAX_COMPARISON_LEDGER_ID_LENGTH = 128;
|
|
32
|
+
export const MAX_COMPARISON_LEDGER_BULK_PAIRS = MAX_COMPARISON_LEDGER_INDEX_ITEMS;
|
|
33
|
+
const MAX_LABEL_LENGTH = 160;
|
|
34
|
+
const MAX_FIELD_LENGTH = 120;
|
|
35
|
+
const MAX_SOURCE_LENGTH = 120;
|
|
36
|
+
const MAX_LIMITATION_SCAN = MAX_COMPARISON_LEDGER_LIMITATIONS * 4;
|
|
37
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/gu;
|
|
38
|
+
const CONTROL_TEST_RE = /[\u0000-\u001f\u007f]/u;
|
|
39
|
+
export function freshComparisonLedgerCounters() {
|
|
40
|
+
return { inputRecords: 0, inputScanTruncations: 0, invalidRecords: 0, duplicateRecords: 0, limitations: 0, truncatedStrings: 0 };
|
|
41
|
+
}
|
|
42
|
+
export function comparisonLedgerRecord(value) {
|
|
43
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
44
|
+
? value
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
export function comparisonLedgerInputArray(value) {
|
|
48
|
+
return Array.isArray(value) ? value : [];
|
|
49
|
+
}
|
|
50
|
+
export function boundedComparisonLedgerText(value, maximum, counters, preserveWhitespace = false) {
|
|
51
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
52
|
+
return '';
|
|
53
|
+
const raw = String(value).replace(CONTROL_RE, ' ');
|
|
54
|
+
const normalised = preserveWhitespace ? raw.trim() : raw.replace(/\s+/gu, ' ').trim();
|
|
55
|
+
if (normalised.length > maximum)
|
|
56
|
+
counters && (counters.truncatedStrings += 1);
|
|
57
|
+
return normalised.slice(0, maximum).trim();
|
|
58
|
+
}
|
|
59
|
+
export function boundedComparisonLedgerId(value, counters) {
|
|
60
|
+
const text = boundedComparisonLedgerText(value, MAX_COMPARISON_LEDGER_ID_LENGTH * 2, counters);
|
|
61
|
+
const safe = text.replace(/[^A-Za-z0-9:._-]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
62
|
+
if (safe.length > MAX_COMPARISON_LEDGER_ID_LENGTH)
|
|
63
|
+
counters && (counters.truncatedStrings += 1);
|
|
64
|
+
return safe.slice(0, MAX_COMPARISON_LEDGER_ID_LENGTH);
|
|
65
|
+
}
|
|
66
|
+
function timestamp(value) {
|
|
67
|
+
if (typeof value !== 'string' || value.length > 64 || CONTROL_TEST_RE.test(value))
|
|
68
|
+
return null;
|
|
69
|
+
const parsed = Date.parse(value);
|
|
70
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
71
|
+
}
|
|
72
|
+
function normaliseCompleteness(value) {
|
|
73
|
+
return value === 'complete' || value === 'partial' || value === 'unavailable' || value === 'not_reported'
|
|
74
|
+
? value
|
|
75
|
+
: 'not_reported';
|
|
76
|
+
}
|
|
77
|
+
export function boundedComparisonLedgerLimitations(value, counters) {
|
|
78
|
+
const raw = Array.isArray(value) ? value : [];
|
|
79
|
+
const unscanned = Math.max(0, raw.length - MAX_LIMITATION_SCAN);
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const all = [];
|
|
82
|
+
for (const item of raw.slice(0, MAX_LIMITATION_SCAN)) {
|
|
83
|
+
const entry = boundedComparisonLedgerText(item, MAX_COMPARISON_LEDGER_STRING_LENGTH, counters);
|
|
84
|
+
if (!entry || seen.has(entry))
|
|
85
|
+
continue;
|
|
86
|
+
seen.add(entry);
|
|
87
|
+
all.push(entry);
|
|
88
|
+
}
|
|
89
|
+
const values = all.slice(0, MAX_COMPARISON_LEDGER_LIMITATIONS);
|
|
90
|
+
const omitted = unscanned + Math.max(0, all.length - values.length);
|
|
91
|
+
counters.limitations += omitted;
|
|
92
|
+
return { values: Object.freeze(values), omitted };
|
|
93
|
+
}
|
|
94
|
+
function exactValue(value, counters) {
|
|
95
|
+
if (value === null || value === undefined)
|
|
96
|
+
return { value: null, truncated: false };
|
|
97
|
+
const traversal = typeof value === 'string'
|
|
98
|
+
? { value, truncated: false }
|
|
99
|
+
: projectComparisonLedgerJson(value);
|
|
100
|
+
const before = counters.truncatedStrings;
|
|
101
|
+
if (traversal.truncated)
|
|
102
|
+
counters.truncatedStrings += 1;
|
|
103
|
+
const projected = boundedComparisonLedgerText(traversal.value, MAX_COMPARISON_LEDGER_STRING_LENGTH, counters, true);
|
|
104
|
+
return { value: projected || null, truncated: counters.truncatedStrings > before };
|
|
105
|
+
}
|
|
106
|
+
function sourceSide(raw, counters) {
|
|
107
|
+
const projected = exactValue(raw.value, counters);
|
|
108
|
+
return {
|
|
109
|
+
side: Object.freeze({
|
|
110
|
+
source: boundedComparisonLedgerText(raw.source, MAX_SOURCE_LENGTH, counters) || 'Not recorded',
|
|
111
|
+
sourceState: boundedComparisonLedgerText(raw.sourceState, 64, counters) || 'not_reported',
|
|
112
|
+
value: projected.value,
|
|
113
|
+
observedAt: timestamp(raw.observedAt),
|
|
114
|
+
publishedAt: timestamp(raw.publishedAt),
|
|
115
|
+
retainedAt: timestamp(raw.retainedAt),
|
|
116
|
+
}),
|
|
117
|
+
truncated: projected.truncated,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function isMode(value) {
|
|
121
|
+
return typeof value === 'string' && COMPARISON_LEDGER_MODES.includes(value);
|
|
122
|
+
}
|
|
123
|
+
function isState(value) {
|
|
124
|
+
return typeof value === 'string' && COMPARISON_LEDGER_STATES.includes(value);
|
|
125
|
+
}
|
|
126
|
+
export function normaliseComparisonLedgerRowWithCounters(raw, counters) {
|
|
127
|
+
const item = comparisonLedgerRecord(raw);
|
|
128
|
+
const earlierRaw = comparisonLedgerRecord(item?.earlier);
|
|
129
|
+
const laterRaw = comparisonLedgerRecord(item?.later);
|
|
130
|
+
const ownerId = boundedComparisonLedgerId(item?.ownerId, counters);
|
|
131
|
+
const entityId = boundedComparisonLedgerId(item?.entityId, counters);
|
|
132
|
+
const mode = item?.mode;
|
|
133
|
+
const state = item?.state;
|
|
134
|
+
const field = boundedComparisonLedgerText(item?.field, MAX_FIELD_LENGTH, counters);
|
|
135
|
+
const family = boundedComparisonLedgerText(item?.family, MAX_FIELD_LENGTH, counters);
|
|
136
|
+
if (!item || !earlierRaw || !laterRaw || !ownerId || !entityId || !isMode(mode) || !isState(state) || !field || !family) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const earlier = sourceSide(earlierRaw, counters);
|
|
140
|
+
const later = sourceSide(laterRaw, counters);
|
|
141
|
+
const rowLimitations = boundedComparisonLedgerLimitations(item.limitations, counters);
|
|
142
|
+
const truncated = item.truncated === true || earlier.truncated || later.truncated || rowLimitations.omitted > 0;
|
|
143
|
+
const completeness = normaliseCompleteness(item.completeness);
|
|
144
|
+
const comparisonId = boundedComparisonLedgerId(item.comparisonId, counters);
|
|
145
|
+
const id = stableComparisonLedgerId('ledger-row', [
|
|
146
|
+
comparisonId, ownerId, entityId, mode, state, family, field,
|
|
147
|
+
earlier.side.source, earlier.side.sourceState, earlier.side.value,
|
|
148
|
+
earlier.side.observedAt, earlier.side.publishedAt, earlier.side.retainedAt,
|
|
149
|
+
later.side.source, later.side.sourceState, later.side.value,
|
|
150
|
+
later.side.observedAt, later.side.publishedAt, later.side.retainedAt,
|
|
151
|
+
completeness, truncated, rowLimitations.omitted, ...rowLimitations.values,
|
|
152
|
+
]);
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
id,
|
|
155
|
+
ownerId,
|
|
156
|
+
entityId,
|
|
157
|
+
mode,
|
|
158
|
+
state,
|
|
159
|
+
field,
|
|
160
|
+
family,
|
|
161
|
+
earlier: earlier.side,
|
|
162
|
+
later: later.side,
|
|
163
|
+
completeness,
|
|
164
|
+
truncated,
|
|
165
|
+
limitations: rowLimitations.values,
|
|
166
|
+
omittedLimitations: rowLimitations.omitted,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export function normaliseComparisonLedgerRow(raw) {
|
|
170
|
+
return normaliseComparisonLedgerRowWithCounters(raw, freshComparisonLedgerCounters());
|
|
171
|
+
}
|
|
172
|
+
export function comparisonLedgerCollector() {
|
|
173
|
+
const output = {
|
|
174
|
+
rows: [],
|
|
175
|
+
totalRows: 0,
|
|
176
|
+
add(row) {
|
|
177
|
+
output.totalRows += 1;
|
|
178
|
+
if (output.rows.length < MAX_COMPARISON_LEDGER_DETAIL_ROWS)
|
|
179
|
+
output.rows.push(row);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return output;
|
|
183
|
+
}
|
|
184
|
+
export function comparisonLedgerValuePresent(value) {
|
|
185
|
+
if (value === null || value === undefined)
|
|
186
|
+
return false;
|
|
187
|
+
if (typeof value === 'string')
|
|
188
|
+
return value.trim() !== '' && value.trim() !== '—';
|
|
189
|
+
if (Array.isArray(value))
|
|
190
|
+
return value.length > 0;
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
export function comparisonLedgerValueState(before, after, removalAllowed) {
|
|
194
|
+
if (!comparisonLedgerValuePresent(before) && comparisonLedgerValuePresent(after))
|
|
195
|
+
return 'added';
|
|
196
|
+
if (comparisonLedgerValuePresent(before) && !comparisonLedgerValuePresent(after))
|
|
197
|
+
return removalAllowed ? 'removed' : 'incomplete';
|
|
198
|
+
return 'different';
|
|
199
|
+
}
|
|
200
|
+
function indexSide(raw, counters) {
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
source: boundedComparisonLedgerText(raw.source, MAX_SOURCE_LENGTH, counters) || 'Not recorded',
|
|
203
|
+
sourceState: boundedComparisonLedgerText(raw.sourceState, 64, counters) || 'not_reported',
|
|
204
|
+
observedAt: timestamp(raw.observedAt),
|
|
205
|
+
publishedAt: timestamp(raw.publishedAt),
|
|
206
|
+
retainedAt: timestamp(raw.retainedAt),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
export function makeComparisonLedgerCandidate(raw, counters) {
|
|
210
|
+
const ownerId = boundedComparisonLedgerId(raw.ownerId, counters);
|
|
211
|
+
const entityId = boundedComparisonLedgerId(raw.entityId, counters);
|
|
212
|
+
const label = boundedComparisonLedgerText(raw.label, MAX_LABEL_LENGTH, counters);
|
|
213
|
+
if (!ownerId || !entityId || !label)
|
|
214
|
+
return null;
|
|
215
|
+
const projectedLimitations = boundedComparisonLedgerLimitations(raw.limitations, counters);
|
|
216
|
+
const id = stableComparisonLedgerId(`ledger-${raw.ownerType}`, raw.idParts);
|
|
217
|
+
const item = Object.freeze({
|
|
218
|
+
id,
|
|
219
|
+
ownerType: raw.ownerType,
|
|
220
|
+
ownerId,
|
|
221
|
+
entityId,
|
|
222
|
+
label,
|
|
223
|
+
mode: raw.mode,
|
|
224
|
+
earlier: indexSide(raw.earlier, counters),
|
|
225
|
+
later: indexSide(raw.later, counters),
|
|
226
|
+
completeness: raw.completeness,
|
|
227
|
+
truncated: raw.truncated || projectedLimitations.omitted > 0,
|
|
228
|
+
limitations: projectedLimitations.values,
|
|
229
|
+
omittedLimitations: projectedLimitations.omitted,
|
|
230
|
+
sourceOmittedRows: Math.max(0, Math.trunc(Number(raw.sourceOmittedRows) || 0)),
|
|
231
|
+
ownerHref: safeComparisonLedgerHref(raw.ownerHref),
|
|
232
|
+
});
|
|
233
|
+
return Object.freeze({ item, buildDetails: raw.buildDetails });
|
|
234
|
+
}
|
|
235
|
+
export function validComparisonLedgerRequestId(value) {
|
|
236
|
+
return typeof value === 'string'
|
|
237
|
+
&& value.length > 0
|
|
238
|
+
&& value.length <= MAX_COMPARISON_LEDGER_ID_LENGTH
|
|
239
|
+
&& !CONTROL_TEST_RE.test(value)
|
|
240
|
+
&& boundedComparisonLedgerId(value) === value;
|
|
241
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const MAX_COLLECTION_SCAN = 256;
|
|
2
|
+
const MAX_DEPTH = 8;
|
|
3
|
+
const MAX_SCALAR_LENGTH = 2_000;
|
|
4
|
+
const MAX_TRAVERSAL_NODES = 1_024;
|
|
5
|
+
const MAX_SERIALISED_LENGTH = 8_192;
|
|
6
|
+
function record(value) {
|
|
7
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: null;
|
|
10
|
+
}
|
|
11
|
+
function boundedJsonString(value, state) {
|
|
12
|
+
if (value.length > MAX_SCALAR_LENGTH)
|
|
13
|
+
state.truncated = true;
|
|
14
|
+
return JSON.stringify(value.slice(0, MAX_SCALAR_LENGTH));
|
|
15
|
+
}
|
|
16
|
+
function stableJson(value, depth, state) {
|
|
17
|
+
if (depth > MAX_DEPTH) {
|
|
18
|
+
state.truncated = true;
|
|
19
|
+
return '"[bounded depth]"';
|
|
20
|
+
}
|
|
21
|
+
if (state.nodes >= MAX_TRAVERSAL_NODES) {
|
|
22
|
+
state.truncated = true;
|
|
23
|
+
return '"[bounded traversal]"';
|
|
24
|
+
}
|
|
25
|
+
state.nodes += 1;
|
|
26
|
+
if (value === null)
|
|
27
|
+
return 'null';
|
|
28
|
+
if (typeof value === 'string')
|
|
29
|
+
return boundedJsonString(value, state);
|
|
30
|
+
if (typeof value === 'number')
|
|
31
|
+
return Number.isFinite(value) ? String(value) : 'null';
|
|
32
|
+
if (typeof value === 'boolean')
|
|
33
|
+
return value ? 'true' : 'false';
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const values = [];
|
|
36
|
+
const limit = Math.min(value.length, MAX_COLLECTION_SCAN);
|
|
37
|
+
for (let index = 0; index < limit && state.nodes < MAX_TRAVERSAL_NODES; index += 1) {
|
|
38
|
+
values.push(stableJson(value[index], depth + 1, state));
|
|
39
|
+
}
|
|
40
|
+
if (values.length < value.length)
|
|
41
|
+
state.truncated = true;
|
|
42
|
+
return `[${values.join(',')}]`;
|
|
43
|
+
}
|
|
44
|
+
const item = record(value);
|
|
45
|
+
if (!item)
|
|
46
|
+
return 'null';
|
|
47
|
+
const keys = [];
|
|
48
|
+
for (const key in item) {
|
|
49
|
+
if (!Object.hasOwn(item, key))
|
|
50
|
+
continue;
|
|
51
|
+
if (keys.length >= MAX_COLLECTION_SCAN) {
|
|
52
|
+
state.truncated = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
keys.push(key);
|
|
56
|
+
}
|
|
57
|
+
keys.sort();
|
|
58
|
+
const values = [];
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
if (state.nodes >= MAX_TRAVERSAL_NODES) {
|
|
61
|
+
state.truncated = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
values.push(`${boundedJsonString(key, state)}:${stableJson(item[key], depth + 1, state)}`);
|
|
65
|
+
}
|
|
66
|
+
if (values.length < keys.length)
|
|
67
|
+
state.truncated = true;
|
|
68
|
+
return `{${values.join(',')}}`;
|
|
69
|
+
}
|
|
70
|
+
export function projectComparisonLedgerJson(value) {
|
|
71
|
+
const state = { truncated: false, nodes: 0 };
|
|
72
|
+
const serialised = stableJson(value, 0, state);
|
|
73
|
+
if (serialised.length > MAX_SERIALISED_LENGTH)
|
|
74
|
+
state.truncated = true;
|
|
75
|
+
return Object.freeze({ value: serialised.slice(0, MAX_SERIALISED_LENGTH), truncated: state.truncated });
|
|
76
|
+
}
|
|
77
|
+
export function stableComparisonLedgerJson(value) {
|
|
78
|
+
return projectComparisonLedgerJson(value).value;
|
|
79
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/gu;
|
|
2
|
+
const MAX_ID_PARTS = 32;
|
|
3
|
+
const MAX_ID_PART_LENGTH = 500;
|
|
4
|
+
function stableHash(value) {
|
|
5
|
+
let first = 0x811c9dc5;
|
|
6
|
+
let second = 0;
|
|
7
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8
|
+
const code = value.charCodeAt(index);
|
|
9
|
+
first ^= code;
|
|
10
|
+
first = Math.imul(first, 0x01000193);
|
|
11
|
+
second = (second + code) | 0;
|
|
12
|
+
second = (second + (second << 10)) | 0;
|
|
13
|
+
second ^= second >>> 6;
|
|
14
|
+
}
|
|
15
|
+
second = (second + (second << 3)) | 0;
|
|
16
|
+
second ^= second >>> 11;
|
|
17
|
+
second = (second + (second << 15)) | 0;
|
|
18
|
+
return `${(first >>> 0).toString(36).padStart(7, '0')}${(second >>> 0).toString(36).padStart(7, '0')}`;
|
|
19
|
+
}
|
|
20
|
+
export function stableComparisonLedgerId(prefix, parts) {
|
|
21
|
+
const encoded = parts.slice(0, MAX_ID_PARTS).map((part) => {
|
|
22
|
+
let value = '';
|
|
23
|
+
try {
|
|
24
|
+
value = part === null || part === undefined || ['string', 'number', 'boolean', 'bigint'].includes(typeof part)
|
|
25
|
+
? String(part ?? '')
|
|
26
|
+
: Object.prototype.toString.call(part);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
value = '[unreadable]';
|
|
30
|
+
}
|
|
31
|
+
const bounded = value.slice(0, MAX_ID_PART_LENGTH);
|
|
32
|
+
const typed = `${typeof part}:${value.length}:${bounded}`;
|
|
33
|
+
return `${typed.length}:${typed}`;
|
|
34
|
+
}).join('');
|
|
35
|
+
const material = `${parts.length}:${encoded}`;
|
|
36
|
+
return `${prefix.slice(0, 64)}-${stableHash(material)}`;
|
|
37
|
+
}
|
|
38
|
+
export function safeComparisonLedgerHref(value) {
|
|
39
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
40
|
+
return '';
|
|
41
|
+
const href = String(value).replace(CONTROL_RE, ' ').replace(/\s+/gu, ' ').trim().slice(0, 500).trim();
|
|
42
|
+
if (!href.startsWith('/') || href.startsWith('//') || href.includes('\\'))
|
|
43
|
+
return '';
|
|
44
|
+
try {
|
|
45
|
+
const base = new URL('https://comparison-ledger.invalid/');
|
|
46
|
+
return new URL(href, base).origin === base.origin ? href : '';
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, SORTED_JSON_V1, SORTED_JSON_V2, } from "./artifact-integrity.js";
|
|
2
|
+
import { normalizeDomain } from "./case-record-core.js";
|
|
3
|
+
import { canonicalDomainControlRecordList } from "./domain-control-records.js";
|
|
4
|
+
export const DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA = 'whoisleuth.domain-control-manifest-input';
|
|
5
|
+
export const DOMAIN_CONTROL_PASSPORT_SCHEMA = 'whoisleuth.domain-control-manifest';
|
|
6
|
+
export const DOMAIN_CONTROL_PASSPORT_VERSION = 1;
|
|
7
|
+
export const DOMAIN_CONTROL_MANIFEST_VERSION = 2;
|
|
8
|
+
export const MAX_DOMAIN_CONTROL_PASSPORT_ENTRIES = 100;
|
|
9
|
+
export const DOMAIN_CONTROL_PASSPORT_LIMITATIONS = Object.freeze([
|
|
10
|
+
'This analyst-authored manifest records intended domain-control state. It does not collect evidence or change registrar, DNS, mail, or certificate configuration.',
|
|
11
|
+
'Empty desired fields are unconfigured rather than claims that a record should be absent.',
|
|
12
|
+
]);
|
|
13
|
+
const INPUT_KEYS = new Set(['schema', 'version', 'expiresAt', 'entries']);
|
|
14
|
+
const MANIFEST_KEYS = new Set(['schema', 'version', 'generatedAt', 'expiresAt', 'entries', 'limitations', 'integrity']);
|
|
15
|
+
const ENTRY_KEYS = new Set(['domain', 'nameservers', 'ds', 'mx', 'caa', 'tlsIssuer', 'tlsSpkiSha256', 'registrarLock', 'renewalReviewAt', 'note']);
|
|
16
|
+
const INTEGRITY_KEYS = new Set(['algorithm', 'canonicalization', 'digestSha256']);
|
|
17
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
18
|
+
function record(value) {
|
|
19
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
20
|
+
? value
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
function exactKeys(value, allowed, label) {
|
|
24
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
25
|
+
if (unknown)
|
|
26
|
+
throw new TypeError(`${label} contains unknown field: ${unknown}.`);
|
|
27
|
+
}
|
|
28
|
+
function text(value, maximum = 300) {
|
|
29
|
+
if (typeof value !== 'string' || value.length > maximum * 4 || CONTROL_RE.test(value))
|
|
30
|
+
return null;
|
|
31
|
+
return value.replace(/\s+/gu, ' ').trim().slice(0, maximum) || null;
|
|
32
|
+
}
|
|
33
|
+
function timestamp(value) {
|
|
34
|
+
const candidate = text(value, 64);
|
|
35
|
+
if (!candidate)
|
|
36
|
+
return null;
|
|
37
|
+
const parsed = Date.parse(candidate);
|
|
38
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
39
|
+
}
|
|
40
|
+
function hostnames(value) {
|
|
41
|
+
if (!Array.isArray(value))
|
|
42
|
+
return [];
|
|
43
|
+
return [...new Set(value.slice(0, 128).flatMap((item) => {
|
|
44
|
+
const normalized = normalizeDomain(item);
|
|
45
|
+
return normalized ? [normalized] : [];
|
|
46
|
+
}))].sort().slice(0, 32);
|
|
47
|
+
}
|
|
48
|
+
function digest(value) {
|
|
49
|
+
const candidate = text(value, 64)?.toLowerCase() ?? '';
|
|
50
|
+
return /^[a-f0-9]{64}$/u.test(candidate) ? candidate : null;
|
|
51
|
+
}
|
|
52
|
+
function normalizeEntry(value) {
|
|
53
|
+
const source = record(value);
|
|
54
|
+
if (!source)
|
|
55
|
+
return null;
|
|
56
|
+
exactKeys(source, ENTRY_KEYS, 'Domain control manifest entry');
|
|
57
|
+
const domain = normalizeDomain(source.domain);
|
|
58
|
+
if (!domain)
|
|
59
|
+
return null;
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
domain,
|
|
62
|
+
nameservers: Object.freeze(hostnames(source.nameservers)),
|
|
63
|
+
ds: Object.freeze(canonicalDomainControlRecordList(source.ds, 'ds')),
|
|
64
|
+
mx: Object.freeze(canonicalDomainControlRecordList(source.mx, 'mx')),
|
|
65
|
+
caa: Object.freeze(canonicalDomainControlRecordList(source.caa, 'caa')),
|
|
66
|
+
tlsIssuer: text(source.tlsIssuer, 300)?.toLowerCase() ?? null,
|
|
67
|
+
tlsSpkiSha256: digest(source.tlsSpkiSha256),
|
|
68
|
+
registrarLock: source.registrarLock === 'required' || source.registrarLock === 'not_required'
|
|
69
|
+
? source.registrarLock
|
|
70
|
+
: null,
|
|
71
|
+
renewalReviewAt: timestamp(source.renewalReviewAt),
|
|
72
|
+
note: text(source.note, 500),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function buildUnsignedDomainControlPassport(input, generatedAtValue) {
|
|
76
|
+
const source = record(input);
|
|
77
|
+
if (!source || source.schema !== DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA || source.version !== DOMAIN_CONTROL_PASSPORT_VERSION) {
|
|
78
|
+
throw new TypeError(`Domain control manifest input must use ${DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA} version ${DOMAIN_CONTROL_PASSPORT_VERSION}.`);
|
|
79
|
+
}
|
|
80
|
+
exactKeys(source, INPUT_KEYS, 'Domain control manifest input');
|
|
81
|
+
const generatedAt = timestamp(generatedAtValue);
|
|
82
|
+
const expiresAt = timestamp(source.expiresAt);
|
|
83
|
+
if (!generatedAt || !expiresAt || Date.parse(expiresAt) <= Date.parse(generatedAt)) {
|
|
84
|
+
throw new TypeError('Domain control manifest expiry must be a valid time after generation.');
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(source.entries) || source.entries.length < 1 || source.entries.length > MAX_DOMAIN_CONTROL_PASSPORT_ENTRIES) {
|
|
87
|
+
throw new TypeError(`Domain control manifest input must contain between 1 and ${MAX_DOMAIN_CONTROL_PASSPORT_ENTRIES} entries.`);
|
|
88
|
+
}
|
|
89
|
+
const entries = source.entries.map(normalizeEntry);
|
|
90
|
+
if (entries.some((entry) => entry === null))
|
|
91
|
+
throw new TypeError('Domain control manifest contains an invalid entry.');
|
|
92
|
+
const normalizedEntries = entries;
|
|
93
|
+
if (new Set(normalizedEntries.map((entry) => entry.domain)).size !== normalizedEntries.length) {
|
|
94
|
+
throw new TypeError('Domain control manifest entries must use unique domains.');
|
|
95
|
+
}
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
schema: DOMAIN_CONTROL_PASSPORT_SCHEMA,
|
|
98
|
+
version: DOMAIN_CONTROL_MANIFEST_VERSION,
|
|
99
|
+
generatedAt,
|
|
100
|
+
expiresAt,
|
|
101
|
+
entries: Object.freeze([...normalizedEntries].sort((left, right) => left.domain.localeCompare(right.domain))),
|
|
102
|
+
limitations: DOMAIN_CONTROL_PASSPORT_LIMITATIONS,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function normalizeDomainControlPassportDocument(value) {
|
|
106
|
+
const source = record(value);
|
|
107
|
+
const integrity = record(source?.integrity);
|
|
108
|
+
if (!source
|
|
109
|
+
|| source.schema !== DOMAIN_CONTROL_PASSPORT_SCHEMA
|
|
110
|
+
|| !integrity
|
|
111
|
+
|| integrity.algorithm !== 'SHA-256'
|
|
112
|
+
|| typeof integrity.digestSha256 !== 'string'
|
|
113
|
+
|| !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)) {
|
|
114
|
+
throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
|
|
115
|
+
}
|
|
116
|
+
exactKeys(source, MANIFEST_KEYS, 'Domain control manifest');
|
|
117
|
+
exactKeys(integrity, INTEGRITY_KEYS, 'Domain control manifest integrity');
|
|
118
|
+
const canonicalization = resolveArtifactCanonicalization(source.version, integrity.canonicalization, [
|
|
119
|
+
{ version: DOMAIN_CONTROL_PASSPORT_VERSION, canonicalization: SORTED_JSON_V1, explicit: true },
|
|
120
|
+
{ version: DOMAIN_CONTROL_MANIFEST_VERSION, canonicalization: SORTED_JSON_V2, explicit: true },
|
|
121
|
+
], 'Domain control manifest');
|
|
122
|
+
if (!Array.isArray(source.limitations)
|
|
123
|
+
|| source.limitations.length !== DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length
|
|
124
|
+
|| source.limitations.some((item, index) => item !== DOMAIN_CONTROL_PASSPORT_LIMITATIONS[index])) {
|
|
125
|
+
throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
|
|
126
|
+
}
|
|
127
|
+
const baseUnsigned = buildUnsignedDomainControlPassport({
|
|
128
|
+
schema: DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA,
|
|
129
|
+
version: DOMAIN_CONTROL_PASSPORT_VERSION,
|
|
130
|
+
expiresAt: source.expiresAt,
|
|
131
|
+
entries: source.entries,
|
|
132
|
+
}, source.generatedAt);
|
|
133
|
+
const unsigned = Object.freeze({ ...baseUnsigned, version: source.version });
|
|
134
|
+
const { integrity: _integrity, ...suppliedUnsigned } = source;
|
|
135
|
+
const canonicalUnsigned = canonicalArtifactJsonFor(unsigned, canonicalization);
|
|
136
|
+
if (canonicalArtifactJsonFor(suppliedUnsigned, canonicalization) !== canonicalUnsigned) {
|
|
137
|
+
throw new TypeError('Domain control manifest must use its canonical normalised content.');
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
manifest: source,
|
|
141
|
+
unsigned,
|
|
142
|
+
canonicalUnsigned,
|
|
143
|
+
canonicalization,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { normalizeDomain } from "./case-record-core.js";
|
|
2
|
+
export const MAX_CANONICAL_DOMAIN_CONTROL_RECORDS = 32;
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: {};
|
|
7
|
+
}
|
|
8
|
+
function text(value, maximum = 500) {
|
|
9
|
+
if (typeof value !== 'string' || value.length > maximum * 4 || /[\u0000-\u001f\u007f]/u.test(value))
|
|
10
|
+
return '';
|
|
11
|
+
return value.replace(/\s+/gu, ' ').trim().slice(0, maximum).trim();
|
|
12
|
+
}
|
|
13
|
+
function integer(value, maximum) {
|
|
14
|
+
const candidate = typeof value === 'number' ? value : typeof value === 'string' && /^\d+$/u.test(value.trim()) ? Number(value) : NaN;
|
|
15
|
+
return Number.isSafeInteger(candidate) && candidate >= 0 && candidate <= maximum ? candidate : null;
|
|
16
|
+
}
|
|
17
|
+
function exchange(value) {
|
|
18
|
+
const candidate = text(value, 253).toLowerCase();
|
|
19
|
+
if (candidate === '.')
|
|
20
|
+
return '.';
|
|
21
|
+
return normalizeDomain(candidate) ?? '';
|
|
22
|
+
}
|
|
23
|
+
export function canonicalMxRecord(value) {
|
|
24
|
+
const item = record(value);
|
|
25
|
+
if (Object.keys(item).length) {
|
|
26
|
+
const host = exchange(item.exchange ?? item.host ?? item.value);
|
|
27
|
+
const priority = integer(item.priority ?? item.preference, 65_535);
|
|
28
|
+
return host && priority !== null ? `${priority} ${host}` : '';
|
|
29
|
+
}
|
|
30
|
+
const candidate = text(value, 500).toLowerCase();
|
|
31
|
+
const match = /^(\d{1,5})\s+(.+)$/u.exec(candidate);
|
|
32
|
+
if (match) {
|
|
33
|
+
const priority = integer(match[1], 65_535);
|
|
34
|
+
const host = exchange(match[2]);
|
|
35
|
+
return priority !== null && host ? `${priority} ${host}` : '';
|
|
36
|
+
}
|
|
37
|
+
return exchange(candidate);
|
|
38
|
+
}
|
|
39
|
+
function caaValue(value) {
|
|
40
|
+
return text(value, 500).replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, '$1$2').trim().toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
export function canonicalCaaRecord(value) {
|
|
43
|
+
const item = record(value);
|
|
44
|
+
if (Object.keys(item).length) {
|
|
45
|
+
const flags = integer(item.critical ?? item.flags ?? 0, 255);
|
|
46
|
+
const tag = text(item.tag, 32).toLowerCase();
|
|
47
|
+
const payload = caaValue(item.value);
|
|
48
|
+
return flags !== null && /^(?:issue|issuewild|iodef)$/u.test(tag) && payload
|
|
49
|
+
? `${flags} ${tag} ${payload}`
|
|
50
|
+
: '';
|
|
51
|
+
}
|
|
52
|
+
const candidate = text(value, 600);
|
|
53
|
+
const match = /^(\d{1,3})\s+(issue|issuewild|iodef)\s+(.+)$/iu.exec(candidate);
|
|
54
|
+
if (!match)
|
|
55
|
+
return '';
|
|
56
|
+
const flags = integer(match[1], 255);
|
|
57
|
+
const tag = match[2]?.toLowerCase() ?? '';
|
|
58
|
+
const payload = caaValue(match[3]);
|
|
59
|
+
return flags !== null && payload ? `${flags} ${tag} ${payload}` : '';
|
|
60
|
+
}
|
|
61
|
+
export function canonicalDsRecord(value) {
|
|
62
|
+
const item = record(value);
|
|
63
|
+
if (Object.keys(item).length) {
|
|
64
|
+
const keyTag = integer(item.keyTag ?? item.key_tag, 65_535);
|
|
65
|
+
const algorithm = integer(item.algorithm, 255);
|
|
66
|
+
const digestType = integer(item.digestType ?? item.digest_type, 255);
|
|
67
|
+
const digest = text(item.digest, 1024).replace(/\s+/gu, '').toLowerCase();
|
|
68
|
+
return keyTag !== null && algorithm !== null && digestType !== null && /^[a-f0-9]{2,1024}$/u.test(digest)
|
|
69
|
+
? `${keyTag} ${algorithm} ${digestType} ${digest}`
|
|
70
|
+
: '';
|
|
71
|
+
}
|
|
72
|
+
const candidate = text(value, 1200);
|
|
73
|
+
const match = /^(\d{1,5})\s+(\d{1,3})\s+(\d{1,3})\s+([a-f0-9\s]+)$/iu.exec(candidate);
|
|
74
|
+
if (!match)
|
|
75
|
+
return '';
|
|
76
|
+
return canonicalDsRecord({ keyTag: match[1], algorithm: match[2], digestType: match[3], digest: match[4] });
|
|
77
|
+
}
|
|
78
|
+
export function canonicalDomainControlRecordList(value, kind) {
|
|
79
|
+
if (!Array.isArray(value))
|
|
80
|
+
return [];
|
|
81
|
+
const normalizer = kind === 'mx' ? canonicalMxRecord : kind === 'caa' ? canonicalCaaRecord : canonicalDsRecord;
|
|
82
|
+
return [...new Set(value
|
|
83
|
+
.slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS * 4)
|
|
84
|
+
.map(normalizer)
|
|
85
|
+
.filter(Boolean))]
|
|
86
|
+
.sort()
|
|
87
|
+
.slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
|
|
88
|
+
}
|