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.
@@ -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
- const toolLower = toolName.toLowerCase();
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.0",
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.0",
9
+ "version": "0.6.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "shellward",
14
- "version": "0.6.0",
14
+ "version": "0.6.1",
15
15
  "runtime": "node",
16
16
  "transport": {
17
17
  "type": "stdio"
@@ -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
- const toolLower = toolName.toLowerCase()
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 /`.