elsabro 2.3.0 → 3.7.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 +668 -20
- package/bin/install.js +0 -0
- package/flows/development-flow.json +452 -0
- package/flows/quick-flow.json +118 -0
- package/package.json +3 -2
- package/references/SYSTEM_INDEX.md +379 -5
- package/references/agent-marketplace.md +2274 -0
- package/references/agent-protocol.md +1126 -0
- package/references/ai-code-suggestions.md +2413 -0
- package/references/checkpointing.md +595 -0
- package/references/collaboration-patterns.md +851 -0
- package/references/collaborative-sessions.md +1081 -0
- package/references/configuration-management.md +1810 -0
- package/references/cost-tracking.md +1095 -0
- package/references/enterprise-sso.md +2001 -0
- package/references/error-contracts-v2.md +968 -0
- package/references/event-driven.md +1031 -0
- package/references/flow-orchestration.md +940 -0
- package/references/flow-visualization.md +1557 -0
- package/references/ide-integrations.md +3513 -0
- package/references/interrupt-system.md +681 -0
- package/references/kubernetes-deployment.md +3099 -0
- package/references/memory-system.md +683 -0
- package/references/mobile-companion.md +3236 -0
- package/references/multi-llm-providers.md +2494 -0
- package/references/multi-project-memory.md +1182 -0
- package/references/observability.md +793 -0
- package/references/output-schemas.md +858 -0
- package/references/performance-profiler.md +955 -0
- package/references/plugin-system.md +1526 -0
- package/references/prompt-management.md +292 -0
- package/references/sandbox-execution.md +303 -0
- package/references/security-system.md +1253 -0
- package/references/streaming.md +696 -0
- package/references/testing-framework.md +1151 -0
- package/references/time-travel.md +802 -0
- package/references/tool-registry.md +886 -0
- package/references/voice-commands.md +3296 -0
- package/templates/agent-marketplace-config.json +220 -0
- package/templates/agent-protocol-config.json +136 -0
- package/templates/ai-suggestions-config.json +100 -0
- package/templates/checkpoint-state.json +61 -0
- package/templates/collaboration-config.json +157 -0
- package/templates/collaborative-sessions-config.json +153 -0
- package/templates/configuration-config.json +245 -0
- package/templates/cost-tracking-config.json +148 -0
- package/templates/enterprise-sso-config.json +438 -0
- package/templates/events-config.json +148 -0
- package/templates/flow-visualization-config.json +196 -0
- package/templates/ide-integrations-config.json +442 -0
- package/templates/kubernetes-config.json +764 -0
- package/templates/memory-state.json +84 -0
- package/templates/mobile-companion-config.json +600 -0
- package/templates/multi-llm-config.json +544 -0
- package/templates/multi-project-memory-config.json +145 -0
- package/templates/observability-config.json +109 -0
- package/templates/performance-profiler-config.json +125 -0
- package/templates/plugin-config.json +170 -0
- package/templates/prompt-management-config.json +86 -0
- package/templates/sandbox-config.json +185 -0
- package/templates/schemas-config.json +65 -0
- package/templates/security-config.json +120 -0
- package/templates/streaming-config.json +72 -0
- package/templates/testing-config.json +81 -0
- package/templates/timetravel-config.json +62 -0
- package/templates/tool-registry-config.json +109 -0
- package/templates/voice-commands-config.json +658 -0
|
@@ -0,0 +1,1253 @@
|
|
|
1
|
+
# Security & Access Control (v3.4)
|
|
2
|
+
|
|
3
|
+
Sistema de seguridad con RBAC, secrets management, audit logging y políticas de acceso.
|
|
4
|
+
|
|
5
|
+
## Arquitectura
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ SECURITY SYSTEM │
|
|
10
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ │
|
|
12
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
13
|
+
│ │ RBAC MANAGER │ │
|
|
14
|
+
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
15
|
+
│ │ │ Roles │ │ Permissions │ │ Grants │ │ │
|
|
16
|
+
│ │ │ admin/user │ │ read/write │ │ role→perm │ │ │
|
|
17
|
+
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|
18
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
19
|
+
│ │ │
|
|
20
|
+
│ ▼ │
|
|
21
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
22
|
+
│ │ SECRETS VAULT │ │
|
|
23
|
+
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
24
|
+
│ │ │ Encryption │ │ Storage │ │ Rotation │ │ │
|
|
25
|
+
│ │ │ AES-256 │ │ Secure │ │ Automatic │ │ │
|
|
26
|
+
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|
27
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
28
|
+
│ │ │
|
|
29
|
+
│ ▼ │
|
|
30
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
31
|
+
│ │ AUDIT LOGGER │ │
|
|
32
|
+
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
33
|
+
│ │ │ Events │ │ Tracking │ │ Forensics │ │ │
|
|
34
|
+
│ │ │ Immutable │ │ Real-time │ │ Export │ │ │
|
|
35
|
+
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|
36
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
37
|
+
│ │ │
|
|
38
|
+
│ ▼ │
|
|
39
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
40
|
+
│ │ POLICY ENGINE │ │
|
|
41
|
+
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
42
|
+
│ │ │ Rules │ │ Evaluation │ │ Enforcement │ │ │
|
|
43
|
+
│ │ │ Declarative│ │ Runtime │ │ Strict │ │ │
|
|
44
|
+
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|
45
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
46
|
+
│ │
|
|
47
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## RBACManager
|
|
53
|
+
|
|
54
|
+
### API Principal
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface Role {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
permissions: string[];
|
|
62
|
+
inherits?: string[]; // Inherit from other roles
|
|
63
|
+
constraints?: RoleConstraint[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Permission {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
resource: string;
|
|
70
|
+
actions: string[]; // read, write, execute, delete
|
|
71
|
+
conditions?: PermissionCondition[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface RoleConstraint {
|
|
75
|
+
type: 'time' | 'location' | 'resource_limit' | 'custom';
|
|
76
|
+
value: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface PermissionCondition {
|
|
80
|
+
field: string;
|
|
81
|
+
operator: 'eq' | 'neq' | 'in' | 'not_in' | 'matches';
|
|
82
|
+
value: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface Principal {
|
|
86
|
+
id: string;
|
|
87
|
+
type: 'user' | 'agent' | 'service';
|
|
88
|
+
roles: string[];
|
|
89
|
+
attributes?: Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class RBACManager {
|
|
93
|
+
private roles: Map<string, Role>;
|
|
94
|
+
private permissions: Map<string, Permission>;
|
|
95
|
+
private principals: Map<string, Principal>;
|
|
96
|
+
private grants: Map<string, Set<string>>; // principal -> roles
|
|
97
|
+
|
|
98
|
+
constructor(config: RBACConfig) {
|
|
99
|
+
this.roles = new Map();
|
|
100
|
+
this.permissions = new Map();
|
|
101
|
+
this.principals = new Map();
|
|
102
|
+
this.grants = new Map();
|
|
103
|
+
this.loadBuiltinRoles();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create role
|
|
107
|
+
createRole(role: Omit<Role, 'id'>): Role {
|
|
108
|
+
const newRole: Role = {
|
|
109
|
+
id: this.generateId('role'),
|
|
110
|
+
...role
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.roles.set(newRole.id, newRole);
|
|
114
|
+
|
|
115
|
+
AuditLogger.log({
|
|
116
|
+
action: 'role.created',
|
|
117
|
+
resource: newRole.id,
|
|
118
|
+
details: { name: role.name }
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return newRole;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Create permission
|
|
125
|
+
createPermission(permission: Omit<Permission, 'id'>): Permission {
|
|
126
|
+
const newPerm: Permission = {
|
|
127
|
+
id: this.generateId('perm'),
|
|
128
|
+
...permission
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.permissions.set(newPerm.id, newPerm);
|
|
132
|
+
|
|
133
|
+
return newPerm;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Assign role to principal
|
|
137
|
+
assignRole(principalId: string, roleId: string): void {
|
|
138
|
+
const principal = this.principals.get(principalId);
|
|
139
|
+
if (!principal) throw new Error(`Principal not found: ${principalId}`);
|
|
140
|
+
|
|
141
|
+
const role = this.roles.get(roleId);
|
|
142
|
+
if (!role) throw new Error(`Role not found: ${roleId}`);
|
|
143
|
+
|
|
144
|
+
if (!this.grants.has(principalId)) {
|
|
145
|
+
this.grants.set(principalId, new Set());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.grants.get(principalId)!.add(roleId);
|
|
149
|
+
principal.roles.push(roleId);
|
|
150
|
+
|
|
151
|
+
AuditLogger.log({
|
|
152
|
+
action: 'role.assigned',
|
|
153
|
+
principal: principalId,
|
|
154
|
+
resource: roleId
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Revoke role from principal
|
|
159
|
+
revokeRole(principalId: string, roleId: string): void {
|
|
160
|
+
const grants = this.grants.get(principalId);
|
|
161
|
+
if (grants) {
|
|
162
|
+
grants.delete(roleId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const principal = this.principals.get(principalId);
|
|
166
|
+
if (principal) {
|
|
167
|
+
principal.roles = principal.roles.filter(r => r !== roleId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
AuditLogger.log({
|
|
171
|
+
action: 'role.revoked',
|
|
172
|
+
principal: principalId,
|
|
173
|
+
resource: roleId
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if principal has permission
|
|
178
|
+
hasPermission(
|
|
179
|
+
principalId: string,
|
|
180
|
+
resource: string,
|
|
181
|
+
action: string,
|
|
182
|
+
context?: PermissionContext
|
|
183
|
+
): boolean {
|
|
184
|
+
const principal = this.principals.get(principalId);
|
|
185
|
+
if (!principal) return false;
|
|
186
|
+
|
|
187
|
+
// Get all effective permissions
|
|
188
|
+
const effectivePermissions = this.getEffectivePermissions(principal);
|
|
189
|
+
|
|
190
|
+
// Check each permission
|
|
191
|
+
for (const permId of effectivePermissions) {
|
|
192
|
+
const perm = this.permissions.get(permId);
|
|
193
|
+
if (!perm) continue;
|
|
194
|
+
|
|
195
|
+
// Check resource match
|
|
196
|
+
if (!this.matchesResource(perm.resource, resource)) continue;
|
|
197
|
+
|
|
198
|
+
// Check action
|
|
199
|
+
if (!perm.actions.includes(action) && !perm.actions.includes('*')) continue;
|
|
200
|
+
|
|
201
|
+
// Check conditions
|
|
202
|
+
if (perm.conditions && !this.evaluateConditions(perm.conditions, context)) continue;
|
|
203
|
+
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Enforce permission (throws if denied)
|
|
211
|
+
enforce(
|
|
212
|
+
principalId: string,
|
|
213
|
+
resource: string,
|
|
214
|
+
action: string,
|
|
215
|
+
context?: PermissionContext
|
|
216
|
+
): void {
|
|
217
|
+
if (!this.hasPermission(principalId, resource, action, context)) {
|
|
218
|
+
AuditLogger.log({
|
|
219
|
+
action: 'permission.denied',
|
|
220
|
+
principal: principalId,
|
|
221
|
+
resource,
|
|
222
|
+
details: { action }
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
throw new PermissionDeniedError(
|
|
226
|
+
`Permission denied: ${principalId} cannot ${action} on ${resource}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
AuditLogger.log({
|
|
231
|
+
action: 'permission.granted',
|
|
232
|
+
principal: principalId,
|
|
233
|
+
resource,
|
|
234
|
+
details: { action }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Get principal's effective permissions
|
|
239
|
+
getEffectivePermissions(principal: Principal): Set<string> {
|
|
240
|
+
const permissions = new Set<string>();
|
|
241
|
+
|
|
242
|
+
for (const roleId of principal.roles) {
|
|
243
|
+
const rolePerms = this.getRolePermissions(roleId);
|
|
244
|
+
rolePerms.forEach(p => permissions.add(p));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return permissions;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Get role with inheritance
|
|
251
|
+
private getRolePermissions(roleId: string): string[] {
|
|
252
|
+
const role = this.roles.get(roleId);
|
|
253
|
+
if (!role) return [];
|
|
254
|
+
|
|
255
|
+
const permissions = [...role.permissions];
|
|
256
|
+
|
|
257
|
+
// Add inherited permissions
|
|
258
|
+
if (role.inherits) {
|
|
259
|
+
for (const inheritedRole of role.inherits) {
|
|
260
|
+
permissions.push(...this.getRolePermissions(inheritedRole));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return permissions;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private matchesResource(pattern: string, resource: string): boolean {
|
|
268
|
+
if (pattern === '*') return true;
|
|
269
|
+
if (pattern === resource) return true;
|
|
270
|
+
|
|
271
|
+
// Wildcard matching
|
|
272
|
+
const regex = new RegExp(
|
|
273
|
+
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
|
274
|
+
);
|
|
275
|
+
return regex.test(resource);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private evaluateConditions(
|
|
279
|
+
conditions: PermissionCondition[],
|
|
280
|
+
context?: PermissionContext
|
|
281
|
+
): boolean {
|
|
282
|
+
if (!context) return true;
|
|
283
|
+
|
|
284
|
+
for (const condition of conditions) {
|
|
285
|
+
const value = context[condition.field];
|
|
286
|
+
|
|
287
|
+
switch (condition.operator) {
|
|
288
|
+
case 'eq':
|
|
289
|
+
if (value !== condition.value) return false;
|
|
290
|
+
break;
|
|
291
|
+
case 'neq':
|
|
292
|
+
if (value === condition.value) return false;
|
|
293
|
+
break;
|
|
294
|
+
case 'in':
|
|
295
|
+
if (!Array.isArray(condition.value) || !condition.value.includes(value))
|
|
296
|
+
return false;
|
|
297
|
+
break;
|
|
298
|
+
case 'not_in':
|
|
299
|
+
if (Array.isArray(condition.value) && condition.value.includes(value))
|
|
300
|
+
return false;
|
|
301
|
+
break;
|
|
302
|
+
case 'matches':
|
|
303
|
+
if (!new RegExp(condition.value as string).test(String(value)))
|
|
304
|
+
return false;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private loadBuiltinRoles(): void {
|
|
313
|
+
// Admin role
|
|
314
|
+
this.createRole({
|
|
315
|
+
name: 'admin',
|
|
316
|
+
description: 'Full system access',
|
|
317
|
+
permissions: ['*']
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Agent roles
|
|
321
|
+
this.createRole({
|
|
322
|
+
name: 'agent:explore',
|
|
323
|
+
description: 'Read-only exploration',
|
|
324
|
+
permissions: ['file:read', 'search:*', 'web:fetch']
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
this.createRole({
|
|
328
|
+
name: 'agent:implement',
|
|
329
|
+
description: 'Read and write access',
|
|
330
|
+
permissions: ['file:read', 'file:write', 'file:create', 'bash:safe']
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
this.createRole({
|
|
334
|
+
name: 'agent:review',
|
|
335
|
+
description: 'Review access',
|
|
336
|
+
permissions: ['file:read', 'git:read']
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private generateId(prefix: string): string {
|
|
341
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## SecretsVault
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
interface Secret {
|
|
352
|
+
id: string;
|
|
353
|
+
name: string;
|
|
354
|
+
value: string; // Encrypted
|
|
355
|
+
type: 'api_key' | 'password' | 'token' | 'certificate' | 'custom';
|
|
356
|
+
metadata: {
|
|
357
|
+
created_at: string;
|
|
358
|
+
updated_at: string;
|
|
359
|
+
expires_at?: string;
|
|
360
|
+
rotation_policy?: RotationPolicy;
|
|
361
|
+
access_count: number;
|
|
362
|
+
last_accessed?: string;
|
|
363
|
+
};
|
|
364
|
+
tags?: string[];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
interface RotationPolicy {
|
|
368
|
+
enabled: boolean;
|
|
369
|
+
interval_days: number;
|
|
370
|
+
notification_days: number;
|
|
371
|
+
auto_rotate?: boolean;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class SecretsVault {
|
|
375
|
+
private secrets: Map<string, Secret>;
|
|
376
|
+
private encryptionKey: Buffer;
|
|
377
|
+
private config: SecretsVaultConfig;
|
|
378
|
+
|
|
379
|
+
constructor(config: SecretsVaultConfig) {
|
|
380
|
+
this.config = config;
|
|
381
|
+
this.secrets = new Map();
|
|
382
|
+
this.encryptionKey = this.deriveKey(config.masterPassword);
|
|
383
|
+
this.loadSecrets();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Store secret
|
|
387
|
+
async store(
|
|
388
|
+
name: string,
|
|
389
|
+
value: string,
|
|
390
|
+
options?: StoreOptions
|
|
391
|
+
): Promise<Secret> {
|
|
392
|
+
const encrypted = this.encrypt(value);
|
|
393
|
+
|
|
394
|
+
const secret: Secret = {
|
|
395
|
+
id: this.generateId(),
|
|
396
|
+
name,
|
|
397
|
+
value: encrypted,
|
|
398
|
+
type: options?.type || 'custom',
|
|
399
|
+
metadata: {
|
|
400
|
+
created_at: new Date().toISOString(),
|
|
401
|
+
updated_at: new Date().toISOString(),
|
|
402
|
+
expires_at: options?.expires_at,
|
|
403
|
+
rotation_policy: options?.rotation_policy,
|
|
404
|
+
access_count: 0
|
|
405
|
+
},
|
|
406
|
+
tags: options?.tags
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
this.secrets.set(name, secret);
|
|
410
|
+
await this.persist();
|
|
411
|
+
|
|
412
|
+
AuditLogger.log({
|
|
413
|
+
action: 'secret.stored',
|
|
414
|
+
resource: name,
|
|
415
|
+
details: { type: secret.type }
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return { ...secret, value: '[REDACTED]' };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Retrieve secret
|
|
422
|
+
async get(name: string, principalId?: string): Promise<string> {
|
|
423
|
+
const secret = this.secrets.get(name);
|
|
424
|
+
if (!secret) throw new Error(`Secret not found: ${name}`);
|
|
425
|
+
|
|
426
|
+
// Check permissions
|
|
427
|
+
if (principalId) {
|
|
428
|
+
RBACManager.enforce(principalId, `secret:${name}`, 'read');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check expiration
|
|
432
|
+
if (secret.metadata.expires_at) {
|
|
433
|
+
if (new Date(secret.metadata.expires_at) < new Date()) {
|
|
434
|
+
throw new Error(`Secret expired: ${name}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update access metadata
|
|
439
|
+
secret.metadata.access_count++;
|
|
440
|
+
secret.metadata.last_accessed = new Date().toISOString();
|
|
441
|
+
|
|
442
|
+
AuditLogger.log({
|
|
443
|
+
action: 'secret.accessed',
|
|
444
|
+
resource: name,
|
|
445
|
+
principal: principalId
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return this.decrypt(secret.value);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Rotate secret
|
|
452
|
+
async rotate(name: string, newValue: string): Promise<void> {
|
|
453
|
+
const secret = this.secrets.get(name);
|
|
454
|
+
if (!secret) throw new Error(`Secret not found: ${name}`);
|
|
455
|
+
|
|
456
|
+
const oldValue = secret.value;
|
|
457
|
+
secret.value = this.encrypt(newValue);
|
|
458
|
+
secret.metadata.updated_at = new Date().toISOString();
|
|
459
|
+
|
|
460
|
+
await this.persist();
|
|
461
|
+
|
|
462
|
+
AuditLogger.log({
|
|
463
|
+
action: 'secret.rotated',
|
|
464
|
+
resource: name
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Emit rotation event
|
|
468
|
+
EventBus.publish('secret.rotated', {
|
|
469
|
+
name,
|
|
470
|
+
type: secret.type
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Delete secret
|
|
475
|
+
async delete(name: string): Promise<boolean> {
|
|
476
|
+
const deleted = this.secrets.delete(name);
|
|
477
|
+
|
|
478
|
+
if (deleted) {
|
|
479
|
+
await this.persist();
|
|
480
|
+
|
|
481
|
+
AuditLogger.log({
|
|
482
|
+
action: 'secret.deleted',
|
|
483
|
+
resource: name
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return deleted;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// List secrets (metadata only)
|
|
491
|
+
list(): SecretMetadata[] {
|
|
492
|
+
return Array.from(this.secrets.values()).map(s => ({
|
|
493
|
+
id: s.id,
|
|
494
|
+
name: s.name,
|
|
495
|
+
type: s.type,
|
|
496
|
+
metadata: s.metadata,
|
|
497
|
+
tags: s.tags
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Check for expiring secrets
|
|
502
|
+
getExpiring(days: number = 7): SecretMetadata[] {
|
|
503
|
+
const cutoff = new Date();
|
|
504
|
+
cutoff.setDate(cutoff.getDate() + days);
|
|
505
|
+
|
|
506
|
+
return this.list().filter(s => {
|
|
507
|
+
if (!s.metadata.expires_at) return false;
|
|
508
|
+
return new Date(s.metadata.expires_at) <= cutoff;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Auto-rotate secrets based on policy
|
|
513
|
+
async autoRotate(): Promise<string[]> {
|
|
514
|
+
const rotated: string[] = [];
|
|
515
|
+
|
|
516
|
+
for (const [name, secret] of this.secrets) {
|
|
517
|
+
const policy = secret.metadata.rotation_policy;
|
|
518
|
+
if (!policy?.enabled || !policy.auto_rotate) continue;
|
|
519
|
+
|
|
520
|
+
const lastRotation = new Date(secret.metadata.updated_at);
|
|
521
|
+
const daysSinceRotation = Math.floor(
|
|
522
|
+
(Date.now() - lastRotation.getTime()) / (1000 * 60 * 60 * 24)
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (daysSinceRotation >= policy.interval_days) {
|
|
526
|
+
// Generate new value (for supported types)
|
|
527
|
+
const newValue = await this.generateNewValue(secret.type);
|
|
528
|
+
if (newValue) {
|
|
529
|
+
await this.rotate(name, newValue);
|
|
530
|
+
rotated.push(name);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return rotated;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Encryption helpers
|
|
539
|
+
private encrypt(plaintext: string): string {
|
|
540
|
+
const iv = crypto.randomBytes(16);
|
|
541
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
|
|
542
|
+
|
|
543
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
|
544
|
+
encrypted += cipher.final('base64');
|
|
545
|
+
|
|
546
|
+
const authTag = cipher.getAuthTag();
|
|
547
|
+
|
|
548
|
+
return JSON.stringify({
|
|
549
|
+
iv: iv.toString('base64'),
|
|
550
|
+
data: encrypted,
|
|
551
|
+
tag: authTag.toString('base64')
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private decrypt(ciphertext: string): string {
|
|
556
|
+
const { iv, data, tag } = JSON.parse(ciphertext);
|
|
557
|
+
|
|
558
|
+
const decipher = crypto.createDecipheriv(
|
|
559
|
+
'aes-256-gcm',
|
|
560
|
+
this.encryptionKey,
|
|
561
|
+
Buffer.from(iv, 'base64')
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
|
565
|
+
|
|
566
|
+
let decrypted = decipher.update(data, 'base64', 'utf8');
|
|
567
|
+
decrypted += decipher.final('utf8');
|
|
568
|
+
|
|
569
|
+
return decrypted;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private deriveKey(password: string): Buffer {
|
|
573
|
+
return crypto.scryptSync(password, this.config.salt || 'elsabro', 32);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private async persist(): Promise<void> {
|
|
577
|
+
const data = JSON.stringify(Array.from(this.secrets.entries()));
|
|
578
|
+
const encrypted = this.encrypt(data);
|
|
579
|
+
await fs.writeFile(this.config.vaultPath, encrypted);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async loadSecrets(): Promise<void> {
|
|
583
|
+
try {
|
|
584
|
+
const encrypted = await fs.readFile(this.config.vaultPath, 'utf-8');
|
|
585
|
+
const data = this.decrypt(encrypted);
|
|
586
|
+
const entries = JSON.parse(data);
|
|
587
|
+
this.secrets = new Map(entries);
|
|
588
|
+
} catch {
|
|
589
|
+
// No vault file yet
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private async generateNewValue(type: string): Promise<string | null> {
|
|
594
|
+
switch (type) {
|
|
595
|
+
case 'api_key':
|
|
596
|
+
return crypto.randomBytes(32).toString('hex');
|
|
597
|
+
case 'password':
|
|
598
|
+
return crypto.randomBytes(24).toString('base64');
|
|
599
|
+
case 'token':
|
|
600
|
+
return crypto.randomBytes(48).toString('base64url');
|
|
601
|
+
default:
|
|
602
|
+
return null; // Cannot auto-generate
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private generateId(): string {
|
|
607
|
+
return `secret_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## AuditLogger
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
interface AuditEvent {
|
|
618
|
+
id: string;
|
|
619
|
+
timestamp: string;
|
|
620
|
+
action: string;
|
|
621
|
+
principal?: string;
|
|
622
|
+
resource?: string;
|
|
623
|
+
details?: Record<string, unknown>;
|
|
624
|
+
outcome: 'success' | 'failure' | 'denied';
|
|
625
|
+
ip_address?: string;
|
|
626
|
+
session_id?: string;
|
|
627
|
+
trace_id?: string;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
interface AuditQuery {
|
|
631
|
+
action?: string;
|
|
632
|
+
principal?: string;
|
|
633
|
+
resource?: string;
|
|
634
|
+
outcome?: string;
|
|
635
|
+
from?: string;
|
|
636
|
+
to?: string;
|
|
637
|
+
limit?: number;
|
|
638
|
+
offset?: number;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
class AuditLogger {
|
|
642
|
+
private static instance: AuditLogger;
|
|
643
|
+
private events: AuditEvent[] = [];
|
|
644
|
+
private config: AuditLoggerConfig;
|
|
645
|
+
private writeStream?: fs.WriteStream;
|
|
646
|
+
|
|
647
|
+
private constructor(config: AuditLoggerConfig) {
|
|
648
|
+
this.config = config;
|
|
649
|
+
this.initializeStorage();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
static getInstance(config?: AuditLoggerConfig): AuditLogger {
|
|
653
|
+
if (!AuditLogger.instance && config) {
|
|
654
|
+
AuditLogger.instance = new AuditLogger(config);
|
|
655
|
+
}
|
|
656
|
+
return AuditLogger.instance;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Log audit event
|
|
660
|
+
static log(event: Partial<AuditEvent>): void {
|
|
661
|
+
const instance = AuditLogger.getInstance();
|
|
662
|
+
instance.logEvent(event);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Log with automatic success outcome
|
|
666
|
+
static success(action: string, details?: Partial<AuditEvent>): void {
|
|
667
|
+
AuditLogger.log({ action, outcome: 'success', ...details });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Log with automatic failure outcome
|
|
671
|
+
static failure(action: string, details?: Partial<AuditEvent>): void {
|
|
672
|
+
AuditLogger.log({ action, outcome: 'failure', ...details });
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Log with automatic denied outcome
|
|
676
|
+
static denied(action: string, details?: Partial<AuditEvent>): void {
|
|
677
|
+
AuditLogger.log({ action, outcome: 'denied', ...details });
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private logEvent(partial: Partial<AuditEvent>): void {
|
|
681
|
+
const event: AuditEvent = {
|
|
682
|
+
id: this.generateId(),
|
|
683
|
+
timestamp: new Date().toISOString(),
|
|
684
|
+
action: partial.action || 'unknown',
|
|
685
|
+
principal: partial.principal,
|
|
686
|
+
resource: partial.resource,
|
|
687
|
+
details: partial.details,
|
|
688
|
+
outcome: partial.outcome || 'success',
|
|
689
|
+
ip_address: partial.ip_address,
|
|
690
|
+
session_id: partial.session_id,
|
|
691
|
+
trace_id: partial.trace_id || TelemetryManager.getCurrentTraceId()
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// Store in memory (with limit)
|
|
695
|
+
this.events.push(event);
|
|
696
|
+
if (this.events.length > this.config.maxMemoryEvents) {
|
|
697
|
+
this.events.shift();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Write to file
|
|
701
|
+
if (this.writeStream) {
|
|
702
|
+
this.writeStream.write(JSON.stringify(event) + '\n');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Emit event for real-time monitoring
|
|
706
|
+
EventBus.publish('audit.event', event);
|
|
707
|
+
|
|
708
|
+
// Check for alert conditions
|
|
709
|
+
this.checkAlerts(event);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Query audit log
|
|
713
|
+
query(options: AuditQuery): AuditEvent[] {
|
|
714
|
+
let filtered = this.events;
|
|
715
|
+
|
|
716
|
+
if (options.action) {
|
|
717
|
+
filtered = filtered.filter(e =>
|
|
718
|
+
e.action.includes(options.action!)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (options.principal) {
|
|
723
|
+
filtered = filtered.filter(e =>
|
|
724
|
+
e.principal === options.principal
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (options.resource) {
|
|
729
|
+
filtered = filtered.filter(e =>
|
|
730
|
+
e.resource?.includes(options.resource!)
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (options.outcome) {
|
|
735
|
+
filtered = filtered.filter(e =>
|
|
736
|
+
e.outcome === options.outcome
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (options.from) {
|
|
741
|
+
filtered = filtered.filter(e =>
|
|
742
|
+
e.timestamp >= options.from!
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (options.to) {
|
|
747
|
+
filtered = filtered.filter(e =>
|
|
748
|
+
e.timestamp <= options.to!
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Pagination
|
|
753
|
+
const offset = options.offset || 0;
|
|
754
|
+
const limit = options.limit || 100;
|
|
755
|
+
|
|
756
|
+
return filtered.slice(offset, offset + limit);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Export audit log
|
|
760
|
+
async exportLogs(
|
|
761
|
+
format: 'json' | 'csv',
|
|
762
|
+
query?: AuditQuery
|
|
763
|
+
): Promise<string> {
|
|
764
|
+
const events = query ? this.query(query) : this.events;
|
|
765
|
+
|
|
766
|
+
if (format === 'csv') {
|
|
767
|
+
const headers = [
|
|
768
|
+
'id', 'timestamp', 'action', 'principal',
|
|
769
|
+
'resource', 'outcome', 'trace_id'
|
|
770
|
+
];
|
|
771
|
+
const rows = events.map(e => [
|
|
772
|
+
e.id, e.timestamp, e.action, e.principal || '',
|
|
773
|
+
e.resource || '', e.outcome, e.trace_id || ''
|
|
774
|
+
]);
|
|
775
|
+
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return JSON.stringify(events, null, 2);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Get statistics
|
|
782
|
+
getStats(timeRange?: { from: string; to: string }): AuditStats {
|
|
783
|
+
const events = timeRange
|
|
784
|
+
? this.query({ from: timeRange.from, to: timeRange.to })
|
|
785
|
+
: this.events;
|
|
786
|
+
|
|
787
|
+
const byAction: Record<string, number> = {};
|
|
788
|
+
const byOutcome: Record<string, number> = {};
|
|
789
|
+
const byPrincipal: Record<string, number> = {};
|
|
790
|
+
|
|
791
|
+
for (const event of events) {
|
|
792
|
+
byAction[event.action] = (byAction[event.action] || 0) + 1;
|
|
793
|
+
byOutcome[event.outcome] = (byOutcome[event.outcome] || 0) + 1;
|
|
794
|
+
if (event.principal) {
|
|
795
|
+
byPrincipal[event.principal] = (byPrincipal[event.principal] || 0) + 1;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
total: events.length,
|
|
801
|
+
byAction,
|
|
802
|
+
byOutcome,
|
|
803
|
+
byPrincipal,
|
|
804
|
+
timeRange
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private checkAlerts(event: AuditEvent): void {
|
|
809
|
+
// Multiple failed authentications
|
|
810
|
+
if (event.action === 'auth.failed') {
|
|
811
|
+
const recentFailures = this.events.filter(e =>
|
|
812
|
+
e.action === 'auth.failed' &&
|
|
813
|
+
e.principal === event.principal &&
|
|
814
|
+
new Date(e.timestamp) > new Date(Date.now() - 5 * 60 * 1000)
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (recentFailures.length >= 5) {
|
|
818
|
+
this.triggerAlert('multiple_auth_failures', event);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Permission denied spike
|
|
823
|
+
if (event.outcome === 'denied') {
|
|
824
|
+
const recentDenials = this.events.filter(e =>
|
|
825
|
+
e.outcome === 'denied' &&
|
|
826
|
+
new Date(e.timestamp) > new Date(Date.now() - 60 * 1000)
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
if (recentDenials.length >= 10) {
|
|
830
|
+
this.triggerAlert('permission_denial_spike', event);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Sensitive resource access
|
|
835
|
+
const sensitiveResources = ['secret:*', 'config:*', 'admin:*'];
|
|
836
|
+
if (event.resource && sensitiveResources.some(p =>
|
|
837
|
+
event.resource!.match(new RegExp(p.replace('*', '.*')))
|
|
838
|
+
)) {
|
|
839
|
+
this.triggerAlert('sensitive_access', event);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private triggerAlert(type: string, event: AuditEvent): void {
|
|
844
|
+
EventBus.publish('audit.alert', {
|
|
845
|
+
type,
|
|
846
|
+
event,
|
|
847
|
+
timestamp: new Date().toISOString()
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private initializeStorage(): void {
|
|
852
|
+
if (this.config.logPath) {
|
|
853
|
+
this.writeStream = fs.createWriteStream(this.config.logPath, {
|
|
854
|
+
flags: 'a'
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private generateId(): string {
|
|
860
|
+
return `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## PolicyEngine
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
interface Policy {
|
|
871
|
+
id: string;
|
|
872
|
+
name: string;
|
|
873
|
+
description: string;
|
|
874
|
+
effect: 'allow' | 'deny';
|
|
875
|
+
principals: string[]; // Patterns
|
|
876
|
+
resources: string[]; // Patterns
|
|
877
|
+
actions: string[];
|
|
878
|
+
conditions?: PolicyCondition[];
|
|
879
|
+
priority: number;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
interface PolicyCondition {
|
|
883
|
+
type: 'time' | 'ip' | 'rate_limit' | 'attribute' | 'custom';
|
|
884
|
+
config: Record<string, unknown>;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
interface PolicyEvaluationResult {
|
|
888
|
+
allowed: boolean;
|
|
889
|
+
matchedPolicy?: Policy;
|
|
890
|
+
reason: string;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
class PolicyEngine {
|
|
894
|
+
private policies: Policy[] = [];
|
|
895
|
+
private cache: Map<string, PolicyEvaluationResult>;
|
|
896
|
+
private rateLimits: Map<string, RateLimitState>;
|
|
897
|
+
|
|
898
|
+
constructor(config: PolicyEngineConfig) {
|
|
899
|
+
this.cache = new Map();
|
|
900
|
+
this.rateLimits = new Map();
|
|
901
|
+
this.loadPolicies(config.policiesPath);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Add policy
|
|
905
|
+
addPolicy(policy: Omit<Policy, 'id'>): Policy {
|
|
906
|
+
const newPolicy: Policy = {
|
|
907
|
+
id: this.generateId(),
|
|
908
|
+
...policy
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
this.policies.push(newPolicy);
|
|
912
|
+
this.policies.sort((a, b) => b.priority - a.priority);
|
|
913
|
+
this.cache.clear();
|
|
914
|
+
|
|
915
|
+
return newPolicy;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Remove policy
|
|
919
|
+
removePolicy(policyId: string): boolean {
|
|
920
|
+
const index = this.policies.findIndex(p => p.id === policyId);
|
|
921
|
+
if (index === -1) return false;
|
|
922
|
+
|
|
923
|
+
this.policies.splice(index, 1);
|
|
924
|
+
this.cache.clear();
|
|
925
|
+
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Evaluate request against policies
|
|
930
|
+
evaluate(request: PolicyRequest): PolicyEvaluationResult {
|
|
931
|
+
// Check cache
|
|
932
|
+
const cacheKey = this.getCacheKey(request);
|
|
933
|
+
if (this.cache.has(cacheKey)) {
|
|
934
|
+
return this.cache.get(cacheKey)!;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Find matching policies
|
|
938
|
+
const matchingPolicies = this.policies.filter(policy =>
|
|
939
|
+
this.matchesPolicy(policy, request)
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
if (matchingPolicies.length === 0) {
|
|
943
|
+
const result = {
|
|
944
|
+
allowed: false,
|
|
945
|
+
reason: 'No matching policy'
|
|
946
|
+
};
|
|
947
|
+
this.cache.set(cacheKey, result);
|
|
948
|
+
return result;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Evaluate in priority order
|
|
952
|
+
for (const policy of matchingPolicies) {
|
|
953
|
+
// Check conditions
|
|
954
|
+
const conditionsMet = this.evaluateConditions(policy.conditions, request);
|
|
955
|
+
|
|
956
|
+
if (conditionsMet) {
|
|
957
|
+
const result: PolicyEvaluationResult = {
|
|
958
|
+
allowed: policy.effect === 'allow',
|
|
959
|
+
matchedPolicy: policy,
|
|
960
|
+
reason: policy.effect === 'allow'
|
|
961
|
+
? `Allowed by policy: ${policy.name}`
|
|
962
|
+
: `Denied by policy: ${policy.name}`
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
this.cache.set(cacheKey, result);
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const result = {
|
|
971
|
+
allowed: false,
|
|
972
|
+
reason: 'No policy conditions met'
|
|
973
|
+
};
|
|
974
|
+
this.cache.set(cacheKey, result);
|
|
975
|
+
return result;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Enforce policy (throws if denied)
|
|
979
|
+
enforce(request: PolicyRequest): void {
|
|
980
|
+
const result = this.evaluate(request);
|
|
981
|
+
|
|
982
|
+
if (!result.allowed) {
|
|
983
|
+
AuditLogger.denied('policy.denied', {
|
|
984
|
+
principal: request.principal,
|
|
985
|
+
resource: request.resource,
|
|
986
|
+
details: {
|
|
987
|
+
action: request.action,
|
|
988
|
+
policy: result.matchedPolicy?.name,
|
|
989
|
+
reason: result.reason
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
throw new PolicyDeniedError(result.reason);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
AuditLogger.success('policy.allowed', {
|
|
997
|
+
principal: request.principal,
|
|
998
|
+
resource: request.resource,
|
|
999
|
+
details: {
|
|
1000
|
+
action: request.action,
|
|
1001
|
+
policy: result.matchedPolicy?.name
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private matchesPolicy(policy: Policy, request: PolicyRequest): boolean {
|
|
1007
|
+
// Check principal
|
|
1008
|
+
const principalMatch = policy.principals.some(p =>
|
|
1009
|
+
this.matchPattern(p, request.principal)
|
|
1010
|
+
);
|
|
1011
|
+
if (!principalMatch) return false;
|
|
1012
|
+
|
|
1013
|
+
// Check resource
|
|
1014
|
+
const resourceMatch = policy.resources.some(r =>
|
|
1015
|
+
this.matchPattern(r, request.resource)
|
|
1016
|
+
);
|
|
1017
|
+
if (!resourceMatch) return false;
|
|
1018
|
+
|
|
1019
|
+
// Check action
|
|
1020
|
+
const actionMatch = policy.actions.some(a =>
|
|
1021
|
+
a === '*' || a === request.action
|
|
1022
|
+
);
|
|
1023
|
+
if (!actionMatch) return false;
|
|
1024
|
+
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private evaluateConditions(
|
|
1029
|
+
conditions: PolicyCondition[] | undefined,
|
|
1030
|
+
request: PolicyRequest
|
|
1031
|
+
): boolean {
|
|
1032
|
+
if (!conditions || conditions.length === 0) return true;
|
|
1033
|
+
|
|
1034
|
+
for (const condition of conditions) {
|
|
1035
|
+
switch (condition.type) {
|
|
1036
|
+
case 'time':
|
|
1037
|
+
if (!this.evaluateTimeCondition(condition.config)) return false;
|
|
1038
|
+
break;
|
|
1039
|
+
|
|
1040
|
+
case 'ip':
|
|
1041
|
+
if (!this.evaluateIPCondition(condition.config, request.ip)) return false;
|
|
1042
|
+
break;
|
|
1043
|
+
|
|
1044
|
+
case 'rate_limit':
|
|
1045
|
+
if (!this.evaluateRateLimit(condition.config, request)) return false;
|
|
1046
|
+
break;
|
|
1047
|
+
|
|
1048
|
+
case 'attribute':
|
|
1049
|
+
if (!this.evaluateAttribute(condition.config, request.attributes))
|
|
1050
|
+
return false;
|
|
1051
|
+
break;
|
|
1052
|
+
|
|
1053
|
+
case 'custom':
|
|
1054
|
+
if (!this.evaluateCustom(condition.config, request)) return false;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private evaluateTimeCondition(config: Record<string, unknown>): boolean {
|
|
1063
|
+
const now = new Date();
|
|
1064
|
+
const hour = now.getHours();
|
|
1065
|
+
const day = now.getDay();
|
|
1066
|
+
|
|
1067
|
+
if (config.hours) {
|
|
1068
|
+
const { start, end } = config.hours as { start: number; end: number };
|
|
1069
|
+
if (hour < start || hour >= end) return false;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (config.days) {
|
|
1073
|
+
const days = config.days as number[];
|
|
1074
|
+
if (!days.includes(day)) return false;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private evaluateIPCondition(
|
|
1081
|
+
config: Record<string, unknown>,
|
|
1082
|
+
ip?: string
|
|
1083
|
+
): boolean {
|
|
1084
|
+
if (!ip) return false;
|
|
1085
|
+
|
|
1086
|
+
const allowed = config.allowed as string[] | undefined;
|
|
1087
|
+
const blocked = config.blocked as string[] | undefined;
|
|
1088
|
+
|
|
1089
|
+
if (blocked && blocked.some(b => this.matchIP(b, ip))) return false;
|
|
1090
|
+
if (allowed && !allowed.some(a => this.matchIP(a, ip))) return false;
|
|
1091
|
+
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private evaluateRateLimit(
|
|
1096
|
+
config: Record<string, unknown>,
|
|
1097
|
+
request: PolicyRequest
|
|
1098
|
+
): boolean {
|
|
1099
|
+
const { requests, window_seconds } = config as {
|
|
1100
|
+
requests: number;
|
|
1101
|
+
window_seconds: number;
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const key = `${request.principal}:${request.resource}:${request.action}`;
|
|
1105
|
+
const state = this.rateLimits.get(key) || {
|
|
1106
|
+
count: 0,
|
|
1107
|
+
window_start: Date.now()
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
// Reset window if expired
|
|
1111
|
+
if (Date.now() - state.window_start > window_seconds * 1000) {
|
|
1112
|
+
state.count = 0;
|
|
1113
|
+
state.window_start = Date.now();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Check limit
|
|
1117
|
+
if (state.count >= requests) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Increment
|
|
1122
|
+
state.count++;
|
|
1123
|
+
this.rateLimits.set(key, state);
|
|
1124
|
+
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private evaluateAttribute(
|
|
1129
|
+
config: Record<string, unknown>,
|
|
1130
|
+
attributes?: Record<string, unknown>
|
|
1131
|
+
): boolean {
|
|
1132
|
+
if (!attributes) return false;
|
|
1133
|
+
|
|
1134
|
+
for (const [key, expected] of Object.entries(config)) {
|
|
1135
|
+
if (attributes[key] !== expected) return false;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private evaluateCustom(
|
|
1142
|
+
config: Record<string, unknown>,
|
|
1143
|
+
request: PolicyRequest
|
|
1144
|
+
): boolean {
|
|
1145
|
+
// Custom condition evaluation via config
|
|
1146
|
+
// Could call external service, run script, etc.
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private matchPattern(pattern: string, value: string): boolean {
|
|
1151
|
+
if (pattern === '*') return true;
|
|
1152
|
+
const regex = new RegExp(
|
|
1153
|
+
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
|
1154
|
+
);
|
|
1155
|
+
return regex.test(value);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private matchIP(pattern: string, ip: string): boolean {
|
|
1159
|
+
// Support CIDR notation
|
|
1160
|
+
if (pattern.includes('/')) {
|
|
1161
|
+
return this.matchCIDR(pattern, ip);
|
|
1162
|
+
}
|
|
1163
|
+
return this.matchPattern(pattern, ip);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private matchCIDR(cidr: string, ip: string): boolean {
|
|
1167
|
+
// Simplified CIDR matching
|
|
1168
|
+
const [network, bits] = cidr.split('/');
|
|
1169
|
+
const mask = ~(Math.pow(2, 32 - parseInt(bits)) - 1);
|
|
1170
|
+
|
|
1171
|
+
const ipNum = this.ipToNum(ip);
|
|
1172
|
+
const networkNum = this.ipToNum(network);
|
|
1173
|
+
|
|
1174
|
+
return (ipNum & mask) === (networkNum & mask);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private ipToNum(ip: string): number {
|
|
1178
|
+
return ip.split('.').reduce((acc, octet) =>
|
|
1179
|
+
(acc << 8) + parseInt(octet), 0
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
private getCacheKey(request: PolicyRequest): string {
|
|
1184
|
+
return `${request.principal}:${request.resource}:${request.action}`;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private async loadPolicies(path?: string): Promise<void> {
|
|
1188
|
+
if (!path) return;
|
|
1189
|
+
try {
|
|
1190
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
1191
|
+
const policies = JSON.parse(content);
|
|
1192
|
+
this.policies = policies;
|
|
1193
|
+
this.policies.sort((a: Policy, b: Policy) => b.priority - a.priority);
|
|
1194
|
+
} catch {
|
|
1195
|
+
// No policies file
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private generateId(): string {
|
|
1200
|
+
return `policy_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
## Comandos
|
|
1208
|
+
|
|
1209
|
+
```bash
|
|
1210
|
+
/elsabro:security roles # Listar roles
|
|
1211
|
+
/elsabro:security permissions # Ver permisos
|
|
1212
|
+
/elsabro:security audit # Ver audit log
|
|
1213
|
+
/elsabro:security secrets list # Listar secrets
|
|
1214
|
+
/elsabro:security policies # Ver políticas
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
---
|
|
1218
|
+
|
|
1219
|
+
## Configuración
|
|
1220
|
+
|
|
1221
|
+
```json
|
|
1222
|
+
{
|
|
1223
|
+
"security": {
|
|
1224
|
+
"enabled": true,
|
|
1225
|
+
"rbac": {
|
|
1226
|
+
"enabled": true,
|
|
1227
|
+
"defaultRole": "agent:explore"
|
|
1228
|
+
},
|
|
1229
|
+
"secrets": {
|
|
1230
|
+
"vaultPath": ".elsabro/vault.enc",
|
|
1231
|
+
"autoRotate": true
|
|
1232
|
+
},
|
|
1233
|
+
"audit": {
|
|
1234
|
+
"enabled": true,
|
|
1235
|
+
"logPath": ".elsabro/audit.log"
|
|
1236
|
+
},
|
|
1237
|
+
"policies": {
|
|
1238
|
+
"enabled": true,
|
|
1239
|
+
"defaultEffect": "deny"
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## Changelog
|
|
1248
|
+
|
|
1249
|
+
- **v3.4.0**: Initial Security System
|
|
1250
|
+
- RBACManager with role inheritance
|
|
1251
|
+
- SecretsVault with encryption
|
|
1252
|
+
- AuditLogger with alerts
|
|
1253
|
+
- PolicyEngine with conditions
|