rails_code_auditor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/exe/rails_code_auditor +4 -0
- data/lib/rails_code_auditor/analyzer.rb +150 -0
- data/lib/rails_code_auditor/grapher.rb +80 -0
- data/lib/rails_code_auditor/html_to_pdf_converter.rb +62 -0
- data/lib/rails_code_auditor/llm_client.rb +71 -0
- data/lib/rails_code_auditor/pdf_generator.rb +80 -0
- data/lib/rails_code_auditor/report_generator.rb +175 -0
- data/lib/rails_code_auditor/scorer.rb +129 -0
- data/lib/rails_code_auditor/simplecov_runner.rb +82 -0
- data/lib/rails_code_auditor/version.rb +5 -0
- data/lib/rails_code_auditor.rb +31 -0
- metadata +270 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 83cd23ea4027538587a1814cbf2a3472bd818fe0cb15bf492a3ab54b565c2e63
|
4
|
+
data.tar.gz: 40438e254bed8d07966559ac52df8a3063f427167fbc236a8eac0fba6a591c1d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e1cdd901ff82b3f2c2d5f0be65175a03b534a6d3a401c34a9c85861401ea2cf416162a1045fe34b2e8cf0ef79a3a220a1c1e9f87b81fff05d06b973735974227
|
7
|
+
data.tar.gz: 721d41c939a3e00321508a4c36033221e58b6f67a6fd1f0ee9df090e1ae289fafe0b65c94e0f07b7e20c6b3c4bad375463b763f00d440c7a96c29af529e6cde2
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module RailsCodeAuditor
|
4
|
+
class Analyzer
|
5
|
+
REPORT_FOLDER = "report"
|
6
|
+
|
7
|
+
def self.ruby_version
|
8
|
+
Gem::Version.new(RUBY_VERSION)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.rails_version
|
12
|
+
defined?(Rails) ? Gem::Version.new(Rails.version) : nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.run_cmd(command, raw: false)
|
16
|
+
puts "Running: #{command}"
|
17
|
+
output = `#{command}`
|
18
|
+
if output.empty?
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
begin
|
22
|
+
raw ? output : JSON.parse(output)
|
23
|
+
rescue StandardError
|
24
|
+
output
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.ensure_report_folder
|
30
|
+
FileUtils.mkdir_p(REPORT_FOLDER)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.write_html_report(tool_name, content)
|
34
|
+
path = File.join(REPORT_FOLDER, "#{tool_name}.html")
|
35
|
+
File.open(path, "w") do |f|
|
36
|
+
f.puts "<html><head><title>#{tool_name.capitalize} Report</title></head><body><pre>"
|
37
|
+
f.puts "<h1>#{tool_name.capitalize} Report</h1>"
|
38
|
+
f.puts content
|
39
|
+
f.puts "</pre></body></html>"
|
40
|
+
end
|
41
|
+
path
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.generate_brakeman_html
|
45
|
+
run_cmd("brakeman -o #{REPORT_FOLDER}/brakeman.html", raw: true)
|
46
|
+
"#{REPORT_FOLDER}/brakeman.html"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.generate_rails_best_practices_html
|
50
|
+
run_cmd("rails_best_practices -f html --output-file #{REPORT_FOLDER}/rails_best_practices.html", raw: true)
|
51
|
+
"#{REPORT_FOLDER}/rails_best_practices.html"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.generate_rubycritic_html
|
55
|
+
run_cmd("rubycritic --no-browser --path #{REPORT_FOLDER}/rubycritic", raw: true)
|
56
|
+
"#{REPORT_FOLDER}/rubycritic/overview.html"
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.generate_reek_html
|
60
|
+
run_cmd("reek --format html > report/reek.html", raw: true)
|
61
|
+
"#{REPORT_FOLDER}/reek.html"
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.run_all
|
65
|
+
ensure_report_folder
|
66
|
+
|
67
|
+
results = {}
|
68
|
+
|
69
|
+
results[:brakeman] = {
|
70
|
+
json: run_cmd("brakeman -f json --no-exit-on-error"),
|
71
|
+
html_path: run_cmd("brakeman -o #{REPORT_FOLDER}/brakeman.html", raw: true)
|
72
|
+
}
|
73
|
+
|
74
|
+
results[:bundler_audit] = {
|
75
|
+
json: run_cmd("bundle audit check --verbose"),
|
76
|
+
html_path: write_html_report("bundler_audit", run_cmd("bundle audit check --verbose"))
|
77
|
+
}
|
78
|
+
|
79
|
+
results[:rubocop] = if ruby_version >= Gem::Version.new("2.7")
|
80
|
+
{
|
81
|
+
json: run_cmd("rubocop --format json"),
|
82
|
+
html_path: write_html_report("rubocop", run_cmd("rubocop --format simple"))
|
83
|
+
}
|
84
|
+
else
|
85
|
+
{ skipped: true, reason: "Rubocop requires Ruby >= 2.7" }
|
86
|
+
end
|
87
|
+
|
88
|
+
results[:rails_best_practices] = {
|
89
|
+
json: run_cmd("rails_best_practices --format json"),
|
90
|
+
html_path: run_cmd("rails_best_practices -f html --output-file #{REPORT_FOLDER}/rails_best_practices.html",
|
91
|
+
raw: true)
|
92
|
+
}
|
93
|
+
|
94
|
+
results[:flay] = {
|
95
|
+
text: run_cmd("flay --mass 50 ."),
|
96
|
+
html_path: write_html_report("flay", run_cmd("flay --mass 50 ."))
|
97
|
+
}
|
98
|
+
|
99
|
+
results[:flog] = {
|
100
|
+
text: run_cmd("flog ."),
|
101
|
+
html_path: write_html_report("flog", run_cmd("flog ."))
|
102
|
+
}
|
103
|
+
|
104
|
+
results[:license_finder] = if ruby_version >= Gem::Version.new("2.7")
|
105
|
+
{
|
106
|
+
json: run_cmd("license_finder --format json"),
|
107
|
+
html_path: write_html_report("license_finder",
|
108
|
+
run_cmd("license_finder --format text"))
|
109
|
+
}
|
110
|
+
else
|
111
|
+
{ skipped: true, reason: "LicenseFinder requires Ruby >= 2.7" }
|
112
|
+
end
|
113
|
+
|
114
|
+
results[:reek] = {
|
115
|
+
json: run_cmd("reek --format json"),
|
116
|
+
html_path: run_cmd("reek --format html > #{REPORT_FOLDER}/reek.html", raw: true)
|
117
|
+
}
|
118
|
+
|
119
|
+
begin
|
120
|
+
Timeout.timeout(300) do
|
121
|
+
results[:rubycritic] = if ruby_version >= Gem::Version.new("2.7")
|
122
|
+
{
|
123
|
+
json: run_cmd("rubycritic --format json"),
|
124
|
+
html_path: run_cmd("rubycritic --no-browser --path #{REPORT_FOLDER}/rubycritic",
|
125
|
+
raw: true)
|
126
|
+
}
|
127
|
+
else
|
128
|
+
{ skipped: true, reason: "RubyCritic requires Ruby >= 2.7" }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
rescue Timeout::Error
|
132
|
+
results[:rubycritic] = { error: "RubyCritic timed out after 5 minutes" }
|
133
|
+
end
|
134
|
+
|
135
|
+
results[:fasterer] = {
|
136
|
+
text: run_cmd("fasterer ."),
|
137
|
+
html_path: write_html_report("fasterer", run_cmd("fasterer ."))
|
138
|
+
}
|
139
|
+
|
140
|
+
# Optional: tools only if Rails >= 5
|
141
|
+
if rails_version && rails_version >= Gem::Version.new("5.0")
|
142
|
+
results[:grover] = {
|
143
|
+
html_path: write_html_report("grover", "Grover logic here (if you use it)")
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
results
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "gruff"
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
|
4
|
+
module RailsCodeAuditor
|
5
|
+
class Grapher
|
6
|
+
REPORT_PATH = "./report".freeze
|
7
|
+
|
8
|
+
def self.generate(results)
|
9
|
+
results = results.with_indifferent_access
|
10
|
+
Dir.mkdir(REPORT_PATH) unless Dir.exist?(REPORT_PATH)
|
11
|
+
|
12
|
+
graphs = []
|
13
|
+
|
14
|
+
summary = {
|
15
|
+
"Security" => results[:security],
|
16
|
+
"Code Quality" => results[:code_quality],
|
17
|
+
"Dependencies" => results[:dependencies],
|
18
|
+
"Test Coverage" => results[:test_coverage],
|
19
|
+
"Overall" => results[:overall]
|
20
|
+
}.compact
|
21
|
+
|
22
|
+
graphs << graph_bar("Audit Scores", summary)
|
23
|
+
|
24
|
+
summary.each do |label, data|
|
25
|
+
graphs << graph_pie(label, data[:score])
|
26
|
+
end
|
27
|
+
|
28
|
+
graphs
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.graph_bar(title, metrics)
|
32
|
+
g = Gruff::Bar.new
|
33
|
+
g.title = title
|
34
|
+
|
35
|
+
labels = {}
|
36
|
+
metrics.each_with_index do |(label, data), index|
|
37
|
+
score = data[:score] || 0
|
38
|
+
labels[index] = label
|
39
|
+
g.data(label, [score], bar_color(score))
|
40
|
+
end
|
41
|
+
|
42
|
+
g.labels = labels
|
43
|
+
|
44
|
+
file_name = "#{title.downcase.gsub(" ", "_")}.png"
|
45
|
+
path = File.join(REPORT_PATH, file_name)
|
46
|
+
g.write(path)
|
47
|
+
|
48
|
+
puts "Generated graph at #{path}"
|
49
|
+
|
50
|
+
{ title: title, path: path }
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.graph_pie(title, score)
|
54
|
+
score = score || 0
|
55
|
+
remaining = 100 - score
|
56
|
+
|
57
|
+
g = Gruff::Pie.new
|
58
|
+
g.title = title
|
59
|
+
g.data("Score", score, bar_color(score))
|
60
|
+
g.data("Remaining", remaining, "#dddddd")
|
61
|
+
|
62
|
+
file_name = "#{title.downcase.gsub(" ", "_")}_pie.png"
|
63
|
+
path = File.join(REPORT_PATH, file_name)
|
64
|
+
g.write(path)
|
65
|
+
|
66
|
+
puts "Generated pie chart: #{path}"
|
67
|
+
|
68
|
+
{ title: title, path: path }
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.bar_color(score)
|
72
|
+
case score
|
73
|
+
when 0..49 then '#e74c3c' # red
|
74
|
+
when 50..74 then '#f1c40f' # yellow
|
75
|
+
when 75..89 then '#3498db' # blue
|
76
|
+
else '#2ecc71' # green
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "grover"
|
3
|
+
|
4
|
+
module RailsCodeAuditor
|
5
|
+
class HtmlToPdfConverter
|
6
|
+
INPUT_PATHS = ["report", "report/rubycritic"]
|
7
|
+
OUTPUT_PATH = "report/pdf"
|
8
|
+
|
9
|
+
def self.puppeteer_installed?
|
10
|
+
system("npx puppeteer --version > /dev/null 2>&1")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.convert_all
|
14
|
+
unless puppeteer_installed?
|
15
|
+
puts "[!] Puppeteer is not installed. Please run: yarn add puppeteer"
|
16
|
+
return []
|
17
|
+
end
|
18
|
+
|
19
|
+
FileUtils.mkdir_p(OUTPUT_PATH)
|
20
|
+
pdf_paths = []
|
21
|
+
|
22
|
+
html_files = INPUT_PATHS.flat_map { |path| Dir["#{path}/*.html"] }
|
23
|
+
|
24
|
+
if html_files.empty?
|
25
|
+
puts "[!] No HTML files found in #{INPUT_PATHS.join(', ')}"
|
26
|
+
return []
|
27
|
+
end
|
28
|
+
|
29
|
+
html_files.each do |html_path|
|
30
|
+
begin
|
31
|
+
relative_name = html_path.sub(%r{^.*?report/}, "").gsub("/", "_")
|
32
|
+
pdf_filename = relative_name.sub(/\.html$/, ".pdf")
|
33
|
+
pdf_output_path = File.join(OUTPUT_PATH, pdf_filename)
|
34
|
+
|
35
|
+
html_content = File.read(html_path)
|
36
|
+
|
37
|
+
grover = Grover.new(
|
38
|
+
html_content,
|
39
|
+
print_background: true,
|
40
|
+
prefer_css_page_size: true,
|
41
|
+
wait_until: 'networkidle0',
|
42
|
+
format: 'A4',
|
43
|
+
margin: { top: '1cm', bottom: '1cm' }
|
44
|
+
)
|
45
|
+
|
46
|
+
File.write(pdf_output_path, grover.to_pdf)
|
47
|
+
pdf_paths << pdf_output_path
|
48
|
+
puts "[✓] PDF generated: #{pdf_output_path}"
|
49
|
+
|
50
|
+
rescue Grover::DependencyError => e
|
51
|
+
puts "[!] Puppeteer is required but not available. Skipping: #{html_path}"
|
52
|
+
next
|
53
|
+
rescue => e
|
54
|
+
puts "[!] Failed to convert #{html_path}: #{e.class} - #{e.message}"
|
55
|
+
next
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
pdf_paths
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "json"
|
2
|
+
require "net/http"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module RailsCodeAuditor
|
6
|
+
class LlmClient
|
7
|
+
def self.sanitize_results(results)
|
8
|
+
results.transform_values do |tool|
|
9
|
+
tool.dup.tap do |entry|
|
10
|
+
if entry.is_a?(Hash) && entry[:details].is_a?(String)
|
11
|
+
entry[:details] = entry[:details].slice(0, 500) # Trim details to avoid LLM overload
|
12
|
+
entry[:details].gsub!(/\e\[[\d;]*m/, '') # Remove ANSI color codes
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
def self.score_with_llm(json_results)
|
18
|
+
sanitized = sanitize_results(json_results)
|
19
|
+
puts "[*] Scoring with LLM (LLaMA3)..."
|
20
|
+
|
21
|
+
prompt = <<~PROMPT
|
22
|
+
Analyze the following Rails code audit summary and return a JSON object like:
|
23
|
+
{
|
24
|
+
"security": 75,
|
25
|
+
"code_quality": 80,
|
26
|
+
"test_coverage": 60,
|
27
|
+
"dependencies": 90,
|
28
|
+
"overall": 76
|
29
|
+
}
|
30
|
+
|
31
|
+
Only return the raw JSON, nothing else.
|
32
|
+
|
33
|
+
Input:
|
34
|
+
#{JSON.pretty_generate(sanitized)}
|
35
|
+
PROMPT
|
36
|
+
|
37
|
+
uri = URI("http://localhost:11434/api/generate")
|
38
|
+
body = {
|
39
|
+
model: "llama3",
|
40
|
+
prompt: prompt,
|
41
|
+
stream: false
|
42
|
+
}
|
43
|
+
|
44
|
+
response = Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json")
|
45
|
+
response_body = JSON.parse(response.body)
|
46
|
+
|
47
|
+
raw_output = response_body["response"].strip
|
48
|
+
# Try to extract JSON from any surrounding text
|
49
|
+
json_match = raw_output.match(/\{.*\}/m)
|
50
|
+
|
51
|
+
if json_match
|
52
|
+
parsed_scores = JSON.parse(json_match[0])
|
53
|
+
scored_results = parsed_scores.transform_values do |score|
|
54
|
+
remark = case score
|
55
|
+
when 90..100 then "Excellent"
|
56
|
+
when 75..89 then "Good"
|
57
|
+
when 60..74 then "Average"
|
58
|
+
else "Needs Improvement"
|
59
|
+
end
|
60
|
+
{ score: score, remark: remark }
|
61
|
+
end
|
62
|
+
scored_results
|
63
|
+
else
|
64
|
+
raise "Response did not contain valid JSON"
|
65
|
+
end
|
66
|
+
rescue => e
|
67
|
+
puts "[!] LLM scoring failed: #{e.message}"
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "prawn"
|
2
|
+
require "prawn/table"
|
3
|
+
require "fileutils"
|
4
|
+
require "combine_pdf"
|
5
|
+
require "active_support/core_ext/string/inflections"
|
6
|
+
|
7
|
+
module RailsCodeAuditor
|
8
|
+
class PdfGenerator
|
9
|
+
OUTPUT_PATH = "code_audit_report.pdf"
|
10
|
+
TEMP_PRWAN_PDF = "tmp/main_audit.pdf"
|
11
|
+
|
12
|
+
def self.generate(results, scores, graphs, html_pdf_paths = [])
|
13
|
+
FileUtils.mkdir_p("tmp")
|
14
|
+
FileUtils.mkdir_p("report/pdf")
|
15
|
+
|
16
|
+
# Generate main Prawn PDF
|
17
|
+
Prawn::Document.generate(TEMP_PRWAN_PDF) do |pdf|
|
18
|
+
pdf.text "Rails Code Audit Report", size: 24, style: :bold, align: :center
|
19
|
+
pdf.move_down 20
|
20
|
+
|
21
|
+
# Summary Scores Table
|
22
|
+
pdf.text "Audit Summary Scores", size: 16, style: :bold
|
23
|
+
pdf.move_down 10
|
24
|
+
summary_data = [["Metric", "Score (0-100)", "Remarks"]]
|
25
|
+
scores.each do |metric, value|
|
26
|
+
summary_data << [metric.to_s.humanize, value[:score], value[:remark]]
|
27
|
+
end
|
28
|
+
pdf.table(summary_data, header: true, width: pdf.bounds.width)
|
29
|
+
pdf.move_down 20
|
30
|
+
|
31
|
+
# Detailed Audit Results
|
32
|
+
pdf.text "Detailed Audit Results", size: 16, style: :bold
|
33
|
+
pdf.move_down 10
|
34
|
+
results.each do |check_name, result|
|
35
|
+
pdf.text check_name.to_s.humanize, size: 12, style: :bold
|
36
|
+
pdf.text "Status: #{result[:status]}"
|
37
|
+
pdf.text "Details: #{result[:details]}"
|
38
|
+
pdf.move_down 10
|
39
|
+
end
|
40
|
+
|
41
|
+
# Graphs (optional)
|
42
|
+
if graphs && graphs.any?
|
43
|
+
pdf.start_new_page
|
44
|
+
pdf.text "Visual Graphs", size: 16, style: :bold
|
45
|
+
pdf.move_down 10
|
46
|
+
|
47
|
+
graphs.each do |graph|
|
48
|
+
if File.exist?(graph[:path])
|
49
|
+
pdf.text graph[:title], size: 12, style: :bold
|
50
|
+
pdf.image graph[:path], fit: [500, 300]
|
51
|
+
pdf.move_down 20
|
52
|
+
else
|
53
|
+
puts "[!] Graph file missing: #{graph[:path]}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
pdf.number_pages "Page <page> of <total>", at: [pdf.bounds.right - 150, 0]
|
59
|
+
end
|
60
|
+
|
61
|
+
puts "[✓] Main audit PDF saved to #{TEMP_PRWAN_PDF}"
|
62
|
+
|
63
|
+
# Merge all PDFs (main + html converted)
|
64
|
+
combined_pdf = CombinePDF.new
|
65
|
+
combined_pdf << CombinePDF.load(TEMP_PRWAN_PDF)
|
66
|
+
|
67
|
+
html_pdf_paths.each do |pdf_path|
|
68
|
+
if File.exist?(pdf_path)
|
69
|
+
puts "[+] Merging #{pdf_path}"
|
70
|
+
combined_pdf << CombinePDF.load(pdf_path)
|
71
|
+
else
|
72
|
+
puts "[!] Skipped missing HTML-generated PDF: #{pdf_path}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
combined_pdf.save(OUTPUT_PATH)
|
77
|
+
puts "[✓] Final merged PDF saved to #{OUTPUT_PATH}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module RailsCodeAuditor
|
2
|
+
class ReportGenerator
|
3
|
+
def self.normalize(results)
|
4
|
+
{
|
5
|
+
brakeman: summarize_or_skip(:brakeman, results) { |res| summarize_brakeman(res[:json]) },
|
6
|
+
bundler_audit: summarize_or_skip(:bundler_audit, results) { |res| summarize_bundler(res[:json]) },
|
7
|
+
rubocop: summarize_or_skip(:rubocop, results) { |res| summarize_rubocop(res[:json]) },
|
8
|
+
rails_best_practices: summarize_or_skip(:rails_best_practices, results) do |res|
|
9
|
+
summarize_rails_best_practices(res[:json])
|
10
|
+
end,
|
11
|
+
flay: summarize_or_skip(:flay, results) { |res| summarize_text_tool("Flay", res[:text]) },
|
12
|
+
flog: summarize_or_skip(:flog, results) { |res| summarize_text_tool("Flog", res[:text]) },
|
13
|
+
license_finder: summarize_or_skip(:license_finder, results) { |res| summarize_license_finder(res[:json]) },
|
14
|
+
reek: summarize_or_skip(:reek, results) { |res| summarize_reek(res[:json]) },
|
15
|
+
rubycritic: summarize_or_skip(:rubycritic, results) { |res| summarize_rubycritic(res[:json]) },
|
16
|
+
fasterer: summarize_or_skip(:fasterer, results) { |res| summarize_fasterer(res[:text]) }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.summarize_or_skip(tool, results)
|
21
|
+
if results[tool]&.dig(:skipped)
|
22
|
+
{
|
23
|
+
status: "Skipped",
|
24
|
+
details: results[tool][:reason] || "Tool not available in this environment"
|
25
|
+
}
|
26
|
+
elsif results[tool].nil?
|
27
|
+
{
|
28
|
+
status: "Not Run",
|
29
|
+
details: "No data available for #{tool}"
|
30
|
+
}
|
31
|
+
else
|
32
|
+
yield(results[tool])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.summarize_brakeman(raw)
|
37
|
+
json = parse_json_input(raw, label: "Brakeman")
|
38
|
+
warnings = json["warnings"] || []
|
39
|
+
summary = warnings.map { |w| "#{w["warning_type"]}: #{w["message"]} in #{w["file"]}" }.join("\n")
|
40
|
+
{
|
41
|
+
status: "#{warnings.size} security warning#{"s" unless warnings.size == 1}",
|
42
|
+
details: summary
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.parse_json_input(input, label: "JSON")
|
47
|
+
case input
|
48
|
+
when String
|
49
|
+
begin
|
50
|
+
JSON.parse(input)
|
51
|
+
rescue JSON::ParserError => e
|
52
|
+
warn "❌ Failed to parse #{label} string: #{e.message}"
|
53
|
+
{}
|
54
|
+
end
|
55
|
+
when Hash
|
56
|
+
input
|
57
|
+
else
|
58
|
+
warn "❌ Unsupported #{label} input type: #{input.class}"
|
59
|
+
{}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.summarize_bundler(raw)
|
64
|
+
json = begin
|
65
|
+
JSON.parse(raw)
|
66
|
+
rescue StandardError
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
vulns = json["advisories"] || []
|
70
|
+
details = vulns.map { |v| "#{v["gem"]}: #{v["title"]}" }.join("\n")
|
71
|
+
{
|
72
|
+
status: "#{vulns.size} vulnerability#{"ies" unless vulns.size == 1}",
|
73
|
+
details: details
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.summarize_rubocop(raw)
|
78
|
+
json = begin
|
79
|
+
JSON.parse(raw)
|
80
|
+
rescue StandardError
|
81
|
+
{}
|
82
|
+
end
|
83
|
+
offenses = json["files"]&.flat_map { |f| f["offenses"] } || []
|
84
|
+
details = offenses.map { |o| "#{o["cop_name"]}: #{o["message"]}" }.join("\n")
|
85
|
+
{
|
86
|
+
status: "#{offenses.size} code offense#{"s" unless offenses.size == 1}",
|
87
|
+
details: details
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.summarize_rails_best_practices(raw)
|
92
|
+
issues = begin
|
93
|
+
JSON.parse(raw)
|
94
|
+
rescue StandardError
|
95
|
+
[]
|
96
|
+
end
|
97
|
+
|
98
|
+
status = "#{issues.size} issue#{"s" unless issues.size == 1}"
|
99
|
+
grouped = issues.group_by { |issue| issue["message"] }
|
100
|
+
|
101
|
+
details = grouped.map do |message, group|
|
102
|
+
"#{message} (#{group.size}x)"
|
103
|
+
end.join("\n")
|
104
|
+
|
105
|
+
{
|
106
|
+
status: status,
|
107
|
+
details: details
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.summarize_text_tool(name, raw)
|
112
|
+
lines = raw ? raw.split("\n").reject(&:empty?) : []
|
113
|
+
{
|
114
|
+
status: "#{lines.size} issue#{"s" unless lines.size == 1}",
|
115
|
+
details: lines.first(10).join("\n") + (lines.size > 10 ? "\n..." : "")
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.summarize_license_finder(raw)
|
120
|
+
json = begin
|
121
|
+
JSON.parse(raw)
|
122
|
+
rescue StandardError
|
123
|
+
[]
|
124
|
+
end
|
125
|
+
problematic = json.select { |pkg| pkg["approved"] == false }
|
126
|
+
details = problematic.map { |p| "#{p["name"]} - #{p["licenses"].join(", ")}" }.join("\n")
|
127
|
+
{
|
128
|
+
status: "#{problematic.size} unapproved license#{"s" unless problematic.size == 1}",
|
129
|
+
details: details
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.summarize_reek(raw)
|
134
|
+
parsed = raw.is_a?(String) ? JSON.parse(raw, symbolize_names: true) : raw
|
135
|
+
|
136
|
+
puts "JSON array but got #{parsed.class}" unless parsed.is_a?(Array)
|
137
|
+
|
138
|
+
total_smells = parsed.size
|
139
|
+
sample_smells = parsed.first(10)
|
140
|
+
|
141
|
+
details = sample_smells.map do |smell|
|
142
|
+
"#{smell["source"]} [#{smell["lines"].join(", ")}]: #{smell["message"]} (#{smell["smell_type"]})"
|
143
|
+
end
|
144
|
+
|
145
|
+
{
|
146
|
+
status: "#{total_smells} smell#{"s" unless total_smells == 1}",
|
147
|
+
details: details.join("\n") + (total_smells > 10 ? "\n..." : "")
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.summarize_rubycritic(raw)
|
152
|
+
lines = raw.to_s.split("\n").map(&:strip).reject(&:empty?)
|
153
|
+
|
154
|
+
# Extract score
|
155
|
+
score_line = lines.find { |line| line.match?(/^Score:\s+\d+(\.\d+)?$/) }
|
156
|
+
score = score_line&.match(/Score:\s+([\d.]+)/)&.captures&.first
|
157
|
+
|
158
|
+
# Extract letter-grade issues (lines that start with a grade followed by a dash)
|
159
|
+
issues = lines.select { |line| line.match?(/^\b[FABCDE]\b\s+-\s+/) }
|
160
|
+
|
161
|
+
{
|
162
|
+
status: score ? "Score: #{score}" : "No score found",
|
163
|
+
details: issues.first(10).join("\n") + (issues.size > 10 ? "\n..." : "")
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.summarize_fasterer(raw)
|
168
|
+
suggestions = raw.lines.select { |line| line.include?(":") }
|
169
|
+
{
|
170
|
+
status: "#{suggestions.size} performance suggestion#{"s" unless suggestions.size == 1}",
|
171
|
+
details: suggestions.join("\n")
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module RailsCodeAuditor
|
2
|
+
class Scorer
|
3
|
+
def self.score(results)
|
4
|
+
scores = {
|
5
|
+
security: {
|
6
|
+
score: security_score(results),
|
7
|
+
remark: remark_for(security_score(results))
|
8
|
+
},
|
9
|
+
code_quality: {
|
10
|
+
score: code_quality_score(results),
|
11
|
+
remark: remark_for(code_quality_score(results))
|
12
|
+
},
|
13
|
+
dependencies: {
|
14
|
+
score: dependency_score(results),
|
15
|
+
remark: remark_for(dependency_score(results))
|
16
|
+
},
|
17
|
+
test_coverage: {
|
18
|
+
score: test_coverage_score(results),
|
19
|
+
remark: remark_for(test_coverage_score(results))
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
overall = overall_score(scores)
|
24
|
+
scores[:overall] = {
|
25
|
+
score: overall,
|
26
|
+
remark: remark_for(overall)
|
27
|
+
}
|
28
|
+
|
29
|
+
scores
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.remark_for(score)
|
33
|
+
case score
|
34
|
+
when 90..100 then "Excellent"
|
35
|
+
when 75..89 then "Good"
|
36
|
+
when 60..74 then "Fair"
|
37
|
+
else "Needs Improvement"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.security_score(results)
|
42
|
+
tool_scores = [
|
43
|
+
extract_issue_count(results.dig(:brakeman, :status)),
|
44
|
+
extract_issue_count(results.dig(:bundler_audit, :status))
|
45
|
+
].compact
|
46
|
+
|
47
|
+
total = tool_scores.sum
|
48
|
+
active_tools = tool_scores.size
|
49
|
+
calculate_score(total, active_tools)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.code_quality_score(results)
|
53
|
+
issue_counts = [
|
54
|
+
extract_issue_count(results.dig(:rubocop, :status)),
|
55
|
+
extract_issue_count(results.dig(:rails_best_practices, :status)),
|
56
|
+
extract_issue_count(results.dig(:reek, :status)),
|
57
|
+
extract_issue_count(results.dig(:flay, :status)),
|
58
|
+
extract_issue_count(results.dig(:flog, :status)),
|
59
|
+
extract_issue_count(results.dig(:fasterer, :status))
|
60
|
+
].compact
|
61
|
+
|
62
|
+
total_issues = issue_counts.sum
|
63
|
+
active_tool_count = issue_counts.size
|
64
|
+
issue_score = calculate_score(total_issues, active_tool_count)
|
65
|
+
|
66
|
+
# Handle RubyCritic separately
|
67
|
+
rubycritic_score = extract_rubycritic_score(results.dig(:rubycritic, :status))
|
68
|
+
|
69
|
+
if rubycritic_score
|
70
|
+
((issue_score + rubycritic_score) / 2.0).round
|
71
|
+
else
|
72
|
+
issue_score
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.extract_rubycritic_score(status)
|
77
|
+
return nil unless status.is_a?(String)
|
78
|
+
return nil if status.downcase.include?("skipped") || status.downcase.include?("not run")
|
79
|
+
|
80
|
+
return unless match = status.match(/Score:\s*([0-9.]+)/)
|
81
|
+
|
82
|
+
match[1].to_f.round
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.dependency_score(results)
|
86
|
+
count = extract_issue_count(results.dig(:license_finder, :status))
|
87
|
+
return 100 if count.nil? # Tool skipped
|
88
|
+
|
89
|
+
calculate_score(count, 1)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.test_coverage_score(results)
|
93
|
+
status = results.dig(:simplecov, :status)
|
94
|
+
return 100 if !status.is_a?(String) || status.downcase.include?("skipped") || status.downcase.include?("not run")
|
95
|
+
|
96
|
+
if status.match(/Coverage:\s*([\d.]+)/)
|
97
|
+
::Regexp.last_match(1).to_f.round
|
98
|
+
else
|
99
|
+
0
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.extract_issue_count(status)
|
104
|
+
return nil unless status.is_a?(String)
|
105
|
+
return nil if status.downcase.include?("skipped") || status.downcase.include?("not run")
|
106
|
+
|
107
|
+
if match = status.match(/(\d+)/)
|
108
|
+
match[1].to_i
|
109
|
+
else
|
110
|
+
0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.calculate_score(issue_count, active_tool_count)
|
115
|
+
return 100 if issue_count == 0
|
116
|
+
return 0 if active_tool_count == 0
|
117
|
+
|
118
|
+
score = 100 - (issue_count.to_f / (active_tool_count * 10)) * 10
|
119
|
+
[[score.round, 0].max, 100].min
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.overall_score(scores_hash)
|
123
|
+
category_scores = scores_hash.values.map { |v| v[:score] }.compact
|
124
|
+
return 0 if category_scores.empty?
|
125
|
+
|
126
|
+
(category_scores.sum / category_scores.size.to_f).round
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "json"
|
2
|
+
require "bundler"
|
3
|
+
|
4
|
+
module RailsCodeAuditor
|
5
|
+
class SimpleCovRunner
|
6
|
+
def self.run
|
7
|
+
setup_file = ".simplecov_setup.rb"
|
8
|
+
|
9
|
+
unless simplecov_installed_in_project?
|
10
|
+
return {
|
11
|
+
status: "Skipped",
|
12
|
+
success: false,
|
13
|
+
error: "simplecov gem not found in the target Rails app",
|
14
|
+
details: "simplecov gem not found in the target Rails app"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
File.write(setup_file, <<~RUBY)
|
19
|
+
require 'simplecov'
|
20
|
+
|
21
|
+
SimpleCov.start 'rails' do
|
22
|
+
enable_coverage :branch
|
23
|
+
add_filter '/test/'
|
24
|
+
add_filter '/spec/'
|
25
|
+
end
|
26
|
+
|
27
|
+
SimpleCov.command_name ENV.fetch("SIMPLECOV_COMMAND_NAME", "Rails Tests")
|
28
|
+
puts "[SimpleCov] started"
|
29
|
+
RUBY
|
30
|
+
|
31
|
+
test_cmd =
|
32
|
+
if Dir.exist?("spec")
|
33
|
+
"SIMPLECOV_COMMAND_NAME='RSpec Tests' bundle exec ruby -r./#{setup_file} -S rspec"
|
34
|
+
elsif Dir.exist?("test")
|
35
|
+
"SIMPLECOV_COMMAND_NAME='Rails Tests' bundle exec ruby -r./#{setup_file} -S rails test"
|
36
|
+
else
|
37
|
+
return { status: "Coverage: 0%", error: "No test directory found", success: false,
|
38
|
+
details: "No test directory found" }
|
39
|
+
end
|
40
|
+
|
41
|
+
success = system(test_cmd)
|
42
|
+
|
43
|
+
coverage_path = "coverage/.last_run.json"
|
44
|
+
if File.exist?(coverage_path)
|
45
|
+
json = JSON.parse(File.read(coverage_path))
|
46
|
+
percent = json["result"]["covered_percent"] || json["result"]["line"]
|
47
|
+
if percent
|
48
|
+
percent = percent.round(2)
|
49
|
+
{
|
50
|
+
status: "Coverage: #{percent}%",
|
51
|
+
success: success,
|
52
|
+
raw_result: json,
|
53
|
+
details: json
|
54
|
+
}
|
55
|
+
else
|
56
|
+
{
|
57
|
+
status: "Coverage: N/A",
|
58
|
+
success: false,
|
59
|
+
error: "Coverage percentage not found in report",
|
60
|
+
details: json
|
61
|
+
}
|
62
|
+
end
|
63
|
+
else
|
64
|
+
{
|
65
|
+
status: "Coverage: 0%",
|
66
|
+
success: success,
|
67
|
+
error: "Coverage file not generated",
|
68
|
+
details: "Coverage file not generated"
|
69
|
+
}
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
File.delete(setup_file) if setup_file && File.exist?(setup_file)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.simplecov_installed_in_project?
|
76
|
+
spec = Bundler.locked_gems.specs.find { |s| s.name == "simplecov" }
|
77
|
+
!spec.nil?
|
78
|
+
rescue StandardError
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "rails_code_auditor/analyzer"
|
2
|
+
require "rails_code_auditor/report_generator"
|
3
|
+
require "rails_code_auditor/pdf_generator"
|
4
|
+
require "rails_code_auditor/grapher"
|
5
|
+
require "rails_code_auditor/scorer"
|
6
|
+
require "rails_code_auditor/llm_client"
|
7
|
+
require "rails_code_auditor/simplecov_runner"
|
8
|
+
|
9
|
+
rails_version = defined?(Rails) ? Gem::Version.new(Rails::VERSION::STRING) : nil
|
10
|
+
USE_GROVER = rails_version.nil? || rails_version >= Gem::Version.new("5.0")
|
11
|
+
|
12
|
+
require "rails_code_auditor/html_to_pdf_converter" if USE_GROVER
|
13
|
+
|
14
|
+
module RailsCodeAuditor
|
15
|
+
def self.run(args)
|
16
|
+
puts "[*] Running Rails Code Auditor..."
|
17
|
+
|
18
|
+
raw_results = Analyzer.run_all
|
19
|
+
results = ReportGenerator.normalize(raw_results)
|
20
|
+
results[:simplecov] = SimpleCovRunner.run
|
21
|
+
scores = if args.include?("--use-llm")
|
22
|
+
LlmClient.score_with_llm(results) || Scorer.score(results)
|
23
|
+
else
|
24
|
+
Scorer.score(results)
|
25
|
+
end
|
26
|
+
graphs = Grapher.generate(scores)
|
27
|
+
html_pdf_paths = USE_GROVER ? HtmlToPdfConverter.convert_all : []
|
28
|
+
PdfGenerator.generate(results, scores, graphs, html_pdf_paths)
|
29
|
+
puts "[✓] Audit complete. PDF report generated."
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_code_auditor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- sivamanikandan
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-07-24 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: brakeman
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '6.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '6.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: bundler-audit
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.9'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.9'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: combine_pdf
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: fasterer
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.7'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.7'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: flay
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.13.3
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 2.13.3
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: flog
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '4.8'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '4.8'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: gruff
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.21'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.21'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: prawn
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.4'
|
117
|
+
type: :runtime
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '2.4'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: prawn-table
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 0.2.2
|
131
|
+
type: :runtime
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 0.2.2
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: rails_best_practices
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '1.22'
|
145
|
+
type: :runtime
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '1.22'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: simplecov
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0.22'
|
159
|
+
type: :runtime
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0.22'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: license_finder
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - "~>"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '7.0'
|
173
|
+
type: :runtime
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - "~>"
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '7.0'
|
180
|
+
- !ruby/object:Gem::Dependency
|
181
|
+
name: rubocop
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - "~>"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '1.60'
|
187
|
+
type: :runtime
|
188
|
+
prerelease: false
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - "~>"
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '1.60'
|
194
|
+
- !ruby/object:Gem::Dependency
|
195
|
+
name: rubycritic
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - "~>"
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: 4.9.2
|
201
|
+
type: :runtime
|
202
|
+
prerelease: false
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - "~>"
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: 4.9.2
|
208
|
+
- !ruby/object:Gem::Dependency
|
209
|
+
name: grover
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
211
|
+
requirements:
|
212
|
+
- - ">="
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: '0'
|
215
|
+
type: :runtime
|
216
|
+
prerelease: false
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
218
|
+
requirements:
|
219
|
+
- - ">="
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: '0'
|
222
|
+
description: rails_code_auditor is a developer-friendly Ruby gem that automates the
|
223
|
+
process of auditing your Rails codebase. It runs a suite of essential tools—including
|
224
|
+
Brakeman, Bundler Audit, RuboCop, Rails Best Practices, Flay, Flog, and License
|
225
|
+
Finder—and consolidates all outputs into a single readable report.
|
226
|
+
email:
|
227
|
+
- sivamanikandan@railsfactory.org
|
228
|
+
executables:
|
229
|
+
- rails_code_auditor
|
230
|
+
extensions: []
|
231
|
+
extra_rdoc_files: []
|
232
|
+
files:
|
233
|
+
- exe/rails_code_auditor
|
234
|
+
- lib/rails_code_auditor.rb
|
235
|
+
- lib/rails_code_auditor/analyzer.rb
|
236
|
+
- lib/rails_code_auditor/grapher.rb
|
237
|
+
- lib/rails_code_auditor/html_to_pdf_converter.rb
|
238
|
+
- lib/rails_code_auditor/llm_client.rb
|
239
|
+
- lib/rails_code_auditor/pdf_generator.rb
|
240
|
+
- lib/rails_code_auditor/report_generator.rb
|
241
|
+
- lib/rails_code_auditor/scorer.rb
|
242
|
+
- lib/rails_code_auditor/simplecov_runner.rb
|
243
|
+
- lib/rails_code_auditor/version.rb
|
244
|
+
homepage: https://github.com/railsfactory-sivamanikandan/rails_code_auditor
|
245
|
+
licenses:
|
246
|
+
- MIT
|
247
|
+
metadata:
|
248
|
+
allowed_push_host: https://rubygems.org
|
249
|
+
homepage_uri: https://github.com/railsfactory-sivamanikandan/rails_code_auditor
|
250
|
+
source_code_uri: https://github.com/railsfactory-sivamanikandan/rails_code_auditor
|
251
|
+
changelog_uri: https://github.com/railsfactory-sivamanikandan/rails_code_auditor/CHANGELOG.md
|
252
|
+
rdoc_options: []
|
253
|
+
require_paths:
|
254
|
+
- lib
|
255
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
256
|
+
requirements:
|
257
|
+
- - ">="
|
258
|
+
- !ruby/object:Gem::Version
|
259
|
+
version: 3.1.0
|
260
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
261
|
+
requirements:
|
262
|
+
- - ">="
|
263
|
+
- !ruby/object:Gem::Version
|
264
|
+
version: '0'
|
265
|
+
requirements: []
|
266
|
+
rubygems_version: 3.6.2
|
267
|
+
specification_version: 4
|
268
|
+
summary: Easily generate consolidated security and code quality reports for Ruby on
|
269
|
+
Rails applications with a single command.
|
270
|
+
test_files: []
|