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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a77305dda091e360bba15b732a2be430928247be5cbe55429b65c1ce36f3d9e1
4
- data.tar.gz: 37464197fbb6178695cabf80c41999f4e37945896aa2bc09d2dd304526b0b792
3
+ metadata.gz: b38d31d7f4411ba2d4de694915019f9059cf19bab67079473c4597066d5e8269
4
+ data.tar.gz: 60d6255349f02a11a5c85adf5ec10b9de6019b7c9d9694d15f3101ffef838301
5
5
  SHA512:
6
- metadata.gz: 599e294ddccdfc257639fdba95b49f302e51b7856fb932c39aa70c06fa632ec6949881079eca7d4e76ceeaa6b76ffd4926991303caf7abe7fdfa7eee496b3382
7
- data.tar.gz: 4d877292385699d548a2776e21d65dd762b19d59eaa7c8934340345c42d7e3e3d9a7c185f6dff8049f3d3950537f08c344c5b4f73410ce2337dbc5b442b5fd8e
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
@@ -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.1"
5
5
  end
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
- private_class_method def self.handle_options(options)
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
- puts "Generating report... (not yet implemented)"
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
- private_class_method def self.run_scan(options)
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
- private_class_method def self.apply_default_scanners(options)
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
- private_class_method def self.display_reports(reports, execution_time)
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
- # Order by severity (ERROR > WARNING > INFO)
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("extra", "severity") || "INFO"
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
- private_class_method def self.format_report(results, scan_type)
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 # Add spacing between items, but not after last
253
+ puts "" if index < results.length - 1
112
254
  end
113
255
  end
114
256
 
115
- # Helper methods for better formatting
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
- private_class_method def self.get_scan_icon(scan_type)
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
- 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)
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("extra", "severity")
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
- 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(", ")})"
292
+ { error_count: error_count, warning_count: warning_count, info_count: info_count }
166
293
  end
167
294
 
168
- private_class_method def self.format_duration(seconds)
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
- 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")
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
- 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 ? "..." : ""}"
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
- 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"
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} | #{extract_short_description(result)}"
327
+ puts " 📁 #{file_info} | #{(result[:extra]&.[](:message) || result["extra"]&.[]("message") || "No description available")[0..80]}..."
199
328
  end
200
329
 
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|
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
- # Displays the help message for the CLI tool.
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 detailed report from the last scan.
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 (e.g., json, sarif, console).
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.0
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.7.1
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: []