@slicedearth/whoisleuth-cli 2.0.0 → 2.1.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/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +17 -10
- package/docs/cli.md +5 -3
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/project-metadata.mjs +1 -1
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +3 -3
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +227 -63
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/brand-profile-model.mjs +1 -1
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
package/cli/saved-lookup.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { isSafeJsonObjectKey, scanBoundedJson, } from "../lib/bounded-json.mjs";
|
|
|
5
5
|
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
6
6
|
import { CliUsageError } from "./arguments.mjs";
|
|
7
7
|
import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../lib/homepage-metadata-contract.mjs";
|
|
8
|
+
import { registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "../lib/registrar-standing-contract.mjs";
|
|
8
9
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
10
|
import { CLI_LOOKUP_SCHEMA as SAVED_LOOKUP_SCHEMA, CLI_LOOKUP_VERSION as SAVED_LOOKUP_SCHEMA_VERSION, PUBLIC_CLI_LOOKUP_VERSION as PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION, MAX_CLI_LOOKUP_BYTES as MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_CLI_LOOKUP_JSON_CONTAINER_ITEMS as MAX_BOUNDED_JSON_CONTAINER_ITEMS, MAX_CLI_LOOKUP_JSON_DEPTH as MAX_BOUNDED_JSON_DEPTH, MAX_CLI_LOOKUP_JSON_KEYS as MAX_BOUNDED_JSON_KEYS, MAX_CLI_LOOKUP_JSON_VALUES as MAX_BOUNDED_JSON_VALUES, MAX_CLI_LOOKUP_STRING_LENGTH as MAX_SAVED_LOOKUP_STRING_LENGTH, SUPPORTED_CLI_LOOKUP_VERSIONS as SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, } from "../packages/contracts/cli-lookup.mjs";
|
|
10
11
|
const RDAP_STATUSES = new Set([
|
|
@@ -315,9 +316,10 @@ function assertLookupEnvelope(document, label) {
|
|
|
315
316
|
const httpResponse = objectOrNull(http?.response);
|
|
316
317
|
const publicationMetadata = pageIdentity?.publicationMetadata;
|
|
317
318
|
const deliveryMetadata = httpResponse?.deliveryMetadata;
|
|
319
|
+
const registrarStanding = document.registrarStanding;
|
|
318
320
|
if (document.version === PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION
|
|
319
|
-
&& (publicationMetadata !== undefined || deliveryMetadata !== undefined)) {
|
|
320
|
-
throw new TypeError(`${label} version ${PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION} cannot contain version ${SAVED_LOOKUP_SCHEMA_VERSION}
|
|
321
|
+
&& (publicationMetadata !== undefined || deliveryMetadata !== undefined || registrarStanding !== undefined)) {
|
|
322
|
+
throw new TypeError(`${label} version ${PUBLIC_SAVED_LOOKUP_SCHEMA_VERSION} cannot contain version ${SAVED_LOOKUP_SCHEMA_VERSION} evidence fields.`);
|
|
321
323
|
}
|
|
322
324
|
if (document.version === SAVED_LOOKUP_SCHEMA_VERSION
|
|
323
325
|
&& (publicationMetadata !== undefined && (!['success', 'partial'].includes(String(pageIdentity?.status))
|
|
@@ -326,6 +328,16 @@ function assertLookupEnvelope(document, label) {
|
|
|
326
328
|
|| !validHttpDeliveryMetadata(deliveryMetadata)))) {
|
|
327
329
|
throw new TypeError(`${label} contains invalid homepage metadata.`);
|
|
328
330
|
}
|
|
331
|
+
if (document.version === SAVED_LOOKUP_SCHEMA_VERSION
|
|
332
|
+
&& registrarStanding !== undefined
|
|
333
|
+
&& !validRegistrarStanding(registrarStanding)) {
|
|
334
|
+
throw new TypeError(`${label} contains invalid registrar standing.`);
|
|
335
|
+
}
|
|
336
|
+
if (document.version === SAVED_LOOKUP_SCHEMA_VERSION
|
|
337
|
+
&& registrarStanding !== undefined
|
|
338
|
+
&& !registrarStandingObservedBy(registrarStanding, generatedAt)) {
|
|
339
|
+
throw new TypeError(`${label} registrar standing was observed after the saved Lookup time.`);
|
|
340
|
+
}
|
|
329
341
|
const rdapParsed = objectOrNull(rdap?.parsed);
|
|
330
342
|
const whoisParsed = objectOrNull(whois?.parsed);
|
|
331
343
|
if (rdapStatus === 'success' && !rdapParsed) {
|
|
@@ -334,6 +346,11 @@ function assertLookupEnvelope(document, label) {
|
|
|
334
346
|
if ((whoisStatus === 'complete' || whoisStatus === 'partial') && !whoisParsed) {
|
|
335
347
|
throw new TypeError('Successful WHOIS input is missing normalised parsed data.');
|
|
336
348
|
}
|
|
349
|
+
if (document.version === SAVED_LOOKUP_SCHEMA_VERSION
|
|
350
|
+
&& validRegistrarStanding(registrarStanding)
|
|
351
|
+
&& registrarStanding.ianaId !== resolveRegistrarIanaId(rdapParsed, whoisParsed)) {
|
|
352
|
+
throw new TypeError(`${label} registrar standing does not match its retained registration sources.`);
|
|
353
|
+
}
|
|
337
354
|
return generatedAt;
|
|
338
355
|
}
|
|
339
356
|
function normalizeCliLookupDocument(value, options = {}) {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { REGISTRY_CAPABILITIES_VERSION, registryCapabilityFor } from "../lib/registry-capabilities.mjs";
|
|
2
|
+
import { buildRiskCalibrationSummaryReport } from "../lib/risk-calibration-summary.mjs";
|
|
3
|
+
import { explainRiskScore, explainRiskScoreV7, RISK_MODEL_VERSION, RISK_REVIEW_THRESHOLD } from "../lib/risk-scoring.mjs";
|
|
4
|
+
import { buildCliCommandCatalogue, formatCliCommandCatalogue, selectCliCommands } from "./command-catalogue.mjs";
|
|
5
|
+
import { CLI_COMMAND_REGISTRY, CLI_COMMANDS, COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, } from "./command-reference.mjs";
|
|
6
|
+
import { buildShellCompletion } from "./completion.mjs";
|
|
7
|
+
import { buildDoctorReport, formatDoctorReport } from "./doctor.mjs";
|
|
8
|
+
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
9
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
10
|
+
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
11
|
+
import { formatTerminalRegistrySupport, formatTerminalRiskCalibration, } from "./formatters/terminal.mjs";
|
|
12
|
+
import { buildCliManual } from "./manual.mjs";
|
|
13
|
+
import { MAX_LOOKALIKE_CALIBRATION_BYTES, buildLookalikeCalibration, formatLookalikeCalibration, } from "./lookalike-calibration.mjs";
|
|
14
|
+
import { MAX_REGISTRY_COHORT_INPUT_BYTES, buildRegistryCohortReport, formatRegistryCohortReport, } from "./registry-cohort.mjs";
|
|
15
|
+
import { buildRegistryDoctorReport, formatRegistryDoctorReport } from "./registry-doctor.mjs";
|
|
16
|
+
import { buildRegistryFixtureScaffold } from "./registry-fixture-scaffold.mjs";
|
|
17
|
+
import { buildRegistrySupportDocument } from "./registry-support.mjs";
|
|
18
|
+
import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset, serializeRiskCalibrationReport, } from "./risk-calibration.mjs";
|
|
19
|
+
import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
|
|
20
|
+
import { SUPPORT_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
21
|
+
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
22
|
+
async function runCompletionCommand(args, dependencies, context) {
|
|
23
|
+
context.writeStdout(buildShellCompletion(args.shell));
|
|
24
|
+
return EXIT_CODES.SUCCESS;
|
|
25
|
+
}
|
|
26
|
+
async function runCommandCatalogueCommand(args, dependencies, context) {
|
|
27
|
+
const selectedCommands = selectCliCommands(CLI_COMMAND_REGISTRY, {
|
|
28
|
+
common: args.common,
|
|
29
|
+
group: args.group,
|
|
30
|
+
mode: args.mode,
|
|
31
|
+
});
|
|
32
|
+
const catalogue = buildCliCommandCatalogue({
|
|
33
|
+
commands: selectedCommands,
|
|
34
|
+
collections: COMMAND_COLLECTION,
|
|
35
|
+
details: COMMAND_DETAILS,
|
|
36
|
+
usage: COMMAND_USAGE,
|
|
37
|
+
packageVersion: context.packageVersion,
|
|
38
|
+
});
|
|
39
|
+
if (!args.quiet) {
|
|
40
|
+
context.writeStdout(args.output === 'json'
|
|
41
|
+
? formatJsonDocument(catalogue)
|
|
42
|
+
: context.terminal(formatCliCommandCatalogue(catalogue), args.color));
|
|
43
|
+
}
|
|
44
|
+
return EXIT_CODES.SUCCESS;
|
|
45
|
+
}
|
|
46
|
+
async function runManualCommand(args, dependencies, context) {
|
|
47
|
+
context.writeStdout(buildCliManual({
|
|
48
|
+
commands: CLI_COMMANDS,
|
|
49
|
+
collections: COMMAND_COLLECTION,
|
|
50
|
+
details: COMMAND_DETAILS,
|
|
51
|
+
usage: COMMAND_USAGE,
|
|
52
|
+
version: context.packageVersion,
|
|
53
|
+
}));
|
|
54
|
+
return EXIT_CODES.SUCCESS;
|
|
55
|
+
}
|
|
56
|
+
async function runDoctorCommand(args, dependencies, context) {
|
|
57
|
+
context.setFailureLabel('CLI diagnostics');
|
|
58
|
+
const buildReport = () => buildDoctorReport({
|
|
59
|
+
version: context.packageVersion,
|
|
60
|
+
generatedAt: context.now(),
|
|
61
|
+
network: args.network,
|
|
62
|
+
presentation: context.presentation(args.color),
|
|
63
|
+
...(dependencies.resolvePublicAddresses ? { resolveAddresses: dependencies.resolvePublicAddresses } : {}),
|
|
64
|
+
...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : {}),
|
|
65
|
+
...(dependencies.whoisQuery ? { queryWhois: dependencies.whoisQuery } : {}),
|
|
66
|
+
});
|
|
67
|
+
const report = args.network
|
|
68
|
+
? await context.withProgress('Checking public DNS, HTTPS, and WHOIS connectivity', buildReport)
|
|
69
|
+
: await buildReport();
|
|
70
|
+
if (!args.quiet) {
|
|
71
|
+
context.writeStdout(args.output === 'json'
|
|
72
|
+
? formatJsonDocument(report)
|
|
73
|
+
: context.terminal(formatDoctorReport(report), args.color));
|
|
74
|
+
}
|
|
75
|
+
return report.state === 'partial' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
76
|
+
}
|
|
77
|
+
async function runRegistrySupportCommand(args, dependencies, context) {
|
|
78
|
+
context.setFailureLabel('Registry support');
|
|
79
|
+
const requestedInput = args.target || await context.readSingleInput();
|
|
80
|
+
if (!requestedInput)
|
|
81
|
+
throw new CliUsageError('registry-support requires one domain or suffix as an argument or on stdin.');
|
|
82
|
+
const lookupCapability = dependencies.registryCapabilityFor || registryCapabilityFor;
|
|
83
|
+
const capability = lookupCapability(requestedInput);
|
|
84
|
+
if (!capability)
|
|
85
|
+
throw new CliUsageError('registry-support requires a valid domain or suffix.');
|
|
86
|
+
const catalogueVersion = dependencies.registryCapabilitiesVersion || REGISTRY_CAPABILITIES_VERSION;
|
|
87
|
+
const document = buildRegistrySupportDocument(requestedInput, capability, catalogueVersion, context.now());
|
|
88
|
+
if (!args.quiet) {
|
|
89
|
+
context.writeStdout(args.output === 'json'
|
|
90
|
+
? formatJsonDocument(document)
|
|
91
|
+
: context.terminal(formatTerminalRegistrySupport(document), args.color));
|
|
92
|
+
}
|
|
93
|
+
return EXIT_CODES.SUCCESS;
|
|
94
|
+
}
|
|
95
|
+
async function runRegistryDoctorCommand(args, dependencies, context) {
|
|
96
|
+
context.setFailureLabel('Registry compatibility diagnostic');
|
|
97
|
+
let input;
|
|
98
|
+
try {
|
|
99
|
+
input = dependencies.readCompareInput
|
|
100
|
+
? await dependencies.readCompareInput(args.source)
|
|
101
|
+
: await context.readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Registry doctor input');
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof CliUsageError)
|
|
105
|
+
throw error;
|
|
106
|
+
throw new CliUsageError(`Could not read registry doctor input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
107
|
+
}
|
|
108
|
+
if (!input.trim())
|
|
109
|
+
throw new CliUsageError('registry-doctor requires one saved Lookup JSON file or a document on stdin.');
|
|
110
|
+
const report = buildRegistryDoctorReport(input, context.now());
|
|
111
|
+
if (!args.quiet) {
|
|
112
|
+
context.writeStdout(args.output === 'json'
|
|
113
|
+
? formatJsonDocument(report)
|
|
114
|
+
: context.terminal(formatRegistryDoctorReport(report), args.color));
|
|
115
|
+
}
|
|
116
|
+
return report.summary.investigate ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
117
|
+
}
|
|
118
|
+
async function runRegistryCohortCommand(args, dependencies, context) {
|
|
119
|
+
context.setFailureLabel('Registry quality cohort');
|
|
120
|
+
let input;
|
|
121
|
+
try {
|
|
122
|
+
input = dependencies.readCompareInput
|
|
123
|
+
? await dependencies.readCompareInput(args.source)
|
|
124
|
+
: await context.readInput(args.source, MAX_REGISTRY_COHORT_INPUT_BYTES, 'Registry cohort input');
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof CliUsageError)
|
|
128
|
+
throw error;
|
|
129
|
+
throw new CliUsageError(`Could not read registry cohort input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
130
|
+
}
|
|
131
|
+
const report = buildRegistryCohortReport(input, context.now());
|
|
132
|
+
if (!args.quiet) {
|
|
133
|
+
context.writeStdout(args.output === 'json'
|
|
134
|
+
? formatJsonDocument(report)
|
|
135
|
+
: context.terminal(formatRegistryCohortReport(report), args.color));
|
|
136
|
+
}
|
|
137
|
+
return report.cohorts.some((cohort) => cohort.state === 'review')
|
|
138
|
+
? EXIT_CODES.PARTIAL_FAILURE
|
|
139
|
+
: EXIT_CODES.SUCCESS;
|
|
140
|
+
}
|
|
141
|
+
async function runRegistryScaffoldCommand(args, dependencies, context) {
|
|
142
|
+
context.setFailureLabel('Registry fixture scaffold');
|
|
143
|
+
try {
|
|
144
|
+
context.writeStdout(buildRegistryFixtureScaffold(args.profile, args.suffix, args.scenario));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Registry fixture scaffold failed'));
|
|
148
|
+
}
|
|
149
|
+
return EXIT_CODES.SUCCESS;
|
|
150
|
+
}
|
|
151
|
+
async function runRiskCalibrationCommand(args, dependencies, context) {
|
|
152
|
+
context.setFailureLabel('Risk calibration');
|
|
153
|
+
let input;
|
|
154
|
+
try {
|
|
155
|
+
input = dependencies.readRiskCalibrationInput
|
|
156
|
+
? await dependencies.readRiskCalibrationInput(args.source)
|
|
157
|
+
: await context.readInput(args.source, MAX_RISK_CALIBRATION_INPUT_BYTES, 'Risk calibration input');
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error instanceof CliUsageError)
|
|
161
|
+
throw error;
|
|
162
|
+
throw new CliUsageError(`Could not read Risk calibration input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
163
|
+
}
|
|
164
|
+
if (!input.trim())
|
|
165
|
+
throw new CliUsageError('risk-calibrate requires one dataset JSON file or a dataset on stdin.');
|
|
166
|
+
const dataset = parseRiskCalibrationDataset(input);
|
|
167
|
+
const report = buildRiskCalibrationReport(dataset, dependencies.explainRiskScore || explainRiskScore, {
|
|
168
|
+
generatedAt: context.now(),
|
|
169
|
+
modelVersion: dependencies.riskModelVersion || RISK_MODEL_VERSION,
|
|
170
|
+
reviewThreshold: dependencies.riskReviewThreshold || RISK_REVIEW_THRESHOLD,
|
|
171
|
+
...(!dependencies.explainRiskScore ? {
|
|
172
|
+
previousModelVersion: 7,
|
|
173
|
+
explainPreviousRiskScore: explainRiskScoreV7,
|
|
174
|
+
} : {}),
|
|
175
|
+
});
|
|
176
|
+
if (!args.quiet) {
|
|
177
|
+
context.writeStdout(args.output === 'summary_json'
|
|
178
|
+
? serializeRiskCalibrationReport(buildRiskCalibrationSummaryReport(report))
|
|
179
|
+
: args.output === 'json'
|
|
180
|
+
? serializeRiskCalibrationReport(report)
|
|
181
|
+
: context.terminal(formatTerminalRiskCalibration(report), args.color));
|
|
182
|
+
}
|
|
183
|
+
return EXIT_CODES.SUCCESS;
|
|
184
|
+
}
|
|
185
|
+
async function runLookalikeCalibrationCommand(args, dependencies, context) {
|
|
186
|
+
context.setFailureLabel('Lookalike review-yield calibration');
|
|
187
|
+
let input;
|
|
188
|
+
try {
|
|
189
|
+
input = dependencies.readRiskCalibrationInput
|
|
190
|
+
? await dependencies.readRiskCalibrationInput(args.source)
|
|
191
|
+
: await context.readInput(args.source, MAX_LOOKALIKE_CALIBRATION_BYTES, 'Lookalike calibration input');
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof CliUsageError)
|
|
195
|
+
throw error;
|
|
196
|
+
throw new CliUsageError(`Could not read lookalike calibration input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
197
|
+
}
|
|
198
|
+
if (!input.trim())
|
|
199
|
+
throw new CliUsageError('lookalike-calibrate requires one dataset JSON file or a document on stdin.');
|
|
200
|
+
let report;
|
|
201
|
+
try {
|
|
202
|
+
report = buildLookalikeCalibration(input, context.now());
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Lookalike calibration input is invalid'));
|
|
206
|
+
}
|
|
207
|
+
if (!args.quiet) {
|
|
208
|
+
context.writeStdout(args.output === 'json'
|
|
209
|
+
? formatJsonDocument(report)
|
|
210
|
+
: context.terminal(formatLookalikeCalibration(report), args.color));
|
|
211
|
+
}
|
|
212
|
+
return EXIT_CODES.SUCCESS;
|
|
213
|
+
}
|
|
214
|
+
const SUPPORT_COMMAND_HANDLERS = Object.freeze({
|
|
215
|
+
'completion': runCompletionCommand,
|
|
216
|
+
'commands': runCommandCatalogueCommand,
|
|
217
|
+
'manual': runManualCommand,
|
|
218
|
+
'doctor': runDoctorCommand,
|
|
219
|
+
'registry-support': runRegistrySupportCommand,
|
|
220
|
+
'registry-doctor': runRegistryDoctorCommand,
|
|
221
|
+
'registry-cohort': runRegistryCohortCommand,
|
|
222
|
+
'registry-scaffold': runRegistryScaffoldCommand,
|
|
223
|
+
'risk-calibrate': runRiskCalibrationCommand,
|
|
224
|
+
'lookalike-calibrate': runLookalikeCalibrationCommand,
|
|
225
|
+
});
|
|
226
|
+
function runSupportCommand(args, dependencies, context) {
|
|
227
|
+
return runDiscriminatedCommandHandler(SUPPORT_COMMAND_HANDLERS, args, dependencies, context);
|
|
228
|
+
}
|
|
229
|
+
export { SUPPORT_COMMAND_HANDLERS, runSupportCommand };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { formatCliJunit } from "./ci-report.mjs";
|
|
2
|
+
import { formatDomainControlMonitor, runDomainControlMonitor } from "./domain-control-monitor.mjs";
|
|
3
|
+
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
4
|
+
import EXIT_CODES from "./exit-codes.mjs";
|
|
5
|
+
import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
|
|
6
|
+
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
7
|
+
import { buildInvestigationPlan, buildWorkflowRecipeCatalogue, formatInvestigationPlan, formatWorkflowRecipeCatalogue, } from "./investigation-plan.mjs";
|
|
8
|
+
import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, runInvestigationRecipe, } from "./investigation-run.mjs";
|
|
9
|
+
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES } from "./offline-evidence-review.mjs";
|
|
10
|
+
import { createBufferedOutput } from "./output-file.mjs";
|
|
11
|
+
import { WORKFLOW_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
12
|
+
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
13
|
+
async function runMonitorOnceCommand(args, dependencies, context) {
|
|
14
|
+
context.setFailureLabel('One-shot domain control review');
|
|
15
|
+
let manifestInput;
|
|
16
|
+
let previousInput = null;
|
|
17
|
+
try {
|
|
18
|
+
manifestInput = dependencies.readArtifactInput
|
|
19
|
+
? await dependencies.readArtifactInput(args.source)
|
|
20
|
+
: await context.readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain-control manifest input');
|
|
21
|
+
if (args.previousSource) {
|
|
22
|
+
previousInput = dependencies.readDiffInput
|
|
23
|
+
? await dependencies.readDiffInput(args.previousSource)
|
|
24
|
+
: await context.readInput(args.previousSource, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Prior monitor snapshot');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof CliUsageError)
|
|
29
|
+
throw error;
|
|
30
|
+
throw new CliUsageError(`Could not read monitor input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
31
|
+
}
|
|
32
|
+
if (!manifestInput.trim())
|
|
33
|
+
throw new CliUsageError('monitor-once requires one domain-control manifest file or a document on stdin.');
|
|
34
|
+
const executeLookup = dependencies.runUnifiedLookup || (await import("../lib/lookup.mjs")).runUnifiedLookup;
|
|
35
|
+
const progress = context.beginProgress('Collecting bounded domain-control evidence');
|
|
36
|
+
let document;
|
|
37
|
+
try {
|
|
38
|
+
document = await runDomainControlMonitor(manifestInput, previousInput, {
|
|
39
|
+
executeLookup,
|
|
40
|
+
now: context.now,
|
|
41
|
+
limit: args.limit,
|
|
42
|
+
concurrency: args.concurrency,
|
|
43
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
44
|
+
onSettled: (completed, total) => progress.update(`Collected ${completed} of ${total} owned domains`),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
context.endProgress();
|
|
49
|
+
}
|
|
50
|
+
if (!args.quiet) {
|
|
51
|
+
context.writeStdout(args.output === 'json'
|
|
52
|
+
? formatJsonDocument(document)
|
|
53
|
+
: args.output === 'junit'
|
|
54
|
+
? formatCliJunit(document)
|
|
55
|
+
: context.terminal(formatDomainControlMonitor(document), args.color));
|
|
56
|
+
}
|
|
57
|
+
const policyFindings = evaluateCliFailPolicies(document, args.failOn || []);
|
|
58
|
+
if (policyFindings.length)
|
|
59
|
+
context.writeStderr(formatFailPolicyNotice(policyFindings));
|
|
60
|
+
return document.collection.failed || policyFindings.length
|
|
61
|
+
? EXIT_CODES.PARTIAL_FAILURE
|
|
62
|
+
: EXIT_CODES.SUCCESS;
|
|
63
|
+
}
|
|
64
|
+
async function runWorkflowPlanCommand(args, dependencies, context) {
|
|
65
|
+
context.setFailureLabel('Investigation plan');
|
|
66
|
+
if ('discovery' in args) {
|
|
67
|
+
const catalogue = buildWorkflowRecipeCatalogue(args.discovery === 'explain' ? args.recipe : null);
|
|
68
|
+
if (!args.quiet) {
|
|
69
|
+
context.writeStdout(args.output === 'json'
|
|
70
|
+
? formatJsonDocument(catalogue)
|
|
71
|
+
: context.terminal(formatWorkflowRecipeCatalogue(catalogue), args.color));
|
|
72
|
+
}
|
|
73
|
+
return EXIT_CODES.SUCCESS;
|
|
74
|
+
}
|
|
75
|
+
const document = buildInvestigationPlan(args.recipe, args.subject, context.now());
|
|
76
|
+
if (!args.quiet) {
|
|
77
|
+
context.writeStdout(args.output === 'json'
|
|
78
|
+
? formatJsonDocument(document)
|
|
79
|
+
: context.terminal(formatInvestigationPlan(document), args.color));
|
|
80
|
+
}
|
|
81
|
+
return EXIT_CODES.SUCCESS;
|
|
82
|
+
}
|
|
83
|
+
async function runWorkflowRecipeCommand(args, dependencies, context) {
|
|
84
|
+
context.setFailureLabel('Investigation workflow');
|
|
85
|
+
let resumeInput = null;
|
|
86
|
+
if (args.resumeSource) {
|
|
87
|
+
try {
|
|
88
|
+
resumeInput = dependencies.readDiffInput
|
|
89
|
+
? await dependencies.readDiffInput(args.resumeSource)
|
|
90
|
+
: await context.readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof CliUsageError)
|
|
94
|
+
throw error;
|
|
95
|
+
throw new CliUsageError(`Could not read investigation resume state: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const document = await runInvestigationRecipe(args.recipe, args.subject, {
|
|
99
|
+
approveNetwork: args.approveNetwork,
|
|
100
|
+
selections: args.selections,
|
|
101
|
+
resumeInput,
|
|
102
|
+
generatedAt: context.now(),
|
|
103
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
104
|
+
execute: async (command, stepArguments) => {
|
|
105
|
+
const stepStdout = createBufferedOutput();
|
|
106
|
+
const stepStderr = createBufferedOutput();
|
|
107
|
+
const exitCode = await context.executeCli([command, ...stepArguments], {
|
|
108
|
+
stdout: stepStdout.stream,
|
|
109
|
+
stderr: stepStderr.stream,
|
|
110
|
+
});
|
|
111
|
+
return { exitCode, stdout: stepStdout.value() };
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (!args.quiet) {
|
|
115
|
+
context.writeStdout(args.output === 'json'
|
|
116
|
+
? formatJsonDocument(document)
|
|
117
|
+
: context.terminal(formatInvestigationRun(document), args.color));
|
|
118
|
+
}
|
|
119
|
+
return document.state === 'step_failed' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
120
|
+
}
|
|
121
|
+
const WORKFLOW_COMMAND_HANDLERS = Object.freeze({
|
|
122
|
+
'monitor-once': runMonitorOnceCommand,
|
|
123
|
+
'workflow-plan': runWorkflowPlanCommand,
|
|
124
|
+
'workflow-run': runWorkflowRecipeCommand,
|
|
125
|
+
});
|
|
126
|
+
function runWorkflowCommand(args, dependencies, context) {
|
|
127
|
+
return runDiscriminatedCommandHandler(WORKFLOW_COMMAND_HANDLERS, args, dependencies, context);
|
|
128
|
+
}
|
|
129
|
+
export { WORKFLOW_COMMAND_HANDLERS, runWorkflowCommand };
|
package/docs/cli-reference.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The WHOISleuth CLI runs locally. Installed `whoisleuth <command> --help`,
|
|
4
4
|
`whoisleuth commands` and `whoisleuth manual` are the exact grammar, option and
|
|
5
5
|
command authorities for that installed version. The public
|
|
6
|
-
[CLI reference](https://whoisleuth.com/cli) provides a searchable view generated
|
|
6
|
+
[CLI reference](https://www.whoisleuth.com/cli) provides a searchable view generated
|
|
7
7
|
from the same registry.
|
|
8
8
|
|
|
9
9
|
This page records the common interfaces and the boundaries that matter across
|
|
@@ -12,7 +12,9 @@ contains the exhaustive recipient, retention and export metadata.
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
Public releases require Node.js 24 or later
|
|
15
|
+
Public releases require Node.js 24 or later. Release verification uses the
|
|
16
|
+
exact Node.js 24 maintainer runtime and separately exercises the installed
|
|
17
|
+
package on Node.js 26:
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
npm exec --yes --ignore-scripts --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
|
|
@@ -107,15 +109,16 @@ Readers accept only their declared public and current versions. Unknown,
|
|
|
107
109
|
unreleased historical and future schemas fail before partial interpretation;
|
|
108
110
|
an invalid import is not treated as an empty document.
|
|
109
111
|
|
|
110
|
-
`inspect-archive` reads current workspace archive
|
|
112
|
+
`inspect-archive` reads current workspace archive v7 and exact versions 5 and 6. It
|
|
111
113
|
reports section metadata and digest-only search results unless `--reveal` is
|
|
112
114
|
explicitly selected. It never searches notes, contacts or arbitrary raw fields.
|
|
113
115
|
|
|
114
116
|
`export` reads supported saved Lookup v1 or v2 and writes current Lookup
|
|
115
|
-
evidence schema
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
evidence schema 28. Published v2 schema 27 and exact v1 schema 26 remain
|
|
118
|
+
readable. Versions 27 and 28 exclude raw registration payloads, expanded
|
|
119
|
+
contacts, credentials, complete query-bearing URLs and provider payloads;
|
|
120
|
+
schema 28 can also retain the bounded, separately attributed registrar-standing
|
|
121
|
+
projection shown by Lookup.
|
|
119
122
|
|
|
120
123
|
`verify-artifact` checks a recognised structure and its applicable integrity
|
|
121
124
|
contract. `interchange-report` describes retained and omitted fields.
|
|
@@ -133,14 +136,18 @@ evidence is accurate, current, safe to share or attributable to a person.
|
|
|
133
136
|
control, activity, intent or maliciousness.
|
|
134
137
|
- Bulk applies one declared collection contract per target. Fast accepts up to
|
|
135
138
|
500 targets and Deep up to 50; each target remains a separate request.
|
|
136
|
-
- Respond commands
|
|
137
|
-
|
|
139
|
+
- Respond commands package browser-created Cases and prepare local packets,
|
|
140
|
+
reports and sharing reviews. They do not create durable Cases, submit,
|
|
141
|
+
publish, notify or grant recipient authorisation.
|
|
138
142
|
- Assurance, comparison and calibration commands describe supplied or retained
|
|
139
143
|
evidence. They do not tune the running model, change infrastructure or turn an
|
|
140
144
|
analyst label into observed truth.
|
|
141
145
|
- `workflow-plan` lists fixed installed recipes without executing them.
|
|
142
146
|
`workflow-run` executes only installed steps, requires approval for network
|
|
143
|
-
work and pauses
|
|
147
|
+
work and pauses at unresolved analyst selections. Repeat
|
|
148
|
+
`--select <step-id>=<path-or-value>` in placeholder order to resume a selected
|
|
149
|
+
step; version-1 checkpoints remain readable and version 2 retains the exact
|
|
150
|
+
selections in the local checkpoint.
|
|
144
151
|
|
|
145
152
|
Use the installed focused help for positional inputs, exact ceilings, options,
|
|
146
153
|
network effects, outputs and command-specific exit behaviour.
|
package/docs/cli.md
CHANGED
|
@@ -12,7 +12,9 @@ lists the network, recipient, retention and export boundary for every command.
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
Public releases require Node.js 24 or later
|
|
15
|
+
Public releases require Node.js 24 or later. Release verification uses the
|
|
16
|
+
exact Node.js 24 maintainer runtime and separately exercises the installed
|
|
17
|
+
package on Node.js 26:
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
npm exec --yes --ignore-scripts --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
|
|
@@ -76,7 +78,7 @@ application:
|
|
|
76
78
|
| Group | Common commands |
|
|
77
79
|
| --- | --- |
|
|
78
80
|
| Investigate | `lookup`, `bulk`, `discover`, `ct-search`, `posture`, `http`, `tls`, `compare`, `brief` |
|
|
79
|
-
| Respond | `case-pack`, `change-packet`, `sharing-review`, `export` |
|
|
81
|
+
| Respond | `case-pack`, `change-packet`, `sharing-review`, `export` (local handoff from browser-created Cases) |
|
|
80
82
|
| Assure | `dnssec-validate`, `mail-transport`, `domain-control`, `assurance`, `workflow-plan`, `diff`, `inspect-archive`, `verify-artifact` |
|
|
81
83
|
| Utilities | `doctor`, `commands`, `completion`, `manual`, `registry-scaffold` |
|
|
82
84
|
|
|
@@ -143,7 +145,7 @@ options override profile defaults.
|
|
|
143
145
|
Run `whoisleuth <command> --help` for exact arguments, input ceilings, network
|
|
144
146
|
effects and output formats. The packaged [CLI reference](cli-reference.md) gives
|
|
145
147
|
the longer-lived compatibility and evidence contracts, while the generated
|
|
146
|
-
[online command reference](https://whoisleuth.com/cli#commands) provides a searchable index.
|
|
148
|
+
[online command reference](https://www.whoisleuth.com/cli#commands) provides a searchable index.
|
|
147
149
|
|
|
148
150
|
## Safety and limitations
|
|
149
151
|
|
package/lib/availability.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { buildHttpObservation, failedHttpObservation, skippedHttpObservation } f
|
|
|
20
20
|
import { collectTlsIntelligence, skippedTlsObservation } from "./tls-intelligence.mjs";
|
|
21
21
|
import { parseRegistryDate, registryDateIso } from "./registry-dates.mjs";
|
|
22
22
|
import { analyzeWebsiteSecurityPosture } from "./website-security-posture.mjs";
|
|
23
|
+
import { analyzeWebsiteTechnology, captureTechnologyResponseHeaders, } from "./website-technology.mjs";
|
|
23
24
|
import { analyzeResponsePolicyHeaders, qualifyResponsePolicyWithCspMeta, } from "./response-policy.mjs";
|
|
24
25
|
import { nonEmptyErrorMessage } from "./error-detail.mjs";
|
|
25
26
|
import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
@@ -250,15 +251,7 @@ async function fetchHomepage(domain, { fetcher = safeFetchDetailed, timeoutMs =
|
|
|
250
251
|
status: 'fetched',
|
|
251
252
|
detail: `Homepage responded over ${scheme.toUpperCase()} (HTTP ${res.status}).`,
|
|
252
253
|
responsePolicy: analyzeResponsePolicyHeaders(res.headers),
|
|
253
|
-
technologyHeaders:
|
|
254
|
-
'cf-ray': res.headers.get('cf-ray') || '',
|
|
255
|
-
'x-drupal-cache': res.headers.get('x-drupal-cache') || '',
|
|
256
|
-
'x-served-by': res.headers.get('x-served-by') || '',
|
|
257
|
-
'x-powered-by': res.headers.get('x-powered-by') || '',
|
|
258
|
-
'x-shopify-stage': res.headers.get('x-shopify-stage') || '',
|
|
259
|
-
'x-sorting-hat-podid': res.headers.get('x-sorting-hat-podid') || '',
|
|
260
|
-
'x-vercel-id': res.headers.get('x-vercel-id') || '',
|
|
261
|
-
},
|
|
254
|
+
technologyHeaders: captureTechnologyResponseHeaders(res.headers),
|
|
262
255
|
http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
|
|
263
256
|
previousAttempts: failures,
|
|
264
257
|
capturedBodyBytes: body.bytesRead,
|
|
@@ -280,6 +273,7 @@ async function fetchHomepage(domain, { fetcher = safeFetchDetailed, timeoutMs =
|
|
|
280
273
|
status: 'responded',
|
|
281
274
|
detail: `Web server responded over ${scheme.toUpperCase()} (HTTP ${res.status}); homepage content was not available for inspection.`,
|
|
282
275
|
responsePolicy: analyzeResponsePolicyHeaders(res.headers),
|
|
276
|
+
technologyHeaders: captureTechnologyResponseHeaders(res.headers),
|
|
283
277
|
http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
|
|
284
278
|
previousAttempts: failures,
|
|
285
279
|
capturedBodyBytes: 0,
|
|
@@ -675,6 +669,15 @@ async function checkDomainAvailability(domain, options = {}) {
|
|
|
675
669
|
});
|
|
676
670
|
}
|
|
677
671
|
}
|
|
672
|
+
if (options.includeTechnologyProfile !== false && htmlSignals.technologyProfile === null
|
|
673
|
+
&& ['fetched', 'responded'].includes(homepage.status)) {
|
|
674
|
+
htmlSignals.technologyProfile = analyzeWebsiteTechnology({
|
|
675
|
+
htmlAvailable: false,
|
|
676
|
+
httpServer: homepage.http?.response?.server,
|
|
677
|
+
responseHeaders: homepage.technologyHeaders,
|
|
678
|
+
observedAt: homepage.http?.observedAt,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
678
681
|
const responsePolicy = qualifyResponsePolicyWithCspMeta(homepage.responsePolicy, htmlSignals.cspMetaPolicy);
|
|
679
682
|
const retainedHttp = options.includeDeliveryMetadata === false
|
|
680
683
|
? withoutHttpDeliveryMetadata(homepage.http)
|
|
@@ -3,15 +3,8 @@
|
|
|
3
3
|
// bounded before a provider or connector result reaches its public model.
|
|
4
4
|
import { domainToASCII } from 'node:url';
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
export function recordOrNull(value) {
|
|
10
|
-
return isRecord(value) ? value : null;
|
|
11
|
-
}
|
|
12
|
-
export function recordOrEmpty(value) {
|
|
13
|
-
return isRecord(value) ? value : {};
|
|
14
|
-
}
|
|
6
|
+
import { isRecord } from "./json-record.mjs";
|
|
7
|
+
export { isRecord, recordOrEmpty, recordOrNull } from "./json-record.mjs";
|
|
15
8
|
export function boundedString(value, maxLength) {
|
|
16
9
|
if (typeof value !== 'string' || /[\u0000-\u001f\u007f]/u.test(value))
|
|
17
10
|
return null;
|
|
@@ -8,6 +8,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
8
8
|
import { RETIRE_BROWSER_CATALOG } from "./generated/retire-browser-catalog.mjs";
|
|
9
9
|
import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
|
|
10
10
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
11
|
+
import { BROWSER_LIBRARY_PROFILE_VERSION, MAX_LIBRARY_FINDINGS, } from "./lookup-child-profile-contract.mjs";
|
|
11
12
|
import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
12
13
|
// Catalogue expressions are reviewed third-party inputs. Keep every individual
|
|
13
14
|
// regular-expression evaluation and the aggregate inline-signature work small
|
|
@@ -17,9 +18,7 @@ const MAX_INLINE_LIBRARY_SCAN_CHARS = 1_024;
|
|
|
17
18
|
const MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS = 4_096;
|
|
18
19
|
const MAX_INLINE_LIBRARY_SCAN_MS = 750;
|
|
19
20
|
const MAX_INLINE_LIBRARY_WORKER_BYTES = 64 * 1024;
|
|
20
|
-
const BROWSER_LIBRARY_PROFILE_VERSION = 2;
|
|
21
21
|
const MAX_LIBRARY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
22
|
-
const MAX_LIBRARY_FINDINGS = 16;
|
|
23
22
|
const MAX_ADVISORY_IDENTIFIERS = 16;
|
|
24
23
|
const MAX_WEAKNESS_CLASSES = 12;
|
|
25
24
|
const MAX_MATCHES_PER_PATTERN = 4;
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// indicator identifiers, descriptions, evidence classes, and bounded counts
|
|
4
4
|
// leave this module; script references and contents are discarded.
|
|
5
5
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
6
|
+
import { CLIENT_BEHAVIOR_PROFILE_VERSION, MAX_CLIENT_BEHAVIOR_INDICATORS, } from "./lookup-child-profile-contract.mjs";
|
|
6
7
|
import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
7
|
-
const CLIENT_BEHAVIOR_PROFILE_VERSION = 1;
|
|
8
|
-
const MAX_CLIENT_BEHAVIOR_INDICATORS = 12;
|
|
9
8
|
const MAX_INDICATOR_OCCURRENCES = 999;
|
|
10
9
|
function boundedOccurrences(value) {
|
|
11
10
|
return Math.max(1, Math.min(MAX_INDICATOR_OCCURRENCES, value));
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// names, values, labels, placeholders, arbitrary attributes, and action URLs
|
|
5
5
|
// never enter this profile.
|
|
6
6
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
|
+
import { CREDENTIAL_SURFACE_PROFILE_VERSION } from "./lookup-child-profile-contract.mjs";
|
|
7
8
|
import { MAX_STATIC_FORMS, MAX_STATIC_INPUTS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
8
|
-
const CREDENTIAL_SURFACE_PROFILE_VERSION = 1;
|
|
9
9
|
function analyzeCredentialSurfaceProfile(input = {}) {
|
|
10
10
|
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html, { baseUrl: input.baseUrl });
|
|
11
11
|
const forms = htmlAnalysis.forms;
|
|
@@ -9,6 +9,13 @@ const SPF_MAX_DEPTH = 5;
|
|
|
9
9
|
const SPF_MAX_BRANCHES = 32;
|
|
10
10
|
const MAX_DMARC_DESTINATIONS = 10;
|
|
11
11
|
const MAX_DEPENDENCIES = 64;
|
|
12
|
+
const DEPENDENCY_KIND_ORDER = Object.freeze({
|
|
13
|
+
nameserver: 0,
|
|
14
|
+
mail_exchange: 1,
|
|
15
|
+
spf_include: 2,
|
|
16
|
+
spf_redirect: 3,
|
|
17
|
+
dmarc_reporting: 4,
|
|
18
|
+
});
|
|
12
19
|
function strictHostname(value) {
|
|
13
20
|
const input = String(value || '').trim().replace(/\.+$/u, '');
|
|
14
21
|
if (/[^\x00-\x7f]/u.test(input))
|
|
@@ -252,6 +259,8 @@ function buildExternalDependencies({ domain, nameservers, mx, spfExpansion, dmar
|
|
|
252
259
|
for (const authorization of dmarcAuthorizations) {
|
|
253
260
|
add('dmarc_reporting', authorization.destination, `DMARC ${authorization.reportType} reporting`, ['authorized', 'self'].includes(authorization.state) ? 'observed' : 'unavailable');
|
|
254
261
|
}
|
|
255
|
-
return output
|
|
262
|
+
return output.sort((left, right) => (DEPENDENCY_KIND_ORDER[left.kind] - DEPENDENCY_KIND_ORDER[right.kind]
|
|
263
|
+
|| left.target.localeCompare(right.target)
|
|
264
|
+
|| left.source.localeCompare(right.source)));
|
|
256
265
|
}
|
|
257
266
|
export { SPF_LOOKUP_LIMIT, SPF_MAX_DEPTH, SPF_VOID_LOOKUP_LIMIT, buildExternalDependencies, expandSpfPolicy, reportDestinationDomain, validateDmarcExternalReporting, };
|