@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
package/cli/http.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MAX_HTTP_CLI_DETAIL_LENGTH = 300;
|
|
2
|
+
const PROBE_STATUSES = new Set(['fetched', 'responded', 'inconclusive']);
|
|
3
|
+
function boundedDetail(value) {
|
|
4
|
+
return String(value || '')
|
|
5
|
+
.replace(/[\x00-\x1f\x7f]+/g, ' ')
|
|
6
|
+
.replace(/\s+/g, ' ')
|
|
7
|
+
.trim()
|
|
8
|
+
.slice(0, MAX_HTTP_CLI_DETAIL_LENGTH) || null;
|
|
9
|
+
}
|
|
10
|
+
function buildHttpProbeResult(domain, probe) {
|
|
11
|
+
const input = probe && typeof probe === 'object' && !Array.isArray(probe)
|
|
12
|
+
? probe
|
|
13
|
+
: {};
|
|
14
|
+
const status = typeof input.status === 'string' && PROBE_STATUSES.has(input.status)
|
|
15
|
+
? input.status
|
|
16
|
+
: 'inconclusive';
|
|
17
|
+
return {
|
|
18
|
+
domain,
|
|
19
|
+
probeStatus: status,
|
|
20
|
+
activityStatus: status === 'fetched' || status === 'responded' ? 'active' : 'unreachable',
|
|
21
|
+
detail: boundedDetail(input.detail),
|
|
22
|
+
http: input.http && typeof input.http === 'object' && !Array.isArray(input.http)
|
|
23
|
+
? input.http
|
|
24
|
+
: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export { MAX_HTTP_CLI_DETAIL_LENGTH, buildHttpProbeResult };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { abortable } from "../lib/abort.mjs";
|
|
2
|
+
import { classifyQuery } from "../lib/classify.mjs";
|
|
3
|
+
import { runUnifiedLookup } from "../lib/lookup.mjs";
|
|
4
|
+
import { CliUsageError, boundedCliErrorMessage } from "./errors.mjs";
|
|
5
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
|
+
import { buildCliEvidenceExport } from "./export-evidence.mjs";
|
|
7
|
+
import { formatLookupEvidenceHtml } from "./formatters/html.mjs";
|
|
8
|
+
import { buildCliLookupDocument, formatJsonDocument } from "./formatters/json.mjs";
|
|
9
|
+
import { formatLookupEvidenceMarkdown } from "./formatters/markdown.mjs";
|
|
10
|
+
import { formatTerminalLookup } from "./formatters/terminal.mjs";
|
|
11
|
+
import { createCliProgressEvents } from "./progress-events.mjs";
|
|
12
|
+
import { lookupStrictExitFindings } from "./strict-exit.mjs";
|
|
13
|
+
async function runLookupCommand(args, dependencies, context) {
|
|
14
|
+
const eventProgress = createCliProgressEvents(context.stderr, {
|
|
15
|
+
command: 'lookup',
|
|
16
|
+
enabled: args.events,
|
|
17
|
+
...(dependencies.now ? { now: dependencies.now } : {}),
|
|
18
|
+
});
|
|
19
|
+
context.setEventProgress(eventProgress);
|
|
20
|
+
eventProgress.emit({ event: 'started' });
|
|
21
|
+
const query = args.query || await context.readSingleInput();
|
|
22
|
+
if (!query)
|
|
23
|
+
throw new CliUsageError('lookup requires one domain, IP address, or ASN as an argument or on stdin.');
|
|
24
|
+
const classify = dependencies.classifyQuery || classifyQuery;
|
|
25
|
+
const executeLookup = dependencies.runUnifiedLookup || runUnifiedLookup;
|
|
26
|
+
let classified;
|
|
27
|
+
try {
|
|
28
|
+
classified = classify(query);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid query'));
|
|
32
|
+
}
|
|
33
|
+
if ((args.output === 'markdown' || args.output === 'html') && classified.type !== 'domain') {
|
|
34
|
+
throw new CliUsageError('Markdown and HTML reports support domain lookups only.');
|
|
35
|
+
}
|
|
36
|
+
const indicator = context.beginProgress(args.deep
|
|
37
|
+
? 'Collecting deep Lookup evidence'
|
|
38
|
+
: 'Collecting registration evidence');
|
|
39
|
+
let settledSources = 0;
|
|
40
|
+
let result;
|
|
41
|
+
try {
|
|
42
|
+
result = await abortable(() => executeLookup(classified, args.deep
|
|
43
|
+
? {
|
|
44
|
+
fast: false,
|
|
45
|
+
compact: false,
|
|
46
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
47
|
+
onSourceSettled: (settlement) => {
|
|
48
|
+
settledSources += 1;
|
|
49
|
+
indicator.update(`Collected ${settledSources} source${settledSources === 1 ? '' : 's'} · ${settlement.source.replaceAll('_', ' ')} ${settlement.state}`);
|
|
50
|
+
eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
: {
|
|
54
|
+
fast: true,
|
|
55
|
+
compact: false,
|
|
56
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
57
|
+
}), dependencies.signal);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
context.endProgress();
|
|
61
|
+
}
|
|
62
|
+
const now = context.now();
|
|
63
|
+
const document = buildCliLookupDocument(query, classified, result, now, args.deep ? 'deep' : 'fast');
|
|
64
|
+
if (!args.quiet) {
|
|
65
|
+
if (args.output === 'json')
|
|
66
|
+
context.writeStdout(formatJsonDocument(document));
|
|
67
|
+
else if (args.output === 'markdown' || args.output === 'html') {
|
|
68
|
+
const loadEvidence = dependencies.loadEvidenceExport || (() => import("../lib/evidence-export.mjs"));
|
|
69
|
+
const evidenceModule = await loadEvidence();
|
|
70
|
+
const report = buildCliEvidenceExport(JSON.stringify(document), evidenceModule, now);
|
|
71
|
+
context.writeStdout(args.output === 'markdown'
|
|
72
|
+
? formatLookupEvidenceMarkdown(report)
|
|
73
|
+
: formatLookupEvidenceHtml(report));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
context.writeStdout(context.terminal(formatTerminalLookup(document, { detail: args.detail }), args.color));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const strictFindings = args.strictExit ? lookupStrictExitFindings(document) : [];
|
|
80
|
+
const exitCode = strictFindings.length ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
81
|
+
if (strictFindings.length && !args.events) {
|
|
82
|
+
context.writeStderr(`Strict exit: ${strictFindings.length} requested source state${strictFindings.length === 1 ? '' : 's'} were incomplete.\n`);
|
|
83
|
+
}
|
|
84
|
+
eventProgress.emit({ event: 'completed', exitCode });
|
|
85
|
+
return exitCode;
|
|
86
|
+
}
|
|
87
|
+
export { runLookupCommand };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { buildBulkComparisonEvidence } from "../lib/bulk-comparison-evidence.mjs";
|
|
2
|
+
import { buildBulkDomainComparison, } from "../frontend/src/lib/analysis/bulk-domain-comparison.js";
|
|
3
|
+
import { relationshipObservation } from "../frontend/src/lib/analysis/relationship-evidence.js";
|
|
4
|
+
import { CliUsageError } from "./errors.mjs";
|
|
5
|
+
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
6
|
+
export const CLI_LOOKUP_DIFF_SCHEMA = 'whoisleuth.cli.lookup-diff';
|
|
7
|
+
export const CLI_LOOKUP_DIFF_VERSION = 1;
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
function text(value, maximum = 300) {
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return null;
|
|
14
|
+
const normalized = value.replace(/[\x00-\x1f\x7f]+/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum);
|
|
15
|
+
return normalized || null;
|
|
16
|
+
}
|
|
17
|
+
function stringList(value, maximum = 100) {
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
return [];
|
|
20
|
+
return [...new Set(value.slice(0, maximum * 2).flatMap((candidate) => {
|
|
21
|
+
const normalized = text(candidate, 253)?.toLowerCase().replace(/\.$/u, '');
|
|
22
|
+
return normalized ? [normalized] : [];
|
|
23
|
+
}))].slice(0, maximum);
|
|
24
|
+
}
|
|
25
|
+
function lifecycleValue(parsed, field) {
|
|
26
|
+
const lifecycle = record(parsed.lifecycle);
|
|
27
|
+
return text(lifecycle[`${field}Iso`] ?? lifecycle[field] ?? parsed[`${field}Iso`] ?? parsed[field], 64);
|
|
28
|
+
}
|
|
29
|
+
function registrarLabel(parsed) {
|
|
30
|
+
const registrar = parsed.registrar;
|
|
31
|
+
if (typeof registrar === 'string')
|
|
32
|
+
return text(registrar);
|
|
33
|
+
const source = record(registrar);
|
|
34
|
+
return text(source.name ?? source.org ?? source.handle);
|
|
35
|
+
}
|
|
36
|
+
function sourceState(value) {
|
|
37
|
+
const normalized = text(value, 40)?.toLowerCase() || 'unavailable';
|
|
38
|
+
if (normalized === 'success' || normalized === 'complete')
|
|
39
|
+
return 'complete';
|
|
40
|
+
if (normalized === 'not_found')
|
|
41
|
+
return 'not_found';
|
|
42
|
+
if (normalized === 'partial')
|
|
43
|
+
return 'partial';
|
|
44
|
+
if (normalized === 'skipped' || normalized === 'disabled' || normalized === 'not_applicable')
|
|
45
|
+
return 'skipped';
|
|
46
|
+
if (normalized === 'unsupported')
|
|
47
|
+
return 'unsupported';
|
|
48
|
+
if (normalized === 'error' || normalized === 'failed' || normalized === 'timeout' || normalized === 'rate_limited')
|
|
49
|
+
return 'error';
|
|
50
|
+
return 'unavailable';
|
|
51
|
+
}
|
|
52
|
+
function sourceCoverage(document, availability) {
|
|
53
|
+
const diagnostics = record(document.diagnostics);
|
|
54
|
+
const entries = [
|
|
55
|
+
['rdap', record(diagnostics.rdap).status],
|
|
56
|
+
['whois', record(diagnostics.whois).status],
|
|
57
|
+
['availability', record(diagnostics.availability).status],
|
|
58
|
+
['dns', record(availability.dns).status],
|
|
59
|
+
['http', record(availability.http).status],
|
|
60
|
+
['tls', record(availability.tls).status],
|
|
61
|
+
];
|
|
62
|
+
return entries.map(([source, status]) => ({ source, state: sourceState(status) }));
|
|
63
|
+
}
|
|
64
|
+
function lookupComparisonInput(document) {
|
|
65
|
+
const availability = record(document.availability);
|
|
66
|
+
const rdapParsed = record(record(document.rdap).parsed);
|
|
67
|
+
const whoisParsed = record(record(document.whois).parsed);
|
|
68
|
+
const preferredRegistry = Object.keys(rdapParsed).length ? rdapParsed : whoisParsed;
|
|
69
|
+
const relationship = relationshipObservation(availability);
|
|
70
|
+
const dns = record(availability.dns);
|
|
71
|
+
const dnsRecords = record(dns.records);
|
|
72
|
+
const comparisonEvidence = buildBulkComparisonEvidence(availability);
|
|
73
|
+
return {
|
|
74
|
+
domain: document.registrableDomain,
|
|
75
|
+
status: 'complete',
|
|
76
|
+
availability: text(availability.state, 40) || 'unknown',
|
|
77
|
+
confidence: text(availability.confidence, 40) || 'unknown',
|
|
78
|
+
registrar: registrarLabel(preferredRegistry) || '—',
|
|
79
|
+
activity: text(availability.activityStatus) || '—',
|
|
80
|
+
risk: null,
|
|
81
|
+
opportunity: null,
|
|
82
|
+
mutationTypes: [],
|
|
83
|
+
trusted: null,
|
|
84
|
+
error: '',
|
|
85
|
+
scanDepth: document.mode,
|
|
86
|
+
createdDate: lifecycleValue(preferredRegistry, 'createdDate'),
|
|
87
|
+
expiryDate: lifecycleValue(preferredRegistry, 'expiryDate'),
|
|
88
|
+
nameservers: relationship.nameservers.length ? relationship.nameservers : stringList(preferredRegistry.nameservers, 20),
|
|
89
|
+
hasMx: typeof availability.hasMx === 'boolean' ? availability.hasMx : null,
|
|
90
|
+
hasNullMx: typeof availability.hasNullMx === 'boolean' ? availability.hasNullMx : null,
|
|
91
|
+
hasSpf: typeof availability.hasSpf === 'boolean' ? availability.hasSpf : null,
|
|
92
|
+
hasDmarc: typeof availability.hasDmarc === 'boolean' ? availability.hasDmarc : null,
|
|
93
|
+
activityStatus: text(availability.activityStatus, 40),
|
|
94
|
+
pageTitle: text(availability.pageTitle),
|
|
95
|
+
faviconHash: relationship.faviconHash,
|
|
96
|
+
faviconPHash: relationship.faviconPHash,
|
|
97
|
+
faviconMatch: false,
|
|
98
|
+
faviconNearMatch: false,
|
|
99
|
+
reusesOfficialAssets: false,
|
|
100
|
+
hasPasswordField: record(record(availability.credentialSurfaceProfile).inputs).categories
|
|
101
|
+
? Number(record(record(record(availability.credentialSurfaceProfile).inputs).categories).password) > 0
|
|
102
|
+
: false,
|
|
103
|
+
hasExternalFormAction: null,
|
|
104
|
+
phishingLanguageMatch: null,
|
|
105
|
+
riskModelVersion: null,
|
|
106
|
+
riskFactors: [],
|
|
107
|
+
dns: {
|
|
108
|
+
status: text(dns.status, 40),
|
|
109
|
+
records: {
|
|
110
|
+
a: stringList(dnsRecords.a),
|
|
111
|
+
aaaa: stringList(dnsRecords.aaaa),
|
|
112
|
+
cname: stringList(dnsRecords.cname),
|
|
113
|
+
caa: Array.isArray(dnsRecords.caa) ? dnsRecords.caa.slice(0, 100) : [],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
dnssec: text(preferredRegistry.dnssec, 40),
|
|
117
|
+
comparisonEvidence,
|
|
118
|
+
relationship,
|
|
119
|
+
sourceCoverage: sourceCoverage(document, availability),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOString()) {
|
|
123
|
+
const left = parseSavedLookupDocument(leftText, { label: 'Left lookup input' });
|
|
124
|
+
const right = parseSavedLookupDocument(rightText, { label: 'Right lookup input' });
|
|
125
|
+
if (left.registrableDomain === right.registrableDomain) {
|
|
126
|
+
throw new CliUsageError('Lookup diff requires documents for two different domains.');
|
|
127
|
+
}
|
|
128
|
+
const comparison = buildBulkDomainComparison(lookupComparisonInput(left), lookupComparisonInput(right), null);
|
|
129
|
+
if (!comparison)
|
|
130
|
+
throw new CliUsageError('Lookup documents could not be compared.');
|
|
131
|
+
return {
|
|
132
|
+
schema: CLI_LOOKUP_DIFF_SCHEMA,
|
|
133
|
+
version: CLI_LOOKUP_DIFF_VERSION,
|
|
134
|
+
generatedAt,
|
|
135
|
+
left: { domain: left.registrableDomain, generatedAt: left.generatedAt, mode: left.mode },
|
|
136
|
+
right: { domain: right.registrableDomain, generatedAt: right.generatedAt, mode: right.mode },
|
|
137
|
+
comparison,
|
|
138
|
+
limitations: [
|
|
139
|
+
'This command compares bounded observations already present in two saved Lookup documents and makes no network request.',
|
|
140
|
+
'A missing value remains distinct from unavailable collection and from an observed difference.',
|
|
141
|
+
'Shared infrastructure or matching values do not establish common ownership, control, intent, safety, or maliciousness.',
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function formatCliLookupDiff(document) {
|
|
146
|
+
const rows = document.comparison.rows.filter((row) => row.state !== 'equal');
|
|
147
|
+
const output = [
|
|
148
|
+
'Domain evidence diff',
|
|
149
|
+
`Left ${document.left.domain}`,
|
|
150
|
+
`Left observed ${document.left.generatedAt}`,
|
|
151
|
+
`Right ${document.right.domain}`,
|
|
152
|
+
`Right observed ${document.right.generatedAt}`,
|
|
153
|
+
`Changed ${document.comparison.counts.different + document.comparison.counts.conflicting}`,
|
|
154
|
+
`One-sided ${document.comparison.counts.missing}`,
|
|
155
|
+
`Unavailable ${document.comparison.counts.unavailable + document.comparison.counts.not_recorded}`,
|
|
156
|
+
'',
|
|
157
|
+
];
|
|
158
|
+
if (!rows.length)
|
|
159
|
+
output.push('No bounded differences were observed in the comparable fields.');
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
output.push(`${row.label} [${row.state.replaceAll('_', ' ')}]`);
|
|
162
|
+
output.push(` Left: ${row.left}`);
|
|
163
|
+
output.push(` Right: ${row.right}`);
|
|
164
|
+
output.push(` Source: ${row.source}`);
|
|
165
|
+
}
|
|
166
|
+
output.push('', 'Limitations:');
|
|
167
|
+
for (const limitation of document.limitations)
|
|
168
|
+
output.push(` - ${limitation}`);
|
|
169
|
+
return `${output.join('\n')}\n`;
|
|
170
|
+
}
|
|
171
|
+
export { buildCliLookupDiff, formatCliLookupDiff };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { getDomain } from 'tldts';
|
|
3
|
+
import { CliUsageError } from "./errors.mjs";
|
|
4
|
+
export const CLI_MAIL_REVIEW_SCHEMA = 'whoisleuth.cli.mail-review';
|
|
5
|
+
export const CLI_MAIL_REVIEW_VERSION = 1;
|
|
6
|
+
export const MAX_MAIL_REVIEW_INPUT_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
export const MAX_MAIL_REVIEW_ROWS = 500;
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
function text(value, maximum = 500) {
|
|
12
|
+
return typeof value === 'string'
|
|
13
|
+
? value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
|
|
14
|
+
: '';
|
|
15
|
+
}
|
|
16
|
+
function boolean(value) {
|
|
17
|
+
return typeof value === 'boolean' ? value : null;
|
|
18
|
+
}
|
|
19
|
+
function mxHosts(availability, summary) {
|
|
20
|
+
const direct = Array.isArray(availability.mxHosts) ? availability.mxHosts : [];
|
|
21
|
+
const summarized = Array.isArray(summary.mx) ? summary.mx : [];
|
|
22
|
+
const output = new Set();
|
|
23
|
+
for (const candidate of [...direct, ...summarized].slice(0, 200)) {
|
|
24
|
+
const normalized = text(candidate, 300).replace(/^\d+\s+/u, '').replace(/\.$/u, '').toLowerCase();
|
|
25
|
+
if (normalized && normalized.length <= 253)
|
|
26
|
+
output.add(normalized);
|
|
27
|
+
if (output.size >= 50)
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
return [...output];
|
|
31
|
+
}
|
|
32
|
+
function providerDomains(hosts) {
|
|
33
|
+
return [...new Set(hosts.flatMap((host) => {
|
|
34
|
+
const labels = host.split('.').filter(Boolean);
|
|
35
|
+
const domain = getDomain(host, { allowPrivateDomains: true })
|
|
36
|
+
?? (labels.length >= 2 ? labels.slice(-2).join('.') : null);
|
|
37
|
+
return domain ? [domain.toLowerCase()] : [];
|
|
38
|
+
}))].sort().slice(0, 20);
|
|
39
|
+
}
|
|
40
|
+
function mailState(input) {
|
|
41
|
+
if (!['success', 'partial'].includes(input.dnsStatus))
|
|
42
|
+
return 'evidence_incomplete';
|
|
43
|
+
if (input.hasNullMx === true)
|
|
44
|
+
return 'null_mx';
|
|
45
|
+
if (input.hasMx === true) {
|
|
46
|
+
if (input.hasSpf === true && input.hasDmarc === true)
|
|
47
|
+
return 'authenticated_mail';
|
|
48
|
+
if (input.hasSpf === false || input.hasDmarc === false)
|
|
49
|
+
return 'mail_auth_gap';
|
|
50
|
+
return 'mail_auth_incomplete';
|
|
51
|
+
}
|
|
52
|
+
if (input.hasMx === false && input.hasNullMx === false)
|
|
53
|
+
return 'no_explicit_mx';
|
|
54
|
+
return 'evidence_incomplete';
|
|
55
|
+
}
|
|
56
|
+
function normalizeMailRow(value) {
|
|
57
|
+
const item = record(value);
|
|
58
|
+
if (item.ok !== true)
|
|
59
|
+
return null;
|
|
60
|
+
const availability = record(item.availability);
|
|
61
|
+
const summary = record(item.dnsSummary);
|
|
62
|
+
const domain = text(item.registrableDomain ?? item.query ?? availability.domain, 253).toLowerCase().replace(/\.$/u, '');
|
|
63
|
+
if (!domain || !domain.includes('.'))
|
|
64
|
+
return null;
|
|
65
|
+
const dns = record(availability.dns);
|
|
66
|
+
const dnsStatus = text(summary.status ?? dns.status, 40).toLowerCase() || 'unavailable';
|
|
67
|
+
const hosts = mxHosts(availability, summary);
|
|
68
|
+
const hasMx = boolean(availability.hasMx) ?? (hosts.length ? true : null);
|
|
69
|
+
const hasNullMx = boolean(availability.hasNullMx) ?? boolean(summary.hasNullMx);
|
|
70
|
+
const hasSpf = boolean(availability.hasSpf) ?? boolean(summary.hasSpf);
|
|
71
|
+
const hasDmarc = boolean(availability.hasDmarc) ?? boolean(summary.hasDmarc);
|
|
72
|
+
const state = mailState({ dnsStatus, hasMx, hasNullMx, hasSpf, hasDmarc });
|
|
73
|
+
return {
|
|
74
|
+
domain,
|
|
75
|
+
state,
|
|
76
|
+
dnsStatus,
|
|
77
|
+
hasMx,
|
|
78
|
+
hasNullMx,
|
|
79
|
+
hasSpf,
|
|
80
|
+
hasDmarc,
|
|
81
|
+
mxHosts: hosts,
|
|
82
|
+
providerDomains: providerDomains(hosts),
|
|
83
|
+
limitations: [
|
|
84
|
+
...(dnsStatus === 'partial' ? ['DNS evidence was partial.'] : []),
|
|
85
|
+
...(state === 'no_explicit_mx' ? ['No explicit MX is not equivalent to a null MX or proof that delivery is impossible.'] : []),
|
|
86
|
+
'Mail configuration does not establish use, control, intent, safety, or maliciousness.',
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseInput(textValue) {
|
|
91
|
+
if (typeof textValue !== 'string' || Buffer.byteLength(textValue, 'utf8') > MAX_MAIL_REVIEW_INPUT_BYTES) {
|
|
92
|
+
throw new CliUsageError(`Mail review input is limited to ${MAX_MAIL_REVIEW_INPUT_BYTES} bytes.`);
|
|
93
|
+
}
|
|
94
|
+
const textInput = textValue.replace(/^\uFEFF/u, '').trim();
|
|
95
|
+
if (!textInput)
|
|
96
|
+
throw new CliUsageError('Mail review requires one Bulk JSON or JSONL document.');
|
|
97
|
+
let values;
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(textInput);
|
|
100
|
+
const document = record(parsed);
|
|
101
|
+
if (document.schema === 'whoisleuth.cli.bulk' && document.version === 2 && Array.isArray(document.results)) {
|
|
102
|
+
values = document.results;
|
|
103
|
+
}
|
|
104
|
+
else if (document.schema === 'whoisleuth.cli.bulk.item' && document.version === 2) {
|
|
105
|
+
values = [document];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new CliUsageError('Mail review requires WHOISleuth Bulk JSON schema version 2.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof CliUsageError)
|
|
113
|
+
throw error;
|
|
114
|
+
try {
|
|
115
|
+
values = textInput.split(/\r?\n/u).filter(Boolean).map((line) => JSON.parse(line));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new CliUsageError('Mail review input must be valid WHOISleuth Bulk JSON or JSONL.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!values.length || values.length > MAX_MAIL_REVIEW_ROWS) {
|
|
122
|
+
throw new CliUsageError(`Mail review supports between 1 and ${MAX_MAIL_REVIEW_ROWS} Bulk rows.`);
|
|
123
|
+
}
|
|
124
|
+
const rows = values.map((value) => record(value));
|
|
125
|
+
if (rows.some((value) => value.schema !== 'whoisleuth.cli.bulk.item' || value.version !== 2)) {
|
|
126
|
+
throw new CliUsageError('Every Mail review row must use WHOISleuth Bulk item schema version 2.');
|
|
127
|
+
}
|
|
128
|
+
return rows;
|
|
129
|
+
}
|
|
130
|
+
function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
|
|
131
|
+
const inputRows = parseInput(textValue);
|
|
132
|
+
const rows = inputRows.map(normalizeMailRow).filter((row) => Boolean(row));
|
|
133
|
+
if (!rows.length)
|
|
134
|
+
throw new CliUsageError('Mail review input contains no completed domain results.');
|
|
135
|
+
const counts = {
|
|
136
|
+
authenticated_mail: 0,
|
|
137
|
+
evidence_incomplete: 0,
|
|
138
|
+
mail_auth_gap: 0,
|
|
139
|
+
mail_auth_incomplete: 0,
|
|
140
|
+
no_explicit_mx: 0,
|
|
141
|
+
null_mx: 0,
|
|
142
|
+
};
|
|
143
|
+
for (const row of rows)
|
|
144
|
+
counts[row.state] += 1;
|
|
145
|
+
const providerMap = new Map();
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
for (const provider of row.providerDomains) {
|
|
148
|
+
const domains = providerMap.get(provider) ?? [];
|
|
149
|
+
domains.push(row.domain);
|
|
150
|
+
providerMap.set(provider, domains);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const providerRelationships = [...providerMap.entries()]
|
|
154
|
+
.filter(([, domains]) => new Set(domains).size >= 2)
|
|
155
|
+
.map(([providerDomain, domains]) => ({
|
|
156
|
+
providerDomain,
|
|
157
|
+
domains: [...new Set(domains)].sort().slice(0, 100),
|
|
158
|
+
method: 'Shared registrable domain of an observed MX hostname',
|
|
159
|
+
limitation: 'Shared mail providers are common and do not establish common ownership, control, intent, safety, or maliciousness.',
|
|
160
|
+
}))
|
|
161
|
+
.sort((left, right) => left.providerDomain.localeCompare(right.providerDomain))
|
|
162
|
+
.slice(0, 100);
|
|
163
|
+
return {
|
|
164
|
+
schema: CLI_MAIL_REVIEW_SCHEMA,
|
|
165
|
+
version: CLI_MAIL_REVIEW_VERSION,
|
|
166
|
+
generatedAt,
|
|
167
|
+
counts,
|
|
168
|
+
rows,
|
|
169
|
+
providerRelationships,
|
|
170
|
+
limitations: [
|
|
171
|
+
'This review is passive and uses DNS evidence already retained in a WHOISleuth Bulk result; it makes no network request.',
|
|
172
|
+
'Null MX, no explicit MX, receiving mail, authentication gaps, and incomplete evidence remain separate states.',
|
|
173
|
+
'SMTP delivery, mailbox existence, catch-all behavior, banner collection, and message acceptance were not tested.',
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function formatCliMailReview(document) {
|
|
178
|
+
const lines = [
|
|
179
|
+
'Passive mail exposure review',
|
|
180
|
+
`Domains ${document.rows.length}`,
|
|
181
|
+
`Authenticated ${document.counts.authenticated_mail}`,
|
|
182
|
+
`Auth gaps ${document.counts.mail_auth_gap}`,
|
|
183
|
+
`Null MX ${document.counts.null_mx}`,
|
|
184
|
+
`No explicit MX ${document.counts.no_explicit_mx}`,
|
|
185
|
+
`Incomplete ${document.counts.evidence_incomplete + document.counts.mail_auth_incomplete}`,
|
|
186
|
+
'',
|
|
187
|
+
];
|
|
188
|
+
for (const row of document.rows) {
|
|
189
|
+
lines.push(`${row.domain} ${row.state.replaceAll('_', ' ')}`);
|
|
190
|
+
lines.push(` MX providers ${row.providerDomains.join(', ') || 'None observed'}`);
|
|
191
|
+
lines.push(` SPF / DMARC ${row.hasSpf === null ? 'unknown' : row.hasSpf ? 'observed' : 'not observed'} / ${row.hasDmarc === null ? 'unknown' : row.hasDmarc ? 'observed' : 'not observed'}`);
|
|
192
|
+
}
|
|
193
|
+
if (document.providerRelationships.length) {
|
|
194
|
+
lines.push('', 'Shared mail-provider relationships');
|
|
195
|
+
for (const relationship of document.providerRelationships) {
|
|
196
|
+
lines.push(`${relationship.providerDomain} ${relationship.domains.join(', ')}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
lines.push('', 'Limitations:');
|
|
200
|
+
for (const limitation of document.limitations)
|
|
201
|
+
lines.push(` - ${limitation}`);
|
|
202
|
+
return `${lines.join('\n')}\n`;
|
|
203
|
+
}
|
|
204
|
+
export { buildCliMailReview, formatCliMailReview, mailState, normalizeMailRow };
|
package/cli/manual.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function roffText(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/\\/gu, '\\\\')
|
|
4
|
+
.replace(/-/gu, '\\-')
|
|
5
|
+
.replace(/^([.'])/gmu, '\\&$1');
|
|
6
|
+
}
|
|
7
|
+
function buildCliManual(options) {
|
|
8
|
+
const commands = options.commands.map((command) => {
|
|
9
|
+
const detail = options.details[command];
|
|
10
|
+
return `.SS ${roffText(command)}\n${roffText(detail.description)}\n.PP\n.B ${roffText(options.usage[command])}\n.PP\nExample: ${roffText(detail.example)}\n.PP\nBoundary: ${roffText(detail.boundary)}`;
|
|
11
|
+
}).join('\n');
|
|
12
|
+
return `.TH WHOISLEUTH 1 "" "WHOISleuth ${roffText(options.version)}" "User Commands"
|
|
13
|
+
.SH NAME
|
|
14
|
+
whoisleuth \- source-aware domain investigation from the terminal
|
|
15
|
+
.SH SYNOPSIS
|
|
16
|
+
.B whoisleuth
|
|
17
|
+
command [options]
|
|
18
|
+
.SH DESCRIPTION
|
|
19
|
+
WHOISleuth performs bounded WHOIS, RDAP, DNS, HTTP, TLS, certificate-transparency, posture, lookalike, and offline evidence operations. Evidence sources remain separately attributed, and missing or failed collection is not converted into a claim of absence or safety.
|
|
20
|
+
.SH COMMANDS
|
|
21
|
+
${commands}
|
|
22
|
+
.SH OUTPUT
|
|
23
|
+
Human-readable output is the default. Versioned JSON and JSONL are available where documented. Diagnostics and optional progress events are written to standard error. Use --output with an optional --force flag for atomic private file output.
|
|
24
|
+
.SH EXIT STATUS
|
|
25
|
+
0 indicates command completion, 2 invalid usage, 3 a collection or comparison failure, 4 an explicitly detected partial result, 70 an internal bootstrap failure, and 130 analyst cancellation.
|
|
26
|
+
.SH PRIVACY
|
|
27
|
+
Network commands disclose the target to the directly queried upstream services. Offline commands do not make network requests. Output files are created with private permissions and are never uploaded by the CLI.
|
|
28
|
+
.SH LICENSE
|
|
29
|
+
AGPL-3.0-only. Copyright 2026 slicedearth.
|
|
30
|
+
.SH SEE ALSO
|
|
31
|
+
whoisleuth --help, whoisleuth doctor, whoisleuth completion
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
export { buildCliManual };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { fetchHomepage } from "../lib/availability.mjs";
|
|
2
|
+
import { searchCertificateTransparency } from "../lib/ct-search.mjs";
|
|
3
|
+
import { checkDomainPosture, normalizeAuditDomain, normalizeDkimSelectors } from "../lib/domain-posture.mjs";
|
|
4
|
+
import { collectTlsIntelligence, normalizeTlsHostname } from "../lib/tls-intelligence.mjs";
|
|
5
|
+
import { CliUsageError } from "./errors.mjs";
|
|
6
|
+
import { buildCliCtSearchDocument, buildCliHttpDocument, buildCliPostureDocument, buildCliTlsDocument, formatJsonDocument, } from "./formatters/json.mjs";
|
|
7
|
+
import { formatTerminalCtSearch, formatTerminalHttp, formatTerminalPosture, formatTerminalTls, } from "./formatters/terminal.mjs";
|
|
8
|
+
import { buildHttpProbeResult } from "./http.mjs";
|
|
9
|
+
import { normalizePostureSelectors } from "./posture.mjs";
|
|
10
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
11
|
+
async function runNetworkCommand(args, dependencies, context) {
|
|
12
|
+
if (args.action === 'ct-search') {
|
|
13
|
+
const keyword = args.keyword || await context.readSingleInput();
|
|
14
|
+
if (!keyword)
|
|
15
|
+
throw new CliUsageError('ct-search requires one keyword as an argument or on stdin.');
|
|
16
|
+
const search = dependencies.searchCertificateTransparency || searchCertificateTransparency;
|
|
17
|
+
const result = await context.withProgress('Searching certificate observations', () => search(keyword));
|
|
18
|
+
const document = buildCliCtSearchDocument(keyword, result, context.now());
|
|
19
|
+
if (!args.quiet) {
|
|
20
|
+
context.writeStdout(args.output === 'json'
|
|
21
|
+
? formatJsonDocument(document)
|
|
22
|
+
: context.terminal(formatTerminalCtSearch(document), args.color));
|
|
23
|
+
}
|
|
24
|
+
return EXIT_CODES.SUCCESS;
|
|
25
|
+
}
|
|
26
|
+
if (args.action === 'posture') {
|
|
27
|
+
const requestedDomain = args.domain || await context.readSingleInput();
|
|
28
|
+
if (!requestedDomain)
|
|
29
|
+
throw new CliUsageError('posture requires one domain as an argument or on stdin.');
|
|
30
|
+
const normalizeDomain = dependencies.normalizeAuditDomain || normalizeAuditDomain;
|
|
31
|
+
const domain = normalizeDomain(requestedDomain);
|
|
32
|
+
if (!domain)
|
|
33
|
+
throw new CliUsageError('posture requires a valid domain name.');
|
|
34
|
+
const normalizeSelectors = dependencies.normalizeDkimSelectors || normalizeDkimSelectors;
|
|
35
|
+
const dkimSelectors = normalizePostureSelectors(args.selectorText, normalizeSelectors);
|
|
36
|
+
const retiredDkimSelectors = normalizePostureSelectors(args.retiredSelectorText, normalizeSelectors)
|
|
37
|
+
.filter((selector) => !dkimSelectors.includes(selector))
|
|
38
|
+
.slice(0, Math.max(0, 10 - dkimSelectors.length));
|
|
39
|
+
const audit = dependencies.checkDomainPosture || checkDomainPosture;
|
|
40
|
+
const report = await context.withProgress('Collecting domain posture evidence', () => audit(domain, {
|
|
41
|
+
dkimSelectors,
|
|
42
|
+
retiredDkimSelectors,
|
|
43
|
+
mailProtectionProfile: args.mailProfile,
|
|
44
|
+
}));
|
|
45
|
+
const document = buildCliPostureDocument(requestedDomain, report, context.now());
|
|
46
|
+
if (!args.quiet) {
|
|
47
|
+
context.writeStdout(args.output === 'json'
|
|
48
|
+
? formatJsonDocument(document)
|
|
49
|
+
: context.terminal(formatTerminalPosture(document), args.color));
|
|
50
|
+
}
|
|
51
|
+
return EXIT_CODES.SUCCESS;
|
|
52
|
+
}
|
|
53
|
+
if (args.action === 'http') {
|
|
54
|
+
const requestedDomain = args.domain || await context.readSingleInput();
|
|
55
|
+
if (!requestedDomain)
|
|
56
|
+
throw new CliUsageError('http requires one domain as an argument or on stdin.');
|
|
57
|
+
const normalizeDomain = dependencies.normalizeAuditDomain || normalizeAuditDomain;
|
|
58
|
+
const domain = normalizeDomain(requestedDomain);
|
|
59
|
+
if (!domain)
|
|
60
|
+
throw new CliUsageError('http requires a valid domain name.');
|
|
61
|
+
const probe = dependencies.fetchHomepage || fetchHomepage;
|
|
62
|
+
const result = buildHttpProbeResult(domain, await context.withProgress('Inspecting the homepage request', () => probe(domain)));
|
|
63
|
+
const document = buildCliHttpDocument(requestedDomain, result, context.now());
|
|
64
|
+
if (!args.quiet) {
|
|
65
|
+
context.writeStdout(args.output === 'json'
|
|
66
|
+
? formatJsonDocument(document)
|
|
67
|
+
: context.terminal(formatTerminalHttp(document), args.color));
|
|
68
|
+
}
|
|
69
|
+
return EXIT_CODES.SUCCESS;
|
|
70
|
+
}
|
|
71
|
+
const requestedHostname = args.hostname || await context.readSingleInput();
|
|
72
|
+
if (!requestedHostname)
|
|
73
|
+
throw new CliUsageError('tls requires one hostname as an argument or on stdin.');
|
|
74
|
+
const normalizeHostname = dependencies.normalizeTlsHostname || normalizeTlsHostname;
|
|
75
|
+
const hostname = normalizeHostname(requestedHostname);
|
|
76
|
+
if (!hostname)
|
|
77
|
+
throw new CliUsageError('tls requires a valid DNS hostname, not an IP address.');
|
|
78
|
+
const collect = dependencies.collectTlsIntelligence || collectTlsIntelligence;
|
|
79
|
+
const result = await context.withProgress('Inspecting the current TLS connection', () => collect(hostname));
|
|
80
|
+
const document = buildCliTlsDocument(requestedHostname, result, context.now());
|
|
81
|
+
if (!args.quiet) {
|
|
82
|
+
context.writeStdout(args.output === 'json'
|
|
83
|
+
? formatJsonDocument(document)
|
|
84
|
+
: context.terminal(formatTerminalTls(document), args.color));
|
|
85
|
+
}
|
|
86
|
+
return EXIT_CODES.SUCCESS;
|
|
87
|
+
}
|
|
88
|
+
export { runNetworkCommand };
|