Class: RailsCodeAuditor::ReportGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_code_auditor/report_generator.rb

Class Method Summary collapse

Class Method Details

.normalize(results) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/rails_code_auditor/report_generator.rb', line 3

def self.normalize(results)
  {
    brakeman: summarize_or_skip(:brakeman, results) { |res| summarize_brakeman(res[:json]) },
    bundler_audit: summarize_or_skip(:bundler_audit, results) { |res| summarize_bundler(res[:json]) },
    rubocop: summarize_or_skip(:rubocop, results) { |res| summarize_rubocop(res[:json]) },
    rails_best_practices: summarize_or_skip(:rails_best_practices, results) do |res|
      summarize_rails_best_practices(res[:json])
    end,
    flay: summarize_or_skip(:flay, results) { |res| summarize_text_tool("Flay", res[:text]) },
    flog: summarize_or_skip(:flog, results) { |res| summarize_text_tool("Flog", res[:text]) },
    license_finder: summarize_or_skip(:license_finder, results) { |res| summarize_license_finder(res[:json]) },
    reek: summarize_or_skip(:reek, results) { |res| summarize_reek(res[:json]) },
    rubycritic: summarize_or_skip(:rubycritic, results) { |res| summarize_rubycritic(res[:json]) },
    fasterer: summarize_or_skip(:fasterer, results) { |res| summarize_fasterer(res[:text]) }
  }
end

.parse_json_input(input, label: "JSON") ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rails_code_auditor/report_generator.rb', line 46

def self.parse_json_input(input, label: "JSON")
  case input
  when String
    begin
      JSON.parse(input)
    rescue JSON::ParserError => e
      warn "❌ Failed to parse #{label} string: #{e.message}"
      {}
    end
  when Hash
    input
  else
    warn "❌ Unsupported #{label} input type: #{input.class}"
    {}
  end
end

.summarize_brakeman(raw) ⇒ Object



36
37
38
39
40
41
42
43
44
# File 'lib/rails_code_auditor/report_generator.rb', line 36

def self.summarize_brakeman(raw)
  json = parse_json_input(raw, label: "Brakeman")
  warnings = json["warnings"] || []
  summary = warnings.map { |w| "#{w["warning_type"]}: #{w["message"]} in #{w["file"]}" }.join("\n")
  {
    status: "#{warnings.size} security warning#{"s" unless warnings.size == 1}",
    details: summary
  }
end

.summarize_bundler(raw) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rails_code_auditor/report_generator.rb', line 63

def self.summarize_bundler(raw)
  json = begin
    JSON.parse(raw)
  rescue StandardError
    {}
  end
  vulns = json["advisories"] || []
  details = vulns.map { |v| "#{v["gem"]}: #{v["title"]}" }.join("\n")
  {
    status: "#{vulns.size} vulnerability#{"ies" unless vulns.size == 1}",
    details: details
  }
end

.summarize_fasterer(raw) ⇒ Object



167
168
169
170
171
172
173
# File 'lib/rails_code_auditor/report_generator.rb', line 167

def self.summarize_fasterer(raw)
  suggestions = raw.lines.select { |line| line.include?(":") }
  {
    status: "#{suggestions.size} performance suggestion#{"s" unless suggestions.size == 1}",
    details: suggestions.join("\n")
  }
end

.summarize_license_finder(raw) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/rails_code_auditor/report_generator.rb', line 119

def self.summarize_license_finder(raw)
  json = begin
    JSON.parse(raw)
  rescue StandardError
    []
  end
  problematic = json.select { |pkg| pkg["approved"] == false }
  details = problematic.map { |p| "#{p["name"]} - #{p["licenses"].join(", ")}" }.join("\n")
  {
    status: "#{problematic.size} unapproved license#{"s" unless problematic.size == 1}",
    details: details
  }
end

.summarize_or_skip(tool, results) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/rails_code_auditor/report_generator.rb', line 20

def self.summarize_or_skip(tool, results)
  if results[tool]&.dig(:skipped)
    {
      status: "Skipped",
      details: results[tool][:reason] || "Tool not available in this environment"
    }
  elsif results[tool].nil?
    {
      status: "Not Run",
      details: "No data available for #{tool}"
    }
  else
    yield(results[tool])
  end
end

.summarize_rails_best_practices(raw) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/rails_code_auditor/report_generator.rb', line 91

def self.summarize_rails_best_practices(raw)
  issues = begin
    JSON.parse(raw)
  rescue StandardError
    []
  end

  status = "#{issues.size} issue#{"s" unless issues.size == 1}"
  grouped = issues.group_by { |issue| issue["message"] }

  details = grouped.map do |message, group|
    "#{message} (#{group.size}x)"
  end.join("\n")

  {
    status: status,
    details: details
  }
end

.summarize_reek(raw) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/rails_code_auditor/report_generator.rb', line 133

def self.summarize_reek(raw)
  parsed = raw.is_a?(String) ? JSON.parse(raw, symbolize_names: true) : raw

  puts "JSON array but got #{parsed.class}" unless parsed.is_a?(Array)

  total_smells = parsed.size
  sample_smells = parsed.first(10)

  details = sample_smells.map do |smell|
    "#{smell["source"]} [#{smell["lines"].join(", ")}]: #{smell["message"]} (#{smell["smell_type"]})"
  end

  {
    status: "#{total_smells} smell#{"s" unless total_smells == 1}",
    details: details.join("\n") + (total_smells > 10 ? "\n..." : "")
  }
end

.summarize_rubocop(raw) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rails_code_auditor/report_generator.rb', line 77

def self.summarize_rubocop(raw)
  json = begin
    JSON.parse(raw)
  rescue StandardError
    {}
  end
  offenses = json["files"]&.flat_map { |f| f["offenses"] } || []
  details = offenses.map { |o| "#{o["cop_name"]}: #{o["message"]}" }.join("\n")
  {
    status: "#{offenses.size} code offense#{"s" unless offenses.size == 1}",
    details: details
  }
end

.summarize_rubycritic(raw) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/rails_code_auditor/report_generator.rb', line 151

def self.summarize_rubycritic(raw)
  lines = raw.to_s.split("\n").map(&:strip).reject(&:empty?)

  # Extract score
  score_line = lines.find { |line| line.match?(/^Score:\s+\d+(\.\d+)?$/) }
  score = score_line&.match(/Score:\s+([\d.]+)/)&.captures&.first

  # Extract letter-grade issues (lines that start with a grade followed by a dash)
  issues = lines.select { |line| line.match?(/^\b[FABCDE]\b\s+-\s+/) }

  {
    status: score ? "Score: #{score}" : "No score found",
    details: issues.first(10).join("\n") + (issues.size > 10 ? "\n..." : "")
  }
end

.summarize_text_tool(name, raw) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/rails_code_auditor/report_generator.rb', line 111

def self.summarize_text_tool(name, raw)
  lines = raw ? raw.split("\n").reject(&:empty?) : []
  {
    status: "#{lines.size} issue#{"s" unless lines.size == 1}",
    details: lines.first(10).join("\n") + (lines.size > 10 ? "\n..." : "")
  }
end