Class: RailsMcpInsight::Analyzers::SecurityAnalyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_mcp_insight/analyzers/security_analyzer.rb

Overview

Static security analysis for common Rails vulnerabilities. Checks for SQL injection, mass assignment, CSRF, XSS patterns.

Constant Summary collapse

CHECKS =
{
  sql_injection: {
    pattern: /\.where\(\s*["'][^"']*#\{/,
    description: "Possible SQL injection via string interpolation in .where()",
    severity: "high"
  },
  raw_sql: {
    pattern: /\.execute\s*\(|ActiveRecord::Base\.connection\.exec/,
    description: "Raw SQL execution — verify inputs are sanitized",
    severity: "medium"
  },
  mass_assignment: {
    pattern: /\.update_attributes?\s*\(\s*params\b(?!.*permit)/,
    description: "Possible mass assignment — params passed directly without permit",
    severity: "high"
  },
  open_redirect: {
    pattern: /redirect_to\s+params\[/,
    description: "Possible open redirect — redirecting to user-supplied URL",
    severity: "high"
  },
  html_safe: {
    pattern: /\.html_safe\b/,
    description: "Using .html_safe may allow XSS if applied to user input",
    severity: "medium"
  },
  raw_helper: {
    pattern: /\braw\s*\(/,
    description: "raw() bypasses HTML escaping — verify content is safe",
    severity: "medium"
  },
  send_method: {
    pattern: /\.send\s*\(\s*params\b/,
    description: "Dynamic dispatch using params — possible arbitrary method call",
    severity: "high"
  },
  eval_usage: {
    pattern: /\beval\s*\(/,
    description: "eval() usage — extremely dangerous if user input reaches here",
    severity: "critical"
  },
  system_call: {
    pattern: /\bsystem\s*\(|\bexec\s*\(|`[^`]*#\{/,
    description: "System/exec call — verify no user input is injected",
    severity: "high"
  },
  cookie_serialization: {
    pattern: /cookies\.signed|cookies\.encrypted.*Marshal/,
    description: "Cookie serialization — ensure using JSON serializer, not Marshal",
    severity: "medium"
  }
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ SecurityAnalyzer

Returns a new instance of SecurityAnalyzer.



61
62
63
# File 'lib/rails_mcp_insight/analyzers/security_analyzer.rb', line 61

def initialize(config)
  @config = config
end

Instance Method Details

#analyze(focus: nil) ⇒ Object

Run all security checks or a specific focus area



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
# File 'lib/rails_mcp_insight/analyzers/security_analyzer.rb', line 66

def analyze(focus: nil)
  checks_to_run = if focus
                    CHECKS.select { |k, _| k.to_s == focus.to_s }
                  else
                    CHECKS
                  end

  findings = []

  ruby_files = Dir.glob(File.join(@config.app_path, "**", "*.rb"))
  ruby_files += Dir.glob(File.join(@config.lib_path, "**", "*.rb")) if Dir.exist?(@config.lib_path)

  ruby_files.each do |file|
    content = File.read(file)
    lines = content.lines

    checks_to_run.each do |check_name, check|
      lines.each_with_index do |line, idx|
        next if line.strip.start_with?("#")

        next unless line.match?(check[:pattern])

        findings << {
          check: check_name.to_s,
          severity: check[:severity],
          description: check[:description],
          file: relative_path(file),
          line: idx + 1,
          code: line.strip
        }
      end
    end
  end

  {
    total_findings: findings.length,
    by_severity: {
      critical: findings.count { |f| f[:severity] == "critical" },
      high: findings.count { |f| f[:severity] == "high" },
      medium: findings.count { |f| f[:severity] == "medium" }
    },
    findings: findings.sort_by { |f| severity_order(f[:severity]) }
  }
end