@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.
- 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 +16 -9
- package/docs/cli.md +16 -2
- 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 +47 -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/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 +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-record-operations.mjs +235 -0
- 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 +235 -61
- 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/interchange/external-findings-import.mjs +113 -45
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- 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/lookup-browser.mjs
CHANGED
|
@@ -1,299 +1,14 @@
|
|
|
1
1
|
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
package/cli/mail-review.mjs
CHANGED
|
@@ -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
|
-
|
|
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 };
|