shellward 0.6.0 → 0.6.1
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/dist/core/engine.js +30 -3
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/core/engine.ts +23 -3
package/dist/core/engine.js
CHANGED
|
@@ -173,6 +173,7 @@ export class ShellWard {
|
|
|
173
173
|
}
|
|
174
174
|
// ========== L2: Data Scanner ==========
|
|
175
175
|
scanData(text, toolName) {
|
|
176
|
+
text = asString(text);
|
|
176
177
|
const [, findings] = redactSensitive(text, this.customSensitive);
|
|
177
178
|
const hasSensitiveData = findings.length > 0;
|
|
178
179
|
const summary = findings.map(f => `${f.name}(${f.count})`).join(', ');
|
|
@@ -195,7 +196,7 @@ export class ShellWard {
|
|
|
195
196
|
}
|
|
196
197
|
// ========== L3: Tool & Command Checker ==========
|
|
197
198
|
checkTool(toolName) {
|
|
198
|
-
const toolLower = toolName.toLowerCase();
|
|
199
|
+
const toolLower = asString(toolName).toLowerCase();
|
|
199
200
|
const enforce = this.config.mode === 'enforce';
|
|
200
201
|
// allowedTools always wins — user-trusted tools bypass policy.
|
|
201
202
|
if (this.allowedTools.has(toolLower))
|
|
@@ -226,7 +227,7 @@ export class ShellWard {
|
|
|
226
227
|
}
|
|
227
228
|
checkCommand(cmd, toolName) {
|
|
228
229
|
const enforce = this.config.mode === 'enforce';
|
|
229
|
-
const parts = splitCommands(cmd);
|
|
230
|
+
const parts = splitCommands(asString(cmd));
|
|
230
231
|
for (const part of parts) {
|
|
231
232
|
// Normalize shell-quote obfuscation (e.g. r''m / r""m → rm) before matching.
|
|
232
233
|
// Only empty quote pairs are stripped, so a real quoted arg like
|
|
@@ -253,6 +254,7 @@ export class ShellWard {
|
|
|
253
254
|
return { allowed: true };
|
|
254
255
|
}
|
|
255
256
|
checkPath(path, operation, toolName) {
|
|
257
|
+
path = asString(path);
|
|
256
258
|
const enforce = this.config.mode === 'enforce';
|
|
257
259
|
const normalizedPath = normalizePath(path);
|
|
258
260
|
for (const rule of PROTECTED_PATHS) {
|
|
@@ -276,6 +278,7 @@ export class ShellWard {
|
|
|
276
278
|
}
|
|
277
279
|
// ========== L4: Injection Detection ==========
|
|
278
280
|
checkInjection(text, options) {
|
|
281
|
+
text = asString(text);
|
|
279
282
|
const threshold = options?.threshold ?? this.config.injectionThreshold;
|
|
280
283
|
const enforce = this.config.mode === 'enforce';
|
|
281
284
|
const hiddenChars = detectHiddenChars(text);
|
|
@@ -327,6 +330,7 @@ export class ShellWard {
|
|
|
327
330
|
// tuned for tool-metadata attacks. Pure & side-effect-light: callable from
|
|
328
331
|
// the SDK, the MCP server, or at plugin tool-discovery time.
|
|
329
332
|
scanToolDefinition(tool, options) {
|
|
333
|
+
tool = (tool && typeof tool === 'object') ? tool : { name: 'unknown' };
|
|
330
334
|
const threshold = options?.threshold ?? 40;
|
|
331
335
|
const findings = [];
|
|
332
336
|
let score = 0;
|
|
@@ -395,6 +399,8 @@ export class ShellWard {
|
|
|
395
399
|
}
|
|
396
400
|
// ========== L5: Security Gate ==========
|
|
397
401
|
checkAction(action, details) {
|
|
402
|
+
action = asString(action);
|
|
403
|
+
details = asString(details);
|
|
398
404
|
if (action === 'exec' || action === 'shell') {
|
|
399
405
|
return this.checkCommand(details);
|
|
400
406
|
}
|
|
@@ -433,6 +439,7 @@ export class ShellWard {
|
|
|
433
439
|
}
|
|
434
440
|
// ========== L6: Response Checker ==========
|
|
435
441
|
checkResponse(content) {
|
|
442
|
+
content = asString(content);
|
|
436
443
|
const canaryLeak = this._canaryToken ? content.includes(this._canaryToken) : false;
|
|
437
444
|
if (canaryLeak) {
|
|
438
445
|
this.log.write({
|
|
@@ -509,7 +516,8 @@ export class ShellWard {
|
|
|
509
516
|
this.evictExpired();
|
|
510
517
|
}
|
|
511
518
|
checkOutbound(toolName, params) {
|
|
512
|
-
|
|
519
|
+
params = (params && typeof params === 'object') ? params : {};
|
|
520
|
+
const toolLower = asString(toolName).toLowerCase();
|
|
513
521
|
const isOutbound = this.outboundTools.has(toolLower);
|
|
514
522
|
const isDualUse = DUAL_USE_TOOLS.has(toolLower);
|
|
515
523
|
const enforce = this.config.mode === 'enforce';
|
|
@@ -614,6 +622,8 @@ export class ShellWard {
|
|
|
614
622
|
}
|
|
615
623
|
extractTextFields(args) {
|
|
616
624
|
const results = [];
|
|
625
|
+
if (!args || typeof args !== 'object')
|
|
626
|
+
return results;
|
|
617
627
|
for (const field of TEXT_FIELDS) {
|
|
618
628
|
if (typeof args[field] === 'string' && args[field].length > 0) {
|
|
619
629
|
results.push(args[field]);
|
|
@@ -700,6 +710,23 @@ function normalizePath(p) {
|
|
|
700
710
|
function truncate(s, max) {
|
|
701
711
|
return s.length > max ? s.slice(0, max) + '...' : s;
|
|
702
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Defensive coercion at public API boundaries: a security check must fail safe
|
|
715
|
+
* on hostile/garbage input, never throw. null/undefined → '', everything else
|
|
716
|
+
* is stringified.
|
|
717
|
+
*/
|
|
718
|
+
function asString(v) {
|
|
719
|
+
if (typeof v === 'string')
|
|
720
|
+
return v;
|
|
721
|
+
if (v == null)
|
|
722
|
+
return '';
|
|
723
|
+
try {
|
|
724
|
+
return String(v);
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return '';
|
|
728
|
+
}
|
|
729
|
+
}
|
|
703
730
|
/**
|
|
704
731
|
* Defeat shell-quote obfuscation for DETECTION (not execution): strip empty
|
|
705
732
|
* quote pairs so `r''m -rf /` and `r""m -rf /` normalize to `rm -rf /`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shellward",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"mcpName": "io.github.jnMetaCode/shellward",
|
|
5
5
|
"description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.",
|
|
6
6
|
"keywords": [
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/jnMetaCode/shellward",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.6.
|
|
9
|
+
"version": "0.6.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "shellward",
|
|
14
|
-
"version": "0.6.
|
|
14
|
+
"version": "0.6.1",
|
|
15
15
|
"runtime": "node",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
package/src/core/engine.ts
CHANGED
|
@@ -256,6 +256,7 @@ export class ShellWard {
|
|
|
256
256
|
// ========== L2: Data Scanner ==========
|
|
257
257
|
|
|
258
258
|
scanData(text: string, toolName?: string): ScanResult {
|
|
259
|
+
text = asString(text)
|
|
259
260
|
const [, findings] = redactSensitive(text, this.customSensitive)
|
|
260
261
|
const hasSensitiveData = findings.length > 0
|
|
261
262
|
const summary = findings.map(f => `${f.name}(${f.count})`).join(', ')
|
|
@@ -282,7 +283,7 @@ export class ShellWard {
|
|
|
282
283
|
// ========== L3: Tool & Command Checker ==========
|
|
283
284
|
|
|
284
285
|
checkTool(toolName: string): CheckResult {
|
|
285
|
-
const toolLower = toolName.toLowerCase()
|
|
286
|
+
const toolLower = asString(toolName).toLowerCase()
|
|
286
287
|
const enforce = this.config.mode === 'enforce'
|
|
287
288
|
|
|
288
289
|
// allowedTools always wins — user-trusted tools bypass policy.
|
|
@@ -317,7 +318,7 @@ export class ShellWard {
|
|
|
317
318
|
|
|
318
319
|
checkCommand(cmd: string, toolName?: string): CheckResult {
|
|
319
320
|
const enforce = this.config.mode === 'enforce'
|
|
320
|
-
const parts = splitCommands(cmd)
|
|
321
|
+
const parts = splitCommands(asString(cmd))
|
|
321
322
|
|
|
322
323
|
for (const part of parts) {
|
|
323
324
|
// Normalize shell-quote obfuscation (e.g. r''m / r""m → rm) before matching.
|
|
@@ -346,6 +347,7 @@ export class ShellWard {
|
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
checkPath(path: string, operation: 'write' | 'delete', toolName?: string): CheckResult {
|
|
350
|
+
path = asString(path)
|
|
349
351
|
const enforce = this.config.mode === 'enforce'
|
|
350
352
|
const normalizedPath = normalizePath(path)
|
|
351
353
|
|
|
@@ -372,6 +374,7 @@ export class ShellWard {
|
|
|
372
374
|
// ========== L4: Injection Detection ==========
|
|
373
375
|
|
|
374
376
|
checkInjection(text: string, options?: { source?: string; threshold?: number }): InjectionResult {
|
|
377
|
+
text = asString(text)
|
|
375
378
|
const threshold = options?.threshold ?? this.config.injectionThreshold
|
|
376
379
|
const enforce = this.config.mode === 'enforce'
|
|
377
380
|
|
|
@@ -430,6 +433,7 @@ export class ShellWard {
|
|
|
430
433
|
// the SDK, the MCP server, or at plugin tool-discovery time.
|
|
431
434
|
|
|
432
435
|
scanToolDefinition(tool: McpToolDefinition, options?: { threshold?: number }): ToolPoisoningResult {
|
|
436
|
+
tool = (tool && typeof tool === 'object') ? tool : { name: 'unknown' }
|
|
433
437
|
const threshold = options?.threshold ?? 40
|
|
434
438
|
const findings: ToolPoisoningFinding[] = []
|
|
435
439
|
let score = 0
|
|
@@ -507,6 +511,8 @@ export class ShellWard {
|
|
|
507
511
|
// ========== L5: Security Gate ==========
|
|
508
512
|
|
|
509
513
|
checkAction(action: string, details: string): CheckResult {
|
|
514
|
+
action = asString(action)
|
|
515
|
+
details = asString(details)
|
|
510
516
|
if (action === 'exec' || action === 'shell') {
|
|
511
517
|
return this.checkCommand(details)
|
|
512
518
|
}
|
|
@@ -550,6 +556,7 @@ export class ShellWard {
|
|
|
550
556
|
// ========== L6: Response Checker ==========
|
|
551
557
|
|
|
552
558
|
checkResponse(content: string): ResponseCheckResult {
|
|
559
|
+
content = asString(content)
|
|
553
560
|
const canaryLeak = this._canaryToken ? content.includes(this._canaryToken) : false
|
|
554
561
|
|
|
555
562
|
if (canaryLeak) {
|
|
@@ -638,7 +645,8 @@ export class ShellWard {
|
|
|
638
645
|
}
|
|
639
646
|
|
|
640
647
|
checkOutbound(toolName: string, params: Record<string, any>): CheckResult {
|
|
641
|
-
|
|
648
|
+
params = (params && typeof params === 'object') ? params : {}
|
|
649
|
+
const toolLower = asString(toolName).toLowerCase()
|
|
642
650
|
const isOutbound = this.outboundTools.has(toolLower)
|
|
643
651
|
const isDualUse = DUAL_USE_TOOLS.has(toolLower)
|
|
644
652
|
const enforce = this.config.mode === 'enforce'
|
|
@@ -757,6 +765,7 @@ export class ShellWard {
|
|
|
757
765
|
|
|
758
766
|
extractTextFields(args: Record<string, any>): string[] {
|
|
759
767
|
const results: string[] = []
|
|
768
|
+
if (!args || typeof args !== 'object') return results
|
|
760
769
|
for (const field of TEXT_FIELDS) {
|
|
761
770
|
if (typeof args[field] === 'string' && args[field].length > 0) {
|
|
762
771
|
results.push(args[field])
|
|
@@ -841,6 +850,17 @@ function truncate(s: string, max: number): string {
|
|
|
841
850
|
return s.length > max ? s.slice(0, max) + '...' : s
|
|
842
851
|
}
|
|
843
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Defensive coercion at public API boundaries: a security check must fail safe
|
|
855
|
+
* on hostile/garbage input, never throw. null/undefined → '', everything else
|
|
856
|
+
* is stringified.
|
|
857
|
+
*/
|
|
858
|
+
function asString(v: unknown): string {
|
|
859
|
+
if (typeof v === 'string') return v
|
|
860
|
+
if (v == null) return ''
|
|
861
|
+
try { return String(v) } catch { return '' }
|
|
862
|
+
}
|
|
863
|
+
|
|
844
864
|
/**
|
|
845
865
|
* Defeat shell-quote obfuscation for DETECTION (not execution): strip empty
|
|
846
866
|
* quote pairs so `r''m -rf /` and `r""m -rf /` normalize to `rm -rf /`.
|