
Medusa Security
- 49 installs
- 36 repo stars
- Updated July 14, 2026
- oimiragieo/agent-studio
Helps with security tasks.
About
medusa-security is a Claude Code skill for security. It helps solo builders move faster with AI-assisted development.
- medusa-security
- Security
- AI-coding skill
Medusa Security by the numbers
- 49 all-time installs (skills.sh)
- Ranked #1,332 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oimiragieo/agent-studio --skill medusa-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 49 |
|---|---|
| repo stars | ★ 36 |
| Last updated | July 14, 2026 |
| Repository | oimiragieo/agent-studio ↗ |
What it does
Helps with security tasks.
Files
Medusa Security Skill
Identity
AI-first security scanner integration skill. Leverages Medusa's 76 scanners and 3,000+ detection patterns for comprehensive security analysis including AI/ML-specific vulnerability detection.
Capabilities
1. Full Scan — All 76 scanners, comprehensive security analysis 2. AI-Only Scan — Prompt injection, MCP security, agent security, RAG security 3. Quick Scan — Git-changed files only for rapid development feedback 4. Targeted Scan — Specific scanner categories (mcp, secrets, prompt-injection, etc.) 5. SARIF Output Parsing — Standard SARIF v2.1.0 structured findings 6. JSON Output Parsing — Medusa-native JSON format 7. OWASP Mapping — Maps findings to OWASP Agentic AI (ASI01-10) and OWASP Top 10 (A01-10) 8. Remediation Guidance — Links findings to agent-studio skills and agents 9. CI/CD Integration — Fail-on thresholds, SARIF upload for GitHub Code Scanning
Prerequisites
Python 3.10+
pip install medusa-securityCheck installation: python -m medusa --version
Workflow: Full Security Scan
# Step 1: Verify installation
python -m medusa --version
# Step 2: Run scan
medusa scan . --format sarif --fail-on high
# Step 3: Parse output (use scripts/main.cjs)
node .claude/skills/medusa-security/scripts/main.cjs --mode full --target .
# Step 4: Review findings by severity
# CRITICAL → immediate fix required
# HIGH → fix before release
# MEDIUM → fix in next sprint
# LOW → track and addressWorkflow: AI-Only Scan
medusa scan . --format sarif --ai-onlyScans only: prompt injection (800+ patterns), MCP security (400+ patterns), agent security (500+ patterns), RAG security (300+ patterns).
Workflow: Quick Scan (Development)
medusa scan . --format sarif --quickOnly scans git-changed files. Use during development for rapid feedback.
Workflow: Targeted Scan
# MCP security only
medusa scan . --format sarif --scanners mcp-server,mcp-config
# Secrets only
medusa scan . --format sarif --scanners secrets,gitleaks,env
# AI context files only
medusa scan . --format sarif --scanners ai-contextOutput Processing
The skill uses helper scripts located at .claude/skills/medusa-security/scripts/:
| Script | Purpose |
|---|---|
sarif-parser.cjs | Parses SARIF v2.1.0 output |
json-parser.cjs | Parses Medusa JSON output |
finding-formatter.cjs | Formats findings with OWASP mapping |
main.cjs | Orchestrates the full pipeline |
cli-wrapper.cjs | Wraps Medusa CLI invocation |
security-review.cjs | Deterministic report writer (no Glob recursion) |
Using the Pipeline
# Full scan with structured output
node .claude/skills/medusa-security/scripts/main.cjs --mode full --target .
# AI-only scan
node .claude/skills/medusa-security/scripts/main.cjs --mode ai-only --target .
# Quick scan (git-changed files)
node .claude/skills/medusa-security/scripts/main.cjs --mode quick --target .Deterministic Security Review (Recommended in Claude sessions)
Use this when you need the final security review report and want to avoid recursive Glob timeouts:
node .claude/skills/medusa-security/scripts/security-review.cjsThis writes:
/.claude/context/reports/security/security-review-medusa-scan-2026-02-17.md
and performs fixed-path checks on:
.claude/hooks/.claude/lib/.claude/skills/medusa-security/scripts/.claude/CLAUDE.md
Important Runtime Guardrail
- Avoid recursive glob patterns like
.claude/skills/medusa-security/**/*in long sessions. - Prefer direct file reads and deterministic script entry points.
OWASP Mapping
Findings are automatically mapped to:
- OWASP Agentic AI Top 10 (ASI01-10): Goal Hijacking, Tool Misuse, Context Poisoning, etc.
- OWASP Top 10 (A01-10): Broken Access Control, Injection, Cryptographic Failures, etc.
Severity Triage
| Severity | Action | Timeline |
|---|---|---|
| CRITICAL | Immediate fix | Before any merge |
| HIGH | Fix before release | Same sprint |
| MEDIUM | Fix in next sprint | Next cycle |
| LOW | Track and address | Backlog |
Agent Integration
| Agent | Usage |
|---|---|
security-architect | Primary consumer. Use for comprehensive security reviews. |
penetration-tester | Use for targeted vulnerability scanning with authorization. |
code-reviewer | Use AI-only scan as part of code review workflow. |
CI/CD Integration
# GitHub Actions example
- name: Security Scan
run: |
pip install medusa-security
medusa scan . --format sarif --fail-on high -o reports/
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: reports/medusa-results.sarifIron Laws
1. ALWAYS verify Medusa installation before scanning — python -m medusa --version first; a missing install produces no output instead of an error, silently masking all vulnerabilities. 2. NEVER rely on AI-only mode as the release gate — AI-only mode misses traditional SAST patterns (SQLi, XSS, path traversal); full scan covering all 76 scanners is required for release-gate decisions. 3. ALWAYS set --fail-on high in CI/CD pipelines — without a fail threshold, pipelines pass even when CRITICAL findings exist, creating false confidence in the security posture. 4. NEVER skip SARIF upload to GitHub Code Scanning — local-only SARIF is lost after the build; uploading via github/codeql-action/upload-sarif@v3 persists findings for PR review, trend tracking, and compliance audit trails. 5. ALWAYS fix CRITICAL and HIGH findings before merging — deploying with unresolved high-severity findings expands the attack surface and nullifies the security posture gain from scanning.
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Skipping installation check | Missing Medusa produces no output, not an error — all vulnerabilities silently missed | Run python -m medusa --version first; abort on non-zero exit |
| Using AI-only mode as a release gate | AI-only misses traditional SAST patterns (SQLi, XSS, path traversal) — 76 scanners needed for full coverage | Use full-scan mode for CI/CD gates; AI-only mode for rapid dev-time feedback only |
| No fail-on threshold in CI | Pipeline passes even when CRITICAL findings exist — false confidence in security posture | Always use --fail-on high in CI pipelines; adjust to --fail-on critical for high-risk repos |
| Ignoring MEDIUM findings | MEDIUM findings compound into exploitable chains when combined with HIGH findings | Triage MEDIUM findings each sprint; never allow them to accumulate without a tracking issue |
| Not uploading SARIF to Code Scanning | Findings live only in local files, lost after build — no PR-level review or trend tracking | Upload SARIF via github/codeql-action/upload-sarif@v3 in every CI run |
Memory Protocol
After scanning:
- Record new vulnerability patterns in
patterns.json - Log significant findings in
issues.md - Track scan history for trend analysis
- Use
recordGotcha()for recurring false positives
const manager = require('.claude/lib/memory/memory-manager.cjs');
manager.recordGotcha({
text: 'False positive: medusa flags X pattern in Y context',
area: 'security-scanning',
});
manager.recordPattern({
text: 'Prompt injection found in CLAUDE.md context files',
area: 'ai-security',
});Related Skills
security-architect— Threat modeling and OWASP analysisstatic-analysis— CodeQL and Semgrep SARIF analysissemgrep-rule-creator— Create custom Semgrep rulesinsecure-defaults— Detect hardcoded credentialsvariant-analysis— Discover vulnerability variants
Invoke the medusa-security skill and follow it exactly as presented to you
'use strict';
/**
* Post-execute hook for medusa-security
* Auto-generated by enterprise-bundle-scaffolder
*
* Records metrics after skill execution.
*/
function postExecute(_context) {
// Record execution metrics
return { ok: true, skill: 'medusa-security' };
}
module.exports = { postExecute };
'use strict';
/**
* Pre-execute hook for medusa-security
* Auto-generated by enterprise-bundle-scaffolder
*
* Validates inputs before skill execution.
*/
function preExecute(context) {
// Validate skill invocation context
if (!context || typeof context !== 'object') {
return { allow: true, message: 'medusa-security: no context to validate' };
}
return { allow: true };
}
module.exports = { preExecute };
medusa-security Research Requirements
Generated: 2026-02-28
Skill Description
-
Research Areas
- Current best practices for medusa-security
- Industry standards and tooling
- Integration patterns
Source References
- To be populated by skill-updater research phase
medusa-security Rules
Purpose
-
Best Practices
- Run full scan before release for comprehensive coverage
- Use ai-only mode for rapid AI/LLM-focused checks
- Use quick mode during development for changed-files-only scanning
- Always review CRITICAL and HIGH findings before deployment
- Use --fail-on high in CI/CD pipelines
Integration Points
See SKILL.md for complete documentation.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "medusa-securityInput",
"description": "Input schema for >-",
"type": "object",
"additionalProperties": true,
"properties": {
"target": {
"type": "string",
"description": "Target file or path for the skill to operate on"
},
"options": {
"type": "object",
"description": "Additional options for skill execution",
"additionalProperties": true
}
}
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "medusa-securityOutput",
"type": "object",
"additionalProperties": true,
"properties": {
"ok": {
"type": "boolean"
},
"summary": {
"type": "string"
}
}
}
'use strict';
const { spawnSync } = require('child_process');
const path = require('path');
const { safeParseJSON } = require(
path.join(__dirname, '..', '..', '..', 'lib', 'utils', 'safe-json.cjs')
);
const { parseSarif } = require(path.join(__dirname, 'sarif-parser.cjs'));
const { parseMedusaJson } = require(path.join(__dirname, 'json-parser.cjs'));
/**
* Injectable spawnSync for testing. Tests replace this via _spawnSync.
* @type {Function}
*/
let _spawnSync = spawnSync;
/**
* Build command-line arguments for medusa scan.
* @param {object} options - Scan options
* @param {string} options.target - Target path to scan
* @param {string} [options.format='sarif'] - Output format (sarif or json)
* @param {boolean} [options.aiOnly=false] - Scan AI-specific patterns only
* @param {boolean} [options.quick=false] - Quick scan mode
* @param {string[]} [options.scanners] - Specific scanners to run
* @param {string} [options.failOn] - Severity threshold for non-zero exit
* @param {string[]} [options.exclude] - Paths to exclude
* @returns {string[]} Array of CLI arguments
*/
function buildScanArgs(options) {
const args = ['scan', options.target];
const format = options.format || 'sarif';
args.push('--format', format);
if (options.aiOnly) {
args.push('--ai-only');
}
if (options.quick) {
args.push('--quick');
}
if (options.scanners && options.scanners.length > 0) {
args.push('--scanners', options.scanners.join(','));
}
if (options.failOn) {
args.push('--fail-on', options.failOn);
}
if (options.exclude && options.exclude.length > 0) {
for (const excl of options.exclude) {
args.push('-e', excl);
}
}
return args;
}
/**
* Check if medusa-security is installed.
* @returns {{ installed: boolean, version: string|null, error?: string }}
*/
function checkInstallation() {
try {
const result = _spawnSync('python', ['-m', 'medusa', '--version'], {
shell: false,
timeout: 10000,
encoding: 'utf-8',
});
if (result.status === 0) {
const stdout = result.stdout instanceof Buffer ? result.stdout.toString() : result.stdout;
const versionMatch = stdout.match(/(\d+\.\d+\.\d+)/);
return {
installed: true,
version: versionMatch ? versionMatch[1] : null,
};
}
const stderr = result.stderr instanceof Buffer ? result.stderr.toString() : result.stderr;
return {
installed: false,
version: null,
error: stderr || 'medusa-security not found',
};
} catch (err) {
return {
installed: false,
version: null,
error: err.message,
};
}
}
/**
* Run a medusa scan on the target path.
* @param {string} target - Path to scan
* @param {object} [options={}] - Scan options
* @returns {{ exitCode: number, findings: Array, raw: string }}
*/
function runMedusaScan(target, options) {
const opts = { target, ...options };
const scanArgs = buildScanArgs(opts);
const format = opts.format || 'sarif';
const result = _spawnSync('python', ['-m', 'medusa', ...scanArgs], {
shell: false,
timeout: 300000,
maxBuffer: 50 * 1024 * 1024,
});
const stdout = result.stdout instanceof Buffer ? result.stdout.toString() : result.stdout || '';
const exitCode = result.status || 0;
let findings = [];
if (stdout.trim()) {
if (format === 'sarif') {
const sarifData = safeParseJSON(stdout);
const parsed = parseSarif(sarifData);
findings = Array.isArray(parsed) ? parsed : parsed.findings || [];
} else {
const jsonData = safeParseJSON(stdout);
findings = parseMedusaJson(jsonData);
}
}
return {
exitCode,
findings,
raw: stdout,
};
}
module.exports = {
buildScanArgs,
checkInstallation,
runMedusaScan,
// Expose for test injection
get _spawnSync() {
return _spawnSync;
},
set _spawnSync(fn) {
_spawnSync = fn;
},
};
'use strict';
// Severity weights for security score calculation
const SEVERITY_WEIGHTS = {
CRITICAL: 25,
HIGH: 15,
MEDIUM: 5,
LOW: 1,
};
// Severity badge icons for formatted output
const SEVERITY_BADGES = {
CRITICAL: '[CRITICAL]',
HIGH: '[HIGH]',
MEDIUM: '[MEDIUM]',
LOW: '[LOW]',
};
/**
* OWASP Agentic AI Top 10 mapping by Medusa category.
*/
const OWASP_AGENTIC_MAP = {
prompt_injection: { id: 'ASI01', name: 'Agent Goal Hijacking' },
mcp_security: { id: 'ASI02', name: 'Tool Misuse' },
ai_security: { id: 'ASI01', name: 'Agent Goal Hijacking' },
memory_poisoning: { id: 'ASI06', name: 'Memory & Context Poisoning' },
tool_abuse: { id: 'ASI02', name: 'Tool Misuse' },
data_exfiltration: { id: 'ASI04', name: 'Sensitive Information Disclosure' },
privilege_escalation: { id: 'ASI05', name: 'Privilege Escalation' },
};
/**
* OWASP Top 10 (2021) mapping by Medusa category.
*/
const OWASP_TOP10_MAP = {
secrets: { id: 'A02', name: 'Cryptographic Failures' },
injection: { id: 'A03', name: 'Injection' },
authentication: { id: 'A07', name: 'Identification and Authentication Failures' },
xss: { id: 'A03', name: 'Injection' },
cryptography: { id: 'A02', name: 'Cryptographic Failures' },
access_control: { id: 'A01', name: 'Broken Access Control' },
misconfiguration: { id: 'A05', name: 'Security Misconfiguration' },
ssrf: { id: 'A10', name: 'Server-Side Request Forgery' },
};
/**
* Agent-studio remediation mapping by category.
*/
const REMEDIATION_MAP = {
prompt_injection: {
skill: 'security-architect',
agent: 'security-architect',
description: 'Use security-architect skill for prompt injection review and input sanitization',
},
mcp_security: {
skill: 'security-architect',
agent: 'security-architect',
description: 'Use security-architect to audit MCP tool descriptions for hidden instructions',
},
ai_security: {
skill: 'security-architect',
agent: 'security-architect',
description: 'Use security-architect for AI security patterns and OWASP Agentic AI review',
},
secrets: {
skill: 'auth-security-expert',
agent: 'security-architect',
description: 'Remove hardcoded secrets; use environment variables or secret managers',
},
injection: {
skill: 'security-architect',
agent: 'developer',
description: 'Use parameterized queries; validate and sanitize all inputs',
},
authentication: {
skill: 'auth-security-expert',
agent: 'security-architect',
description: 'Review authentication flow; implement MFA and secure session management',
},
xss: {
skill: 'security-architect',
agent: 'developer',
description: 'Sanitize output; use CSP headers; encode user-generated content',
},
general: {
skill: 'security-architect',
agent: 'code-reviewer',
description: 'General security review recommended',
},
};
/**
* Format a single finding into a human-readable string.
* @param {object} finding - Standardized finding object
* @returns {string} Formatted finding string
*/
function formatFinding(finding) {
const badge = SEVERITY_BADGES[finding.severity] || '[UNKNOWN]';
const location = `${finding.file}:${finding.line}:${finding.column}`;
return `${badge} ${finding.ruleId} at ${location}\n ${finding.message}`;
}
/**
* Map a finding to OWASP Agentic AI Top 10 category.
* @param {object} finding - Standardized finding object
* @returns {{ id: string, name: string }} OWASP Agentic mapping
*/
function mapToOwaspAgentic(finding) {
const category = finding.category || 'general';
return OWASP_AGENTIC_MAP[category] || { id: 'ASI01', name: 'Agent Goal Hijacking' };
}
/**
* Map a finding to OWASP Top 10 (2021) category.
* @param {object} finding - Standardized finding object
* @returns {{ id: string, name: string }} OWASP Top 10 mapping
*/
function mapToOwaspTop10(finding) {
const category = finding.category || 'general';
return OWASP_TOP10_MAP[category] || { id: 'A04', name: 'Insecure Design' };
}
/**
* Generate a markdown report from findings.
* @param {Array} findings - Array of standardized findings
* @returns {string} Markdown report
*/
function generateMarkdownReport(findings) {
const summary = generateSummary(findings);
const lines = [];
lines.push('# Medusa Security Scan Report');
lines.push('');
lines.push('## Summary');
lines.push('');
lines.push(`| Metric | Value |`);
lines.push(`| --- | --- |`);
lines.push(`| Total Findings | ${summary.total} |`);
lines.push(`| Critical | ${summary.critical} |`);
lines.push(`| High | ${summary.high} |`);
lines.push(`| Medium | ${summary.medium} |`);
lines.push(`| Low | ${summary.low} |`);
lines.push(`| Security Score | ${summary.securityScore}/100 |`);
lines.push('');
if (findings.length === 0) {
lines.push('No findings detected.');
return lines.join('\n');
}
lines.push('## Findings');
lines.push('');
lines.push('| Severity | Rule | File | Line | Message |');
lines.push('| --- | --- | --- | --- | --- |');
for (const finding of findings) {
const sev = finding.severity || 'MEDIUM';
const rule = finding.ruleId || 'N/A';
const file = finding.file || 'N/A';
const line = finding.line || 0;
const msg = (finding.message || '').replace(/\|/g, '\\|');
lines.push(`| ${sev} | ${rule} | ${file} | ${line} | ${msg} |`);
}
lines.push('');
return lines.join('\n');
}
/**
* Generate a summary object from findings.
* @param {Array} findings - Array of standardized findings
* @returns {{ total: number, critical: number, high: number, medium: number, low: number, securityScore: number }}
*/
function generateSummary(findings) {
const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
for (const finding of findings) {
const sev = finding.severity || 'MEDIUM';
if (counts[sev] !== undefined) {
counts[sev]++;
} else {
counts.MEDIUM++;
}
}
const total = findings.length;
const maxPenalty = 100;
let penalty = 0;
penalty += counts.CRITICAL * SEVERITY_WEIGHTS.CRITICAL;
penalty += counts.HIGH * SEVERITY_WEIGHTS.HIGH;
penalty += counts.MEDIUM * SEVERITY_WEIGHTS.MEDIUM;
penalty += counts.LOW * SEVERITY_WEIGHTS.LOW;
const securityScore = Math.max(0, Math.min(100, maxPenalty - penalty));
return {
total,
critical: counts.CRITICAL,
high: counts.HIGH,
medium: counts.MEDIUM,
low: counts.LOW,
securityScore,
};
}
/**
* Map a finding to agent-studio remediation references.
* @param {object} finding - Standardized finding object
* @returns {{ skill?: string, agent?: string, description: string }}
*/
function mapToRemediation(finding) {
const category = finding.category || 'general';
return REMEDIATION_MAP[category] || REMEDIATION_MAP.general;
}
module.exports = {
formatFinding,
mapToOwaspAgentic,
mapToOwaspTop10,
generateMarkdownReport,
generateSummary,
mapToRemediation,
SEVERITY_WEIGHTS,
SEVERITY_BADGES,
OWASP_AGENTIC_MAP,
OWASP_TOP10_MAP,
REMEDIATION_MAP,
};
'use strict';
const path = require('path');
const { safeParseJSON } = require(
path.join(__dirname, '..', '..', '..', 'lib', 'utils', 'safe-json.cjs')
);
/**
* Parse Medusa JSON output into standardized findings array.
* @param {object|string} jsonData - Medusa JSON object or JSON string
* @returns {Array} Array of standardized findings
*/
function parseMedusaJson(jsonData) {
let data = jsonData;
if (typeof data === 'string') {
data = safeParseJSON(data);
}
if (!data || !Array.isArray(data.results)) {
return [];
}
return data.results.map(result => ({
severity: result.severity || 'MEDIUM',
scanner: result.scanner || '',
ruleId: result.rule_id || '',
message: result.message || '',
file: result.file || '',
line: result.line || 0,
column: result.column || 0,
cweId: result.cwe_id || null,
category: result.category || 'general',
}));
}
/**
* Group findings by severity level.
* @param {Array} findings - Array of findings
* @returns {{ CRITICAL: Array, HIGH: Array, MEDIUM: Array, LOW: Array }}
*/
function groupBySeverity(findings) {
const grouped = {
CRITICAL: [],
HIGH: [],
MEDIUM: [],
LOW: [],
};
for (const finding of findings) {
const severity = finding.severity || 'MEDIUM';
if (grouped[severity]) {
grouped[severity].push(finding);
} else {
grouped.MEDIUM.push(finding);
}
}
return grouped;
}
/**
* Filter findings by category.
* @param {Array} findings - Array of findings
* @param {string} category - Category to filter by
* @returns {Array} Filtered findings
*/
function filterByCategory(findings, category) {
return findings.filter(f => f.category === category);
}
module.exports = {
parseMedusaJson,
groupBySeverity,
filterByCategory,
};
'use strict';
const path = require('path');
const { runMedusaScan } = require(path.join(__dirname, 'cli-wrapper.cjs'));
const { generateSummary, generateMarkdownReport } = require(
path.join(__dirname, 'finding-formatter.cjs')
);
/**
* Scan modes map to CLI options.
*/
const MODE_OPTIONS = {
full: {},
'ai-only': { aiOnly: true },
quick: { quick: true },
targeted: {},
};
/**
* Run a Medusa security scan with the given options.
*
* @param {object} options - Scan options
* @param {string} [options.mode='full'] - Scan mode: full, ai-only, quick, targeted
* @param {string} [options.target='.'] - Target path to scan
* @param {string[]} [options.scanners] - Scanners for targeted mode
* @param {string} [options.failOn] - Severity threshold for non-zero exit
* @param {string} [options.format='sarif'] - Output format (sarif or json)
* @param {string[]} [options.exclude] - Paths to exclude
* @returns {{ findings: Array, summary: object, report: string, exitCode: number }}
*/
function runScan(options) {
const mode = options.mode || 'full';
const target = options.target || '.';
// Build CLI options from mode
const modeOpts = MODE_OPTIONS[mode] || {};
const cliOptions = {
...modeOpts,
};
// Pass through format, failOn, exclude
if (options.format) {
cliOptions.format = options.format;
}
if (options.failOn) {
cliOptions.failOn = options.failOn;
}
if (options.exclude) {
cliOptions.exclude = options.exclude;
}
// Targeted mode uses specific scanners
if (mode === 'targeted' && options.scanners) {
cliOptions.scanners = options.scanners;
}
// Run the scan via CLI wrapper
const scanResult = runMedusaScan(target, cliOptions);
// Generate summary and report from findings
const summary = generateSummary(scanResult.findings);
const report = generateMarkdownReport(scanResult.findings);
return {
findings: scanResult.findings,
summary,
report,
exitCode: scanResult.exitCode,
};
}
module.exports = {
runScan,
};
'use strict';
const path = require('path');
const { safeParseJSON } = require(
path.join(__dirname, '..', '..', '..', 'lib', 'utils', 'safe-json.cjs')
);
/**
* Map SARIF level to severity enum.
* @param {string} level - SARIF level (error, warning, note, none)
* @returns {string} Severity: CRITICAL, HIGH, MEDIUM, or LOW
*/
function mapSarifLevel(level) {
switch (level) {
case 'error':
return 'HIGH';
case 'warning':
return 'MEDIUM';
case 'note':
return 'LOW';
default:
return 'MEDIUM';
}
}
/**
* Rule ID prefix to category mapping.
*/
const RULE_CATEGORY_MAP = {
PI: 'prompt_injection',
MCP: 'mcp_security',
SEC: 'secrets',
AI: 'ai_security',
AUTH: 'authentication',
CRYPTO: 'cryptography',
INJ: 'injection',
XSS: 'xss',
};
/**
* Categorize a Medusa rule ID into a human-readable category.
* Rule IDs follow pattern: MEDUSA-{PREFIX}-{NUMBER}
* @param {string} ruleId - Medusa rule ID
* @returns {string} Category name
*/
function categorizeRuleId(ruleId) {
if (!ruleId || typeof ruleId !== 'string') {
return 'general';
}
const parts = ruleId.split('-');
// Expected format: MEDUSA-PREFIX-NUMBER
if (parts.length >= 3 && parts[0] === 'MEDUSA') {
const prefix = parts[1];
return RULE_CATEGORY_MAP[prefix] || 'general';
}
return 'general';
}
/**
* Determine severity from SARIF level and rule ID.
* Promotes error-level AI security findings (prompt injection, MCP) to CRITICAL.
* @param {string} level - SARIF level
* @param {string} ruleId - Medusa rule ID
* @returns {string} Severity
*/
function determineSeverity(level, ruleId) {
const baseSeverity = mapSarifLevel(level);
if (baseSeverity === 'HIGH') {
const category = categorizeRuleId(ruleId);
if (category === 'prompt_injection' || category === 'ai_security') {
return 'CRITICAL';
}
}
return baseSeverity;
}
/**
* Extract location data from a SARIF result's locations array.
* @param {object} result - SARIF result object
* @returns {{ file: string, line: number, column: number }}
*/
function extractLocation(result) {
const empty = { file: '', line: 0, column: 0 };
if (!Array.isArray(result.locations) || result.locations.length === 0) {
return empty;
}
const phys = (result.locations[0] || {}).physicalLocation;
if (!phys) {
return empty;
}
const file = phys.artifactLocation && phys.artifactLocation.uri ? phys.artifactLocation.uri : '';
const line = phys.region ? phys.region.startLine || 0 : 0;
const column = phys.region ? phys.region.startColumn || 0 : 0;
return { file, line, column };
}
/**
* Validate that a data object is valid SARIF v2.1.0.
* @param {*} data - Parsed data to validate
* @returns {boolean}
*/
function isValidSarif(data) {
return data && data.version === '2.1.0' && Array.isArray(data.runs);
}
/**
* Convert a single SARIF result into a standardized finding.
* @param {object} result - SARIF result object
* @returns {object} Standardized finding
*/
function convertResult(result) {
const ruleId = result.ruleId || 'UNKNOWN';
const level = result.level || 'warning';
const message = result.message && result.message.text ? result.message.text : '';
const location = extractLocation(result);
return {
ruleId,
severity: determineSeverity(level, ruleId),
category: categorizeRuleId(ruleId),
message,
file: location.file,
line: location.line,
column: location.column,
};
}
/**
* Parse a SARIF v2.1.0 object into standardized findings array.
* @param {object|string} sarifData - SARIF object or JSON string
* @returns {Array|{error: string, findings: Array}} Array of findings or error object
*/
function parseSarif(sarifData) {
let data = sarifData;
if (typeof data === 'string') {
data = safeParseJSON(data);
if (!data || (!data.version && !data.runs)) {
return { error: 'Failed to parse SARIF JSON string', findings: [] };
}
}
if (!isValidSarif(data)) {
return { error: 'Invalid SARIF structure: missing version 2.1.0 or runs array', findings: [] };
}
const findings = [];
for (const run of data.runs) {
if (!Array.isArray(run.results)) {
continue;
}
for (const result of run.results) {
findings.push(convertResult(result));
}
}
return findings;
}
module.exports = {
parseSarif,
mapSarifLevel,
categorizeRuleId,
determineSeverity,
RULE_CATEGORY_MAP,
};
'use strict';
const fs = require('fs');
const path = require('path');
const { checkInstallation, runMedusaScan } = require(path.join(__dirname, 'cli-wrapper.cjs'));
const { generateSummary } = require(path.join(__dirname, 'finding-formatter.cjs'));
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
const REPORT_PATH = path.join(
PROJECT_ROOT,
'.claude',
'context',
'reports',
'security-review-medusa-scan-2026-02-17.md'
);
const TARGETS = [
path.join(PROJECT_ROOT, '.claude', 'hooks'),
path.join(PROJECT_ROOT, '.claude', 'lib'),
path.join(PROJECT_ROOT, '.claude', 'skills', 'medusa-security', 'scripts'),
path.join(PROJECT_ROOT, '.claude', 'CLAUDE.md'),
];
const CODE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.json', '.md']);
const CHECKS = [
{
id: 'shell_true',
title: 'Potential shell injection surface (`shell: true`)',
regex: /shell\s*:\s*true/gm,
severity: 'HIGH',
},
{
id: 'raw_json_parse',
title: 'Raw `JSON.parse` usage (prefer `safeParseJSON`)',
regex: /JSON\.parse\s*\(/gm,
severity: 'MEDIUM',
},
{
id: 'exec_sync',
title: 'Blocking shell execution (`execSync`)',
regex: /\bexecSync\s*\(/gm,
severity: 'MEDIUM',
},
{
id: 'exec_async',
title: 'Shell command execution (`exec`)',
regex: /\bexec\s*\(/gm,
severity: 'MEDIUM',
},
];
function toPosixPath(filePath) {
return path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/');
}
function shouldScanFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
return CODE_EXTENSIONS.has(ext);
}
function walkFiles(startPath, out = []) {
if (!fs.existsSync(startPath)) {
return out;
}
const stat = fs.statSync(startPath);
if (stat.isFile()) {
if (shouldScanFile(startPath)) {
out.push(startPath);
}
return out;
}
if (!stat.isDirectory()) {
return out;
}
for (const entry of fs.readdirSync(startPath, { withFileTypes: true })) {
if (entry.name === 'node_modules' || entry.name === '.git') {
continue;
}
const abs = path.join(startPath, entry.name);
if (entry.isDirectory()) {
walkFiles(abs, out);
} else if (entry.isFile() && shouldScanFile(abs)) {
out.push(abs);
}
}
return out;
}
function findLineNumber(content, matchIndex) {
return content.slice(0, matchIndex).split('\n').length;
}
function runManualChecks() {
const files = [];
for (const target of TARGETS) {
walkFiles(target, files);
}
const findings = [];
for (const filePath of files) {
let content = '';
try {
content = fs.readFileSync(filePath, 'utf8');
} catch {
continue;
}
for (const check of CHECKS) {
check.regex.lastIndex = 0;
let match = check.regex.exec(content);
while (match) {
findings.push({
ruleId: `MANUAL-${check.id.toUpperCase()}`,
severity: check.severity,
category: 'general',
message: check.title,
file: toPosixPath(filePath),
line: findLineNumber(content, match.index),
column: 1,
});
if (findings.length >= 200) {
return { findings, filesScanned: files.length };
}
match = check.regex.exec(content);
}
}
}
return { findings, filesScanned: files.length };
}
function severityCounts(findings) {
return findings.reduce(
(acc, finding) => {
const sev = finding.severity || 'MEDIUM';
if (acc[sev] !== undefined) {
acc[sev] += 1;
}
return acc;
},
{ CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
);
}
function topFindings(findings, limit = 20) {
return findings.slice(0, limit);
}
function buildReport(scanMeta, medusaFindings, manualFindings, filesScanned) {
const medusaSummary = generateSummary(medusaFindings);
const manualSummary = generateSummary(manualFindings);
const totalCounts = severityCounts([...medusaFindings, ...manualFindings]);
const lines = [];
lines.push('<!-- Agent: security-architect | Task: #7 | Session: 2026-02-17 -->');
lines.push('');
lines.push('# Security Review: Medusa Scan');
lines.push('');
lines.push(`- Generated: ${new Date().toISOString()}`);
lines.push(`- Project: \`${PROJECT_ROOT.replace(/\\/g, '/')}\``);
lines.push(`- Medusa Installed: **${scanMeta.installed ? 'yes' : 'no'}**`);
lines.push(`- Medusa Version: **${scanMeta.version || 'n/a'}**`);
if (scanMeta.error) {
lines.push(`- Medusa Error: \`${scanMeta.error.replace(/\s+/g, ' ').trim()}\``);
}
lines.push(`- Files Scanned (manual checks): **${filesScanned}**`);
lines.push('');
lines.push('## Severity Breakdown');
lines.push('');
lines.push('| Source | Critical | High | Medium | Low | Total |');
lines.push('| --- | --- | --- | --- | --- | --- |');
lines.push(
`| Medusa | ${medusaSummary.critical} | ${medusaSummary.high} | ${medusaSummary.medium} | ${medusaSummary.low} | ${medusaSummary.total} |`
);
lines.push(
`| Manual | ${manualSummary.critical} | ${manualSummary.high} | ${manualSummary.medium} | ${manualSummary.low} | ${manualSummary.total} |`
);
lines.push(
`| Combined | ${totalCounts.CRITICAL} | ${totalCounts.HIGH} | ${totalCounts.MEDIUM} | ${totalCounts.LOW} | ${medusaFindings.length + manualFindings.length} |`
);
lines.push('');
lines.push('## Top Findings');
lines.push('');
lines.push('| Severity | Rule | File | Line | Message |');
lines.push('| --- | --- | --- | --- | --- |');
for (const finding of topFindings([...medusaFindings, ...manualFindings])) {
const msg = (finding.message || '').replace(/\|/g, '\\|');
lines.push(
`| ${finding.severity || 'MEDIUM'} | ${finding.ruleId || 'N/A'} | ${finding.file || 'N/A'} | ${finding.line || 0} | ${msg} |`
);
}
if (medusaFindings.length + manualFindings.length === 0) {
lines.push('| LOW | NONE | n/a | 0 | No findings detected |');
}
lines.push('');
lines.push('## Notes');
lines.push('');
lines.push(
'- This review intentionally avoids recursive `Glob` calls to prevent ripgrep timeout failures.'
);
lines.push(
'- Manual checks cover shell execution risk and unsafe parsing patterns in high-value framework paths.'
);
lines.push('');
return lines.join('\n');
}
function writeReport(content) {
fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
fs.writeFileSync(REPORT_PATH, content, 'utf8');
}
function run() {
const installMeta = checkInstallation();
let medusaFindings = [];
if (installMeta.installed) {
try {
const scan = runMedusaScan('.', { format: 'sarif', failOn: 'high' });
medusaFindings = Array.isArray(scan.findings) ? scan.findings : [];
} catch (err) {
installMeta.error = err && err.message ? err.message : 'scan failed';
}
}
const manual = runManualChecks();
const report = buildReport(installMeta, medusaFindings, manual.findings, manual.filesScanned);
writeReport(report);
process.stdout.write(`${REPORT_PATH}\n`);
}
if (require.main === module) {
run();
}
module.exports = {
run,
runManualChecks,
buildReport,
};
medusa-security Implementation Template
Goal
- Define target outcome and acceptance criteria.
TDD
1. Red 2. Green 3. Refactor
Verification
- lint
- format
- targeted tests