gem_guard 0.1.4 → 0.1.7
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/gem_guard/cli.rb +180 -6
- data/lib/gem_guard/config.rb +193 -0
- data/lib/gem_guard/sbom_generator.rb +152 -0
- data/lib/gem_guard/version.rb +1 -1
- data/lib/gem_guard.rb +3 -1
- data/templates/circleci-config.yml +107 -0
- data/templates/github-actions.yml +85 -0
- data/templates/gitlab-ci.yml +112 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 747dee6cd137e68fae4e5086d37a40bc5fabb67672f67d06a901627ac47c00cd
|
4
|
+
data.tar.gz: 75c793fc063db05b04635f2c12fa7abf7169f815f57bd6c64f8da0da2d3ea03a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb271a7619b956d0b6271d3fc20c9cee8d75709951b7e8984c051de4e261ba3fdba76ea450fbd2871ece6ec0acc89bb68263e514bb1af9e53dc66e4bdc277d88
|
7
|
+
data.tar.gz: 5ebc3d1e492ea6d273ed740b67b61db1ea729e19eebfc1107b4511112d31641c1347ed2af90c7dd7939350d151826b18432d91279cedcf5873ff8b1a2809fd6a
|
data/lib/gem_guard/cli.rb
CHANGED
@@ -1,11 +1,73 @@
|
|
1
1
|
require "thor"
|
2
|
+
require "stringio"
|
2
3
|
|
3
4
|
module GemGuard
|
4
5
|
class CLI < Thor
|
6
|
+
# Exit codes for CI/CD integration
|
7
|
+
EXIT_SUCCESS = 0
|
8
|
+
EXIT_VULNERABILITIES_FOUND = 1
|
9
|
+
EXIT_ERROR = 2
|
10
|
+
|
5
11
|
desc "scan", "Scan dependencies for known vulnerabilities"
|
6
|
-
option :format, type: :string,
|
7
|
-
option :lockfile, type: :string,
|
12
|
+
option :format, type: :string, desc: "Output format (table, json)"
|
13
|
+
option :lockfile, type: :string, desc: "Path to Gemfile.lock"
|
14
|
+
option :config, type: :string, default: ".gemguard.yml", desc: "Path to config file"
|
15
|
+
option :fail_on_vulnerabilities, type: :boolean, desc: "Exit with code 1 if vulnerabilities found"
|
16
|
+
option :severity_threshold, type: :string, desc: "Minimum severity level (low, medium, high, critical)"
|
17
|
+
option :output, type: :string, desc: "Output file path"
|
8
18
|
def scan
|
19
|
+
config = Config.new(options[:config])
|
20
|
+
|
21
|
+
# Override config with CLI options
|
22
|
+
lockfile_path = options[:lockfile] || config.lockfile_path
|
23
|
+
format = options[:format] || config.output_format
|
24
|
+
fail_on_vulns = options[:fail_on_vulnerabilities].nil? ? config.fail_on_vulnerabilities? : options[:fail_on_vulnerabilities]
|
25
|
+
severity_threshold = options[:severity_threshold] || config.severity_threshold
|
26
|
+
output_file = options[:output] || config.output_file
|
27
|
+
|
28
|
+
unless File.exist?(lockfile_path)
|
29
|
+
puts "Error: #{lockfile_path} not found"
|
30
|
+
exit EXIT_ERROR
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
dependencies = Parser.new.parse(lockfile_path)
|
35
|
+
vulnerabilities = VulnerabilityFetcher.new.fetch_for(dependencies)
|
36
|
+
|
37
|
+
# Filter vulnerabilities based on config
|
38
|
+
filtered_vulnerabilities = filter_vulnerabilities(vulnerabilities, config)
|
39
|
+
|
40
|
+
analysis = Analyzer.new.analyze(dependencies, filtered_vulnerabilities)
|
41
|
+
|
42
|
+
# Filter analysis based on severity threshold
|
43
|
+
filtered_analysis = filter_analysis_by_severity(analysis, severity_threshold, config)
|
44
|
+
|
45
|
+
if output_file
|
46
|
+
output_content = capture_report_output(filtered_analysis, format)
|
47
|
+
File.write(output_file, output_content)
|
48
|
+
puts "Report written to #{output_file}"
|
49
|
+
else
|
50
|
+
Reporter.new.report(filtered_analysis, format: format)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Exit with appropriate code for CI/CD
|
54
|
+
if filtered_analysis.has_vulnerabilities? && fail_on_vulns
|
55
|
+
exit EXIT_VULNERABILITIES_FOUND
|
56
|
+
else
|
57
|
+
exit EXIT_SUCCESS
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
puts "Error: #{e.message}"
|
61
|
+
exit EXIT_ERROR
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
desc "sbom", "Generate Software Bill of Materials (SBOM)"
|
66
|
+
option :format, type: :string, default: "spdx", desc: "SBOM format (spdx, cyclone-dx)"
|
67
|
+
option :lockfile, type: :string, default: "Gemfile.lock", desc: "Path to Gemfile.lock"
|
68
|
+
option :output, type: :string, desc: "Output file path (default: stdout)"
|
69
|
+
option :project, type: :string, default: "ruby-project", desc: "Project name for SBOM"
|
70
|
+
def sbom
|
9
71
|
lockfile_path = options[:lockfile]
|
10
72
|
|
11
73
|
unless File.exist?(lockfile_path)
|
@@ -14,17 +76,129 @@ module GemGuard
|
|
14
76
|
end
|
15
77
|
|
16
78
|
dependencies = Parser.new.parse(lockfile_path)
|
17
|
-
|
18
|
-
|
79
|
+
generator = SbomGenerator.new
|
80
|
+
|
81
|
+
sbom_data = case options[:format].downcase
|
82
|
+
when "spdx"
|
83
|
+
generator.generate_spdx(dependencies, options[:project])
|
84
|
+
when "cyclone-dx", "cyclonedx"
|
85
|
+
generator.generate_cyclone_dx(dependencies, options[:project])
|
86
|
+
else
|
87
|
+
puts "Error: Unsupported format '#{options[:format]}'. Use 'spdx' or 'cyclone-dx'"
|
88
|
+
exit 1
|
89
|
+
end
|
90
|
+
|
91
|
+
output_json = JSON.pretty_generate(sbom_data)
|
19
92
|
|
20
|
-
|
93
|
+
if options[:output]
|
94
|
+
File.write(options[:output], output_json)
|
95
|
+
puts "SBOM written to #{options[:output]}"
|
96
|
+
else
|
97
|
+
puts output_json
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
desc "config", "Manage configuration"
|
102
|
+
option :init, type: :boolean, desc: "Initialize a new .gemguard.yml config file"
|
103
|
+
option :show, type: :boolean, desc: "Show current configuration"
|
104
|
+
option :path, type: :string, default: ".gemguard.yml", desc: "Config file path"
|
105
|
+
def config
|
106
|
+
config_file = Config.new(options[:path])
|
21
107
|
|
22
|
-
|
108
|
+
if options[:init]
|
109
|
+
if File.exist?(options[:path])
|
110
|
+
puts "Config file #{options[:path]} already exists"
|
111
|
+
exit EXIT_ERROR
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create default config file
|
115
|
+
default_config = {
|
116
|
+
"lockfile" => "Gemfile.lock",
|
117
|
+
"format" => "table",
|
118
|
+
"fail_on_vulnerabilities" => true,
|
119
|
+
"severity_threshold" => "low",
|
120
|
+
"ignore_vulnerabilities" => [],
|
121
|
+
"ignore_gems" => [],
|
122
|
+
"output_file" => nil,
|
123
|
+
"project_name" => config_file.send(:detect_project_name),
|
124
|
+
"sbom" => {
|
125
|
+
"format" => "spdx",
|
126
|
+
"include_dev_dependencies" => false
|
127
|
+
},
|
128
|
+
"scan" => {
|
129
|
+
"sources" => ["osv", "ruby_advisory_db"],
|
130
|
+
"timeout" => 30
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
File.write(options[:path], YAML.dump(default_config))
|
135
|
+
puts "Created #{options[:path]} with default configuration"
|
136
|
+
elsif options[:show]
|
137
|
+
if config_file.exists?
|
138
|
+
puts File.read(options[:path])
|
139
|
+
else
|
140
|
+
puts "No config file found at #{options[:path]}"
|
141
|
+
puts "Run 'gem_guard config --init' to create one"
|
142
|
+
end
|
143
|
+
else
|
144
|
+
puts "Usage: gem_guard config [--init|--show] [--path PATH]"
|
145
|
+
puts " --init Create a new .gemguard.yml config file"
|
146
|
+
puts " --show Display current configuration"
|
147
|
+
puts " --path Specify config file path (default: .gemguard.yml)"
|
148
|
+
end
|
23
149
|
end
|
24
150
|
|
25
151
|
desc "version", "Show gem_guard version"
|
26
152
|
def version
|
27
153
|
puts GemGuard::VERSION
|
28
154
|
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def filter_vulnerabilities(vulnerabilities, config)
|
159
|
+
vulnerabilities.reject do |vuln|
|
160
|
+
config.should_ignore_vulnerability?(vuln.id)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def filter_analysis_by_severity(analysis, severity_threshold, config)
|
165
|
+
return analysis unless severity_threshold
|
166
|
+
|
167
|
+
filtered_vulnerable_deps = analysis.vulnerable_dependencies.select do |vuln_dep|
|
168
|
+
vuln_dep.vulnerabilities.any? do |vuln|
|
169
|
+
config.meets_severity_threshold?(extract_severity_level(vuln.severity))
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Create new analysis with filtered vulnerabilities
|
174
|
+
GemGuard::Analysis.new(filtered_vulnerable_deps)
|
175
|
+
end
|
176
|
+
|
177
|
+
def extract_severity_level(severity_string)
|
178
|
+
return "unknown" if severity_string.nil? || severity_string.empty?
|
179
|
+
|
180
|
+
# Extract severity from CVSS string or direct severity
|
181
|
+
case severity_string.downcase
|
182
|
+
when /critical/
|
183
|
+
"critical"
|
184
|
+
when /high/
|
185
|
+
"high"
|
186
|
+
when /medium/
|
187
|
+
"medium"
|
188
|
+
when /low/
|
189
|
+
"low"
|
190
|
+
else
|
191
|
+
"unknown"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def capture_report_output(analysis, format)
|
196
|
+
old_stdout = $stdout
|
197
|
+
$stdout = StringIO.new
|
198
|
+
Reporter.new.report(analysis, format: format)
|
199
|
+
$stdout.string
|
200
|
+
ensure
|
201
|
+
$stdout = old_stdout
|
202
|
+
end
|
29
203
|
end
|
30
204
|
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module GemGuard
|
4
|
+
class Config
|
5
|
+
DEFAULT_CONFIG = {
|
6
|
+
"lockfile" => "Gemfile.lock",
|
7
|
+
"format" => "table",
|
8
|
+
"fail_on_vulnerabilities" => true,
|
9
|
+
"severity_threshold" => "low",
|
10
|
+
"ignore_vulnerabilities" => [],
|
11
|
+
"ignore_gems" => [],
|
12
|
+
"output_file" => nil,
|
13
|
+
"project_name" => nil,
|
14
|
+
"sbom" => {
|
15
|
+
"format" => "spdx",
|
16
|
+
"include_dev_dependencies" => false
|
17
|
+
},
|
18
|
+
"scan" => {
|
19
|
+
"sources" => ["osv", "ruby_advisory_db"],
|
20
|
+
"timeout" => 30
|
21
|
+
}
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
SEVERITY_LEVELS = %w[low medium high critical].freeze
|
25
|
+
|
26
|
+
def initialize(config_path = ".gemguard.yml")
|
27
|
+
@config_path = config_path
|
28
|
+
@config = load_config
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(key)
|
32
|
+
keys = key.split(".")
|
33
|
+
value = @config
|
34
|
+
|
35
|
+
keys.each do |k|
|
36
|
+
value = value[k] if value.is_a?(Hash)
|
37
|
+
end
|
38
|
+
|
39
|
+
value
|
40
|
+
end
|
41
|
+
|
42
|
+
def set(key, value)
|
43
|
+
keys = key.split(".")
|
44
|
+
target = @config
|
45
|
+
|
46
|
+
keys[0..-2].each do |k|
|
47
|
+
target[k] ||= {}
|
48
|
+
target = target[k]
|
49
|
+
end
|
50
|
+
|
51
|
+
target[keys.last] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
def save
|
55
|
+
File.write(@config_path, YAML.dump(@config))
|
56
|
+
end
|
57
|
+
|
58
|
+
def exists?
|
59
|
+
File.exist?(@config_path)
|
60
|
+
end
|
61
|
+
|
62
|
+
def lockfile_path
|
63
|
+
get("lockfile")
|
64
|
+
end
|
65
|
+
|
66
|
+
def output_format
|
67
|
+
get("format")
|
68
|
+
end
|
69
|
+
|
70
|
+
def fail_on_vulnerabilities?
|
71
|
+
get("fail_on_vulnerabilities")
|
72
|
+
end
|
73
|
+
|
74
|
+
def severity_threshold
|
75
|
+
get("severity_threshold")
|
76
|
+
end
|
77
|
+
|
78
|
+
def ignored_vulnerabilities
|
79
|
+
get("ignore_vulnerabilities") || []
|
80
|
+
end
|
81
|
+
|
82
|
+
def ignored_gems
|
83
|
+
get("ignore_gems") || []
|
84
|
+
end
|
85
|
+
|
86
|
+
def output_file
|
87
|
+
get("output_file")
|
88
|
+
end
|
89
|
+
|
90
|
+
def project_name
|
91
|
+
get("project_name") || detect_project_name
|
92
|
+
end
|
93
|
+
|
94
|
+
def sbom_format
|
95
|
+
get("sbom.format")
|
96
|
+
end
|
97
|
+
|
98
|
+
def include_dev_dependencies?
|
99
|
+
get("sbom.include_dev_dependencies")
|
100
|
+
end
|
101
|
+
|
102
|
+
def vulnerability_sources
|
103
|
+
get("scan.sources")
|
104
|
+
end
|
105
|
+
|
106
|
+
def scan_timeout
|
107
|
+
get("scan.timeout")
|
108
|
+
end
|
109
|
+
|
110
|
+
def should_ignore_vulnerability?(vulnerability_id)
|
111
|
+
ignored_vulnerabilities.include?(vulnerability_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
def should_ignore_gem?(gem_name)
|
115
|
+
ignored_gems.include?(gem_name)
|
116
|
+
end
|
117
|
+
|
118
|
+
def meets_severity_threshold?(severity)
|
119
|
+
return true if severity.nil? || severity.empty?
|
120
|
+
|
121
|
+
severity_index = SEVERITY_LEVELS.index(severity.downcase)
|
122
|
+
threshold_index = SEVERITY_LEVELS.index(severity_threshold.downcase)
|
123
|
+
|
124
|
+
return true if severity_index.nil? || threshold_index.nil?
|
125
|
+
|
126
|
+
severity_index >= threshold_index
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def load_config
|
132
|
+
if File.exist?(@config_path)
|
133
|
+
user_config = YAML.load_file(@config_path) || {}
|
134
|
+
deep_merge(deep_dup(DEFAULT_CONFIG), user_config)
|
135
|
+
else
|
136
|
+
deep_dup(DEFAULT_CONFIG)
|
137
|
+
end
|
138
|
+
rescue Psych::SyntaxError => e
|
139
|
+
puts "Warning: Invalid YAML in #{@config_path}: #{e.message}"
|
140
|
+
puts "Using default configuration."
|
141
|
+
deep_dup(DEFAULT_CONFIG)
|
142
|
+
end
|
143
|
+
|
144
|
+
def deep_dup(obj)
|
145
|
+
case obj
|
146
|
+
when Hash
|
147
|
+
obj.each_with_object({}) { |(key, value), hash| hash[key] = deep_dup(value) }
|
148
|
+
when Array
|
149
|
+
obj.map { |item| deep_dup(item) }
|
150
|
+
else
|
151
|
+
begin
|
152
|
+
obj.dup
|
153
|
+
rescue
|
154
|
+
obj
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def deep_merge(hash1, hash2)
|
160
|
+
result = hash1.dup
|
161
|
+
|
162
|
+
hash2.each do |key, value|
|
163
|
+
result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
|
164
|
+
deep_merge(result[key], value)
|
165
|
+
else
|
166
|
+
value
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
result
|
171
|
+
end
|
172
|
+
|
173
|
+
def detect_project_name
|
174
|
+
# Try to detect project name from various sources
|
175
|
+
if File.exist?("Gemfile")
|
176
|
+
gemfile_content = File.read("Gemfile")
|
177
|
+
if gemfile_content =~ /gem\s+['"]([^'"]+)['"]/
|
178
|
+
return $1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
if File.exist?("*.gemspec")
|
183
|
+
gemspec_files = Dir.glob("*.gemspec")
|
184
|
+
unless gemspec_files.empty?
|
185
|
+
return File.basename(gemspec_files.first, ".gemspec")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Fallback to directory name
|
190
|
+
File.basename(Dir.pwd)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "json"
|
2
|
+
require "digest"
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module GemGuard
|
6
|
+
class SbomGenerator
|
7
|
+
SPDX_VERSION = "SPDX-2.3"
|
8
|
+
CYCLONE_DX_VERSION = "1.5"
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@document_id = "SPDXRef-DOCUMENT"
|
12
|
+
@creation_time = Time.now.utc.iso8601
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_spdx(dependencies, project_name = "ruby-project")
|
16
|
+
{
|
17
|
+
"spdxVersion" => SPDX_VERSION,
|
18
|
+
"dataLicense" => "CC0-1.0",
|
19
|
+
"SPDXID" => @document_id,
|
20
|
+
"name" => "#{project_name}-sbom",
|
21
|
+
"documentNamespace" => "https://gem-guard.dev/#{project_name}/#{@creation_time}",
|
22
|
+
"creationInfo" => {
|
23
|
+
"created" => @creation_time,
|
24
|
+
"creators" => ["Tool: gem_guard-#{GemGuard::VERSION}"],
|
25
|
+
"licenseListVersion" => "3.21"
|
26
|
+
},
|
27
|
+
"packages" => build_spdx_packages(dependencies, project_name),
|
28
|
+
"relationships" => build_spdx_relationships(dependencies)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_cyclone_dx(dependencies, project_name = "ruby-project")
|
33
|
+
{
|
34
|
+
"bomFormat" => "CycloneDX",
|
35
|
+
"specVersion" => CYCLONE_DX_VERSION,
|
36
|
+
"serialNumber" => "urn:uuid:#{generate_uuid}",
|
37
|
+
"version" => 1,
|
38
|
+
"metadata" => {
|
39
|
+
"timestamp" => @creation_time,
|
40
|
+
"tools" => [
|
41
|
+
{
|
42
|
+
"vendor" => "GemGuard",
|
43
|
+
"name" => "gem_guard",
|
44
|
+
"version" => GemGuard::VERSION
|
45
|
+
}
|
46
|
+
],
|
47
|
+
"component" => {
|
48
|
+
"type" => "application",
|
49
|
+
"name" => project_name,
|
50
|
+
"version" => "1.0.0"
|
51
|
+
}
|
52
|
+
},
|
53
|
+
"components" => build_cyclone_dx_components(dependencies)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def build_spdx_packages(dependencies, project_name)
|
60
|
+
packages = []
|
61
|
+
|
62
|
+
# Add root package
|
63
|
+
packages << {
|
64
|
+
"SPDXID" => "SPDXRef-Package-#{sanitize_name(project_name)}",
|
65
|
+
"name" => project_name,
|
66
|
+
"downloadLocation" => "NOASSERTION",
|
67
|
+
"filesAnalyzed" => false,
|
68
|
+
"copyrightText" => "NOASSERTION"
|
69
|
+
}
|
70
|
+
|
71
|
+
# Add dependency packages
|
72
|
+
dependencies.each_with_index do |dep, index|
|
73
|
+
packages << {
|
74
|
+
"SPDXID" => "SPDXRef-Package-#{sanitize_name(dep.name)}",
|
75
|
+
"name" => dep.name,
|
76
|
+
"versionInfo" => dep.version,
|
77
|
+
"downloadLocation" => gem_download_url(dep.name, dep.version),
|
78
|
+
"filesAnalyzed" => false,
|
79
|
+
"homepage" => gem_homepage_url(dep.name),
|
80
|
+
"copyrightText" => "NOASSERTION",
|
81
|
+
"externalRefs" => [
|
82
|
+
{
|
83
|
+
"referenceCategory" => "PACKAGE-MANAGER",
|
84
|
+
"referenceType" => "purl",
|
85
|
+
"referenceLocator" => "pkg:gem/#{dep.name}@#{dep.version}"
|
86
|
+
}
|
87
|
+
]
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
packages
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_spdx_relationships(dependencies)
|
95
|
+
relationships = []
|
96
|
+
|
97
|
+
dependencies.each do |dep|
|
98
|
+
relationships << {
|
99
|
+
"spdxElementId" => @document_id,
|
100
|
+
"relationshipType" => "DESCRIBES",
|
101
|
+
"relatedSpdxElement" => "SPDXRef-Package-#{sanitize_name(dep.name)}"
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
relationships
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_cyclone_dx_components(dependencies)
|
109
|
+
dependencies.map do |dep|
|
110
|
+
{
|
111
|
+
"type" => "library",
|
112
|
+
"bom-ref" => "pkg:gem/#{dep.name}@#{dep.version}",
|
113
|
+
"name" => dep.name,
|
114
|
+
"version" => dep.version,
|
115
|
+
"purl" => "pkg:gem/#{dep.name}@#{dep.version}",
|
116
|
+
"externalReferences" => [
|
117
|
+
{
|
118
|
+
"type" => "distribution",
|
119
|
+
"url" => gem_download_url(dep.name, dep.version)
|
120
|
+
},
|
121
|
+
{
|
122
|
+
"type" => "website",
|
123
|
+
"url" => gem_homepage_url(dep.name)
|
124
|
+
}
|
125
|
+
]
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def sanitize_name(name)
|
131
|
+
name.gsub(/[^a-zA-Z0-9\-_]/, "-")
|
132
|
+
end
|
133
|
+
|
134
|
+
def gem_download_url(name, version)
|
135
|
+
"https://rubygems.org/downloads/#{name}-#{version}.gem"
|
136
|
+
end
|
137
|
+
|
138
|
+
def gem_homepage_url(name)
|
139
|
+
"https://rubygems.org/gems/#{name}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def generate_uuid
|
143
|
+
# Simple UUID v4 generation
|
144
|
+
bytes = Array.new(16) { rand(256) }
|
145
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40 # Version 4
|
146
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80 # Variant bits
|
147
|
+
|
148
|
+
format = "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
|
149
|
+
format % bytes
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/lib/gem_guard/version.rb
CHANGED
data/lib/gem_guard.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require_relative "gem_guard/version"
|
2
|
-
require_relative "gem_guard/cli"
|
3
2
|
require_relative "gem_guard/parser"
|
4
3
|
require_relative "gem_guard/vulnerability_fetcher"
|
5
4
|
require_relative "gem_guard/analyzer"
|
6
5
|
require_relative "gem_guard/reporter"
|
6
|
+
require_relative "gem_guard/sbom_generator"
|
7
|
+
require_relative "gem_guard/config"
|
8
|
+
require_relative "gem_guard/cli"
|
7
9
|
|
8
10
|
module GemGuard
|
9
11
|
class Error < StandardError; end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# CircleCI configuration for GemGuard security scanning
|
2
|
+
# Copy this content to .circleci/config.yml in your repository
|
3
|
+
|
4
|
+
version: 2.1
|
5
|
+
|
6
|
+
orbs:
|
7
|
+
ruby: circleci/ruby@2.1.0
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
security-scan:
|
11
|
+
docker:
|
12
|
+
- image: cimg/ruby:3.3
|
13
|
+
parameters:
|
14
|
+
ruby-version:
|
15
|
+
type: string
|
16
|
+
default: "3.3"
|
17
|
+
steps:
|
18
|
+
- checkout
|
19
|
+
|
20
|
+
- ruby/install-deps:
|
21
|
+
bundler-version: "2.4.0"
|
22
|
+
|
23
|
+
- run:
|
24
|
+
name: Install GemGuard
|
25
|
+
command: gem install gem_guard
|
26
|
+
|
27
|
+
- run:
|
28
|
+
name: Run vulnerability scan
|
29
|
+
command: |
|
30
|
+
echo "Running GemGuard security scan..."
|
31
|
+
gem_guard scan --format json --output security-report.json
|
32
|
+
gem_guard scan --format table
|
33
|
+
|
34
|
+
- run:
|
35
|
+
name: Generate SBOM
|
36
|
+
command: |
|
37
|
+
echo "Generating Software Bill of Materials..."
|
38
|
+
gem_guard sbom --format spdx --output sbom-spdx.json
|
39
|
+
gem_guard sbom --format cyclone-dx --output sbom-cyclone.json
|
40
|
+
|
41
|
+
- store_artifacts:
|
42
|
+
path: security-report.json
|
43
|
+
destination: security-reports/
|
44
|
+
|
45
|
+
- store_artifacts:
|
46
|
+
path: sbom-spdx.json
|
47
|
+
destination: sbom/
|
48
|
+
|
49
|
+
- store_artifacts:
|
50
|
+
path: sbom-cyclone.json
|
51
|
+
destination: sbom/
|
52
|
+
|
53
|
+
- run:
|
54
|
+
name: Check for vulnerabilities
|
55
|
+
command: |
|
56
|
+
if [ -f security-report.json ]; then
|
57
|
+
VULN_COUNT=$(ruby -rjson -e "puts JSON.parse(File.read('security-report.json'))['vulnerabilities']&.length || 0")
|
58
|
+
echo "Found $VULN_COUNT vulnerabilities"
|
59
|
+
|
60
|
+
if [ "$VULN_COUNT" -gt 0 ]; then
|
61
|
+
echo "⚠️ Vulnerabilities detected! Check the artifacts for details."
|
62
|
+
exit 1
|
63
|
+
else
|
64
|
+
echo "✅ No vulnerabilities found!"
|
65
|
+
fi
|
66
|
+
fi
|
67
|
+
|
68
|
+
security-scan-matrix:
|
69
|
+
docker:
|
70
|
+
- image: cimg/ruby:<< parameters.ruby-version >>
|
71
|
+
parameters:
|
72
|
+
ruby-version:
|
73
|
+
type: string
|
74
|
+
steps:
|
75
|
+
- checkout
|
76
|
+
- ruby/install-deps
|
77
|
+
- run:
|
78
|
+
name: Install GemGuard
|
79
|
+
command: gem install gem_guard
|
80
|
+
- run:
|
81
|
+
name: Security scan for Ruby << parameters.ruby-version >>
|
82
|
+
command: |
|
83
|
+
gem_guard scan --format json --output security-report-<< parameters.ruby-version >>.json
|
84
|
+
gem_guard scan
|
85
|
+
- store_artifacts:
|
86
|
+
path: security-report-<< parameters.ruby-version >>.json
|
87
|
+
|
88
|
+
workflows:
|
89
|
+
security-checks:
|
90
|
+
jobs:
|
91
|
+
- security-scan:
|
92
|
+
name: security-scan-main
|
93
|
+
|
94
|
+
security-matrix:
|
95
|
+
jobs:
|
96
|
+
- security-scan-matrix:
|
97
|
+
matrix:
|
98
|
+
parameters:
|
99
|
+
ruby-version: ["3.1", "3.2", "3.3"]
|
100
|
+
name: security-scan-ruby-<< matrix.ruby-version >>
|
101
|
+
triggers:
|
102
|
+
- schedule:
|
103
|
+
cron: "0 2 * * *" # Daily at 2 AM UTC
|
104
|
+
filters:
|
105
|
+
branches:
|
106
|
+
only:
|
107
|
+
- main
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# GitHub Actions workflow for GemGuard security scanning
|
2
|
+
# Copy this file to .github/workflows/gemguard.yml in your repository
|
3
|
+
|
4
|
+
name: Security Scan with GemGuard
|
5
|
+
|
6
|
+
on:
|
7
|
+
push:
|
8
|
+
branches: [ main, develop ]
|
9
|
+
pull_request:
|
10
|
+
branches: [ main ]
|
11
|
+
schedule:
|
12
|
+
# Run daily at 2 AM UTC
|
13
|
+
- cron: '0 2 * * *'
|
14
|
+
|
15
|
+
jobs:
|
16
|
+
security-scan:
|
17
|
+
runs-on: ubuntu-latest
|
18
|
+
|
19
|
+
strategy:
|
20
|
+
matrix:
|
21
|
+
ruby-version: ['3.1', '3.2', '3.3']
|
22
|
+
|
23
|
+
steps:
|
24
|
+
- uses: actions/checkout@v4
|
25
|
+
|
26
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
27
|
+
uses: ruby/setup-ruby@v1
|
28
|
+
with:
|
29
|
+
ruby-version: ${{ matrix.ruby-version }}
|
30
|
+
bundler-cache: true
|
31
|
+
|
32
|
+
- name: Install GemGuard
|
33
|
+
run: gem install gem_guard
|
34
|
+
|
35
|
+
- name: Run GemGuard vulnerability scan
|
36
|
+
run: |
|
37
|
+
gem_guard scan --format json --output security-report.json
|
38
|
+
gem_guard scan --format table
|
39
|
+
|
40
|
+
- name: Generate SBOM
|
41
|
+
run: |
|
42
|
+
gem_guard sbom --format spdx --output sbom-spdx.json
|
43
|
+
gem_guard sbom --format cyclone-dx --output sbom-cyclone.json
|
44
|
+
|
45
|
+
- name: Upload security artifacts
|
46
|
+
uses: actions/upload-artifact@v4
|
47
|
+
if: always()
|
48
|
+
with:
|
49
|
+
name: security-reports-ruby-${{ matrix.ruby-version }}
|
50
|
+
path: |
|
51
|
+
security-report.json
|
52
|
+
sbom-spdx.json
|
53
|
+
sbom-cyclone.json
|
54
|
+
retention-days: 30
|
55
|
+
|
56
|
+
- name: Comment PR with security report
|
57
|
+
if: github.event_name == 'pull_request'
|
58
|
+
uses: actions/github-script@v7
|
59
|
+
with:
|
60
|
+
script: |
|
61
|
+
const fs = require('fs');
|
62
|
+
try {
|
63
|
+
const report = fs.readFileSync('security-report.json', 'utf8');
|
64
|
+
const data = JSON.parse(report);
|
65
|
+
|
66
|
+
if (data.vulnerabilities && data.vulnerabilities.length > 0) {
|
67
|
+
const comment = `## 🚨 Security Vulnerabilities Found
|
68
|
+
|
69
|
+
GemGuard detected ${data.vulnerabilities.length} vulnerabilities in this PR.
|
70
|
+
|
71
|
+
Please review the security report artifact for details.
|
72
|
+
|
73
|
+
**High/Critical vulnerabilities:** ${data.high_severity_count || 0}
|
74
|
+
`;
|
75
|
+
|
76
|
+
github.rest.issues.createComment({
|
77
|
+
issue_number: context.issue.number,
|
78
|
+
owner: context.repo.owner,
|
79
|
+
repo: context.repo.repo,
|
80
|
+
body: comment
|
81
|
+
});
|
82
|
+
}
|
83
|
+
} catch (error) {
|
84
|
+
console.log('No security report found or error reading report');
|
85
|
+
}
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# GitLab CI configuration for GemGuard security scanning
|
2
|
+
# Copy this content to your .gitlab-ci.yml file
|
3
|
+
|
4
|
+
stages:
|
5
|
+
- security
|
6
|
+
- report
|
7
|
+
|
8
|
+
variables:
|
9
|
+
BUNDLE_PATH: vendor/bundle
|
10
|
+
BUNDLE_JOBS: 4
|
11
|
+
BUNDLE_RETRY: 3
|
12
|
+
|
13
|
+
.ruby_template: &ruby_template
|
14
|
+
image: ruby:3.3
|
15
|
+
before_script:
|
16
|
+
- gem install bundler
|
17
|
+
- bundle install --path $BUNDLE_PATH
|
18
|
+
- gem install gem_guard
|
19
|
+
cache:
|
20
|
+
key: gems-$CI_COMMIT_REF_SLUG
|
21
|
+
paths:
|
22
|
+
- vendor/bundle/
|
23
|
+
|
24
|
+
security_scan:
|
25
|
+
<<: *ruby_template
|
26
|
+
stage: security
|
27
|
+
script:
|
28
|
+
- echo "Running GemGuard security scan..."
|
29
|
+
- gem_guard scan --format json --output security-report.json
|
30
|
+
- gem_guard scan --format table
|
31
|
+
- echo "Generating SBOM..."
|
32
|
+
- gem_guard sbom --format spdx --output sbom-spdx.json
|
33
|
+
- gem_guard sbom --format cyclone-dx --output sbom-cyclone.json
|
34
|
+
artifacts:
|
35
|
+
reports:
|
36
|
+
# GitLab security report format (if you want to convert)
|
37
|
+
dependency_scanning: security-report.json
|
38
|
+
paths:
|
39
|
+
- security-report.json
|
40
|
+
- sbom-spdx.json
|
41
|
+
- sbom-cyclone.json
|
42
|
+
expire_in: 30 days
|
43
|
+
when: always
|
44
|
+
allow_failure: false
|
45
|
+
only:
|
46
|
+
- main
|
47
|
+
- develop
|
48
|
+
- merge_requests
|
49
|
+
|
50
|
+
security_scan_ruby_3_1:
|
51
|
+
<<: *ruby_template
|
52
|
+
image: ruby:3.1
|
53
|
+
stage: security
|
54
|
+
script:
|
55
|
+
- gem_guard scan --format json --output security-report-ruby31.json
|
56
|
+
- gem_guard scan
|
57
|
+
artifacts:
|
58
|
+
paths:
|
59
|
+
- security-report-ruby31.json
|
60
|
+
expire_in: 7 days
|
61
|
+
only:
|
62
|
+
- schedules
|
63
|
+
|
64
|
+
security_scan_ruby_3_2:
|
65
|
+
<<: *ruby_template
|
66
|
+
image: ruby:3.2
|
67
|
+
stage: security
|
68
|
+
script:
|
69
|
+
- gem_guard scan --format json --output security-report-ruby32.json
|
70
|
+
- gem_guard scan
|
71
|
+
artifacts:
|
72
|
+
paths:
|
73
|
+
- security-report-ruby32.json
|
74
|
+
expire_in: 7 days
|
75
|
+
only:
|
76
|
+
- schedules
|
77
|
+
|
78
|
+
# Optional: Create a summary report
|
79
|
+
security_report:
|
80
|
+
stage: report
|
81
|
+
image: alpine:latest
|
82
|
+
before_script:
|
83
|
+
- apk add --no-cache jq
|
84
|
+
script:
|
85
|
+
- |
|
86
|
+
echo "## Security Scan Summary" > security-summary.md
|
87
|
+
echo "" >> security-summary.md
|
88
|
+
if [ -f security-report.json ]; then
|
89
|
+
VULN_COUNT=$(jq '.vulnerabilities | length' security-report.json)
|
90
|
+
HIGH_COUNT=$(jq '.high_severity_count // 0' security-report.json)
|
91
|
+
echo "- **Total vulnerabilities found:** $VULN_COUNT" >> security-summary.md
|
92
|
+
echo "- **High/Critical severity:** $HIGH_COUNT" >> security-summary.md
|
93
|
+
echo "" >> security-summary.md
|
94
|
+
|
95
|
+
if [ "$VULN_COUNT" -gt 0 ]; then
|
96
|
+
echo "⚠️ **Action required:** Please review and address the identified vulnerabilities." >> security-summary.md
|
97
|
+
else
|
98
|
+
echo "✅ **No vulnerabilities found!**" >> security-summary.md
|
99
|
+
fi
|
100
|
+
else
|
101
|
+
echo "❌ **Error:** Security report not found." >> security-summary.md
|
102
|
+
fi
|
103
|
+
cat security-summary.md
|
104
|
+
artifacts:
|
105
|
+
paths:
|
106
|
+
- security-summary.md
|
107
|
+
expire_in: 30 days
|
108
|
+
dependencies:
|
109
|
+
- security_scan
|
110
|
+
only:
|
111
|
+
- main
|
112
|
+
- develop
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gem_guard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilbur Suero
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -101,11 +101,16 @@ files:
|
|
101
101
|
- lib/gem_guard.rb
|
102
102
|
- lib/gem_guard/analyzer.rb
|
103
103
|
- lib/gem_guard/cli.rb
|
104
|
+
- lib/gem_guard/config.rb
|
104
105
|
- lib/gem_guard/parser.rb
|
105
106
|
- lib/gem_guard/reporter.rb
|
107
|
+
- lib/gem_guard/sbom_generator.rb
|
106
108
|
- lib/gem_guard/version.rb
|
107
109
|
- lib/gem_guard/vulnerability_fetcher.rb
|
108
110
|
- plan.md
|
111
|
+
- templates/circleci-config.yml
|
112
|
+
- templates/github-actions.yml
|
113
|
+
- templates/gitlab-ci.yml
|
109
114
|
homepage: https://github.com/wilburhimself/gem_guard
|
110
115
|
licenses:
|
111
116
|
- MIT
|