@unrdf/kgc-probe 26.4.2

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/src/guards.mjs ADDED
@@ -0,0 +1,1213 @@
1
+ /**
2
+ * @fileoverview KGC Probe - Guard Registry with Security Enforcement
3
+ *
4
+ * Implements poka-yoke (error-proofing) guards for:
5
+ * - Environment variable access control (H1-H7)
6
+ * - File path restrictions (H8-H14)
7
+ * - Network URL whitelisting (H15-H17)
8
+ * - Command execution guards (H18-H25)
9
+ *
10
+ * All 25 forbidden patterns from the specification are implemented.
11
+ *
12
+ * @module @unrdf/kgc-probe/guards
13
+ */
14
+
15
+ import { z } from 'zod';
16
+
17
+ // ============================================================================
18
+ // SCHEMAS
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Guard decision result
23
+ * @type {z.ZodSchema}
24
+ */
25
+ export const GuardDecisionSchema = z.object({
26
+ allowed: z.boolean().describe('Whether the operation is allowed'),
27
+ reason: z.string().optional().describe('Reason for denial'),
28
+ guardId: z.string().optional().describe('Guard that made the decision'),
29
+ pattern: z.string().optional().describe('Pattern that matched'),
30
+ severity: z.enum(['critical', 'high', 'medium', 'low']).optional(),
31
+ receipt: z.object({
32
+ id: z.string(),
33
+ timestamp: z.string(),
34
+ operation: z.string(),
35
+ target: z.string(),
36
+ decision: z.enum(['allow', 'deny'])
37
+ }).optional()
38
+ }).describe('Guard decision result');
39
+
40
+ /**
41
+ * Guard configuration schema
42
+ * @type {z.ZodSchema}
43
+ */
44
+ export const GuardConfigSchema = z.object({
45
+ quality_check: z.object({
46
+ critical_observations_threshold: z.number().positive().default(50),
47
+ confidence_min: z.number().min(0).max(1).default(0.6)
48
+ }).optional(),
49
+ completeness_check: z.object({
50
+ coverage_min: z.number().min(0).max(1).default(0.7)
51
+ }).optional(),
52
+ severity_limit: z.object({
53
+ critical_limit: z.number().positive().default(10)
54
+ }).optional(),
55
+ network_allowlist: z.array(z.object({
56
+ hostname: z.string(),
57
+ paths: z.array(z.string()).default(['**']),
58
+ methods: z.array(z.string()).default(['GET'])
59
+ })).optional(),
60
+ cache_ttl_ms: z.number().positive().default(300000)
61
+ }).describe('Guard thresholds and limits');
62
+
63
+ // ============================================================================
64
+ // LRU CACHE
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Simple LRU cache for guard decisions
69
+ * @class LRUCache
70
+ */
71
+ class LRUCache {
72
+ /**
73
+ * @param {number} maxSize - Maximum cache entries
74
+ * @param {number} ttlMs - Time-to-live in milliseconds
75
+ */
76
+ constructor(maxSize = 1000, ttlMs = 300000) {
77
+ /** @type {Map<string, {value: any, timestamp: number}>} */
78
+ this.cache = new Map();
79
+ this.maxSize = maxSize;
80
+ this.ttlMs = ttlMs;
81
+ }
82
+
83
+ /**
84
+ * Get cached value
85
+ * @param {string} key - Cache key
86
+ * @returns {any | undefined} Cached value or undefined
87
+ */
88
+ get(key) {
89
+ const entry = this.cache.get(key);
90
+ if (!entry) return undefined;
91
+
92
+ // Check TTL
93
+ if (Date.now() - entry.timestamp > this.ttlMs) {
94
+ this.cache.delete(key);
95
+ return undefined;
96
+ }
97
+
98
+ // Move to end (most recently used)
99
+ this.cache.delete(key);
100
+ this.cache.set(key, entry);
101
+ return entry.value;
102
+ }
103
+
104
+ /**
105
+ * Set cached value
106
+ * @param {string} key - Cache key
107
+ * @param {any} value - Value to cache
108
+ */
109
+ set(key, value) {
110
+ // Remove oldest if at capacity
111
+ if (this.cache.size >= this.maxSize) {
112
+ const firstKey = this.cache.keys().next().value;
113
+ this.cache.delete(firstKey);
114
+ }
115
+
116
+ this.cache.set(key, {
117
+ value,
118
+ timestamp: Date.now()
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Check if key exists and is valid
124
+ * @param {string} key - Cache key
125
+ * @returns {boolean}
126
+ */
127
+ has(key) {
128
+ return this.get(key) !== undefined;
129
+ }
130
+
131
+ /**
132
+ * Clear cache
133
+ */
134
+ clear() {
135
+ this.cache.clear();
136
+ }
137
+
138
+ /**
139
+ * Get cache stats
140
+ * @returns {{size: number, maxSize: number, hitRate: number}}
141
+ */
142
+ stats() {
143
+ return {
144
+ size: this.cache.size,
145
+ maxSize: this.maxSize,
146
+ hitRate: 0 // Would track hits/misses in production
147
+ };
148
+ }
149
+ }
150
+
151
+ // ============================================================================
152
+ // PATTERN MATCHING UTILITIES
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Convert wildcard pattern to regex
157
+ * @param {string} pattern - Wildcard pattern (e.g., "*TOKEN", "AWS_*")
158
+ * @returns {RegExp} Compiled regex
159
+ */
160
+ function patternToRegex(pattern) {
161
+ // Use placeholders to avoid regex replacement conflicts
162
+ const DOUBLE_STAR = '\u0000DOUBLE_STAR\u0000';
163
+ const SINGLE_STAR = '\u0001SINGLE_STAR\u0001';
164
+ const QUESTION = '\u0002QUESTION\u0002';
165
+
166
+ let escaped = pattern
167
+ // Replace wildcards with placeholders first
168
+ .replace(/\*\*/g, DOUBLE_STAR)
169
+ .replace(/\*/g, SINGLE_STAR)
170
+ .replace(/\?/g, QUESTION)
171
+ // Escape regex special chars
172
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
173
+ // Replace placeholders with regex patterns
174
+ .replace(new RegExp(DOUBLE_STAR, 'g'), '.*')
175
+ .replace(new RegExp(SINGLE_STAR, 'g'), '[^/]*')
176
+ .replace(new RegExp(QUESTION, 'g'), '.');
177
+
178
+ return new RegExp(`^${escaped}$`, 'i');
179
+ }
180
+
181
+ /**
182
+ * Check if value matches any pattern
183
+ * @param {string} value - Value to check
184
+ * @param {string[]} patterns - Patterns to match against
185
+ * @returns {{matched: boolean, pattern: string | null}}
186
+ */
187
+ function matchesAnyPattern(value, patterns) {
188
+ for (const pattern of patterns) {
189
+ const regex = patternToRegex(pattern);
190
+ if (regex.test(value)) {
191
+ return { matched: true, pattern };
192
+ }
193
+ }
194
+ return { matched: false, pattern: null };
195
+ }
196
+
197
+ /**
198
+ * Normalize path (expand ~, resolve .., normalize slashes)
199
+ * @param {string} filePath - Path to normalize
200
+ * @returns {string} Normalized path
201
+ */
202
+ function normalizePath(filePath) {
203
+ if (!filePath || typeof filePath !== 'string') return '';
204
+
205
+ let normalized = filePath;
206
+
207
+ // Expand ~ to home directory placeholder
208
+ if (normalized.startsWith('~')) {
209
+ normalized = '/home/user' + normalized.slice(1);
210
+ }
211
+
212
+ // Normalize slashes
213
+ normalized = normalized.replace(/\\/g, '/').replace(/\/+/g, '/');
214
+
215
+ // Remove trailing slash (except for root)
216
+ if (normalized.length > 1 && normalized.endsWith('/')) {
217
+ normalized = normalized.slice(0, -1);
218
+ }
219
+
220
+ return normalized;
221
+ }
222
+
223
+ /**
224
+ * Generate UUID v4
225
+ * @returns {string}
226
+ */
227
+ function generateUUID() {
228
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
229
+ const r = (Math.random() * 16) | 0;
230
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
231
+ return v.toString(16);
232
+ });
233
+ }
234
+
235
+ // ============================================================================
236
+ // FORBIDDEN PATTERNS (25 Categories)
237
+ // ============================================================================
238
+
239
+ /**
240
+ * H1-H7: Environment variable forbidden patterns
241
+ * @type {string[]}
242
+ */
243
+ const ENV_VAR_FORBIDDEN_PATTERNS = [
244
+ // H1: Credentials & API Keys
245
+ '*TOKEN',
246
+ '*KEY',
247
+ '*SECRET',
248
+ '*PASSWORD',
249
+ '*CREDENTIAL*',
250
+ // H2: Cloud Provider Credentials
251
+ 'AWS_*',
252
+ 'AZURE_*',
253
+ 'GCP_*',
254
+ 'GOOGLE_*',
255
+ // H3: VCS Credentials
256
+ 'GITHUB_*',
257
+ 'GITLAB_*',
258
+ 'BITBUCKET_*',
259
+ 'GIT_CREDENTIALS',
260
+ // H4: Service API Keys
261
+ '*_API_KEY',
262
+ '*_API_SECRET',
263
+ '*_BEARER_TOKEN',
264
+ 'STRIPE_*',
265
+ 'SLACK_*',
266
+ 'SENDGRID_*',
267
+ // H5: Encryption Keys
268
+ 'ENCRYPTION_*',
269
+ 'CIPHER_*',
270
+ '*_PRIVATE_KEY',
271
+ // H6: Database Credentials
272
+ 'DB_*',
273
+ 'DATABASE_*',
274
+ '*_CONN_STRING',
275
+ 'MONGO_*',
276
+ 'POSTGRES_*',
277
+ 'MYSQL_*',
278
+ 'REDIS_*',
279
+ // H7: Sensitive Configuration
280
+ 'ADMIN_*',
281
+ 'ROOT_*',
282
+ 'MASTER_*',
283
+ '*PRIVATE*'
284
+ ];
285
+
286
+ /**
287
+ * H8-H14: File path forbidden patterns
288
+ * @type {{pattern: string, category: string}[]}
289
+ */
290
+ const FILE_PATH_FORBIDDEN_PATTERNS = [
291
+ // H8: SSH Keys
292
+ { pattern: '**/.ssh/**', category: 'SSH_KEYS' },
293
+ { pattern: '**/id_rsa', category: 'SSH_KEYS' },
294
+ { pattern: '**/id_dsa', category: 'SSH_KEYS' },
295
+ { pattern: '**/id_ed25519', category: 'SSH_KEYS' },
296
+ { pattern: '**/authorized_keys', category: 'SSH_KEYS' },
297
+ { pattern: '**/known_hosts', category: 'SSH_KEYS' },
298
+ // H9: AWS Configuration
299
+ { pattern: '**/.aws/**', category: 'AWS_CONFIG' },
300
+ { pattern: '**/credentials', category: 'AWS_CONFIG' },
301
+ // H10: NPM Registry
302
+ { pattern: '**/.npmrc', category: 'NPM_REGISTRY' },
303
+ { pattern: '**/*/.npmrc', category: 'NPM_REGISTRY' },
304
+ { pattern: '**/npm-auth.json', category: 'NPM_REGISTRY' },
305
+ // H11: Environment Files
306
+ { pattern: '**/.env', category: 'ENV_FILE' },
307
+ { pattern: '**/.env.local', category: 'ENV_FILE' },
308
+ { pattern: '**/.env.*.local', category: 'ENV_FILE' },
309
+ { pattern: '**/.env.production', category: 'ENV_FILE' },
310
+ { pattern: '**/secrets.yml', category: 'ENV_FILE' },
311
+ { pattern: '**/secrets.yaml', category: 'ENV_FILE' },
312
+ // H12: Git Configuration
313
+ { pattern: '**/.git/config', category: 'GIT_CONFIG' },
314
+ { pattern: '**/.gitcredentials', category: 'GIT_CONFIG' },
315
+ { pattern: '**/git-credentials', category: 'GIT_CONFIG' },
316
+ // H13: Kubernetes Config
317
+ { pattern: '**/.kube/config', category: 'KUBE_CONFIG' },
318
+ { pattern: '**/kubeconfig*', category: 'KUBE_CONFIG' },
319
+ // H14: Docker Registry
320
+ { pattern: '**/.docker/config.json', category: 'DOCKER_CONFIG' },
321
+ { pattern: '**/.dockercfg', category: 'DOCKER_CONFIG' },
322
+ // System files
323
+ { pattern: '/etc/passwd', category: 'SYSTEM_USER_DB' },
324
+ { pattern: '/etc/shadow', category: 'SYSTEM_PASSWORD_DB' },
325
+ { pattern: '/etc/sudoers', category: 'SYSTEM_SUDO' },
326
+ { pattern: '/proc/*/environ', category: 'PROCESS_ENV' },
327
+ { pattern: '/proc/self/**', category: 'PROCESS_MEMORY' }
328
+ ];
329
+
330
+ /**
331
+ * H15-H17: Network allowed hosts (whitelist approach)
332
+ * @type {{hostname: string, paths: string[], methods: string[]}[]}
333
+ */
334
+ const NETWORK_ALLOWED_HOSTS = [
335
+ { hostname: 'api.github.com', paths: ['**'], methods: ['GET'] },
336
+ { hostname: 'registry.npmjs.org', paths: ['**'], methods: ['GET'] },
337
+ { hostname: 'github.com', paths: ['**'], methods: ['GET'] },
338
+ { hostname: 'docs.github.com', paths: ['**'], methods: ['GET'] },
339
+ { hostname: 'localhost', paths: ['**'], methods: ['GET', 'POST'] },
340
+ { hostname: '127.0.0.1', paths: ['**'], methods: ['GET', 'POST'] }
341
+ ];
342
+
343
+ /**
344
+ * H15-H17: Network forbidden domains
345
+ * @type {string[]}
346
+ */
347
+ const NETWORK_FORBIDDEN_DOMAINS = [
348
+ 'metadata.google.internal',
349
+ '169.254.169.254', // AWS/Azure metadata
350
+ '169.254.*',
351
+ '*.internal',
352
+ '*.local',
353
+ '10.*',
354
+ '172.16.*',
355
+ '172.17.*',
356
+ '172.18.*',
357
+ '172.19.*',
358
+ '172.20.*',
359
+ '172.21.*',
360
+ '172.22.*',
361
+ '172.23.*',
362
+ '172.24.*',
363
+ '172.25.*',
364
+ '172.26.*',
365
+ '172.27.*',
366
+ '172.28.*',
367
+ '172.29.*',
368
+ '172.30.*',
369
+ '172.31.*',
370
+ '192.168.*'
371
+ ];
372
+
373
+ /**
374
+ * H18-H25: Forbidden commands and patterns
375
+ * @type {string[]}
376
+ */
377
+ const FORBIDDEN_COMMANDS = [
378
+ 'env',
379
+ 'printenv',
380
+ 'set',
381
+ 'declare',
382
+ 'whoami',
383
+ 'id',
384
+ 'groups',
385
+ 'ps',
386
+ 'cat /etc/passwd',
387
+ 'cat /etc/shadow',
388
+ 'curl',
389
+ 'wget',
390
+ 'nc',
391
+ 'netcat',
392
+ 'nmap',
393
+ 'ssh',
394
+ 'scp',
395
+ 'rsync'
396
+ ];
397
+
398
+ /**
399
+ * H21: Dangerous shell patterns (injection vectors)
400
+ * @type {string[]}
401
+ */
402
+ const DANGEROUS_SHELL_PATTERNS = [
403
+ '|', // Pipe
404
+ '&', // Background/AND
405
+ ';', // Sequence
406
+ '>', // Redirect output
407
+ '<', // Redirect input
408
+ '>>', // Append
409
+ '`', // Backtick substitution
410
+ '$(', // Command substitution
411
+ '${', // Variable expansion
412
+ '&&', // Conditional AND
413
+ '||' // Conditional OR
414
+ ];
415
+
416
+ // ============================================================================
417
+ // GUARD REGISTRY
418
+ // ============================================================================
419
+
420
+ /**
421
+ * GuardRegistry - Security-focused guard management with LRU caching
422
+ *
423
+ * Implements all 25 forbidden pattern categories from the specification:
424
+ * - H1-H7: Environment variable guards
425
+ * - H8-H14: File path guards
426
+ * - H15-H17: Network URL guards
427
+ * - H18-H25: Command execution guards
428
+ *
429
+ * @class GuardRegistry
430
+ * @example
431
+ * const registry = new GuardRegistry();
432
+ * const decision = registry.checkEnvironmentVariable('AWS_SECRET_KEY');
433
+ * if (!decision.allowed) {
434
+ * console.log('Access denied:', decision.reason);
435
+ * }
436
+ */
437
+ export class GuardRegistry {
438
+ /**
439
+ * Create guard registry with security guards and quality guards
440
+ * @param {Object} [config] - Guard configuration
441
+ */
442
+ constructor(config = {}) {
443
+ /** @type {Map<string, {id: string, validate: Function}>} */
444
+ this.guards = new Map();
445
+
446
+ /** @type {Object} */
447
+ this.config = GuardConfigSchema.parse(config);
448
+
449
+ /** @type {LRUCache} */
450
+ this.cache = new LRUCache(1000, this.config.cache_ttl_ms || 300000);
451
+
452
+ /** @type {Array<Object>} */
453
+ this.auditLog = [];
454
+
455
+ /** @type {string[]} */
456
+ this.envForbiddenPatterns = [...ENV_VAR_FORBIDDEN_PATTERNS];
457
+
458
+ /** @type {{pattern: string, category: string}[]} */
459
+ this.fileForbiddenPatterns = [...FILE_PATH_FORBIDDEN_PATTERNS];
460
+
461
+ /** @type {{hostname: string, paths: string[], methods: string[]}[]} */
462
+ this.networkAllowlist = [
463
+ ...NETWORK_ALLOWED_HOSTS,
464
+ ...(this.config.network_allowlist || [])
465
+ ];
466
+
467
+ // Register default guards
468
+ this.registerDefault();
469
+ }
470
+
471
+ /**
472
+ * Register default quality + security guards
473
+ * @private
474
+ */
475
+ registerDefault() {
476
+ // Quality guards (original 5)
477
+ this.register('quality_check', {
478
+ id: 'quality_check',
479
+ validate: (observations) => this.validateQuality(observations)
480
+ });
481
+
482
+ this.register('completeness_check', {
483
+ id: 'completeness_check',
484
+ validate: (observations) => this.validateCompleteness(observations)
485
+ });
486
+
487
+ this.register('severity_limit', {
488
+ id: 'severity_limit',
489
+ validate: (observations) => this.validateSeverity(observations)
490
+ });
491
+
492
+ this.register('integrity_check', {
493
+ id: 'integrity_check',
494
+ validate: (observations) => this.validateIntegrity(observations)
495
+ });
496
+
497
+ this.register('agent_coverage', {
498
+ id: 'agent_coverage',
499
+ validate: (observations) => this.validateAgentCoverage(observations)
500
+ });
501
+
502
+ // Security guards (H1-H25)
503
+ this.register('env_var_guard', {
504
+ id: 'env_var_guard',
505
+ validate: (name) => this.checkEnvironmentVariable(name)
506
+ });
507
+
508
+ this.register('file_path_guard', {
509
+ id: 'file_path_guard',
510
+ validate: (path) => this.checkFilePathAccess(path)
511
+ });
512
+
513
+ this.register('network_guard', {
514
+ id: 'network_guard',
515
+ validate: (url) => this.checkNetworkURL(url)
516
+ });
517
+
518
+ this.register('command_guard', {
519
+ id: 'command_guard',
520
+ validate: (cmd) => this.checkCommandExecution(cmd)
521
+ });
522
+ }
523
+
524
+ /**
525
+ * Register custom guard
526
+ * @param {string} id - Guard identifier
527
+ * @param {{id: string, validate: Function}} guard - Guard implementation
528
+ */
529
+ register(id, guard) {
530
+ this.guards.set(id, guard);
531
+ }
532
+
533
+ /**
534
+ * Get guard by ID
535
+ * @param {string} id - Guard identifier
536
+ * @returns {{id: string, validate: Function} | undefined}
537
+ */
538
+ get(id) {
539
+ return this.guards.get(id);
540
+ }
541
+
542
+ /**
543
+ * List all guard IDs
544
+ * @returns {string[]}
545
+ */
546
+ list() {
547
+ return Array.from(this.guards.keys());
548
+ }
549
+
550
+ /**
551
+ * Run single guard
552
+ * @param {string} guardId - Guard identifier
553
+ * @param {any} input - Input to validate
554
+ * @returns {Object[] | Object} Violations or decision
555
+ */
556
+ validate(guardId, input) {
557
+ const guard = this.guards.get(guardId);
558
+ if (!guard) {
559
+ throw new Error(`Guard not found: ${guardId}`);
560
+ }
561
+ return guard.validate(input);
562
+ }
563
+
564
+ /**
565
+ * Run all observation guards
566
+ * @param {Array} observations - Observations to validate
567
+ * @returns {Object[]} All violations from all guards
568
+ */
569
+ validateAll(observations) {
570
+ const allViolations = [];
571
+ const observationGuards = ['quality_check', 'completeness_check', 'severity_limit', 'integrity_check', 'agent_coverage'];
572
+
573
+ for (const guardId of observationGuards) {
574
+ const guard = this.guards.get(guardId);
575
+ if (guard) {
576
+ try {
577
+ const violations = guard.validate(observations);
578
+ allViolations.push(...violations);
579
+ } catch (err) {
580
+ console.error(`Guard ${guardId} error:`, err);
581
+ }
582
+ }
583
+ }
584
+
585
+ return allViolations;
586
+ }
587
+
588
+ // =========================================================================
589
+ // SECURITY GUARDS (H1-H25)
590
+ // =========================================================================
591
+
592
+ /**
593
+ * Check environment variable access (H1-H7)
594
+ *
595
+ * @param {string} name - Environment variable name
596
+ * @returns {{allowed: boolean, reason?: string, guardId?: string, pattern?: string, severity?: string, receipt?: Object}}
597
+ * @example
598
+ * const result = registry.checkEnvironmentVariable('AWS_SECRET_KEY');
599
+ * // { allowed: false, reason: 'Matches forbidden pattern: AWS_*', ... }
600
+ */
601
+ checkEnvironmentVariable(name) {
602
+ // Input validation
603
+ if (!name || typeof name !== 'string') {
604
+ return this.createDenial('env-var-access', name || '', 'INVALID_INPUT', 'Invalid variable name format', 'G-H1-ENV', 'high');
605
+ }
606
+
607
+ // Check cache
608
+ const cacheKey = `env:${name.toUpperCase()}`;
609
+ const cached = this.cache.get(cacheKey);
610
+ if (cached !== undefined) {
611
+ return cached;
612
+ }
613
+
614
+ const normalized = name.toUpperCase().trim();
615
+ const match = matchesAnyPattern(normalized, this.envForbiddenPatterns);
616
+
617
+ let result;
618
+ if (match.matched) {
619
+ result = this.createDenial(
620
+ 'env-var-access',
621
+ name,
622
+ 'FORBIDDEN_CREDENTIAL_PATTERN',
623
+ `Environment variable matches forbidden pattern: ${match.pattern}`,
624
+ 'G-H1-ENV-TOKEN',
625
+ 'critical',
626
+ match.pattern
627
+ );
628
+ } else {
629
+ result = { allowed: true };
630
+ }
631
+
632
+ // Cache and audit
633
+ this.cache.set(cacheKey, result);
634
+ this.logAudit('env-var-access', name, result.allowed, 'G-H1-ENV-TOKEN', result.reason);
635
+
636
+ return result;
637
+ }
638
+
639
+ /**
640
+ * Check file path access (H8-H14)
641
+ *
642
+ * @param {string} path - File path to check
643
+ * @param {string} [operation='read'] - Operation type (read|write|stat)
644
+ * @returns {{allowed: boolean, reason?: string, guardId?: string, category?: string, severity?: string, receipt?: Object}}
645
+ * @example
646
+ * const result = registry.checkFilePathAccess('~/.ssh/id_rsa');
647
+ * // { allowed: false, reason: 'Access to SSH keys forbidden', ... }
648
+ */
649
+ checkFilePathAccess(path, operation = 'read') {
650
+ // Input validation
651
+ if (!path || typeof path !== 'string') {
652
+ return this.createDenial('fs-' + operation, path || '', 'INVALID_INPUT', 'Invalid file path', 'G-H8-FILE', 'high');
653
+ }
654
+
655
+ // Check cache
656
+ const cacheKey = `file:${path}:${operation}`;
657
+ const cached = this.cache.get(cacheKey);
658
+ if (cached !== undefined) {
659
+ return cached;
660
+ }
661
+
662
+ const normalizedPath = normalizePath(path);
663
+
664
+ // Check against forbidden patterns
665
+ for (const forbidden of this.fileForbiddenPatterns) {
666
+ const regex = patternToRegex(forbidden.pattern);
667
+ if (regex.test(normalizedPath) || regex.test(path)) {
668
+ const result = this.createDenial(
669
+ 'fs-' + operation,
670
+ path,
671
+ 'FORBIDDEN_FILE_PATH',
672
+ `Access to ${forbidden.category} files forbidden`,
673
+ 'G-H8-' + forbidden.category,
674
+ 'critical',
675
+ forbidden.pattern
676
+ );
677
+ result.category = forbidden.category;
678
+ result.resolvedPath = normalizedPath;
679
+
680
+ this.cache.set(cacheKey, result);
681
+ this.logAudit('fs-' + operation, path, false, 'G-H8-' + forbidden.category, result.reason);
682
+ return result;
683
+ }
684
+ }
685
+
686
+ const result = { allowed: true };
687
+ this.cache.set(cacheKey, result);
688
+ this.logAudit('fs-' + operation, path, true, 'G-H8-FILE', null);
689
+ return result;
690
+ }
691
+
692
+ /**
693
+ * Check network URL access (H15-H17)
694
+ *
695
+ * @param {string} url - URL to check
696
+ * @param {string} [method='GET'] - HTTP method
697
+ * @returns {{allowed: boolean, reason?: string, guardId?: string, hostname?: string, severity?: string, receipt?: Object}}
698
+ * @example
699
+ * const result = registry.checkNetworkURL('http://169.254.169.254/latest/meta-data/');
700
+ * // { allowed: false, reason: 'Network access to metadata service forbidden', ... }
701
+ */
702
+ checkNetworkURL(url, method = 'GET') {
703
+ // Input validation
704
+ if (!url || typeof url !== 'string') {
705
+ return this.createDenial('network-request', url || '', 'INVALID_INPUT', 'Invalid URL format', 'G-H15-NETWORK', 'high');
706
+ }
707
+
708
+ // Check cache
709
+ const cacheKey = `net:${method}:${url}`;
710
+ const cached = this.cache.get(cacheKey);
711
+ if (cached !== undefined) {
712
+ return cached;
713
+ }
714
+
715
+ // Parse URL
716
+ let parsed;
717
+ try {
718
+ parsed = new URL(url);
719
+ } catch {
720
+ return this.createDenial('network-request', url, 'INVALID_URL', 'Cannot parse URL', 'G-H15-NETWORK', 'high');
721
+ }
722
+
723
+ const hostname = parsed.hostname;
724
+ const pathname = parsed.pathname || '/';
725
+
726
+ // Check forbidden domains first
727
+ const forbiddenMatch = matchesAnyPattern(hostname, NETWORK_FORBIDDEN_DOMAINS);
728
+ if (forbiddenMatch.matched) {
729
+ const result = this.createDenial(
730
+ 'network-request',
731
+ url,
732
+ 'FORBIDDEN_HOST',
733
+ `Network access to ${hostname} forbidden (matches ${forbiddenMatch.pattern})`,
734
+ 'G-H16-DNS',
735
+ 'critical',
736
+ forbiddenMatch.pattern
737
+ );
738
+ result.hostname = hostname;
739
+
740
+ this.cache.set(cacheKey, result);
741
+ this.logAudit('network-request', url, false, 'G-H16-DNS', result.reason);
742
+ return result;
743
+ }
744
+
745
+ // Check against allowlist
746
+ let allowed = false;
747
+ for (const entry of this.networkAllowlist) {
748
+ if (entry.hostname === hostname || entry.hostname === '*') {
749
+ // Check method
750
+ if (!entry.methods.includes(method) && !entry.methods.includes('*')) {
751
+ continue;
752
+ }
753
+
754
+ // Check path
755
+ for (const pathPattern of entry.paths) {
756
+ const pathRegex = patternToRegex(pathPattern);
757
+ if (pathRegex.test(pathname)) {
758
+ allowed = true;
759
+ break;
760
+ }
761
+ }
762
+ if (allowed) break;
763
+ }
764
+ }
765
+
766
+ let result;
767
+ if (!allowed) {
768
+ result = this.createDenial(
769
+ 'network-request',
770
+ url,
771
+ 'NOT_IN_ALLOWLIST',
772
+ `Host ${hostname} not in network allowlist`,
773
+ 'G-H15-NETWORK-URL',
774
+ 'high'
775
+ );
776
+ result.hostname = hostname;
777
+ } else {
778
+ result = { allowed: true };
779
+ }
780
+
781
+ this.cache.set(cacheKey, result);
782
+ this.logAudit('network-request', url, result.allowed, 'G-H15-NETWORK-URL', result.reason);
783
+ return result;
784
+ }
785
+
786
+ /**
787
+ * Check command execution (H18-H25)
788
+ *
789
+ * @param {string} command - Command to check
790
+ * @param {string[]} [args=[]] - Command arguments
791
+ * @returns {{allowed: boolean, reason?: string, guardId?: string, severity?: string, receipt?: Object}}
792
+ * @example
793
+ * const result = registry.checkCommandExecution('cat /etc/passwd');
794
+ * // { allowed: false, reason: 'Forbidden command: cat /etc/passwd', ... }
795
+ */
796
+ checkCommandExecution(command, args = []) {
797
+ // Input validation
798
+ if (!command || typeof command !== 'string') {
799
+ return this.createDenial('command-execution', command || '', 'INVALID_INPUT', 'Invalid command', 'G-H21-CMD', 'high');
800
+ }
801
+
802
+ // Check cache
803
+ const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
804
+ const cacheKey = `cmd:${fullCommand}`;
805
+ const cached = this.cache.get(cacheKey);
806
+ if (cached !== undefined) {
807
+ return cached;
808
+ }
809
+
810
+ const commandLower = fullCommand.toLowerCase();
811
+
812
+ // Check forbidden commands
813
+ for (const forbidden of FORBIDDEN_COMMANDS) {
814
+ if (commandLower.includes(forbidden.toLowerCase())) {
815
+ const result = this.createDenial(
816
+ 'command-execution',
817
+ fullCommand,
818
+ 'FORBIDDEN_COMMAND',
819
+ `Forbidden command detected: ${forbidden}`,
820
+ 'G-H21-CMD',
821
+ 'critical',
822
+ forbidden
823
+ );
824
+
825
+ this.cache.set(cacheKey, result);
826
+ this.logAudit('command-execution', fullCommand, false, 'G-H21-CMD', result.reason);
827
+ return result;
828
+ }
829
+ }
830
+
831
+ // Check for shell injection patterns
832
+ for (const pattern of DANGEROUS_SHELL_PATTERNS) {
833
+ if (fullCommand.includes(pattern)) {
834
+ const result = this.createDenial(
835
+ 'command-execution',
836
+ fullCommand,
837
+ 'SHELL_INJECTION_ATTEMPT',
838
+ `Dangerous shell pattern detected: ${pattern}`,
839
+ 'G-H21-SHELL-INJECTION',
840
+ 'critical',
841
+ pattern
842
+ );
843
+
844
+ this.cache.set(cacheKey, result);
845
+ this.logAudit('command-execution', fullCommand, false, 'G-H21-SHELL-INJECTION', result.reason);
846
+ return result;
847
+ }
848
+ }
849
+
850
+ // Check arguments for injection
851
+ for (const arg of args) {
852
+ for (const pattern of DANGEROUS_SHELL_PATTERNS) {
853
+ if (arg.includes(pattern)) {
854
+ const result = this.createDenial(
855
+ 'command-execution',
856
+ fullCommand,
857
+ 'SHELL_INJECTION_IN_ARGS',
858
+ `Dangerous pattern in argument: ${pattern}`,
859
+ 'G-H21-SHELL-INJECTION',
860
+ 'critical',
861
+ pattern
862
+ );
863
+
864
+ this.cache.set(cacheKey, result);
865
+ this.logAudit('command-execution', fullCommand, false, 'G-H21-SHELL-INJECTION', result.reason);
866
+ return result;
867
+ }
868
+ }
869
+ }
870
+
871
+ const result = { allowed: true };
872
+ this.cache.set(cacheKey, result);
873
+ this.logAudit('command-execution', fullCommand, true, 'G-H21-CMD', null);
874
+ return result;
875
+ }
876
+
877
+ // =========================================================================
878
+ // QUALITY GUARDS (Original 5)
879
+ // =========================================================================
880
+
881
+ /**
882
+ * Validate observation quality
883
+ * @param {Array} observations - Observations to check
884
+ * @returns {Object[]} Violations
885
+ * @private
886
+ */
887
+ validateQuality(observations) {
888
+ const violations = [];
889
+ const thresholds = {
890
+ critical_observations: this.config.quality_check?.critical_observations_threshold || 50,
891
+ confidence_min: this.config.quality_check?.confidence_min || 0.6
892
+ };
893
+
894
+ const lowConfidence = observations.filter(
895
+ o => o.metrics?.confidence < thresholds.confidence_min
896
+ );
897
+
898
+ if (lowConfidence.length > thresholds.critical_observations) {
899
+ violations.push({
900
+ guard_id: 'quality_check',
901
+ severity: 'warning',
902
+ details: {
903
+ message: 'High count of low-confidence observations',
904
+ count: lowConfidence.length,
905
+ threshold: thresholds.critical_observations,
906
+ confidence_min: thresholds.confidence_min
907
+ }
908
+ });
909
+ }
910
+
911
+ const avgConfidence = observations.length > 0
912
+ ? observations.reduce((sum, o) => sum + (o.metrics?.confidence || 0), 0) / observations.length
913
+ : 1.0;
914
+
915
+ if (avgConfidence < 0.7) {
916
+ violations.push({
917
+ guard_id: 'quality_check',
918
+ severity: 'warning',
919
+ details: {
920
+ message: 'Average confidence below 70%',
921
+ actual: avgConfidence,
922
+ threshold: 0.7
923
+ }
924
+ });
925
+ }
926
+
927
+ return violations;
928
+ }
929
+
930
+ /**
931
+ * Validate completeness observations
932
+ * @param {Array} observations - Observations to check
933
+ * @returns {Object[]} Violations
934
+ * @private
935
+ */
936
+ validateCompleteness(observations) {
937
+ const violations = [];
938
+ const thresholds = {
939
+ coverage_min: this.config.completeness_check?.coverage_min || 0.7
940
+ };
941
+
942
+ const completenessObs = observations.filter(
943
+ o => o.kind === 'completeness' || o.kind === 'completeness_level'
944
+ );
945
+
946
+ if (completenessObs.length === 0) {
947
+ return [];
948
+ }
949
+
950
+ const avgCoverage = completenessObs.reduce(
951
+ (sum, o) => sum + (o.metrics?.coverage || 0),
952
+ 0
953
+ ) / completenessObs.length;
954
+
955
+ if (avgCoverage < thresholds.coverage_min) {
956
+ violations.push({
957
+ guard_id: 'completeness_check',
958
+ severity: 'warning',
959
+ details: {
960
+ message: 'Data coverage below threshold',
961
+ coverage: avgCoverage,
962
+ threshold: thresholds.coverage_min,
963
+ observations_checked: completenessObs.length
964
+ }
965
+ });
966
+ }
967
+
968
+ return violations;
969
+ }
970
+
971
+ /**
972
+ * Validate severity limits
973
+ * @param {Array} observations - Observations to check
974
+ * @returns {Object[]} Violations
975
+ * @private
976
+ */
977
+ validateSeverity(observations) {
978
+ const violations = [];
979
+ const thresholds = {
980
+ critical_limit: this.config.severity_limit?.critical_limit || 10
981
+ };
982
+
983
+ const criticalCount = observations.filter(o => o.severity === 'critical').length;
984
+
985
+ if (criticalCount > thresholds.critical_limit) {
986
+ violations.push({
987
+ guard_id: 'severity_limit',
988
+ severity: 'critical',
989
+ details: {
990
+ message: 'Critical violations exceed limit',
991
+ count: criticalCount,
992
+ limit: thresholds.critical_limit
993
+ }
994
+ });
995
+ }
996
+
997
+ return violations;
998
+ }
999
+
1000
+ /**
1001
+ * Validate artifact integrity
1002
+ * @param {Array} observations - Observations to check
1003
+ * @returns {Object[]} Violations
1004
+ * @private
1005
+ */
1006
+ validateIntegrity(observations) {
1007
+ const violations = [];
1008
+ let malformed = 0;
1009
+
1010
+ for (const obs of observations) {
1011
+ if (!obs.id || !obs.agent || !obs.timestamp || !obs.kind) {
1012
+ malformed++;
1013
+ }
1014
+ if (!obs.metrics || typeof obs.metrics.confidence !== 'number') {
1015
+ malformed++;
1016
+ }
1017
+ }
1018
+
1019
+ if (malformed > 0) {
1020
+ violations.push({
1021
+ guard_id: 'integrity_check',
1022
+ severity: 'critical',
1023
+ details: {
1024
+ message: 'Malformed observations detected',
1025
+ count: malformed,
1026
+ total: observations.length
1027
+ }
1028
+ });
1029
+ }
1030
+
1031
+ return violations;
1032
+ }
1033
+
1034
+ /**
1035
+ * Validate agent coverage
1036
+ * @param {Array} observations - Observations to check
1037
+ * @returns {Object[]} Violations
1038
+ * @private
1039
+ */
1040
+ validateAgentCoverage(observations) {
1041
+ const violations = [];
1042
+ const expectedAgents = 10;
1043
+
1044
+ const uniqueAgents = new Set(
1045
+ observations
1046
+ .filter(o => !o.agent.startsWith('guard:'))
1047
+ .map(o => o.agent)
1048
+ );
1049
+
1050
+ const coverageRatio = uniqueAgents.size / expectedAgents;
1051
+ if (coverageRatio < 0.7) {
1052
+ violations.push({
1053
+ guard_id: 'agent_coverage',
1054
+ severity: 'warning',
1055
+ details: {
1056
+ message: 'Agent coverage below 70%',
1057
+ agents_active: uniqueAgents.size,
1058
+ agents_expected: expectedAgents,
1059
+ coverage: coverageRatio
1060
+ }
1061
+ });
1062
+ }
1063
+
1064
+ return violations;
1065
+ }
1066
+
1067
+ // =========================================================================
1068
+ // UTILITY METHODS
1069
+ // =========================================================================
1070
+
1071
+ /**
1072
+ * Create denial receipt
1073
+ * @private
1074
+ * @param {string} operation - Operation type
1075
+ * @param {string} target - Target of operation
1076
+ * @param {string} reasonCode - Reason code
1077
+ * @param {string} message - Human-readable message
1078
+ * @param {string} guardId - Guard ID
1079
+ * @param {string} severity - Severity level
1080
+ * @param {string} [pattern] - Matched pattern
1081
+ * @returns {Object} Denial decision with receipt
1082
+ */
1083
+ createDenial(operation, target, reasonCode, message, guardId, severity, pattern = undefined) {
1084
+ const receipt = {
1085
+ id: generateUUID(),
1086
+ timestamp: new Date().toISOString(),
1087
+ operation,
1088
+ target: String(target).substring(0, 100), // Truncate for safety
1089
+ decision: 'deny',
1090
+ reasonCode,
1091
+ guardId
1092
+ };
1093
+
1094
+ return {
1095
+ allowed: false,
1096
+ reason: message,
1097
+ reasonCode,
1098
+ guardId,
1099
+ severity,
1100
+ pattern,
1101
+ receipt
1102
+ };
1103
+ }
1104
+
1105
+ /**
1106
+ * Log audit entry
1107
+ * @private
1108
+ * @param {string} operation - Operation type
1109
+ * @param {string} target - Target of operation
1110
+ * @param {boolean} allowed - Whether allowed
1111
+ * @param {string} guardId - Guard ID
1112
+ * @param {string | null} reason - Reason if denied
1113
+ */
1114
+ logAudit(operation, target, allowed, guardId, reason) {
1115
+ const entry = {
1116
+ timestamp: new Date().toISOString(),
1117
+ guardId,
1118
+ operation,
1119
+ decision: allowed ? 'ALLOW' : 'DENY',
1120
+ target: String(target).substring(0, 100),
1121
+ reason: reason || null
1122
+ };
1123
+
1124
+ this.auditLog.push(entry);
1125
+
1126
+ // Keep audit log bounded (last 10000 entries)
1127
+ if (this.auditLog.length > 10000) {
1128
+ this.auditLog = this.auditLog.slice(-10000);
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * Get audit log entries
1134
+ * @param {Object} [filter] - Optional filter
1135
+ * @param {string} [filter.operation] - Filter by operation
1136
+ * @param {string} [filter.decision] - Filter by decision (ALLOW|DENY)
1137
+ * @param {number} [filter.limit] - Max entries to return
1138
+ * @returns {Object[]} Audit log entries
1139
+ */
1140
+ getAuditLog(filter = {}) {
1141
+ let entries = [...this.auditLog];
1142
+
1143
+ if (filter.operation) {
1144
+ entries = entries.filter(e => e.operation === filter.operation);
1145
+ }
1146
+ if (filter.decision) {
1147
+ entries = entries.filter(e => e.decision === filter.decision);
1148
+ }
1149
+ if (filter.limit) {
1150
+ entries = entries.slice(-filter.limit);
1151
+ }
1152
+
1153
+ return entries;
1154
+ }
1155
+
1156
+ /**
1157
+ * Get cache statistics
1158
+ * @returns {{size: number, maxSize: number}}
1159
+ */
1160
+ getCacheStats() {
1161
+ return this.cache.stats();
1162
+ }
1163
+
1164
+ /**
1165
+ * Clear all caches
1166
+ */
1167
+ clearCache() {
1168
+ this.cache.clear();
1169
+ }
1170
+
1171
+ /**
1172
+ * Get forbidden pattern count (for verification)
1173
+ * @returns {{env: number, file: number, network: number, command: number, total: number}}
1174
+ */
1175
+ getForbiddenPatternCount() {
1176
+ return {
1177
+ env: this.envForbiddenPatterns.length,
1178
+ file: this.fileForbiddenPatterns.length,
1179
+ network: NETWORK_FORBIDDEN_DOMAINS.length,
1180
+ command: FORBIDDEN_COMMANDS.length + DANGEROUS_SHELL_PATTERNS.length,
1181
+ total: this.envForbiddenPatterns.length +
1182
+ this.fileForbiddenPatterns.length +
1183
+ NETWORK_FORBIDDEN_DOMAINS.length +
1184
+ FORBIDDEN_COMMANDS.length +
1185
+ DANGEROUS_SHELL_PATTERNS.length
1186
+ };
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * Create GuardRegistry instance
1192
+ * @param {Object} [config] - Guard configuration
1193
+ * @returns {GuardRegistry} New guard registry
1194
+ * @example
1195
+ * const registry = createGuardRegistry({
1196
+ * network_allowlist: [
1197
+ * { hostname: 'api.example.com', paths: ['**'], methods: ['GET'] }
1198
+ * ]
1199
+ * });
1200
+ */
1201
+ export function createGuardRegistry(config) {
1202
+ return new GuardRegistry(config);
1203
+ }
1204
+
1205
+ // Export pattern constants for testing
1206
+ export const PATTERNS = {
1207
+ ENV_VAR_FORBIDDEN_PATTERNS,
1208
+ FILE_PATH_FORBIDDEN_PATTERNS,
1209
+ NETWORK_FORBIDDEN_DOMAINS,
1210
+ NETWORK_ALLOWED_HOSTS,
1211
+ FORBIDDEN_COMMANDS,
1212
+ DANGEROUS_SHELL_PATTERNS
1213
+ };