shield_ast 1.1.0 → 1.2.1
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 +4 -4
- data/lib/reports/templates/pdf_report_template.rb +147 -0
- data/lib/shield_ast/version.rb +1 -1
- data/lib/shield_ast.rb +196 -70
- metadata +74 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b38d31d7f4411ba2d4de694915019f9059cf19bab67079473c4597066d5e8269
|
4
|
+
data.tar.gz: 60d6255349f02a11a5c85adf5ec10b9de6019b7c9d9694d15f3101ffef838301
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: afc99954084847d8c6d9e9d1348638169039d2bc08a0370d7f5d86e50ad2a6d146f0724423151f168941b909a20bf5386ec767e6cd0d03d34249e0a00290c9c5
|
7
|
+
data.tar.gz: c69b120cd7847b4b2f8aca24a6f9a5ac609cd12fea9eb6f95502996b050ccb3994250588f1dc59772766a0c3334f147906b7c6c79db213c51cedbead4cb4e09d
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "prawn"
|
4
|
+
require "prawn/table"
|
5
|
+
|
6
|
+
Prawn::Document.generate(output_file) do
|
7
|
+
def extract_short_description(result)
|
8
|
+
message = result[:extra]&.[](:message) || result["extra"]&.[]("message") || result.dig(:extra,
|
9
|
+
:message) || result.dig(
|
10
|
+
"extra", "message"
|
11
|
+
) || "No description available"
|
12
|
+
description = message.gsub("\n", " ").strip
|
13
|
+
description.length > 80 ? "#{description[0..80]}..." : description
|
14
|
+
end
|
15
|
+
|
16
|
+
font "Helvetica"
|
17
|
+
font_size 12
|
18
|
+
|
19
|
+
text "Shield AST Scan Report", size: 20, style: :bold, align: :center
|
20
|
+
move_down 10
|
21
|
+
text "Generated by Shield AST v#{version}", size: 10, align: :center
|
22
|
+
text generated_at, size: 10, align: :center
|
23
|
+
move_down 20
|
24
|
+
|
25
|
+
text "Summary", size: 16, style: :bold
|
26
|
+
move_down 10
|
27
|
+
text "Generated on: #{generated_at}"
|
28
|
+
text "Scan Duration: #{scan_duration}"
|
29
|
+
text "Total Issues Found: #{total_issues}"
|
30
|
+
text "Severity Breakdown:", style: :bold
|
31
|
+
text "Errors: #{severity_summary[:error_count]} (High Severity)", color: "DC3545" # Red
|
32
|
+
text "Warnings: #{severity_summary[:warning_count]} (Medium Severity)", color: "FFC107" # Yellow
|
33
|
+
text "Info: #{severity_summary[:info_count]} (Low Severity)", color: "17A2B8" # Blue
|
34
|
+
move_down 20
|
35
|
+
|
36
|
+
if sast_results.any?
|
37
|
+
text "Static Application Security Testing (SAST)", size: 16, style: :bold
|
38
|
+
text "Issues Found: #{sast_results.length}", size: 12
|
39
|
+
move_down 10
|
40
|
+
table_data = [%w[Severity Title File Description]]
|
41
|
+
sast_results.each do |result|
|
42
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
43
|
+
:severity) || result.dig("extra",
|
44
|
+
"severity") || "INFO"
|
45
|
+
message = result.dig(:extra, :message) || result.dig("extra", "message") || "Unknown issue"
|
46
|
+
title = message.split(".")[0].strip
|
47
|
+
file_info = if result[:path] || result["path"]
|
48
|
+
"#{File.basename(result[:path] || result["path"])}:#{result.dig(
|
49
|
+
:start, :line
|
50
|
+
) || result.dig("start", "line") || "N/A"}"
|
51
|
+
else
|
52
|
+
"N/A"
|
53
|
+
end
|
54
|
+
description = extract_short_description(result)
|
55
|
+
severity_color = case severity.upcase
|
56
|
+
when "ERROR" then "DC3545"
|
57
|
+
when "WARNING" then "FFC107"
|
58
|
+
else "17A2B8"
|
59
|
+
end
|
60
|
+
table_data << [{ content: severity, text_color: severity_color }, title, file_info, description]
|
61
|
+
rescue StandardError => e
|
62
|
+
table_data << [{ content: "ERROR", text_color: "DC3545" }, "Invalid result", "N/A", "Error: #{e.message}"]
|
63
|
+
end
|
64
|
+
table table_data, header: true, width: bounds.width, cell_style: { size: 10 } do
|
65
|
+
cells.padding = 5
|
66
|
+
cells.borders = %i[top bottom left right]
|
67
|
+
row(0).font_style = :bold
|
68
|
+
row(0).background_color = "007BFF"
|
69
|
+
row(0).text_color = "FFFFFF"
|
70
|
+
end
|
71
|
+
move_down 20
|
72
|
+
end
|
73
|
+
|
74
|
+
if sca_results.any?
|
75
|
+
text "Software Composition Analysis (SCA)", size: 16, style: :bold
|
76
|
+
text "Issues Found: #{sca_results.length}", size: 12
|
77
|
+
move_down 10
|
78
|
+
table_data = [["Severity", "Title", "File", "Vuln Version", "Fixed Version", "Description"]]
|
79
|
+
sca_results.each_with_index do |result, _index|
|
80
|
+
severity = result[:severity] || result["severity"] || "INFO"
|
81
|
+
title = (result[:title] || result["title"] || "Unknown issue").to_s
|
82
|
+
file = (result[:file] || result["file"] || "N/A").to_s
|
83
|
+
vuln_version = (result[:vulnerable_version] || result["vulnerable_version"] || "N/A").to_s
|
84
|
+
fixed_version = (result[:fixed_version] || result["fixed_version"] || "N/A").to_s
|
85
|
+
description = (result[:description] || result["description"] || "No description").to_s
|
86
|
+
description = description[0..80] + (description.length > 80 ? "..." : "")
|
87
|
+
severity_color = case severity.upcase
|
88
|
+
when "ERROR" then "DC3545"
|
89
|
+
when "WARNING" then "FFC107"
|
90
|
+
else "17A2B8"
|
91
|
+
end
|
92
|
+
table_data << [{ content: severity, text_color: severity_color }, title, file, vuln_version, fixed_version,
|
93
|
+
description]
|
94
|
+
rescue StandardError => e
|
95
|
+
table_data << [{ content: "ERROR", text_color: "DC3545" }, "Invalid result", "N/A", "N/A", "N/A",
|
96
|
+
"Error: #{e.message}"]
|
97
|
+
end
|
98
|
+
table table_data, header: true, width: bounds.width, cell_style: { size: 10 } do
|
99
|
+
cells.padding = 5
|
100
|
+
cells.borders = %i[top bottom left right]
|
101
|
+
row(0).font_style = :bold
|
102
|
+
row(0).background_color = "007BFF"
|
103
|
+
row(0).text_color = "FFFFFF"
|
104
|
+
end
|
105
|
+
move_down 20
|
106
|
+
end
|
107
|
+
|
108
|
+
if iac_results.any?
|
109
|
+
text "Infrastructure as Code (IaC)", size: 16, style: :bold
|
110
|
+
text "Issues Found: #{iac_results.length}", size: 12
|
111
|
+
move_down 10
|
112
|
+
table_data = [%w[Severity Title File Description]]
|
113
|
+
iac_results.each_with_index do |result, _index|
|
114
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
115
|
+
:severity) || result.dig("extra",
|
116
|
+
"severity") || "INFO"
|
117
|
+
message = result.dig(:extra, :message) || result.dig("extra", "message") || "Unknown issue"
|
118
|
+
title = message.split(".")[0].strip
|
119
|
+
file_info = if result[:path] || result["path"]
|
120
|
+
"#{File.basename(result[:path] || result["path"])}:#{result.dig(
|
121
|
+
:start, :line
|
122
|
+
) || result.dig("start", "line") || "N/A"}"
|
123
|
+
else
|
124
|
+
"N/A"
|
125
|
+
end
|
126
|
+
description = extract_short_description(result)
|
127
|
+
severity_color = case severity.upcase
|
128
|
+
when "ERROR" then "DC3545"
|
129
|
+
when "WARNING" then "FFC107"
|
130
|
+
else "17A2B8"
|
131
|
+
end
|
132
|
+
table_data << [{ content: severity, text_color: severity_color }, title, file_info, description]
|
133
|
+
rescue StandardError => e
|
134
|
+
table_data << [{ content: "ERROR", text_color: "DC3545" }, "Invalid result", "N/A", "Error: #{e.message}"]
|
135
|
+
end
|
136
|
+
table table_data, header: true, width: bounds.width, cell_style: { size: 10 } do
|
137
|
+
cells.padding = 5
|
138
|
+
cells.borders = %i[top bottom left right]
|
139
|
+
row(0).font_style = :bold
|
140
|
+
row(0).background_color = "007BFF"
|
141
|
+
row(0).text_color = "FFFFFF"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
move_down 20
|
146
|
+
text "Generated by Shield AST v#{version}", size: 10, align: :center
|
147
|
+
end
|
data/lib/shield_ast/version.rb
CHANGED
data/lib/shield_ast.rb
CHANGED
@@ -1,23 +1,43 @@
|
|
1
|
+
# lib/shield_ast/main.rb
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require_relative "shield_ast/version"
|
4
5
|
require_relative "shield_ast/runner"
|
5
|
-
|
6
6
|
require "json"
|
7
|
+
require "fileutils"
|
8
|
+
require "erb"
|
9
|
+
require "prawn"
|
10
|
+
require "prawn/table"
|
7
11
|
|
8
12
|
# Main module for the Shield AST gem.
|
9
13
|
module ShieldAst
|
10
14
|
class Error < StandardError; end
|
11
15
|
|
12
16
|
# Main class for the Shield AST command-line tool.
|
13
|
-
# Handles command-line argument parsing and delegates to the Runner.
|
14
17
|
class Main
|
18
|
+
SCAN_DATA_FILE = File.join(Dir.pwd, "lib", "reports", "scan_data.json")
|
19
|
+
REPORT_JSON_FILE = File.join(Dir.pwd, "lib", "reports", "scan_report.json")
|
20
|
+
REPORT_PDF_FILE = File.join(Dir.pwd, "lib", "reports", "scan_report.pdf")
|
21
|
+
PDF_TEMPLATE = File.join(__dir__, "reports", "templates", "pdf_report_template.rb")
|
22
|
+
|
15
23
|
def self.call(args)
|
24
|
+
ascii_banner
|
25
|
+
|
26
|
+
unless scanner_exists?("osv-scanner") && scanner_exists?("semgrep")
|
27
|
+
puts "\e[31m[!] ERROR:\e[0m Required tools not found."
|
28
|
+
puts " Install: \e[33mosv-scanner\e[0m, \e[33msemgrep\e[0m"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
16
32
|
options = parse_args(args)
|
17
33
|
handle_options(options)
|
18
34
|
end
|
19
35
|
|
20
|
-
|
36
|
+
def self.scanner_exists?(scanner)
|
37
|
+
system("which #{scanner} > /dev/null 2>&1")
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.handle_options(options)
|
21
41
|
if options[:help]
|
22
42
|
show_help
|
23
43
|
elsif options[:version]
|
@@ -25,14 +45,14 @@ module ShieldAst
|
|
25
45
|
elsif options[:command] == "scan"
|
26
46
|
run_scan(options)
|
27
47
|
elsif options[:command] == "report"
|
28
|
-
|
48
|
+
generate_report(options)
|
29
49
|
else
|
30
50
|
puts "Invalid command. Use 'ast help' for more information."
|
31
51
|
show_help
|
32
52
|
end
|
33
53
|
end
|
34
54
|
|
35
|
-
|
55
|
+
def self.run_scan(options)
|
36
56
|
path = options[:path] || Dir.pwd
|
37
57
|
options = apply_default_scanners(options)
|
38
58
|
|
@@ -45,9 +65,129 @@ module ShieldAst
|
|
45
65
|
execution_time = end_time - start_time
|
46
66
|
|
47
67
|
display_reports(reports, execution_time)
|
68
|
+
save_scan_data(reports, execution_time)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.save_scan_data(reports, execution_time)
|
72
|
+
normalized_reports = {}
|
73
|
+
reports.each do |key, value|
|
74
|
+
normalized_reports[key.to_sym] = value.transform_keys(&:to_sym)
|
75
|
+
end
|
76
|
+
data = {
|
77
|
+
reports: normalized_reports,
|
78
|
+
execution_time: execution_time,
|
79
|
+
generated_at: Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
|
80
|
+
}
|
81
|
+
FileUtils.mkdir_p(File.dirname(SCAN_DATA_FILE))
|
82
|
+
File.write(SCAN_DATA_FILE, JSON.pretty_generate(data))
|
83
|
+
puts "Scan data saved to: #{SCAN_DATA_FILE}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.load_scan_data
|
87
|
+
unless File.exist?(SCAN_DATA_FILE)
|
88
|
+
puts "Error: Scan data file #{SCAN_DATA_FILE} does not exist."
|
89
|
+
return nil
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
JSON.parse(File.read(SCAN_DATA_FILE), symbolize_names: true)
|
94
|
+
rescue JSON::ParserError => e
|
95
|
+
puts "Error: Invalid scan data in #{SCAN_DATA_FILE}: #{e.message}"
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.generate_report(options)
|
101
|
+
scan_data = load_scan_data
|
102
|
+
unless scan_data
|
103
|
+
puts "No scan data available. Please run 'ast scan' first."
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
output_format = options[:output] || "json"
|
108
|
+
unless %w[json pdf].include?(output_format)
|
109
|
+
puts "Error: Invalid output format '#{output_format}'. Use 'json' or 'pdf'."
|
110
|
+
return
|
111
|
+
end
|
112
|
+
|
113
|
+
puts "Generating #{output_format.upcase} report..."
|
114
|
+
|
115
|
+
if output_format == "json"
|
116
|
+
generate_json_report(scan_data)
|
117
|
+
elsif output_format == "pdf"
|
118
|
+
generate_pdf_report(scan_data)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.generate_json_report(scan_data)
|
123
|
+
FileUtils.mkdir_p(File.dirname(REPORT_JSON_FILE))
|
124
|
+
report = {
|
125
|
+
generated_at: scan_data[:generated_at],
|
126
|
+
scan_duration: format_duration(scan_data[:execution_time]),
|
127
|
+
total_issues: calculate_total_issues(scan_data[:reports]),
|
128
|
+
severity_summary: calculate_severity_summary(scan_data[:reports]),
|
129
|
+
reports: scan_data[:reports]
|
130
|
+
}
|
131
|
+
File.write(REPORT_JSON_FILE, JSON.pretty_generate(report))
|
132
|
+
puts "JSON report generated at: #{REPORT_JSON_FILE}"
|
48
133
|
end
|
49
134
|
|
50
|
-
|
135
|
+
def self.generate_pdf_report(scan_data)
|
136
|
+
unless File.exist?(PDF_TEMPLATE)
|
137
|
+
puts "Error: PDF template file #{PDF_TEMPLATE} not found."
|
138
|
+
return
|
139
|
+
end
|
140
|
+
|
141
|
+
FileUtils.mkdir_p(File.dirname(REPORT_PDF_FILE))
|
142
|
+
|
143
|
+
version = ShieldAst::VERSION
|
144
|
+
generated_at = scan_data[:generated_at]
|
145
|
+
scan_duration = format_duration(scan_data[:execution_time])
|
146
|
+
sast_results = normalize_results(sort_by_severity(scan_data[:reports][:sast]&.[](:results) || []))
|
147
|
+
sca_results = normalize_results(sort_by_severity(scan_data[:reports][:sca]&.[](:results) || []))
|
148
|
+
iac_results = normalize_results(sort_by_severity(scan_data[:reports][:iac]&.[](:results) || []))
|
149
|
+
total_issues = calculate_total_issues(scan_data[:reports])
|
150
|
+
severity_summary = calculate_severity_summary(scan_data[:reports])
|
151
|
+
output_file = REPORT_PDF_FILE
|
152
|
+
|
153
|
+
begin
|
154
|
+
template_context = Object.new
|
155
|
+
template_context.instance_variable_set(:@version, version)
|
156
|
+
template_context.instance_variable_set(:@generated_at, generated_at)
|
157
|
+
template_context.instance_variable_set(:@scan_duration, scan_duration)
|
158
|
+
template_context.instance_variable_set(:@sast_results, sast_results)
|
159
|
+
template_context.instance_variable_set(:@sca_results, sca_results)
|
160
|
+
template_context.instance_variable_set(:@iac_results, iac_results)
|
161
|
+
template_context.instance_variable_set(:@total_issues, total_issues)
|
162
|
+
template_context.instance_variable_set(:@severity_summary, severity_summary)
|
163
|
+
template_context.instance_variable_set(:@output_file, output_file)
|
164
|
+
template = File.read(PDF_TEMPLATE)
|
165
|
+
template_context.instance_eval template, PDF_TEMPLATE
|
166
|
+
puts "PDF report generated at: #{REPORT_PDF_FILE}"
|
167
|
+
rescue StandardError => e
|
168
|
+
puts "Error: Failed to generate PDF: #{e.message}"
|
169
|
+
puts "Error: Backtrace: #{e.backtrace.join("\n")}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.normalize_results(results)
|
174
|
+
results.map do |result|
|
175
|
+
normalized = result.transform_keys(&:to_sym)
|
176
|
+
normalized[:severity] ||= normalized[:extra]&.[](:severity) || normalized[:extra]&.[]("severity") || "INFO"
|
177
|
+
normalized[:vulnerable_version] = normalized[:vulnerable_version].to_s if normalized[:vulnerable_version]
|
178
|
+
normalized[:fixed_version] = normalized[:fixed_version].to_s if normalized[:fixed_version]
|
179
|
+
normalized
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.calculate_total_issues(reports)
|
184
|
+
sast_count = (reports[:sast]&.[](:results) || reports["sast"]&.[]("results") || []).length
|
185
|
+
sca_count = (reports[:sca]&.[](:results) || reports["sca"]&.[]("results") || []).length
|
186
|
+
iac_count = (reports[:iac]&.[](:results) || reports["iac"]&.[]("results") || []).length
|
187
|
+
sast_count + sca_count + iac_count
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.apply_default_scanners(options)
|
51
191
|
options.tap do |o|
|
52
192
|
if !o[:sast] && !o[:sca] && !o[:iac]
|
53
193
|
o[:sast] = true
|
@@ -57,16 +197,15 @@ module ShieldAst
|
|
57
197
|
end
|
58
198
|
end
|
59
199
|
|
60
|
-
|
200
|
+
def self.display_reports(reports, execution_time)
|
61
201
|
total_issues = 0
|
62
202
|
|
63
203
|
reports.each do |type, report_data|
|
64
|
-
results = report_data["results"] || []
|
204
|
+
results = report_data[:results] || report_data["results"] || []
|
65
205
|
total_issues += results.length
|
66
206
|
|
67
207
|
next if results.empty?
|
68
208
|
|
69
|
-
# Order by severity showing top 5 only
|
70
209
|
sorted_results = sort_by_severity(results)
|
71
210
|
top_results = sorted_results.first(5)
|
72
211
|
remaining_count = results.length - top_results.length
|
@@ -87,33 +226,35 @@ module ShieldAst
|
|
87
226
|
puts "✅ No security issues found! Your code looks clean."
|
88
227
|
else
|
89
228
|
severity_summary = calculate_severity_summary(reports)
|
90
|
-
puts "📊 Total: #{total_issues} findings #{severity_summary}"
|
229
|
+
puts "📊 Total: #{total_issues} findings {error_count: #{severity_summary[:error_count]}, warning_count: #{severity_summary[:warning_count]}, info_count: #{severity_summary[:info_count]}}"
|
91
230
|
end
|
92
231
|
end
|
93
232
|
|
94
|
-
|
95
|
-
private_class_method def self.sort_by_severity(results)
|
233
|
+
def self.sort_by_severity(results)
|
96
234
|
severity_order = { "ERROR" => 0, "WARNING" => 1, "INFO" => 2 }
|
97
235
|
|
98
236
|
results.sort_by do |result|
|
99
|
-
severity = result["severity"] || result.dig(
|
237
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
238
|
+
:severity) || result.dig("extra",
|
239
|
+
"severity") || "INFO"
|
100
240
|
severity_order[severity.upcase] || 3
|
241
|
+
rescue TypeError
|
242
|
+
3
|
101
243
|
end
|
102
244
|
end
|
103
245
|
|
104
|
-
|
246
|
+
def self.format_report(results, scan_type)
|
105
247
|
results.each_with_index do |result, index|
|
106
248
|
if scan_type == :sca && has_sca_format?(result)
|
107
249
|
format_sca_result(result)
|
108
250
|
else
|
109
251
|
format_default_result(result)
|
110
252
|
end
|
111
|
-
puts "" if index < results.length - 1
|
253
|
+
puts "" if index < results.length - 1
|
112
254
|
end
|
113
255
|
end
|
114
256
|
|
115
|
-
|
116
|
-
private_class_method def self.get_severity_icon(severity)
|
257
|
+
def self.get_severity_icon(severity)
|
117
258
|
case severity&.upcase
|
118
259
|
when "ERROR" then "🔴"
|
119
260
|
when "WARNING" then "🟡"
|
@@ -122,7 +263,7 @@ module ShieldAst
|
|
122
263
|
end
|
123
264
|
end
|
124
265
|
|
125
|
-
|
266
|
+
def self.get_scan_icon(scan_type)
|
126
267
|
case scan_type
|
127
268
|
when :sast then "🔍"
|
128
269
|
when :sca then "📦"
|
@@ -131,24 +272,15 @@ module ShieldAst
|
|
131
272
|
end
|
132
273
|
end
|
133
274
|
|
134
|
-
|
135
|
-
message = result["extra"]["message"] || "No description available"
|
136
|
-
description = message.gsub("\n", " ").strip
|
137
|
-
if description.length > 80
|
138
|
-
"#{description[0..80]}..."
|
139
|
-
else
|
140
|
-
description
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
private_class_method def self.calculate_severity_summary(reports)
|
275
|
+
def self.calculate_severity_summary(reports)
|
145
276
|
error_count = 0
|
146
277
|
warning_count = 0
|
147
278
|
info_count = 0
|
148
279
|
|
149
280
|
reports.each_value do |report_data|
|
150
|
-
(report_data["results"] || []).each do |result|
|
151
|
-
severity = result["severity"] || result.dig(
|
281
|
+
(report_data[:results] || report_data["results"] || []).each do |result|
|
282
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
283
|
+
:severity) || result.dig("extra", "severity")
|
152
284
|
case severity&.upcase
|
153
285
|
when "ERROR" then error_count += 1
|
154
286
|
when "WARNING" then warning_count += 1
|
@@ -157,15 +289,10 @@ module ShieldAst
|
|
157
289
|
end
|
158
290
|
end
|
159
291
|
|
160
|
-
|
161
|
-
parts << "#{error_count} 🔴" if error_count.positive?
|
162
|
-
parts << "#{warning_count} 🟡" if warning_count.positive?
|
163
|
-
parts << "#{info_count} 🔵" if info_count.positive?
|
164
|
-
|
165
|
-
"(#{parts.join(", ")})"
|
292
|
+
{ error_count: error_count, warning_count: warning_count, info_count: info_count }
|
166
293
|
end
|
167
294
|
|
168
|
-
|
295
|
+
def self.format_duration(seconds)
|
169
296
|
if seconds < 1
|
170
297
|
"#{(seconds * 1000).round}ms"
|
171
298
|
elsif seconds < 60
|
@@ -177,32 +304,33 @@ module ShieldAst
|
|
177
304
|
end
|
178
305
|
end
|
179
306
|
|
180
|
-
|
181
|
-
result.key?(
|
182
|
-
|
307
|
+
def self.has_sca_format?(result)
|
308
|
+
(result.key?(:title) || result.key?("title")) &&
|
309
|
+
(result.key?(:description) || result.key?("description")) &&
|
310
|
+
(result.key?(:vulnerable_version) || result.key?("vulnerable_version")) &&
|
311
|
+
(result.key?(:fixed_version) || result.key?("fixed_version"))
|
183
312
|
end
|
184
313
|
|
185
|
-
|
186
|
-
severity_icon = get_severity_icon(result[
|
187
|
-
puts " #{severity_icon} #{result["title"]} (#{result["vulnerable_version"]} → #{result["fixed_version"]})"
|
188
|
-
puts " 📁 #{result["file"]} | #{result["description"][0..80]}#{result["description"].length > 80 ? "..." : ""}"
|
314
|
+
def self.format_sca_result(result)
|
315
|
+
severity_icon = get_severity_icon(result[:severity] || result["severity"])
|
316
|
+
puts " #{severity_icon} #{result[:title] || result["title"]} (#{result[:vulnerable_version] || result["vulnerable_version"]} → #{result[:fixed_version] || result["fixed_version"]})"
|
317
|
+
puts " 📁 #{result[:file] || result["file"]} | #{(result[:description] || result["description"] || "")[0..80]}#{(result[:description] || result["description"] || "").length > 80 ? "..." : ""}"
|
189
318
|
end
|
190
319
|
|
191
|
-
|
192
|
-
severity_icon = get_severity_icon(result["extra"]["severity"
|
193
|
-
message = result["extra"]["message"
|
320
|
+
def self.format_default_result(result)
|
321
|
+
severity_icon = get_severity_icon(result[:severity] || result["severity"] || result[:extra]&.[](:severity) || result["extra"]&.[]("severity"))
|
322
|
+
message = result[:extra]&.[](:message) || result["extra"]&.[]("message") || "Unknown issue"
|
194
323
|
title = message.split(".")[0].strip
|
195
|
-
file_info = "#{File.basename(result["path"])}:#{result["start"]["line"
|
324
|
+
file_info = "#{File.basename(result[:path] || result["path"] || "N/A")}:#{result[:start]&.[](:line) || result["start"]&.[]("line") || "N/A"}"
|
196
325
|
|
197
326
|
puts " #{severity_icon} #{title}"
|
198
|
-
puts " 📁 #{file_info} | #{
|
327
|
+
puts " 📁 #{file_info} | #{(result[:extra]&.[](:message) || result["extra"]&.[]("message") || "No description available")[0..80]}..."
|
199
328
|
end
|
200
329
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
args.each do |arg|
|
330
|
+
def self.parse_args(args)
|
331
|
+
options = { command: nil, path: nil, sast: false, sca: false, iac: false, help: false, version: false,
|
332
|
+
output: nil }
|
333
|
+
args.each_with_index do |arg, index|
|
206
334
|
case arg
|
207
335
|
when "scan" then options[:command] = "scan"
|
208
336
|
when "report" then options[:command] = "report"
|
@@ -211,48 +339,46 @@ module ShieldAst
|
|
211
339
|
when "-i", "--iac" then options[:iac] = true
|
212
340
|
when "-h", "--help" then options[:help] = true
|
213
341
|
when "--version" then options[:version] = true
|
342
|
+
when "-o", "--output"
|
343
|
+
options[:output] = args[index + 1] if index + 1 < args.length
|
214
344
|
when /^[^-]/ then options[:path] = arg if options[:command] == "scan" && options[:path].nil?
|
215
345
|
end
|
216
346
|
end
|
217
347
|
options
|
218
348
|
end
|
219
349
|
|
220
|
-
|
221
|
-
private_class_method def self.show_help
|
350
|
+
def self.show_help
|
222
351
|
puts <<~HELP
|
223
352
|
ast - A powerful command-line tool for Application Security Testing
|
224
|
-
|
225
353
|
Usage:
|
226
354
|
ast [command] [options]
|
227
|
-
|
228
355
|
Commands:
|
229
356
|
scan [path] Scans a directory for vulnerabilities. Defaults to the current directory.
|
230
|
-
report Generates a
|
357
|
+
report Generates a report from the last scan in JSON or PDF format.
|
231
358
|
help Shows this help message.
|
232
|
-
|
233
359
|
Options:
|
234
360
|
-s, --sast Run Static Application Security Testing (SAST) with Semgrep.
|
235
361
|
-c, --sca Run Software Composition Analysis (SCA) with OSV Scanner.
|
236
362
|
-i, --iac Run Infrastructure as Code (IaC) analysis with Semgrep.
|
237
|
-
-o, --output Specify the output format (
|
363
|
+
-o, --output Specify the output format for report (json or pdf, default: json).
|
238
364
|
-h, --help Show this help message.
|
239
365
|
--version Show the ast version.
|
240
|
-
|
241
366
|
Examples:
|
242
|
-
# Scan the current directory for all types of vulnerabilities
|
243
367
|
ast scan
|
244
|
-
|
245
|
-
# Run only SAST and SCA on a specific project folder
|
246
368
|
ast scan /path/to/project --sast --sca
|
247
|
-
|
248
|
-
# Generate a report in SARIF format
|
249
|
-
ast report --output sarif
|
250
|
-
|
369
|
+
ast report --output pdf
|
251
370
|
Description:
|
252
371
|
ast is an all-in-one command-line tool that automates security testing by
|
253
372
|
integrating popular open-source scanners for SAST, SCA, and IaC, helping you
|
254
373
|
find and fix vulnerabilities early in the development lifecycle.
|
255
374
|
HELP
|
256
375
|
end
|
376
|
+
|
377
|
+
def self.ascii_banner
|
378
|
+
puts <<~BANNER
|
379
|
+
[>>> SHIELD AST <<<]
|
380
|
+
powered by open source (semgrep + osv-scanner) \n
|
381
|
+
BANNER
|
382
|
+
end
|
257
383
|
end
|
258
384
|
end
|
metadata
CHANGED
@@ -1,14 +1,84 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shield_ast
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jose Augusto
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: erb
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '4.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '4.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: fileutils
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.7'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.7'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: json
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.6'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.6'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: prawn
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.4'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.4'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: prawn-table
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.2'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.2'
|
12
82
|
description: |-
|
13
83
|
Shield AST is an all-in-one command-line tool that automates security testing by integrating
|
14
84
|
popular open-source scanners for SAST, SCA, and IaC, helping you find and fix vulnerabilities
|
@@ -26,6 +96,7 @@ files:
|
|
26
96
|
- README.md
|
27
97
|
- Rakefile
|
28
98
|
- exe/ast
|
99
|
+
- lib/reports/templates/pdf_report_template.rb
|
29
100
|
- lib/shield_ast.rb
|
30
101
|
- lib/shield_ast/iac.rb
|
31
102
|
- lib/shield_ast/runner.rb
|
@@ -55,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
126
|
- !ruby/object:Gem::Version
|
56
127
|
version: '0'
|
57
128
|
requirements: []
|
58
|
-
rubygems_version: 3.
|
129
|
+
rubygems_version: 3.6.9
|
59
130
|
specification_version: 4
|
60
131
|
summary: A command-line tool for multi-scanner Application Security Testing.
|
61
132
|
test_files: []
|