All files ShellAnalyzer.ts

100% Statements 38/38
100% Branches 9/9
100% Functions 11/11
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162                    1x                                                                                                                                                                             1x 3x     24x 24x 24x         24x 6x 6x 5x 5x   1x     18x     24x       288x       25x 23x 6x 2x 1x               6x     6x       6x   5x 5x 5x 4x     1x     1x      
import { AIProvider, ShellAnalysis, ShellIssue, SeverityLevel } from '../core/interfaces';
 
interface ShellPattern {
  type: ShellIssue['type'];
  pattern: RegExp;
  severity: SeverityLevel;
  description: string;
  suggestion?: string;
}
 
const SHELL_PATTERNS: ShellPattern[] = [
  {
    type: 'destructive',
    pattern: /\brm\s+-[a-z]*f[a-z]*\s+[/~$]/i,
    severity: 'critical',
    description: 'rm -rf on root/home paths can destroy the filesystem.',
    suggestion: 'Specify exact paths and test with --dry-run first.',
  },
  {
    type: 'destructive',
    pattern: /\bdd\s+if=\/dev\/zero|if=\/dev\/random/i,
    severity: 'critical',
    description: 'dd with zero/random input can overwrite entire disks.',
    suggestion: 'Use with extreme caution. Specify exact byte count.',
  },
  {
    type: 'privilege-escalation',
    pattern: /\bsudo\s+(?:su|bash|sh|zsh|fish)\b/i,
    severity: 'critical',
    description: 'sudo shell drops you into a root shell with no audit trail.',
    suggestion: 'Use sudo with specific commands instead of opening a shell.',
  },
  {
    type: 'insecure-pipe',
    pattern: /curl\s+[^|]*\|\s*(?:bash|sh|zsh|python|perl|ruby|node)/i,
    severity: 'critical',
    description: 'Piping remote script directly to shell without inspection.',
    suggestion: 'Download the script first, review it, then execute separately.',
  },
  {
    type: 'insecure-pipe',
    pattern: /wget\s+[^|]*\|\s*(?:bash|sh|zsh|python)/i,
    severity: 'critical',
    description: 'wget piped to shell executes unverified remote code.',
    suggestion: 'Download and inspect before executing: wget url && cat script.sh && bash script.sh',
  },
  {
    type: 'privilege-escalation',
    pattern: /chmod\s+[0-9]*7[0-9][0-9]\s+/,
    severity: 'high',
    description: 'chmod 777 grants world-writable permissions — security risk.',
    suggestion: 'Use minimum required permissions: chmod 755 for executables, 644 for files.',
  },
  {
    type: 'command-injection',
    pattern: /\$\(|`[^`]*\$[A-Z_]+[^`]*`/,
    severity: 'high',
    description: 'Command substitution with environment variables — injection risk if unsanitized.',
    suggestion: 'Quote variables: "$VAR" and validate inputs before use.',
  },
  {
    type: 'network-exfiltration',
    pattern: /(?:curl|wget|nc|netcat)\s+.*\s+(?:\/etc\/passwd|\/etc\/shadow|~\/\.ssh)/i,
    severity: 'critical',
    description: 'Sensitive system files being sent over network.',
    suggestion: 'This is a red flag. Investigate immediately.',
  },
  {
    type: 'destructive',
    pattern: />\s*\/dev\/s[a-z][a-z0-9]|>\s*\/dev\/hd[a-z]/,
    severity: 'critical',
    description: 'Writing to raw disk device can destroy data.',
    suggestion: 'Never write to block devices without explicit backup plan.',
  },
  {
    type: 'privilege-escalation',
    pattern: /crontab\s+-[ei]/i,
    severity: 'medium',
    description: 'Modifying cron jobs can establish persistence.',
    suggestion: 'Review crontab changes carefully for unexpected entries.',
  },
  {
    type: 'network-exfiltration',
    pattern: /\bssh\s+.*-R\s+\d+:/i,
    severity: 'high',
    description: 'Reverse SSH tunnel — can expose internal services externally.',
    suggestion: 'Ensure this tunnel is authorized and monitored.',
  },
  {
    type: 'destructive',
    pattern: /\btruncate\s+-s\s+0|:\s*>\s*(?:\/[a-z]+){2,}/,
    severity: 'high',
    description: 'Truncating files to zero bytes destroys content.',
    suggestion: 'Backup before truncating. Use with caution on system logs.',
  },
];
 
export class ShellAnalyzer {
  constructor(private readonly ai: AIProvider | null) {}
 
  async analyze(command: string): Promise<ShellAnalysis> {
    const rawIssues = this._runHeuristicPatterns(command);
    const issues: ShellIssue[] = rawIssues.map(r => ({ type: r.type, description: r.description, severity: r.severity }));
    const riskLevel = this._computeRisk(issues);
 
    let suggestion: string | undefined;
    let saferAlternative: string | undefined;
 
    if (this.ai && issues.length > 0) {
      try {
        const result = await this._aiAnalyze(command, issues);
        suggestion = result.suggestion;
        saferAlternative = result.saferAlternative;
      } catch {
        suggestion = rawIssues[0]?.suggestion;
      }
    } else {
      suggestion = rawIssues[0]?.suggestion;
    }
 
    return { command, riskLevel, issues, suggestion, saferAlternative };
  }
 
  private _runHeuristicPatterns(command: string): ShellPattern[] {
    return SHELL_PATTERNS.filter(sp => sp.pattern.test(command));
  }
 
  private _computeRisk(issues: ShellIssue[]): SeverityLevel {
    if (!issues.length) return 'info';
    if (issues.some(i => i.severity === 'critical')) return 'critical';
    if (issues.some(i => i.severity === 'high')) return 'high';
    if (issues.some(i => i.severity === 'medium')) return 'medium';
    return 'low';
  }
 
  private async _aiAnalyze(command: string, issues: ShellIssue[]): Promise<{
    suggestion?: string;
    saferAlternative?: string;
  }> {
 
    const prompt = `Analyze this shell command and provide safety guidance:
 
Command: ${command}
Detected issues: ${issues.map(i => i.description).join('; ')}
 
Respond ONLY with JSON: {"suggestion": "short safety tip", "saferAlternative": "safer version of the command or null"}`;
 
    const response = await this.ai!.complete(prompt, { maxTokens: 200, temperature: 0 });
 
    try {
      const jsonMatch = response.match(/\{[\s\S]*\}/);
      if (jsonMatch) {
        return JSON.parse(jsonMatch[0]) as { suggestion?: string; saferAlternative?: string };
      }
    } catch {
      return {};
    }
 
    return {};
  }
}