@slicedearth/whoisleuth-cli 2.0.1 → 2.2.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 (97) 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 +16 -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 +47 -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-record-operations.mjs +235 -0
  83. package/packages/cases/case-response-model.mjs +13 -9
  84. package/packages/cases/case-response-packet.mjs +297 -112
  85. package/packages/comparison/page-similarity.mjs +23 -0
  86. package/packages/contracts/case-portability.mjs +235 -61
  87. package/packages/contracts/cli-lookup.mjs +4 -3
  88. package/packages/contracts/domain-control-manifest.mjs +1 -1
  89. package/packages/contracts/investigation-portability.mjs +1 -1
  90. package/packages/contracts/risk-calibration.mjs +2 -2
  91. package/packages/contracts/workspace-portability.mjs +34 -6
  92. package/packages/interchange/external-findings-import.mjs +113 -45
  93. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  94. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  95. package/packages/workspace/website-snapshot-model.mjs +79 -6
  96. package/third-party-notices.txt +1 -1
  97. package/cli/artifact-structure.mjs +0 -2507
@@ -1,299 +1,14 @@
1
1
  import { StringDecoder } from 'node:string_decoder';
2
- import { formatTerminalLookup, safeTerminalValue } from "./formatters/terminal.mjs";
3
- import { presentTerminalOutput, terminalPresentation, wrapTerminalOutput, } from "./terminal-presentation.mjs";
4
- const MIN_LOOKUP_BROWSER_HEIGHT = 12;
5
- const MAX_LOOKUP_BROWSER_HEIGHT = 80;
6
- const MAX_LOOKUP_BROWSER_PANELS = 9;
2
+ import { safeTerminalValue } from "./formatters/terminal.mjs";
3
+ import { terminalPresentation, } from "./terminal-presentation.mjs";
4
+ import { MIN_LOOKUP_BROWSER_HEIGHT, MAX_LOOKUP_BROWSER_PANELS, MAX_LOOKUP_BROWSER_SEARCH_BYTES, MAX_LOOKUP_BROWSER_SEARCH_MATCHES, MAX_LOOKUP_BROWSER_SEARCH_SCALARS, boundedBrowserDimensions, boundedSearchText, buildLookupBrowserHelpFrame, buildLookupBrowserFrame, buildLookupBrowserPanels, buildLookupBrowserSearchFrame, buildLookupCollectionFrame, findLookupBrowserMatches, renderedPanelLineOffset, renderLookupBrowser, terminalDisplayWidth, } from "./lookup-browser-view.mjs";
7
5
  const ENTER_ALTERNATE_SCREEN = '\u001b[?1049h\u001b[?25l';
8
6
  const LEAVE_ALTERNATE_SCREEN = '\u001b[?25h\u001b[?1049l';
9
7
  const CLEAR_SCREEN = '\u001b[2J\u001b[H';
10
8
  const ESCAPE_SEQUENCE_WAIT_MS = 40;
11
9
  const MAX_PENDING_INPUT_LENGTH = 512;
12
10
  const MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES = 2_048;
13
- const MAX_LOOKUP_BROWSER_SEARCH_BYTES = 256;
14
- const MAX_LOOKUP_BROWSER_SEARCH_SCALARS = 80;
15
- const MAX_LOOKUP_BROWSER_SEARCH_MATCHES = 256;
16
- const ANSI_SEQUENCE_RE = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
17
11
  const COMPLETE_CSI_RE = /^\u001b\[[0-?]*[ -/]*[@-~]/u;
18
- function boundedHeight(value) {
19
- const height = Number(value);
20
- if (!Number.isSafeInteger(height))
21
- return 24;
22
- return Math.max(MIN_LOOKUP_BROWSER_HEIGHT, Math.min(MAX_LOOKUP_BROWSER_HEIGHT, height));
23
- }
24
- function panelId(value, index) {
25
- const normalized = value.toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '');
26
- return normalized || `panel-${index + 1}`;
27
- }
28
- function terminalCharacterWidth(character) {
29
- const codePoint = character.codePointAt(0) || 0;
30
- if (/\p{Mark}/u.test(character)
31
- || codePoint === 0x200d
32
- || codePoint >= 0xfe00 && codePoint <= 0xfe0f
33
- || codePoint >= 0xe0100 && codePoint <= 0xe01ef)
34
- return 0;
35
- return codePoint >= 0x1100 && (codePoint <= 0x115f
36
- || codePoint === 0x2329
37
- || codePoint === 0x232a
38
- || codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f
39
- || codePoint >= 0xac00 && codePoint <= 0xd7a3
40
- || codePoint >= 0xf900 && codePoint <= 0xfaff
41
- || codePoint >= 0xfe10 && codePoint <= 0xfe19
42
- || codePoint >= 0xfe30 && codePoint <= 0xfe6f
43
- || codePoint >= 0xff00 && codePoint <= 0xff60
44
- || codePoint >= 0xffe0 && codePoint <= 0xffe6
45
- || codePoint >= 0x1f300 && codePoint <= 0x1faff
46
- || codePoint >= 0x20000 && codePoint <= 0x3fffd) ? 2 : 1;
47
- }
48
- function terminalDisplayWidth(value) {
49
- return Array.from(value.replace(ANSI_SEQUENCE_RE, ''))
50
- .reduce((total, character) => total + terminalCharacterWidth(character), 0);
51
- }
52
- function hardWrapTerminalLine(line, width) {
53
- if (!line || terminalDisplayWidth(line) <= width)
54
- return [line];
55
- const lines = [];
56
- let current = '';
57
- let currentWidth = 0;
58
- for (const character of line) {
59
- const characterWidth = terminalCharacterWidth(character);
60
- if (current && currentWidth + characterWidth > width) {
61
- lines.push(current);
62
- current = '';
63
- currentWidth = 0;
64
- }
65
- current += character;
66
- currentWidth += characterWidth;
67
- }
68
- if (current || !lines.length)
69
- lines.push(current);
70
- return lines;
71
- }
72
- function clipTerminalLine(line, width) {
73
- if (terminalDisplayWidth(line) <= width)
74
- return line;
75
- const maximumContentWidth = Math.max(1, width - 1);
76
- let clipped = '';
77
- let clippedWidth = 0;
78
- for (const character of line) {
79
- const characterWidth = terminalCharacterWidth(character);
80
- if (clippedWidth + characterWidth > maximumContentWidth)
81
- break;
82
- clipped += character;
83
- clippedWidth += characterWidth;
84
- }
85
- return `${clipped}…`;
86
- }
87
- function wrapLookupBrowserLines(value, width) {
88
- const softlyWrapped = wrapTerminalOutput(`${value}\n`, width).replace(/\n$/u, '').split('\n');
89
- return softlyWrapped.flatMap((line) => hardWrapTerminalLine(line, width));
90
- }
91
- function buildLookupBrowserPanels(document) {
92
- const blocks = formatTerminalLookup(document, { detail: 'verbose' })
93
- .trimEnd()
94
- .split(/\n\n/gu)
95
- .slice(0, MAX_LOOKUP_BROWSER_PANELS);
96
- return blocks.flatMap((block, index) => {
97
- const [heading, ...lines] = block.split('\n');
98
- if (!heading?.endsWith(':') || lines.length === 0)
99
- return [];
100
- const label = heading.slice(0, -1);
101
- return [Object.freeze({
102
- id: panelId(label, index),
103
- label,
104
- lines: Object.freeze(lines),
105
- })];
106
- });
107
- }
108
- function browserTabLine(panels, activeIndex, width) {
109
- const labels = panels.map((panel, index) => index === activeIndex
110
- ? `[${index + 1} ${panel.label}]`
111
- : `${index + 1} ${panel.label}`);
112
- const complete = labels.join(' ');
113
- if (terminalDisplayWidth(complete) <= width)
114
- return complete;
115
- const visible = [activeIndex - 1, activeIndex, activeIndex + 1]
116
- .filter((index) => index >= 0 && index < panels.length)
117
- .map((index) => labels[index]);
118
- const windowed = `${activeIndex > 1 ? '‹ ' : ''}${visible.join(' ')}${activeIndex + 2 < panels.length ? ' ›' : ''}`;
119
- return clipTerminalLine(windowed, width);
120
- }
121
- function boundedTargetLine(query, width) {
122
- return clipTerminalLine(`Target ${query}`, width);
123
- }
124
- function boundedBrowserDimensions(options) {
125
- return {
126
- width: Math.max(40, Math.min(160, Number.isSafeInteger(options.width) ? Number(options.width) : 100)),
127
- height: boundedHeight(options.height),
128
- };
129
- }
130
- function boundedSearchText(value) {
131
- const text = typeof value === 'string' ? value : '';
132
- if (Buffer.byteLength(text, 'utf8') > MAX_LOOKUP_BROWSER_SEARCH_BYTES
133
- || Array.from(text).length > MAX_LOOKUP_BROWSER_SEARCH_SCALARS
134
- || /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u.test(text)) {
135
- throw new RangeError(`Lookup browser search is limited to ${MAX_LOOKUP_BROWSER_SEARCH_SCALARS} characters and ${MAX_LOOKUP_BROWSER_SEARCH_BYTES} UTF-8 bytes.`);
136
- }
137
- return text;
138
- }
139
- function findLookupBrowserMatches(panels, query) {
140
- const normalized = boundedSearchText(query).trim().toLocaleLowerCase('en-US');
141
- if (!normalized)
142
- return { matches: [], truncated: false };
143
- const matches = [];
144
- let truncated = false;
145
- outer: for (let panelIndex = 0; panelIndex < panels.length; panelIndex += 1) {
146
- const panel = panels[panelIndex];
147
- const lines = [panel.label, ...panel.lines];
148
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
149
- if (!String(lines[lineIndex] || '').toLocaleLowerCase('en-US').includes(normalized))
150
- continue;
151
- if (matches.length >= MAX_LOOKUP_BROWSER_SEARCH_MATCHES) {
152
- truncated = true;
153
- break outer;
154
- }
155
- matches.push(Object.freeze({ panelIndex, lineIndex: Math.max(0, lineIndex - 1) }));
156
- }
157
- }
158
- return { matches, truncated };
159
- }
160
- function renderedPanelLineOffset(panel, lineIndex, width) {
161
- if (lineIndex <= 0)
162
- return 0;
163
- return panel.lines
164
- .slice(0, Math.min(lineIndex, panel.lines.length))
165
- .reduce((total, line) => total + wrapLookupBrowserLines(line, width).length, 0);
166
- }
167
- function renderStandaloneBrowserFrame(lines, options) {
168
- const { width, height } = boundedBrowserDimensions(options);
169
- const clipped = lines.slice(0, height).map((line) => clipTerminalLine(line, width));
170
- return presentTerminalOutput(`${clipped.join('\n')}\n`, {
171
- color: options.color === true,
172
- interactive: true,
173
- palette: options.palette || 'auto',
174
- width: null,
175
- });
176
- }
177
- function buildLookupBrowserHelpFrame(document, options = {}) {
178
- const query = safeTerminalValue(document?.query ?? options.query, 'unknown target');
179
- const mode = safeTerminalValue(document?.mode ?? options.mode, 'unknown mode');
180
- return renderStandaloneBrowserFrame([
181
- 'WHOISleuth evidence browser help',
182
- boundedTargetLine(query, boundedBrowserDimensions(options).width),
183
- `Mode ${mode}`,
184
- '',
185
- options.collecting ? 'Collection' : 'Panels',
186
- options.collecting
187
- ? ' q / Ctrl-C cancel the active collection'
188
- : ' ←/→ h/l Tab move panels · 1-9 select',
189
- options.collecting
190
- ? ' progress is transient and is never retained'
191
- : ' ↑/↓ j/k PgUp/PgDn Home/End scroll',
192
- options.collecting ? ' ? / Esc return to collection progress' : ' / search rendered panels · n/N matches',
193
- ' ? / Esc return · q close',
194
- ], options);
195
- }
196
- function buildLookupBrowserSearchFrame(document, query, notice, options = {}) {
197
- return renderStandaloneBrowserFrame([
198
- 'WHOISleuth evidence browser search',
199
- boundedTargetLine(safeTerminalValue(document.query, 'unknown target'), boundedBrowserDimensions(options).width),
200
- `Mode ${safeTerminalValue(document.mode, 'unknown mode')}`,
201
- '',
202
- `Search ${query || 'type a bounded term'}`,
203
- notice || 'Search covers only the bounded text rendered in the evidence panels.',
204
- '',
205
- 'Enter apply · Backspace edit · Esc cancel',
206
- ], options);
207
- }
208
- function buildLookupCollectionFrame(queryValue, modeValue, sources, options = {}) {
209
- const { width, height } = boundedBrowserDimensions(options);
210
- const query = safeTerminalValue(queryValue, 'unknown target');
211
- const mode = modeValue === 'deep' ? 'deep' : 'fast';
212
- const header = [
213
- 'WHOISleuth evidence browser',
214
- boundedTargetLine(query, width),
215
- `Mode ${mode}`,
216
- 'Collection in progress:',
217
- ];
218
- const footer = ['? help · progress is transient', 'q/Esc/Ctrl-C cancel'];
219
- const bodyCapacity = Math.max(1, height - header.length - footer.length);
220
- const body = mode === 'fast'
221
- ? [
222
- 'Registration evidence collecting',
223
- 'Fast mode waits for the final bounded result.',
224
- 'Per-source states are not fabricated.',
225
- ]
226
- : sources.flatMap((source) => {
227
- const state = source.state === 'pending'
228
- ? 'pending'
229
- : `${source.state}${source.complete ? ' · complete' : ' · incomplete'}${source.truncated ? ' · truncated' : ''}`;
230
- const limitation = source.state !== 'pending' && source.limitation
231
- ? ` · ${source.limitation}`
232
- : '';
233
- return [clipTerminalLine(`${source.source.replaceAll('_', ' ')} ${state}${limitation}`, width)];
234
- });
235
- const visible = body.slice(0, bodyCapacity);
236
- if (body.length > visible.length && visible.length) {
237
- visible[visible.length - 1] = `+${body.length - visible.length + 1} source states not visible at this height`;
238
- }
239
- return renderStandaloneBrowserFrame([...header, ...visible, ...footer], {
240
- ...options,
241
- width,
242
- height,
243
- });
244
- }
245
- function buildLookupBrowserFrame(document, options = {}) {
246
- const panels = buildLookupBrowserPanels(document);
247
- const { width, height } = boundedBrowserDimensions(options);
248
- if (!panels.length)
249
- return Object.freeze({
250
- value: `${clipTerminalLine('WHOISleuth evidence browser', width)}\n${clipTerminalLine('No bounded terminal panels were available.', width)}\n`,
251
- scrollOffset: 0,
252
- maximumScrollOffset: 0,
253
- pageSize: 0,
254
- totalBodyLines: 0,
255
- });
256
- const activeIndex = Math.max(0, Math.min(panels.length - 1, Number.isSafeInteger(options.activeIndex) ? Number(options.activeIndex) : 0));
257
- const panel = panels[activeIndex];
258
- const query = safeTerminalValue(document.query, 'unknown target');
259
- const mode = safeTerminalValue(document.mode, 'unknown mode');
260
- const header = [
261
- 'WHOISleuth evidence browser',
262
- boundedTargetLine(query, width),
263
- `Mode ${clipTerminalLine(mode, Math.max(1, width - 8))}`,
264
- browserTabLine(panels, activeIndex, width),
265
- `${panel.label}:`,
266
- ];
267
- const footerLineCount = 2;
268
- const body = wrapLookupBrowserLines(panel.lines.join('\n'), width);
269
- const bodyCapacity = Math.max(1, height - header.length - footerLineCount);
270
- const maximumScrollOffset = Math.max(0, body.length - bodyCapacity);
271
- const requestedScrollOffset = Number.isSafeInteger(options.scrollOffset) ? Number(options.scrollOffset) : 0;
272
- const scrollOffset = Math.max(0, Math.min(maximumScrollOffset, requestedScrollOffset));
273
- const visibleBody = body.slice(scrollOffset, scrollOffset + bodyCapacity);
274
- const firstVisibleLine = body.length ? scrollOffset + 1 : 0;
275
- const lastVisibleLine = body.length ? scrollOffset + visibleBody.length : 0;
276
- const footer = [
277
- options.searchStatus || `View ${activeIndex + 1}/${panels.length} · lines ${firstVisibleLine}-${lastVisibleLine}/${body.length}`,
278
- '←/→ panels · ↑/↓ scroll · / search · ? help · q close',
279
- ].map((line) => clipTerminalLine(line, width));
280
- const value = [...header, ...visibleBody, ...footer].slice(0, height).join('\n');
281
- return Object.freeze({
282
- value: presentTerminalOutput(`${value}\n`, {
283
- color: options.color === true,
284
- interactive: true,
285
- palette: options.palette || 'auto',
286
- width: null,
287
- }),
288
- scrollOffset,
289
- maximumScrollOffset,
290
- pageSize: bodyCapacity,
291
- totalBodyLines: body.length,
292
- });
293
- }
294
- function renderLookupBrowser(document, options = {}) {
295
- return buildLookupBrowserFrame(document, options).value;
296
- }
297
12
  function canBrowseLookup(input, output, environment = process.env) {
298
13
  return input?.isTTY === true
299
14
  && output?.isTTY === true
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
2
2
  import { getDomain } from 'tldts';
3
3
  import { scanBoundedJson } from "../lib/bounded-json.mjs";
4
4
  import { isValidAsciiDomainName } from "../lib/hostname.mjs";
5
+ import { isRecord, recordOrEmpty } from "../lib/json-record.mjs";
5
6
  import { CliUsageError } from "./errors.mjs";
6
7
  export const CLI_MAIL_REVIEW_SCHEMA = 'whoisleuth.cli.mail-review';
7
8
  export const CLI_MAIL_REVIEW_VERSION = 3;
@@ -9,12 +10,7 @@ export const MAX_MAIL_REVIEW_INPUT_BYTES = 16 * 1024 * 1024;
9
10
  export const MAX_MAIL_REVIEW_ROWS = 500;
10
11
  const UNSAFE_TEXT_RE = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u;
11
12
  const UNSAFE_TEXT_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]+/gu;
12
- function record(value) {
13
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
14
- }
15
- function isRecord(value) {
16
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
17
- }
13
+ const record = recordOrEmpty;
18
14
  function text(value, maximum = 500) {
19
15
  return typeof value === 'string'
20
16
  ? value.replace(UNSAFE_TEXT_GLOBAL_RE, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
@@ -0,0 +1,19 @@
1
+ import { CASE_RESPONSE_PACKET_SCHEMA } from "../packages/cases/case-response-packet.mjs";
2
+ import { INVESTIGATION_CAPSULE_SCHEMA } from "../packages/investigation/investigation-capsule.mjs";
3
+ import { LOOKUP_EVIDENCE_SCHEMA } from "../lib/evidence-export.mjs";
4
+ import { validateCaseResponsePacket } from "./artifact-validation/case-response.mjs";
5
+ import { validateInvestigationCapsuleStructure } from "./artifact-validation/investigation-capsule.mjs";
6
+ import { validateLookupEvidenceArtifactStructure } from "./artifact-validation/lookup-evidence.mjs";
7
+ import { validateSignedDigestArtifactStructure } from "./artifact-validation/signed-review.mjs";
8
+ import {} from "./artifact-validation/structure-primitives.mjs";
9
+ export { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateSignedDigestArtifactStructure };
10
+ export function validateOfflineArtifactStructure(schema, value) {
11
+ if (schema === CASE_RESPONSE_PACKET_SCHEMA)
12
+ validateCaseResponsePacket(value);
13
+ else if (schema === INVESTIGATION_CAPSULE_SCHEMA)
14
+ validateInvestigationCapsuleStructure(value);
15
+ else if (schema === LOOKUP_EVIDENCE_SCHEMA)
16
+ validateLookupEvidenceArtifactStructure(value);
17
+ else
18
+ validateSignedDigestArtifactStructure(schema, value);
19
+ }
@@ -0,0 +1,269 @@
1
+ import { MAX_OFFLINE_ARTIFACT_BYTES, formatOfflineArtifactVerification, isCompleteOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
2
+ import { MAX_CASE_PACK_INPUT_BYTES, buildCliCasePack, formatCliCasePack } from "./case-pack.mjs";
3
+ import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, } from "./compare.mjs";
4
+ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
5
+ import EXIT_CODES from "./exit-codes.mjs";
6
+ import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
7
+ import { buildCliLookupBrief, formatCliLookupBrief } from "./lookup-brief.mjs";
8
+ import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, } from "./mail-review.mjs";
9
+ import { buildCliPageComparison, formatCliPageComparison } from "./page-compare.mjs";
10
+ import { MAX_SOURCE_RELIABILITY_INPUT_BYTES, buildSourceReliabilityReport, formatSourceReliabilityReport, } from "./source-reliability.mjs";
11
+ import { buildInterchangeFidelityReport, formatInterchangeFidelityReport, } from "./interchange-report.mjs";
12
+ import { buildCliCompareDocument, formatJsonDocument, } from "./formatters/json.mjs";
13
+ import { formatTerminalCompare } from "./formatters/terminal.mjs";
14
+ import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
15
+ import { REVIEW_INLINE_COMMANDS } from "./inline-command-families.mjs";
16
+ import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
17
+ async function runVerifyArtifactCommand(args, dependencies, context) {
18
+ context.setFailureLabel('Artefact verification');
19
+ let input;
20
+ try {
21
+ input = dependencies.readArtifactInput
22
+ ? await dependencies.readArtifactInput(args.source)
23
+ : await context.readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input');
24
+ }
25
+ catch (error) {
26
+ if (error instanceof CliUsageError)
27
+ throw error;
28
+ throw new CliUsageError(`Could not read artifact input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
29
+ }
30
+ if (!input.trim())
31
+ throw new CliUsageError('verify-artifact requires one JSON file or an artefact on stdin.');
32
+ const passphrase = args.passphraseSource ? await context.readPassphraseSource(args.passphraseSource) : null;
33
+ let manifest = null;
34
+ if (args.manifestSource && args.manifestEntryId) {
35
+ let raw;
36
+ try {
37
+ raw = dependencies.readArtifactInput
38
+ ? await dependencies.readArtifactInput(args.manifestSource)
39
+ : await context.readInput(args.manifestSource, MAX_OFFLINE_ARTIFACT_BYTES, 'Investigation manifest input');
40
+ }
41
+ catch (error) {
42
+ if (error instanceof CliUsageError)
43
+ throw error;
44
+ throw new CliUsageError(`Could not read investigation manifest input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
45
+ }
46
+ if (!raw.trim())
47
+ throw new CliUsageError('The investigation manifest input is empty.');
48
+ manifest = Object.freeze({ raw, entryId: args.manifestEntryId });
49
+ }
50
+ const report = await verifyOfflineArtifact(input, { passphrase, manifest });
51
+ if (!args.quiet) {
52
+ context.writeStdout(args.output === 'json'
53
+ ? formatJsonDocument(report)
54
+ : context.terminal(formatOfflineArtifactVerification(report), args.color));
55
+ }
56
+ return args.strictExit && !isCompleteOfflineArtifactVerification(report)
57
+ ? EXIT_CODES.PARTIAL_FAILURE
58
+ : EXIT_CODES.SUCCESS;
59
+ }
60
+ async function runInterchangeReportCommand(args, dependencies, context) {
61
+ context.setFailureLabel('Interchange fidelity report');
62
+ let input;
63
+ try {
64
+ input = dependencies.readArtifactInput
65
+ ? await dependencies.readArtifactInput(args.source)
66
+ : await context.readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Interchange input');
67
+ }
68
+ catch (error) {
69
+ if (error instanceof CliUsageError)
70
+ throw error;
71
+ throw new CliUsageError(`Could not read interchange input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
72
+ }
73
+ if (!input.trim())
74
+ throw new CliUsageError('interchange-report requires one JSON file or an artefact on stdin.');
75
+ const passphrase = args.passphraseSource ? await context.readPassphraseSource(args.passphraseSource) : null;
76
+ const report = await buildInterchangeFidelityReport(input, {
77
+ generatedAt: context.now(),
78
+ passphrase,
79
+ });
80
+ if (!args.quiet) {
81
+ context.writeStdout(args.output === 'json'
82
+ ? formatJsonDocument(report)
83
+ : context.terminal(formatInterchangeFidelityReport(report), args.color));
84
+ }
85
+ return EXIT_CODES.SUCCESS;
86
+ }
87
+ async function runSourceReportCommand(args, dependencies, context) {
88
+ context.setFailureLabel('Source reliability report');
89
+ let input;
90
+ try {
91
+ input = dependencies.readSourceReliabilityInput
92
+ ? await dependencies.readSourceReliabilityInput(args.source)
93
+ : await context.readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input');
94
+ }
95
+ catch (error) {
96
+ if (error instanceof CliUsageError)
97
+ throw error;
98
+ throw new CliUsageError(`Could not read source reliability input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
99
+ }
100
+ if (!input.trim())
101
+ throw new CliUsageError('source-report requires one JSON file or lookup documents on stdin.');
102
+ const report = buildSourceReliabilityReport(input, context.now());
103
+ if (!args.quiet) {
104
+ context.writeStdout(args.output === 'json'
105
+ ? formatJsonDocument(report)
106
+ : context.terminal(formatSourceReliabilityReport(report), args.color));
107
+ }
108
+ return EXIT_CODES.SUCCESS;
109
+ }
110
+ async function runCompareCommand(args, dependencies, context) {
111
+ context.setFailureLabel('Registry comparison');
112
+ let input;
113
+ try {
114
+ input = dependencies.readCompareInput
115
+ ? await dependencies.readCompareInput(args.source)
116
+ : await context.readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input');
117
+ }
118
+ catch (error) {
119
+ if (error instanceof CliUsageError)
120
+ throw error;
121
+ throw new CliUsageError(`Could not read comparison input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
122
+ }
123
+ if (!input.trim())
124
+ throw new CliUsageError('compare requires one lookup JSON file or a lookup document on stdin.');
125
+ const parsed = parseCliLookupDocument(input);
126
+ const loadComparison = dependencies.loadRegistryComparison || (() => import("../lib/registry-comparison.mjs"));
127
+ const comparisonModule = await loadComparison();
128
+ const result = compareLookupDocument(parsed, comparisonModule.compareRegistrySources, comparisonModule.compareRdapPublications);
129
+ const document = buildCliCompareDocument(result, context.now());
130
+ if (!args.quiet) {
131
+ context.writeStdout(args.output === 'json'
132
+ ? formatJsonDocument(document)
133
+ : context.terminal(formatTerminalCompare(document), args.color));
134
+ }
135
+ return EXIT_CODES.SUCCESS;
136
+ }
137
+ async function runPageCompareCommand(args, dependencies, context) {
138
+ context.setFailureLabel('Static page comparison');
139
+ const readDiffInput = dependencies.readDiffInput
140
+ || ((source) => context.readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Page comparison input'));
141
+ let leftInput;
142
+ let rightInput;
143
+ try {
144
+ [leftInput, rightInput] = await Promise.all([
145
+ readDiffInput(args.leftSource),
146
+ readDiffInput(args.rightSource),
147
+ ]);
148
+ }
149
+ catch (error) {
150
+ if (error instanceof CliUsageError)
151
+ throw error;
152
+ throw new CliUsageError(`Could not read page comparison input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
153
+ }
154
+ const document = buildCliPageComparison(leftInput, rightInput, context.now());
155
+ if (!args.quiet) {
156
+ context.writeStdout(args.output === 'json'
157
+ ? formatJsonDocument(document)
158
+ : context.terminal(formatCliPageComparison(document), args.color));
159
+ }
160
+ return EXIT_CODES.SUCCESS;
161
+ }
162
+ async function runMailReviewCommand(args, dependencies, context) {
163
+ context.setFailureLabel('Passive mail review');
164
+ let input;
165
+ try {
166
+ input = dependencies.readMailReviewInput
167
+ ? await dependencies.readMailReviewInput(args.source)
168
+ : await context.readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input');
169
+ }
170
+ catch (error) {
171
+ if (error instanceof CliUsageError)
172
+ throw error;
173
+ throw new CliUsageError(`Could not read mail review input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
174
+ }
175
+ const document = buildCliMailReview(input, context.now());
176
+ if (!args.quiet) {
177
+ context.writeStdout(args.output === 'json'
178
+ ? formatJsonDocument(document)
179
+ : context.terminal(formatCliMailReview(document), args.color));
180
+ }
181
+ return EXIT_CODES.SUCCESS;
182
+ }
183
+ async function runOfflineEvidenceReviewCommand(args, dependencies, context) {
184
+ context.setFailureLabel('Offline evidence review');
185
+ let input;
186
+ try {
187
+ input = dependencies.readArtifactInput
188
+ ? await dependencies.readArtifactInput(args.source)
189
+ : await context.readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input');
190
+ }
191
+ catch (error) {
192
+ if (error instanceof CliUsageError)
193
+ throw error;
194
+ throw new CliUsageError(`Could not read offline evidence input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
195
+ }
196
+ if (!input.trim())
197
+ throw new CliUsageError('review-evidence requires one JSON file or a document on stdin.');
198
+ const document = args.mmdbSource
199
+ ? await buildOfflineEvidenceReviewWithLocalResources(input, context.now(), { mmdbPath: args.mmdbSource })
200
+ : buildOfflineEvidenceReview(input, context.now());
201
+ if (!args.quiet) {
202
+ context.writeStdout(args.output === 'json'
203
+ ? formatJsonDocument(document)
204
+ : context.terminal(formatOfflineEvidenceReview(document), args.color));
205
+ }
206
+ if (args.strictExit) {
207
+ const result = document.result && typeof document.result === 'object' && !Array.isArray(document.result)
208
+ ? document.result
209
+ : {};
210
+ const gate = result.gate && typeof result.gate === 'object' && !Array.isArray(result.gate)
211
+ ? result.gate
212
+ : null;
213
+ const zoneMismatch = document.kind === 'zone_intent' && (result.complete !== true
214
+ || (result.counts && typeof result.counts === 'object' && !Array.isArray(result.counts)
215
+ && ['different', 'missing', 'unexpected', 'incomplete'].some((key) => Number(result.counts[key]) > 0)));
216
+ if (gate?.pass === false || zoneMismatch)
217
+ return EXIT_CODES.PARTIAL_FAILURE;
218
+ }
219
+ return EXIT_CODES.SUCCESS;
220
+ }
221
+ async function runBriefOrCasePackCommand(args, dependencies, context) {
222
+ const isBrief = args.action === 'brief';
223
+ context.setFailureLabel(isBrief ? 'Lookup brief' : 'Case pack');
224
+ let input;
225
+ try {
226
+ input = dependencies.readArtifactInput
227
+ ? await dependencies.readArtifactInput(args.source)
228
+ : await context.readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : MAX_CASE_PACK_INPUT_BYTES, isBrief ? 'Lookup brief input' : 'Case-pack input');
229
+ }
230
+ catch (error) {
231
+ if (error instanceof CliUsageError)
232
+ throw error;
233
+ throw new CliUsageError(`Could not read ${isBrief ? 'Lookup brief' : 'case-pack'} input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
234
+ }
235
+ if (!input.trim())
236
+ throw new CliUsageError(`${args.action} requires one JSON file or a document on stdin.`);
237
+ if (args.action === 'brief') {
238
+ const document = buildCliLookupBrief(input, context.now());
239
+ if (!args.quiet) {
240
+ context.writeStdout(args.output === 'json'
241
+ ? formatJsonDocument(document)
242
+ : context.terminal(formatCliLookupBrief(document), args.color));
243
+ }
244
+ }
245
+ else {
246
+ const document = buildCliCasePack(input, { audience: args.audience, reviewed: args.reviewed }, context.now());
247
+ if (!args.quiet) {
248
+ context.writeStdout(args.output === 'json'
249
+ ? formatJsonDocument(document)
250
+ : context.terminal(formatCliCasePack(document), args.color));
251
+ }
252
+ }
253
+ return EXIT_CODES.SUCCESS;
254
+ }
255
+ const REVIEW_COMMAND_HANDLERS = Object.freeze({
256
+ 'verify-artifact': runVerifyArtifactCommand,
257
+ 'interchange-report': runInterchangeReportCommand,
258
+ 'source-report': runSourceReportCommand,
259
+ 'compare': runCompareCommand,
260
+ 'page-compare': runPageCompareCommand,
261
+ 'mail-review': runMailReviewCommand,
262
+ 'review-evidence': runOfflineEvidenceReviewCommand,
263
+ 'brief': runBriefOrCasePackCommand,
264
+ 'case-pack': runBriefOrCasePackCommand,
265
+ });
266
+ function runReviewCommand(args, dependencies, context) {
267
+ return runDiscriminatedCommandHandler(REVIEW_COMMAND_HANDLERS, args, dependencies, context);
268
+ }
269
+ export { REVIEW_COMMAND_HANDLERS, runReviewCommand };