shield_ast 1.0.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/sca.rb +5 -23
- data/lib/shield_ast/version.rb +1 -1
- data/lib/shield_ast.rb +195 -67
- metadata +73 -8
- data/.idea/.gitignore +0 -8
- data/.idea/dictionaries/project.xml +0 -7
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/shield_ast.iml +0 -48
- data/.idea/vcs.xml +0 -6
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/sca.rb
CHANGED
@@ -25,25 +25,15 @@ module ShieldAst
|
|
25
25
|
puts "Exit code: #{exit_code}" if ENV["DEBUG"]
|
26
26
|
puts "Output: #{output}" if ENV["DEBUG"]
|
27
27
|
|
28
|
-
# OSV Scanner exit codes:
|
29
|
-
# 0: No vulnerabilities found
|
30
|
-
# 1: Vulnerabilities found
|
31
|
-
# 1-126: Vulnerability result related errors
|
32
|
-
# 127: General error
|
33
|
-
# 128: No packages found
|
34
|
-
# 129-255: Non result related errors
|
35
|
-
|
36
28
|
case exit_code
|
37
29
|
when 0
|
38
|
-
{ "results" => [] }
|
30
|
+
{ "results" => [] }
|
39
31
|
when 1
|
40
|
-
{ "results" => parse_json_output(output) }
|
32
|
+
{ "results" => parse_json_output(output) }
|
41
33
|
when 1..126
|
42
|
-
# Vulnerability related errors, but try to parse results anyway
|
43
34
|
puts "OSV Scanner vulnerability error (exit code: #{exit_code})" if ENV["DEBUG"]
|
44
35
|
{ "results" => parse_json_output(output) }
|
45
36
|
when 127
|
46
|
-
# General error, but if we have JSON output, use it
|
47
37
|
if output.include?('{"results"')
|
48
38
|
puts "OSV Scanner completed with general error but has results" if ENV["DEBUG"]
|
49
39
|
{ "results" => parse_json_output(output) }
|
@@ -111,7 +101,6 @@ module ShieldAst
|
|
111
101
|
severity = determine_severity(vuln, package_data)
|
112
102
|
file_path = determine_file_path(ecosystem)
|
113
103
|
|
114
|
-
# Extract fixed version info
|
115
104
|
fixed_version = extract_fixed_version(vuln)
|
116
105
|
|
117
106
|
{
|
@@ -142,7 +131,6 @@ module ShieldAst
|
|
142
131
|
end
|
143
132
|
|
144
133
|
def self.extract_fixed_version(vuln)
|
145
|
-
# Try to find fixed version in affected ranges
|
146
134
|
if vuln["affected"] && vuln["affected"].is_a?(Array)
|
147
135
|
vuln["affected"].each do |affected|
|
148
136
|
if affected["ranges"] && affected["ranges"].is_a?(Array)
|
@@ -155,14 +143,12 @@ module ShieldAst
|
|
155
143
|
end
|
156
144
|
end
|
157
145
|
|
158
|
-
# Also check database_specific for fixed version
|
159
146
|
if affected["database_specific"] && affected["database_specific"]["last_affected"]
|
160
|
-
return ">
|
147
|
+
return "> #{affected["database_specific"]["last_affected"]}"
|
161
148
|
end
|
162
149
|
end
|
163
150
|
end
|
164
151
|
|
165
|
-
# Fallback: check database_specific at root level
|
166
152
|
if vuln["database_specific"]
|
167
153
|
return vuln["database_specific"]["fixed_version"] if vuln["database_specific"]["fixed_version"]
|
168
154
|
end
|
@@ -171,17 +157,13 @@ module ShieldAst
|
|
171
157
|
end
|
172
158
|
|
173
159
|
def self.determine_severity(vuln, package_data)
|
174
|
-
|
175
|
-
if vuln.dig("database_specific", "severity")
|
176
|
-
return map_severity(vuln["database_specific"]["severity"])
|
177
|
-
end
|
160
|
+
return map_severity(vuln["database_specific"]["severity"]) if vuln.dig("database_specific", "severity")
|
178
161
|
|
179
|
-
# Check groups max_severity
|
180
162
|
groups = package_data&.dig("groups") || []
|
181
163
|
max_severity = groups.first&.dig("max_severity")
|
182
164
|
return cvss_to_severity(max_severity.to_f) if max_severity
|
183
165
|
|
184
|
-
"WARNING" # Default
|
166
|
+
"WARNING" # Default severity
|
185
167
|
end
|
186
168
|
|
187
169
|
def self.determine_file_path(ecosystem)
|
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}"
|
121
|
+
end
|
122
|
+
|
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
|
48
159
|
end
|
49
160
|
|
50
|
-
|
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,19 +185,27 @@ 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
|
-
|
197
|
+
sorted_results = sort_by_severity(results)
|
198
|
+
top_results = sorted_results.first(5)
|
199
|
+
remaining_count = results.length - top_results.length
|
200
|
+
|
201
|
+
puts "\n#{get_scan_icon(type)} #{type.to_s.upcase} (#{results.length} #{results.length == 1 ? "issue" : "issues"}#{remaining_count.positive? ? ", showing top 5" : ""})"
|
70
202
|
puts "-" * 60
|
71
203
|
|
72
|
-
format_report(
|
204
|
+
format_report(top_results, type)
|
205
|
+
|
206
|
+
if remaining_count.positive?
|
207
|
+
puts " ... and #{remaining_count} more #{remaining_count == 1 ? "issue" : "issues"} (run with --verbose to see all)"
|
208
|
+
end
|
73
209
|
end
|
74
210
|
|
75
211
|
puts "\n✅ Scan finished in: #{format_duration(execution_time)}"
|
@@ -78,23 +214,35 @@ module ShieldAst
|
|
78
214
|
puts "✅ No security issues found! Your code looks clean."
|
79
215
|
else
|
80
216
|
severity_summary = calculate_severity_summary(reports)
|
81
|
-
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]}}"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.sort_by_severity(results)
|
222
|
+
severity_order = { "ERROR" => 0, "WARNING" => 1, "INFO" => 2 }
|
223
|
+
|
224
|
+
results.sort_by do |result|
|
225
|
+
severity = result[:severity] || result["severity"] || result.dig(:extra,
|
226
|
+
:severity) || result.dig("extra",
|
227
|
+
"severity") || "INFO"
|
228
|
+
severity_order[severity.upcase] || 3
|
229
|
+
rescue TypeError
|
230
|
+
3
|
82
231
|
end
|
83
232
|
end
|
84
233
|
|
85
|
-
|
234
|
+
def self.format_report(results, scan_type)
|
86
235
|
results.each_with_index do |result, index|
|
87
236
|
if scan_type == :sca && has_sca_format?(result)
|
88
237
|
format_sca_result(result)
|
89
238
|
else
|
90
239
|
format_default_result(result)
|
91
240
|
end
|
92
|
-
puts "" if index < results.length - 1
|
241
|
+
puts "" if index < results.length - 1
|
93
242
|
end
|
94
243
|
end
|
95
244
|
|
96
|
-
|
97
|
-
private_class_method def self.get_severity_icon(severity)
|
245
|
+
def self.get_severity_icon(severity)
|
98
246
|
case severity&.upcase
|
99
247
|
when "ERROR" then "🔴"
|
100
248
|
when "WARNING" then "🟡"
|
@@ -103,7 +251,7 @@ module ShieldAst
|
|
103
251
|
end
|
104
252
|
end
|
105
253
|
|
106
|
-
|
254
|
+
def self.get_scan_icon(scan_type)
|
107
255
|
case scan_type
|
108
256
|
when :sast then "🔍"
|
109
257
|
when :sca then "📦"
|
@@ -112,23 +260,15 @@ module ShieldAst
|
|
112
260
|
end
|
113
261
|
end
|
114
262
|
|
115
|
-
|
116
|
-
description = result["extra"]["message"].gsub("\n", " ").strip
|
117
|
-
if description.length > 80
|
118
|
-
"#{description[0..80]}..."
|
119
|
-
else
|
120
|
-
description
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
private_class_method def self.calculate_severity_summary(reports)
|
263
|
+
def self.calculate_severity_summary(reports)
|
125
264
|
error_count = 0
|
126
265
|
warning_count = 0
|
127
266
|
info_count = 0
|
128
267
|
|
129
268
|
reports.each_value do |report_data|
|
130
|
-
(report_data["results"] || []).each do |result|
|
131
|
-
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")
|
132
272
|
case severity&.upcase
|
133
273
|
when "ERROR" then error_count += 1
|
134
274
|
when "WARNING" then warning_count += 1
|
@@ -137,15 +277,10 @@ module ShieldAst
|
|
137
277
|
end
|
138
278
|
end
|
139
279
|
|
140
|
-
|
141
|
-
parts << "#{error_count} 🔴" if error_count.positive?
|
142
|
-
parts << "#{warning_count} 🟡" if warning_count.positive?
|
143
|
-
parts << "#{info_count} 🔵" if info_count.positive?
|
144
|
-
|
145
|
-
"(#{parts.join(", ")})"
|
280
|
+
{ error_count: error_count, warning_count: warning_count, info_count: info_count }
|
146
281
|
end
|
147
282
|
|
148
|
-
|
283
|
+
def self.format_duration(seconds)
|
149
284
|
if seconds < 1
|
150
285
|
"#{(seconds * 1000).round}ms"
|
151
286
|
elsif seconds < 60
|
@@ -157,31 +292,33 @@ module ShieldAst
|
|
157
292
|
end
|
158
293
|
end
|
159
294
|
|
160
|
-
|
161
|
-
result.key?(
|
162
|
-
|
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"))
|
163
300
|
end
|
164
301
|
|
165
|
-
|
166
|
-
severity_icon = get_severity_icon(result[
|
167
|
-
puts " #{severity_icon} #{result["title"]} (#{result["vulnerable_version"]} → #{result["fixed_version"]})"
|
168
|
-
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 ? "..." : ""}"
|
169
306
|
end
|
170
307
|
|
171
|
-
|
172
|
-
severity_icon = get_severity_icon(result["extra"]["severity"
|
173
|
-
|
174
|
-
|
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"
|
311
|
+
title = message.split(".")[0].strip
|
312
|
+
file_info = "#{File.basename(result[:path] || result["path"] || "N/A")}:#{result[:start]&.[](:line) || result["start"]&.[]("line") || "N/A"}"
|
175
313
|
|
176
314
|
puts " #{severity_icon} #{title}"
|
177
|
-
puts " 📁 #{file_info} | #{
|
315
|
+
puts " 📁 #{file_info} | #{(result[:extra]&.[](:message) || result["extra"]&.[]("message") || "No description available")[0..80]}..."
|
178
316
|
end
|
179
317
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
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|
|
185
322
|
case arg
|
186
323
|
when "scan" then options[:command] = "scan"
|
187
324
|
when "report" then options[:command] = "report"
|
@@ -190,43 +327,34 @@ module ShieldAst
|
|
190
327
|
when "-i", "--iac" then options[:iac] = true
|
191
328
|
when "-h", "--help" then options[:help] = true
|
192
329
|
when "--version" then options[:version] = true
|
330
|
+
when "-o", "--output"
|
331
|
+
options[:output] = args[index + 1] if index + 1 < args.length
|
193
332
|
when /^[^-]/ then options[:path] = arg if options[:command] == "scan" && options[:path].nil?
|
194
333
|
end
|
195
334
|
end
|
196
335
|
options
|
197
336
|
end
|
198
337
|
|
199
|
-
|
200
|
-
private_class_method def self.show_help
|
338
|
+
def self.show_help
|
201
339
|
puts <<~HELP
|
202
340
|
ast - A powerful command-line tool for Application Security Testing
|
203
|
-
|
204
341
|
Usage:
|
205
342
|
ast [command] [options]
|
206
|
-
|
207
343
|
Commands:
|
208
344
|
scan [path] Scans a directory for vulnerabilities. Defaults to the current directory.
|
209
|
-
report Generates a
|
345
|
+
report Generates a report from the last scan in JSON or PDF format.
|
210
346
|
help Shows this help message.
|
211
|
-
|
212
347
|
Options:
|
213
348
|
-s, --sast Run Static Application Security Testing (SAST) with Semgrep.
|
214
349
|
-c, --sca Run Software Composition Analysis (SCA) with OSV Scanner.
|
215
350
|
-i, --iac Run Infrastructure as Code (IaC) analysis with Semgrep.
|
216
|
-
-o, --output Specify the output format (
|
351
|
+
-o, --output Specify the output format for report (json or pdf, default: json).
|
217
352
|
-h, --help Show this help message.
|
218
353
|
--version Show the ast version.
|
219
|
-
|
220
354
|
Examples:
|
221
|
-
# Scan the current directory for all types of vulnerabilities
|
222
355
|
ast scan
|
223
|
-
|
224
|
-
# Run only SAST and SCA on a specific project folder
|
225
356
|
ast scan /path/to/project --sast --sca
|
226
|
-
|
227
|
-
# Generate a report in SARIF format
|
228
|
-
ast report --output sarif
|
229
|
-
|
357
|
+
ast report --output pdf
|
230
358
|
Description:
|
231
359
|
ast is an all-in-one command-line tool that automates security testing by
|
232
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
|
@@ -20,18 +90,13 @@ executables:
|
|
20
90
|
extensions: []
|
21
91
|
extra_rdoc_files: []
|
22
92
|
files:
|
23
|
-
- ".idea/.gitignore"
|
24
|
-
- ".idea/dictionaries/project.xml"
|
25
|
-
- ".idea/misc.xml"
|
26
|
-
- ".idea/modules.xml"
|
27
|
-
- ".idea/shield_ast.iml"
|
28
|
-
- ".idea/vcs.xml"
|
29
93
|
- CHANGELOG.md
|
30
94
|
- CODE_OF_CONDUCT.md
|
31
95
|
- LICENSE.txt
|
32
96
|
- README.md
|
33
97
|
- Rakefile
|
34
98
|
- exe/ast
|
99
|
+
- lib/reports/templates/pdf_report_template.rb
|
35
100
|
- lib/shield_ast.rb
|
36
101
|
- lib/shield_ast/iac.rb
|
37
102
|
- lib/shield_ast/runner.rb
|
data/.idea/.gitignore
DELETED
data/.idea/misc.xml
DELETED
data/.idea/modules.xml
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
-
<project version="4">
|
3
|
-
<component name="ProjectModuleManager">
|
4
|
-
<modules>
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/shield_ast.iml" filepath="$PROJECT_DIR$/.idea/shield_ast.iml" />
|
6
|
-
</modules>
|
7
|
-
</component>
|
8
|
-
</project>
|
data/.idea/shield_ast.iml
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
-
<module type="RUBY_MODULE" version="4">
|
3
|
-
<component name="ModuleRunConfigurationManager">
|
4
|
-
<shared />
|
5
|
-
</component>
|
6
|
-
<component name="NewModuleRootManager">
|
7
|
-
<content url="file://$MODULE_DIR$">
|
8
|
-
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
|
9
|
-
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
10
|
-
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
11
|
-
</content>
|
12
|
-
<orderEntry type="inheritedJdk" />
|
13
|
-
<orderEntry type="sourceFolder" forTests="false" />
|
14
|
-
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, rbenv: 3.4.5) [gem]" level="application" />
|
15
|
-
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.7.1, rbenv: 3.4.5) [gem]" level="application" />
|
16
|
-
<orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, rbenv: 3.4.5) [gem]" level="application" />
|
17
|
-
<orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, rbenv: 3.4.5) [gem]" level="application" />
|
18
|
-
<orderEntry type="library" scope="PROVIDED" name="erb (v5.0.2, rbenv: 3.4.5) [gem]" level="application" />
|
19
|
-
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.1, rbenv: 3.4.5) [gem]" level="application" />
|
20
|
-
<orderEntry type="library" scope="PROVIDED" name="irb (v1.15.2, rbenv: 3.4.5) [gem]" level="application" />
|
21
|
-
<orderEntry type="library" scope="PROVIDED" name="json (v2.13.2, rbenv: 3.4.5) [gem]" level="application" />
|
22
|
-
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, rbenv: 3.4.5) [gem]" level="application" />
|
23
|
-
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, rbenv: 3.4.5) [gem]" level="application" />
|
24
|
-
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.27.0, rbenv: 3.4.5) [gem]" level="application" />
|
25
|
-
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.9.0, rbenv: 3.4.5) [gem]" level="application" />
|
26
|
-
<orderEntry type="library" scope="PROVIDED" name="pp (v0.6.2, rbenv: 3.4.5) [gem]" level="application" />
|
27
|
-
<orderEntry type="library" scope="PROVIDED" name="prettyprint (v0.2.0, rbenv: 3.4.5) [gem]" level="application" />
|
28
|
-
<orderEntry type="library" scope="PROVIDED" name="prism (v1.4.0, rbenv: 3.4.5) [gem]" level="application" />
|
29
|
-
<orderEntry type="library" scope="PROVIDED" name="psych (v5.2.6, rbenv: 3.4.5) [gem]" level="application" />
|
30
|
-
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, rbenv: 3.4.5) [gem]" level="application" />
|
31
|
-
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, rbenv: 3.4.5) [gem]" level="application" />
|
32
|
-
<orderEntry type="library" scope="PROVIDED" name="rake (v13.3.0, rbenv: 3.4.5) [gem]" level="application" />
|
33
|
-
<orderEntry type="library" scope="PROVIDED" name="rdoc (v6.14.2, rbenv: 3.4.5) [gem]" level="application" />
|
34
|
-
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.11.0, rbenv: 3.4.5) [gem]" level="application" />
|
35
|
-
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.2, rbenv: 3.4.5) [gem]" level="application" />
|
36
|
-
<orderEntry type="library" scope="PROVIDED" name="rspec (v3.13.1, rbenv: 3.4.5) [gem]" level="application" />
|
37
|
-
<orderEntry type="library" scope="PROVIDED" name="rspec-core (v3.13.5, rbenv: 3.4.5) [gem]" level="application" />
|
38
|
-
<orderEntry type="library" scope="PROVIDED" name="rspec-expectations (v3.13.5, rbenv: 3.4.5) [gem]" level="application" />
|
39
|
-
<orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.13.5, rbenv: 3.4.5) [gem]" level="application" />
|
40
|
-
<orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.13.4, rbenv: 3.4.5) [gem]" level="application" />
|
41
|
-
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.79.1, rbenv: 3.4.5) [gem]" level="application" />
|
42
|
-
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.46.0, rbenv: 3.4.5) [gem]" level="application" />
|
43
|
-
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.4.5) [gem]" level="application" />
|
44
|
-
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.7, rbenv: 3.4.5) [gem]" level="application" />
|
45
|
-
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.1.4, rbenv: 3.4.5) [gem]" level="application" />
|
46
|
-
<orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.0.4, rbenv: 3.4.5) [gem]" level="application" />
|
47
|
-
</component>
|
48
|
-
</module>
|