shield_ast 1.1.0 → 1.2.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 +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 +177 -70
- metadata +73 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f6c61da40db55d33cb491cfac9aee8d6a42160b8703db338498d8990852245c
|
4
|
+
data.tar.gz: a72f17d19e7b92057db035e00783c51d2007e874ec216f3b3301d1b9c640f349
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2cb6e62fa8dd5f7bf41f8411af8f4420179ba9b356413bb204eddc26a7213704d37a8534d4dcec5e6a62ed12b439dada3b1551ea5802eae12975fce8c84321c
|
7
|
+
data.tar.gz: 2dca66c6eac359384237bf73e57621f911f8bea31d4b6a481325e5ab7f7011a7656114949908df4c026c060b3dedf6c025f0f2114b81d5cce4f5d0d86806a5d2
|
@@ -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,31 @@
|
|
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)
|
16
24
|
options = parse_args(args)
|
17
25
|
handle_options(options)
|
18
26
|
end
|
19
27
|
|
20
|
-
|
28
|
+
def self.handle_options(options)
|
21
29
|
if options[:help]
|
22
30
|
show_help
|
23
31
|
elsif options[:version]
|
@@ -25,14 +33,14 @@ module ShieldAst
|
|
25
33
|
elsif options[:command] == "scan"
|
26
34
|
run_scan(options)
|
27
35
|
elsif options[:command] == "report"
|
28
|
-
|
36
|
+
generate_report(options)
|
29
37
|
else
|
30
38
|
puts "Invalid command. Use 'ast help' for more information."
|
31
39
|
show_help
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
35
|
-
|
43
|
+
def self.run_scan(options)
|
36
44
|
path = options[:path] || Dir.pwd
|
37
45
|
options = apply_default_scanners(options)
|
38
46
|
|
@@ -45,9 +53,129 @@ module ShieldAst
|
|
45
53
|
execution_time = end_time - start_time
|
46
54
|
|
47
55
|
display_reports(reports, execution_time)
|
56
|
+
save_scan_data(reports, execution_time)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.save_scan_data(reports, execution_time)
|
60
|
+
normalized_reports = {}
|
61
|
+
reports.each do |key, value|
|
62
|
+
normalized_reports[key.to_sym] = value.transform_keys(&:to_sym)
|
63
|
+
end
|
64
|
+
data = {
|
65
|
+
reports: normalized_reports,
|
66
|
+
execution_time: execution_time,
|
67
|
+
generated_at: Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
|
68
|
+
}
|
69
|
+
FileUtils.mkdir_p(File.dirname(SCAN_DATA_FILE))
|
70
|
+
File.write(SCAN_DATA_FILE, JSON.pretty_generate(data))
|
71
|
+
puts "Scan data saved to: #{SCAN_DATA_FILE}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.load_scan_data
|
75
|
+
unless File.exist?(SCAN_DATA_FILE)
|
76
|
+
puts "Error: Scan data file #{SCAN_DATA_FILE} does not exist."
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
|
80
|
+
begin
|
81
|
+
JSON.parse(File.read(SCAN_DATA_FILE), symbolize_names: true)
|
82
|
+
rescue JSON::ParserError => e
|
83
|
+
puts "Error: Invalid scan data in #{SCAN_DATA_FILE}: #{e.message}"
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.generate_report(options)
|
89
|
+
scan_data = load_scan_data
|
90
|
+
unless scan_data
|
91
|
+
puts "No scan data available. Please run 'ast scan' first."
|
92
|
+
return
|
93
|
+
end
|
94
|
+
|
95
|
+
output_format = options[:output] || "json"
|
96
|
+
unless %w[json pdf].include?(output_format)
|
97
|
+
puts "Error: Invalid output format '#{output_format}'. Use 'json' or 'pdf'."
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
puts "Generating #{output_format.upcase} report..."
|
102
|
+
|
103
|
+
if output_format == "json"
|
104
|
+
generate_json_report(scan_data)
|
105
|
+
elsif output_format == "pdf"
|
106
|
+
generate_pdf_report(scan_data)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.generate_json_report(scan_data)
|
111
|
+
FileUtils.mkdir_p(File.dirname(REPORT_JSON_FILE))
|
112
|
+
report = {
|
113
|
+
generated_at: scan_data[:generated_at],
|
114
|
+
scan_duration: format_duration(scan_data[:execution_time]),
|
115
|
+
total_issues: calculate_total_issues(scan_data[:reports]),
|
116
|
+
severity_summary: calculate_severity_summary(scan_data[:reports]),
|
117
|
+
reports: scan_data[:reports]
|
118
|
+
}
|
119
|
+
File.write(REPORT_JSON_FILE, JSON.pretty_generate(report))
|
120
|
+
puts "JSON report generated at: #{REPORT_JSON_FILE}"
|
48
121
|
end
|
49
122
|
|
50
|
-
|
123
|
+
def self.generate_pdf_report(scan_data)
|
124
|
+
unless File.exist?(PDF_TEMPLATE)
|
125
|
+
puts "Error: PDF template file #{PDF_TEMPLATE} not found."
|
126
|
+
return
|
127
|
+
end
|
128
|
+
|
129
|
+
FileUtils.mkdir_p(File.dirname(REPORT_PDF_FILE))
|
130
|
+
|
131
|
+
version = ShieldAst::VERSION
|
132
|
+
generated_at = scan_data[:generated_at]
|
133
|
+
scan_duration = format_duration(scan_data[:execution_time])
|
134
|
+
sast_results = normalize_results(sort_by_severity(scan_data[:reports][:sast]&.[](:results) || []))
|
135
|
+
sca_results = normalize_results(sort_by_severity(scan_data[:reports][:sca]&.[](:results) || []))
|
136
|
+
iac_results = normalize_results(sort_by_severity(scan_data[:reports][:iac]&.[](:results) || []))
|
137
|
+
total_issues = calculate_total_issues(scan_data[:reports])
|
138
|
+
severity_summary = calculate_severity_summary(scan_data[:reports])
|
139
|
+
output_file = REPORT_PDF_FILE
|
140
|
+
|
141
|
+
begin
|
142
|
+
template_context = Object.new
|
143
|
+
template_context.instance_variable_set(:@version, version)
|
144
|
+
template_context.instance_variable_set(:@generated_at, generated_at)
|
145
|
+
template_context.instance_variable_set(:@scan_duration, scan_duration)
|
146
|
+
template_context.instance_variable_set(:@sast_results, sast_results)
|
147
|
+
template_context.instance_variable_set(:@sca_results, sca_results)
|
148
|
+
template_context.instance_variable_set(:@iac_results, iac_results)
|
149
|
+
template_context.instance_variable_set(:@total_issues, total_issues)
|
150
|
+
template_context.instance_variable_set(:@severity_summary, severity_summary)
|
151
|
+
template_context.instance_variable_set(:@output_file, output_file)
|
152
|
+
template = File.read(PDF_TEMPLATE)
|
153
|
+
template_context.instance_eval template, PDF_TEMPLATE
|
154
|
+
puts "PDF report generated at: #{REPORT_PDF_FILE}"
|
155
|
+
rescue StandardError => e
|
156
|
+
puts "Error: Failed to generate PDF: #{e.message}"
|
157
|
+
puts "Error: Backtrace: #{e.backtrace.join("\n")}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.normalize_results(results)
|
162
|
+
results.map do |result|
|
163
|
+
normalized = result.transform_keys(&:to_sym)
|
164
|
+
normalized[:severity] ||= normalized[:extra]&.[](:severity) || normalized[:extra]&.[]("severity") || "INFO"
|
165
|
+
normalized[:vulnerable_version] = normalized[:vulnerable_version].to_s if normalized[:vulnerable_version]
|
166
|
+
normalized[:fixed_version] = normalized[:fixed_version].to_s if normalized[:fixed_version]
|
167
|
+
normalized
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.calculate_total_issues(reports)
|
172
|
+
sast_count = (reports[:sast]&.[](:results) || reports["sast"]&.[]("results") || []).length
|
173
|
+
sca_count = (reports[:sca]&.[](:results) || reports["sca"]&.[]("results") || []).length
|
174
|
+
iac_count = (reports[:iac]&.[](:results) || reports["iac"]&.[]("results") || []).length
|
175
|
+
sast_count + sca_count + iac_count
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.apply_default_scanners(options)
|
51
179
|
options.tap do |o|
|
52
180
|
if !o[:sast] && !o[:sca] && !o[:iac]
|
53
181
|
o[:sast] = true
|
@@ -57,16 +185,15 @@ module ShieldAst
|
|
57
185
|
end
|
58
186
|
end
|
59
187
|
|
60
|
-
|
188
|
+
def self.display_reports(reports, execution_time)
|
61
189
|
total_issues = 0
|
62
190
|
|
63
191
|
reports.each do |type, report_data|
|
64
|
-
results = report_data["results"] || []
|
192
|
+
results = report_data[:results] || report_data["results"] || []
|
65
193
|
total_issues += results.length
|
66
194
|
|
67
195
|
next if results.empty?
|
68
196
|
|
69
|
-
# Order by severity showing top 5 only
|
70
197
|
sorted_results = sort_by_severity(results)
|
71
198
|
top_results = sorted_results.first(5)
|
72
199
|
remaining_count = results.length - top_results.length
|
@@ -87,33 +214,35 @@ module ShieldAst
|
|
87
214
|
puts "✅ No security issues found! Your code looks clean."
|
88
215
|
else
|
89
216
|
severity_summary = calculate_severity_summary(reports)
|
90
|
-
puts "📊 Total: #{total_issues} findings #{severity_summary}"
|
217
|
+
puts "📊 Total: #{total_issues} findings {error_count: #{severity_summary[:error_count]}, warning_count: #{severity_summary[:warning_count]}, info_count: #{severity_summary[:info_count]}}"
|
91
218
|
end
|
92
219
|
end
|
93
220
|
|
94
|
-
|
95
|
-
private_class_method def self.sort_by_severity(results)
|
221
|
+
def self.sort_by_severity(results)
|
96
222
|
severity_order = { "ERROR" => 0, "WARNING" => 1, "INFO" => 2 }
|
97
223
|
|
98
224
|
results.sort_by do |result|
|
99
|
-
severity = result["severity"] || result.dig(
|
225
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
226
|
+
:severity) || result.dig("extra",
|
227
|
+
"severity") || "INFO"
|
100
228
|
severity_order[severity.upcase] || 3
|
229
|
+
rescue TypeError
|
230
|
+
3
|
101
231
|
end
|
102
232
|
end
|
103
233
|
|
104
|
-
|
234
|
+
def self.format_report(results, scan_type)
|
105
235
|
results.each_with_index do |result, index|
|
106
236
|
if scan_type == :sca && has_sca_format?(result)
|
107
237
|
format_sca_result(result)
|
108
238
|
else
|
109
239
|
format_default_result(result)
|
110
240
|
end
|
111
|
-
puts "" if index < results.length - 1
|
241
|
+
puts "" if index < results.length - 1
|
112
242
|
end
|
113
243
|
end
|
114
244
|
|
115
|
-
|
116
|
-
private_class_method def self.get_severity_icon(severity)
|
245
|
+
def self.get_severity_icon(severity)
|
117
246
|
case severity&.upcase
|
118
247
|
when "ERROR" then "🔴"
|
119
248
|
when "WARNING" then "🟡"
|
@@ -122,7 +251,7 @@ module ShieldAst
|
|
122
251
|
end
|
123
252
|
end
|
124
253
|
|
125
|
-
|
254
|
+
def self.get_scan_icon(scan_type)
|
126
255
|
case scan_type
|
127
256
|
when :sast then "🔍"
|
128
257
|
when :sca then "📦"
|
@@ -131,24 +260,15 @@ module ShieldAst
|
|
131
260
|
end
|
132
261
|
end
|
133
262
|
|
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)
|
263
|
+
def self.calculate_severity_summary(reports)
|
145
264
|
error_count = 0
|
146
265
|
warning_count = 0
|
147
266
|
info_count = 0
|
148
267
|
|
149
268
|
reports.each_value do |report_data|
|
150
|
-
(report_data["results"] || []).each do |result|
|
151
|
-
severity = result["severity"] || result.dig(
|
269
|
+
(report_data[:results] || report_data["results"] || []).each do |result|
|
270
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
271
|
+
:severity) || result.dig("extra", "severity")
|
152
272
|
case severity&.upcase
|
153
273
|
when "ERROR" then error_count += 1
|
154
274
|
when "WARNING" then warning_count += 1
|
@@ -157,15 +277,10 @@ module ShieldAst
|
|
157
277
|
end
|
158
278
|
end
|
159
279
|
|
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(", ")})"
|
280
|
+
{ error_count: error_count, warning_count: warning_count, info_count: info_count }
|
166
281
|
end
|
167
282
|
|
168
|
-
|
283
|
+
def self.format_duration(seconds)
|
169
284
|
if seconds < 1
|
170
285
|
"#{(seconds * 1000).round}ms"
|
171
286
|
elsif seconds < 60
|
@@ -177,32 +292,33 @@ module ShieldAst
|
|
177
292
|
end
|
178
293
|
end
|
179
294
|
|
180
|
-
|
181
|
-
result.key?(
|
182
|
-
|
295
|
+
def self.has_sca_format?(result)
|
296
|
+
(result.key?(:title) || result.key?("title")) &&
|
297
|
+
(result.key?(:description) || result.key?("description")) &&
|
298
|
+
(result.key?(:vulnerable_version) || result.key?("vulnerable_version")) &&
|
299
|
+
(result.key?(:fixed_version) || result.key?("fixed_version"))
|
183
300
|
end
|
184
301
|
|
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 ? "..." : ""}"
|
302
|
+
def self.format_sca_result(result)
|
303
|
+
severity_icon = get_severity_icon(result[:severity] || result["severity"])
|
304
|
+
puts " #{severity_icon} #{result[:title] || result["title"]} (#{result[:vulnerable_version] || result["vulnerable_version"]} → #{result[:fixed_version] || result["fixed_version"]})"
|
305
|
+
puts " 📁 #{result[:file] || result["file"]} | #{(result[:description] || result["description"] || "")[0..80]}#{(result[:description] || result["description"] || "").length > 80 ? "..." : ""}"
|
189
306
|
end
|
190
307
|
|
191
|
-
|
192
|
-
severity_icon = get_severity_icon(result["extra"]["severity"
|
193
|
-
message = result["extra"]["message"
|
308
|
+
def self.format_default_result(result)
|
309
|
+
severity_icon = get_severity_icon(result[:severity] || result["severity"] || result[:extra]&.[](:severity) || result["extra"]&.[]("severity"))
|
310
|
+
message = result[:extra]&.[](:message) || result["extra"]&.[]("message") || "Unknown issue"
|
194
311
|
title = message.split(".")[0].strip
|
195
|
-
file_info = "#{File.basename(result["path"])}:#{result["start"]["line"
|
312
|
+
file_info = "#{File.basename(result[:path] || result["path"] || "N/A")}:#{result[:start]&.[](:line) || result["start"]&.[]("line") || "N/A"}"
|
196
313
|
|
197
314
|
puts " #{severity_icon} #{title}"
|
198
|
-
puts " 📁 #{file_info} | #{
|
315
|
+
puts " 📁 #{file_info} | #{(result[:extra]&.[](:message) || result["extra"]&.[]("message") || "No description available")[0..80]}..."
|
199
316
|
end
|
200
317
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
args.each do |arg|
|
318
|
+
def self.parse_args(args)
|
319
|
+
options = { command: nil, path: nil, sast: false, sca: false, iac: false, help: false, version: false,
|
320
|
+
output: nil }
|
321
|
+
args.each_with_index do |arg, index|
|
206
322
|
case arg
|
207
323
|
when "scan" then options[:command] = "scan"
|
208
324
|
when "report" then options[:command] = "report"
|
@@ -211,43 +327,34 @@ module ShieldAst
|
|
211
327
|
when "-i", "--iac" then options[:iac] = true
|
212
328
|
when "-h", "--help" then options[:help] = true
|
213
329
|
when "--version" then options[:version] = true
|
330
|
+
when "-o", "--output"
|
331
|
+
options[:output] = args[index + 1] if index + 1 < args.length
|
214
332
|
when /^[^-]/ then options[:path] = arg if options[:command] == "scan" && options[:path].nil?
|
215
333
|
end
|
216
334
|
end
|
217
335
|
options
|
218
336
|
end
|
219
337
|
|
220
|
-
|
221
|
-
private_class_method def self.show_help
|
338
|
+
def self.show_help
|
222
339
|
puts <<~HELP
|
223
340
|
ast - A powerful command-line tool for Application Security Testing
|
224
|
-
|
225
341
|
Usage:
|
226
342
|
ast [command] [options]
|
227
|
-
|
228
343
|
Commands:
|
229
344
|
scan [path] Scans a directory for vulnerabilities. Defaults to the current directory.
|
230
|
-
report Generates a
|
345
|
+
report Generates a report from the last scan in JSON or PDF format.
|
231
346
|
help Shows this help message.
|
232
|
-
|
233
347
|
Options:
|
234
348
|
-s, --sast Run Static Application Security Testing (SAST) with Semgrep.
|
235
349
|
-c, --sca Run Software Composition Analysis (SCA) with OSV Scanner.
|
236
350
|
-i, --iac Run Infrastructure as Code (IaC) analysis with Semgrep.
|
237
|
-
-o, --output Specify the output format (
|
351
|
+
-o, --output Specify the output format for report (json or pdf, default: json).
|
238
352
|
-h, --help Show this help message.
|
239
353
|
--version Show the ast version.
|
240
|
-
|
241
354
|
Examples:
|
242
|
-
# Scan the current directory for all types of vulnerabilities
|
243
355
|
ast scan
|
244
|
-
|
245
|
-
# Run only SAST and SCA on a specific project folder
|
246
356
|
ast scan /path/to/project --sast --sca
|
247
|
-
|
248
|
-
# Generate a report in SARIF format
|
249
|
-
ast report --output sarif
|
250
|
-
|
357
|
+
ast report --output pdf
|
251
358
|
Description:
|
252
359
|
ast is an all-in-one command-line tool that automates security testing by
|
253
360
|
integrating popular open-source scanners for SAST, SCA, and IaC, helping you
|
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.
|
4
|
+
version: 1.2.0
|
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
|