raqeb-cli 1.0.0 ā 1.3.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 +303 -49
- package/bin/raqeb-completion.js +32 -0
- package/bin/raqeb.js +154 -1
- package/lib/interactive.js +668 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,53 +1,133 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Raqeb CLI
|
|
2
2
|
|
|
3
|
-
Official
|
|
3
|
+
Official command-line tool for **Raqeb Privileged Access Management (PAM)** platform.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## š Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- š **Database PAM**: Get temporary database credentials with auto-expiration
|
|
8
|
+
- š **Secrets Management**: Securely retrieve application secrets
|
|
9
|
+
- š¤ **Service Accounts**: Programmatic API access for applications
|
|
10
|
+
- š **API Keys**: Personal access tokens for developers
|
|
11
|
+
- š **Zero Trust**: Dynamic credentials that automatically expire
|
|
12
|
+
- š **Audit Logging**: Track all access activities
|
|
13
|
+
|
|
14
|
+
## š¦ Installation
|
|
15
|
+
|
|
16
|
+
### npm (Node.js)
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
19
|
npm install -g raqeb-cli
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
###
|
|
22
|
+
### PyPI (Python)
|
|
14
23
|
|
|
15
24
|
```bash
|
|
16
|
-
|
|
25
|
+
pip install raqeb-cli
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
### Homebrew (macOS/Linux)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
brew tap Tzamun-Arabia-IT-Co/raqeb
|
|
32
|
+
brew install raqeb-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Chocolatey (Windows)
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
choco install raqeb-cli
|
|
39
|
+
```
|
|
20
40
|
|
|
21
|
-
###
|
|
41
|
+
### RPM (RedHat/Fedora/CentOS)
|
|
22
42
|
|
|
23
43
|
```bash
|
|
24
|
-
|
|
44
|
+
# Download from GitHub Releases
|
|
45
|
+
curl -LO https://github.com/Tzamun-Arabia-IT-Co/raqeb-cli/releases/latest/download/raqeb-cli.rpm
|
|
46
|
+
sudo rpm -ivh raqeb-cli.rpm
|
|
25
47
|
```
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
## šÆ Quick Start
|
|
50
|
+
|
|
51
|
+
### 1. Login with Service Account
|
|
28
52
|
|
|
29
53
|
```bash
|
|
30
|
-
|
|
54
|
+
# Using service account API key (for applications/CI/CD)
|
|
55
|
+
raqeb login --api-key sa_your_service_account_key
|
|
56
|
+
|
|
57
|
+
# Or using personal API key (for developers)
|
|
58
|
+
raqeb login --api-key ak_your_personal_key
|
|
31
59
|
```
|
|
32
60
|
|
|
33
|
-
###
|
|
61
|
+
### 2. Get Temporary Database Credentials
|
|
34
62
|
|
|
35
63
|
```bash
|
|
36
|
-
|
|
64
|
+
# Get read-only access for 2 hours
|
|
65
|
+
raqeb db connect prod-mysql --ttl 2 --access-level read-only
|
|
66
|
+
|
|
67
|
+
# Returns:
|
|
68
|
+
# Username: temp_user_abc123
|
|
69
|
+
# Password: random_secure_password
|
|
70
|
+
# Host: mysql.example.com
|
|
71
|
+
# Port: 3306
|
|
72
|
+
# Expires: 2026-02-14 20:00:00
|
|
37
73
|
```
|
|
38
74
|
|
|
39
|
-
###
|
|
75
|
+
### 3. Retrieve Application Secrets
|
|
40
76
|
|
|
41
77
|
```bash
|
|
42
|
-
|
|
78
|
+
# Get secret value
|
|
79
|
+
raqeb secrets get stripe-api-key
|
|
80
|
+
|
|
81
|
+
# Returns the decrypted secret value
|
|
43
82
|
```
|
|
44
83
|
|
|
45
|
-
|
|
84
|
+
### 4. Manage API Keys
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# List your API keys
|
|
88
|
+
raqeb keys list
|
|
89
|
+
|
|
90
|
+
# Create new API key
|
|
91
|
+
raqeb keys create "My Dev Key" --scopes secrets:read,databases:read
|
|
92
|
+
|
|
93
|
+
# Delete API key
|
|
94
|
+
raqeb keys delete <key-id>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## š Commands Reference
|
|
46
98
|
|
|
47
99
|
### Authentication
|
|
48
100
|
|
|
49
101
|
```bash
|
|
102
|
+
# Login with API key
|
|
50
103
|
raqeb login --api-key <key>
|
|
104
|
+
|
|
105
|
+
# Login with interactive prompt
|
|
106
|
+
raqeb login
|
|
107
|
+
|
|
108
|
+
# Logout
|
|
109
|
+
raqeb logout
|
|
110
|
+
|
|
111
|
+
# Show current user
|
|
112
|
+
raqeb whoami
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Database PAM
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Get temporary database credentials
|
|
119
|
+
raqeb db connect <database-id> [options]
|
|
120
|
+
--ttl <hours> Time to live in hours (default: 4, max: 24)
|
|
121
|
+
--access-level <level> Access level: read-only, read-write, admin (default: read-only)
|
|
122
|
+
|
|
123
|
+
# List active database leases
|
|
124
|
+
raqeb db leases
|
|
125
|
+
|
|
126
|
+
# Revoke database credentials
|
|
127
|
+
raqeb db revoke <lease-id>
|
|
128
|
+
|
|
129
|
+
# List available databases
|
|
130
|
+
raqeb db list
|
|
51
131
|
```
|
|
52
132
|
|
|
53
133
|
### Secrets Management
|
|
@@ -55,80 +135,254 @@ raqeb login --api-key <key>
|
|
|
55
135
|
```bash
|
|
56
136
|
# Get secret value
|
|
57
137
|
raqeb secrets get <secret-id>
|
|
58
|
-
```
|
|
59
138
|
|
|
60
|
-
|
|
139
|
+
# List all secrets (names only, not values)
|
|
140
|
+
raqeb secrets list
|
|
61
141
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
raqeb db connect <database-id> [options]
|
|
65
|
-
--ttl <hours> Time to live (default: 4)
|
|
66
|
-
--access-level <level> read-only, read-write, or admin (default: read-only)
|
|
142
|
+
# Create new secret
|
|
143
|
+
raqeb secrets create <name> <value> [--description <desc>]
|
|
67
144
|
|
|
68
|
-
#
|
|
69
|
-
raqeb
|
|
145
|
+
# Update secret
|
|
146
|
+
raqeb secrets update <secret-id> <new-value>
|
|
147
|
+
|
|
148
|
+
# Delete secret
|
|
149
|
+
raqeb secrets delete <secret-id>
|
|
70
150
|
```
|
|
71
151
|
|
|
72
|
-
### API Keys
|
|
152
|
+
### API Keys (Personal)
|
|
73
153
|
|
|
74
154
|
```bash
|
|
75
|
-
# List API keys
|
|
155
|
+
# List your API keys
|
|
76
156
|
raqeb keys list
|
|
77
157
|
|
|
78
158
|
# Create new API key
|
|
79
|
-
raqeb keys create <name> [
|
|
159
|
+
raqeb keys create <name> [options]
|
|
160
|
+
--scopes <scopes> Comma-separated scopes (default: secrets:read)
|
|
161
|
+
--expires-in <days> Expiration in days (default: 90, max: 365)
|
|
80
162
|
|
|
81
163
|
# Delete API key
|
|
82
164
|
raqeb keys delete <key-id>
|
|
165
|
+
|
|
166
|
+
# Rotate API key
|
|
167
|
+
raqeb keys rotate <key-id>
|
|
83
168
|
```
|
|
84
169
|
|
|
85
|
-
|
|
170
|
+
### Service Accounts (Admin Only)
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# List service accounts
|
|
174
|
+
raqeb sa list
|
|
175
|
+
|
|
176
|
+
# Create service account
|
|
177
|
+
raqeb sa create <name> [options]
|
|
178
|
+
--scopes <scopes> Comma-separated scopes
|
|
179
|
+
--allowed-databases <ids> Comma-separated database IDs
|
|
180
|
+
--allowed-secrets <ids> Comma-separated secret IDs
|
|
181
|
+
|
|
182
|
+
# Delete service account
|
|
183
|
+
raqeb sa delete <account-id>
|
|
184
|
+
|
|
185
|
+
# View service account details
|
|
186
|
+
raqeb sa get <account-id>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Audit Logs
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# View audit logs
|
|
193
|
+
raqeb audit list [options]
|
|
194
|
+
--limit <n> Number of records (default: 50)
|
|
195
|
+
--user <user-id> Filter by user
|
|
196
|
+
--action <action> Filter by action type
|
|
197
|
+
--from <date> Start date (YYYY-MM-DD)
|
|
198
|
+
--to <date> End date (YYYY-MM-DD)
|
|
199
|
+
|
|
200
|
+
# Export audit logs
|
|
201
|
+
raqeb audit export --format csv --output audit.csv
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## āļø Configuration
|
|
86
205
|
|
|
87
206
|
Configuration is stored in `~/.raqeb/config.json`
|
|
88
207
|
|
|
89
208
|
```json
|
|
90
209
|
{
|
|
91
210
|
"base_url": "https://app.raqeb.cloud/api/v1",
|
|
92
|
-
"api_key": "
|
|
211
|
+
"api_key": "sa_your_service_account_key",
|
|
212
|
+
"tenant": "your-tenant-subdomain"
|
|
93
213
|
}
|
|
94
214
|
```
|
|
95
215
|
|
|
96
|
-
|
|
216
|
+
### Environment Variables
|
|
97
217
|
|
|
98
|
-
|
|
218
|
+
You can also use environment variables:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
export RAQEB_API_KEY="sa_your_api_key"
|
|
222
|
+
export RAQEB_BASE_URL="https://app.raqeb.cloud/api/v1"
|
|
223
|
+
export RAQEB_TENANT="your-tenant"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Multi-Tenant Support
|
|
227
|
+
|
|
228
|
+
If you work with multiple Raqeb tenants:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Switch tenant
|
|
232
|
+
raqeb config set-tenant production-tenant
|
|
233
|
+
|
|
234
|
+
# Or specify tenant per command
|
|
235
|
+
raqeb --tenant staging-tenant db connect staging-db
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## š” Use Cases & Examples
|
|
239
|
+
|
|
240
|
+
### 1. CI/CD Pipeline Integration
|
|
99
241
|
|
|
100
242
|
```bash
|
|
101
243
|
#!/bin/bash
|
|
102
|
-
#
|
|
244
|
+
# GitHub Actions / Jenkins / GitLab CI
|
|
245
|
+
|
|
246
|
+
# Login with service account (stored in CI secrets)
|
|
247
|
+
raqeb login --api-key $RAQEB_SERVICE_ACCOUNT_KEY
|
|
103
248
|
|
|
104
|
-
#
|
|
249
|
+
# Get temporary database credentials for migration
|
|
250
|
+
CREDS=$(raqeb db connect prod-mysql --ttl 1 --access-level read-write)
|
|
251
|
+
DB_USER=$(echo "$CREDS" | grep "Username:" | awk '{print $2}')
|
|
252
|
+
DB_PASS=$(echo "$CREDS" | grep "Password:" | awk '{print $2}')
|
|
253
|
+
|
|
254
|
+
# Run database migration
|
|
255
|
+
mysql -h prod-mysql.example.com -u $DB_USER -p$DB_PASS < migration.sql
|
|
256
|
+
|
|
257
|
+
# Credentials automatically expire after 1 hour
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 2. Application Secrets Retrieval
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
#!/bin/bash
|
|
264
|
+
# Retrieve secrets for application startup
|
|
265
|
+
|
|
266
|
+
# Login
|
|
105
267
|
raqeb login --api-key $RAQEB_API_KEY
|
|
106
268
|
|
|
107
|
-
# Get
|
|
108
|
-
raqeb
|
|
269
|
+
# Get all required secrets
|
|
270
|
+
export STRIPE_API_KEY=$(raqeb secrets get stripe-api-key)
|
|
271
|
+
export AWS_ACCESS_KEY=$(raqeb secrets get aws-access-key)
|
|
272
|
+
export DATABASE_URL=$(raqeb secrets get database-url)
|
|
273
|
+
|
|
274
|
+
# Start application
|
|
275
|
+
npm start
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 3. Developer Local Access
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# Developer needs temporary access to staging database
|
|
282
|
+
|
|
283
|
+
# Login with personal API key
|
|
284
|
+
raqeb login --api-key ak_developer_key
|
|
285
|
+
|
|
286
|
+
# Get 4-hour read-only access
|
|
287
|
+
raqeb db connect staging-postgres --ttl 4 --access-level read-only
|
|
288
|
+
|
|
289
|
+
# Connect with provided credentials
|
|
290
|
+
psql -h staging-db.example.com -U temp_user_xyz789 -d staging
|
|
109
291
|
|
|
110
|
-
#
|
|
292
|
+
# Credentials auto-expire after 4 hours
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 4. Automated Testing
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
#!/bin/bash
|
|
299
|
+
# Integration tests with temporary database
|
|
300
|
+
|
|
301
|
+
# Login
|
|
302
|
+
raqeb login --api-key $RAQEB_TEST_KEY
|
|
303
|
+
|
|
304
|
+
# Get test database credentials
|
|
305
|
+
raqeb db connect test-mysql --ttl 1 --access-level read-write
|
|
111
306
|
|
|
112
|
-
#
|
|
307
|
+
# Run tests
|
|
308
|
+
npm test
|
|
309
|
+
|
|
310
|
+
# Cleanup (optional - auto-expires anyway)
|
|
113
311
|
raqeb db revoke $LEASE_ID
|
|
114
312
|
```
|
|
115
313
|
|
|
116
|
-
###
|
|
314
|
+
### 5. Service Account Management (Admin)
|
|
117
315
|
|
|
118
316
|
```bash
|
|
119
|
-
#
|
|
120
|
-
|
|
317
|
+
# Admin creates service account for production app
|
|
318
|
+
|
|
319
|
+
raqeb sa create "Production Backend" \
|
|
320
|
+
--scopes "databases:read,databases:write,secrets:read" \
|
|
321
|
+
--allowed-databases "prod-mysql,prod-postgres" \
|
|
322
|
+
--allowed-secrets "stripe-key,aws-key"
|
|
121
323
|
|
|
122
|
-
#
|
|
123
|
-
|
|
324
|
+
# Returns service account API key (sa_xxx)
|
|
325
|
+
# Store in production environment variables
|
|
124
326
|
```
|
|
125
327
|
|
|
126
|
-
##
|
|
328
|
+
## š Security Best Practices
|
|
329
|
+
|
|
330
|
+
### API Key Management
|
|
331
|
+
|
|
332
|
+
- **Never commit API keys** to version control
|
|
333
|
+
- **Use environment variables** in CI/CD pipelines
|
|
334
|
+
- **Rotate keys regularly** (every 90 days recommended)
|
|
335
|
+
- **Use service accounts** for applications, personal keys for development
|
|
336
|
+
- **Limit scopes** to minimum required permissions
|
|
337
|
+
|
|
338
|
+
### Database Access
|
|
339
|
+
|
|
340
|
+
- **Request minimum TTL** needed for your task
|
|
341
|
+
- **Use read-only access** when possible
|
|
342
|
+
- **Revoke credentials** when done (optional - auto-expires)
|
|
343
|
+
- **Monitor audit logs** for suspicious activity
|
|
344
|
+
|
|
345
|
+
### Secrets Management
|
|
346
|
+
|
|
347
|
+
- **Encrypt secrets** at rest (handled by Raqeb)
|
|
348
|
+
- **Limit secret access** to specific service accounts
|
|
349
|
+
- **Rotate secrets** regularly
|
|
350
|
+
- **Use different secrets** for different environments
|
|
351
|
+
|
|
352
|
+
## š Documentation
|
|
353
|
+
|
|
354
|
+
- **Full Documentation**: https://docs.raqeb.cloud
|
|
355
|
+
- **API Reference**: https://docs.raqeb.cloud/api
|
|
356
|
+
- **Service Accounts Guide**: https://docs.raqeb.cloud/service-accounts
|
|
357
|
+
- **Database PAM Guide**: https://docs.raqeb.cloud/database-pam
|
|
358
|
+
- **Secrets Management**: https://docs.raqeb.cloud/secrets
|
|
359
|
+
|
|
360
|
+
## š Support
|
|
361
|
+
|
|
362
|
+
- **Email**: support@tzamun.sa
|
|
363
|
+
- **Website**: https://raqeb.cloud
|
|
364
|
+
- **GitHub Issues**: https://github.com/Tzamun-Arabia-IT-Co/raqeb-cli/issues
|
|
365
|
+
- **Documentation**: https://docs.raqeb.cloud
|
|
366
|
+
|
|
367
|
+
## š License
|
|
368
|
+
|
|
369
|
+
MIT License - Copyright (c) 2026 Tzamun Arabia IT Co
|
|
370
|
+
|
|
371
|
+
## š Features Roadmap
|
|
372
|
+
|
|
373
|
+
- [ ] Multi-factor authentication support
|
|
374
|
+
- [ ] SSH key management
|
|
375
|
+
- [ ] Certificate management
|
|
376
|
+
- [ ] Kubernetes secrets integration
|
|
377
|
+
- [ ] Terraform provider
|
|
378
|
+
- [ ] Ansible integration
|
|
379
|
+
|
|
380
|
+
## š¤ Contributing
|
|
381
|
+
|
|
382
|
+
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
|
|
127
383
|
|
|
128
|
-
|
|
384
|
+
## š Version
|
|
129
385
|
|
|
130
|
-
|
|
386
|
+
Current version: 1.0.0
|
|
131
387
|
|
|
132
|
-
|
|
133
|
-
- Email: support@raqeb.cloud
|
|
134
|
-
- GitHub: https://github.com/raqeb/cli
|
|
388
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Generate shell completion scripts
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const shell = process.argv[2] || 'bash';
|
|
9
|
+
|
|
10
|
+
const completionsDir = path.join(__dirname, '..', 'completions');
|
|
11
|
+
|
|
12
|
+
switch (shell) {
|
|
13
|
+
case 'bash':
|
|
14
|
+
const bashScript = fs.readFileSync(path.join(completionsDir, 'bash', 'raqeb-completion.sh'), 'utf8');
|
|
15
|
+
console.log(bashScript);
|
|
16
|
+
break;
|
|
17
|
+
|
|
18
|
+
case 'zsh':
|
|
19
|
+
const zshScript = fs.readFileSync(path.join(completionsDir, 'zsh', '_raqeb'), 'utf8');
|
|
20
|
+
console.log(zshScript);
|
|
21
|
+
break;
|
|
22
|
+
|
|
23
|
+
case 'fish':
|
|
24
|
+
console.log('# Fish completion not yet implemented');
|
|
25
|
+
console.log('# Use: raqeb completion bash > ~/.config/fish/completions/raqeb.fish');
|
|
26
|
+
break;
|
|
27
|
+
|
|
28
|
+
default:
|
|
29
|
+
console.error(`Unknown shell: ${shell}`);
|
|
30
|
+
console.error('Supported shells: bash, zsh, fish');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
package/bin/raqeb.js
CHANGED
|
@@ -6,6 +6,8 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const chalk = require('chalk');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const Table = require('cli-table3');
|
|
9
11
|
|
|
10
12
|
const CONFIG_DIR = path.join(os.homedir(), '.raqeb');
|
|
11
13
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -45,6 +47,15 @@ function getClient() {
|
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// Format output (table or JSON)
|
|
51
|
+
function formatOutput(data, options = {}) {
|
|
52
|
+
if (options.json) {
|
|
53
|
+
console.log(JSON.stringify(data, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
// Login command
|
|
49
60
|
program
|
|
50
61
|
.command('login')
|
|
@@ -84,11 +95,97 @@ secrets
|
|
|
84
95
|
const db = program.command('db').description('Manage database access');
|
|
85
96
|
|
|
86
97
|
db
|
|
87
|
-
.command('
|
|
98
|
+
.command('list')
|
|
99
|
+
.description('List all available databases')
|
|
100
|
+
.option('--json', 'Output in JSON format')
|
|
101
|
+
.addHelpText('after', `
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
$ raqeb db list
|
|
105
|
+
$ raqeb db list --json
|
|
106
|
+
`)
|
|
107
|
+
.action(async (options) => {
|
|
108
|
+
try {
|
|
109
|
+
const client = getClient();
|
|
110
|
+
const response = await client.get('/databases');
|
|
111
|
+
const databases = response.data;
|
|
112
|
+
|
|
113
|
+
if (options.json) {
|
|
114
|
+
console.log(JSON.stringify(databases, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (databases.length === 0) {
|
|
119
|
+
console.log(chalk.yellow('No databases available'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const table = new Table({
|
|
124
|
+
head: ['ID', 'Name', 'Type', 'Host', 'Status'],
|
|
125
|
+
style: { head: ['cyan'] }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
databases.forEach(db => {
|
|
129
|
+
table.push([
|
|
130
|
+
db.id,
|
|
131
|
+
db.name,
|
|
132
|
+
db.type,
|
|
133
|
+
db.host || 'N/A',
|
|
134
|
+
db.is_active ? chalk.green('Active') : chalk.red('Inactive')
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log('\n' + table.toString());
|
|
139
|
+
console.log(chalk.gray(`\nTotal: ${databases.length} database(s)`));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(chalk.red(`ā Error: ${error.response?.data?.detail || error.message}`));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
db
|
|
147
|
+
.command('connect [database-id]')
|
|
88
148
|
.description('Get temporary database credentials')
|
|
89
149
|
.option('--ttl <hours>', 'Time to live in hours', '4')
|
|
90
150
|
.option('--access-level <level>', 'Access level (read-only, read-write, admin)', 'read-only')
|
|
151
|
+
.option('--reason <text>', 'Reason for access (for audit trail)')
|
|
152
|
+
.option('--json', 'Output in JSON format')
|
|
153
|
+
.addHelpText('after', `
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
$ raqeb db connect prod-mysql --ttl 2
|
|
157
|
+
$ raqeb db connect staging-postgres --access-level read-write --reason "Migration"
|
|
158
|
+
$ raqeb db connect dev-mongodb --json
|
|
159
|
+
$ raqeb db connect # Interactive mode - prompts for database
|
|
160
|
+
`)
|
|
91
161
|
.action(async (databaseId, options) => {
|
|
162
|
+
// Interactive prompt if database-id not provided
|
|
163
|
+
if (!databaseId) {
|
|
164
|
+
try {
|
|
165
|
+
const client = getClient();
|
|
166
|
+
const dbResponse = await client.get('/databases');
|
|
167
|
+
const databases = dbResponse.data;
|
|
168
|
+
|
|
169
|
+
if (databases.length === 0) {
|
|
170
|
+
console.log(chalk.yellow('No databases available'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const answer = await inquirer.prompt([{
|
|
175
|
+
type: 'list',
|
|
176
|
+
name: 'database',
|
|
177
|
+
message: 'Which database?',
|
|
178
|
+
choices: databases.map(db => ({
|
|
179
|
+
name: `${db.name} (${db.type})`,
|
|
180
|
+
value: db.id
|
|
181
|
+
}))
|
|
182
|
+
}]);
|
|
183
|
+
databaseId = answer.database;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(chalk.red(`ā Error fetching databases: ${error.message}`));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
92
189
|
try {
|
|
93
190
|
const client = getClient();
|
|
94
191
|
const response = await client.post('/service-accounts/databases/dynamic-credentials', {
|
|
@@ -98,6 +195,11 @@ db
|
|
|
98
195
|
});
|
|
99
196
|
const data = response.data;
|
|
100
197
|
|
|
198
|
+
if (options.json) {
|
|
199
|
+
console.log(JSON.stringify(data, null, 2));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
101
203
|
console.log(chalk.cyan('\nš Temporary Database Credentials'));
|
|
102
204
|
console.log(chalk.gray('ā'.repeat(40)));
|
|
103
205
|
console.log(chalk.white(`Username: ${data.username}`));
|
|
@@ -204,6 +306,57 @@ keys
|
|
|
204
306
|
}
|
|
205
307
|
});
|
|
206
308
|
|
|
309
|
+
// Interactive command
|
|
310
|
+
program
|
|
311
|
+
.command('interactive')
|
|
312
|
+
.alias('i')
|
|
313
|
+
.alias('shell')
|
|
314
|
+
.description('Launch interactive mode')
|
|
315
|
+
.addHelpText('after', `
|
|
316
|
+
|
|
317
|
+
Interactive mode provides a guided experience with:
|
|
318
|
+
- Command discovery with /
|
|
319
|
+
- Tab completion
|
|
320
|
+
- Command history
|
|
321
|
+
- Interactive prompts
|
|
322
|
+
|
|
323
|
+
Examples:
|
|
324
|
+
$ raqeb interactive
|
|
325
|
+
$ raqeb -i
|
|
326
|
+
$ raqeb shell
|
|
327
|
+
`)
|
|
328
|
+
.action(() => {
|
|
329
|
+
const InteractiveShell = require('../lib/interactive');
|
|
330
|
+
const client = getClient();
|
|
331
|
+
const shell = new InteractiveShell(client);
|
|
332
|
+
shell.start();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Completion command
|
|
336
|
+
program
|
|
337
|
+
.command('completion <shell>')
|
|
338
|
+
.description('Generate shell completion script')
|
|
339
|
+
.addHelpText('after', `
|
|
340
|
+
|
|
341
|
+
Supported shells: bash, zsh, fish
|
|
342
|
+
|
|
343
|
+
Installation:
|
|
344
|
+
Bash:
|
|
345
|
+
$ raqeb completion bash > /etc/bash_completion.d/raqeb
|
|
346
|
+
$ source /etc/bash_completion.d/raqeb
|
|
347
|
+
|
|
348
|
+
Zsh:
|
|
349
|
+
$ raqeb completion zsh > ~/.zsh/completion/_raqeb
|
|
350
|
+
$ fpath=(~/.zsh/completion $fpath)
|
|
351
|
+
|
|
352
|
+
Fish:
|
|
353
|
+
$ raqeb completion fish > ~/.config/fish/completions/raqeb.fish
|
|
354
|
+
`)
|
|
355
|
+
.action((shell) => {
|
|
356
|
+
const completionScript = require('./raqeb-completion.js');
|
|
357
|
+
// Script will output to stdout
|
|
358
|
+
});
|
|
359
|
+
|
|
207
360
|
program
|
|
208
361
|
.name('raqeb')
|
|
209
362
|
.description('Raqeb CLI - Database PAM and Secrets Management')
|
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const Table = require('cli-table3');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class InteractiveShell {
|
|
10
|
+
constructor(apiClient) {
|
|
11
|
+
this.client = apiClient;
|
|
12
|
+
this.history = [];
|
|
13
|
+
this.rl = null;
|
|
14
|
+
this.config = this.loadConfig();
|
|
15
|
+
this.userInfo = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadConfig() {
|
|
19
|
+
try {
|
|
20
|
+
const configPath = path.join(os.homedir(), '.raqeb', 'config.json');
|
|
21
|
+
if (fs.existsSync(configPath)) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// Config not found or invalid
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async fetchUserInfo() {
|
|
31
|
+
try {
|
|
32
|
+
const response = await this.client.get('/auth/me');
|
|
33
|
+
this.userInfo = response.data;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
this.userInfo = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async start() {
|
|
40
|
+
await this.fetchUserInfo();
|
|
41
|
+
this.showWelcome();
|
|
42
|
+
this.setupReadline();
|
|
43
|
+
this.rl.prompt();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setupReadline() {
|
|
47
|
+
this.rl = readline.createInterface({
|
|
48
|
+
input: process.stdin,
|
|
49
|
+
output: process.stdout,
|
|
50
|
+
prompt: chalk.cyan('raqeb> '),
|
|
51
|
+
completer: this.completer.bind(this)
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.rl.on('line', async (line) => {
|
|
55
|
+
const input = line.trim();
|
|
56
|
+
|
|
57
|
+
if (input) {
|
|
58
|
+
this.history.push(input);
|
|
59
|
+
await this.handleCommand(input);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.rl.prompt();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.rl.on('close', () => {
|
|
66
|
+
console.log(chalk.gray('\nGoodbye! š'));
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
completer(line) {
|
|
72
|
+
const commands = [
|
|
73
|
+
'/', '/help', '/exit', '/clear',
|
|
74
|
+
'/db', '/db list', '/db connect', '/db sessions', '/db revoke',
|
|
75
|
+
'/secrets', '/secrets list', '/secrets get', '/secrets set', '/secrets delete',
|
|
76
|
+
'/keys', '/keys list', '/keys create', '/keys delete',
|
|
77
|
+
'/audit', '/audit logs'
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const hits = commands.filter((c) => c.startsWith(line));
|
|
81
|
+
return [hits.length ? hits : commands, line];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
showWelcome() {
|
|
85
|
+
console.clear();
|
|
86
|
+
|
|
87
|
+
// ASCII Logo
|
|
88
|
+
console.log(chalk.cyan.bold(`
|
|
89
|
+
āāāāāāā āāāāāāā āāāāāāā āāāāāāāāāāāāāāā
|
|
90
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
91
|
+
āāāāāāāāāāāāāāāāāāā āāāāāāāāā āāāāāāāā
|
|
92
|
+
āāāāāāāāāāāāāāāāāāāāā āāāāāāāāā āāāāāāāā
|
|
93
|
+
āāā āāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
94
|
+
āāā āāāāāā āāā āāāāāāā āāāāāāāāāāāāāāā
|
|
95
|
+
`));
|
|
96
|
+
|
|
97
|
+
console.log(chalk.cyan('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
98
|
+
console.log(chalk.cyan('ā Raqeb CLI - Interactive Mode v1.3.0 ā'));
|
|
99
|
+
console.log(chalk.cyan('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
100
|
+
|
|
101
|
+
// Show user info if available
|
|
102
|
+
if (this.userInfo) {
|
|
103
|
+
console.log(chalk.gray('\nš Session Information:'));
|
|
104
|
+
console.log(chalk.white(` User: ${chalk.green(this.userInfo.email || 'Unknown')}`));
|
|
105
|
+
console.log(chalk.white(` Tenant: ${chalk.green(this.userInfo.tenant_name || this.userInfo.tenant_id || 'Unknown')}`));
|
|
106
|
+
console.log(chalk.white(` Role: ${chalk.yellow(this.userInfo.role || 'user')}`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(chalk.cyan('\nš” Type / to see all commands, or /help for help\n'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async handleCommand(input) {
|
|
113
|
+
try {
|
|
114
|
+
if (input === '/') {
|
|
115
|
+
this.showAllCommands();
|
|
116
|
+
} else if (input === '/help') {
|
|
117
|
+
this.showHelp();
|
|
118
|
+
} else if (input === '/exit' || input === '/quit') {
|
|
119
|
+
this.rl.close();
|
|
120
|
+
} else if (input === '/clear') {
|
|
121
|
+
console.clear();
|
|
122
|
+
this.showWelcome();
|
|
123
|
+
} else if (input.startsWith('/db')) {
|
|
124
|
+
await this.handleDatabase(input);
|
|
125
|
+
} else if (input.startsWith('/secrets')) {
|
|
126
|
+
await this.handleSecrets(input);
|
|
127
|
+
} else if (input.startsWith('/keys')) {
|
|
128
|
+
await this.handleKeys(input);
|
|
129
|
+
} else if (input.startsWith('/audit')) {
|
|
130
|
+
await this.handleAudit(input);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(chalk.red(`Unknown command: ${input}`));
|
|
133
|
+
console.log(chalk.gray('Type / to see all commands'));
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(chalk.red(`ā Error: ${error.message}`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
showAllCommands() {
|
|
141
|
+
console.log(chalk.bold('\nš Available Commands:\n'));
|
|
142
|
+
console.log(chalk.cyan(' /db') + ' Database operations');
|
|
143
|
+
console.log(chalk.cyan(' /secrets') + ' Secrets management');
|
|
144
|
+
console.log(chalk.cyan(' /keys') + ' API keys management');
|
|
145
|
+
console.log(chalk.cyan(' /audit') + ' Audit logs');
|
|
146
|
+
console.log(chalk.cyan(' /help') + ' Show this help');
|
|
147
|
+
console.log(chalk.cyan(' /clear') + ' Clear screen');
|
|
148
|
+
console.log(chalk.cyan(' /exit') + ' Exit interactive mode');
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
showHelp() {
|
|
153
|
+
console.log(chalk.bold('\nš Interactive Mode Help\n'));
|
|
154
|
+
console.log(chalk.white('Commands:'));
|
|
155
|
+
console.log(' / Show all available commands');
|
|
156
|
+
console.log(' /db Database operations');
|
|
157
|
+
console.log(' /db list List all databases');
|
|
158
|
+
console.log(' /db connect Connect to a database');
|
|
159
|
+
console.log(' /secrets Secrets management');
|
|
160
|
+
console.log(' /secrets list List all secrets');
|
|
161
|
+
console.log(' /secrets get Get a secret value');
|
|
162
|
+
console.log(' /keys API keys management');
|
|
163
|
+
console.log(' /audit View audit logs');
|
|
164
|
+
console.log(' /help Show this help');
|
|
165
|
+
console.log(' /clear Clear the screen');
|
|
166
|
+
console.log(' /exit Exit interactive mode');
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(chalk.gray('Tips:'));
|
|
169
|
+
console.log(chalk.gray(' - Use TAB for autocomplete'));
|
|
170
|
+
console.log(chalk.gray(' - Use UP/DOWN arrows for command history'));
|
|
171
|
+
console.log(chalk.gray(' - Press Ctrl+C or type /exit to quit'));
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async handleDatabase(input) {
|
|
176
|
+
const parts = input.split(' ');
|
|
177
|
+
const subcommand = parts[1];
|
|
178
|
+
|
|
179
|
+
if (!subcommand || subcommand === 'help') {
|
|
180
|
+
console.log(chalk.bold('\nšļø Database Commands:\n'));
|
|
181
|
+
console.log(' /db list List all databases');
|
|
182
|
+
console.log(' /db connect Connect to a database');
|
|
183
|
+
console.log(' /db sessions View active sessions');
|
|
184
|
+
console.log(' /db revoke <id> Revoke credentials');
|
|
185
|
+
console.log();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
switch (subcommand) {
|
|
190
|
+
case 'list':
|
|
191
|
+
await this.listDatabases();
|
|
192
|
+
break;
|
|
193
|
+
case 'connect':
|
|
194
|
+
await this.connectDatabase(parts[2]);
|
|
195
|
+
break;
|
|
196
|
+
case 'sessions':
|
|
197
|
+
await this.showSessions();
|
|
198
|
+
break;
|
|
199
|
+
case 'revoke':
|
|
200
|
+
await this.revokeCredentials(parts[2]);
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
console.log(chalk.red(`Unknown database command: ${subcommand}`));
|
|
204
|
+
console.log(chalk.gray('Type /db for available commands'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async listDatabases() {
|
|
209
|
+
try {
|
|
210
|
+
const response = await this.client.get('/databases');
|
|
211
|
+
const databases = response.data;
|
|
212
|
+
|
|
213
|
+
if (databases.length === 0) {
|
|
214
|
+
console.log(chalk.yellow('\nNo databases available'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const table = new Table({
|
|
219
|
+
head: ['ID', 'Name', 'Type', 'Host', 'Status'],
|
|
220
|
+
style: { head: ['cyan'] }
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
databases.forEach(db => {
|
|
224
|
+
table.push([
|
|
225
|
+
db.id,
|
|
226
|
+
db.name,
|
|
227
|
+
db.type,
|
|
228
|
+
db.host || 'N/A',
|
|
229
|
+
db.is_active ? chalk.green('ā') + ' Active' : chalk.red('ā') + ' Inactive'
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
console.log('\n' + table.toString());
|
|
234
|
+
console.log(chalk.gray(`\nTotal: ${databases.length} database(s)\n`));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async connectDatabase(databaseId) {
|
|
241
|
+
try {
|
|
242
|
+
// If no database ID provided, prompt for it
|
|
243
|
+
if (!databaseId) {
|
|
244
|
+
const response = await this.client.get('/databases');
|
|
245
|
+
const databases = response.data;
|
|
246
|
+
|
|
247
|
+
if (databases.length === 0) {
|
|
248
|
+
console.log(chalk.yellow('\nNo databases available'));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const answer = await inquirer.prompt([{
|
|
253
|
+
type: 'list',
|
|
254
|
+
name: 'database',
|
|
255
|
+
message: 'Which database?',
|
|
256
|
+
choices: databases.map(db => ({
|
|
257
|
+
name: `${db.name} (${db.type})`,
|
|
258
|
+
value: db.id
|
|
259
|
+
}))
|
|
260
|
+
}]);
|
|
261
|
+
databaseId = answer.database;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Prompt for connection options
|
|
265
|
+
const options = await inquirer.prompt([
|
|
266
|
+
{
|
|
267
|
+
type: 'list',
|
|
268
|
+
name: 'accessLevel',
|
|
269
|
+
message: 'Access level?',
|
|
270
|
+
choices: ['read-only', 'read-write', 'admin'],
|
|
271
|
+
default: 'read-only'
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
type: 'number',
|
|
275
|
+
name: 'ttl',
|
|
276
|
+
message: 'TTL (hours)?',
|
|
277
|
+
default: 2,
|
|
278
|
+
validate: (value) => value > 0 && value <= 24 || 'TTL must be between 1 and 24 hours'
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
type: 'input',
|
|
282
|
+
name: 'reason',
|
|
283
|
+
message: 'Reason for access? (optional)',
|
|
284
|
+
default: ''
|
|
285
|
+
}
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
// Request credentials
|
|
289
|
+
const response = await this.client.post('/service-accounts/databases/dynamic-credentials', {
|
|
290
|
+
database_id: databaseId,
|
|
291
|
+
ttl_hours: options.ttl,
|
|
292
|
+
access_level: options.accessLevel,
|
|
293
|
+
reason: options.reason || undefined
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const data = response.data;
|
|
297
|
+
|
|
298
|
+
console.log(chalk.green('\nā Credentials Generated!\n'));
|
|
299
|
+
const table = new Table({
|
|
300
|
+
style: { head: ['cyan'] }
|
|
301
|
+
});
|
|
302
|
+
table.push(
|
|
303
|
+
['Username', data.username],
|
|
304
|
+
['Password', data.password],
|
|
305
|
+
['Access Level', data.access_level],
|
|
306
|
+
['Expires', data.expires_at],
|
|
307
|
+
['Lease ID', data.lease_id]
|
|
308
|
+
);
|
|
309
|
+
console.log(table.toString());
|
|
310
|
+
console.log(chalk.yellow(`\nā ļø These credentials will expire in ${options.ttl} hours`));
|
|
311
|
+
console.log(chalk.gray(`To revoke early: /db revoke ${data.lease_id}\n`));
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async showSessions() {
|
|
318
|
+
console.log(chalk.yellow('\nā ļø Sessions view not yet implemented\n'));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async revokeCredentials(leaseId) {
|
|
322
|
+
if (!leaseId) {
|
|
323
|
+
console.log(chalk.red('\nā Lease ID required'));
|
|
324
|
+
console.log(chalk.gray('Usage: /db revoke <lease-id>\n'));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await this.client.post(`/service-accounts/leases/${leaseId}/revoke`);
|
|
330
|
+
console.log(chalk.green(`\nā Lease ${leaseId} revoked successfully\n`));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async handleSecrets(input) {
|
|
337
|
+
const parts = input.split(' ');
|
|
338
|
+
const subcommand = parts[1];
|
|
339
|
+
|
|
340
|
+
if (!subcommand || subcommand === 'help') {
|
|
341
|
+
console.log(chalk.bold('\nš Secrets Commands:\n'));
|
|
342
|
+
console.log(' /secrets list List all secrets');
|
|
343
|
+
console.log(' /secrets get <name> Get secret value');
|
|
344
|
+
console.log(' /secrets set Create/update secret');
|
|
345
|
+
console.log(' /secrets delete <name> Delete secret');
|
|
346
|
+
console.log();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
switch (subcommand) {
|
|
351
|
+
case 'list':
|
|
352
|
+
await this.listSecrets();
|
|
353
|
+
break;
|
|
354
|
+
case 'get':
|
|
355
|
+
await this.getSecret(parts[2]);
|
|
356
|
+
break;
|
|
357
|
+
case 'set':
|
|
358
|
+
await this.setSecret();
|
|
359
|
+
break;
|
|
360
|
+
case 'delete':
|
|
361
|
+
await this.deleteSecret(parts[2]);
|
|
362
|
+
break;
|
|
363
|
+
default:
|
|
364
|
+
console.log(chalk.red(`Unknown secrets command: ${subcommand}`));
|
|
365
|
+
console.log(chalk.gray('Type /secrets for available commands'));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async listSecrets() {
|
|
370
|
+
try {
|
|
371
|
+
const response = await this.client.get('/service-accounts/secrets');
|
|
372
|
+
const secrets = response.data;
|
|
373
|
+
|
|
374
|
+
if (!secrets || secrets.length === 0) {
|
|
375
|
+
console.log(chalk.yellow('\nNo secrets found\n'));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log(chalk.bold('\nš Secrets:\n'));
|
|
380
|
+
const table = new Table({
|
|
381
|
+
head: [chalk.cyan('Name'), chalk.cyan('Created'), chalk.cyan('Updated')],
|
|
382
|
+
style: { head: ['cyan'] }
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
secrets.forEach(secret => {
|
|
386
|
+
table.push([
|
|
387
|
+
secret.name,
|
|
388
|
+
new Date(secret.created_at).toLocaleString(),
|
|
389
|
+
secret.updated_at ? new Date(secret.updated_at).toLocaleString() : 'Never'
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
console.log(table.toString());
|
|
394
|
+
console.log(chalk.gray(`\nTotal: ${secrets.length} secret(s)\n`));
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async getSecret(secretId) {
|
|
401
|
+
if (!secretId) {
|
|
402
|
+
console.log(chalk.red('\nā Secret name required'));
|
|
403
|
+
console.log(chalk.gray('Usage: /secrets get <name>\n'));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const response = await this.client.post('/service-accounts/secrets/retrieve', null, {
|
|
409
|
+
params: { secret_id: secretId }
|
|
410
|
+
});
|
|
411
|
+
const data = response.data;
|
|
412
|
+
|
|
413
|
+
console.log(chalk.cyan(`\nš Secret: ${data.name}`));
|
|
414
|
+
console.log(chalk.white(`Value: ${data.value}`));
|
|
415
|
+
console.log(chalk.gray(`Retrieved at: ${data.retrieved_at}\n`));
|
|
416
|
+
} catch (error) {
|
|
417
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async setSecret() {
|
|
422
|
+
try {
|
|
423
|
+
const answers = await inquirer.prompt([
|
|
424
|
+
{
|
|
425
|
+
type: 'input',
|
|
426
|
+
name: 'name',
|
|
427
|
+
message: 'Secret name:',
|
|
428
|
+
validate: (value) => value.trim() !== '' || 'Name is required'
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: 'password',
|
|
432
|
+
name: 'value',
|
|
433
|
+
message: 'Secret value:',
|
|
434
|
+
mask: '*',
|
|
435
|
+
validate: (value) => value.trim() !== '' || 'Value is required'
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
type: 'input',
|
|
439
|
+
name: 'description',
|
|
440
|
+
message: 'Description (optional):',
|
|
441
|
+
default: ''
|
|
442
|
+
}
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
await this.client.post('/service-accounts/secrets', {
|
|
446
|
+
name: answers.name,
|
|
447
|
+
value: answers.value,
|
|
448
|
+
description: answers.description || undefined
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
console.log(chalk.green(`\nā Secret '${answers.name}' created successfully\n`));
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async deleteSecret(secretName) {
|
|
458
|
+
if (!secretName) {
|
|
459
|
+
console.log(chalk.red('\nā Secret name required'));
|
|
460
|
+
console.log(chalk.gray('Usage: /secrets delete <name>\n'));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const confirm = await inquirer.prompt([{
|
|
466
|
+
type: 'confirm',
|
|
467
|
+
name: 'confirmed',
|
|
468
|
+
message: `Delete secret '${secretName}'?`,
|
|
469
|
+
default: false
|
|
470
|
+
}]);
|
|
471
|
+
|
|
472
|
+
if (!confirm.confirmed) {
|
|
473
|
+
console.log(chalk.gray('\nCancelled\n'));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await this.client.delete(`/service-accounts/secrets/${secretName}`);
|
|
478
|
+
console.log(chalk.green(`\nā Secret '${secretName}' deleted successfully\n`));
|
|
479
|
+
} catch (error) {
|
|
480
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async handleKeys(input) {
|
|
485
|
+
const parts = input.split(' ');
|
|
486
|
+
const subcommand = parts[1];
|
|
487
|
+
|
|
488
|
+
if (!subcommand || subcommand === 'help') {
|
|
489
|
+
console.log(chalk.bold('\nš API Keys Commands:\n'));
|
|
490
|
+
console.log(' /keys list List all API keys');
|
|
491
|
+
console.log(' /keys create Create new API key');
|
|
492
|
+
console.log(' /keys delete <id> Delete API key');
|
|
493
|
+
console.log();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
switch (subcommand) {
|
|
498
|
+
case 'list':
|
|
499
|
+
await this.listKeys();
|
|
500
|
+
break;
|
|
501
|
+
case 'create':
|
|
502
|
+
await this.createKey();
|
|
503
|
+
break;
|
|
504
|
+
case 'delete':
|
|
505
|
+
await this.deleteKey(parts[2]);
|
|
506
|
+
break;
|
|
507
|
+
default:
|
|
508
|
+
console.log(chalk.red(`Unknown keys command: ${subcommand}`));
|
|
509
|
+
console.log(chalk.gray('Type /keys for available commands'));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async listKeys() {
|
|
514
|
+
try {
|
|
515
|
+
const response = await this.client.get('/service-accounts/api-keys');
|
|
516
|
+
const keys = response.data;
|
|
517
|
+
|
|
518
|
+
if (!keys || keys.length === 0) {
|
|
519
|
+
console.log(chalk.yellow('\nNo API keys found\n'));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
console.log(chalk.bold('\nš API Keys:\n'));
|
|
524
|
+
const table = new Table({
|
|
525
|
+
head: [chalk.cyan('ID'), chalk.cyan('Name'), chalk.cyan('Prefix'), chalk.cyan('Created'), chalk.cyan('Last Used')],
|
|
526
|
+
style: { head: ['cyan'] }
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
keys.forEach(key => {
|
|
530
|
+
table.push([
|
|
531
|
+
key.id.substring(0, 8) + '...',
|
|
532
|
+
key.name || 'Unnamed',
|
|
533
|
+
key.api_key_prefix || 'N/A',
|
|
534
|
+
new Date(key.created_at).toLocaleDateString(),
|
|
535
|
+
key.last_used_at ? new Date(key.last_used_at).toLocaleDateString() : 'Never'
|
|
536
|
+
]);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
console.log(table.toString());
|
|
540
|
+
console.log(chalk.gray(`\nTotal: ${keys.length} key(s)\n`));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async createKey() {
|
|
547
|
+
try {
|
|
548
|
+
const answers = await inquirer.prompt([
|
|
549
|
+
{
|
|
550
|
+
type: 'input',
|
|
551
|
+
name: 'name',
|
|
552
|
+
message: 'Key name:',
|
|
553
|
+
validate: (value) => value.trim() !== '' || 'Name is required'
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
type: 'checkbox',
|
|
557
|
+
name: 'scopes',
|
|
558
|
+
message: 'Select scopes:',
|
|
559
|
+
choices: [
|
|
560
|
+
{ name: 'Read Secrets', value: 'secrets:read', checked: true },
|
|
561
|
+
{ name: 'Write Secrets', value: 'secrets:write' },
|
|
562
|
+
{ name: 'Read Databases', value: 'databases:read', checked: true },
|
|
563
|
+
{ name: 'Write Databases', value: 'databases:write' },
|
|
564
|
+
{ name: 'Manage API Keys', value: 'api-keys:manage' }
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
const response = await this.client.post('/service-accounts/api-keys', {
|
|
570
|
+
name: answers.name,
|
|
571
|
+
scopes: answers.scopes
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
console.log(chalk.green('\nā API Key Created!\n'));
|
|
575
|
+
console.log(chalk.yellow('ā ļø Save this key now - it won\'t be shown again:\n'));
|
|
576
|
+
console.log(chalk.white.bold(response.data.api_key));
|
|
577
|
+
console.log();
|
|
578
|
+
} catch (error) {
|
|
579
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async deleteKey(keyId) {
|
|
584
|
+
if (!keyId) {
|
|
585
|
+
console.log(chalk.red('\nā Key ID required'));
|
|
586
|
+
console.log(chalk.gray('Usage: /keys delete <id>\n'));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const confirm = await inquirer.prompt([{
|
|
592
|
+
type: 'confirm',
|
|
593
|
+
name: 'confirmed',
|
|
594
|
+
message: `Delete API key '${keyId}'?`,
|
|
595
|
+
default: false
|
|
596
|
+
}]);
|
|
597
|
+
|
|
598
|
+
if (!confirm.confirmed) {
|
|
599
|
+
console.log(chalk.gray('\nCancelled\n'));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
await this.client.delete(`/service-accounts/api-keys/${keyId}`);
|
|
604
|
+
console.log(chalk.green(`\nā API key deleted successfully\n`));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async handleAudit(input) {
|
|
611
|
+
const parts = input.split(' ');
|
|
612
|
+
const subcommand = parts[1];
|
|
613
|
+
|
|
614
|
+
if (!subcommand || subcommand === 'help') {
|
|
615
|
+
console.log(chalk.bold('\nš Audit Commands:\n'));
|
|
616
|
+
console.log(' /audit logs View recent audit logs');
|
|
617
|
+
console.log(' /audit logs <limit> View last N logs');
|
|
618
|
+
console.log();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
switch (subcommand) {
|
|
623
|
+
case 'logs':
|
|
624
|
+
await this.showAuditLogs(parts[2] ? parseInt(parts[2]) : 10);
|
|
625
|
+
break;
|
|
626
|
+
default:
|
|
627
|
+
console.log(chalk.red(`Unknown audit command: ${subcommand}`));
|
|
628
|
+
console.log(chalk.gray('Type /audit for available commands'));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async showAuditLogs(limit = 10) {
|
|
633
|
+
try {
|
|
634
|
+
const response = await this.client.get('/audit/logs', {
|
|
635
|
+
params: { limit }
|
|
636
|
+
});
|
|
637
|
+
const logs = response.data;
|
|
638
|
+
|
|
639
|
+
if (!logs || logs.length === 0) {
|
|
640
|
+
console.log(chalk.yellow('\nNo audit logs found\n'));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log(chalk.bold(`\nš Recent Audit Logs (Last ${limit}):\n`));
|
|
645
|
+
const table = new Table({
|
|
646
|
+
head: [chalk.cyan('Time'), chalk.cyan('User'), chalk.cyan('Action'), chalk.cyan('Resource')],
|
|
647
|
+
style: { head: ['cyan'] },
|
|
648
|
+
colWidths: [20, 25, 20, 30]
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
logs.forEach(log => {
|
|
652
|
+
table.push([
|
|
653
|
+
new Date(log.timestamp).toLocaleString(),
|
|
654
|
+
log.user_email || 'System',
|
|
655
|
+
log.action,
|
|
656
|
+
log.resource_type + '/' + (log.resource_id || 'N/A')
|
|
657
|
+
]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
console.log(table.toString());
|
|
661
|
+
console.log(chalk.gray(`\nShowing ${logs.length} log(s)\n`));
|
|
662
|
+
} catch (error) {
|
|
663
|
+
throw new Error(error.response?.data?.detail || error.message);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
module.exports = InteractiveShell;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raqeb-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Raqeb CLI - Command-line tool for Database PAM and Developer Secrets Management",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"commander": "^11.0.0",
|
|
30
30
|
"chalk": "^4.1.2",
|
|
31
31
|
"ora": "^5.4.1",
|
|
32
|
-
"inquirer": "^8.2.5"
|
|
32
|
+
"inquirer": "^8.2.5",
|
|
33
|
+
"cli-table3": "^0.6.3"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=14.0.0"
|