/ src / agents / security.js
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  }