@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
package/lib/ct-search.mjs
CHANGED
|
@@ -10,8 +10,9 @@ import { parse } from 'tldts';
|
|
|
10
10
|
import { normalizeCtQuery } from "./ct-query.mjs";
|
|
11
11
|
import { safeFetch, readTextCapped } from "./safe-fetch.mjs";
|
|
12
12
|
import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
13
|
-
import { createObservation } from "
|
|
13
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
14
14
|
import { isValidAsciiHostname } from "./hostname.mjs";
|
|
15
|
+
import { normalizeCtTimestamp } from "../packages/evidence/observation.mjs";
|
|
15
16
|
import { MAX_CT_RESPONSE_CERTIFICATE_GROUPS, MAX_CT_RESPONSE_DOMAINS_PER_GROUP, MAX_CT_RESPONSE_HOSTNAMES_PER_GROUP, MAX_CT_RESPONSE_HOSTNAMES_PER_MATCH, MAX_CT_RESPONSE_RESULTS, MAX_CT_RESPONSE_TIMESTAMP_LENGTH, } from "./ct-response-bounds.mjs";
|
|
16
17
|
const CRT_SH_TIMEOUT_MS = 20000;
|
|
17
18
|
const MAX_RESULTS = MAX_CT_RESPONSE_RESULTS;
|
|
@@ -107,7 +108,7 @@ async function fetchCrtSh(keyword, attempt = 0, dependencies = {}) {
|
|
|
107
108
|
? `crt.sh is temporarily overloaded (${res.status} after ${attempt + 1} attempts) - it's a free public service that intermittently struggles under load; try again in a moment.`
|
|
108
109
|
: `crt.sh returned ${res.status}`);
|
|
109
110
|
}
|
|
110
|
-
const { text, truncated } = await readTextCapped(res, CRT_SH_MAX_BYTES);
|
|
111
|
+
const { text, truncated } = await readTextCapped(res, CRT_SH_MAX_BYTES, { fatalUtf8: true });
|
|
111
112
|
if (truncated) {
|
|
112
113
|
throw new Error(`crt.sh returned more than ${CRT_SH_MAX_BYTES / (1024 * 1024)}MB of results for "${keyword}" - try a narrower/more specific keyword.`);
|
|
113
114
|
}
|
|
@@ -155,6 +156,16 @@ function resolveCertId(row, index) {
|
|
|
155
156
|
}
|
|
156
157
|
return `row:${index}`;
|
|
157
158
|
}
|
|
159
|
+
function suppliedCertificateIdentityMalformed(row) {
|
|
160
|
+
const hasId = Object.hasOwn(row, 'id') && row.id !== null && row.id !== undefined;
|
|
161
|
+
if (hasId)
|
|
162
|
+
return canonicalCertId(row.id) === null;
|
|
163
|
+
const hasIssuer = Object.hasOwn(row, 'issuer_ca_id') && row.issuer_ca_id !== null && row.issuer_ca_id !== undefined;
|
|
164
|
+
const hasSerial = Object.hasOwn(row, 'serial_number') && row.serial_number !== null && row.serial_number !== undefined;
|
|
165
|
+
return hasIssuer || hasSerial
|
|
166
|
+
? canonicalCertId(row.issuer_ca_id) === null || normalizeSerial(row.serial_number) === null
|
|
167
|
+
: false;
|
|
168
|
+
}
|
|
158
169
|
/**
|
|
159
170
|
* Canonicalises a crt.sh certificate ID field for use in identity comparison.
|
|
160
171
|
* Accepts only safe positive integers or digit-only strings that can be
|
|
@@ -230,20 +241,9 @@ function normalizeSerial(value) {
|
|
|
230
241
|
* @returns {string | null}
|
|
231
242
|
*/
|
|
232
243
|
function validateEntryTimestamp(value) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return null;
|
|
237
|
-
// Reject control characters and overlong strings before any parsing.
|
|
238
|
-
if (/[\x00-\x1f\x7f]/.test(value))
|
|
239
|
-
return null;
|
|
240
|
-
const trimmed = value.trim();
|
|
241
|
-
if (trimmed.length === 0)
|
|
242
|
-
return null;
|
|
243
|
-
const parsed = new Date(trimmed);
|
|
244
|
-
if (Number.isNaN(parsed.getTime()))
|
|
245
|
-
return null;
|
|
246
|
-
return parsed.toISOString();
|
|
244
|
+
return typeof value === 'string' && value.length <= MAX_TIMESTAMP_LENGTH
|
|
245
|
+
? normalizeCtTimestamp(value)
|
|
246
|
+
: null;
|
|
247
247
|
}
|
|
248
248
|
// ---------------------------------------------------------------------------
|
|
249
249
|
// Pure summarizer
|
|
@@ -288,10 +288,24 @@ function summarizeCtResults(rows) {
|
|
|
288
288
|
const certificateMap = new Map();
|
|
289
289
|
let namesExamined = 0;
|
|
290
290
|
let workTruncated = false;
|
|
291
|
+
let rejectedRows = 0;
|
|
291
292
|
rowLoop: for (let i = 0; i < rows.length; i++) {
|
|
292
293
|
const row = rows[i];
|
|
293
|
-
if (!row || typeof row !== 'object')
|
|
294
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
295
|
+
rejectedRows += 1;
|
|
294
296
|
continue;
|
|
297
|
+
}
|
|
298
|
+
const missingNameFields = ![row.name_value, row.common_name]
|
|
299
|
+
.some((value) => typeof value === 'string'
|
|
300
|
+
&& /[^\p{White_Space}\p{Default_Ignorable_Code_Point}\u0000-\u001f\u007f-\u009f]/u.test(value));
|
|
301
|
+
const malformedNameField = ['name_value', 'common_name'].some((field) => (Object.hasOwn(row, field) && typeof row[field] !== 'string'));
|
|
302
|
+
const malformedTimestamp = Object.hasOwn(row, 'entry_timestamp')
|
|
303
|
+
&& row.entry_timestamp !== null
|
|
304
|
+
&& row.entry_timestamp !== undefined
|
|
305
|
+
&& validateEntryTimestamp(row.entry_timestamp) === null;
|
|
306
|
+
if (missingNameFields || malformedNameField || malformedTimestamp || suppliedCertificateIdentityMalformed(row)) {
|
|
307
|
+
rejectedRows += 1;
|
|
308
|
+
}
|
|
295
309
|
const certId = resolveCertId(row, i);
|
|
296
310
|
// Collect only direct string fields. Implicitly stringifying a nested
|
|
297
311
|
// array or object would traverse attacker-controlled structure before any
|
|
@@ -442,7 +456,8 @@ function summarizeCtResults(rows) {
|
|
|
442
456
|
certificateGroupsTruncated,
|
|
443
457
|
namesExamined,
|
|
444
458
|
workTruncated,
|
|
445
|
-
|
|
459
|
+
rejectedRows,
|
|
460
|
+
truncated: rejectedRows > 0 || workTruncated || legacyTruncated || matchTruncated || perMatchTruncated,
|
|
446
461
|
};
|
|
447
462
|
}
|
|
448
463
|
// ---------------------------------------------------------------------------
|
|
@@ -454,12 +469,12 @@ async function searchCertificateTransparency(keyword, dependencies = {}) {
|
|
|
454
469
|
if (!trimmed) {
|
|
455
470
|
return {
|
|
456
471
|
domains: [], certCount: 0, truncated: false, matches: [], certificateGroups: [], certificateGroupsTruncated: false,
|
|
457
|
-
namesExamined: 0, workTruncated: false,
|
|
472
|
+
namesExamined: 0, workTruncated: false, rejectedRows: 0,
|
|
458
473
|
observation: createObservation({
|
|
459
474
|
status: 'success', observedAt: new Date().toISOString(), source: 'certificate_transparency',
|
|
460
475
|
durationMs: Date.now() - startedAt, complete: true, truncated: false,
|
|
461
476
|
limitations: ['Certificate Transparency observations indicate public certificate logging, not current site activity or maliciousness.'],
|
|
462
|
-
diagnostics: { certificateRows: 0, namesExamined: 0, workTruncated: false, matches: 0, certificateGroups: 0 },
|
|
477
|
+
diagnostics: { certificateRows: 0, rejectedRows: 0, namesExamined: 0, workTruncated: false, matches: 0, certificateGroups: 0 },
|
|
463
478
|
}),
|
|
464
479
|
};
|
|
465
480
|
}
|
|
@@ -477,10 +492,11 @@ async function searchCertificateTransparency(keyword, dependencies = {}) {
|
|
|
477
492
|
truncated: summary.truncated || summary.certificateGroupsTruncated,
|
|
478
493
|
limitations: [
|
|
479
494
|
'Certificate Transparency observations indicate public certificate logging, not current site activity or maliciousness.',
|
|
495
|
+
...(summary.rejectedRows ? [`${summary.rejectedRows} certificate row${summary.rejectedRows === 1 ? '' : 's'} contained rejected content; unusable rows or malformed fields were excluded, so retained results are partial.`] : []),
|
|
480
496
|
...(summary.workTruncated ? ['The response contained more hostname entries than the bounded local analysis could examine; retained results are partial.'] : []),
|
|
481
497
|
...(summary.certificateGroupsTruncated ? ['The optional certificate-group projection was capped independently of the registrable-domain result set.'] : []),
|
|
482
498
|
],
|
|
483
|
-
diagnostics: { certificateRows: data.length, namesExamined: summary.namesExamined, workTruncated: summary.workTruncated, matches: summary.matches.length, certificateGroups: summary.certificateGroups.length, certificateGroupsTruncated: summary.certificateGroupsTruncated },
|
|
499
|
+
diagnostics: { certificateRows: data.length, rejectedRows: summary.rejectedRows, namesExamined: summary.namesExamined, workTruncated: summary.workTruncated, matches: summary.matches.length, certificateGroups: summary.certificateGroups.length, certificateGroupsTruncated: summary.certificateGroupsTruncated },
|
|
484
500
|
}),
|
|
485
501
|
};
|
|
486
502
|
}
|
|
@@ -8,6 +8,8 @@ import crypto from 'node:crypto';
|
|
|
8
8
|
import { safeFetch, readTextCapped } from "./safe-fetch.mjs";
|
|
9
9
|
const DEFAULT_NAMESPACE = 'whoisleuth:operation-budget:v1';
|
|
10
10
|
const DEFAULT_LEASE_TTL_MS = 5 * 60 * 1000;
|
|
11
|
+
const MIN_LEASE_TTL_MS = 30_000;
|
|
12
|
+
const MAX_LEASE_TTL_MS = 15 * 60 * 1000;
|
|
11
13
|
const REQUEST_TIMEOUT_MS = 4_000;
|
|
12
14
|
const MAX_RESPONSE_BYTES = 16 * 1024;
|
|
13
15
|
const PROVIDER_RETRY_AFTER_SECONDS = 5;
|
|
@@ -150,7 +152,7 @@ function normalizedNamespace(value = DEFAULT_NAMESPACE) {
|
|
|
150
152
|
}
|
|
151
153
|
function normalizedLeaseTtl(value = DEFAULT_LEASE_TTL_MS) {
|
|
152
154
|
const ttl = Number(value);
|
|
153
|
-
if (!Number.isSafeInteger(ttl) || ttl <
|
|
155
|
+
if (!Number.isSafeInteger(ttl) || ttl < MIN_LEASE_TTL_MS || ttl > MAX_LEASE_TTL_MS) {
|
|
154
156
|
throw new Error('The distributed-budget lease TTL must be between 30 seconds and 15 minutes');
|
|
155
157
|
}
|
|
156
158
|
return ttl;
|
|
@@ -458,4 +460,4 @@ function createDistributedOperationBudget(config, dependencies = {}) {
|
|
|
458
460
|
status,
|
|
459
461
|
};
|
|
460
462
|
}
|
|
461
|
-
export { DEFAULT_NAMESPACE, DEFAULT_LEASE_TTL_MS, PROVIDER_RETRY_AFTER_SECONDS, DAY_WINDOW_MS, THIRTY_DAY_WINDOW_MS, ACQUIRE_SCRIPT, ACQUIRE_WITH_USAGE_SCRIPT, RELEASE_SCRIPT, STATUS_SCRIPT, normalizedRestUrl, normalizedToken, normalizedNamespace, normalizedLimits, normalizedUsageLimits, normalizedOperationFeature, createRestCommandClient, createDistributedOperationBudget, };
|
|
463
|
+
export { DEFAULT_NAMESPACE, DEFAULT_LEASE_TTL_MS, MIN_LEASE_TTL_MS, MAX_LEASE_TTL_MS, REQUEST_TIMEOUT_MS, MAX_RESPONSE_BYTES, PROVIDER_RETRY_AFTER_SECONDS, MAX_USAGE_LIMIT, DAY_WINDOW_MS, THIRTY_DAY_WINDOW_MS, ACQUIRE_SCRIPT, ACQUIRE_WITH_USAGE_SCRIPT, RELEASE_SCRIPT, STATUS_SCRIPT, normalizedRestUrl, normalizedToken, normalizedNamespace, normalizedLimits, normalizedUsageLimits, normalizedOperationFeature, createRestCommandClient, createDistributedOperationBudget, };
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// availability and never treats a failed query as record absence.
|
|
5
5
|
import { promises as dns } from 'node:dns';
|
|
6
6
|
import * as net from 'node:net';
|
|
7
|
-
import { createObservation } from "
|
|
7
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
8
|
+
import { isValidAsciiHostname } from "./hostname.mjs";
|
|
8
9
|
import { isPrivateAddress } from "./safe-fetch.mjs";
|
|
9
10
|
const DNS_DELEGATION_HEALTH_VERSION = 1;
|
|
10
11
|
const MAX_AUTHORITIES = 4;
|
|
@@ -26,10 +27,8 @@ function boundedText(value, maximum) {
|
|
|
26
27
|
return value.replace(/\s+/gu, ' ').trim() || null;
|
|
27
28
|
}
|
|
28
29
|
function hostname(value) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
return null;
|
|
32
|
-
return normalized.split('.').every((label) => (/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/iu.test(label))) ? normalized : null;
|
|
30
|
+
const candidate = boundedText(value, 253)?.replace(/\.+$/u, '') ?? '';
|
|
31
|
+
return isValidAsciiHostname(candidate) ? candidate.toLowerCase() : null;
|
|
33
32
|
}
|
|
34
33
|
function hostnames(value, maximum = MAX_NAMESERVERS) {
|
|
35
34
|
const output = new Set();
|
|
@@ -529,7 +528,8 @@ async function collectDnsDelegationHealth(domainValue, parentQueryValue, options
|
|
|
529
528
|
? 'Fewer than two complete authority observations were available for one or more record types'
|
|
530
529
|
: 'Observed A, AAAA, CAA, and MX answers agree across selected authorities', recordMatrix.map((row) => `${row.type}: ${row.state}`).join(' · '), 'Review propagation timing and the intended zone values before relying on a DNS change.')] : []),
|
|
531
530
|
];
|
|
532
|
-
const collectionIncomplete = parentStatus
|
|
531
|
+
const collectionIncomplete = parentStatus !== 'success'
|
|
532
|
+
|| candidates.length === 0
|
|
533
533
|
|| parentQuery.truncated === true
|
|
534
534
|
|| registry.truncated
|
|
535
535
|
|| authorities.some((authority) => authority.state !== 'success')
|
|
@@ -557,6 +557,9 @@ async function collectDnsDelegationHealth(domainValue, parentQueryValue, options
|
|
|
557
557
|
'A direct answer does not prove global reachability, and a failed query is not evidence that the record is absent.',
|
|
558
558
|
'DNS health does not decide registration availability, ownership, control, intent, safety, or maliciousness.',
|
|
559
559
|
'The DNSSEC check compares bounded registry publication fields; it does not validate the full cryptographic chain.',
|
|
560
|
+
...(!candidates.length
|
|
561
|
+
? ['No eligible authority was identified, so the delegation-health observation is incomplete.']
|
|
562
|
+
: []),
|
|
560
563
|
],
|
|
561
564
|
diagnostics: {
|
|
562
565
|
authorityCount: authorities.length,
|
package/lib/dns-intelligence.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { promises as dns } from 'node:dns';
|
|
|
6
6
|
import * as net from 'node:net';
|
|
7
7
|
import { classifyMxRecords } from "./dns-mx.mjs";
|
|
8
8
|
import { collectDnsDelegationHealth, skippedDnsDelegationHealth, } from "./dns-delegation-health.mjs";
|
|
9
|
-
import { createObservation } from "
|
|
9
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
10
10
|
import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, } from "./lookup-network-evidence-bounds.mjs";
|
|
11
11
|
import { isPrivateAddress } from "./safe-fetch.mjs";
|
|
12
12
|
import { resolveServiceBindingRecords } from "./service-binding-dns.mjs";
|
|
@@ -10,6 +10,7 @@ import { randomInt } from 'node:crypto';
|
|
|
10
10
|
import { isIP } from 'node:net';
|
|
11
11
|
import { domainToASCII } from 'node:url';
|
|
12
12
|
import { isPrivateAddress } from "./safe-fetch.mjs";
|
|
13
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
13
14
|
import { MAX_DNS_MESSAGE_BYTES, defaultTcpExchange, formatIpv6, } from "./service-binding-dns.mjs";
|
|
14
15
|
const DNSSEC_CHAIN_SCHEMA = 'whoisleuth.dnssec-chain-validation';
|
|
15
16
|
const DNSSEC_CHAIN_VERSION = 1;
|
|
@@ -95,9 +96,7 @@ function normalizedText(value, maximum) {
|
|
|
95
96
|
}
|
|
96
97
|
function normalizedTimestamp(value) {
|
|
97
98
|
const text = normalizedText(value, 64);
|
|
98
|
-
|
|
99
|
-
return null;
|
|
100
|
-
return new Date(text).toISOString();
|
|
99
|
+
return text ? normalizeExplicitIsoTimestamp(text) : null;
|
|
101
100
|
}
|
|
102
101
|
function normalizeTrustAnchorDs(value) {
|
|
103
102
|
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { domainToASCII } from 'node:url';
|
|
4
|
+
import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
4
5
|
const DNSSEC_EVIDENCE_SCHEMA = 'whoisleuth.dnssec-evidence-validation';
|
|
5
6
|
const DNSSEC_EVIDENCE_VERSION = 1;
|
|
6
7
|
const MAX_DNSSEC_RECORDS = 50;
|
|
@@ -99,20 +100,25 @@ function dsDigest(owner, dnskey, digestType) {
|
|
|
99
100
|
}
|
|
100
101
|
function normalizeRrsig(value) {
|
|
101
102
|
const source = record(value);
|
|
102
|
-
if (!source
|
|
103
|
+
if (!source)
|
|
104
|
+
return null;
|
|
105
|
+
const normalizedInception = normalizeLegacyIsoTimestamp(source.inception);
|
|
106
|
+
const normalizedExpiration = normalizeLegacyIsoTimestamp(source.expiration);
|
|
107
|
+
if (!normalizedInception || !normalizedExpiration)
|
|
103
108
|
return null;
|
|
104
|
-
const inception = Date.parse(
|
|
105
|
-
const expiration = Date.parse(
|
|
106
|
-
if (
|
|
109
|
+
const inception = Date.parse(normalizedInception);
|
|
110
|
+
const expiration = Date.parse(normalizedExpiration);
|
|
111
|
+
if (inception > expiration)
|
|
107
112
|
return null;
|
|
108
113
|
return Object.freeze({ inception, expiration });
|
|
109
114
|
}
|
|
110
115
|
function signatureTimeState(value, observedAt) {
|
|
111
|
-
if (!value.length
|
|
116
|
+
if (!value.length)
|
|
112
117
|
return 'unavailable';
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
118
|
+
const normalizedObservedAt = normalizeLegacyIsoTimestamp(observedAt);
|
|
119
|
+
if (!normalizedObservedAt)
|
|
115
120
|
return 'unavailable';
|
|
121
|
+
const observed = Date.parse(normalizedObservedAt);
|
|
116
122
|
let withinWindow = 0;
|
|
117
123
|
for (const item of value) {
|
|
118
124
|
if (observed >= item.inception && observed <= item.expiration)
|
package/lib/domain-assurance.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { domainToASCII } from 'node:url';
|
|
2
2
|
import { exactKeys } from "./bounded-contract-normalizers.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
3
4
|
const DOMAIN_ASSURANCE_INPUT_SCHEMA = 'whoisleuth.domain-assurance.input';
|
|
4
5
|
const DOMAIN_ASSURANCE_SCHEMA = 'whoisleuth.domain-assurance';
|
|
5
6
|
const DOMAIN_ASSURANCE_VERSION = 2;
|
|
6
|
-
const DOMAIN_ASSURANCE_SUPPORTED_INPUT_VERSIONS = Object.freeze([
|
|
7
|
+
const DOMAIN_ASSURANCE_SUPPORTED_INPUT_VERSIONS = Object.freeze([DOMAIN_ASSURANCE_VERSION]);
|
|
7
8
|
const MAX_ASSURANCE_INPUT_BYTES = 2 * 1024 * 1024;
|
|
8
9
|
const ROOT_KEYS = Object.freeze({
|
|
9
10
|
'planned-change': new Set(['schema', 'version', 'kind', 'domain', 'change']),
|
|
@@ -45,9 +46,10 @@ function optionalText(value, maximum = 160) {
|
|
|
45
46
|
}
|
|
46
47
|
function timestamp(value, field) {
|
|
47
48
|
const normalized = text(value, field, 64);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const canonical = normalizeExplicitIsoTimestamp(normalized);
|
|
50
|
+
if (canonical)
|
|
51
|
+
return canonical;
|
|
52
|
+
throw new Error(`${field} must be an ISO timestamp with an explicit timezone.`);
|
|
51
53
|
}
|
|
52
54
|
function optionalTimestamp(value, field) {
|
|
53
55
|
if (value === null || value === undefined || value === '')
|
|
@@ -252,7 +254,7 @@ const RETIREMENT_CHECKS = Object.freeze([
|
|
|
252
254
|
['delegatedDnsMonitored', 'Delegated DNS remains monitored during retirement', true],
|
|
253
255
|
['reRegistrationPrevented', 'Registration continuity or defensive renewal is planned', true],
|
|
254
256
|
]);
|
|
255
|
-
function buildRetirement(input
|
|
257
|
+
function buildRetirement(input) {
|
|
256
258
|
const checksInput = record(input.checks);
|
|
257
259
|
exactKeys(checksInput, new Set(RETIREMENT_CHECKS.map(([id]) => id)), 'checks');
|
|
258
260
|
const fixedChecks = RETIREMENT_CHECKS.map(([id, checkLabel, expected]) => {
|
|
@@ -266,7 +268,7 @@ function buildRetirement(input, inputVersion) {
|
|
|
266
268
|
});
|
|
267
269
|
const fixedIds = new Set(RETIREMENT_CHECKS.map(([id]) => id));
|
|
268
270
|
const customIds = new Set();
|
|
269
|
-
const customChecks =
|
|
271
|
+
const customChecks = input.customChecks !== undefined
|
|
270
272
|
? boundedArray(input.customChecks, 'customChecks', 0, 20).map((raw, index) => {
|
|
271
273
|
const item = record(raw);
|
|
272
274
|
exactKeys(item, CUSTOM_RETIREMENT_CHECK_KEYS, `customChecks[${index}]`);
|
|
@@ -304,18 +306,16 @@ function buildRetirement(input, inputVersion) {
|
|
|
304
306
|
}
|
|
305
307
|
function buildDomainAssurance(inputRaw, generatedAt = new Date().toISOString()) {
|
|
306
308
|
const input = record(inputRaw);
|
|
307
|
-
if (input.schema !== DOMAIN_ASSURANCE_INPUT_SCHEMA
|
|
308
|
-
|
|
309
|
-
throw new Error(`Domain assurance input must use ${DOMAIN_ASSURANCE_INPUT_SCHEMA} version 1 or ${DOMAIN_ASSURANCE_VERSION}.`);
|
|
309
|
+
if (input.schema !== DOMAIN_ASSURANCE_INPUT_SCHEMA || input.version !== DOMAIN_ASSURANCE_VERSION) {
|
|
310
|
+
throw new Error(`Domain assurance input must use ${DOMAIN_ASSURANCE_INPUT_SCHEMA} version ${DOMAIN_ASSURANCE_VERSION}.`);
|
|
310
311
|
}
|
|
311
|
-
const inputVersion = input.version;
|
|
312
312
|
const kind = enumValue(input.kind, 'kind', ['planned-change', 'recovery-dependencies', 'retirement']);
|
|
313
|
-
exactKeys(input, kind === 'retirement'
|
|
313
|
+
exactKeys(input, kind === 'retirement' ? RETIREMENT_ROOT_KEYS_V2 : ROOT_KEYS[kind], 'Domain assurance input');
|
|
314
314
|
const result = kind === 'planned-change'
|
|
315
315
|
? buildPlannedChange(input)
|
|
316
316
|
: kind === 'recovery-dependencies'
|
|
317
317
|
? buildRecoveryDependencies(input)
|
|
318
|
-
: buildRetirement(input
|
|
318
|
+
: buildRetirement(input);
|
|
319
319
|
return {
|
|
320
320
|
schema: DOMAIN_ASSURANCE_SCHEMA,
|
|
321
321
|
version: DOMAIN_ASSURANCE_VERSION,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exactKeys, requireBoundedString, requireDomainName, requireIsoTimestamp, requireRecord, } from "./bounded-contract-normalizers.mjs";
|
|
2
2
|
import { DOMAIN_ASSURANCE_INPUT_SCHEMA, buildDomainAssurance, } from "./domain-assurance.mjs";
|
|
3
3
|
import { DOMAIN_CHANGE_INPUT_SCHEMA, reviewDomainChange, } from "./domain-change-review.mjs";
|
|
4
|
-
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../
|
|
4
|
+
import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../packages/evidence/artifact-integrity.mjs";
|
|
5
5
|
export const DOMAIN_CHANGE_PACKET_INPUT_SCHEMA = 'whoisleuth.domain-change-packet.input';
|
|
6
6
|
export const DOMAIN_CHANGE_PACKET_SCHEMA = 'whoisleuth.domain-change-packet';
|
|
7
7
|
export const DOMAIN_CHANGE_PACKET_INPUT_VERSION = 1;
|