ship-safe 7.0.0 → 9.0.0
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 +80 -21
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agentic-security-agent.js +35 -0
- package/cli/agents/cicd-scanner.js +22 -0
- package/cli/agents/config-auditor.js +235 -0
- package/cli/agents/deep-analyzer.js +473 -133
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/index.js +63 -22
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/orchestrator.js +13 -3
- package/cli/agents/supply-chain-agent.js +1 -1
- package/cli/bin/ship-safe.js +129 -5
- package/cli/commands/audit.js +149 -3
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/init.js +104 -0
- package/cli/commands/mcp.js +270 -0
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/scan-mcp.js +78 -0
- package/cli/commands/scan-skill.js +248 -5
- package/cli/commands/watch.js +142 -5
- package/cli/index.js +5 -0
- package/cli/providers/llm-provider.js +50 -2
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/patterns.js +1 -0
- package/cli/utils/plugin-loader.js +276 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/security-memory.js +296 -0
- package/package.json +2 -2
package/cli/commands/mcp.js
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* scan_secrets - Scan a directory for leaked secrets
|
|
25
25
|
* get_checklist - Return the launch-day security checklist
|
|
26
26
|
* analyze_file - Analyze a single file for security issues
|
|
27
|
+
* scan_repo - Run a full multi-agent security scan on a repo
|
|
28
|
+
* get_findings - Read findings from a saved ship-safe report file
|
|
29
|
+
* suppress_finding - Add a ship-safe-ignore comment to suppress a finding
|
|
27
30
|
*
|
|
28
31
|
* PROTOCOL:
|
|
29
32
|
* JSON-RPC 2.0 over stdio (MCP spec: https://modelcontextprotocol.io)
|
|
@@ -34,6 +37,10 @@ import path from 'path';
|
|
|
34
37
|
import fg from 'fast-glob';
|
|
35
38
|
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, TEST_FILE_PATTERNS, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
36
39
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
40
|
+
import { buildOrchestrator } from '../agents/index.js';
|
|
41
|
+
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
42
|
+
import { autoDetectProvider } from '../providers/llm-provider.js';
|
|
43
|
+
import { DeepAnalyzer } from '../agents/deep-analyzer.js';
|
|
37
44
|
|
|
38
45
|
// =============================================================================
|
|
39
46
|
// MCP TOOL DEFINITIONS
|
|
@@ -80,6 +87,74 @@ const TOOLS = [
|
|
|
80
87
|
required: ['path'],
|
|
81
88
|
},
|
|
82
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: 'scan_repo',
|
|
92
|
+
description: 'Run a full multi-agent security scan on a repository or directory. Runs all 20+ ship-safe security agents (injection, auth bypass, secrets, supply chain, LLM security, etc.) and returns a structured findings report with severity ratings and remediation advice. Use this when the user asks to audit, scan, or check the security of their project.',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
path: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'The directory path to scan. Use "." for current directory.',
|
|
99
|
+
},
|
|
100
|
+
agents: {
|
|
101
|
+
type: 'array',
|
|
102
|
+
items: { type: 'string' },
|
|
103
|
+
description: 'Specific agent names to run (optional). Omit to run all agents.',
|
|
104
|
+
},
|
|
105
|
+
llm: {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
description: 'Enable LLM-powered deep analysis for critical/high findings (default: false). Requires ANTHROPIC_API_KEY or similar env var.',
|
|
108
|
+
},
|
|
109
|
+
outputFile: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'Optional path to save the JSON report for later retrieval with get_findings.',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['path'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'get_findings',
|
|
119
|
+
description: 'Read and return findings from a ship-safe JSON report file previously saved by scan_repo or the ship-safe CLI (npx ship-safe audit --json). Useful for reviewing or referencing a prior scan without re-running it.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
reportPath: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Path to the ship-safe JSON report file (e.g. ship-safe-report.json).',
|
|
126
|
+
},
|
|
127
|
+
severity: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
enum: ['critical', 'high', 'medium', 'low'],
|
|
130
|
+
description: 'Filter findings by minimum severity (optional).',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ['reportPath'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'suppress_finding',
|
|
138
|
+
description: 'Add a ship-safe-ignore comment to a specific line in a file to suppress a false-positive security finding. The comment tells ship-safe\'s scanner to skip that line in future scans. Always explain why the suppression is safe.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
file: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Path to the file containing the false-positive finding.',
|
|
145
|
+
},
|
|
146
|
+
line: {
|
|
147
|
+
type: 'number',
|
|
148
|
+
description: 'Line number of the finding to suppress (1-indexed).',
|
|
149
|
+
},
|
|
150
|
+
reason: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Brief explanation of why this finding is a false positive (appended to the ignore comment).',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
required: ['file', 'line', 'reason'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
83
158
|
];
|
|
84
159
|
|
|
85
160
|
// =============================================================================
|
|
@@ -163,6 +238,192 @@ async function analyzeFile({ path: filePath }) {
|
|
|
163
238
|
};
|
|
164
239
|
}
|
|
165
240
|
|
|
241
|
+
async function scanRepo({ path: targetPath, agents: agentFilter, llm = false, outputFile }) {
|
|
242
|
+
const rootPath = path.resolve(targetPath);
|
|
243
|
+
|
|
244
|
+
if (!fs.existsSync(rootPath)) {
|
|
245
|
+
return { error: `Path does not exist: ${rootPath}` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MCP communicates over stdout as JSON-RPC. Suppress all console output during
|
|
249
|
+
// the scan so spinner text and log lines don't pollute the transport stream.
|
|
250
|
+
const noop = () => {};
|
|
251
|
+
const savedLog = console.log;
|
|
252
|
+
const savedWarn = console.warn;
|
|
253
|
+
const savedError = console.error;
|
|
254
|
+
const savedInfo = console.info;
|
|
255
|
+
console.log = console.warn = console.error = console.info = noop;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const orchestrator = buildOrchestrator();
|
|
259
|
+
const context = { rootPath };
|
|
260
|
+
|
|
261
|
+
// Run all agents (quiet:true suppresses ora spinners; console is already nulled)
|
|
262
|
+
const { findings, recon } = await orchestrator.runAll(rootPath, {
|
|
263
|
+
agents: agentFilter,
|
|
264
|
+
timeout: 30000,
|
|
265
|
+
concurrency: 6,
|
|
266
|
+
quiet: true,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Optional: LLM deep analysis
|
|
270
|
+
let deepStats = null;
|
|
271
|
+
if (llm) {
|
|
272
|
+
const provider = autoDetectProvider(rootPath, {});
|
|
273
|
+
if (provider) {
|
|
274
|
+
const analyzer = new DeepAnalyzer({ provider, budgetCents: 50, verbose: false });
|
|
275
|
+
await analyzer.analyze(findings, { rootPath, recon });
|
|
276
|
+
deepStats = analyzer.getStats();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Score
|
|
281
|
+
const scorer = new ScoringEngine();
|
|
282
|
+
const { score, grade } = scorer.score(findings);
|
|
283
|
+
|
|
284
|
+
const SEV_ORDER = ['critical', 'high', 'medium', 'low'];
|
|
285
|
+
const bySeverity = {};
|
|
286
|
+
for (const sev of SEV_ORDER) {
|
|
287
|
+
bySeverity[sev] = findings.filter(f => f.severity === sev).length;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const report = {
|
|
291
|
+
scannedAt: new Date().toISOString(),
|
|
292
|
+
rootPath,
|
|
293
|
+
score,
|
|
294
|
+
grade,
|
|
295
|
+
totalFindings: findings.length,
|
|
296
|
+
bySeverity,
|
|
297
|
+
findings: findings.map(f => ({
|
|
298
|
+
title: f.title,
|
|
299
|
+
severity: f.severity,
|
|
300
|
+
category: f.category,
|
|
301
|
+
rule: f.rule,
|
|
302
|
+
file: f.file ? path.relative(rootPath, f.file) : null,
|
|
303
|
+
line: f.line,
|
|
304
|
+
description: f.description,
|
|
305
|
+
remediation: f.remediation,
|
|
306
|
+
confidence: f.confidence,
|
|
307
|
+
...(f.deepAnalysis ? { deepAnalysis: f.deepAnalysis } : {}),
|
|
308
|
+
})),
|
|
309
|
+
...(deepStats ? { deepAnalysis: deepStats } : {}),
|
|
310
|
+
summary: `Score: ${score}/100 (${grade}) — ${findings.length} finding(s): ${bySeverity.critical} critical, ${bySeverity.high} high, ${bySeverity.medium} medium, ${bySeverity.low} low.`,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (outputFile) {
|
|
314
|
+
const outPath = path.resolve(outputFile);
|
|
315
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
316
|
+
report.savedTo = outPath;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return report;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { error: `Scan failed: ${err.message}` };
|
|
322
|
+
} finally {
|
|
323
|
+
// Always restore console so other tool calls are not affected
|
|
324
|
+
console.log = savedLog;
|
|
325
|
+
console.warn = savedWarn;
|
|
326
|
+
console.error = savedError;
|
|
327
|
+
console.info = savedInfo;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getFindings({ reportPath, severity }) {
|
|
332
|
+
const absPath = path.resolve(reportPath);
|
|
333
|
+
|
|
334
|
+
if (!fs.existsSync(absPath)) {
|
|
335
|
+
return { error: `Report file not found: ${absPath}` };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let report;
|
|
339
|
+
try {
|
|
340
|
+
report = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return { error: `Failed to parse report: ${err.message}` };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const findings = report.findings ?? [];
|
|
346
|
+
const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
347
|
+
const filtered = severity
|
|
348
|
+
? findings.filter(f => (SEV_RANK[f.severity] ?? 0) >= (SEV_RANK[severity] ?? 0))
|
|
349
|
+
: findings;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
reportPath: absPath,
|
|
353
|
+
scannedAt: report.scannedAt,
|
|
354
|
+
score: report.score,
|
|
355
|
+
grade: report.grade,
|
|
356
|
+
totalFindings: filtered.length,
|
|
357
|
+
bySeverity: report.bySeverity,
|
|
358
|
+
findings: filtered,
|
|
359
|
+
summary: report.summary,
|
|
360
|
+
...(severity ? { filter: `severity >= ${severity}` } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function suppressFinding({ file, line, reason }) {
|
|
365
|
+
const absPath = path.resolve(file);
|
|
366
|
+
|
|
367
|
+
if (!fs.existsSync(absPath)) {
|
|
368
|
+
return { error: `File not found: ${absPath}` };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let content;
|
|
372
|
+
try {
|
|
373
|
+
content = fs.readFileSync(absPath, 'utf-8');
|
|
374
|
+
} catch (err) {
|
|
375
|
+
return { error: `Cannot read file: ${err.message}` };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const lines = content.split('\n');
|
|
379
|
+
const lineIdx = line - 1; // Convert to 0-indexed
|
|
380
|
+
|
|
381
|
+
if (lineIdx < 0 || lineIdx >= lines.length) {
|
|
382
|
+
return { error: `Line ${line} is out of range (file has ${lines.length} lines)` };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const targetLine = lines[lineIdx];
|
|
386
|
+
|
|
387
|
+
// Already suppressed?
|
|
388
|
+
if (/ship-safe-ignore/i.test(targetLine)) {
|
|
389
|
+
return { alreadySuppressed: true, file: absPath, line, message: 'Line already has a ship-safe-ignore comment.' };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Detect indentation and comment style
|
|
393
|
+
const indent = targetLine.match(/^(\s*)/)?.[1] ?? '';
|
|
394
|
+
const isJs = /\.(js|ts|jsx|tsx|mjs|cjs|java|c|cpp|cs|go|rs|swift|kt)$/.test(file);
|
|
395
|
+
const isPy = /\.py$/.test(file);
|
|
396
|
+
const isRb = /\.rb$/.test(file);
|
|
397
|
+
const isHtml = /\.(html?|vue|svelte)$/.test(file);
|
|
398
|
+
|
|
399
|
+
let ignoreComment;
|
|
400
|
+
if (isHtml) {
|
|
401
|
+
ignoreComment = `${indent}<!-- ship-safe-ignore — ${reason} -->`;
|
|
402
|
+
} else if (isPy || isRb) {
|
|
403
|
+
ignoreComment = `${indent}# ship-safe-ignore — ${reason}`;
|
|
404
|
+
} else {
|
|
405
|
+
ignoreComment = `${indent}// ship-safe-ignore — ${reason}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Insert ignore comment on the line BEFORE the finding
|
|
409
|
+
lines.splice(lineIdx, 0, ignoreComment);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
fs.writeFileSync(absPath, lines.join('\n'), 'utf-8');
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return { error: `Cannot write file: ${err.message}` };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
suppressed: true,
|
|
419
|
+
file: absPath,
|
|
420
|
+
originalLine: line,
|
|
421
|
+
insertedLine: line, // The ignore comment is now on this line, original moved to line+1
|
|
422
|
+
comment: ignoreComment,
|
|
423
|
+
message: `Added ship-safe-ignore comment before line ${line} in ${path.basename(file)}.`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
166
427
|
// =============================================================================
|
|
167
428
|
// SCAN UTILITIES (shared with scan command)
|
|
168
429
|
// =============================================================================
|
|
@@ -283,6 +544,15 @@ async function handleRequest(request) {
|
|
|
283
544
|
case 'analyze_file':
|
|
284
545
|
result = await analyzeFile(args);
|
|
285
546
|
break;
|
|
547
|
+
case 'scan_repo':
|
|
548
|
+
result = await scanRepo(args);
|
|
549
|
+
break;
|
|
550
|
+
case 'get_findings':
|
|
551
|
+
result = getFindings(args);
|
|
552
|
+
break;
|
|
553
|
+
case 'suppress_finding':
|
|
554
|
+
result = suppressFinding(args);
|
|
555
|
+
break;
|
|
286
556
|
default:
|
|
287
557
|
return respondError(-32601, `Unknown tool: ${name}`);
|
|
288
558
|
}
|
package/cli/commands/red-team.js
CHANGED
|
@@ -17,7 +17,7 @@ import fs from 'fs';
|
|
|
17
17
|
import path from 'path';
|
|
18
18
|
import chalk from 'chalk';
|
|
19
19
|
import ora from 'ora';
|
|
20
|
-
import {
|
|
20
|
+
import { buildOrchestratorAsync } from '../agents/index.js';
|
|
21
21
|
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
22
22
|
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
23
23
|
import { HTMLReporter } from '../agents/html-reporter.js';
|
|
@@ -39,7 +39,7 @@ export async function redTeamCommand(targetPath = '.', options = {}) {
|
|
|
39
39
|
console.log();
|
|
40
40
|
|
|
41
41
|
// ── 1. Run orchestrator ─────────────────────────────────────────────────────
|
|
42
|
-
const orchestrator =
|
|
42
|
+
const orchestrator = await buildOrchestratorAsync(absolutePath, { quiet: true });
|
|
43
43
|
|
|
44
44
|
const agentFilter = options.agents
|
|
45
45
|
? options.agents.split(',').map(a => a.trim())
|
package/cli/commands/scan-mcp.js
CHANGED
|
@@ -109,6 +109,72 @@ const MCP_TOOL_PATTERNS = [
|
|
|
109
109
|
severity: 'medium',
|
|
110
110
|
target: 'any',
|
|
111
111
|
},
|
|
112
|
+
|
|
113
|
+
// ── Hermes Agent: Function-Call Poisoning (ASI-03, ASI-05) ───────────────
|
|
114
|
+
{
|
|
115
|
+
name: 'Hermes: XML tool_call injection in description',
|
|
116
|
+
regex: /<tool_call>[\s\S]{0,300}<\/tool_call>/gi,
|
|
117
|
+
severity: 'critical',
|
|
118
|
+
target: 'description',
|
|
119
|
+
owasp: 'ASI-03',
|
|
120
|
+
note: 'Description embeds a Hermes-format <tool_call> block — will be parsed and executed by agents consuming this manifest.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'Hermes: Function-call format injection',
|
|
124
|
+
regex: /<function_calls>[\s\S]{0,300}<\/function_calls>/gi,
|
|
125
|
+
severity: 'critical',
|
|
126
|
+
target: 'description',
|
|
127
|
+
owasp: 'ASI-03',
|
|
128
|
+
note: 'Description embeds a <function_calls> block matching Hermes/Claude XML call format.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Hermes: tool_choice manipulation',
|
|
132
|
+
regex: /tool_choice\s*[=:]\s*["']?(?:auto|any|none|required)["']?\s*(?:,|\}|$)/gi,
|
|
133
|
+
severity: 'high',
|
|
134
|
+
target: 'description',
|
|
135
|
+
owasp: 'ASI-03',
|
|
136
|
+
note: 'Description attempts to override tool_choice routing, steering agent to call attacker-controlled tools.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Hermes: Forced tool invocation via description',
|
|
140
|
+
regex: /(?:you\s+must\s+(?:call|invoke|use)\s+(?:the\s+)?tool|always\s+(?:call|invoke|run)\s+(?:the\s+)?(?:tool|function)|tool\s+MUST\s+be\s+(?:called|invoked|used))/gi,
|
|
141
|
+
severity: 'high',
|
|
142
|
+
target: 'description',
|
|
143
|
+
owasp: 'ASI-03',
|
|
144
|
+
note: 'Instruction in tool description coerces the LLM agent into calling a specific tool, bypassing agent autonomy.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Hermes: Schema bypass via additionalProperties',
|
|
148
|
+
regex: /"additionalProperties"\s*:\s*true/gi,
|
|
149
|
+
severity: 'high',
|
|
150
|
+
target: 'schema',
|
|
151
|
+
owasp: 'ASI-03',
|
|
152
|
+
note: 'Tool input schema allows arbitrary extra properties — attackers can inject undeclared parameters that bypass input validation.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'Hermes: Late binding via env-var registry URL',
|
|
156
|
+
regex: /(?:HERMES_REGISTRY_URL|AGENT_REGISTRY|TOOL_REGISTRY_URL|REGISTRY_ENDPOINT)\s*[=:]/gi,
|
|
157
|
+
severity: 'critical',
|
|
158
|
+
target: 'any',
|
|
159
|
+
owasp: 'ASI-05',
|
|
160
|
+
note: 'Tool definition references a runtime-resolved registry URL — attacker who controls the env var can swap the entire tool registry at execution time.',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'Hermes: Namespace collision / tool shadowing',
|
|
164
|
+
regex: /(?:override\s+(?:existing\s+)?tool|shadow\s+tool|replace\s+(?:the\s+)?(?:existing\s+)?tool|re-register\s+tool)/gi,
|
|
165
|
+
severity: 'critical',
|
|
166
|
+
target: 'description',
|
|
167
|
+
owasp: 'ASI-05',
|
|
168
|
+
note: 'Description explicitly documents shadowing a previously registered tool — classic namespace collision attack.',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'Hermes: Recursive sub-agent invocation in description',
|
|
172
|
+
regex: /(?:spawn\s+(?:a\s+)?(?:new\s+)?(?:sub[-\s]?agent|child[-\s]?agent|nested[-\s]?agent)|create\s+(?:a\s+)?(?:sub[-\s]?agent|child[-\s]?agent)|recursively\s+call\s+(?:agent|tool))/gi,
|
|
173
|
+
severity: 'high',
|
|
174
|
+
target: 'description',
|
|
175
|
+
owasp: 'ASI-02',
|
|
176
|
+
note: 'Description instructs the agent to spawn sub-agents — could lead to unbounded recursion or privilege escalation through child agents.',
|
|
177
|
+
},
|
|
112
178
|
];
|
|
113
179
|
|
|
114
180
|
// Dangerous tool name keywords — flag tools whose names suggest shell/exec access
|
|
@@ -348,6 +414,18 @@ function analyzeToolDefinition(tool) {
|
|
|
348
414
|
}
|
|
349
415
|
}
|
|
350
416
|
|
|
417
|
+
// Check for additionalProperties: true at the top-level schema (schema bypass)
|
|
418
|
+
const topSchema = tool.inputSchema || tool.input_schema || {};
|
|
419
|
+
if (topSchema.additionalProperties === true) {
|
|
420
|
+
findings.push({
|
|
421
|
+
check: 'schema-analysis',
|
|
422
|
+
name: 'Hermes: Schema bypass — additionalProperties: true',
|
|
423
|
+
severity: 'high',
|
|
424
|
+
tool: name,
|
|
425
|
+
matched: 'Top-level inputSchema has additionalProperties: true — arbitrary params accepted',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
351
429
|
// Check for excessive required parameters (information harvesting)
|
|
352
430
|
const required = tool.inputSchema?.required || tool.input_schema?.required || [];
|
|
353
431
|
const properties = tool.inputSchema?.properties || tool.input_schema?.properties || {};
|