@slicedearth/whoisleuth-cli 2.0.1 → 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.
Files changed (95) hide show
  1. package/cli/arguments.mjs +21 -5
  2. package/cli/artifact-validation/case-response.mjs +771 -0
  3. package/cli/artifact-validation/investigation-capsule.mjs +426 -0
  4. package/cli/artifact-validation/lookup-evidence.mjs +718 -0
  5. package/cli/artifact-validation/signed-review.mjs +598 -0
  6. package/cli/artifact-validation/structure-primitives.mjs +105 -0
  7. package/cli/artifact-verify.mjs +1 -1
  8. package/cli/assurance-command-runner.mjs +324 -0
  9. package/cli/command-reference.mjs +1175 -843
  10. package/cli/completion.mjs +56 -9
  11. package/cli/discriminated-command-handlers.mjs +11 -0
  12. package/cli/doctor.mjs +2 -2
  13. package/cli/evidence-command-runner.mjs +41 -31
  14. package/cli/export-evidence.mjs +1 -1
  15. package/cli/fail-policy.mjs +33 -8
  16. package/cli/formatters/terminal-command-formats.mjs +437 -0
  17. package/cli/formatters/terminal-lookup.mjs +758 -0
  18. package/cli/formatters/terminal-metadata.mjs +58 -0
  19. package/cli/formatters/terminal-shared.mjs +83 -0
  20. package/cli/formatters/terminal.mjs +5 -1232
  21. package/cli/history-command-runner.mjs +132 -0
  22. package/cli/inline-command-families.mjs +45 -0
  23. package/cli/inline-command-runner.mjs +55 -0
  24. package/cli/investigation-plan.mjs +9 -2
  25. package/cli/investigation-run.mjs +114 -9
  26. package/cli/lookup-brief.mjs +141 -23
  27. package/cli/lookup-browser-view.mjs +289 -0
  28. package/cli/lookup-browser.mjs +3 -288
  29. package/cli/mail-review.mjs +2 -6
  30. package/cli/offline-artifact-validation.mjs +19 -0
  31. package/cli/review-command-runner.mjs +269 -0
  32. package/cli/runner.mjs +15 -972
  33. package/cli/saved-lookup.mjs +19 -2
  34. package/cli/support-command-runner.mjs +229 -0
  35. package/cli/workflow-command-runner.mjs +129 -0
  36. package/docs/cli-reference.md +16 -9
  37. package/docs/cli.md +4 -2
  38. package/lib/availability.mjs +12 -9
  39. package/lib/bounded-contract-normalizers.mjs +2 -9
  40. package/lib/browser-library-profile.mjs +1 -2
  41. package/lib/client-behavior-profile.mjs +1 -2
  42. package/lib/credential-surface-profile.mjs +1 -1
  43. package/lib/domain-posture-analysis.mjs +10 -1
  44. package/lib/domain-posture.mjs +51 -23
  45. package/lib/evidence-export.mjs +46 -5
  46. package/lib/evidence-report-markdown.mjs +4 -0
  47. package/lib/evidence-report.mjs +50 -0
  48. package/lib/generated/cisa-kev-catalog.mjs +34 -4
  49. package/lib/generated/registrar-standing-catalogue.mjs +367 -0
  50. package/lib/json-record.mjs +12 -0
  51. package/lib/lookup-child-profile-contract.mjs +858 -0
  52. package/lib/lookup-contract-primitives.mjs +98 -0
  53. package/lib/lookup-diagnostics.mjs +96 -0
  54. package/lib/lookup-network-evidence-bounds.mjs +1 -0
  55. package/lib/lookup-response.mjs +367 -0
  56. package/lib/lookup.mjs +28 -443
  57. package/lib/page-role-profile.mjs +1 -3
  58. package/lib/registrar-standing-catalogue-contract.mjs +70 -0
  59. package/lib/registrar-standing-contract.mjs +300 -0
  60. package/lib/registrar-standing.mjs +269 -0
  61. package/lib/registry-insights.mjs +2 -8
  62. package/lib/risk-scoring.mjs +26 -10
  63. package/lib/sslbl-certificates.generated.mjs +138 -136
  64. package/lib/structured-data-identity.mjs +1 -3
  65. package/lib/technology-evidence-role.mjs +51 -0
  66. package/lib/threat-intelligence-runtime.mjs +3 -3
  67. package/lib/threat-intelligence-types.mjs +13 -0
  68. package/lib/threatfox-intelligence.mjs +1 -3
  69. package/lib/tls-intelligence.mjs +1 -2
  70. package/lib/urlhaus-intelligence.mjs +1 -3
  71. package/lib/urlscan-intelligence.mjs +1 -3
  72. package/lib/website-security-posture.mjs +1 -2
  73. package/lib/website-technology.mjs +72 -23
  74. package/lib/whois-common-formats.mjs +276 -0
  75. package/lib/whois-dialects.mjs +534 -0
  76. package/lib/whois-parser-support.mjs +305 -0
  77. package/lib/whois-parser.mjs +135 -0
  78. package/lib/whois.mjs +6 -1187
  79. package/package.json +2 -2
  80. package/packages/cases/case-evidence-model.mjs +9 -0
  81. package/packages/cases/case-migration-model.mjs +17 -11
  82. package/packages/cases/case-response-model.mjs +13 -9
  83. package/packages/cases/case-response-packet.mjs +297 -112
  84. package/packages/comparison/page-similarity.mjs +23 -0
  85. package/packages/contracts/case-portability.mjs +225 -61
  86. package/packages/contracts/cli-lookup.mjs +4 -3
  87. package/packages/contracts/domain-control-manifest.mjs +1 -1
  88. package/packages/contracts/investigation-portability.mjs +1 -1
  89. package/packages/contracts/risk-calibration.mjs +2 -2
  90. package/packages/contracts/workspace-portability.mjs +34 -6
  91. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  92. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  93. package/packages/workspace/website-snapshot-model.mjs +79 -6
  94. package/third-party-notices.txt +1 -1
  95. package/cli/artifact-structure.mjs +0 -2507
@@ -52,9 +52,32 @@ function maximumFilePositionals(command) {
52
52
  function positionalEnumValues(command) {
53
53
  return positionalValues(command, 1);
54
54
  }
55
- const VALUE_OPTIONS = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options)
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) => [option.option, option.values])));
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 completion
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>/dev/null
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
- ${Object.entries(VALUE_OPTIONS).map(([option, values]) => ` '${option}' = @(${values.map((value) => `'${value}'`).join(', ')})`).join('\n')}
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
- if ($previousOwned -and $values.ContainsKey($previous)) {
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[$previous]) {
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
- if (Buffer.byteLength(script, 'utf8') > MAX_CLI_COMPLETION_BYTES) {
589
- throw new RangeError(`Generated completion is limited to ${MAX_CLI_COMPLETION_BYTES} UTF-8 bytes.`);
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} satisfies the supported Node ${MINIMUM_NODE_MAJOR}+ runtime.`
31
- : `Node ${nodeVersion} is below the supported Node ${MINIMUM_NODE_MAJOR}+ runtime.`,
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
- export async function runEvidenceCommand(args, dependencies) {
67
- if (args.action === 'inspect-archive') {
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.');
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
- if (args.action === 'sign-artifact') {
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;
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 };
@@ -1,6 +1,6 @@
1
1
  import { projectCliLookupComparisonInput } from "./compare.mjs";
2
2
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
3
- import { validateLookupEvidenceArtifactStructure } from "./artifact-structure.mjs";
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;
@@ -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
- if (!policies.length || policies.some((item) => !CLI_FAIL_POLICIES.includes(item))) {
12
- throw new CliUsageError(`--fail-on supports: ${CLI_FAIL_POLICIES.join(', ')}.`);
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 risk = Number(record(root.risk).score ?? root.riskScore);
48
- const severity = String(root.severity || record(root.review).severity || '').toLowerCase();
49
- if ((Number.isFinite(risk) && risk >= 70) || severity === 'danger' || severity === 'critical') {
50
- findings.push({ policy, reason: Number.isFinite(risk) ? `risk score ${risk} met the 70 review threshold` : `explicit ${severity} severity was present` });
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 {