@slicedearth/whoisleuth-cli 1.32.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/LICENSE +661 -0
- package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
- package/NOTICE +76 -0
- package/README.md +84 -0
- package/TRADEMARKS.md +41 -0
- package/bin/whoisleuth.mjs +35 -0
- package/cli/archive-inspect.mjs +198 -0
- package/cli/arguments.mjs +1022 -0
- package/cli/artifact-verify.mjs +210 -0
- package/cli/bulk-checkpoint.mjs +218 -0
- package/cli/bulk-command-runner.mjs +109 -0
- package/cli/bulk-output.mjs +112 -0
- package/cli/bulk.mjs +133 -0
- package/cli/compare.mjs +238 -0
- package/cli/completion.mjs +175 -0
- package/cli/csv.mjs +1 -0
- package/cli/discover.mjs +50 -0
- package/cli/discovery-command-runner.mjs +36 -0
- package/cli/discovery-observation-snapshot.mjs +315 -0
- package/cli/discovery-scan-command-runner.mjs +141 -0
- package/cli/discovery-scan.mjs +325 -0
- package/cli/discovery-snapshot.mjs +133 -0
- package/cli/discovery-workflow.mjs +73 -0
- package/cli/doctor.mjs +137 -0
- package/cli/errors.mjs +21 -0
- package/cli/evidence-command-arguments.mjs +145 -0
- package/cli/evidence-command-runner.mjs +106 -0
- package/cli/evidence-signing.mjs +215 -0
- package/cli/exit-codes.mjs +9 -0
- package/cli/export-evidence.mjs +28 -0
- package/cli/formatters/evidence-report.mjs +3 -0
- package/cli/formatters/html.mjs +69 -0
- package/cli/formatters/json.mjs +135 -0
- package/cli/formatters/markdown.mjs +3 -0
- package/cli/formatters/terminal.mjs +658 -0
- package/cli/http.mjs +27 -0
- package/cli/lookup-command-runner.mjs +87 -0
- package/cli/lookup-diff.mjs +171 -0
- package/cli/mail-review.mjs +204 -0
- package/cli/manual.mjs +34 -0
- package/cli/network-command-runner.mjs +88 -0
- package/cli/output-file.mjs +97 -0
- package/cli/page-compare.mjs +123 -0
- package/cli/posture.mjs +20 -0
- package/cli/progress-events.mjs +53 -0
- package/cli/progress.mjs +56 -0
- package/cli/registry-access.mjs +3 -0
- package/cli/registry-support.mjs +120 -0
- package/cli/risk-calibration.mjs +294 -0
- package/cli/runner-types.mjs +1 -0
- package/cli/runner.mjs +726 -0
- package/cli/saved-lookup.mjs +87 -0
- package/cli/source-reliability.mjs +727 -0
- package/cli/strict-exit.mjs +55 -0
- package/cli/terminal-presentation.mjs +147 -0
- package/docs/cli.md +827 -0
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
- package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
- package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
- package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
- package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
- package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
- package/frontend/src/lib/analysis/bulk-sort.js +49 -0
- package/frontend/src/lib/analysis/campaign-model.js +265 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
- package/frontend/src/lib/analysis/case-migration-model.js +248 -0
- package/frontend/src/lib/analysis/case-model.js +5 -0
- package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
- package/frontend/src/lib/analysis/case-record-core.js +232 -0
- package/frontend/src/lib/analysis/case-record-model.js +7 -0
- package/frontend/src/lib/analysis/case-record-operations.js +224 -0
- package/frontend/src/lib/analysis/case-response-model.js +640 -0
- package/frontend/src/lib/analysis/case-response-packet.js +573 -0
- package/frontend/src/lib/analysis/case-storage-model.js +93 -0
- package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
- package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
- package/frontend/src/lib/analysis/http-summary.js +165 -0
- package/frontend/src/lib/analysis/investigation-guide.js +500 -0
- package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
- package/frontend/src/lib/analysis/page-baseline.js +272 -0
- package/frontend/src/lib/analysis/page-similarity.js +179 -0
- package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
- package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
- package/frontend/src/lib/analysis/scoring.js +135 -0
- package/frontend/src/lib/analysis/shortlist-model.js +209 -0
- package/frontend/src/lib/analysis/utils.js +265 -0
- package/frontend/src/lib/analysis/watchlist-history.js +643 -0
- package/frontend/src/lib/analysis/watchlist-store.js +127 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
- package/frontend/src/lib/analysis/workspace-archive.js +472 -0
- package/lib/abort.mjs +26 -0
- package/lib/abusech-auth.mjs +19 -0
- package/lib/availability.mjs +716 -0
- package/lib/bounded-contract-normalizers.mjs +61 -0
- package/lib/browser-library-profile.mjs +310 -0
- package/lib/bulk-comparison-evidence.mjs +83 -0
- package/lib/classify.mjs +93 -0
- package/lib/client-behavior-profile.mjs +83 -0
- package/lib/credential-surface-profile.mjs +66 -0
- package/lib/csv.mjs +23 -0
- package/lib/ct-query.mjs +27 -0
- package/lib/ct-search.mjs +393 -0
- package/lib/distributed-operation-budget.mjs +461 -0
- package/lib/dns-delegation-health.mjs +344 -0
- package/lib/dns-intelligence.mjs +739 -0
- package/lib/dns-mx.mjs +24 -0
- package/lib/dns-resolver-selection.mjs +33 -0
- package/lib/domain-posture-analysis.mjs +254 -0
- package/lib/domain-posture-parsers.mjs +358 -0
- package/lib/domain-posture.mjs +745 -0
- package/lib/error-detail.mjs +10 -0
- package/lib/evidence-export.mjs +431 -0
- package/lib/evidence-report-markdown.mjs +71 -0
- package/lib/evidence-report.mjs +391 -0
- package/lib/external-intelligence-risk.mjs +108 -0
- package/lib/favicon.mjs +146 -0
- package/lib/feature-policy.mjs +70 -0
- package/lib/generated/retire-browser-catalog.mjs +8724 -0
- package/lib/generated/unicode-confusables-17.mjs +93 -0
- package/lib/hostname.mjs +38 -0
- package/lib/html-signals.mjs +680 -0
- package/lib/http-intelligence.mjs +232 -0
- package/lib/idn-confusable-policy.mjs +86 -0
- package/lib/idn-confusables.mjs +465 -0
- package/lib/lookup-cache.mjs +130 -0
- package/lib/lookup-progress.mjs +291 -0
- package/lib/lookup-source-progress.mjs +109 -0
- package/lib/lookup-threat-provider-inventory.mjs +13 -0
- package/lib/lookup.mjs +592 -0
- package/lib/observation.mjs +114 -0
- package/lib/observed-network-context.mjs +355 -0
- package/lib/operation-budget.mjs +489 -0
- package/lib/page-fingerprints.mjs +481 -0
- package/lib/page-role-profile.mjs +117 -0
- package/lib/perceptual-hash.mjs +451 -0
- package/lib/rdap-attempts.mjs +34 -0
- package/lib/rdap-bootstrap.mjs +173 -0
- package/lib/rdap-capabilities.mjs +292 -0
- package/lib/rdap-client.mjs +125 -0
- package/lib/rdap-normalization.mjs +691 -0
- package/lib/rdap-registrar.mjs +196 -0
- package/lib/rdap-transport.mjs +39 -0
- package/lib/rdap-types.mjs +1 -0
- package/lib/rdap-validation.mjs +94 -0
- package/lib/rdap.mjs +16 -0
- package/lib/registry-access.mjs +32 -0
- package/lib/registry-capabilities.mjs +117 -0
- package/lib/registry-capability-catalogue.mjs +1206 -0
- package/lib/registry-comparison.mjs +318 -0
- package/lib/registry-dates.mjs +190 -0
- package/lib/registry-insights.mjs +325 -0
- package/lib/response-policy.mjs +256 -0
- package/lib/risk-scoring.mjs +164 -0
- package/lib/safe-fetch.mjs +446 -0
- package/lib/security-txt.mjs +359 -0
- package/lib/service-binding-dns.mjs +608 -0
- package/lib/sslbl-certificates.generated.mjs +143 -0
- package/lib/sslbl-intelligence.mjs +241 -0
- package/lib/static-html-analysis.mjs +340 -0
- package/lib/structured-data-identity.mjs +323 -0
- package/lib/threat-intelligence-contract.mjs +5 -0
- package/lib/threat-intelligence-definition-registry.mjs +34 -0
- package/lib/threat-intelligence-fixtures.mjs +82 -0
- package/lib/threat-intelligence-manifests.mjs +512 -0
- package/lib/threat-intelligence-runtime.mjs +662 -0
- package/lib/threat-intelligence-targets.mjs +168 -0
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +307 -0
- package/lib/tls-intelligence.mjs +814 -0
- package/lib/typosquat-generator.mjs +884 -0
- package/lib/urlhaus-intelligence.mjs +361 -0
- package/lib/urlscan-intelligence.mjs +349 -0
- package/lib/website-security-posture.mjs +325 -0
- package/lib/website-technology.mjs +437 -0
- package/lib/whois-authority.mjs +98 -0
- package/lib/whois-chain.mjs +261 -0
- package/lib/whois-contacts.mjs +163 -0
- package/lib/whois-contracts.mjs +3 -0
- package/lib/whois-normalization.mjs +27 -0
- package/lib/whois-transport.mjs +100 -0
- package/lib/whois-values.mjs +92 -0
- package/lib/whois.mjs +1187 -0
- package/package.json +55 -0
- package/third-party-notices.txt +5571 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { unlinkSync } from 'node:fs';
|
|
3
|
+
import { link, open, rename, unlink } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { CliUsageError } from "./errors.mjs";
|
|
6
|
+
export const MAX_CLI_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
7
|
+
const MAX_CLI_OUTPUT_PATH_LENGTH = 4096;
|
|
8
|
+
const pendingOutputFiles = new Set();
|
|
9
|
+
function safeOutputPath(value) {
|
|
10
|
+
if (typeof value !== 'string' || !value || value.length > MAX_CLI_OUTPUT_PATH_LENGTH) {
|
|
11
|
+
throw new CliUsageError(`Output paths are limited to ${MAX_CLI_OUTPUT_PATH_LENGTH} characters.`);
|
|
12
|
+
}
|
|
13
|
+
if (/[\x00-\x1f\x7f]/u.test(value)) {
|
|
14
|
+
throw new CliUsageError('Output paths cannot contain control characters.');
|
|
15
|
+
}
|
|
16
|
+
return resolve(value);
|
|
17
|
+
}
|
|
18
|
+
function createBufferedOutput() {
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
stream: {
|
|
23
|
+
isTTY: false,
|
|
24
|
+
columns: 80,
|
|
25
|
+
write(chunk) {
|
|
26
|
+
const value = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
27
|
+
bytes += Buffer.byteLength(value, 'utf8');
|
|
28
|
+
if (bytes > MAX_CLI_OUTPUT_BYTES) {
|
|
29
|
+
throw new CliUsageError(`Generated output is limited to ${MAX_CLI_OUTPUT_BYTES} bytes.`);
|
|
30
|
+
}
|
|
31
|
+
chunks.push(value);
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
value: () => chunks.join(''),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function writePrivateFile(pathValue, content, options = {}) {
|
|
39
|
+
const target = safeOutputPath(pathValue);
|
|
40
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_CLI_OUTPUT_BYTES) {
|
|
41
|
+
throw new CliUsageError(`Generated output is limited to ${MAX_CLI_OUTPUT_BYTES} bytes.`);
|
|
42
|
+
}
|
|
43
|
+
const directory = dirname(target);
|
|
44
|
+
const temporary = join(directory, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
|
|
45
|
+
let temporaryCreated = false;
|
|
46
|
+
try {
|
|
47
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
48
|
+
temporaryCreated = true;
|
|
49
|
+
pendingOutputFiles.add(temporary);
|
|
50
|
+
try {
|
|
51
|
+
await handle.writeFile(content, { encoding: 'utf8' });
|
|
52
|
+
await handle.sync();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await handle.close();
|
|
56
|
+
}
|
|
57
|
+
if (options.force) {
|
|
58
|
+
await rename(temporary, target);
|
|
59
|
+
temporaryCreated = false;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
try {
|
|
63
|
+
await link(temporary, target);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : null;
|
|
67
|
+
if (code === 'EEXIST') {
|
|
68
|
+
throw new CliUsageError(options.existingFileMessage || `Output file already exists: ${target}. Use --force to replace it.`);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
await unlink(temporary);
|
|
73
|
+
temporaryCreated = false;
|
|
74
|
+
}
|
|
75
|
+
return target;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (temporaryCreated)
|
|
79
|
+
await unlink(temporary).catch(() => { });
|
|
80
|
+
pendingOutputFiles.delete(temporary);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function cleanupPendingOutputFilesSync() {
|
|
84
|
+
for (const path of pendingOutputFiles) {
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(path);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A second interrupt is an emergency exit. Best-effort cleanup must not
|
|
90
|
+
// delay it or replace the analyst-requested exit status.
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
pendingOutputFiles.delete(path);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export { cleanupPendingOutputFilesSync, createBufferedOutput, writePrivateFile };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { buildBulkComparisonEvidence } from "../lib/bulk-comparison-evidence.mjs";
|
|
2
|
+
import { createPageBaseline } from "../frontend/src/lib/analysis/page-baseline.js";
|
|
3
|
+
import { comparePageBaselines } from "../frontend/src/lib/analysis/page-similarity.js";
|
|
4
|
+
import { CliUsageError } from "./errors.mjs";
|
|
5
|
+
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
6
|
+
export const CLI_PAGE_COMPARE_SCHEMA = 'whoisleuth.cli.page-compare';
|
|
7
|
+
export const CLI_PAGE_COMPARE_VERSION = 2;
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
function stringList(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return [];
|
|
14
|
+
return [...new Set(value.flatMap((candidate) => typeof candidate === 'string' ? [candidate] : []))].sort().slice(0, 20);
|
|
15
|
+
}
|
|
16
|
+
function setComparison(left, right) {
|
|
17
|
+
if (!left.length || !right.length)
|
|
18
|
+
return 'unavailable';
|
|
19
|
+
const shared = left.filter((value) => right.includes(value));
|
|
20
|
+
if (shared.length === left.length && shared.length === right.length)
|
|
21
|
+
return 'equal';
|
|
22
|
+
return shared.length ? 'overlap' : 'different';
|
|
23
|
+
}
|
|
24
|
+
function scalarComparison(left, right) {
|
|
25
|
+
if (!left || !right)
|
|
26
|
+
return 'unavailable';
|
|
27
|
+
return left === right ? 'equal' : 'different';
|
|
28
|
+
}
|
|
29
|
+
function comparisonContext(document) {
|
|
30
|
+
const availability = record(document.availability);
|
|
31
|
+
const pageBaseline = createPageBaseline(document.registrableDomain, availability);
|
|
32
|
+
if (!pageBaseline) {
|
|
33
|
+
throw new CliUsageError(`${document.registrableDomain} does not contain a complete, supported static page-identity observation. Run a deep lookup and save its JSON output first.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
availability,
|
|
37
|
+
pageBaseline,
|
|
38
|
+
compact: buildBulkComparisonEvidence(availability),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildCliPageComparison(leftText, rightText, generatedAt = new Date().toISOString()) {
|
|
42
|
+
const leftDocument = parseSavedLookupDocument(leftText, { label: 'Left page-comparison input' });
|
|
43
|
+
const rightDocument = parseSavedLookupDocument(rightText, { label: 'Right page-comparison input' });
|
|
44
|
+
if (leftDocument.registrableDomain === rightDocument.registrableDomain) {
|
|
45
|
+
throw new CliUsageError('Page comparison requires lookup documents for two different domains.');
|
|
46
|
+
}
|
|
47
|
+
const left = comparisonContext(leftDocument);
|
|
48
|
+
const right = comparisonContext(rightDocument);
|
|
49
|
+
const page = comparePageBaselines(left.pageBaseline, right.pageBaseline);
|
|
50
|
+
if (!page)
|
|
51
|
+
throw new CliUsageError('The static page observations could not be compared.');
|
|
52
|
+
const leftTechnology = stringList(left.compact.technology.ids);
|
|
53
|
+
const rightTechnology = stringList(right.compact.technology.ids);
|
|
54
|
+
const sharedTechnology = leftTechnology.filter((id) => rightTechnology.includes(id));
|
|
55
|
+
const technologyUsable = ['success', 'partial'].includes(left.compact.technology.state)
|
|
56
|
+
&& ['success', 'partial'].includes(right.compact.technology.state);
|
|
57
|
+
const tlsUsable = ['success', 'partial'].includes(left.compact.tls.state)
|
|
58
|
+
&& ['success', 'partial'].includes(right.compact.tls.state);
|
|
59
|
+
return {
|
|
60
|
+
schema: CLI_PAGE_COMPARE_SCHEMA,
|
|
61
|
+
version: CLI_PAGE_COMPARE_VERSION,
|
|
62
|
+
generatedAt,
|
|
63
|
+
left: {
|
|
64
|
+
domain: leftDocument.registrableDomain,
|
|
65
|
+
observedAt: left.pageBaseline.observedAt,
|
|
66
|
+
complete: left.pageBaseline.complete,
|
|
67
|
+
},
|
|
68
|
+
right: {
|
|
69
|
+
domain: rightDocument.registrableDomain,
|
|
70
|
+
observedAt: right.pageBaseline.observedAt,
|
|
71
|
+
complete: right.pageBaseline.complete,
|
|
72
|
+
},
|
|
73
|
+
page,
|
|
74
|
+
technology: {
|
|
75
|
+
state: technologyUsable ? setComparison(leftTechnology, rightTechnology) : 'unavailable',
|
|
76
|
+
leftSourceState: left.compact.technology.state,
|
|
77
|
+
rightSourceState: right.compact.technology.state,
|
|
78
|
+
leftIds: leftTechnology,
|
|
79
|
+
rightIds: rightTechnology,
|
|
80
|
+
sharedIds: sharedTechnology,
|
|
81
|
+
partial: left.compact.technology.truncated || right.compact.technology.truncated
|
|
82
|
+
|| left.compact.technology.state === 'partial' || right.compact.technology.state === 'partial',
|
|
83
|
+
},
|
|
84
|
+
tls: {
|
|
85
|
+
leftSourceState: left.compact.tls.state,
|
|
86
|
+
rightSourceState: right.compact.tls.state,
|
|
87
|
+
issuer: {
|
|
88
|
+
state: tlsUsable ? scalarComparison(left.compact.tls.issuerLabel, right.compact.tls.issuerLabel) : 'unavailable',
|
|
89
|
+
left: left.compact.tls.issuerLabel,
|
|
90
|
+
right: right.compact.tls.issuerLabel,
|
|
91
|
+
},
|
|
92
|
+
publicKey: {
|
|
93
|
+
state: tlsUsable ? scalarComparison(left.compact.tls.spkiSha256, right.compact.tls.spkiSha256) : 'unavailable',
|
|
94
|
+
leftSha256: left.compact.tls.spkiSha256,
|
|
95
|
+
rightSha256: right.compact.tls.spkiSha256,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
limitations: [
|
|
99
|
+
'This comparison is offline and uses only bounded static page, favicon, technology, and TLS evidence retained in two saved deep lookups.',
|
|
100
|
+
'A partial, missing, or unavailable component is never interpreted as an observed difference or absence.',
|
|
101
|
+
'Matching components are investigative relationships and do not establish copying, common ownership, control, intent, safety, or maliciousness.',
|
|
102
|
+
'Static page collection does not execute JavaScript and can differ from a rendered browser capture.',
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function formatCliPageComparison(document) {
|
|
107
|
+
const lines = [
|
|
108
|
+
'Static page comparison',
|
|
109
|
+
`Left ${document.left.domain}`,
|
|
110
|
+
`Right ${document.right.domain}`,
|
|
111
|
+
`Page evidence ${document.page.partial ? 'Partial' : 'Complete'}`,
|
|
112
|
+
'',
|
|
113
|
+
'Page identity',
|
|
114
|
+
];
|
|
115
|
+
for (const component of document.page.components) {
|
|
116
|
+
lines.push(`${component.label.padEnd(18)} ${component.outcome}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('', 'Technology', `Relationship ${document.technology.state}`, `Shared ${document.technology.sharedIds.join(', ') || 'None observed'}`, `Left ${document.technology.leftIds.join(', ') || 'Unavailable'}`, `Right ${document.technology.rightIds.join(', ') || 'Unavailable'}`, '', 'TLS', `Issuer ${document.tls.issuer.state}`, `Public key ${document.tls.publicKey.state}`, '', 'Limitations:');
|
|
119
|
+
for (const limitation of document.limitations)
|
|
120
|
+
lines.push(` - ${limitation}`);
|
|
121
|
+
return `${lines.join('\n')}\n`;
|
|
122
|
+
}
|
|
123
|
+
export { buildCliPageComparison, formatCliPageComparison };
|
package/cli/posture.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CliUsageError } from "./arguments.mjs";
|
|
2
|
+
const MAX_POSTURE_SELECTORS = 10;
|
|
3
|
+
function normalizePostureSelectors(raw, normalizeSelectors) {
|
|
4
|
+
if (raw === null || raw === undefined || raw === '')
|
|
5
|
+
return [];
|
|
6
|
+
const tokens = String(raw).split(',').map((value) => value.trim());
|
|
7
|
+
if (tokens.some((value) => !value)) {
|
|
8
|
+
throw new CliUsageError('--selectors cannot contain empty entries.');
|
|
9
|
+
}
|
|
10
|
+
const canonical = [...new Set(tokens.map((value) => value.toLowerCase().replace(/^\.+|\.+$/g, '')))];
|
|
11
|
+
if (canonical.length > MAX_POSTURE_SELECTORS) {
|
|
12
|
+
throw new CliUsageError(`--selectors supports at most ${MAX_POSTURE_SELECTORS} unique selectors.`);
|
|
13
|
+
}
|
|
14
|
+
const normalized = normalizeSelectors(tokens);
|
|
15
|
+
if (normalized.length !== canonical.length || normalized.some((value, index) => value !== canonical[index])) {
|
|
16
|
+
throw new CliUsageError('--selectors contains an invalid DKIM selector.');
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
export { MAX_POSTURE_SELECTORS, normalizePostureSelectors };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const CLI_PROGRESS_EVENT_SCHEMA = 'whoisleuth.cli.progress';
|
|
2
|
+
export const CLI_PROGRESS_EVENT_VERSION = 1;
|
|
3
|
+
const MAX_CLI_PROGRESS_EVENTS = 2_100;
|
|
4
|
+
const MAX_CLI_PROGRESS_EVENT_DETAIL_LENGTH = 120;
|
|
5
|
+
function text(value) {
|
|
6
|
+
if (typeof value !== 'string')
|
|
7
|
+
return null;
|
|
8
|
+
const normalized = value
|
|
9
|
+
.replace(/[\x00-\x1f\x7f]+/gu, ' ')
|
|
10
|
+
.replace(/\s+/gu, ' ')
|
|
11
|
+
.trim()
|
|
12
|
+
.slice(0, MAX_CLI_PROGRESS_EVENT_DETAIL_LENGTH);
|
|
13
|
+
return normalized || null;
|
|
14
|
+
}
|
|
15
|
+
function integer(value, minimum, maximum) {
|
|
16
|
+
return Number.isSafeInteger(value) && Number(value) >= minimum && Number(value) <= maximum
|
|
17
|
+
? Number(value)
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
function createCliProgressEvents(stream, options) {
|
|
21
|
+
const enabled = options.enabled === true && Boolean(stream);
|
|
22
|
+
const now = options.now || (() => new Date().toISOString());
|
|
23
|
+
let sequence = 0;
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
enabled,
|
|
26
|
+
emit(fields) {
|
|
27
|
+
if (!enabled || !stream || sequence >= MAX_CLI_PROGRESS_EVENTS)
|
|
28
|
+
return;
|
|
29
|
+
const source = text(fields.source);
|
|
30
|
+
const state = text(fields.state);
|
|
31
|
+
const reason = text(fields.reason);
|
|
32
|
+
const index = integer(fields.index, 0, 10_000);
|
|
33
|
+
const exitCode = integer(fields.exitCode, 0, 255);
|
|
34
|
+
const document = {
|
|
35
|
+
schema: CLI_PROGRESS_EVENT_SCHEMA,
|
|
36
|
+
version: CLI_PROGRESS_EVENT_VERSION,
|
|
37
|
+
sequence,
|
|
38
|
+
generatedAt: now(),
|
|
39
|
+
command: options.command,
|
|
40
|
+
event: fields.event,
|
|
41
|
+
...(source ? { source } : {}),
|
|
42
|
+
...(state ? { state } : {}),
|
|
43
|
+
...(reason ? { reason } : {}),
|
|
44
|
+
...(index !== null ? { index } : {}),
|
|
45
|
+
...(typeof fields.ok === 'boolean' ? { ok: fields.ok } : {}),
|
|
46
|
+
...(exitCode !== null ? { exitCode } : {}),
|
|
47
|
+
};
|
|
48
|
+
stream.write(`${JSON.stringify(document)}\n`);
|
|
49
|
+
sequence += 1;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export { createCliProgressEvents };
|
package/cli/progress.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { terminalPresentation, tone, } from "./terminal-presentation.mjs";
|
|
2
|
+
const PROGRESS_FRAMES = Object.freeze(['◐', '◓', '◑', '◒']);
|
|
3
|
+
const MAX_PROGRESS_MESSAGE_LENGTH = 160;
|
|
4
|
+
function safeProgressMessage(value) {
|
|
5
|
+
return String(value || 'Working')
|
|
6
|
+
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
7
|
+
.replace(/\s+/gu, ' ')
|
|
8
|
+
.trim()
|
|
9
|
+
.slice(0, MAX_PROGRESS_MESSAGE_LENGTH) || 'Working';
|
|
10
|
+
}
|
|
11
|
+
function createTerminalProgress(stream, options) {
|
|
12
|
+
const environment = options.environment || process.env;
|
|
13
|
+
const presentation = terminalPresentation(stream, options.color, environment);
|
|
14
|
+
const enabled = options.enabled
|
|
15
|
+
&& presentation.interactive
|
|
16
|
+
&& environment.WHOISLEUTH_NO_PROGRESS !== '1';
|
|
17
|
+
const now = options.now || Date.now;
|
|
18
|
+
let startedAt = 0;
|
|
19
|
+
let frame = 0;
|
|
20
|
+
let message = '';
|
|
21
|
+
let timer = null;
|
|
22
|
+
function render() {
|
|
23
|
+
if (!enabled || !stream)
|
|
24
|
+
return;
|
|
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);
|
|
28
|
+
stream.write(`\r\u001b[2K${marker} ${message} · ${elapsed}`);
|
|
29
|
+
frame += 1;
|
|
30
|
+
}
|
|
31
|
+
function start(nextMessage) {
|
|
32
|
+
if (!enabled || timer)
|
|
33
|
+
return;
|
|
34
|
+
startedAt = now();
|
|
35
|
+
message = safeProgressMessage(nextMessage);
|
|
36
|
+
render();
|
|
37
|
+
timer = setInterval(render, 250);
|
|
38
|
+
timer.unref?.();
|
|
39
|
+
}
|
|
40
|
+
function update(nextMessage) {
|
|
41
|
+
if (!enabled)
|
|
42
|
+
return;
|
|
43
|
+
message = safeProgressMessage(nextMessage);
|
|
44
|
+
render();
|
|
45
|
+
}
|
|
46
|
+
function stop() {
|
|
47
|
+
if (!enabled || !stream)
|
|
48
|
+
return;
|
|
49
|
+
if (timer)
|
|
50
|
+
clearInterval(timer);
|
|
51
|
+
timer = null;
|
|
52
|
+
stream.write('\r\u001b[2K');
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze({ enabled, start, update, stop });
|
|
55
|
+
}
|
|
56
|
+
export { createTerminalProgress, safeProgressMessage, };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { registryStandardsCoverageSnapshot, } from "../lib/registry-capabilities.mjs";
|
|
2
|
+
const REGISTRY_SUPPORT_SCHEMA = 'whoisleuth.cli.registry-support';
|
|
3
|
+
const REGISTRY_SUPPORT_SCHEMA_VERSION = 2;
|
|
4
|
+
const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
|
|
5
|
+
const MAX_REGISTRY_SUPPORT_TEXT_LENGTH = 2048;
|
|
6
|
+
const MAX_REGISTRY_SUPPORT_REFERENCES = 20;
|
|
7
|
+
const MAX_REGISTRY_SUPPORT_REFERENCE_LENGTH = 2048;
|
|
8
|
+
function boundedText(value, fallback = 'unknown', maximum = MAX_REGISTRY_SUPPORT_TEXT_LENGTH) {
|
|
9
|
+
if (typeof value !== 'string')
|
|
10
|
+
return fallback;
|
|
11
|
+
const normalized = value
|
|
12
|
+
.replace(/[\x00-\x1f\x7f]+/g, ' ')
|
|
13
|
+
.replace(/\s+/g, ' ')
|
|
14
|
+
.trim()
|
|
15
|
+
.slice(0, maximum);
|
|
16
|
+
return normalized || fallback;
|
|
17
|
+
}
|
|
18
|
+
function boundedList(values, kind) {
|
|
19
|
+
if (!Array.isArray(values))
|
|
20
|
+
return [];
|
|
21
|
+
const output = [];
|
|
22
|
+
for (const value of values.slice(0, MAX_REGISTRY_SUPPORT_REFERENCES)) {
|
|
23
|
+
const text = boundedText(value, '', MAX_REGISTRY_SUPPORT_REFERENCE_LENGTH);
|
|
24
|
+
if (!text)
|
|
25
|
+
continue;
|
|
26
|
+
if (kind === 'token' && !/^[a-z0-9][a-z0-9_-]{0,127}$/i.test(text))
|
|
27
|
+
continue;
|
|
28
|
+
if (kind === 'file' && (text.startsWith('/') || text.includes('..') || !/^[a-z0-9_./-]+$/i.test(text)))
|
|
29
|
+
continue;
|
|
30
|
+
if (kind === 'url') {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(text);
|
|
33
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!output.includes(text))
|
|
41
|
+
output.push(text);
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
function boundedCount(value, maximum = 100000) {
|
|
46
|
+
return Number.isSafeInteger(value) && Number(value) >= 0
|
|
47
|
+
? Math.min(Number(value), maximum)
|
|
48
|
+
: 0;
|
|
49
|
+
}
|
|
50
|
+
function projectStandardsCoverage(snapshot) {
|
|
51
|
+
const counts = snapshot?.counts || {};
|
|
52
|
+
return {
|
|
53
|
+
schema: REGISTRY_STANDARDS_COVERAGE_SCHEMA,
|
|
54
|
+
version: boundedCount(snapshot?.version, 1000),
|
|
55
|
+
verifiedAt: boundedText(snapshot?.verifiedAt, 'unknown', 32),
|
|
56
|
+
rootZoneVersion: boundedText(snapshot?.sources?.rootZoneVersion, 'unknown', 64),
|
|
57
|
+
rdapBootstrapPublication: boundedText(snapshot?.sources?.rdapBootstrapPublication, 'unknown', 64),
|
|
58
|
+
genericAndRestricted: {
|
|
59
|
+
total: boundedCount(counts.generic) + boundedCount(counts.genericRestricted),
|
|
60
|
+
rdapCovered: boundedCount(counts.genericAndRestrictedRdapCovered),
|
|
61
|
+
},
|
|
62
|
+
sponsored: {
|
|
63
|
+
total: boundedCount(counts.sponsored),
|
|
64
|
+
rdapCovered: boundedCount(counts.sponsoredRdapCovered),
|
|
65
|
+
},
|
|
66
|
+
infrastructure: {
|
|
67
|
+
total: boundedCount(counts.infrastructure),
|
|
68
|
+
rdapCovered: boundedCount(counts.infrastructureRdapCovered),
|
|
69
|
+
},
|
|
70
|
+
interpretation: boundedText(snapshot?.interpretation),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function buildRegistrySupportDocument(requestedInput, capability, catalogueVersion, generatedAt = new Date().toISOString(), standardsSnapshot = registryStandardsCoverageSnapshot()) {
|
|
74
|
+
const suffix = boundedText(capability.suffixes?.[0], '', 63).toLowerCase();
|
|
75
|
+
if (!suffix || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(suffix)) {
|
|
76
|
+
throw new TypeError('Registry capability did not provide a valid suffix.');
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
schema: REGISTRY_SUPPORT_SCHEMA,
|
|
80
|
+
version: REGISTRY_SUPPORT_SCHEMA_VERSION,
|
|
81
|
+
generatedAt: boundedText(generatedAt, '', 64),
|
|
82
|
+
requestedInput: boundedText(requestedInput, '', 253),
|
|
83
|
+
suffix,
|
|
84
|
+
catalogueVersion: Number.isSafeInteger(catalogueVersion) && catalogueVersion > 0 ? catalogueVersion : 0,
|
|
85
|
+
standardsCoverage: projectStandardsCoverage(standardsSnapshot),
|
|
86
|
+
profile: {
|
|
87
|
+
id: boundedText(capability.id, 'unknown', 128),
|
|
88
|
+
explicitSuffixProfile: capability.explicitSuffixProfile === true,
|
|
89
|
+
registryClass: boundedText(capability.registryClass, 'unknown', 64),
|
|
90
|
+
coverageState: boundedText(capability.coverageState, 'unknown', 64),
|
|
91
|
+
rdap: {
|
|
92
|
+
discovery: boundedText(capability.rdapDiscovery, 'unknown', 64),
|
|
93
|
+
accessProfile: boundedText(capability.rdapAccessProfile, 'unknown', 128),
|
|
94
|
+
},
|
|
95
|
+
whois: {
|
|
96
|
+
discovery: boundedText(capability.whoisDiscovery, 'unknown', 64),
|
|
97
|
+
accessProfile: boundedText(capability.whoisAccessProfile, 'unknown', 128),
|
|
98
|
+
queryProfile: boundedText(capability.whoisQueryProfile, 'unknown', 128),
|
|
99
|
+
queryScope: boundedText(capability.whoisQueryScope, 'unknown', 64),
|
|
100
|
+
encodingProfile: boundedText(capability.whoisEncodingProfile, 'unknown', 64),
|
|
101
|
+
parserProfile: boundedText(capability.whoisParserProfile, 'unknown', 128),
|
|
102
|
+
},
|
|
103
|
+
fallbackProfile: capability.fallbackProfile
|
|
104
|
+
? boundedText(capability.fallbackProfile, 'unknown', 128)
|
|
105
|
+
: null,
|
|
106
|
+
},
|
|
107
|
+
verification: {
|
|
108
|
+
fixtureScenarios: boundedList(capability.fixtureScenarios, 'token'),
|
|
109
|
+
files: boundedList(capability.verificationFiles, 'file'),
|
|
110
|
+
documentationUrls: boundedList(capability.documentationUrls, 'url'),
|
|
111
|
+
},
|
|
112
|
+
limitation: boundedText(capability.limitation),
|
|
113
|
+
interpretation: {
|
|
114
|
+
authority: 'context_only',
|
|
115
|
+
liveReachability: 'not_tested',
|
|
116
|
+
statement: 'Catalogue coverage does not test current live reachability or decide registration, availability, ownership, safety, or maliciousness.',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export { MAX_REGISTRY_SUPPORT_REFERENCE_LENGTH, MAX_REGISTRY_SUPPORT_REFERENCES, MAX_REGISTRY_SUPPORT_TEXT_LENGTH, REGISTRY_STANDARDS_COVERAGE_SCHEMA, REGISTRY_SUPPORT_SCHEMA, REGISTRY_SUPPORT_SCHEMA_VERSION, buildRegistrySupportDocument, projectStandardsCoverage, };
|