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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a77305dda091e360bba15b732a2be430928247be5cbe55429b65c1ce36f3d9e1
4
- data.tar.gz: 37464197fbb6178695cabf80c41999f4e37945896aa2bc09d2dd304526b0b792
3
+ metadata.gz: 7f6c61da40db55d33cb491cfac9aee8d6a42160b8703db338498d8990852245c
4
+ data.tar.gz: a72f17d19e7b92057db035e00783c51d2007e874ec216f3b3301d1b9c640f349
5
5
  SHA512:
6
- metadata.gz: 599e294ddccdfc257639fdba95b49f302e51b7856fb932c39aa70c06fa632ec6949881079eca7d4e76ceeaa6b76ffd4926991303caf7abe7fdfa7eee496b3382
7
- data.tar.gz: 4d877292385699d548a2776e21d65dd762b19d59eaa7c8934340345c42d7e3e3d9a7c185f6dff8049f3d3950537f08c344c5b4f73410ce2337dbc5b442b5fd8e
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShieldAst
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
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
- private_class_method def self.handle_options(options)
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
- puts "Generating report... (not yet implemented)"
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
- private_class_method def self.run_scan(options)
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
- private_class_method def self.apply_default_scanners(options)
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
- private_class_method def self.display_reports(reports, execution_time)
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
- # Order by severity (ERROR > WARNING > INFO)
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("extra", "severity") || "INFO"
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
- private_class_method def self.format_report(results, scan_type)
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 # Add spacing between items, but not after last
241
+ puts "" if index < results.length - 1
112
242
  end
113
243
  end
114
244
 
115
- # Helper methods for better formatting
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
- private_class_method def self.get_scan_icon(scan_type)
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
- private_class_method def self.extract_short_description(result)
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("extra", "severity")
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
- parts = []
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
- private_class_method def self.format_duration(seconds)
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
- private_class_method def self.has_sca_format?(result)
181
- result.key?("title") && result.key?("description") &&
182
- result.key?("vulnerable_version") && result.key?("fixed_version")
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
- private_class_method def self.format_sca_result(result)
186
- severity_icon = get_severity_icon(result['severity'])
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
- private_class_method def self.format_default_result(result)
192
- severity_icon = get_severity_icon(result["extra"]["severity"])
193
- message = result["extra"]["message"] || "Unknown issue"
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} | #{extract_short_description(result)}"
315
+ puts " 📁 #{file_info} | #{(result[:extra]&.[](:message) || result["extra"]&.[]("message") || "No description available")[0..80]}..."
199
316
  end
200
317
 
201
- # Parses command-line arguments to build an options hash.
202
- private_class_method def self.parse_args(args)
203
- options = { command: nil, path: nil, sast: false, sca: false, iac: false, help: false, version: false }
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
- # Displays the help message for the CLI tool.
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 detailed report from the last scan.
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 (e.g., json, sarif, console).
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.1.0
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