UNPKG

mcp-security-agent

Version:

An MCP-based security scanner and agentic AI for vulnerability detection

687 lines 27.6 kB
import { SeverityLevel, RuleAction } from '../types/index.js'; export class PolicyEngine { logger; policies = new Map(); defaultPolicies = []; constructor(logger) { this.logger = logger; this.initializeDefaultPolicies(); } /** * Initialize default security policies */ initializeDefaultPolicies() { // Global security policies const globalPolicies = [ { id: 'global-security', name: 'Global Security Policy', description: 'Default security policies for all projects', version: '1.0.0', scope: 'global', inheritance: 'allow', enabled: true, priority: 0, rules: [ { id: 'no-hardcoded-secrets', name: 'No Hardcoded Secrets', description: 'Prevent hardcoded API keys, passwords, and tokens', type: 'regex', action: RuleAction.BLOCK, pattern: '(api[_-]?key|password|secret|token)\\s*[:=]\\s*[\'"][^\'"]+[\'"]', conditions: [ { field: 'content', operator: 'regex', value: '(api[_-]?key|password|secret|token)\\s*[:=]\\s*[\'"][^\'"]+[\'"]', description: 'Contains hardcoded secret pattern' } ], severity: SeverityLevel.HIGH, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-259', url: 'https://cwe.mitre.org/data/definitions/259.html' }, { type: 'OWASP', id: 'A02:2021', url: 'https://owasp.org/Top10/A02_2021-Cryptographic_Failures/' } ], metadata: {} }, { id: 'no-sql-injection', name: 'No SQL Injection', description: 'Prevent SQL injection vulnerabilities', type: 'regex', action: RuleAction.BLOCK, pattern: 'SELECT\\s+.*\\s+FROM\\s+.*\\s+WHERE\\s+.*\\+\\s*\\$', conditions: [ { field: 'content', operator: 'regex', value: 'SELECT\\s+.*\\s+FROM\\s+.*\\s+WHERE\\s+.*\\+\\s*\\$', description: 'Contains potential SQL injection pattern' } ], severity: SeverityLevel.CRITICAL, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-89', url: 'https://cwe.mitre.org/data/definitions/89.html' }, { type: 'OWASP', id: 'A03:2021', url: 'https://owasp.org/Top10/A03_2021-Injection/' } ], metadata: {} }, { id: 'no-xss', name: 'No Cross-Site Scripting', description: 'Prevent XSS vulnerabilities', type: 'regex', action: RuleAction.WARN, pattern: 'innerHTML\\s*=\\s*.*\\+\\s*\\$', conditions: [ { field: 'content', operator: 'regex', value: 'innerHTML\\s*=\\s*.*\\+\\s*\\$', description: 'Contains potential XSS pattern' } ], severity: SeverityLevel.HIGH, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-79', url: 'https://cwe.mitre.org/data/definitions/79.html' }, { type: 'OWASP', id: 'A03:2021', url: 'https://owasp.org/Top10/A03_2021-Injection/' } ], metadata: {} }, { id: 'no-command-injection', name: 'No Command Injection', description: 'Prevent command injection vulnerabilities', type: 'regex', action: RuleAction.BLOCK, pattern: 'exec\\s*\\(\\s*.*\\+\\s*\\$', conditions: [ { field: 'content', operator: 'regex', value: 'exec\\s*\\(\\s*.*\\+\\s*\\$', description: 'Contains potential command injection pattern' } ], severity: SeverityLevel.CRITICAL, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-78', url: 'https://cwe.mitre.org/data/definitions/78.html' }, { type: 'OWASP', id: 'A03:2021', url: 'https://owasp.org/Top10/A03_2021-Injection/' } ], metadata: {} }, { id: 'no-path-traversal', name: 'No Path Traversal', description: 'Prevent path traversal vulnerabilities', type: 'regex', action: RuleAction.BLOCK, pattern: 'fs\\.readFile\\s*\\(\\s*.*\\+\\s*\\$', conditions: [ { field: 'content', operator: 'regex', value: 'fs\\.readFile\\s*\\(\\s*.*\\+\\s*\\$', description: 'Contains potential path traversal pattern' } ], severity: SeverityLevel.HIGH, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-22', url: 'https://cwe.mitre.org/data/definitions/22.html' }, { type: 'OWASP', id: 'A01:2021', url: 'https://owasp.org/Top10/A01_2021-Broken_Access_Control/' } ], metadata: {} } ], metadata: {} }, { id: 'dependency-security', name: 'Dependency Security Policy', description: 'Security policies for dependency management', version: '1.0.0', scope: 'global', inheritance: 'allow', enabled: true, priority: 1, rules: [ { id: 'no-critical-vulnerabilities', name: 'No Critical Vulnerabilities', description: 'Prevent dependencies with critical vulnerabilities', type: 'dependency', action: RuleAction.BLOCK, conditions: [ { field: 'severity', operator: 'equals', value: 'critical', description: 'Dependency has critical vulnerability' } ], severity: SeverityLevel.CRITICAL, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-1104', url: 'https://cwe.mitre.org/data/definitions/1104.html' }, { type: 'OWASP', id: 'A06:2021', url: 'https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/' } ], metadata: {} }, { id: 'no-outdated-dependencies', name: 'No Outdated Dependencies', description: 'Prevent severely outdated dependencies', type: 'dependency', action: RuleAction.WARN, conditions: [ { field: 'daysOutdated', operator: 'greater_than', value: 365, description: 'Dependency is more than 1 year outdated' } ], severity: SeverityLevel.MEDIUM, enabled: true, scope: 'global', inheritance: 'allow', references: [ { type: 'CWE', id: 'CWE-1104', url: 'https://cwe.mitre.org/data/definitions/1104.html' }, { type: 'OWASP', id: 'A06:2021', url: 'https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/' } ], metadata: {} } ], metadata: {} }, { id: 'configuration-security', name: 'Configuration Security Policy', description: 'Security policies for configuration files', version: '1.0.0', scope: 'global', inheritance: 'allow', enabled: true, priority: 2, rules: [ { id: 'no-debug-mode', name: 'No Debug Mode in Production', description: 'Prevent debug mode in production configurations', type: 'config', action: RuleAction.WARN, conditions: [ { field: 'debug', operator: 'equals', value: true, description: 'Debug mode is enabled' }, { field: 'environment', operator: 'equals', value: 'production', description: 'Environment is production' } ], severity: SeverityLevel.MEDIUM, enabled: true, scope: 'global', inheritance: 'deny', references: [ { type: 'CWE', id: 'CWE-489', url: 'https://cwe.mitre.org/data/definitions/489.html' }, { type: 'OWASP', id: 'A05:2021', url: 'https://owasp.org/Top10/A05_2021-Security_Misconfiguration/' } ], metadata: {} }, { id: 'secure-cors', name: 'Secure CORS Configuration', description: 'Ensure CORS is properly configured', type: 'config', action: RuleAction.WARN, conditions: [ { field: 'cors.origin', operator: 'equals', value: '*', description: 'CORS origin is set to wildcard' } ], severity: SeverityLevel.MEDIUM, enabled: true, scope: 'global', inheritance: 'allow', references: [ { type: 'CWE', id: 'CWE-942', url: 'https://cwe.mitre.org/data/definitions/942.html' }, { type: 'OWASP', id: 'A05:2021', url: 'https://owasp.org/Top10/A05_2021-Security_Misconfiguration/' } ], metadata: {} } ], metadata: {} } ]; // Add default policies globalPolicies.forEach(policy => { this.policies.set(policy.id, policy); this.defaultPolicies.push(policy); }); this.logger.info('Default security policies initialized', { policyCount: globalPolicies.length, ruleCount: globalPolicies.reduce((sum, policy) => sum + policy.rules.length, 0) }); } /** * Add a new security policy */ addPolicy(policy) { if (this.policies.has(policy.id)) { throw new Error(`Policy with ID ${policy.id} already exists`); } this.policies.set(policy.id, policy); this.logger.info('Security policy added', { policyId: policy.id, policyName: policy.name, ruleCount: policy.rules.length, scope: policy.scope }); } /** * Remove a security policy */ removePolicy(policyId) { const policy = this.policies.get(policyId); if (!policy) { return false; } // Don't allow removal of default policies if (this.defaultPolicies.some(p => p.id === policyId)) { throw new Error(`Cannot remove default policy: ${policyId}`); } this.policies.delete(policyId); this.logger.info('Security policy removed', { policyId }); return true; } /** * Get a security policy by ID */ getPolicy(policyId) { return this.policies.get(policyId); } /** * Get all policies */ getAllPolicies() { return Array.from(this.policies.values()); } /** * Get policies by scope */ getPoliciesByScope(scope) { return Array.from(this.policies.values()).filter(policy => policy.scope === scope); } /** * Enable or disable a policy */ setPolicyEnabled(policyId, enabled) { const policy = this.policies.get(policyId); if (!policy) { return false; } policy.enabled = enabled; this.logger.info('Policy enabled/disabled', { policyId, enabled }); return true; } /** * Update a policy rule */ updatePolicyRule(policyId, ruleId, updatedRule) { const policy = this.policies.get(policyId); if (!policy) { return false; } const ruleIndex = policy.rules.findIndex(rule => rule.id === ruleId); if (ruleIndex === -1) { return false; } policy.rules[ruleIndex] = updatedRule; this.logger.info('Policy rule updated', { policyId, ruleId }); return true; } /** * Evaluate a policy against a context */ async evaluatePolicy(policy, context) { const results = this.evaluatePolicyRules(policy, context); return results[0] || { policyId: policy.id, ruleId: 'default', matched: false, severity: SeverityLevel.INFO, evidence: 'No rules matched', trace: { ruleId: 'default', ruleName: 'Default', matched: false, evidence: 'No rules matched', conditions: [], context: {} }, recommendations: [], metadata: {} }; } /** * Evaluate all rules in a policy against a context */ evaluatePolicyRules(policy, context) { if (!policy.enabled) { return []; } const results = []; for (const rule of policy.rules) { if (!rule.enabled) { continue; } const result = this.evaluateRule(rule, context); if (result.matched) { const allowed = rule.action !== RuleAction.BLOCK || !result.matched; results.push({ policyId: policy.id, ruleId: rule.id, matched: true, severity: rule.severity, evidence: result.evidence, trace: result.trace, recommendations: this.generateRecommendations(rule), allowed, metadata: {} }); } } return results; } /** * Evaluate all applicable policies against a context */ async evaluateAllPolicies(context) { const results = []; for (const policy of this.policies.values()) { if (!policy.enabled) { continue; } const policyResult = await this.evaluatePolicy(policy, context); results.push(policyResult); } return results; } /** * Evaluate a single rule against a context */ evaluateRule(rule, context) { const trace = { ruleId: rule.id, ruleName: rule.name, matched: false, evidence: '', conditions: [], context: { ...context } }; let matched = true; const evidence = []; for (const condition of rule.conditions) { const conditionResult = this.evaluateCondition(condition, context); trace.conditions.push(conditionResult); if (!conditionResult.matched) { matched = false; } else { evidence.push(conditionResult.actualValue); } } trace.matched = matched; trace.evidence = evidence.join(', '); return { matched, evidence: trace.evidence, trace }; } /** * Evaluate a single condition against a context */ evaluateCondition(condition, context) { const actualValue = this.getFieldValue(context, condition.field); let matched = false; switch (condition.operator) { case 'equals': matched = actualValue === condition.value; break; case 'contains': matched = String(actualValue).includes(String(condition.value)); break; case 'regex': try { const regex = new RegExp(String(condition.value), 'i'); matched = regex.test(String(actualValue)); } catch (error) { matched = false; } break; case 'greater_than': matched = Number(actualValue) > Number(condition.value); break; case 'less_than': matched = Number(actualValue) < Number(condition.value); break; case 'in': matched = Array.isArray(condition.value) && condition.value.includes(actualValue); break; case 'not_in': matched = Array.isArray(condition.value) && !condition.value.includes(actualValue); break; } return { condition, matched, actualValue }; } /** * Get field value from context using dot notation */ getFieldValue(context, field) { const keys = field.split('.'); let value = context; for (const key of keys) { if (value && typeof value === 'object' && key in value) { value = value[key]; } else { return undefined; } } return value; } /** * Generate recommendations for a rule */ generateRecommendations(rule) { const recommendations = []; switch (rule.id) { case 'no-hardcoded-secrets': recommendations.push('Use environment variables or secure secret management'); recommendations.push('Consider using a secrets manager like AWS Secrets Manager or HashiCorp Vault'); break; case 'no-sql-injection': recommendations.push('Use parameterized queries or prepared statements'); recommendations.push('Implement input validation and sanitization'); break; case 'no-xss': recommendations.push('Use proper output encoding'); recommendations.push('Implement Content Security Policy (CSP)'); break; case 'no-command-injection': recommendations.push('Avoid command execution with user input'); recommendations.push('Use built-in APIs instead of shell commands'); break; case 'no-path-traversal': recommendations.push('Validate and sanitize file paths'); recommendations.push('Use path normalization and whitelist allowed directories'); break; case 'no-critical-vulnerabilities': recommendations.push('Update vulnerable dependencies to latest secure versions'); recommendations.push('Consider alternative packages if updates are not available'); break; case 'no-debug-mode': recommendations.push('Disable debug mode in production environments'); recommendations.push('Use environment-specific configuration files'); break; case 'secure-cors': recommendations.push('Configure CORS with specific allowed origins'); recommendations.push('Avoid using wildcard (*) for CORS origins'); break; default: recommendations.push('Review and address the security issue'); recommendations.push('Follow security best practices for the specific vulnerability type'); } return recommendations; } /** * Get policy inheritance chain */ getPolicyInheritance(policyId) { const policy = this.policies.get(policyId); if (!policy) { return []; } const inheritance = [policy]; // Add parent policies based on scope if (policy.scope === 'repo') { const teamPolicies = this.getPoliciesByScope('team'); inheritance.push(...teamPolicies); } if (policy.scope === 'repo' || policy.scope === 'team') { const globalPolicies = this.getPoliciesByScope('global'); inheritance.push(...globalPolicies); } return inheritance; } /** * Export policies to human-readable format */ exportPolicies(format = 'json') { const policies = Array.from(this.policies.values()); switch (format) { case 'json': return JSON.stringify(policies, null, 2); case 'yaml': // Simple YAML conversion return this.convertToYAML(policies); case 'rego': return this.convertToRego(policies); default: return JSON.stringify(policies, null, 2); } } /** * Convert policies to YAML format */ convertToYAML(policies) { let yaml = '# Security Policies\n\n'; for (const policy of policies) { yaml += `# ${policy.name}\n`; yaml += `# ${policy.description}\n`; yaml += `policy_${policy.id}:\n`; yaml += ` scope: ${policy.scope}\n`; yaml += ` enabled: ${policy.enabled}\n`; yaml += ` rules:\n`; for (const rule of policy.rules) { yaml += ` - id: ${rule.id}\n`; yaml += ` name: ${rule.name}\n`; yaml += ` severity: ${rule.severity}\n`; yaml += ` enabled: ${rule.enabled}\n`; if (rule.pattern) { yaml += ` pattern: ${rule.pattern}\n`; } yaml += ` conditions:\n`; for (const condition of rule.conditions) { yaml += ` - field: ${condition.field}\n`; yaml += ` operator: ${condition.operator}\n`; yaml += ` value: ${condition.value}\n`; } yaml += '\n'; } } return yaml; } /** * Convert policies to Rego format */ convertToRego(policies) { let rego = 'package security.policies\n\n'; for (const policy of policies) { rego += `# ${policy.name}\n`; rego += `# ${policy.description}\n`; for (const rule of policy.rules) { rego += `violation[{"msg": msg, "severity": "${rule.severity}", "rule": "${rule.id}"}] {\n`; if (rule.pattern) { rego += ` input.content = data.patterns.${rule.id}\n`; } for (const condition of rule.conditions) { rego += ` ${condition.field} ${this.convertOperatorToRego(condition.operator)} ${this.convertValueToRego(condition.value)}\n`; } rego += ` msg := "${rule.description}"\n`; rego += `}\n\n`; } } return rego; } /** * Convert operator to Rego syntax */ convertOperatorToRego(operator) { switch (operator) { case 'equals': return '=='; case 'contains': return 'contains'; case 'greater_than': return '>'; case 'less_than': return '<'; case 'in': return 'in'; case 'not_in': return 'not in'; default: return '=='; } } /** * Convert value to Rego syntax */ convertValueToRego(value) { if (typeof value === 'string') { return `"${value}"`; } if (Array.isArray(value)) { return `[${value.map(v => this.convertValueToRego(v)).join(', ')}]`; } return String(value); } } //# sourceMappingURL=PolicyEngine.js.map