security.js
1 /** 2 * Security Agent 3 * 4 * Responsible for security audits, compliance validation, and vulnerability detection. 5 */ 6 7 import { BaseAgent } from './base-agent.js'; 8 import { execSync } from 'child_process'; 9 import fs from 'fs/promises'; 10 import { addReviewItem } from '../utils/human-review-queue.js'; 11 import { 12 performThreatModeling, 13 analyzeCodeSecurity, 14 generateSecureFix, 15 } from './utils/agent-claude-api.js'; 16 import { readFile, editFile } from './utils/file-operations.js'; 17 18 export class SecurityAgent extends BaseAgent { 19 constructor() { 20 super('security', ['base.md', 'security.md']); 21 } 22 23 /** 24 * Process a security task 25 * 26 * @param {Object} task - Task object 27 * @returns {Promise<void>} 28 */ 29 async processTask(task) { 30 try { 31 // Validate context exists and parse if needed 32 if (!task.context_json) { 33 // Some tasks may not require context (e.g., scan_dependencies) 34 task.context_json = {}; 35 } 36 37 const context = 38 typeof task.context_json === 'string' ? JSON.parse(task.context_json) : task.context_json; 39 40 // Ensure context is attached to task for handlers 41 task.context_json = context; 42 43 switch (task.task_type) { 44 case 'audit_code': 45 await this.auditCode(task); 46 break; 47 48 case 'scan_dependencies': 49 await this.scanDependencies(task); 50 break; 51 52 case 'verify_compliance': 53 await this.verifyCompliance(task); 54 break; 55 56 case 'scan_secrets': 57 await this.scanSecrets(task); 58 break; 59 60 case 'threat_model': 61 await this.threatModel(task); 62 break; 63 64 case 'fix_security_issue': 65 await this.fixSecurityIssue(task); 66 break; 67 68 case 'review_dependency_update': 69 // This is now handled by scan_dependencies 70 await this.scanDependencies(task); 71 break; 72 73 case 'generate_sbom': 74 await this.generateSbom(task); 75 break; 76 77 // Task types assigned to wrong agent - delegate 78 case 'implement_feature': 79 case 'fix_bug': 80 await this.delegateToCorrectAgent(task); 81 break; 82 83 default: 84 // Unknown task types - delegate to correct agent via task routing 85 await this.log('warn', 'Unknown task type received, delegating', { 86 task_id: task.id, 87 task_type: task.task_type, 88 }); 89 await this.delegateToCorrectAgent(task); 90 } 91 } catch (error) { 92 await this.log('error', `Security task ${task.id} failed: ${error.message}`, { 93 task_id: task.id, 94 task_type: task.task_type, 95 error: error.message, 96 stack: error.stack, 97 }); 98 throw error; // Re-throw so task manager can handle 99 } 100 } 101 /** 102 * Audit code for security vulnerabilities 103 * 104 * @param {Object} task - Task with audit requirements 105 * @returns {Promise<void>} 106 */ 107 async auditCode(task) { 108 const context = task.context_json || {}; 109 const { files, focus_areas } = context; 110 111 await this.log('info', 'Starting security audit', { 112 task_id: task.id, 113 files: files?.length || 'all', 114 focus_areas, 115 }); 116 117 const findings = []; 118 119 // Check for SQL injection 120 if (!focus_areas || focus_areas.includes('sql_injection')) { 121 const sqlFindings = await this.checkSqlInjection(files); 122 findings.push(...sqlFindings); 123 } 124 125 // Check for secrets 126 if (!focus_areas || focus_areas.includes('secrets')) { 127 const secretFindings = await this.checkSecrets(files); 128 findings.push(...secretFindings); 129 } 130 131 // Check for command injection 132 if (!focus_areas || focus_areas.includes('command_injection')) { 133 const cmdFindings = await this.checkCommandInjection(files); 134 findings.push(...cmdFindings); 135 } 136 137 // Categorize by severity 138 const critical = findings.filter(f => f.severity === 'critical'); 139 const high = findings.filter(f => f.severity === 'high'); 140 const medium = findings.filter(f => f.severity === 'medium'); 141 const low = findings.filter(f => f.severity === 'low'); 142 143 await this.log('info', 'Security audit complete', { 144 task_id: task.id, 145 total_findings: findings.length, 146 critical: critical.length, 147 high: high.length, 148 medium: medium.length, 149 low: low.length, 150 }); 151 152 // Escalate critical and high findings 153 for (const finding of [...critical, ...high]) { 154 addReviewItem({ 155 file: finding.file, 156 reason: `${finding.type}: ${finding.description}`, 157 type: 'security', 158 priority: finding.severity, 159 }); 160 } 161 162 await this.completeTask(task.id, { 163 findings, 164 summary: { 165 total: findings.length, 166 by_severity: { 167 critical: critical.length, 168 high: high.length, 169 medium: medium.length, 170 low: low.length, 171 }, 172 }, 173 }); 174 } 175 176 /** 177 * Check for SQL injection vulnerabilities 178 * 179 * @param {string[]} [files] - Files to check 180 * @returns {Promise<Array>} - Findings 181 */ 182 async checkSqlInjection(files = null) { 183 const findings = []; 184 185 // Pattern: String interpolation in SQL queries 186 const pattern = /db\.(exec|prepare|query)\s*\(`[^`]*\$\{/; 187 188 const filesToCheck = files || (await this.getJsFiles()); 189 190 for (const file of filesToCheck) { 191 try { 192 const content = await fs.readFile(file, 'utf8'); 193 const lines = content.split('\n'); 194 195 for (let i = 0; i < lines.length; i++) { 196 if (pattern.test(lines[i])) { 197 findings.push({ 198 type: 'sql_injection', 199 severity: 'critical', 200 file, 201 line: i + 1, 202 description: 'Potential SQL injection: string interpolation in query', 203 recommendation: 'Use prepared statements with parameterized queries', 204 }); 205 } 206 } 207 } catch (error) { 208 await this.log('warn', 'Failed to check file for SQL injection', { 209 file, 210 error: error.message, 211 }); 212 } 213 } 214 215 return findings; 216 } 217 218 /** 219 * Check for hardcoded secrets 220 * 221 * @param {string[]} [files] - Files to check 222 * @returns {Promise<Array>} - Findings 223 */ 224 async checkSecrets(files = null) { 225 const findings = []; 226 227 const secretPatterns = [ 228 { pattern: /api[_-]?key\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/i, type: 'api_key' }, 229 { pattern: /password\s*[:=]\s*['"][^'"]{8,}['"]/i, type: 'password' }, 230 { pattern: /secret\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/i, type: 'secret' }, 231 { pattern: /token\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/i, type: 'token' }, 232 { pattern: /(sk|pk)_live_[a-zA-Z0-9]{20,}/, type: 'stripe_key' }, 233 { pattern: /AIza[a-zA-Z0-9_-]{35}/, type: 'google_api_key' }, 234 ]; 235 236 const filesToCheck = files || (await this.getJsFiles()); 237 238 for (const file of filesToCheck) { 239 try { 240 const content = await fs.readFile(file, 'utf8'); 241 const lines = content.split('\n'); 242 243 for (let i = 0; i < lines.length; i++) { 244 const line = lines[i]; 245 246 // Skip if it's using process.env (safe) 247 if (/process\.env\./i.test(line)) continue; 248 249 // Skip comments 250 if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue; 251 252 for (const { pattern, type } of secretPatterns) { 253 if (pattern.test(line)) { 254 findings.push({ 255 type: 'hardcoded_secret', 256 severity: 'critical', 257 file, 258 line: i + 1, 259 description: `Potential hardcoded ${type}`, 260 recommendation: 'Use environment variables (process.env)', 261 }); 262 } 263 } 264 } 265 } catch (error) { 266 await this.log('warn', 'Failed to check file for secrets', { 267 file, 268 error: error.message, 269 }); 270 } 271 } 272 273 return findings; 274 } 275 276 /** 277 * Check for command injection vulnerabilities 278 * 279 * @param {string[]} [files] - Files to check 280 * @returns {Promise<Array>} - Findings 281 */ 282 async checkCommandInjection(files = null) { 283 const findings = []; 284 285 // Pattern: execSync/exec with string interpolation 286 const pattern = /(execSync|exec|spawn)\s*\(`[^`]*\$\{/; 287 288 const filesToCheck = files || (await this.getJsFiles()); 289 290 for (const file of filesToCheck) { 291 try { 292 const content = await fs.readFile(file, 'utf8'); 293 const lines = content.split('\n'); 294 295 for (let i = 0; i < lines.length; i++) { 296 if (pattern.test(lines[i])) { 297 findings.push({ 298 type: 'command_injection', 299 severity: 'high', 300 file, 301 line: i + 1, 302 description: 'Potential command injection: string interpolation in shell command', 303 recommendation: 'Use spawn() with array arguments or sanitize input', 304 }); 305 } 306 } 307 } catch (error) { 308 await this.log('warn', 'Failed to check file for command injection', { 309 file, 310 error: error.message, 311 }); 312 } 313 } 314 315 return findings; 316 } 317 318 /** 319 * Get all JS files in src/ 320 * 321 * @returns {Promise<string[]>} - File paths 322 */ 323 async getJsFiles() { 324 try { 325 const output = execSync('find src -name "*.js" -type f', { 326 encoding: 'utf8', 327 }); 328 329 return output.trim().split('\n').filter(Boolean); 330 } catch (error) { 331 await this.log('error', 'Failed to get JS files', { 332 error: error.message, 333 }); 334 return []; 335 } 336 } 337 338 /** 339 * Scan dependencies for vulnerabilities 340 * 341 * @param {Object} task - Task object 342 * @returns {Promise<void>} 343 */ 344 async scanDependencies(task) { 345 await this.log('info', 'Scanning dependencies', { 346 task_id: task.id, 347 }); 348 349 try { 350 // Run npm audit 351 const output = execSync('npm audit --json', { 352 encoding: 'utf8', 353 }); 354 355 const auditReport = JSON.parse(output); 356 357 const vulnerabilities = auditReport.vulnerabilities || {}; 358 const criticalCount = Object.values(vulnerabilities).filter( 359 v => v.severity === 'critical' 360 ).length; 361 const highCount = Object.values(vulnerabilities).filter(v => v.severity === 'high').length; 362 363 await this.log('info', 'Dependency scan complete', { 364 task_id: task.id, 365 critical: criticalCount, 366 high: highCount, 367 total: Object.keys(vulnerabilities).length, 368 }); 369 370 // Escalate critical and high vulnerabilities 371 if (criticalCount > 0 || highCount > 0) { 372 addReviewItem({ 373 file: 'package.json', 374 reason: `npm audit found ${criticalCount} critical and ${highCount} high severity vulnerabilities`, 375 type: 'security', 376 priority: criticalCount > 0 ? 'critical' : 'high', 377 }); 378 } 379 380 await this.completeTask(task.id, { 381 vulnerabilities: Object.keys(vulnerabilities).length, 382 critical: criticalCount, 383 high: highCount, 384 }); 385 } catch (error) { 386 await this.log('error', 'Dependency scan failed', { 387 task_id: task.id, 388 error: error.message, 389 }); 390 391 await this.failTask(task.id, error.message); 392 } 393 } 394 395 /** 396 * Verify compliance (TCPA, CAN-SPAM, GDPR) 397 * 398 * @param {Object} task - Task object 399 * @returns {Promise<void>} 400 */ 401 async verifyCompliance(task) { 402 const context = task.context_json || {}; 403 const { compliance_type, files } = context; 404 405 await this.log('info', 'Verifying compliance', { 406 task_id: task.id, 407 type: compliance_type, 408 }); 409 410 const violations = []; 411 412 if (compliance_type === 'tcpa' || compliance_type === 'all') { 413 const tcpaViolations = await this.checkTcpaCompliance(files); 414 violations.push(...tcpaViolations); 415 } 416 417 if (compliance_type === 'can-spam' || compliance_type === 'all') { 418 const canSpamViolations = await this.checkCanSpamCompliance(files); 419 violations.push(...canSpamViolations); 420 } 421 422 if (compliance_type === 'gdpr' || compliance_type === 'all') { 423 const gdprViolations = await this.checkGdprCompliance(files); 424 violations.push(...gdprViolations); 425 } 426 427 await this.completeTask(task.id, { 428 compliance_type, 429 violations, 430 compliant: violations.length === 0, 431 }); 432 } 433 434 /** 435 * Check TCPA compliance (SMS) 436 * 437 * @param {string[]} [files] - Files to check 438 * @returns {Promise<Array>} - Violations 439 */ 440 async checkTcpaCompliance(files = null) { 441 const violations = []; 442 443 const filesToCheck = files || ['src/outreach/sms.js']; 444 445 for (const file of filesToCheck) { 446 try { 447 const content = await fs.readFile(file, 'utf8'); 448 449 // Check for STOP keyword 450 if (!content.includes('STOP') && !content.includes('opt-out')) { 451 violations.push({ 452 type: 'tcpa_opt_out', 453 file, 454 description: 'Missing STOP keyword for opt-out', 455 severity: 'high', 456 }); 457 } 458 459 // Check for business hours 460 if (!content.match(/hour.*8.*21|business.*hours/i)) { 461 violations.push({ 462 type: 'tcpa_business_hours', 463 file, 464 description: 'No business hours check (8am-9pm)', 465 severity: 'medium', 466 }); 467 } 468 } catch (error) { 469 // File might not exist 470 } 471 } 472 473 return violations; 474 } 475 476 /** 477 * Check CAN-SPAM compliance (Email) 478 * 479 * @param {string[]} [files] - Files to check 480 * @returns {Promise<Array>} - Violations 481 */ 482 async checkCanSpamCompliance(files = null) { 483 const violations = []; 484 485 const filesToCheck = files || ['src/outreach/email.js']; 486 487 for (const file of filesToCheck) { 488 try { 489 const content = await fs.readFile(file, 'utf8'); 490 491 // Check for unsubscribe link 492 if (!content.includes('UNSUBSCRIBE') && !content.includes('unsubscribe')) { 493 violations.push({ 494 type: 'can_spam_unsubscribe', 495 file, 496 description: 'Missing unsubscribe link', 497 severity: 'critical', 498 }); 499 } 500 501 // Check for physical address 502 if (!content.includes('SENDER_ADDRESS') && !content.match(/physical.*address/i)) { 503 violations.push({ 504 type: 'can_spam_address', 505 file, 506 description: 'Missing physical address requirement', 507 severity: 'high', 508 }); 509 } 510 } catch (error) { 511 // File might not exist 512 } 513 } 514 515 return violations; 516 } 517 518 /** 519 * Check GDPR compliance 520 * 521 * @param {string[]} [files] - Files to check 522 * @returns {Promise<Array>} - Violations 523 */ 524 async checkGdprCompliance(files = null) { 525 const violations = []; 526 527 // Check for EU country blocking 528 const filesToCheck = files || ['src/stages/scoring.js', 'src/stages/rescoring.js']; 529 530 for (const file of filesToCheck) { 531 try { 532 const content = await fs.readFile(file, 'utf8'); 533 534 // Check for EU country handling 535 if (!content.match(/EU_COUNTRIES|gdpr_blocked/i)) { 536 violations.push({ 537 type: 'gdpr_eu_blocking', 538 file, 539 description: 'No GDPR EU country blocking detected', 540 severity: 'medium', 541 }); 542 } 543 } catch (error) { 544 // File might not exist 545 } 546 } 547 548 return violations; 549 } 550 551 /** 552 * Scan for secrets in codebase 553 * 554 * @param {Object} task - Task object 555 * @returns {Promise<void>} 556 */ 557 async scanSecrets(task) { 558 const context = task.context_json || {}; 559 const { files } = context; 560 561 await this.log('info', 'Scanning for secrets', { 562 task_id: task.id, 563 files: files?.length || 'all', 564 }); 565 566 const findings = await this.checkSecrets(files); 567 568 if (findings.length > 0) { 569 for (const finding of findings) { 570 addReviewItem({ 571 file: finding.file, 572 reason: `${finding.description} at line ${finding.line}`, 573 type: 'security', 574 priority: 'critical', 575 }); 576 } 577 } 578 579 await this.completeTask(task.id, { 580 secrets_found: findings.length, 581 findings, 582 }); 583 } 584 585 /** 586 * Perform threat modeling on a component 587 * 588 * @param {Object} task - Task with component to analyze 589 * @returns {Promise<void>} 590 */ 591 async threatModel(task) { 592 const context = task.context_json || {}; 593 const { component, component_type, data_flow, files } = context; 594 595 if (!context || (!component && (!files || files.length === 0))) { 596 await this.failTask(task.id, 'Missing required field: component or files in context'); 597 return; 598 } 599 600 await this.log('info', 'Starting threat modeling', { 601 task_id: task.id, 602 component_type, 603 files: files?.length || 0, 604 }); 605 606 try { 607 let componentContent = component; 608 609 // If files provided, read and concatenate them 610 if (files && files.length > 0) { 611 const fileContents = []; 612 for (const file of files) { 613 try { 614 const { content } = await readFile(file); 615 fileContents.push(`// File: ${file}\n${content}`); 616 } catch (error) { 617 await this.log('warn', 'Failed to read file for threat modeling', { 618 file, 619 error: error.message, 620 }); 621 } 622 } 623 componentContent = fileContents.join('\n\n'); 624 } 625 626 // Perform STRIDE threat modeling using Claude 627 const threatModel = await performThreatModeling('security', task.id, { 628 component: componentContent, 629 componentType: component_type || 'general', 630 dataFlow: data_flow, 631 }); 632 633 // Calculate risk levels 634 const threats = threatModel.threats || []; 635 const critical = threats.filter(t => t.risk_level === 'critical'); 636 const high = threats.filter(t => t.risk_level === 'high'); 637 const medium = threats.filter(t => t.risk_level === 'medium'); 638 const low = threats.filter(t => t.risk_level === 'low'); 639 640 await this.log('info', 'Threat modeling complete', { 641 task_id: task.id, 642 total_threats: threats.length, 643 critical: critical.length, 644 high: high.length, 645 medium: medium.length, 646 low: low.length, 647 }); 648 649 // Create fix_security_issue tasks for critical and high threats 650 for (const threat of [...critical, ...high]) { 651 const fixTaskId = await this.createTask({ 652 task_type: 'fix_security_issue', 653 assigned_to: 'security', 654 priority: threat.risk_level === 'critical' ? 10 : 8, 655 context: { 656 vulnerability: threat.title, 657 description: threat.description, 658 attack_scenario: threat.attack_scenario, 659 mitigation: threat.mitigation, 660 risk_level: threat.risk_level, 661 stride_category: threat.stride_category, 662 cwe_id: threat.cwe_id, 663 dread_score: threat.dread, 664 source: 'threat_model', 665 parent_threat_model_task_id: task.id, 666 }, 667 }); 668 669 await this.log('info', 'Created fix task for threat', { 670 threat_title: threat.title, 671 risk_level: threat.risk_level, 672 fix_task_id: fixTaskId, 673 }); 674 } 675 676 // Add critical threats to human review queue 677 for (const threat of critical) { 678 addReviewItem({ 679 file: files?.[0] || 'threat-model', 680 reason: `Critical threat: ${threat.title} - ${threat.description}`, 681 type: 'security', 682 priority: 'critical', 683 }); 684 } 685 686 await this.completeTask(task.id, { 687 threat_model: threatModel, 688 summary: { 689 total_threats: threats.length, 690 by_risk: { 691 critical: critical.length, 692 high: high.length, 693 medium: medium.length, 694 low: low.length, 695 }, 696 priority_threats: threatModel.priority_threats || [], 697 }, 698 }); 699 } catch (error) { 700 await this.log('error', 'Threat modeling failed', { 701 task_id: task.id, 702 error: error.message, 703 }); 704 705 await this.failTask(task.id, error.message); 706 } 707 } 708 709 /** 710 * Calculate DREAD score for a threat 711 * 712 * @param {Object} threat - Threat object with DREAD metrics 713 * @returns {number} - Average DREAD score (1-10) 714 */ 715 calculateDreadScore(threat) { 716 const { damage, reproducibility, exploitability, affected_users, discoverability } = 717 threat.dread || {}; 718 719 if (!damage || !reproducibility || !exploitability || !affected_users || !discoverability) { 720 return 0; 721 } 722 723 const total = damage + reproducibility + exploitability + affected_users + discoverability; 724 const average = total / 5; 725 726 return parseFloat(average.toFixed(2)); 727 } 728 729 /** 730 * Get risk level from DREAD score 731 * 732 * @param {Object} threat - Threat with DREAD score 733 * @returns {string} - Risk level: critical|high|medium|low 734 */ 735 getRiskLevel(threat) { 736 const score = threat.dread?.average || this.calculateDreadScore(threat); 737 738 if (score >= 8.5) return 'critical'; 739 if (score >= 7.0) return 'high'; 740 if (score >= 4.0) return 'medium'; 741 return 'low'; 742 } 743 744 /** 745 * Get security context for a specific vulnerability type 746 * 747 * @param {string} issueType - Type of security issue 748 * @returns {Object} - Context with common patterns, fixes, and test guidance 749 */ 750 getSecurityContext(issueType) { 751 const contexts = { 752 sql_injection: { 753 patterns: [/db\.(exec|prepare|query)\s*\(`[^`]*\$\{/, /\.(exec|query)\([^)]*\+[^)]*\)/], 754 fix_template: 'Use parameterized queries with placeholders (?, $1, etc)', 755 test_guidance: "Test with malicious inputs like: ' OR 1=1--, '; DROP TABLE--", 756 severity: 'critical', 757 }, 758 xss: { 759 patterns: [/innerHTML\s*=/, /document\.write\s*\(/, /eval\s*\(/], 760 fix_template: 'Use textContent or sanitize HTML with DOMPurify', 761 test_guidance: 'Test with: <script>alert(1)</script>, <img src=x onerror=alert(1)>', 762 severity: 'high', 763 }, 764 command_injection: { 765 patterns: [ 766 /(execSync|exec|spawn)\s*\(`[^`]*\$\{/, 767 /child_process\.(exec|execSync)\([^)]*\+[^)]*\)/, 768 ], 769 fix_template: 'Use spawn with array arguments or sanitize input thoroughly', 770 test_guidance: 'Test with: ; ls -la, && cat /etc/passwd, | whoami', 771 severity: 'high', 772 }, 773 secrets: { 774 patterns: [/api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i, /password\s*[:=]\s*['"][^'"]+['"]/i], 775 fix_template: 'Move to environment variables (process.env)', 776 test_guidance: 'Verify secret is not in git history or logs', 777 severity: 'critical', 778 }, 779 path_traversal: { 780 patterns: [/\.\.\//, /path\.join\([^)]*req\.(params|query|body)/], 781 fix_template: 'Validate and sanitize paths, use path.resolve() and check startsWith()', 782 test_guidance: 'Test with: ../../../etc/passwd, ....//....//etc/passwd', 783 severity: 'high', 784 }, 785 }; 786 787 return ( 788 contexts[issueType] || { 789 patterns: [], 790 fix_template: 'Follow security best practices for this vulnerability type', 791 test_guidance: 'Create comprehensive security tests', 792 severity: 'medium', 793 } 794 ); 795 } 796 797 /** 798 * Fix a security issue (auto-fix with Claude) 799 * 800 * @param {Object} task - Task object 801 * @returns {Promise<void>} 802 */ 803 async fixSecurityIssue(task) { 804 const context = task.context_json || {}; 805 const { 806 file, 807 vulnerability, 808 description, 809 mitigation, 810 risk_level, 811 type, 812 line, 813 recommendation, 814 source, 815 } = context; 816 817 if (!context || (!vulnerability && !description)) { 818 await this.failTask( 819 task.id, 820 'Missing required field: vulnerability or description in context' 821 ); 822 return; 823 } 824 825 await this.log('info', 'Fixing security issue', { 826 task_id: task.id, 827 file, 828 vulnerability, 829 risk_level, 830 source, 831 }); 832 833 try { 834 // If no file specified, cannot auto-fix 835 if (!file) { 836 await this.log('warn', 'No file specified, cannot auto-fix', { 837 task_id: task.id, 838 }); 839 840 // Escalate to developer 841 const devTaskId = await this.createTask({ 842 task_type: 'fix_bug', 843 assigned_to: 'developer', 844 priority: risk_level === 'critical' ? 10 : 8, 845 context: { 846 error_type: 'security', 847 error_message: vulnerability || description, 848 suggested_fix: mitigation || recommendation, 849 security_review: true, 850 }, 851 }); 852 853 await this.completeTask(task.id, { 854 developer_task_id: devTaskId, 855 note: 'Escalated to developer (no file specified)', 856 }); 857 return; 858 } 859 860 // Read the file 861 const { content: fileContent } = await readFile(file); 862 863 // Get security context for this issue type 864 const context = this.getSecurityContext(type); 865 866 // Build finding object for Claude 867 const finding = { 868 type: type || 'security_issue', 869 severity: risk_level || context.severity, 870 line: line || null, 871 description: description || vulnerability, 872 recommendation: recommendation || mitigation || context.fix_template, 873 }; 874 875 await this.log('info', 'Generating secure fix with Claude', { 876 task_id: task.id, 877 file, 878 vulnerability_type: finding.type, 879 }); 880 881 // Generate fix using Claude 882 const fix = await generateSecureFix('security', task.id, { 883 code: fileContent, 884 finding, 885 fileName: file, 886 }); 887 888 await this.log('info', 'Secure fix generated', { 889 task_id: task.id, 890 fix_explanation: fix.explanation, 891 }); 892 893 // Apply the fix 894 const editResult = await editFile(file, { 895 oldContent: fix.old_string, 896 newContent: fix.new_string, 897 }); 898 899 await this.log('info', 'Security fix applied', { 900 task_id: task.id, 901 file, 902 backup: editResult.backupPath, 903 }); 904 905 // Re-scan the file to verify fix 906 await this.log('info', 'Re-scanning file to verify fix', { 907 task_id: task.id, 908 }); 909 910 const { content: updatedContent } = await readFile(file); 911 const verificationResult = await analyzeCodeSecurity( 912 'security', 913 task.id, 914 updatedContent, 915 type, 916 file 917 ); 918 919 const remainingFindings = verificationResult.findings || []; 920 const fixedSuccessfully = remainingFindings.length === 0; 921 922 if (fixedSuccessfully) { 923 await this.log('info', 'Security issue fixed and verified', { 924 task_id: task.id, 925 file, 926 }); 927 } else { 928 await this.log('warn', 'Fix applied but verification found issues', { 929 task_id: task.id, 930 remaining_findings: remainingFindings.length, 931 }); 932 } 933 934 // Run tests (create QA task) 935 const qaTaskId = await this.createTask({ 936 task_type: 'run_tests', 937 assigned_to: 'qa', 938 priority: risk_level === 'critical' ? 10 : 8, 939 context: { 940 files: [file], 941 test_type: 'security', 942 security_context: { 943 vulnerability_type: finding.type, 944 test_guidance: context.test_guidance, 945 fixed_by_security_agent: true, 946 }, 947 }, 948 }); 949 950 await this.log('info', 'QA task created for testing', { 951 task_id: task.id, 952 qa_task_id: qaTaskId, 953 }); 954 955 // Complete task 956 await this.completeTask(task.id, { 957 fixed: true, 958 file, 959 fix_explanation: fix.explanation, 960 testing_notes: fix.testing_notes, 961 backup_path: editResult.backupPath, 962 diff: editResult.diff, 963 verification: { 964 fixed_successfully: fixedSuccessfully, 965 remaining_findings: remainingFindings.length, 966 }, 967 qa_task_id: qaTaskId, 968 }); 969 } catch (error) { 970 await this.log('error', 'Failed to fix security issue', { 971 task_id: task.id, 972 file, 973 error: error.message, 974 }); 975 976 // If auto-fix fails, escalate to developer 977 await this.log('info', 'Escalating to developer after failed auto-fix', { 978 task_id: task.id, 979 }); 980 981 const devTaskId = await this.createTask({ 982 task_type: 'fix_bug', 983 assigned_to: 'developer', 984 priority: risk_level === 'critical' ? 10 : 8, 985 context: { 986 error_type: 'security', 987 error_message: vulnerability || description, 988 file, 989 suggested_fix: mitigation || recommendation, 990 security_review: true, 991 auto_fix_failed: true, 992 auto_fix_error: error.message, 993 }, 994 }); 995 996 await this.completeTask(task.id, { 997 fixed: false, 998 developer_task_id: devTaskId, 999 error: error.message, 1000 note: 'Auto-fix failed, escalated to developer', 1001 }); 1002 } 1003 } 1004 1005 /** 1006 * Review dependency update for security concerns 1007 * 1008 * @param {Object} task - Task with dependency update details 1009 * @returns {Promise<void>} 1010 */ 1011 async reviewDependencyUpdate(task) { 1012 const context = task.context_json || {}; 1013 const { package_name, old_version, new_version } = context; 1014 1015 if (!context || !package_name) { 1016 await this.failTask(task.id, 'Missing required field: package_name in context'); 1017 return; 1018 } 1019 1020 await this.log('info', 'Reviewing dependency update', { 1021 task_id: task.id, 1022 package: package_name, 1023 old_version, 1024 new_version, 1025 }); 1026 1027 try { 1028 // Run npm audit on the specific package 1029 const auditResult = execSync(`npm audit --package-lock-only`, { 1030 encoding: 'utf8', 1031 stdio: 'pipe', 1032 }); 1033 1034 const issues = auditResult.includes(package_name) ? ['Security vulnerabilities found'] : []; 1035 1036 await this.completeTask(task.id, { 1037 approved: issues.length === 0, 1038 issues, 1039 recommendation: 1040 issues.length === 0 ? 'Safe to update' : 'Review vulnerabilities before updating', 1041 }); 1042 } catch (error) { 1043 await this.log('warn', 'Dependency review failed', { 1044 task_id: task.id, 1045 error: error.message, 1046 }); 1047 1048 await this.completeTask(task.id, { 1049 approved: true, // Don't block on audit failures 1050 note: 'Audit failed, manual review recommended', 1051 }); 1052 } 1053 } 1054 1055 /** 1056 * Generate Software Bill of Materials (SBOM) 1057 * 1058 * @param {Object} task - Task object 1059 * @returns {Promise<void>} 1060 */ 1061 async generateSbom(task) { 1062 const context = task.context_json || {}; 1063 const { format = 'cyclonedx' } = context; 1064 1065 await this.log('info', 'Generating SBOM', { 1066 task_id: task.id, 1067 format, 1068 }); 1069 1070 try { 1071 // Use npm sbom command (available in npm 9+) 1072 let sbomOutput; 1073 1074 if (format === 'cyclonedx') { 1075 sbomOutput = execSync('npm sbom --sbom-format=cyclonedx', { 1076 encoding: 'utf8', 1077 timeout: 60000, 1078 }); 1079 } else if (format === 'spdx') { 1080 sbomOutput = execSync('npm sbom --sbom-format=spdx', { 1081 encoding: 'utf8', 1082 timeout: 60000, 1083 }); 1084 } else { 1085 throw new Error(`Unsupported SBOM format: ${format}`); 1086 } 1087 1088 // Parse SBOM to count components 1089 const sbom = JSON.parse(sbomOutput); 1090 const componentCount = sbom.components?.length || 0; 1091 1092 await this.log('info', 'SBOM generated successfully', { 1093 task_id: task.id, 1094 format, 1095 component_count: componentCount, 1096 }); 1097 1098 await this.completeTask(task.id, { 1099 sbom_generated: true, 1100 format, 1101 component_count: componentCount, 1102 sbom_preview: sbomOutput.substring(0, 500), 1103 note: 'SBOM stored in task result for compliance tracking', 1104 }); 1105 } catch (error) { 1106 await this.log('error', 'SBOM generation failed', { 1107 task_id: task.id, 1108 error: error.message, 1109 }); 1110 1111 // Fallback: generate simple dependency list 1112 try { 1113 const pkgJson = JSON.parse(await fs.readFile('package.json', 'utf8')); 1114 const dependencies = { 1115 ...pkgJson.dependencies, 1116 ...pkgJson.devDependencies, 1117 }; 1118 1119 await this.completeTask(task.id, { 1120 sbom_generated: false, 1121 fallback: true, 1122 dependency_count: Object.keys(dependencies).length, 1123 dependencies: Object.keys(dependencies), 1124 note: 'npm sbom failed - using fallback dependency list', 1125 }); 1126 } catch (fallbackError) { 1127 await this.failTask(task.id, `SBOM generation failed: ${error.message}`); 1128 } 1129 } 1130 } 1131 }