@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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for Tooling Surface Probe
|
|
3
|
+
* @module @unrdf/kgc-probe/probes/tooling.test
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import {
|
|
9
|
+
probeTooling,
|
|
10
|
+
isCommandAllowed,
|
|
11
|
+
argsAreSafe,
|
|
12
|
+
safeExec,
|
|
13
|
+
ObservationSchema,
|
|
14
|
+
ProbeConfigSchema,
|
|
15
|
+
} from './tooling.mjs';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// GUARD TESTS (CRITICAL)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
describe('Command Allowlist Guard (Poka-Yoke)', () => {
|
|
22
|
+
it('should allow git command', () => {
|
|
23
|
+
assert.equal(isCommandAllowed('git'), true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should allow node command', () => {
|
|
27
|
+
assert.equal(isCommandAllowed('node'), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should allow npm command', () => {
|
|
31
|
+
assert.equal(isCommandAllowed('npm'), true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should allow pnpm command', () => {
|
|
35
|
+
assert.equal(isCommandAllowed('pnpm'), true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should allow which command', () => {
|
|
39
|
+
assert.equal(isCommandAllowed('which'), true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should deny arbitrary commands (rm)', () => {
|
|
43
|
+
assert.equal(isCommandAllowed('rm'), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should deny arbitrary commands (curl)', () => {
|
|
47
|
+
assert.equal(isCommandAllowed('curl'), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should deny arbitrary commands (bash)', () => {
|
|
51
|
+
assert.equal(isCommandAllowed('bash'), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should deny shell metacharacter injection', () => {
|
|
55
|
+
assert.equal(isCommandAllowed('git; rm -rf /'), false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Argument Safety Guard', () => {
|
|
60
|
+
it('should allow safe arguments', () => {
|
|
61
|
+
assert.equal(argsAreSafe(['--version']), true);
|
|
62
|
+
assert.equal(argsAreSafe(['--help']), true);
|
|
63
|
+
assert.equal(argsAreSafe(['-v']), true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should deny arguments with semicolons', () => {
|
|
67
|
+
assert.equal(argsAreSafe(['--foo; rm -rf /']), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should deny arguments with pipe characters', () => {
|
|
71
|
+
assert.equal(argsAreSafe(['--foo | cat /etc/passwd']), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should deny arguments with backticks', () => {
|
|
75
|
+
assert.equal(argsAreSafe(['--foo `whoami`']), false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should deny arguments with dollar signs', () => {
|
|
79
|
+
assert.equal(argsAreSafe(['--foo $(whoami)']), false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should deny arguments with redirects', () => {
|
|
83
|
+
assert.equal(argsAreSafe(['--foo > /tmp/evil']), false);
|
|
84
|
+
assert.equal(argsAreSafe(['--foo < /etc/passwd']), false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// SAFE EXECUTION TESTS
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
describe('safeExec', () => {
|
|
93
|
+
it('should deny non-allowlisted commands', async () => {
|
|
94
|
+
const result = await safeExec('rm', ['-rf', '/'], 5000);
|
|
95
|
+
assert.equal(result.success, false);
|
|
96
|
+
assert.equal(result.guardDecision, 'denied');
|
|
97
|
+
assert.match(result.stderr, /not in allowlist/i);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should deny commands with unsafe arguments', async () => {
|
|
101
|
+
const result = await safeExec('git', ['--help; rm -rf /'], 5000);
|
|
102
|
+
assert.equal(result.success, false);
|
|
103
|
+
assert.equal(result.guardDecision, 'denied');
|
|
104
|
+
assert.match(result.stderr, /shell metacharacters/i);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should execute git --version successfully', async () => {
|
|
108
|
+
const result = await safeExec('git', ['--version'], 5000);
|
|
109
|
+
assert.equal(result.guardDecision, 'allowed');
|
|
110
|
+
// May succeed or fail depending on environment, but should not be denied
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should execute node --version successfully', async () => {
|
|
114
|
+
const result = await safeExec('node', ['--version'], 5000);
|
|
115
|
+
assert.equal(result.guardDecision, 'allowed');
|
|
116
|
+
// May succeed or fail depending on environment, but should not be denied
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should timeout commands exceeding timeout', async () => {
|
|
120
|
+
// This test is environment-dependent, skip if git unavailable
|
|
121
|
+
try {
|
|
122
|
+
const result = await safeExec('git', ['--version'], 1); // 1ms timeout
|
|
123
|
+
// If it completes in <1ms, that's fine
|
|
124
|
+
assert.ok(['allowed', 'unknown'].includes(result.guardDecision));
|
|
125
|
+
} catch {
|
|
126
|
+
// Timeout handling varies by environment
|
|
127
|
+
assert.ok(true);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// SCHEMA VALIDATION TESTS
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
describe('ObservationSchema', () => {
|
|
137
|
+
it('should validate valid observation', () => {
|
|
138
|
+
const obs = {
|
|
139
|
+
method: 'tooling.git_version',
|
|
140
|
+
inputs: { command: 'git', args: ['--version'] },
|
|
141
|
+
outputs: { version: '2.34.1', available: true },
|
|
142
|
+
guardDecision: 'allowed',
|
|
143
|
+
metadata: { timestamp: Date.now() },
|
|
144
|
+
};
|
|
145
|
+
const result = ObservationSchema.safeParse(obs);
|
|
146
|
+
assert.equal(result.success, true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should require method field', () => {
|
|
150
|
+
const obs = {
|
|
151
|
+
inputs: {},
|
|
152
|
+
outputs: {},
|
|
153
|
+
};
|
|
154
|
+
const result = ObservationSchema.safeParse(obs);
|
|
155
|
+
assert.equal(result.success, false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should require inputs field', () => {
|
|
159
|
+
const obs = {
|
|
160
|
+
method: 'test',
|
|
161
|
+
outputs: {},
|
|
162
|
+
};
|
|
163
|
+
const result = ObservationSchema.safeParse(obs);
|
|
164
|
+
assert.equal(result.success, false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should require outputs field', () => {
|
|
168
|
+
const obs = {
|
|
169
|
+
method: 'test',
|
|
170
|
+
inputs: {},
|
|
171
|
+
};
|
|
172
|
+
const result = ObservationSchema.safeParse(obs);
|
|
173
|
+
assert.equal(result.success, false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should allow optional guardDecision', () => {
|
|
177
|
+
const obs = {
|
|
178
|
+
method: 'test',
|
|
179
|
+
inputs: {},
|
|
180
|
+
outputs: {},
|
|
181
|
+
guardDecision: 'allowed',
|
|
182
|
+
};
|
|
183
|
+
const result = ObservationSchema.safeParse(obs);
|
|
184
|
+
assert.equal(result.success, true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should validate guardDecision enum', () => {
|
|
188
|
+
const obs = {
|
|
189
|
+
method: 'test',
|
|
190
|
+
inputs: {},
|
|
191
|
+
outputs: {},
|
|
192
|
+
guardDecision: 'invalid',
|
|
193
|
+
};
|
|
194
|
+
const result = ObservationSchema.safeParse(obs);
|
|
195
|
+
assert.equal(result.success, false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('ProbeConfigSchema', () => {
|
|
200
|
+
it('should use default timeout of 5000ms', () => {
|
|
201
|
+
const result = ProbeConfigSchema.parse({});
|
|
202
|
+
assert.equal(result.timeout, 5000);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should allow custom timeout', () => {
|
|
206
|
+
const result = ProbeConfigSchema.parse({ timeout: 3000 });
|
|
207
|
+
assert.equal(result.timeout, 3000);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should reject timeout > 10000ms', () => {
|
|
211
|
+
assert.throws(() => {
|
|
212
|
+
ProbeConfigSchema.parse({ timeout: 15000 });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should reject negative timeout', () => {
|
|
217
|
+
assert.throws(() => {
|
|
218
|
+
ProbeConfigSchema.parse({ timeout: -1000 });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should use default strict: false', () => {
|
|
223
|
+
const result = ProbeConfigSchema.parse({});
|
|
224
|
+
assert.equal(result.strict, false);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// INTEGRATION TESTS
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
describe('probeTooling', () => {
|
|
233
|
+
it('should return array of observations', async () => {
|
|
234
|
+
const observations = await probeTooling();
|
|
235
|
+
assert.ok(Array.isArray(observations));
|
|
236
|
+
assert.ok(observations.length > 0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should validate all observations', async () => {
|
|
240
|
+
const observations = await probeTooling();
|
|
241
|
+
observations.forEach(obs => {
|
|
242
|
+
const result = ObservationSchema.safeParse(obs);
|
|
243
|
+
assert.equal(result.success, true, `Invalid observation: ${JSON.stringify(obs)}`);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should include git probe', async () => {
|
|
248
|
+
const observations = await probeTooling();
|
|
249
|
+
const gitObs = observations.find(obs => obs.method === 'tooling.git_version');
|
|
250
|
+
assert.ok(gitObs, 'Missing git observation');
|
|
251
|
+
assert.ok('available' in gitObs.outputs);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should include node probe', async () => {
|
|
255
|
+
const observations = await probeTooling();
|
|
256
|
+
const nodeObs = observations.find(obs => obs.method === 'tooling.node_version');
|
|
257
|
+
assert.ok(nodeObs, 'Missing node observation');
|
|
258
|
+
assert.ok('available' in nodeObs.outputs);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should include npm probe', async () => {
|
|
262
|
+
const observations = await probeTooling();
|
|
263
|
+
const npmObs = observations.find(obs => obs.method === 'tooling.npm_version');
|
|
264
|
+
assert.ok(npmObs, 'Missing npm observation');
|
|
265
|
+
assert.ok('available' in npmObs.outputs);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should include pnpm probe', async () => {
|
|
269
|
+
const observations = await probeTooling();
|
|
270
|
+
const pnpmObs = observations.find(obs => obs.method === 'tooling.pnpm_version');
|
|
271
|
+
assert.ok(pnpmObs, 'Missing pnpm observation');
|
|
272
|
+
assert.ok('available' in pnpmObs.outputs);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should include shell probes', async () => {
|
|
276
|
+
const observations = await probeTooling();
|
|
277
|
+
const shObs = observations.find(obs => obs.method === 'tooling.shell_sh');
|
|
278
|
+
const bashObs = observations.find(obs => obs.method === 'tooling.shell_bash');
|
|
279
|
+
assert.ok(shObs, 'Missing sh observation');
|
|
280
|
+
assert.ok(bashObs, 'Missing bash observation');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should deny build tools not in allowlist', async () => {
|
|
284
|
+
const observations = await probeTooling();
|
|
285
|
+
const makeObs = observations.find(obs => obs.method === 'tooling.build_make');
|
|
286
|
+
const cmakeObs = observations.find(obs => obs.method === 'tooling.build_cmake');
|
|
287
|
+
assert.ok(makeObs, 'Missing make observation');
|
|
288
|
+
assert.ok(cmakeObs, 'Missing cmake observation');
|
|
289
|
+
assert.equal(makeObs.guardDecision, 'denied');
|
|
290
|
+
assert.equal(cmakeObs.guardDecision, 'denied');
|
|
291
|
+
assert.equal(makeObs.outputs.available, false);
|
|
292
|
+
assert.equal(cmakeObs.outputs.available, false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should include package manager detection', async () => {
|
|
296
|
+
const observations = await probeTooling();
|
|
297
|
+
const pkgMgrObs = observations.find(obs => obs.method === 'tooling.package_manager');
|
|
298
|
+
assert.ok(pkgMgrObs, 'Missing package manager observation');
|
|
299
|
+
assert.ok('primary' in pkgMgrObs.outputs);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should respect custom timeout', async () => {
|
|
303
|
+
const observations = await probeTooling({ timeout: 3000 });
|
|
304
|
+
assert.ok(observations.length > 0);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should handle execution errors gracefully', async () => {
|
|
308
|
+
// This test verifies the try-catch fallback
|
|
309
|
+
// In normal execution, this won't trigger, but ensures the path exists
|
|
310
|
+
const observations = await probeTooling();
|
|
311
|
+
// Should not throw, should return valid observations
|
|
312
|
+
assert.ok(observations.length > 0);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// GUARD DECISION TESTS (CRITICAL)
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
describe('Guard Decisions', () => {
|
|
321
|
+
it('should mark allowlisted successful commands as "allowed"', async () => {
|
|
322
|
+
const observations = await probeTooling();
|
|
323
|
+
const nodeObs = observations.find(obs => obs.method === 'tooling.node_version');
|
|
324
|
+
// Node should be available in test environment
|
|
325
|
+
if (nodeObs.outputs.available) {
|
|
326
|
+
assert.equal(nodeObs.guardDecision, 'allowed');
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should mark non-allowlisted commands as "denied"', async () => {
|
|
331
|
+
const observations = await probeTooling();
|
|
332
|
+
const makeObs = observations.find(obs => obs.method === 'tooling.build_make');
|
|
333
|
+
assert.equal(makeObs.guardDecision, 'denied');
|
|
334
|
+
assert.equal(makeObs.metadata.reason, 'not_in_allowlist');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should never execute denied commands', async () => {
|
|
338
|
+
const observations = await probeTooling();
|
|
339
|
+
const deniedObs = observations.filter(obs => obs.guardDecision === 'denied');
|
|
340
|
+
deniedObs.forEach(obs => {
|
|
341
|
+
// Denied commands should not have version info
|
|
342
|
+
assert.ok(!obs.outputs.version, `Denied command ${obs.method} has version`);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// SECURITY TESTS (CRITICAL)
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
describe('Security - NO arbitrary command execution', () => {
|
|
352
|
+
it('should prevent command injection via method name', async () => {
|
|
353
|
+
// This test ensures the allowlist guard is enforced
|
|
354
|
+
const result = await safeExec('rm -rf /', [], 5000);
|
|
355
|
+
assert.equal(result.guardDecision, 'denied');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should prevent command injection via arguments', async () => {
|
|
359
|
+
const result = await safeExec('git', ['--version; whoami'], 5000);
|
|
360
|
+
assert.equal(result.guardDecision, 'denied');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should prevent shell expansion', async () => {
|
|
364
|
+
const result = await safeExec('git', ['--version $(whoami)'], 5000);
|
|
365
|
+
assert.equal(result.guardDecision, 'denied');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should prevent pipe injection', async () => {
|
|
369
|
+
const result = await safeExec('git', ['--version | cat /etc/passwd'], 5000);
|
|
370
|
+
assert.equal(result.guardDecision, 'denied');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Standalone execution verification (minimal dependencies)
|
|
4
|
+
* Verifies safe command execution without requiring Zod
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// GUARD IMPLEMENTATION
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const ALLOWED_COMMANDS = new Set(['git', 'node', 'npm', 'pnpm', 'which']);
|
|
17
|
+
|
|
18
|
+
function isCommandAllowed(command) {
|
|
19
|
+
return ALLOWED_COMMANDS.has(command);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function argsAreSafe(args) {
|
|
23
|
+
const dangerous = /[;&|`$<>(){}[\]\\'"]/;
|
|
24
|
+
return args.every(arg => !dangerous.test(arg));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function safeExec(command, args, timeout) {
|
|
28
|
+
if (!isCommandAllowed(command)) {
|
|
29
|
+
return {
|
|
30
|
+
stdout: '',
|
|
31
|
+
stderr: `Command '${command}' not in allowlist`,
|
|
32
|
+
success: false,
|
|
33
|
+
guardDecision: 'denied',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!argsAreSafe(args)) {
|
|
38
|
+
return {
|
|
39
|
+
stdout: '',
|
|
40
|
+
stderr: 'Arguments contain shell metacharacters',
|
|
41
|
+
success: false,
|
|
42
|
+
guardDecision: 'denied',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
48
|
+
timeout,
|
|
49
|
+
maxBuffer: 1024 * 1024,
|
|
50
|
+
shell: false,
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
stdout: stdout.trim(),
|
|
56
|
+
stderr: stderr.trim(),
|
|
57
|
+
success: true,
|
|
58
|
+
guardDecision: 'allowed',
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
stdout: error.stdout?.trim() || '',
|
|
63
|
+
stderr: error.stderr?.trim() || error.message,
|
|
64
|
+
success: false,
|
|
65
|
+
guardDecision: error.code === 'ETIMEDOUT' ? 'unknown' : 'allowed',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// VERIFICATION TESTS
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
console.log('ā” Execution Verification\n');
|
|
75
|
+
|
|
76
|
+
let passed = 0;
|
|
77
|
+
let failed = 0;
|
|
78
|
+
|
|
79
|
+
function test(name, condition) {
|
|
80
|
+
if (condition) {
|
|
81
|
+
console.log(`ā
${name}`);
|
|
82
|
+
passed++;
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`ā ${name}`);
|
|
85
|
+
failed++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Test 1: Deny non-allowlisted command
|
|
90
|
+
console.log('š Guard Enforcement:');
|
|
91
|
+
const result1 = await safeExec('rm', ['-rf', '/'], 5000);
|
|
92
|
+
test('Deny rm command', result1.guardDecision === 'denied' && !result1.success);
|
|
93
|
+
|
|
94
|
+
// Test 2: Deny unsafe arguments
|
|
95
|
+
const result2 = await safeExec('git', ['--version; whoami'], 5000);
|
|
96
|
+
test('Deny unsafe arguments', result2.guardDecision === 'denied' && !result2.success);
|
|
97
|
+
|
|
98
|
+
// Test 3: Execute safe command (git)
|
|
99
|
+
console.log('\nāļø Safe Execution:');
|
|
100
|
+
const result3 = await safeExec('git', ['--version'], 5000);
|
|
101
|
+
test('Execute git --version', result3.guardDecision === 'allowed');
|
|
102
|
+
if (result3.success) {
|
|
103
|
+
console.log(` ā ${result3.stdout}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Test 4: Execute safe command (node)
|
|
107
|
+
const result4 = await safeExec('node', ['--version'], 5000);
|
|
108
|
+
test('Execute node --version', result4.guardDecision === 'allowed');
|
|
109
|
+
if (result4.success) {
|
|
110
|
+
console.log(` ā ${result4.stdout}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Test 5: Verify shell=false (no expansion)
|
|
114
|
+
console.log('\nš”ļø Shell Isolation:');
|
|
115
|
+
const result5 = await safeExec('git', ['--version'], 5000);
|
|
116
|
+
test('No shell execution', result5.guardDecision === 'allowed'); // Would fail if shell metacharacters worked
|
|
117
|
+
|
|
118
|
+
console.log('\nš Results:');
|
|
119
|
+
console.log(` Passed: ${passed}`);
|
|
120
|
+
console.log(` Failed: ${failed}`);
|
|
121
|
+
console.log(` Total: ${passed + failed}`);
|
|
122
|
+
|
|
123
|
+
if (failed === 0) {
|
|
124
|
+
console.log('\nā
All execution tests passed!');
|
|
125
|
+
console.log('ā
Safe command execution verified!');
|
|
126
|
+
console.log('ā
Guard constraints enforced!');
|
|
127
|
+
process.exit(0);
|
|
128
|
+
} else {
|
|
129
|
+
console.log('\nā Some execution tests failed!');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Standalone guard verification (NO dependencies)
|
|
4
|
+
* Verifies allowlist and argument safety without requiring Zod
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// GUARD IMPLEMENTATION (copied from tooling.mjs for standalone verification)
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
const ALLOWED_COMMANDS = new Set(['git', 'node', 'npm', 'pnpm', 'which']);
|
|
12
|
+
|
|
13
|
+
function isCommandAllowed(command) {
|
|
14
|
+
return ALLOWED_COMMANDS.has(command);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function argsAreSafe(args) {
|
|
18
|
+
const dangerous = /[;&|`$<>(){}[\]\\'"]/;
|
|
19
|
+
return args.every(arg => !dangerous.test(arg));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// VERIFICATION TESTS
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
console.log('š Guard Verification (Poka-Yoke)\n');
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
|
|
31
|
+
function test(name, condition) {
|
|
32
|
+
if (condition) {
|
|
33
|
+
console.log(`ā
${name}`);
|
|
34
|
+
passed++;
|
|
35
|
+
} else {
|
|
36
|
+
console.log(`ā ${name}`);
|
|
37
|
+
failed++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Allowlist tests
|
|
42
|
+
console.log('š Command Allowlist:');
|
|
43
|
+
test('Allow git', isCommandAllowed('git'));
|
|
44
|
+
test('Allow node', isCommandAllowed('node'));
|
|
45
|
+
test('Allow npm', isCommandAllowed('npm'));
|
|
46
|
+
test('Allow pnpm', isCommandAllowed('pnpm'));
|
|
47
|
+
test('Allow which', isCommandAllowed('which'));
|
|
48
|
+
test('Deny rm', !isCommandAllowed('rm'));
|
|
49
|
+
test('Deny curl', !isCommandAllowed('curl'));
|
|
50
|
+
test('Deny bash', !isCommandAllowed('bash'));
|
|
51
|
+
test('Deny arbitrary', !isCommandAllowed('evil-command'));
|
|
52
|
+
|
|
53
|
+
console.log('\nš”ļø Argument Safety:');
|
|
54
|
+
test('Safe: --version', argsAreSafe(['--version']));
|
|
55
|
+
test('Safe: --help', argsAreSafe(['--help']));
|
|
56
|
+
test('Unsafe: semicolon', !argsAreSafe(['--foo; rm -rf /']));
|
|
57
|
+
test('Unsafe: pipe', !argsAreSafe(['--foo | cat']));
|
|
58
|
+
test('Unsafe: backtick', !argsAreSafe(['--foo `whoami`']));
|
|
59
|
+
test('Unsafe: dollar', !argsAreSafe(['--foo $(whoami)']));
|
|
60
|
+
test('Unsafe: redirect', !argsAreSafe(['--foo > /tmp/evil']));
|
|
61
|
+
|
|
62
|
+
console.log('\nš Results:');
|
|
63
|
+
console.log(` Passed: ${passed}`);
|
|
64
|
+
console.log(` Failed: ${failed}`);
|
|
65
|
+
console.log(` Total: ${passed + failed}`);
|
|
66
|
+
|
|
67
|
+
if (failed === 0) {
|
|
68
|
+
console.log('\nā
All guard constraints verified!');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
} else {
|
|
71
|
+
console.log('\nā Some guard constraints failed!');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|