@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/completion.mjs
CHANGED
|
@@ -52,9 +52,32 @@ function maximumFilePositionals(command) {
|
|
|
52
52
|
function positionalEnumValues(command) {
|
|
53
53
|
return positionalValues(command, 1);
|
|
54
54
|
}
|
|
55
|
-
const
|
|
55
|
+
const VALUE_OPTION_DEFINITIONS = Object.freeze(CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
|
|
56
56
|
.filter((option) => option.values.length > 0)
|
|
57
|
-
.map((option) =>
|
|
57
|
+
.map((option) => Object.freeze({ command: definition.command, option: option.option, values: option.values }))));
|
|
58
|
+
const COMMAND_SPECIFIC_VALUE_OPTION_NAMES = new Set(VALUE_OPTION_DEFINITIONS
|
|
59
|
+
.filter((entry, _index, entries) => new Set(entries
|
|
60
|
+
.filter((candidate) => candidate.option === entry.option)
|
|
61
|
+
.map((candidate) => JSON.stringify(candidate.values))).size > 1)
|
|
62
|
+
.map((entry) => entry.option));
|
|
63
|
+
const VALUE_OPTIONS = Object.freeze(Object.fromEntries(VALUE_OPTION_DEFINITIONS
|
|
64
|
+
.filter((entry) => !COMMAND_SPECIFIC_VALUE_OPTION_NAMES.has(entry.option))
|
|
65
|
+
.map((entry) => [entry.option, entry.values])));
|
|
66
|
+
const VALUE_OPTIONS_BY_COMMAND = Object.freeze(Object.fromEntries(VALUE_OPTION_DEFINITIONS
|
|
67
|
+
.filter((entry) => COMMAND_SPECIFIC_VALUE_OPTION_NAMES.has(entry.option))
|
|
68
|
+
.map((entry) => [`${entry.command}:${entry.option}`, entry.values])));
|
|
69
|
+
const commandValueOptionGroups = new Map();
|
|
70
|
+
for (const entry of VALUE_OPTION_DEFINITIONS.filter((candidate) => COMMAND_SPECIFIC_VALUE_OPTION_NAMES.has(candidate.option))) {
|
|
71
|
+
const key = `${entry.option}\u0000${JSON.stringify(entry.values)}`;
|
|
72
|
+
const group = commandValueOptionGroups.get(key) ?? { option: entry.option, values: entry.values, commands: [] };
|
|
73
|
+
group.commands.push(entry.command);
|
|
74
|
+
commandValueOptionGroups.set(key, group);
|
|
75
|
+
}
|
|
76
|
+
const COMMAND_VALUE_OPTION_GROUPS = Object.freeze([...commandValueOptionGroups.values()].map((group) => Object.freeze({
|
|
77
|
+
option: group.option,
|
|
78
|
+
values: group.values,
|
|
79
|
+
commands: Object.freeze([...group.commands]),
|
|
80
|
+
})));
|
|
58
81
|
function integerValues(command, option, whenOptionPresent) {
|
|
59
82
|
const range = commandOptionSpec(command, option)?.integerRanges
|
|
60
83
|
.find((candidate) => candidate.whenOptionPresent === whenOptionPresent);
|
|
@@ -71,6 +94,7 @@ function commandOptionPatterns(kinds) {
|
|
|
71
94
|
}
|
|
72
95
|
function bashCompletion() {
|
|
73
96
|
const cases = CLI_COMMANDS.map((command) => ` ${command}) options="${commandOptions(command)}"; value_options="${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).join(' ')}"; file_limit=${maximumFilePositionals(command)} ;;`).join('\n');
|
|
97
|
+
const commandValueCases = COMMAND_VALUE_OPTION_GROUPS.map((group) => ` ${group.commands.map((command) => `${command}:${group.option}`).join('|')}) COMPREPLY=( $(compgen -W "${group.values.join(' ')}" -- "\${current}") ); return ;;`).join('\n');
|
|
74
98
|
const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) COMPREPLY=( $(compgen -W "${values.join(' ')}" -- "\${current}") ); return ;;`).join('\n');
|
|
75
99
|
const integerCases = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
|
|
76
100
|
.filter((option) => option.valueKind === 'integer')
|
|
@@ -128,6 +152,9 @@ ${cases}
|
|
|
128
152
|
fi
|
|
129
153
|
done
|
|
130
154
|
if [[ " \${options} " == *" \${previous} "* ]]; then
|
|
155
|
+
case "\${command}:\${previous}" in
|
|
156
|
+
${commandValueCases}
|
|
157
|
+
esac
|
|
131
158
|
case "\${previous}" in
|
|
132
159
|
${valueCases}
|
|
133
160
|
esac
|
|
@@ -152,6 +179,7 @@ complete -F _whoisleuth_completion whoisleuth
|
|
|
152
179
|
function zshCompletion() {
|
|
153
180
|
const commandEntries = CLI_COMMANDS.map((command) => `'${command}:${COMMAND_DESCRIPTIONS[command] || command}'`).join(' ');
|
|
154
181
|
const cases = CLI_COMMANDS.map((command) => ` ${command}) options=(${commandOptions(command)}); value_options=(${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).join(' ')}); file_limit=${maximumFilePositionals(command)} ;;`).join('\n');
|
|
182
|
+
const commandValueCases = COMMAND_VALUE_OPTION_GROUPS.map((group) => ` ${group.commands.map((command) => `${command}:${group.option}`).join('|')}) compadd -- ${group.values.join(' ')}; return ;;`).join('\n');
|
|
155
183
|
const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) compadd -- ${values.join(' ')}; return ;;`).join('\n');
|
|
156
184
|
const integerCases = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
|
|
157
185
|
.filter((option) => option.valueKind === 'integer')
|
|
@@ -209,6 +237,9 @@ ${cases}
|
|
|
209
237
|
fi
|
|
210
238
|
done
|
|
211
239
|
if [[ " \${options[*]} " == *" \${previous} "* ]]; then
|
|
240
|
+
case "\${command}:\${previous}" in
|
|
241
|
+
${commandValueCases}
|
|
242
|
+
esac
|
|
212
243
|
case "\${previous}" in
|
|
213
244
|
${valueCases}
|
|
214
245
|
esac
|
|
@@ -255,6 +286,15 @@ function fishCompletion() {
|
|
|
255
286
|
+ (group.commands.includes('lookup') ? '; or __whoisleuth_direct_lookup_target' : '');
|
|
256
287
|
return `complete -c whoisleuth -n '${condition}'${group.option === HELP_LONG_ALIAS ? ` -s ${HELP_SHORT_ALIAS.slice(1)}` : ''} -l ${group.option.slice(2)}${group.arity === 1 ? ' -r' : ''}${group.file ? ' -F' : ''}`;
|
|
257
288
|
});
|
|
289
|
+
const commandValueLines = COMMAND_VALUE_OPTION_GROUPS.flatMap((group) => {
|
|
290
|
+
const lines = [
|
|
291
|
+
`complete -c whoisleuth -n '__whoisleuth_command_is ${group.commands.join(' ')}; and __fish_prev_arg_in ${group.option}' -a '${group.values.join(' ')}'`,
|
|
292
|
+
];
|
|
293
|
+
if (group.commands.includes('lookup')) {
|
|
294
|
+
lines.push(`complete -c whoisleuth -n '__whoisleuth_direct_lookup_target; and __fish_prev_arg_in ${group.option}' -a '${group.values.join(' ')}'`);
|
|
295
|
+
}
|
|
296
|
+
return lines;
|
|
297
|
+
});
|
|
258
298
|
const valueLines = Object.entries(VALUE_OPTIONS).flatMap(([option, values]) => {
|
|
259
299
|
const commands = CLI_COMMANDS.filter((command) => commandOptionSpec(command, option) !== null);
|
|
260
300
|
const namedCondition = commands.length === CLI_COMMANDS.length
|
|
@@ -305,7 +345,7 @@ function fishCompletion() {
|
|
|
305
345
|
? [`complete -c whoisleuth -n '__whoisleuth_position_is 0 ${command}' -a '${values.join(' ')}'`]
|
|
306
346
|
: [];
|
|
307
347
|
});
|
|
308
|
-
return `# WHOISleuth fish
|
|
348
|
+
return `# WHOISleuth fish
|
|
309
349
|
function __whoisleuth_clear_direct_lookup_cache --on-event fish_prompt
|
|
310
350
|
set -e __whoisleuth_direct_lookup_cache_key
|
|
311
351
|
set -e __whoisleuth_direct_lookup_cache_status
|
|
@@ -322,7 +362,7 @@ function __whoisleuth_direct_lookup_target
|
|
|
322
362
|
if test "\$__whoisleuth_direct_lookup_cache_key" = "\$cache_key"
|
|
323
363
|
return \$__whoisleuth_direct_lookup_cache_status
|
|
324
364
|
end
|
|
325
|
-
\$words[1] \$first --plan --json >/dev/null 2
|
|
365
|
+
\$words[1] \$first --plan --json >/dev/null 2>&1
|
|
326
366
|
set -l result \$status
|
|
327
367
|
set -g __whoisleuth_direct_lookup_cache_key \$cache_key
|
|
328
368
|
set -g __whoisleuth_direct_lookup_cache_status \$result
|
|
@@ -398,6 +438,7 @@ ${positionalEnumLines.join('\n')}
|
|
|
398
438
|
complete -c whoisleuth -n '__whoisleuth_file_position' -F
|
|
399
439
|
${optionLines.join('\n')}
|
|
400
440
|
${valueLines.join('\n')}
|
|
441
|
+
${commandValueLines.join('\n')}
|
|
401
442
|
${integerLines.join('\n')}
|
|
402
443
|
`;
|
|
403
444
|
}
|
|
@@ -439,7 +480,10 @@ Register-ArgumentCompleter -Native -CommandName whoisleuth -ScriptBlock {
|
|
|
439
480
|
${Object.entries(commandOptions).map(([command, options]) => ` '${command}' = @(${options.map((option) => `'${option}'`).join(', ')})`).join('\n')}
|
|
440
481
|
}
|
|
441
482
|
$values = @{
|
|
442
|
-
${
|
|
483
|
+
${[
|
|
484
|
+
...Object.entries(VALUE_OPTIONS).map(([option, values]) => ` '${option}' = @(${values.map((value) => `'${value}'`).join(', ')})`),
|
|
485
|
+
...Object.entries(VALUE_OPTIONS_BY_COMMAND).map(([key, values]) => ` '${key}' = @(${values.map((value) => `'${value}'`).join(', ')})`),
|
|
486
|
+
].join('\n')}
|
|
443
487
|
}
|
|
444
488
|
$fileOptions = @{
|
|
445
489
|
${CLI_COMMANDS.map((command) => ` '${command}' = @(${optionNamesByKind(command, ['file']).map((option) => `'${option}'`).join(', ')})`).join('\n')}
|
|
@@ -528,9 +572,11 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
|
|
|
528
572
|
if (-not $matched) { & $noMatch }
|
|
529
573
|
return
|
|
530
574
|
}
|
|
531
|
-
|
|
575
|
+
$valueKey = "${'$'}{command}:${'$'}previous"
|
|
576
|
+
if (-not $values.ContainsKey($valueKey)) { $valueKey = $previous }
|
|
577
|
+
if ($previousOwned -and $values.ContainsKey($valueKey)) {
|
|
532
578
|
$matched = $false
|
|
533
|
-
foreach ($candidate in $values[$
|
|
579
|
+
foreach ($candidate in $values[$valueKey]) {
|
|
534
580
|
if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
535
581
|
[System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
|
|
536
582
|
$matched = $true
|
|
@@ -585,8 +631,9 @@ function buildShellCompletion(shell) {
|
|
|
585
631
|
: shell === 'fish'
|
|
586
632
|
? fishCompletion()
|
|
587
633
|
: powershellCompletion();
|
|
588
|
-
|
|
589
|
-
|
|
634
|
+
const byteLength = Buffer.byteLength(script, 'utf8');
|
|
635
|
+
if (byteLength > MAX_CLI_COMPLETION_BYTES) {
|
|
636
|
+
throw new RangeError(`Generated ${shell} completion is ${byteLength} UTF-8 bytes; the limit is ${MAX_CLI_COMPLETION_BYTES}.`);
|
|
590
637
|
}
|
|
591
638
|
return script;
|
|
592
639
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function runDiscriminatedCommandHandler(handlers, args, ...tail) {
|
|
2
|
+
// TypeScript cannot retain the correlation between a discriminated union and
|
|
3
|
+
// an indexed mapped type. The map's public type proves that correlation;
|
|
4
|
+
// this local cast only performs the already-checked lookup.
|
|
5
|
+
const handler = handlers[args.action];
|
|
6
|
+
if (typeof handler !== 'function') {
|
|
7
|
+
throw new Error(`No command handler is registered for ${String(args.action)}.`);
|
|
8
|
+
}
|
|
9
|
+
return handler(args, ...tail);
|
|
10
|
+
}
|
|
11
|
+
export { runDiscriminatedCommandHandler };
|
package/cli/doctor.mjs
CHANGED
|
@@ -27,8 +27,8 @@ async function buildDoctorReport(options) {
|
|
|
27
27
|
label: 'Node runtime',
|
|
28
28
|
state: nodeSupported ? 'pass' : 'partial',
|
|
29
29
|
detail: nodeSupported
|
|
30
|
-
? `Node ${nodeVersion}
|
|
31
|
-
: `Node ${nodeVersion} is below the
|
|
30
|
+
? `Node ${nodeVersion} meets the minimum Node ${MINIMUM_NODE_MAJOR} runtime requirement.`
|
|
31
|
+
: `Node ${nodeVersion} is below the minimum Node ${MINIMUM_NODE_MAJOR} runtime requirement.`,
|
|
32
32
|
}),
|
|
33
33
|
Object.freeze({
|
|
34
34
|
id: 'platform',
|
|
@@ -5,6 +5,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
|
5
5
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
6
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
7
7
|
import { readCliTextInput } from "./input.mjs";
|
|
8
|
+
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
8
9
|
export function isEvidenceCommand(args) {
|
|
9
10
|
return args.action === 'inspect-archive'
|
|
10
11
|
|| args.action === 'sign-artifact'
|
|
@@ -63,39 +64,39 @@ async function readPassphrase(source, dependencies) {
|
|
|
63
64
|
}
|
|
64
65
|
return passphrase;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
throw new CliUsageError('inspect-archive requires one workspace archive file or an archive on stdin.');
|
|
71
|
-
}
|
|
72
|
-
const passphrase = args.passphraseSource
|
|
73
|
-
? await readPassphrase(args.passphraseSource, dependencies)
|
|
74
|
-
: null;
|
|
75
|
-
const report = await inspectWorkspaceArchive(input, {
|
|
76
|
-
passphrase,
|
|
77
|
-
search: args.search,
|
|
78
|
-
reveal: args.reveal,
|
|
79
|
-
requireMatch: args.requireMatch,
|
|
80
|
-
expectedContentDigest: args.expectedContentDigest,
|
|
81
|
-
});
|
|
82
|
-
if (!args.quiet) {
|
|
83
|
-
dependencies.stdout.write(args.output === 'json'
|
|
84
|
-
? formatJsonDocument(report)
|
|
85
|
-
: formatArchiveInspection(report));
|
|
86
|
-
}
|
|
87
|
-
return EXIT_CODES.SUCCESS;
|
|
67
|
+
async function runInspectArchiveCommand(args, dependencies) {
|
|
68
|
+
const input = await readArtifact(args.source, 'Archive input', dependencies);
|
|
69
|
+
if (!input.trim()) {
|
|
70
|
+
throw new CliUsageError('inspect-archive requires one workspace archive file or an archive on stdin.');
|
|
88
71
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
72
|
+
const passphrase = args.passphraseSource
|
|
73
|
+
? await readPassphrase(args.passphraseSource, dependencies)
|
|
74
|
+
: null;
|
|
75
|
+
const report = await inspectWorkspaceArchive(input, {
|
|
76
|
+
passphrase,
|
|
77
|
+
search: args.search,
|
|
78
|
+
reveal: args.reveal,
|
|
79
|
+
requireMatch: args.requireMatch,
|
|
80
|
+
expectedContentDigest: args.expectedContentDigest,
|
|
81
|
+
});
|
|
82
|
+
if (!args.quiet) {
|
|
83
|
+
dependencies.stdout.write(args.output === 'json'
|
|
84
|
+
? formatJsonDocument(report)
|
|
85
|
+
: formatArchiveInspection(report));
|
|
98
86
|
}
|
|
87
|
+
return EXIT_CODES.SUCCESS;
|
|
88
|
+
}
|
|
89
|
+
async function runSignArtifactCommand(args, dependencies) {
|
|
90
|
+
const input = await readArtifact(args.source, 'Evidence artefact input', dependencies);
|
|
91
|
+
if (!input.trim()) {
|
|
92
|
+
throw new CliUsageError('sign-artifact requires one reviewed artefact file or an artefact on stdin.');
|
|
93
|
+
}
|
|
94
|
+
const privateKey = await readKey(args.privateKeySource, 'Private key file', dependencies.readPrivateKeyFile, dependencies);
|
|
95
|
+
const signed = await signEvidencePackage(input, privateKey, dependencies.now ? dependencies.now() : new Date().toISOString());
|
|
96
|
+
dependencies.stdout.write(formatJsonDocument(signed));
|
|
97
|
+
return EXIT_CODES.SUCCESS;
|
|
98
|
+
}
|
|
99
|
+
async function runVerifySignatureCommand(args, dependencies) {
|
|
99
100
|
const input = await readArtifact(args.source, 'Signed evidence package input', dependencies);
|
|
100
101
|
if (!input.trim()) {
|
|
101
102
|
throw new CliUsageError('verify-signature requires one signed package file or a package on stdin.');
|
|
@@ -111,3 +112,12 @@ export async function runEvidenceCommand(args, dependencies) {
|
|
|
111
112
|
}
|
|
112
113
|
return EXIT_CODES.SUCCESS;
|
|
113
114
|
}
|
|
115
|
+
const EVIDENCE_COMMAND_HANDLERS = Object.freeze({
|
|
116
|
+
'inspect-archive': runInspectArchiveCommand,
|
|
117
|
+
'sign-artifact': runSignArtifactCommand,
|
|
118
|
+
'verify-signature': runVerifySignatureCommand,
|
|
119
|
+
});
|
|
120
|
+
function runEvidenceCommand(args, dependencies) {
|
|
121
|
+
return runDiscriminatedCommandHandler(EVIDENCE_COMMAND_HANDLERS, args, dependencies);
|
|
122
|
+
}
|
|
123
|
+
export { EVIDENCE_COMMAND_HANDLERS, runEvidenceCommand };
|
package/cli/export-evidence.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { projectCliLookupComparisonInput } from "./compare.mjs";
|
|
2
2
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
3
|
-
import { validateLookupEvidenceArtifactStructure } from "./artifact-
|
|
3
|
+
import { validateLookupEvidenceArtifactStructure } from "./offline-artifact-validation.mjs";
|
|
4
4
|
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
5
5
|
import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, serializeLookupEvidence, } from "../lib/evidence-export.mjs";
|
|
6
6
|
const APPLICATION_VERSION = WHOISLEUTH_APPLICATION_VERSION;
|
package/cli/fail-policy.mjs
CHANGED
|
@@ -1,27 +1,42 @@
|
|
|
1
1
|
import { lookupStrictExitFindings } from "./strict-exit.mjs";
|
|
2
2
|
import { CliUsageError } from "./errors.mjs";
|
|
3
3
|
export const CLI_FAIL_POLICIES = ['source-failure', 'inconclusive', 'danger', 'material-drift'];
|
|
4
|
+
export const CLI_FAIL_POLICIES_BY_COMMAND = Object.freeze({
|
|
5
|
+
lookup: Object.freeze(['source-failure', 'inconclusive', 'danger']),
|
|
6
|
+
bulk: Object.freeze(['source-failure', 'inconclusive', 'danger']),
|
|
7
|
+
'discover-scan': Object.freeze(['source-failure', 'inconclusive', 'danger']),
|
|
8
|
+
'monitor-once': Object.freeze(['source-failure', 'inconclusive', 'material-drift']),
|
|
9
|
+
});
|
|
4
10
|
function record(value) {
|
|
5
11
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
6
12
|
}
|
|
7
|
-
export function parseCliFailPolicies(value) {
|
|
13
|
+
export function parseCliFailPolicies(value, command) {
|
|
8
14
|
if (typeof value !== 'string' || !value || value.length > 100)
|
|
9
15
|
throw new CliUsageError('--fail-on requires a comma-separated policy list.');
|
|
10
16
|
const policies = [...new Set(value.split(',').map((item) => item.trim().toLowerCase()).filter(Boolean))];
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
const supported = CLI_FAIL_POLICIES_BY_COMMAND[command];
|
|
18
|
+
if (!policies.length || policies.some((item) => !supported.includes(item))) {
|
|
19
|
+
throw new CliUsageError(`--fail-on for ${command} supports: ${supported.join(', ')}.`);
|
|
13
20
|
}
|
|
14
21
|
return policies;
|
|
15
22
|
}
|
|
16
23
|
function availabilityStates(document) {
|
|
17
24
|
const direct = String(record(document.availability).state || '').toLowerCase();
|
|
25
|
+
const review = String(record(document.review).state || '').toLowerCase();
|
|
18
26
|
const results = Array.isArray(document.results) ? document.results : [];
|
|
19
27
|
const resultStates = results.flatMap((value) => {
|
|
20
28
|
const item = record(value);
|
|
21
29
|
const state = item.availabilityState ?? record(item.availability).state;
|
|
22
30
|
return typeof state === 'string' ? [state.toLowerCase()] : [];
|
|
23
31
|
});
|
|
24
|
-
return [direct, ...resultStates].filter(Boolean);
|
|
32
|
+
return [direct, review, ...resultStates].filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
function riskScore(value) {
|
|
35
|
+
const raw = record(value.risk).score ?? value.riskScore;
|
|
36
|
+
if (raw === null || raw === undefined || raw === '')
|
|
37
|
+
return null;
|
|
38
|
+
const parsed = Number(raw);
|
|
39
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
25
40
|
}
|
|
26
41
|
export function evaluateCliFailPolicies(document, policies) {
|
|
27
42
|
const root = record(document);
|
|
@@ -44,10 +59,20 @@ export function evaluateCliFailPolicies(document, policies) {
|
|
|
44
59
|
findings.push({ policy, reason: `${count} authority or availability state(s) remained inconclusive` });
|
|
45
60
|
}
|
|
46
61
|
else if (policy === 'danger') {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
const documents = [root, ...(Array.isArray(root.results) ? root.results.map(record) : [])];
|
|
63
|
+
const risks = documents.map(riskScore).filter((score) => score !== null);
|
|
64
|
+
const severities = documents
|
|
65
|
+
.flatMap((item) => [item.severity, record(item.review).severity])
|
|
66
|
+
.map((severity) => String(severity || '').toLowerCase())
|
|
67
|
+
.filter((severity) => severity === 'danger' || severity === 'critical');
|
|
68
|
+
const highestRisk = risks.length ? Math.max(...risks) : null;
|
|
69
|
+
if ((highestRisk !== null && highestRisk >= 70) || severities.length) {
|
|
70
|
+
findings.push({
|
|
71
|
+
policy,
|
|
72
|
+
reason: highestRisk !== null && highestRisk >= 70
|
|
73
|
+
? `risk score ${highestRisk} met the 70 review threshold`
|
|
74
|
+
: `explicit ${severities[0]} severity was present`,
|
|
75
|
+
});
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
78
|
else {
|