@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1
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/project-metadata.mjs +1 -1
- 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 +13 -5
- 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} +71 -48
- 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,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
2
3
|
import { normalizeEncryptedDnsAdapter, planEncryptedDnsQuery, } from "../lib/encrypted-dns-contract.mjs";
|
|
3
4
|
import { validateDnssecEvidence } from "../lib/dnssec-evidence-validation.mjs";
|
|
4
5
|
import { reviewDomainChange } from "../lib/domain-change-review.mjs";
|
|
@@ -13,6 +14,7 @@ import { reviewZoneIntent } from "../lib/zone-intent-review.mjs";
|
|
|
13
14
|
import { reviewDnsConvergence } from "../lib/dns-convergence-review.mjs";
|
|
14
15
|
import { compareTrustStoreEvidence } from "../lib/trust-store-comparison.mjs";
|
|
15
16
|
import { CliUsageError } from "./errors.mjs";
|
|
17
|
+
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
16
18
|
import { LOCAL_MMDB_QUERY_SCHEMA, reviewLocalMmdb } from "./local-mmdb-review.mjs";
|
|
17
19
|
const OFFLINE_EVIDENCE_REVIEW_SCHEMA = 'whoisleuth.cli.offline-evidence-review';
|
|
18
20
|
const OFFLINE_EVIDENCE_REVIEW_VERSION = 1;
|
|
@@ -31,12 +33,14 @@ function parseInput(value) {
|
|
|
31
33
|
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > MAX_OFFLINE_EVIDENCE_INPUT_BYTES) {
|
|
32
34
|
throw new CliUsageError(`Offline evidence input is limited to ${MAX_OFFLINE_EVIDENCE_INPUT_BYTES} bytes.`);
|
|
33
35
|
}
|
|
36
|
+
const normalized = value.replace(/^\uFEFF/u, '');
|
|
34
37
|
let parsed;
|
|
35
38
|
try {
|
|
36
|
-
|
|
39
|
+
scanBoundedJson(normalized);
|
|
40
|
+
parsed = JSON.parse(normalized);
|
|
37
41
|
}
|
|
38
42
|
catch {
|
|
39
|
-
throw new CliUsageError('Offline evidence review requires one valid JSON document.');
|
|
43
|
+
throw new CliUsageError('Offline evidence review requires one valid bounded JSON document without duplicate keys.');
|
|
40
44
|
}
|
|
41
45
|
const document = record(parsed);
|
|
42
46
|
if (document.version !== 1 || typeof document.schema !== 'string') {
|
|
@@ -81,8 +85,8 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
81
85
|
kind = 'tlsa';
|
|
82
86
|
result = analyzeTlsaEvidence({
|
|
83
87
|
serviceName: input.serviceName,
|
|
84
|
-
dnssecState:
|
|
85
|
-
pkixValidationState:
|
|
88
|
+
dnssecState: 'unavailable',
|
|
89
|
+
pkixValidationState: 'unavailable',
|
|
86
90
|
records: input.records,
|
|
87
91
|
certificateDerBase64: input.certificateDerBase64,
|
|
88
92
|
spkiDerBase64: input.spkiDerBase64,
|
|
@@ -237,7 +241,14 @@ function formatOfflineEvidenceReview(document) {
|
|
|
237
241
|
const cards = Array.isArray(result.cards) ? result.cards : [];
|
|
238
242
|
for (const value of cards) {
|
|
239
243
|
const card = record(value);
|
|
240
|
-
lines.push(`${
|
|
244
|
+
lines.push(`${safeTerminalValue(card.label ?? card.family, 'Evidence')} ${safeTerminalValue(card.state, 'unavailable').replaceAll('_', ' ')} (${safeTerminalValue(card.completeness, 'unavailable')})`);
|
|
245
|
+
lines.push(` Source ${safeTerminalValue(card.source, 'not supplied')}`);
|
|
246
|
+
lines.push(` Authority ${safeTerminalValue(card.authority, 'not supplied')}`);
|
|
247
|
+
const cardLimitations = Array.isArray(card.limitations) ? card.limitations : [];
|
|
248
|
+
for (const limitation of cardLimitations) {
|
|
249
|
+
if (typeof limitation === 'string')
|
|
250
|
+
lines.push(` Limitation: ${safeTerminalValue(limitation)}`);
|
|
251
|
+
}
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
254
|
else {
|
|
@@ -258,7 +269,7 @@ function formatOfflineEvidenceReview(document) {
|
|
|
258
269
|
lines.push('', 'Review reasons:');
|
|
259
270
|
for (const reason of reasons.slice(0, 8)) {
|
|
260
271
|
if (typeof reason === 'string')
|
|
261
|
-
lines.push(` - ${reason
|
|
272
|
+
lines.push(` - ${safeTerminalValue(reason)}`);
|
|
262
273
|
}
|
|
263
274
|
if (reasons.length > 8)
|
|
264
275
|
lines.push(` - ${reasons.length - 8} more reason(s) omitted.`);
|
|
@@ -269,7 +280,7 @@ function formatOfflineEvidenceReview(document) {
|
|
|
269
280
|
const nestedLimitations = Array.isArray(result.limitations) ? result.limitations : [];
|
|
270
281
|
for (const limitation of nestedLimitations) {
|
|
271
282
|
if (typeof limitation === 'string')
|
|
272
|
-
lines.push(` - ${limitation}`);
|
|
283
|
+
lines.push(` - ${safeTerminalValue(limitation)}`);
|
|
273
284
|
}
|
|
274
285
|
return `${lines.join('\n')}\n`;
|
|
275
286
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isIP } from 'node:net';
|
|
2
|
-
import { parseExternalFindingsDocument, } from "../
|
|
2
|
+
import { parseExternalFindingsDocument, } from "../packages/interchange/external-findings-import.mjs";
|
|
3
3
|
import { requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
4
|
export const OPEN_ASSET_MODEL_BRIDGE_SCHEMA = 'whoisleuth.open-asset-model-bridge';
|
|
5
5
|
export const OPEN_ASSET_MODEL_BRIDGE_VERSION = 1;
|
package/cli/output-file.mjs
CHANGED
|
@@ -5,7 +5,14 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
5
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
6
|
export const MAX_CLI_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
7
7
|
const MAX_CLI_OUTPUT_PATH_LENGTH = 4096;
|
|
8
|
-
const pendingOutputFiles = new
|
|
8
|
+
const pendingOutputFiles = new Map();
|
|
9
|
+
const OUTPUT_FILE_OPERATIONS = Object.freeze({
|
|
10
|
+
randomUUID,
|
|
11
|
+
open,
|
|
12
|
+
link,
|
|
13
|
+
rename,
|
|
14
|
+
unlink,
|
|
15
|
+
});
|
|
9
16
|
function safeOutputPath(value) {
|
|
10
17
|
if (typeof value !== 'string' || !value || value.length > MAX_CLI_OUTPUT_PATH_LENGTH) {
|
|
11
18
|
throw new CliUsageError(`Output paths are limited to ${MAX_CLI_OUTPUT_PATH_LENGTH} characters.`);
|
|
@@ -35,18 +42,22 @@ function createBufferedOutput() {
|
|
|
35
42
|
value: () => chunks.join(''),
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
|
-
async function writePrivateFile(pathValue, content, options = {}) {
|
|
45
|
+
async function writePrivateFile(pathValue, content, options = {}, operations = OUTPUT_FILE_OPERATIONS) {
|
|
39
46
|
const target = safeOutputPath(pathValue);
|
|
40
47
|
if (Buffer.byteLength(content, 'utf8') > MAX_CLI_OUTPUT_BYTES) {
|
|
41
48
|
throw new CliUsageError(`Generated output is limited to ${MAX_CLI_OUTPUT_BYTES} bytes.`);
|
|
42
49
|
}
|
|
43
50
|
const directory = dirname(target);
|
|
44
|
-
const temporary = join(directory, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
|
|
51
|
+
const temporary = join(directory, `.${basename(target)}.${process.pid}.${operations.randomUUID()}.tmp`);
|
|
45
52
|
let temporaryCreated = false;
|
|
46
53
|
try {
|
|
47
|
-
const handle = await open(temporary, 'wx', 0o600);
|
|
54
|
+
const handle = await operations.open(temporary, 'wx', 0o600);
|
|
48
55
|
temporaryCreated = true;
|
|
49
|
-
pendingOutputFiles.
|
|
56
|
+
pendingOutputFiles.set(temporary, {
|
|
57
|
+
remove: () => operations.unlink(temporary),
|
|
58
|
+
published: false,
|
|
59
|
+
settled: false,
|
|
60
|
+
});
|
|
50
61
|
try {
|
|
51
62
|
await handle.writeFile(content, { encoding: 'utf8' });
|
|
52
63
|
await handle.sync();
|
|
@@ -55,12 +66,12 @@ async function writePrivateFile(pathValue, content, options = {}) {
|
|
|
55
66
|
await handle.close();
|
|
56
67
|
}
|
|
57
68
|
if (options.force) {
|
|
58
|
-
await rename(temporary, target);
|
|
69
|
+
await operations.rename(temporary, target);
|
|
59
70
|
temporaryCreated = false;
|
|
60
71
|
}
|
|
61
72
|
else {
|
|
62
73
|
try {
|
|
63
|
-
await link(temporary, target);
|
|
74
|
+
await operations.link(temporary, target);
|
|
64
75
|
}
|
|
65
76
|
catch (error) {
|
|
66
77
|
const code = error && typeof error === 'object' && 'code' in error ? error.code : null;
|
|
@@ -69,19 +80,64 @@ async function writePrivateFile(pathValue, content, options = {}) {
|
|
|
69
80
|
}
|
|
70
81
|
throw error;
|
|
71
82
|
}
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
const pending = pendingOutputFiles.get(temporary);
|
|
84
|
+
if (pending)
|
|
85
|
+
pending.published = true;
|
|
86
|
+
try {
|
|
87
|
+
await operations.unlink(temporary);
|
|
88
|
+
temporaryCreated = false;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// The target is already published. A second best-effort cleanup runs
|
|
92
|
+
// below, and signal cleanup retains ownership if it still cannot run.
|
|
93
|
+
}
|
|
74
94
|
}
|
|
75
95
|
return target;
|
|
76
96
|
}
|
|
77
97
|
finally {
|
|
78
|
-
if (temporaryCreated)
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
if (temporaryCreated) {
|
|
99
|
+
try {
|
|
100
|
+
await operations.unlink(temporary);
|
|
101
|
+
temporaryCreated = false;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Keep the path registered for best-effort signal cleanup.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (temporaryCreated) {
|
|
108
|
+
const pending = pendingOutputFiles.get(temporary);
|
|
109
|
+
if (pending)
|
|
110
|
+
pending.settled = true;
|
|
111
|
+
}
|
|
112
|
+
if (!temporaryCreated)
|
|
113
|
+
pendingOutputFiles.delete(temporary);
|
|
81
114
|
}
|
|
82
115
|
}
|
|
116
|
+
async function cleanupPendingOutputFiles() {
|
|
117
|
+
const entries = [...pendingOutputFiles].filter(([, pending]) => pending.settled);
|
|
118
|
+
let removed = 0;
|
|
119
|
+
for (const [path, pending] of entries) {
|
|
120
|
+
try {
|
|
121
|
+
await pending.remove();
|
|
122
|
+
pendingOutputFiles.delete(path);
|
|
123
|
+
removed += 1;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// The caller receives an explicit retained count and can disclose that
|
|
127
|
+
// the published output is intact while temporary cleanup remains due.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const retainedFiles = [...pendingOutputFiles.values()].filter((item) => item.settled);
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
attempted: entries.length,
|
|
133
|
+
removed,
|
|
134
|
+
retained: retainedFiles.length,
|
|
135
|
+
retainedPublished: retainedFiles.filter((item) => item.published).length,
|
|
136
|
+
retainedUnpublished: retainedFiles.filter((item) => !item.published).length,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
83
139
|
function cleanupPendingOutputFilesSync() {
|
|
84
|
-
for (const path of pendingOutputFiles) {
|
|
140
|
+
for (const path of pendingOutputFiles.keys()) {
|
|
85
141
|
try {
|
|
86
142
|
unlinkSync(path);
|
|
87
143
|
}
|
|
@@ -94,4 +150,4 @@ function cleanupPendingOutputFilesSync() {
|
|
|
94
150
|
}
|
|
95
151
|
}
|
|
96
152
|
}
|
|
97
|
-
export { cleanupPendingOutputFilesSync, createBufferedOutput, writePrivateFile };
|
|
153
|
+
export { cleanupPendingOutputFiles, cleanupPendingOutputFilesSync, createBufferedOutput, writePrivateFile };
|
package/cli/page-compare.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { buildBulkComparisonEvidence } from "../lib/bulk-comparison-evidence.mjs";
|
|
2
|
-
import { createPageBaseline } from "../
|
|
3
|
-
import { comparePageBaselines } from "../
|
|
2
|
+
import { createPageBaseline } from "../packages/workspace/page-baseline.mjs";
|
|
3
|
+
import { comparePageBaselines } from "../packages/comparison/page-similarity.mjs";
|
|
4
4
|
import { CliUsageError } from "./errors.mjs";
|
|
5
5
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
6
|
-
|
|
7
|
-
export
|
|
6
|
+
import { CLI_PAGE_COMPARE_SCHEMA, CLI_PAGE_COMPARE_VERSION, } from "../packages/contracts/offline-comparison.mjs";
|
|
7
|
+
export { CLI_PAGE_COMPARE_SCHEMA, CLI_PAGE_COMPARE_VERSION };
|
|
8
8
|
function record(value) {
|
|
9
9
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
10
|
}
|
|
@@ -56,6 +56,7 @@ function buildCliPageComparison(leftText, rightText, generatedAt = new Date().to
|
|
|
56
56
|
&& ['success', 'partial'].includes(right.compact.technology.state);
|
|
57
57
|
const tlsUsable = ['success', 'partial'].includes(left.compact.tls.state)
|
|
58
58
|
&& ['success', 'partial'].includes(right.compact.tls.state);
|
|
59
|
+
const tlsPartial = left.compact.tls.state === 'partial' || right.compact.tls.state === 'partial';
|
|
59
60
|
const technologyPartial = left.compact.technology.truncated || right.compact.technology.truncated
|
|
60
61
|
|| left.compact.technology.state === 'partial' || right.compact.technology.state === 'partial';
|
|
61
62
|
return {
|
|
@@ -88,12 +89,16 @@ function buildCliPageComparison(leftText, rightText, generatedAt = new Date().to
|
|
|
88
89
|
leftSourceState: left.compact.tls.state,
|
|
89
90
|
rightSourceState: right.compact.tls.state,
|
|
90
91
|
issuer: {
|
|
91
|
-
state: tlsUsable
|
|
92
|
+
state: tlsUsable
|
|
93
|
+
? tlsPartial ? 'partial' : scalarComparison(left.compact.tls.issuerLabel, right.compact.tls.issuerLabel)
|
|
94
|
+
: 'unavailable',
|
|
92
95
|
left: left.compact.tls.issuerLabel,
|
|
93
96
|
right: right.compact.tls.issuerLabel,
|
|
94
97
|
},
|
|
95
98
|
publicKey: {
|
|
96
|
-
state: tlsUsable
|
|
99
|
+
state: tlsUsable
|
|
100
|
+
? tlsPartial ? 'partial' : scalarComparison(left.compact.tls.spkiSha256, right.compact.tls.spkiSha256)
|
|
101
|
+
: 'unavailable',
|
|
97
102
|
leftSha256: left.compact.tls.spkiSha256,
|
|
98
103
|
rightSha256: right.compact.tls.spkiSha256,
|
|
99
104
|
},
|
package/cli/progress-events.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { terminalSafeJson } from "./formatters/json.mjs";
|
|
1
2
|
export const CLI_PROGRESS_EVENT_SCHEMA = 'whoisleuth.cli.progress';
|
|
2
3
|
export const CLI_PROGRESS_EVENT_VERSION = 1;
|
|
3
4
|
const MAX_CLI_PROGRESS_EVENTS = 2_100;
|
|
@@ -45,7 +46,7 @@ function createCliProgressEvents(stream, options) {
|
|
|
45
46
|
...(typeof fields.ok === 'boolean' ? { ok: fields.ok } : {}),
|
|
46
47
|
...(exitCode !== null ? { exitCode } : {}),
|
|
47
48
|
};
|
|
48
|
-
stream.write(`${
|
|
49
|
+
stream.write(`${terminalSafeJson(document)}\n`);
|
|
49
50
|
sequence += 1;
|
|
50
51
|
},
|
|
51
52
|
});
|
package/cli/progress.mjs
CHANGED
|
@@ -10,7 +10,7 @@ function safeProgressMessage(value) {
|
|
|
10
10
|
}
|
|
11
11
|
function createTerminalProgress(stream, options) {
|
|
12
12
|
const environment = options.environment || process.env;
|
|
13
|
-
const presentation = terminalPresentation(stream, options.color, environment);
|
|
13
|
+
const presentation = terminalPresentation(stream, options.color, environment, options.palette);
|
|
14
14
|
const enabled = options.enabled
|
|
15
15
|
&& presentation.interactive
|
|
16
16
|
&& environment.WHOISLEUTH_NO_PROGRESS !== '1';
|
|
@@ -23,8 +23,8 @@ function createTerminalProgress(stream, options) {
|
|
|
23
23
|
if (!enabled || !stream)
|
|
24
24
|
return;
|
|
25
25
|
const elapsedSeconds = Math.max(0, Math.floor((now() - startedAt) / 1000));
|
|
26
|
-
const marker = tone(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] || '·', 'accent', presentation.color);
|
|
27
|
-
const elapsed = tone(`${elapsedSeconds}s`, 'dim', presentation.color);
|
|
26
|
+
const marker = tone(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] || '·', 'accent', presentation.color ? presentation.palette : false);
|
|
27
|
+
const elapsed = tone(`${elapsedSeconds}s`, 'dim', presentation.color ? presentation.palette : false);
|
|
28
28
|
stream.write(`\r\u001b[2K${marker} ${message} · ${elapsed}`);
|
|
29
29
|
frame += 1;
|
|
30
30
|
}
|
package/cli/registry-cohort.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { canonicalArtifactJsonV2 } from "../
|
|
2
|
+
import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
3
4
|
import { scanBoundedJson } from "./bounded-json.mjs";
|
|
4
5
|
import { CliUsageError } from "./errors.mjs";
|
|
5
6
|
import { buildRegistryDoctorReport } from "./registry-doctor.mjs";
|
|
6
7
|
import { SAVED_LOOKUP_SCHEMA, parseSavedLookupDocument, } from "./saved-lookup.mjs";
|
|
7
8
|
export const REGISTRY_COHORT_SCHEMA = 'whoisleuth.cli.registry-cohort';
|
|
8
|
-
export const LEGACY_REGISTRY_COHORT_VERSION = 1;
|
|
9
9
|
export const REGISTRY_COHORT_VERSION = 2;
|
|
10
10
|
export const MAX_REGISTRY_COHORT_INPUT_BYTES = 16 * 1024 * 1024;
|
|
11
11
|
export const MAX_REGISTRY_COHORT_SAMPLES = 500;
|
|
@@ -14,8 +14,6 @@ export const MAX_REGISTRY_COHORT_INPUT_POINTS = 2_000;
|
|
|
14
14
|
export const MAX_REGISTRY_COHORT_TIMELINE_POINTS = 50;
|
|
15
15
|
export const MAX_REGISTRY_COHORT_OMISSIONS = 1_000_000;
|
|
16
16
|
export const MIN_REGISTRY_COHORT_SAMPLE = 5;
|
|
17
|
-
const LEGACY_ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'sampleCount', 'minimumCohortSample', 'cohorts', 'limitations']);
|
|
18
|
-
const LEGACY_COHORT_KEYS = new Set(['suffix', 'profileId', 'sampleCount', 'state', 'sourceAlignment', 'publication']);
|
|
19
17
|
const ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'inputFamily', 'sampleCount', 'reportsMerged', 'minimumCohortSample', 'sampleWindow', 'cohorts', 'omissions', 'truncated', 'limitations']);
|
|
20
18
|
const COHORT_KEYS = new Set(['suffix', 'profileId', 'sampleCount', 'state', 'latestState', 'sampleWindow', 'sourceAlignment', 'publication', 'timeline', 'timelineOmitted']);
|
|
21
19
|
const POINT_KEYS = new Set(['reportGeneratedAt', 'sampleWindow', 'sampleCount', 'state', 'sourceAlignment', 'publication']);
|
|
@@ -29,11 +27,6 @@ const LIMITATIONS = Object.freeze([
|
|
|
29
27
|
'A merged cohort keeps the most conservative retained state and separately reports the latest point. Duplicate points are suppressed and inherited omission counts are not summed across overlapping retained reports.',
|
|
30
28
|
'Review fixture provenance and source health before changing a parser or registry access profile.',
|
|
31
29
|
]);
|
|
32
|
-
const LEGACY_LIMITATIONS = Object.freeze([
|
|
33
|
-
'The report intentionally omits domains, queries and raw evidence. It groups saved observations only by suffix and capability profile.',
|
|
34
|
-
`Cohorts with fewer than ${MIN_REGISTRY_COHORT_SAMPLE} observations remain insufficient sample and must not drive catalogue changes.`,
|
|
35
|
-
'Repeated observations from one environment are not representative by themselves. Review fixture provenance and source health before changing a parser or access profile.',
|
|
36
|
-
]);
|
|
37
30
|
function record(value, label) {
|
|
38
31
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
39
32
|
throw new CliUsageError(`${label} must be an object.`);
|
|
@@ -56,10 +49,10 @@ function timestamp(value, label) {
|
|
|
56
49
|
if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
57
50
|
throw new CliUsageError(`${label} is invalid.`);
|
|
58
51
|
}
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
53
|
+
if (normalized)
|
|
54
|
+
return normalized;
|
|
55
|
+
throw new CliUsageError(`${label} is invalid.`);
|
|
63
56
|
}
|
|
64
57
|
function identifier(value, label) {
|
|
65
58
|
if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,119}$/u.test(value))
|
|
@@ -219,39 +212,6 @@ function sourceReportsFromLookups(documents, generatedAt) {
|
|
|
219
212
|
const observed = reports.map((report) => report.generatedAt).sort();
|
|
220
213
|
return [Object.freeze({ generatedAt, sampleWindow: Object.freeze({ from: observed[0], to: observed.at(-1) }), cohorts: Object.freeze(cohorts), duplicateTimelinePoints: 0 })];
|
|
221
214
|
}
|
|
222
|
-
function parseLegacyReport(value) {
|
|
223
|
-
const root = exact(value, LEGACY_ROOT_KEYS, 'Registry cohort v1 report');
|
|
224
|
-
if (root.schema !== REGISTRY_COHORT_SCHEMA || root.version !== LEGACY_REGISTRY_COHORT_VERSION)
|
|
225
|
-
throw new CliUsageError('Registry cohort report version is unsupported.');
|
|
226
|
-
const generatedAt = timestamp(root.generatedAt, 'Registry cohort v1 generatedAt');
|
|
227
|
-
const sampleCount = integer(root.sampleCount, 1, MAX_REGISTRY_COHORT_SAMPLES, 'Registry cohort v1 sample count');
|
|
228
|
-
if (root.minimumCohortSample !== MIN_REGISTRY_COHORT_SAMPLE)
|
|
229
|
-
throw new CliUsageError('Registry cohort v1 minimum sample is unsupported.');
|
|
230
|
-
boundedLimitations(root.limitations, LEGACY_LIMITATIONS, 'Registry cohort v1');
|
|
231
|
-
if (!Array.isArray(root.cohorts) || !root.cohorts.length || root.cohorts.length > MAX_REGISTRY_COHORT_GROUPS)
|
|
232
|
-
throw new CliUsageError('Registry cohort v1 groups are invalid.');
|
|
233
|
-
const seen = new Set();
|
|
234
|
-
let represented = 0;
|
|
235
|
-
const cohorts = root.cohorts.map((candidate, index) => {
|
|
236
|
-
const item = exact(candidate, LEGACY_COHORT_KEYS, `Registry cohort v1 group ${index + 1}`);
|
|
237
|
-
const count = integer(item.sampleCount, 1, sampleCount, 'Registry cohort v1 group sample count');
|
|
238
|
-
const sources = sourceAlignment(item.sourceAlignment, count, 'Registry cohort v1 source alignment');
|
|
239
|
-
const published = publication(item.publication, count, 'Registry cohort v1 publication');
|
|
240
|
-
const pointState = state(item.state, 'Registry cohort v1 state');
|
|
241
|
-
if (pointState !== derivedState(count, sources, published))
|
|
242
|
-
throw new CliUsageError('Registry cohort v1 state does not match its retained counts.');
|
|
243
|
-
const key = `${suffix(item.suffix)}\u0000${identifier(item.profileId, 'Registry cohort v1 profile ID')}`;
|
|
244
|
-
if (seen.has(key))
|
|
245
|
-
throw new CliUsageError('Registry cohort v1 groups must be unique.');
|
|
246
|
-
seen.add(key);
|
|
247
|
-
represented += count;
|
|
248
|
-
const point = Object.freeze({ reportGeneratedAt: generatedAt, sampleWindow: Object.freeze({ from: generatedAt, to: generatedAt }), sampleCount: count, state: pointState, sourceAlignment: sources, publication: published });
|
|
249
|
-
return Object.freeze({ suffix: key.split('\u0000')[0], profileId: key.split('\u0000')[1], sampleWindow: point.sampleWindow, points: Object.freeze([point]), inheritedState: pointState, upstreamTimelineOmitted: 0 });
|
|
250
|
-
});
|
|
251
|
-
if (represented !== sampleCount)
|
|
252
|
-
throw new CliUsageError('Registry cohort v1 sample count does not match its groups.');
|
|
253
|
-
return Object.freeze({ generatedAt, sampleWindow: Object.freeze({ from: generatedAt, to: generatedAt }), cohorts: Object.freeze(cohorts), duplicateTimelinePoints: 0 });
|
|
254
|
-
}
|
|
255
215
|
function parseCurrentReport(value) {
|
|
256
216
|
const root = exact(value, ROOT_KEYS, 'Registry cohort v2 report');
|
|
257
217
|
if (root.schema !== REGISTRY_COHORT_SCHEMA || root.version !== REGISTRY_COHORT_VERSION)
|
|
@@ -406,11 +366,9 @@ export function buildRegistryCohortReport(text, generatedAt = new Date().toISOSt
|
|
|
406
366
|
const reports = [];
|
|
407
367
|
let retainedPoints = 0;
|
|
408
368
|
for (const document of documents) {
|
|
409
|
-
const report = document.version ===
|
|
410
|
-
?
|
|
411
|
-
:
|
|
412
|
-
? parseCurrentReport(document)
|
|
413
|
-
: (() => { throw new CliUsageError('Registry cohort report version is unsupported.'); })();
|
|
369
|
+
const report = document.version === REGISTRY_COHORT_VERSION
|
|
370
|
+
? parseCurrentReport(document)
|
|
371
|
+
: (() => { throw new CliUsageError('Registry cohort report version is unsupported.'); })();
|
|
414
372
|
retainedPoints += report.cohorts.reduce((total, cohort) => total + cohort.points.length, 0);
|
|
415
373
|
if (retainedPoints > MAX_REGISTRY_COHORT_INPUT_POINTS) {
|
|
416
374
|
throw new CliUsageError(`Retained registry cohort input contains more than ${MAX_REGISTRY_COHORT_INPUT_POINTS} timeline points.`);
|
package/cli/registry-doctor.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { registryCapabilityFor, REGISTRY_CAPABILITIES_VERSION } from "../lib/registry-capabilities.mjs";
|
|
2
2
|
import { recordOrEmpty } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
|
+
import { registryDateIso } from "../lib/registry-dates.mjs";
|
|
3
4
|
import { CliUsageError } from "./errors.mjs";
|
|
4
5
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
5
6
|
const REGISTRY_DOCTOR_SCHEMA = 'whoisleuth.cli.registry-doctor';
|
|
6
|
-
const REGISTRY_DOCTOR_VERSION =
|
|
7
|
+
const REGISTRY_DOCTOR_VERSION = 3;
|
|
7
8
|
function status(value) {
|
|
8
9
|
return typeof value === 'string'
|
|
9
10
|
? value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').trim().toLowerCase().slice(0, 40) || 'unavailable'
|
|
@@ -41,27 +42,37 @@ function publicationQuality(parsed, rdapStatus) {
|
|
|
41
42
|
objectClass: unavailable,
|
|
42
43
|
objectIdentifier: unavailable,
|
|
43
44
|
mediaType: unavailable,
|
|
44
|
-
baseConformance: { state: 'unavailable', declarations: [] },
|
|
45
|
+
baseConformance: { state: 'unavailable', declarations: [], truncated: false },
|
|
45
46
|
redactionMetadata: { state: 'unavailable', count: 0, truncated: false },
|
|
46
|
-
selfLink: { state: 'unavailable', observedRelations: [] },
|
|
47
|
-
events: { state: 'unavailable', count: 0, conflictingActions: [] },
|
|
47
|
+
selfLink: { state: 'unavailable', observedRelations: [], truncated: false },
|
|
48
|
+
events: { state: 'unavailable', count: 0, conflictingActions: [], truncated: false },
|
|
48
49
|
reviewItems: 0,
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
const objectClassName = typeof parsed.objectClassName === 'string' ? parsed.objectClassName.trim().toLowerCase() : '';
|
|
52
53
|
const identifier = [parsed.handle, parsed.objectIdentifier, parsed.registryObjectId, parsed.id]
|
|
53
54
|
.find((value) => typeof value === 'string' && Boolean(value.trim()))?.trim() ?? null;
|
|
54
|
-
const
|
|
55
|
+
const conformanceValues = Array.isArray(parsed.conformance) ? parsed.conformance : [];
|
|
56
|
+
const conformanceTruncated = parsed.conformanceTruncated === true || conformanceValues.length > 100;
|
|
57
|
+
const declarations = stringArray(conformanceValues);
|
|
55
58
|
const redactions = Array.isArray(parsed.redactions) ? parsed.redactions.slice(0, 100) : [];
|
|
56
|
-
const
|
|
59
|
+
const redactionsTruncated = parsed.redactionsTruncated === true
|
|
60
|
+
|| (Array.isArray(parsed.redactions) && parsed.redactions.length > 100);
|
|
61
|
+
const linkValues = Array.isArray(parsed.links) ? parsed.links : [];
|
|
62
|
+
const links = linkValues.slice(0, 100).map(recordOrEmpty);
|
|
63
|
+
const linksTruncated = parsed.linksTruncated === true
|
|
64
|
+
|| linkValues.length > 100
|
|
65
|
+
|| links.some((link) => Array.isArray(link.rel) && link.rel.length > 10);
|
|
57
66
|
const relations = [...new Set(links.flatMap((link) => stringArray(link.rel, 10).length
|
|
58
67
|
? stringArray(link.rel, 10)
|
|
59
68
|
: typeof link.rel === 'string' ? [link.rel.trim().toLowerCase()] : []).filter(Boolean))];
|
|
60
|
-
const
|
|
69
|
+
const eventValues = Array.isArray(parsed.events) ? parsed.events : [];
|
|
70
|
+
const events = eventValues.slice(0, 100).map(recordOrEmpty);
|
|
71
|
+
const eventsTruncated = parsed.eventsTruncated === true || eventValues.length > 100;
|
|
61
72
|
const actionDates = new Map();
|
|
62
73
|
for (const event of events) {
|
|
63
74
|
const action = typeof event.action === 'string' ? event.action.trim().toLowerCase() : '';
|
|
64
|
-
const date =
|
|
75
|
+
const date = registryDateIso(event.date);
|
|
65
76
|
if (!action || !date)
|
|
66
77
|
continue;
|
|
67
78
|
const dates = actionDates.get(action) ?? [];
|
|
@@ -79,8 +90,21 @@ function publicationQuality(parsed, rdapStatus) {
|
|
|
79
90
|
['reinstantiation', 'reinstantiationDateIso', true],
|
|
80
91
|
['last update of rdap database', 'databaseUpdatedDateIso', true],
|
|
81
92
|
];
|
|
93
|
+
const lifecycleDates = new Map();
|
|
94
|
+
let lifecycleMalformed = false;
|
|
95
|
+
for (const [, field] of lifecycleMappings) {
|
|
96
|
+
const supplied = lifecycle[field];
|
|
97
|
+
if (supplied === undefined || supplied === null)
|
|
98
|
+
continue;
|
|
99
|
+
const normalized = registryDateIso(supplied);
|
|
100
|
+
if (normalized)
|
|
101
|
+
lifecycleDates.set(field, normalized);
|
|
102
|
+
else
|
|
103
|
+
lifecycleMalformed = true;
|
|
104
|
+
}
|
|
82
105
|
const conflictingActions = lifecycleMappings.flatMap(([action, field, newest]) => {
|
|
83
|
-
const
|
|
106
|
+
const publishedDate = lifecycleDates.get(field);
|
|
107
|
+
const published = publishedDate ? Date.parse(publishedDate) : Number.NaN;
|
|
84
108
|
if (!Number.isFinite(published))
|
|
85
109
|
return [];
|
|
86
110
|
const dates = (actionDates.get(action) ?? []).map(Date.parse).filter(Number.isFinite);
|
|
@@ -90,30 +114,41 @@ function publicationQuality(parsed, rdapStatus) {
|
|
|
90
114
|
return selected === published ? [] : [action];
|
|
91
115
|
}).sort();
|
|
92
116
|
const objectClassState = !objectClassName ? 'not_observed' : objectClassName === 'domain' ? 'observed' : 'inconsistent';
|
|
93
|
-
const baseConformanceState = declarations.includes('rdap_level_0')
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
? '
|
|
98
|
-
:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
const baseConformanceState = declarations.includes('rdap_level_0')
|
|
118
|
+
? 'observed'
|
|
119
|
+
: conformanceTruncated ? 'unavailable' : 'not_observed';
|
|
120
|
+
const selfLinkState = relations.includes('self')
|
|
121
|
+
? 'observed'
|
|
122
|
+
: linksTruncated ? 'unavailable' : 'not_observed';
|
|
123
|
+
const hasLifecycle = lifecycleDates.size > 0;
|
|
124
|
+
const eventsState = eventsTruncated || lifecycleMalformed
|
|
125
|
+
? 'unavailable'
|
|
126
|
+
: conflictingActions.length
|
|
127
|
+
? 'inconsistent'
|
|
128
|
+
: events.length && hasLifecycle
|
|
129
|
+
? 'observed'
|
|
130
|
+
: events.length
|
|
131
|
+
? 'unavailable'
|
|
132
|
+
: 'not_observed';
|
|
103
133
|
return {
|
|
104
134
|
objectClass: { state: objectClassState, value: objectClassName || null },
|
|
105
135
|
objectIdentifier: { state: identifier ? 'observed' : 'not_observed', value: identifier },
|
|
106
136
|
mediaType: { state: 'unavailable', value: null },
|
|
107
|
-
baseConformance: { state: baseConformanceState, declarations },
|
|
137
|
+
baseConformance: { state: baseConformanceState, declarations, truncated: conformanceTruncated },
|
|
108
138
|
redactionMetadata: {
|
|
109
|
-
state: redactions.length ? 'observed' : 'not_observed',
|
|
139
|
+
state: redactions.length ? 'observed' : redactionsTruncated ? 'unavailable' : 'not_observed',
|
|
110
140
|
count: redactions.length,
|
|
111
|
-
truncated:
|
|
141
|
+
truncated: redactionsTruncated,
|
|
142
|
+
},
|
|
143
|
+
selfLink: { state: selfLinkState, observedRelations: relations.sort(), truncated: linksTruncated },
|
|
144
|
+
events: {
|
|
145
|
+
state: eventsState,
|
|
146
|
+
count: events.length,
|
|
147
|
+
conflictingActions: eventsTruncated || lifecycleMalformed ? [] : conflictingActions,
|
|
148
|
+
truncated: eventsTruncated,
|
|
112
149
|
},
|
|
113
|
-
selfLink: { state: selfLinkState, observedRelations: relations.sort() },
|
|
114
|
-
events: { state: eventsState, count: events.length, conflictingActions },
|
|
115
150
|
reviewItems: [objectClassState, baseConformanceState, selfLinkState, eventsState]
|
|
116
|
-
.filter((state) => state
|
|
151
|
+
.filter((state) => state !== 'observed').length,
|
|
117
152
|
};
|
|
118
153
|
}
|
|
119
154
|
function buildRegistryDoctorReport(raw, generatedAt = new Date().toISOString()) {
|
|
@@ -182,6 +217,10 @@ function buildRegistryDoctorReport(raw, generatedAt = new Date().toISOString())
|
|
|
182
217
|
'A catalogue alignment does not prove live reachability, parser completeness, current registry policy, registration, availability, ownership, safety, or maliciousness.',
|
|
183
218
|
'A missing registry object identifier can be a publication characteristic. It remains not observed and is not converted into a collection failure.',
|
|
184
219
|
'Publication-quality observations describe the saved normalised RDAP object. The saved Lookup format does not retain the response media type, so that field remains unavailable.',
|
|
220
|
+
...(publication.baseConformance.truncated || publication.redactionMetadata.truncated
|
|
221
|
+
|| publication.selfLink.truncated || publication.events.truncated
|
|
222
|
+
? ['One or more bounded RDAP publication families were truncated; omitted values remain unavailable and cannot support absence or consistency conclusions.']
|
|
223
|
+
: []),
|
|
185
224
|
'Use fixture-based maintenance checks before changing a registry parser or access profile; automated tests must not contact live registries.',
|
|
186
225
|
],
|
|
187
226
|
};
|
|
@@ -203,9 +242,9 @@ function formatRegistryDoctorReport(report) {
|
|
|
203
242
|
output.push('', 'RDAP publication quality');
|
|
204
243
|
output.push(` Object class: ${report.publication.objectClass.state.replaceAll('_', ' ')}`);
|
|
205
244
|
output.push(` Object identifier: ${report.publication.objectIdentifier.state.replaceAll('_', ' ')}`);
|
|
206
|
-
output.push(` Base conformance: ${report.publication.baseConformance.state.replaceAll('_', ' ')}`);
|
|
207
|
-
output.push(` Self link: ${report.publication.selfLink.state.replaceAll('_', ' ')}`);
|
|
208
|
-
output.push(` Events: ${report.publication.events.state.replaceAll('_', ' ')}`);
|
|
245
|
+
output.push(` Base conformance: ${report.publication.baseConformance.state.replaceAll('_', ' ')}${report.publication.baseConformance.truncated ? ' (partial)' : ''}`);
|
|
246
|
+
output.push(` Self link: ${report.publication.selfLink.state.replaceAll('_', ' ')}${report.publication.selfLink.truncated ? ' (partial)' : ''}`);
|
|
247
|
+
output.push(` Events: ${report.publication.events.state.replaceAll('_', ' ')}${report.publication.events.truncated ? ' (partial)' : ''}`);
|
|
209
248
|
output.push(' Media type: unavailable in the saved Lookup contract');
|
|
210
249
|
if (report.recommendations.length) {
|
|
211
250
|
output.push('', 'Recommendations:');
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { canonicalArtifactJsonV2 } from "../
|
|
4
|
-
import { BULK_SESSION_SCHEMA, MAX_BULK_SESSIONS, MAX_BULK_SESSION_ROWS, SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, mergeBulkSessions, normalizeBulkSession, } from "../
|
|
5
|
-
import { buildBulkComparisonCandidates } from "../
|
|
6
|
-
import { MAX_COMPARISON_LEDGER_DETAIL_ROWS, boundedComparisonLedgerLimitations, comparisonLedgerCollector, comparisonLedgerValuePresent, freshComparisonLedgerCounters, makeComparisonLedgerCandidate, normaliseComparisonLedgerRowWithCounters, stableComparisonLedgerId, stableComparisonLedgerJson, } from "../
|
|
3
|
+
import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
+
import { BULK_SESSION_SCHEMA, MAX_BULK_SESSIONS, MAX_BULK_SESSION_ROWS, SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, mergeBulkSessions, normalizeBulkSession, } from "../packages/workspace/bulk-session-model.mjs";
|
|
5
|
+
import { buildBulkComparisonCandidates } from "../packages/comparison/comparison-ledger-bulk.mjs";
|
|
6
|
+
import { MAX_COMPARISON_LEDGER_DETAIL_ROWS, boundedComparisonLedgerLimitations, comparisonLedgerCollector, comparisonLedgerValuePresent, freshComparisonLedgerCounters, makeComparisonLedgerCandidate, normaliseComparisonLedgerRowWithCounters, stableComparisonLedgerId, stableComparisonLedgerJson, } from "../packages/comparison/comparison-ledger-contract.mjs";
|
|
7
7
|
import { DOMAIN_PORTFOLIO_INPUT_SCHEMA, DOMAIN_PORTFOLIO_REVIEW_SCHEMA, DOMAIN_PORTFOLIO_REVIEW_VERSION, reviewDomainPortfolio, } from "../lib/domain-portfolio-review.mjs";
|
|
8
8
|
import { parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
9
9
|
import { CliUsageError } from "./errors.mjs";
|
|
10
10
|
import { buildCliLookupDiff, formatCliLookupDiff } from "./lookup-diff.mjs";
|
|
11
11
|
import { SAVED_LOOKUP_SCHEMA } from "./saved-lookup.mjs";
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
export const MAX_RETAINED_ARTIFACT_DIFF_BYTES = 8 * 1024 * 1024;
|
|
12
|
+
import { CLI_COMPARISON_LEDGER_SCHEMA, CLI_COMPARISON_LEDGER_VERSION, MAX_RETAINED_ARTIFACT_DIFF_BYTES, } from "../packages/contracts/offline-comparison.mjs";
|
|
13
|
+
export { CLI_COMPARISON_LEDGER_SCHEMA, CLI_COMPARISON_LEDGER_VERSION, MAX_RETAINED_ARTIFACT_DIFF_BYTES };
|
|
15
14
|
const BULK_ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'sessions', 'limitations']);
|
|
16
15
|
const PORTFOLIO_ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'portfolioLabel', 'assets', 'simulations', 'renewalQueue', 'recoveryCycles', 'unknownCounts', 'limitations']);
|
|
17
16
|
const SAFE_SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
@@ -63,7 +62,7 @@ function parseBulkExport(root) {
|
|
|
63
62
|
const rawSession = record(candidate);
|
|
64
63
|
const rawResults = Array.isArray(rawSession?.results) ? rawSession.results : null;
|
|
65
64
|
const rawDomains = Array.isArray(rawSession?.domains) ? rawSession.domains : null;
|
|
66
|
-
const session = normalizeBulkSession(candidate
|
|
65
|
+
const session = normalizeBulkSession(candidate);
|
|
67
66
|
if (!session || !rawResults || !rawDomains
|
|
68
67
|
|| rawResults.length > MAX_BULK_SESSION_ROWS
|
|
69
68
|
|| rawResults.length !== session.results.length
|