zen-ai-pentest 2.2.0__py3-none-any.whl → 2.3.0__py3-none-any.whl
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.
- api/auth.py +61 -7
- api/csrf_protection.py +286 -0
- api/main.py +77 -11
- api/rate_limiter.py +317 -0
- api/rate_limiter_v2.py +586 -0
- autonomous/ki_analysis_agent.py +1033 -0
- benchmarks/__init__.py +12 -142
- benchmarks/agent_performance.py +374 -0
- benchmarks/api_performance.py +479 -0
- benchmarks/scan_performance.py +272 -0
- modules/agent_coordinator.py +255 -0
- modules/api_key_manager.py +501 -0
- modules/benchmark.py +706 -0
- modules/cve_updater.py +303 -0
- modules/false_positive_filter.py +149 -0
- modules/output_formats.py +1088 -0
- modules/risk_scoring.py +206 -0
- {zen_ai_pentest-2.2.0.dist-info → zen_ai_pentest-2.3.0.dist-info}/METADATA +134 -289
- {zen_ai_pentest-2.2.0.dist-info → zen_ai_pentest-2.3.0.dist-info}/RECORD +23 -9
- {zen_ai_pentest-2.2.0.dist-info → zen_ai_pentest-2.3.0.dist-info}/WHEEL +0 -0
- {zen_ai_pentest-2.2.0.dist-info → zen_ai_pentest-2.3.0.dist-info}/entry_points.txt +0 -0
- {zen_ai_pentest-2.2.0.dist-info → zen_ai_pentest-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {zen_ai_pentest-2.2.0.dist-info → zen_ai_pentest-2.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""API Key Management System
|
|
2
|
+
|
|
3
|
+
Secure storage and management of API keys with:
|
|
4
|
+
- Encrypted storage using Fernet
|
|
5
|
+
- Role-based permissions
|
|
6
|
+
- Key rotation
|
|
7
|
+
- Audit logging
|
|
8
|
+
- OS keyring integration
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import secrets
|
|
13
|
+
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Dict, List, Optional, Set
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from dataclasses import dataclass, asdict
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from cryptography.fernet import Fernet
|
|
23
|
+
from cryptography.hazmat.primitives import hashes
|
|
24
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
|
|
25
|
+
CRYPTO_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
CRYPTO_AVAILABLE = False
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import keyring
|
|
31
|
+
KEYRING_AVAILABLE = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
KEYRING_AVAILABLE = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Permission(Enum):
|
|
37
|
+
"""API Key permissions"""
|
|
38
|
+
READ = "read"
|
|
39
|
+
WRITE = "write"
|
|
40
|
+
DELETE = "delete"
|
|
41
|
+
ADMIN = "admin"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class KeyStatus(Enum):
|
|
45
|
+
"""Key status"""
|
|
46
|
+
ACTIVE = "active"
|
|
47
|
+
EXPIRED = "expired"
|
|
48
|
+
REVOKED = "revoked"
|
|
49
|
+
ROTATED = "rotated"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class APIKey:
|
|
54
|
+
"""API Key structure"""
|
|
55
|
+
key_id: str
|
|
56
|
+
name: str
|
|
57
|
+
hashed_key: str
|
|
58
|
+
permissions: List[str]
|
|
59
|
+
created_at: str
|
|
60
|
+
expires_at: Optional[str]
|
|
61
|
+
last_used: Optional[str]
|
|
62
|
+
status: str
|
|
63
|
+
created_by: str
|
|
64
|
+
metadata: Dict
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class AuditEntry:
|
|
69
|
+
"""Audit log entry"""
|
|
70
|
+
timestamp: str
|
|
71
|
+
action: str
|
|
72
|
+
key_id: str
|
|
73
|
+
user: str
|
|
74
|
+
ip_address: Optional[str]
|
|
75
|
+
success: bool
|
|
76
|
+
details: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class APIKeyManager:
|
|
80
|
+
"""Secure API Key Management"""
|
|
81
|
+
|
|
82
|
+
name = "api_key_manager"
|
|
83
|
+
version = "1.0.0"
|
|
84
|
+
|
|
85
|
+
# Default key expiration (90 days)
|
|
86
|
+
DEFAULT_EXPIRY_DAYS = 90
|
|
87
|
+
|
|
88
|
+
def __init__(self, storage_path: str = "data/api_keys.json"):
|
|
89
|
+
self.storage_path = Path(storage_path)
|
|
90
|
+
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
self.audit_log_path = Path(storage_path.replace(".json", "_audit.json"))
|
|
92
|
+
self._encryption_key: Optional[bytes] = None
|
|
93
|
+
self._fernet: Optional['Fernet'] = None
|
|
94
|
+
|
|
95
|
+
if CRYPTO_AVAILABLE:
|
|
96
|
+
self._init_encryption()
|
|
97
|
+
|
|
98
|
+
def _init_encryption(self):
|
|
99
|
+
"""Initialize encryption"""
|
|
100
|
+
# Try to get encryption key from keyring
|
|
101
|
+
if KEYRING_AVAILABLE:
|
|
102
|
+
self._encryption_key = keyring.get_password(
|
|
103
|
+
"zen-ai-pentest", "api_key_encryption"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Generate new key if not exists
|
|
107
|
+
if not self._encryption_key:
|
|
108
|
+
self._encryption_key = Fernet.generate_key().decode()
|
|
109
|
+
if KEYRING_AVAILABLE:
|
|
110
|
+
keyring.set_password(
|
|
111
|
+
"zen-ai-pentest",
|
|
112
|
+
"api_key_encryption",
|
|
113
|
+
self._encryption_key
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
# Fallback: store in file (less secure)
|
|
117
|
+
key_file = self.storage_path.parent / ".encryption_key"
|
|
118
|
+
key_file.write_text(self._encryption_key)
|
|
119
|
+
key_file.chmod(0o600) # Owner read/write only
|
|
120
|
+
|
|
121
|
+
self._fernet = Fernet(self._encryption_key.encode())
|
|
122
|
+
|
|
123
|
+
def _encrypt(self, data: str) -> str:
|
|
124
|
+
"""Encrypt string data"""
|
|
125
|
+
if not CRYPTO_AVAILABLE or not self._fernet:
|
|
126
|
+
# Fallback: base64 encode (not secure, but functional)
|
|
127
|
+
import base64
|
|
128
|
+
return base64.b64encode(data.encode()).decode()
|
|
129
|
+
return self._fernet.encrypt(data.encode()).decode()
|
|
130
|
+
|
|
131
|
+
def _decrypt(self, data: str) -> str:
|
|
132
|
+
"""Decrypt string data"""
|
|
133
|
+
if not CRYPTO_AVAILABLE or not self._fernet:
|
|
134
|
+
import base64
|
|
135
|
+
return base64.b64decode(data.encode()).decode()
|
|
136
|
+
return self._fernet.decrypt(data.encode()).decode()
|
|
137
|
+
|
|
138
|
+
def _hash_key(self, key: str) -> str:
|
|
139
|
+
"""Hash API key for storage"""
|
|
140
|
+
return hashlib.sha256(key.encode()).hexdigest()
|
|
141
|
+
|
|
142
|
+
def generate_key(
|
|
143
|
+
self,
|
|
144
|
+
name: str,
|
|
145
|
+
permissions: List[str],
|
|
146
|
+
created_by: str = "system",
|
|
147
|
+
expires_days: Optional[int] = None,
|
|
148
|
+
metadata: Optional[Dict] = None
|
|
149
|
+
) -> tuple[str, str]:
|
|
150
|
+
"""
|
|
151
|
+
Generate new API key
|
|
152
|
+
Returns: (key_id, plain_key)
|
|
153
|
+
"""
|
|
154
|
+
# Generate secure random key
|
|
155
|
+
key_id = f"zen_{secrets.token_urlsafe(16)}"
|
|
156
|
+
plain_key = f"zen_{secrets.token_urlsafe(32)}"
|
|
157
|
+
|
|
158
|
+
# Calculate expiration
|
|
159
|
+
created_at = datetime.utcnow()
|
|
160
|
+
if expires_days is not None:
|
|
161
|
+
expires_at = (created_at + timedelta(days=expires_days)).isoformat()
|
|
162
|
+
else:
|
|
163
|
+
expires_at = (created_at + timedelta(days=self.DEFAULT_EXPIRY_DAYS)).isoformat()
|
|
164
|
+
|
|
165
|
+
# Create key record
|
|
166
|
+
api_key = APIKey(
|
|
167
|
+
key_id=key_id,
|
|
168
|
+
name=name,
|
|
169
|
+
hashed_key=self._hash_key(plain_key),
|
|
170
|
+
permissions=permissions,
|
|
171
|
+
created_at=created_at.isoformat(),
|
|
172
|
+
expires_at=expires_at,
|
|
173
|
+
last_used=None,
|
|
174
|
+
status=KeyStatus.ACTIVE.value,
|
|
175
|
+
created_by=created_by,
|
|
176
|
+
metadata=metadata or {}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Store
|
|
180
|
+
self._save_key(api_key)
|
|
181
|
+
|
|
182
|
+
# Audit log
|
|
183
|
+
self._log_audit(
|
|
184
|
+
action="key_created",
|
|
185
|
+
key_id=key_id,
|
|
186
|
+
user=created_by,
|
|
187
|
+
success=True,
|
|
188
|
+
details=f"Key '{name}' created with permissions: {permissions}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return key_id, plain_key
|
|
192
|
+
|
|
193
|
+
def validate_key(self, plain_key: str, required_permission: Optional[str] = None) -> Optional[APIKey]:
|
|
194
|
+
"""
|
|
195
|
+
Validate API key and check permissions
|
|
196
|
+
"""
|
|
197
|
+
key_hash = self._hash_key(plain_key)
|
|
198
|
+
keys = self._load_keys()
|
|
199
|
+
|
|
200
|
+
for key_data in keys.values():
|
|
201
|
+
if key_data["hashed_key"] == key_hash:
|
|
202
|
+
key = APIKey(**key_data)
|
|
203
|
+
|
|
204
|
+
# Check status
|
|
205
|
+
if key.status != KeyStatus.ACTIVE.value:
|
|
206
|
+
self._log_audit(
|
|
207
|
+
action="key_validation_failed",
|
|
208
|
+
key_id=key.key_id,
|
|
209
|
+
user="system",
|
|
210
|
+
success=False,
|
|
211
|
+
details=f"Key status: {key.status}"
|
|
212
|
+
)
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# Check expiration
|
|
216
|
+
if key.expires_at:
|
|
217
|
+
expires = datetime.fromisoformat(key.expires_at)
|
|
218
|
+
if datetime.utcnow() > expires:
|
|
219
|
+
key.status = KeyStatus.EXPIRED.value
|
|
220
|
+
self._save_key(key)
|
|
221
|
+
self._log_audit(
|
|
222
|
+
action="key_expired",
|
|
223
|
+
key_id=key.key_id,
|
|
224
|
+
user="system",
|
|
225
|
+
success=False,
|
|
226
|
+
details="Key expired"
|
|
227
|
+
)
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# Check permission
|
|
231
|
+
if required_permission and required_permission not in key.permissions:
|
|
232
|
+
self._log_audit(
|
|
233
|
+
action="key_permission_denied",
|
|
234
|
+
key_id=key.key_id,
|
|
235
|
+
user="system",
|
|
236
|
+
success=False,
|
|
237
|
+
details=f"Required: {required_permission}, Has: {key.permissions}"
|
|
238
|
+
)
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
# Update last used
|
|
242
|
+
key.last_used = datetime.utcnow().isoformat()
|
|
243
|
+
self._save_key(key)
|
|
244
|
+
|
|
245
|
+
self._log_audit(
|
|
246
|
+
action="key_validated",
|
|
247
|
+
key_id=key.key_id,
|
|
248
|
+
user="system",
|
|
249
|
+
success=True,
|
|
250
|
+
details=f"Permission checked: {required_permission}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return key
|
|
254
|
+
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def revoke_key(self, key_id: str, revoked_by: str) -> bool:
|
|
258
|
+
"""Revoke API key"""
|
|
259
|
+
key = self._get_key_by_id(key_id)
|
|
260
|
+
if not key:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
key.status = KeyStatus.REVOKED.value
|
|
264
|
+
self._save_key(key)
|
|
265
|
+
|
|
266
|
+
self._log_audit(
|
|
267
|
+
action="key_revoked",
|
|
268
|
+
key_id=key_id,
|
|
269
|
+
user=revoked_by,
|
|
270
|
+
success=True,
|
|
271
|
+
details="Key revoked"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
def rotate_key(self, key_id: str, rotated_by: str) -> Optional[tuple[str, str]]:
|
|
277
|
+
"""
|
|
278
|
+
Rotate API key (create new, invalidate old)
|
|
279
|
+
Returns: (new_key_id, new_plain_key) or None
|
|
280
|
+
"""
|
|
281
|
+
old_key = self._get_key_by_id(key_id)
|
|
282
|
+
if not old_key:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
# Mark old key as rotated
|
|
286
|
+
old_key.status = KeyStatus.ROTATED.value
|
|
287
|
+
old_key.metadata["rotated_to"] = "pending"
|
|
288
|
+
old_key.metadata["rotated_at"] = datetime.utcnow().isoformat()
|
|
289
|
+
self._save_key(old_key)
|
|
290
|
+
|
|
291
|
+
# Generate new key with same permissions
|
|
292
|
+
new_key_id, new_plain_key = self.generate_key(
|
|
293
|
+
name=f"{old_key.name} (rotated)",
|
|
294
|
+
permissions=old_key.permissions,
|
|
295
|
+
created_by=rotated_by,
|
|
296
|
+
expires_days=self.DEFAULT_EXPIRY_DAYS,
|
|
297
|
+
metadata={"rotated_from": old_key.key_id}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Update old key with rotation info
|
|
301
|
+
old_key.metadata["rotated_to"] = new_key_id
|
|
302
|
+
self._save_key(old_key)
|
|
303
|
+
|
|
304
|
+
self._log_audit(
|
|
305
|
+
action="key_rotated",
|
|
306
|
+
key_id=key_id,
|
|
307
|
+
user=rotated_by,
|
|
308
|
+
success=True,
|
|
309
|
+
details=f"Rotated to: {new_key_id}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return new_key_id, new_plain_key
|
|
313
|
+
|
|
314
|
+
def list_keys(self, status: Optional[str] = None) -> List[APIKey]:
|
|
315
|
+
"""List all API keys (without sensitive data)"""
|
|
316
|
+
keys = self._load_keys()
|
|
317
|
+
result = []
|
|
318
|
+
|
|
319
|
+
for key_data in keys.values():
|
|
320
|
+
if status is None or key_data["status"] == status:
|
|
321
|
+
result.append(APIKey(**key_data))
|
|
322
|
+
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
def get_audit_log(self, key_id: Optional[str] = None, limit: int = 100) -> List[AuditEntry]:
|
|
326
|
+
"""Get audit log entries"""
|
|
327
|
+
logs = self._load_audit_log()
|
|
328
|
+
|
|
329
|
+
if key_id:
|
|
330
|
+
logs = [log for log in logs if log["key_id"] == key_id]
|
|
331
|
+
|
|
332
|
+
# Sort by timestamp (newest first) and limit
|
|
333
|
+
logs.sort(key=lambda x: x["timestamp"], reverse=True)
|
|
334
|
+
|
|
335
|
+
return [AuditEntry(**log) for log in logs[:limit]]
|
|
336
|
+
|
|
337
|
+
def cleanup_expired_keys(self) -> int:
|
|
338
|
+
"""Remove expired keys older than 30 days"""
|
|
339
|
+
keys = self._load_keys()
|
|
340
|
+
removed = 0
|
|
341
|
+
cutoff = datetime.utcnow() - timedelta(days=30)
|
|
342
|
+
|
|
343
|
+
for key_id, key_data in list(keys.items()):
|
|
344
|
+
if key_data["status"] == KeyStatus.EXPIRED.value:
|
|
345
|
+
expires_at = datetime.fromisoformat(key_data["expires_at"])
|
|
346
|
+
if expires_at < cutoff:
|
|
347
|
+
del keys[key_id]
|
|
348
|
+
removed += 1
|
|
349
|
+
|
|
350
|
+
self._save_keys(keys)
|
|
351
|
+
return removed
|
|
352
|
+
|
|
353
|
+
def _load_keys(self) -> Dict:
|
|
354
|
+
"""Load keys from storage"""
|
|
355
|
+
if not self.storage_path.exists():
|
|
356
|
+
return {}
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
data = self.storage_path.read_text()
|
|
360
|
+
decrypted = self._decrypt(data)
|
|
361
|
+
return json.loads(decrypted)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logging.error(f"Failed to load keys: {e}")
|
|
364
|
+
return {}
|
|
365
|
+
|
|
366
|
+
def _save_key(self, key: APIKey):
|
|
367
|
+
"""Save single key"""
|
|
368
|
+
keys = self._load_keys()
|
|
369
|
+
keys[key.key_id] = asdict(key)
|
|
370
|
+
self._save_keys(keys)
|
|
371
|
+
|
|
372
|
+
def _save_keys(self, keys: Dict):
|
|
373
|
+
"""Save all keys"""
|
|
374
|
+
data = json.dumps(keys, indent=2)
|
|
375
|
+
encrypted = self._encrypt(data)
|
|
376
|
+
self.storage_path.write_text(encrypted)
|
|
377
|
+
self.storage_path.chmod(0o600) # Owner only
|
|
378
|
+
|
|
379
|
+
def _get_key_by_id(self, key_id: str) -> Optional[APIKey]:
|
|
380
|
+
"""Get key by ID"""
|
|
381
|
+
keys = self._load_keys()
|
|
382
|
+
if key_id in keys:
|
|
383
|
+
return APIKey(**keys[key_id])
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
def _log_audit(self, action: str, key_id: str, user: str, success: bool, details: str):
|
|
387
|
+
"""Add audit log entry"""
|
|
388
|
+
entry = AuditEntry(
|
|
389
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
390
|
+
action=action,
|
|
391
|
+
key_id=key_id,
|
|
392
|
+
user=user,
|
|
393
|
+
ip_address=None,
|
|
394
|
+
success=success,
|
|
395
|
+
details=details
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
logs = self._load_audit_log()
|
|
399
|
+
logs.append(asdict(entry))
|
|
400
|
+
|
|
401
|
+
# Keep only last 10000 entries
|
|
402
|
+
if len(logs) > 10000:
|
|
403
|
+
logs = logs[-10000:]
|
|
404
|
+
|
|
405
|
+
self._save_audit_log(logs)
|
|
406
|
+
|
|
407
|
+
def _load_audit_log(self) -> List[Dict]:
|
|
408
|
+
"""Load audit log"""
|
|
409
|
+
if not self.audit_log_path.exists():
|
|
410
|
+
return []
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
data = self.audit_log_path.read_text()
|
|
414
|
+
return json.loads(data)
|
|
415
|
+
except Exception:
|
|
416
|
+
return []
|
|
417
|
+
|
|
418
|
+
def _save_audit_log(self, logs: List[Dict]):
|
|
419
|
+
"""Save audit log"""
|
|
420
|
+
self.audit_log_path.write_text(json.dumps(logs, indent=2))
|
|
421
|
+
self.audit_log_path.chmod(0o600)
|
|
422
|
+
|
|
423
|
+
def get_info(self) -> Dict:
|
|
424
|
+
"""Get module info"""
|
|
425
|
+
return {
|
|
426
|
+
"name": self.name,
|
|
427
|
+
"version": self.version,
|
|
428
|
+
"encryption": "Fernet (AES-128)" if CRYPTO_AVAILABLE else "Base64 (fallback)",
|
|
429
|
+
"storage": str(self.storage_path),
|
|
430
|
+
"keyring_available": KEYRING_AVAILABLE,
|
|
431
|
+
"crypto_available": CRYPTO_AVAILABLE
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# CLI Interface
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
import argparse
|
|
438
|
+
|
|
439
|
+
parser = argparse.ArgumentParser(description="API Key Management")
|
|
440
|
+
parser.add_argument("action", choices=["create", "list", "revoke", "rotate", "audit", "cleanup"])
|
|
441
|
+
parser.add_argument("--name", help="Key name")
|
|
442
|
+
parser.add_argument("--permissions", nargs="+", default=["read"], help="Permissions")
|
|
443
|
+
parser.add_argument("--key-id", help="Key ID for operations")
|
|
444
|
+
parser.add_argument("--user", default="cli", help="User performing action")
|
|
445
|
+
|
|
446
|
+
args = parser.parse_args()
|
|
447
|
+
|
|
448
|
+
manager = APIKeyManager()
|
|
449
|
+
|
|
450
|
+
if args.action == "create":
|
|
451
|
+
if not args.name:
|
|
452
|
+
print("Error: --name required")
|
|
453
|
+
exit(1)
|
|
454
|
+
key_id, plain_key = manager.generate_key(
|
|
455
|
+
name=args.name,
|
|
456
|
+
permissions=args.permissions,
|
|
457
|
+
created_by=args.user
|
|
458
|
+
)
|
|
459
|
+
print(f"Key ID: {key_id}")
|
|
460
|
+
print(f"API Key: {plain_key}")
|
|
461
|
+
print("WARNING: Save this key now - it won't be shown again!")
|
|
462
|
+
|
|
463
|
+
elif args.action == "list":
|
|
464
|
+
keys = manager.list_keys()
|
|
465
|
+
print(f"{'Key ID':<30} {'Name':<20} {'Status':<10} {'Permissions'}")
|
|
466
|
+
print("-" * 80)
|
|
467
|
+
for key in keys:
|
|
468
|
+
perms = ", ".join(key.permissions)
|
|
469
|
+
print(f"{key.key_id:<30} {key.name:<20} {key.status:<10} {perms}")
|
|
470
|
+
|
|
471
|
+
elif args.action == "revoke":
|
|
472
|
+
if not args.key_id:
|
|
473
|
+
print("Error: --key-id required")
|
|
474
|
+
exit(1)
|
|
475
|
+
if manager.revoke_key(args.key_id, args.user):
|
|
476
|
+
print(f"Key {args.key_id} revoked")
|
|
477
|
+
else:
|
|
478
|
+
print(f"Key {args.key_id} not found")
|
|
479
|
+
|
|
480
|
+
elif args.action == "rotate":
|
|
481
|
+
if not args.key_id:
|
|
482
|
+
print("Error: --key-id required")
|
|
483
|
+
exit(1)
|
|
484
|
+
result = manager.rotate_key(args.key_id, args.user)
|
|
485
|
+
if result:
|
|
486
|
+
print(f"Key rotated. New Key ID: {result[0]}")
|
|
487
|
+
print(f"New API Key: {result[1]}")
|
|
488
|
+
else:
|
|
489
|
+
print(f"Key {args.key_id} not found")
|
|
490
|
+
|
|
491
|
+
elif args.action == "audit":
|
|
492
|
+
logs = manager.get_audit_log(args.key_id)
|
|
493
|
+
print(f"{'Timestamp':<25} {'Action':<20} {'Key ID':<15} {'Success'}")
|
|
494
|
+
print("-" * 70)
|
|
495
|
+
for log in logs[:20]:
|
|
496
|
+
status = "✓" if log.success else "✗"
|
|
497
|
+
print(f"{log.timestamp:<25} {log.action:<20} {log.key_id:<15} {status}")
|
|
498
|
+
|
|
499
|
+
elif args.action == "cleanup":
|
|
500
|
+
removed = manager.cleanup_expired_keys()
|
|
501
|
+
print(f"Removed {removed} expired keys")
|