@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/README.md +414 -0
- package/package.json +81 -0
- package/src/agents/index.mjs +1402 -0
- package/src/artifact.mjs +405 -0
- package/src/cli.mjs +932 -0
- package/src/config.mjs +115 -0
- package/src/guards.mjs +1213 -0
- package/src/index.mjs +347 -0
- package/src/merge.mjs +196 -0
- package/src/observation.mjs +193 -0
- package/src/orchestrator.mjs +315 -0
- package/src/probe.mjs +58 -0
- package/src/probes/CONCURRENCY-PROBE.md +256 -0
- package/src/probes/README.md +275 -0
- package/src/probes/concurrency.mjs +1175 -0
- package/src/probes/filesystem.mjs +731 -0
- package/src/probes/filesystem.test.mjs +244 -0
- package/src/probes/network.mjs +503 -0
- package/src/probes/performance.mjs +816 -0
- package/src/probes/persistence.mjs +785 -0
- package/src/probes/runtime.mjs +589 -0
- package/src/probes/tooling.mjs +454 -0
- package/src/probes/tooling.test.mjs +372 -0
- package/src/probes/verify-execution.mjs +131 -0
- package/src/probes/verify-guards.mjs +73 -0
- package/src/probes/wasm.mjs +715 -0
- package/src/receipt.mjs +197 -0
- package/src/receipts/index.mjs +813 -0
- package/src/reporter.example.mjs +223 -0
- package/src/reporter.mjs +555 -0
- package/src/reporters/markdown.mjs +355 -0
- package/src/reporters/rdf.mjs +383 -0
- package/src/storage/index.mjs +827 -0
- package/src/types.mjs +1028 -0
- package/src/utils/errors.mjs +397 -0
- package/src/utils/index.mjs +32 -0
- package/src/utils/logger.mjs +236 -0
- package/src/vocabulary.ttl +169 -0
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
|
+
};
|