@zincapp/znvault-cli 3.3.1 → 4.0.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/README.md +32 -28
- package/dist/commands/advisor.d.ts +14 -1
- package/dist/commands/advisor.d.ts.map +1 -1
- package/dist/commands/advisor.js +352 -331
- package/dist/commands/advisor.js.map +1 -1
- package/dist/commands/agent/index.d.ts +2 -1
- package/dist/commands/agent/index.d.ts.map +1 -1
- package/dist/commands/agent/index.js +23 -19
- package/dist/commands/agent/index.js.map +1 -1
- package/dist/commands/apikey/index.d.ts +2 -1
- package/dist/commands/apikey/index.d.ts.map +1 -1
- package/dist/commands/apikey/index.js +23 -14
- package/dist/commands/apikey/index.js.map +1 -1
- package/dist/commands/audit.d.ts +2 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +7 -2
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/group.d.ts +2 -1
- package/dist/commands/group.d.ts.map +1 -1
- package/dist/commands/group.js +47 -37
- package/dist/commands/group.js.map +1 -1
- package/dist/commands/host/index.d.ts +2 -1
- package/dist/commands/host/index.d.ts.map +1 -1
- package/dist/commands/host/index.js +22 -18
- package/dist/commands/host/index.js.map +1 -1
- package/dist/commands/kms/index.d.ts +2 -1
- package/dist/commands/kms/index.d.ts.map +1 -1
- package/dist/commands/kms/index.js +11 -6
- package/dist/commands/kms/index.js.map +1 -1
- package/dist/commands/policy/index.d.ts +2 -1
- package/dist/commands/policy/index.d.ts.map +1 -1
- package/dist/commands/policy/index.js +149 -101
- package/dist/commands/policy/index.js.map +1 -1
- package/dist/commands/quarantine.d.ts +2 -1
- package/dist/commands/quarantine.d.ts.map +1 -1
- package/dist/commands/quarantine.js +380 -339
- package/dist/commands/quarantine.js.map +1 -1
- package/dist/commands/role.d.ts +2 -1
- package/dist/commands/role.d.ts.map +1 -1
- package/dist/commands/role.js +62 -40
- package/dist/commands/role.js.map +1 -1
- package/dist/commands/ssh/index.d.ts +2 -1
- package/dist/commands/ssh/index.d.ts.map +1 -1
- package/dist/commands/ssh/index.js +18 -13
- package/dist/commands/ssh/index.js.map +1 -1
- package/dist/commands/sso/index.d.ts +2 -1
- package/dist/commands/sso/index.d.ts.map +1 -1
- package/dist/commands/sso/index.js +8 -5
- package/dist/commands/sso/index.js.map +1 -1
- package/dist/commands/superadmin/accounts.d.ts +10 -0
- package/dist/commands/superadmin/accounts.d.ts.map +1 -0
- package/dist/commands/{superadmin.js → superadmin/accounts.js} +16 -15
- package/dist/commands/superadmin/accounts.js.map +1 -0
- package/dist/commands/superadmin/index.d.ts +20 -0
- package/dist/commands/superadmin/index.d.ts.map +1 -0
- package/dist/commands/superadmin/index.js +60 -0
- package/dist/commands/superadmin/index.js.map +1 -0
- package/dist/commands/user.d.ts +2 -1
- package/dist/commands/user.d.ts.map +1 -1
- package/dist/commands/user.js +15 -8
- package/dist/commands/user.js.map +1 -1
- package/dist/index.js +18 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/command-context.d.ts +49 -0
- package/dist/lib/command-context.d.ts.map +1 -0
- package/dist/lib/command-context.js +81 -0
- package/dist/lib/command-context.js.map +1 -0
- package/dist/lib/context-help.d.ts.map +1 -1
- package/dist/lib/context-help.js +5 -0
- package/dist/lib/context-help.js.map +1 -1
- package/package.json +1 -1
- package/dist/commands/superadmin.d.ts +0 -3
- package/dist/commands/superadmin.d.ts.map +0 -1
- package/dist/commands/superadmin.js.map +0 -1
|
@@ -3,391 +3,432 @@ import { client } from '../lib/client.js';
|
|
|
3
3
|
import { promptConfirm } from '../lib/prompts.js';
|
|
4
4
|
import * as output from '../lib/output.js';
|
|
5
5
|
import { formatTtl as formatDuration } from '../lib/format-helpers.js';
|
|
6
|
+
import { resolveContext, withRegisterContext, } from '../lib/command-context.js';
|
|
6
7
|
function formatBackoffLevel(level) {
|
|
7
8
|
const labels = ['', '1m', '5m', '30m', '4h', '24h'];
|
|
8
9
|
return labels[level] ?? `Level ${level}`;
|
|
9
10
|
}
|
|
10
|
-
export function registerQuarantineCommands(
|
|
11
|
-
const
|
|
11
|
+
export function registerQuarantineCommands(parent, opts) {
|
|
12
|
+
const ctx = resolveContext(opts);
|
|
13
|
+
withRegisterContext(ctx, () => { registerQuarantineCommandsInner(parent, ctx); });
|
|
14
|
+
}
|
|
15
|
+
function registerQuarantineCommandsInner(parent, ctx) {
|
|
16
|
+
// Helper: only register --tenant in superadmin context
|
|
17
|
+
const t = (cmd, desc, shortFlag = true) => ctx === 'superadmin'
|
|
18
|
+
? cmd.option(shortFlag ? '-t, --tenant <tenant>' : '--tenant <tenant>', desc)
|
|
19
|
+
: cmd;
|
|
20
|
+
const quarantine = parent
|
|
12
21
|
.command('quarantine')
|
|
13
|
-
.description(
|
|
22
|
+
.description(ctx === 'superadmin'
|
|
23
|
+
? 'IP quarantine management (cross-tenant)'
|
|
24
|
+
: 'IP quarantine management commands');
|
|
14
25
|
// List quarantined IPs
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
{
|
|
27
|
+
const listCmd = quarantine
|
|
28
|
+
.command('list')
|
|
29
|
+
.description('List quarantined IPs')
|
|
30
|
+
.option('-s, --status <status>', 'Filter by status (active, released, expired)');
|
|
31
|
+
t(listCmd, 'Filter by tenant ID');
|
|
32
|
+
listCmd
|
|
33
|
+
.option('--include-expired', 'Include expired quarantines')
|
|
34
|
+
.option('--limit <number>', 'Number of entries to show', '50')
|
|
35
|
+
.option('--json', 'Output as JSON')
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
const spinner = output.spinner('Fetching quarantined IPs...').start();
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.listQuarantines({
|
|
40
|
+
status: options.status,
|
|
41
|
+
tenantId: options.tenant,
|
|
42
|
+
includeExpired: options.includeExpired,
|
|
43
|
+
limit: parseInt(options.limit, 10),
|
|
44
|
+
});
|
|
45
|
+
spinner.stop();
|
|
46
|
+
if (options.json) {
|
|
47
|
+
output.json(result);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (result.items.length === 0) {
|
|
51
|
+
output.info('No quarantined IPs found');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
output.table(['IP Address', 'Status', 'Failures', 'Level', 'Blocked Until', 'Tenant', 'Reason'], result.items.map(q => [
|
|
55
|
+
q.ipAddress,
|
|
56
|
+
output.formatStatus(q.status),
|
|
57
|
+
q.failureCount,
|
|
58
|
+
formatBackoffLevel(q.backoffLevel),
|
|
59
|
+
q.status === 'active' ? output.formatRelativeTime(q.blockedUntil) : '-',
|
|
60
|
+
q.tenantId ?? '-',
|
|
61
|
+
q.reason.substring(0, 30),
|
|
62
|
+
]));
|
|
63
|
+
output.info(`Showing ${result.items.length} of ${result.pagination.total} quarantines`);
|
|
36
64
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
spinner.fail('Failed to list quarantines');
|
|
67
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
68
|
+
process.exit(1);
|
|
40
69
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
output.formatStatus(q.status),
|
|
44
|
-
q.failureCount,
|
|
45
|
-
formatBackoffLevel(q.backoffLevel),
|
|
46
|
-
q.status === 'active' ? output.formatRelativeTime(q.blockedUntil) : '-',
|
|
47
|
-
q.tenantId ?? '-',
|
|
48
|
-
q.reason.substring(0, 30),
|
|
49
|
-
]));
|
|
50
|
-
output.info(`Showing ${result.items.length} of ${result.pagination.total} quarantines`);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
spinner.fail('Failed to list quarantines');
|
|
54
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
58
72
|
// Get quarantine details
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
'IP Address': q.ipAddress,
|
|
77
|
-
'Status': output.formatStatus(q.status),
|
|
78
|
-
'Tenant': q.tenantId ?? 'N/A (System)',
|
|
79
|
-
'Failure Count': q.failureCount,
|
|
80
|
-
'Backoff Level': `${q.backoffLevel} (${formatBackoffLevel(q.backoffLevel)})`,
|
|
81
|
-
'Blocked Until': q.status === 'active' ? output.formatDate(q.blockedUntil) : '-',
|
|
82
|
-
'Reason': q.reason,
|
|
83
|
-
'First Failure': output.formatDate(q.firstFailureAt),
|
|
84
|
-
'Last Failure': output.formatDate(q.lastFailureAt),
|
|
85
|
-
});
|
|
86
|
-
if (q.lastFailurePath) {
|
|
87
|
-
output.section('Last Request');
|
|
73
|
+
{
|
|
74
|
+
const getCmd = quarantine
|
|
75
|
+
.command('get <id>')
|
|
76
|
+
.description('Get quarantine details by ID');
|
|
77
|
+
t(getCmd, 'Tenant ID (superadmin only, cross-tenant)');
|
|
78
|
+
getCmd
|
|
79
|
+
.option('--json', 'Output as JSON')
|
|
80
|
+
.action(async (id, options) => {
|
|
81
|
+
const spinner = output.spinner('Fetching quarantine details...').start();
|
|
82
|
+
try {
|
|
83
|
+
const q = await client.getQuarantine(id, options.tenant);
|
|
84
|
+
spinner.stop();
|
|
85
|
+
if (options.json) {
|
|
86
|
+
output.json(q);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
output.section('Quarantine Details');
|
|
88
90
|
output.keyValue({
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
'
|
|
91
|
+
'ID': q.id,
|
|
92
|
+
'IP Address': q.ipAddress,
|
|
93
|
+
'Status': output.formatStatus(q.status),
|
|
94
|
+
'Tenant': q.tenantId ?? 'N/A (System)',
|
|
95
|
+
'Failure Count': q.failureCount,
|
|
96
|
+
'Backoff Level': `${q.backoffLevel} (${formatBackoffLevel(q.backoffLevel)})`,
|
|
97
|
+
'Blocked Until': q.status === 'active' ? output.formatDate(q.blockedUntil) : '-',
|
|
98
|
+
'Reason': q.reason,
|
|
99
|
+
'First Failure': output.formatDate(q.firstFailureAt),
|
|
100
|
+
'Last Failure': output.formatDate(q.lastFailureAt),
|
|
94
101
|
});
|
|
102
|
+
if (q.lastFailurePath) {
|
|
103
|
+
output.section('Last Request');
|
|
104
|
+
output.keyValue({
|
|
105
|
+
'Path': q.lastFailurePath,
|
|
106
|
+
'Method': q.lastFailureMethod ?? '-',
|
|
107
|
+
'User Agent': q.userAgent?.substring(0, 50) ?? '-',
|
|
108
|
+
'API Key ID': q.apiKeyId ?? '-',
|
|
109
|
+
'Agent ID': q.agentId ?? '-',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (q.releasedAt) {
|
|
113
|
+
output.section('Release Info');
|
|
114
|
+
output.keyValue({
|
|
115
|
+
'Released At': output.formatDate(q.releasedAt),
|
|
116
|
+
'Released By': q.releasedBy ?? '-',
|
|
117
|
+
'Release Reason': q.releaseReason ?? '-',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
95
121
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
output.
|
|
99
|
-
|
|
100
|
-
'Released By': q.releasedBy ?? '-',
|
|
101
|
-
'Release Reason': q.releaseReason ?? '-',
|
|
102
|
-
});
|
|
122
|
+
catch (err) {
|
|
123
|
+
spinner.fail('Failed to get quarantine');
|
|
124
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
125
|
+
process.exit(1);
|
|
103
126
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
catch (err) {
|
|
107
|
-
spinner.fail('Failed to get quarantine');
|
|
108
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
112
129
|
// Release a quarantine by ID
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
{
|
|
131
|
+
const releaseCmd = quarantine
|
|
132
|
+
.command('release <id> <reason>')
|
|
133
|
+
.description('Release a quarantine by ID');
|
|
134
|
+
t(releaseCmd, 'Tenant ID (superadmin only, cross-tenant)');
|
|
135
|
+
releaseCmd
|
|
136
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
137
|
+
.option('--json', 'Output as JSON')
|
|
138
|
+
.action(async (id, reason, options) => {
|
|
139
|
+
try {
|
|
140
|
+
// Get quarantine info first
|
|
141
|
+
const q = await client.getQuarantine(id, options.tenant);
|
|
142
|
+
if (!options.yes) {
|
|
143
|
+
const confirmed = await promptConfirm(`Are you sure you want to release quarantine for IP ${q.ipAddress}?`);
|
|
144
|
+
if (!confirmed) {
|
|
145
|
+
output.info('Cancelled');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const spinner = output.spinner('Releasing quarantine...').start();
|
|
150
|
+
try {
|
|
151
|
+
const result = await client.releaseQuarantine(id, reason, options.tenant);
|
|
152
|
+
spinner.succeed('Quarantine released');
|
|
153
|
+
if (options.json) {
|
|
154
|
+
output.json(result);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
output.success(result.message);
|
|
128
158
|
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
spinner.fail('Failed to release quarantine');
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
166
|
+
process.exit(1);
|
|
129
167
|
}
|
|
130
|
-
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Release all quarantines for an IP
|
|
171
|
+
{
|
|
172
|
+
const releaseIpCmd = quarantine
|
|
173
|
+
.command('release-ip <ip> <reason>')
|
|
174
|
+
.description('Release all quarantines for an IP address');
|
|
175
|
+
t(releaseIpCmd, 'Filter by tenant ID');
|
|
176
|
+
releaseIpCmd
|
|
177
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
178
|
+
.option('--json', 'Output as JSON')
|
|
179
|
+
.action(async (ip, reason, options) => {
|
|
180
|
+
try {
|
|
181
|
+
if (!options.yes) {
|
|
182
|
+
const confirmed = await promptConfirm(`Are you sure you want to release all quarantines for IP ${ip}?`);
|
|
183
|
+
if (!confirmed) {
|
|
184
|
+
output.info('Cancelled');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const spinner = output.spinner('Releasing IP quarantines...').start();
|
|
189
|
+
try {
|
|
190
|
+
const result = await client.releaseQuarantineIp(ip, reason, options.tenant);
|
|
191
|
+
spinner.succeed('Quarantine(s) released');
|
|
192
|
+
if (options.json) {
|
|
193
|
+
output.json(result);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
output.success(result.message);
|
|
197
|
+
if (result.releasedCount !== undefined) {
|
|
198
|
+
output.info(`Released ${result.releasedCount} quarantine(s)`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
spinner.fail('Failed to release IP quarantines');
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Get failure history for an IP
|
|
213
|
+
{
|
|
214
|
+
const historyCmd = quarantine
|
|
215
|
+
.command('history <ip>')
|
|
216
|
+
.description('Get failure history for an IP address');
|
|
217
|
+
t(historyCmd, 'Filter by tenant ID');
|
|
218
|
+
historyCmd
|
|
219
|
+
.option('--limit <number>', 'Number of entries to show', '100')
|
|
220
|
+
.option('--json', 'Output as JSON')
|
|
221
|
+
.action(async (ip, options) => {
|
|
222
|
+
const spinner = output.spinner('Fetching failure history...').start();
|
|
131
223
|
try {
|
|
132
|
-
const result = await client.
|
|
133
|
-
|
|
224
|
+
const result = await client.getQuarantineHistory(ip, {
|
|
225
|
+
limit: parseInt(options.limit, 10),
|
|
226
|
+
tenantId: options.tenant,
|
|
227
|
+
});
|
|
228
|
+
spinner.stop();
|
|
134
229
|
if (options.json) {
|
|
135
230
|
output.json(result);
|
|
136
231
|
return;
|
|
137
232
|
}
|
|
138
|
-
|
|
233
|
+
if (result.failures.length === 0) {
|
|
234
|
+
output.info(`No failure history found for IP ${ip}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
output.section(`Failure History for ${ip}`);
|
|
238
|
+
output.table(['Time', 'Type', 'Path', 'Method', 'Username', 'API Key', 'Quarantine'], result.failures.map(f => [
|
|
239
|
+
output.formatRelativeTime(f.ts),
|
|
240
|
+
f.failureType,
|
|
241
|
+
f.path?.substring(0, 25) ?? '-',
|
|
242
|
+
f.method ?? '-',
|
|
243
|
+
f.username ?? '-',
|
|
244
|
+
f.apiKeyPrefix ?? '-',
|
|
245
|
+
f.quarantineId ? 'Yes' : '-',
|
|
246
|
+
]));
|
|
247
|
+
output.info(`Showing ${result.failures.length} entries`);
|
|
139
248
|
}
|
|
140
249
|
catch (err) {
|
|
141
|
-
spinner.fail('Failed to
|
|
142
|
-
|
|
250
|
+
spinner.fail('Failed to get failure history');
|
|
251
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
252
|
+
process.exit(1);
|
|
143
253
|
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!confirmed) {
|
|
162
|
-
output.info('Cancelled');
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Get quarantine statistics
|
|
257
|
+
{
|
|
258
|
+
const statsCmd = quarantine
|
|
259
|
+
.command('stats')
|
|
260
|
+
.description('Get quarantine statistics');
|
|
261
|
+
t(statsCmd, 'Filter by tenant ID');
|
|
262
|
+
statsCmd
|
|
263
|
+
.option('--json', 'Output as JSON')
|
|
264
|
+
.action(async (options) => {
|
|
265
|
+
const spinner = output.spinner('Fetching statistics...').start();
|
|
266
|
+
try {
|
|
267
|
+
const stats = await client.getQuarantineStats(options.tenant);
|
|
268
|
+
spinner.stop();
|
|
269
|
+
if (options.json) {
|
|
270
|
+
output.json(stats);
|
|
163
271
|
return;
|
|
164
272
|
}
|
|
273
|
+
output.section('Quarantine Statistics');
|
|
274
|
+
output.keyValue({
|
|
275
|
+
'Active Quarantines': stats.activeQuarantines,
|
|
276
|
+
'Quarantines (24h)': stats.totalQuarantines24h,
|
|
277
|
+
'Unique IPs (24h)': stats.uniqueIpsQuarantined24h,
|
|
278
|
+
'Failures (1h)': stats.failuresLast1h,
|
|
279
|
+
'Failures (24h)': stats.failuresLast24h,
|
|
280
|
+
});
|
|
281
|
+
if (stats.topFailingIps.length > 0) {
|
|
282
|
+
output.section('Top Failing IPs (24h)');
|
|
283
|
+
output.table(['IP Address', 'Failures'], stats.topFailingIps.map(ip => [ip.ip, ip.count]));
|
|
284
|
+
}
|
|
285
|
+
console.log();
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
spinner.fail('Failed to get statistics');
|
|
289
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
290
|
+
process.exit(1);
|
|
165
291
|
}
|
|
166
|
-
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// Get quarantine configuration
|
|
295
|
+
//
|
|
296
|
+
// Semantics:
|
|
297
|
+
// - Tenant context (top-level `quarantine config`): operates on caller's
|
|
298
|
+
// own tenant (via JWT).
|
|
299
|
+
// - Superadmin context without --tenant: operates on system (null-tenant) config.
|
|
300
|
+
// - Superadmin context with --tenant X: operates on tenant X's config.
|
|
301
|
+
{
|
|
302
|
+
const configCmd = quarantine
|
|
303
|
+
.command('config')
|
|
304
|
+
.description('Get quarantine configuration');
|
|
305
|
+
t(configCmd, 'Tenant ID (omit for system config)');
|
|
306
|
+
configCmd
|
|
307
|
+
.option('--json', 'Output as JSON')
|
|
308
|
+
.action(async (options) => {
|
|
309
|
+
const spinner = output.spinner('Fetching configuration...').start();
|
|
167
310
|
try {
|
|
168
|
-
const
|
|
169
|
-
spinner.
|
|
311
|
+
const config = await client.getQuarantineConfig(options.tenant);
|
|
312
|
+
spinner.stop();
|
|
170
313
|
if (options.json) {
|
|
171
|
-
output.json(
|
|
314
|
+
output.json(config);
|
|
172
315
|
return;
|
|
173
316
|
}
|
|
174
|
-
output.
|
|
175
|
-
|
|
176
|
-
|
|
317
|
+
output.section('Quarantine Configuration');
|
|
318
|
+
output.keyValue({
|
|
319
|
+
'Enabled': config.enabled ? 'Yes' : 'No',
|
|
320
|
+
'Failures Before Quarantine': config.failuresBeforeQuarantine,
|
|
321
|
+
'Lockdown Threshold (IPs)': config.lockdownThresholdIps,
|
|
322
|
+
'Lockdown Window': formatDuration(config.lockdownThresholdWindowSeconds),
|
|
323
|
+
'Auto-Expire After': formatDuration(config.autoExpireAfterSeconds),
|
|
324
|
+
});
|
|
325
|
+
output.section('Backoff Durations');
|
|
326
|
+
output.keyValue({
|
|
327
|
+
'Level 1': formatDuration(config.backoffLevel1Seconds),
|
|
328
|
+
'Level 2': formatDuration(config.backoffLevel2Seconds),
|
|
329
|
+
'Level 3': formatDuration(config.backoffLevel3Seconds),
|
|
330
|
+
'Level 4': formatDuration(config.backoffLevel4Seconds),
|
|
331
|
+
'Level 5': formatDuration(config.backoffLevel5Seconds),
|
|
332
|
+
});
|
|
333
|
+
if (config.ipAllowlist.length > 0) {
|
|
334
|
+
output.section('IP Allowlist');
|
|
335
|
+
config.ipAllowlist.forEach(ip => output.info(` ${ip}`));
|
|
177
336
|
}
|
|
337
|
+
console.log();
|
|
178
338
|
}
|
|
179
339
|
catch (err) {
|
|
180
|
-
spinner.fail('Failed to
|
|
181
|
-
|
|
340
|
+
spinner.fail('Failed to get configuration');
|
|
341
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
342
|
+
process.exit(1);
|
|
182
343
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Set quarantine configuration
|
|
347
|
+
{
|
|
348
|
+
const setConfigCmd = quarantine
|
|
349
|
+
.command('set-config')
|
|
350
|
+
.description('Update quarantine configuration');
|
|
351
|
+
t(setConfigCmd, 'Tenant ID (omit for system config)');
|
|
352
|
+
setConfigCmd
|
|
353
|
+
.option('--enabled <bool>', 'Enable/disable quarantine')
|
|
354
|
+
.option('--failures <num>', 'Failures before quarantine')
|
|
355
|
+
.option('--lockdown-ips <num>', 'Number of IPs to trigger lockdown')
|
|
356
|
+
.option('--lockdown-window <sec>', 'Lockdown threshold window (seconds)')
|
|
357
|
+
.option('--level1 <sec>', 'Level 1 backoff (seconds)')
|
|
358
|
+
.option('--level2 <sec>', 'Level 2 backoff (seconds)')
|
|
359
|
+
.option('--level3 <sec>', 'Level 3 backoff (seconds)')
|
|
360
|
+
.option('--level4 <sec>', 'Level 4 backoff (seconds)')
|
|
361
|
+
.option('--level5 <sec>', 'Level 5 backoff (seconds)')
|
|
362
|
+
.option('--auto-expire <sec>', 'Auto-expire after (seconds)')
|
|
363
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
364
|
+
.option('--json', 'Output as JSON')
|
|
365
|
+
.action(async (options) => {
|
|
366
|
+
const updates = {};
|
|
367
|
+
if (options.enabled !== undefined) {
|
|
368
|
+
updates.enabled = options.enabled === 'true';
|
|
207
369
|
}
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
return;
|
|
370
|
+
if (options.failures !== undefined) {
|
|
371
|
+
updates.failuresBeforeQuarantine = parseInt(options.failures, 10);
|
|
211
372
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
output.formatRelativeTime(f.ts),
|
|
215
|
-
f.failureType,
|
|
216
|
-
f.path?.substring(0, 25) ?? '-',
|
|
217
|
-
f.method ?? '-',
|
|
218
|
-
f.username ?? '-',
|
|
219
|
-
f.apiKeyPrefix ?? '-',
|
|
220
|
-
f.quarantineId ? 'Yes' : '-',
|
|
221
|
-
]));
|
|
222
|
-
output.info(`Showing ${result.failures.length} entries`);
|
|
223
|
-
}
|
|
224
|
-
catch (err) {
|
|
225
|
-
spinner.fail('Failed to get failure history');
|
|
226
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
// Get quarantine statistics
|
|
231
|
-
quarantine
|
|
232
|
-
.command('stats')
|
|
233
|
-
.description('Get quarantine statistics')
|
|
234
|
-
.option('-t, --tenant <tenant>', 'Filter by tenant ID')
|
|
235
|
-
.option('--json', 'Output as JSON')
|
|
236
|
-
.action(async (options) => {
|
|
237
|
-
const spinner = output.spinner('Fetching statistics...').start();
|
|
238
|
-
try {
|
|
239
|
-
const stats = await client.getQuarantineStats(options.tenant);
|
|
240
|
-
spinner.stop();
|
|
241
|
-
if (options.json) {
|
|
242
|
-
output.json(stats);
|
|
243
|
-
return;
|
|
373
|
+
if (options.lockdownIps !== undefined) {
|
|
374
|
+
updates.lockdownThresholdIps = parseInt(options.lockdownIps, 10);
|
|
244
375
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'Active Quarantines': stats.activeQuarantines,
|
|
248
|
-
'Quarantines (24h)': stats.totalQuarantines24h,
|
|
249
|
-
'Unique IPs (24h)': stats.uniqueIpsQuarantined24h,
|
|
250
|
-
'Failures (1h)': stats.failuresLast1h,
|
|
251
|
-
'Failures (24h)': stats.failuresLast24h,
|
|
252
|
-
});
|
|
253
|
-
if (stats.topFailingIps.length > 0) {
|
|
254
|
-
output.section('Top Failing IPs (24h)');
|
|
255
|
-
output.table(['IP Address', 'Failures'], stats.topFailingIps.map(ip => [ip.ip, ip.count]));
|
|
376
|
+
if (options.lockdownWindow !== undefined) {
|
|
377
|
+
updates.lockdownThresholdWindowSeconds = parseInt(options.lockdownWindow, 10);
|
|
256
378
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
catch (err) {
|
|
260
|
-
spinner.fail('Failed to get statistics');
|
|
261
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
262
|
-
process.exit(1);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
// Get quarantine configuration
|
|
266
|
-
quarantine
|
|
267
|
-
.command('config')
|
|
268
|
-
.description('Get quarantine configuration')
|
|
269
|
-
.option('-t, --tenant <tenant>', 'Tenant ID (superadmin only)')
|
|
270
|
-
.option('--json', 'Output as JSON')
|
|
271
|
-
.action(async (options) => {
|
|
272
|
-
const spinner = output.spinner('Fetching configuration...').start();
|
|
273
|
-
try {
|
|
274
|
-
const config = await client.getQuarantineConfig(options.tenant);
|
|
275
|
-
spinner.stop();
|
|
276
|
-
if (options.json) {
|
|
277
|
-
output.json(config);
|
|
278
|
-
return;
|
|
379
|
+
if (options.level1 !== undefined) {
|
|
380
|
+
updates.backoffLevel1Seconds = parseInt(options.level1, 10);
|
|
279
381
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
'Enabled': config.enabled ? 'Yes' : 'No',
|
|
283
|
-
'Failures Before Quarantine': config.failuresBeforeQuarantine,
|
|
284
|
-
'Lockdown Threshold (IPs)': config.lockdownThresholdIps,
|
|
285
|
-
'Lockdown Window': formatDuration(config.lockdownThresholdWindowSeconds),
|
|
286
|
-
'Auto-Expire After': formatDuration(config.autoExpireAfterSeconds),
|
|
287
|
-
});
|
|
288
|
-
output.section('Backoff Durations');
|
|
289
|
-
output.keyValue({
|
|
290
|
-
'Level 1': formatDuration(config.backoffLevel1Seconds),
|
|
291
|
-
'Level 2': formatDuration(config.backoffLevel2Seconds),
|
|
292
|
-
'Level 3': formatDuration(config.backoffLevel3Seconds),
|
|
293
|
-
'Level 4': formatDuration(config.backoffLevel4Seconds),
|
|
294
|
-
'Level 5': formatDuration(config.backoffLevel5Seconds),
|
|
295
|
-
});
|
|
296
|
-
if (config.ipAllowlist.length > 0) {
|
|
297
|
-
output.section('IP Allowlist');
|
|
298
|
-
config.ipAllowlist.forEach(ip => output.info(` ${ip}`));
|
|
382
|
+
if (options.level2 !== undefined) {
|
|
383
|
+
updates.backoffLevel2Seconds = parseInt(options.level2, 10);
|
|
299
384
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
.option('--lockdown-window <sec>', 'Lockdown threshold window (seconds)')
|
|
317
|
-
.option('--level1 <sec>', 'Level 1 backoff (seconds)')
|
|
318
|
-
.option('--level2 <sec>', 'Level 2 backoff (seconds)')
|
|
319
|
-
.option('--level3 <sec>', 'Level 3 backoff (seconds)')
|
|
320
|
-
.option('--level4 <sec>', 'Level 4 backoff (seconds)')
|
|
321
|
-
.option('--level5 <sec>', 'Level 5 backoff (seconds)')
|
|
322
|
-
.option('--auto-expire <sec>', 'Auto-expire after (seconds)')
|
|
323
|
-
.option('-y, --yes', 'Skip confirmation')
|
|
324
|
-
.option('--json', 'Output as JSON')
|
|
325
|
-
.action(async (options) => {
|
|
326
|
-
const updates = {};
|
|
327
|
-
if (options.enabled !== undefined) {
|
|
328
|
-
updates.enabled = options.enabled === 'true';
|
|
329
|
-
}
|
|
330
|
-
if (options.failures !== undefined) {
|
|
331
|
-
updates.failuresBeforeQuarantine = parseInt(options.failures, 10);
|
|
332
|
-
}
|
|
333
|
-
if (options.lockdownIps !== undefined) {
|
|
334
|
-
updates.lockdownThresholdIps = parseInt(options.lockdownIps, 10);
|
|
335
|
-
}
|
|
336
|
-
if (options.lockdownWindow !== undefined) {
|
|
337
|
-
updates.lockdownThresholdWindowSeconds = parseInt(options.lockdownWindow, 10);
|
|
338
|
-
}
|
|
339
|
-
if (options.level1 !== undefined) {
|
|
340
|
-
updates.backoffLevel1Seconds = parseInt(options.level1, 10);
|
|
341
|
-
}
|
|
342
|
-
if (options.level2 !== undefined) {
|
|
343
|
-
updates.backoffLevel2Seconds = parseInt(options.level2, 10);
|
|
344
|
-
}
|
|
345
|
-
if (options.level3 !== undefined) {
|
|
346
|
-
updates.backoffLevel3Seconds = parseInt(options.level3, 10);
|
|
347
|
-
}
|
|
348
|
-
if (options.level4 !== undefined) {
|
|
349
|
-
updates.backoffLevel4Seconds = parseInt(options.level4, 10);
|
|
350
|
-
}
|
|
351
|
-
if (options.level5 !== undefined) {
|
|
352
|
-
updates.backoffLevel5Seconds = parseInt(options.level5, 10);
|
|
353
|
-
}
|
|
354
|
-
if (options.autoExpire !== undefined) {
|
|
355
|
-
updates.autoExpireAfterSeconds = parseInt(options.autoExpire, 10);
|
|
356
|
-
}
|
|
357
|
-
if (Object.keys(updates).length === 0) {
|
|
358
|
-
output.error('No configuration options provided');
|
|
359
|
-
output.info('Use --help to see available options');
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
try {
|
|
363
|
-
if (!options.yes) {
|
|
364
|
-
output.section('Configuration Changes');
|
|
365
|
-
output.keyValue(updates);
|
|
366
|
-
const confirmed = await promptConfirm('Apply these changes?');
|
|
367
|
-
if (!confirmed) {
|
|
368
|
-
output.info('Cancelled');
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
385
|
+
if (options.level3 !== undefined) {
|
|
386
|
+
updates.backoffLevel3Seconds = parseInt(options.level3, 10);
|
|
387
|
+
}
|
|
388
|
+
if (options.level4 !== undefined) {
|
|
389
|
+
updates.backoffLevel4Seconds = parseInt(options.level4, 10);
|
|
390
|
+
}
|
|
391
|
+
if (options.level5 !== undefined) {
|
|
392
|
+
updates.backoffLevel5Seconds = parseInt(options.level5, 10);
|
|
393
|
+
}
|
|
394
|
+
if (options.autoExpire !== undefined) {
|
|
395
|
+
updates.autoExpireAfterSeconds = parseInt(options.autoExpire, 10);
|
|
396
|
+
}
|
|
397
|
+
if (Object.keys(updates).length === 0) {
|
|
398
|
+
output.error('No configuration options provided');
|
|
399
|
+
output.info('Use --help to see available options');
|
|
400
|
+
process.exit(1);
|
|
371
401
|
}
|
|
372
|
-
const spinner = output.spinner('Updating configuration...').start();
|
|
373
402
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
403
|
+
if (!options.yes) {
|
|
404
|
+
output.section('Configuration Changes');
|
|
405
|
+
output.keyValue(updates);
|
|
406
|
+
const confirmed = await promptConfirm('Apply these changes?');
|
|
407
|
+
if (!confirmed) {
|
|
408
|
+
output.info('Cancelled');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const spinner = output.spinner('Updating configuration...').start();
|
|
413
|
+
try {
|
|
414
|
+
const result = await client.updateQuarantineConfig(updates, options.tenant);
|
|
415
|
+
spinner.succeed('Configuration updated');
|
|
416
|
+
if (options.json) {
|
|
417
|
+
output.json(result);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
output.success('Quarantine configuration updated successfully');
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
spinner.fail('Failed to update configuration');
|
|
424
|
+
throw err;
|
|
379
425
|
}
|
|
380
|
-
output.success('Quarantine configuration updated successfully');
|
|
381
426
|
}
|
|
382
427
|
catch (err) {
|
|
383
|
-
|
|
384
|
-
|
|
428
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
429
|
+
process.exit(1);
|
|
385
430
|
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
392
433
|
}
|
|
393
434
|
//# sourceMappingURL=quarantine.js.map
|