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.
@@ -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")