gem_guard 0.1.6 → 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 +147 -9
- data/lib/gem_guard/config.rb +193 -0
- data/lib/gem_guard/version.rb +1 -1
- data/lib/gem_guard.rb +1 -0
- data/templates/circleci-config.yml +107 -0
- data/templates/github-actions.yml +85 -0
- data/templates/gitlab-ci.yml +112 -0
- metadata +6 -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,25 +1,65 @@
|
|
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
|
9
|
-
|
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
|
10
27
|
|
11
28
|
unless File.exist?(lockfile_path)
|
12
29
|
puts "Error: #{lockfile_path} not found"
|
13
|
-
exit
|
30
|
+
exit EXIT_ERROR
|
14
31
|
end
|
15
32
|
|
16
|
-
|
17
|
-
|
18
|
-
|
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)
|
19
44
|
|
20
|
-
|
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
|
21
52
|
|
22
|
-
|
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
|
23
63
|
end
|
24
64
|
|
25
65
|
desc "sbom", "Generate Software Bill of Materials (SBOM)"
|
@@ -58,9 +98,107 @@ module GemGuard
|
|
58
98
|
end
|
59
99
|
end
|
60
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])
|
107
|
+
|
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
|
149
|
+
end
|
150
|
+
|
61
151
|
desc "version", "Show gem_guard version"
|
62
152
|
def version
|
63
153
|
puts GemGuard::VERSION
|
64
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
|
65
203
|
end
|
66
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
|
data/lib/gem_guard/version.rb
CHANGED
data/lib/gem_guard.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "gem_guard/vulnerability_fetcher"
|
|
4
4
|
require_relative "gem_guard/analyzer"
|
5
5
|
require_relative "gem_guard/reporter"
|
6
6
|
require_relative "gem_guard/sbom_generator"
|
7
|
+
require_relative "gem_guard/config"
|
7
8
|
require_relative "gem_guard/cli"
|
8
9
|
|
9
10
|
module GemGuard
|
@@ -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,12 +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
|
106
107
|
- lib/gem_guard/sbom_generator.rb
|
107
108
|
- lib/gem_guard/version.rb
|
108
109
|
- lib/gem_guard/vulnerability_fetcher.rb
|
109
110
|
- plan.md
|
111
|
+
- templates/circleci-config.yml
|
112
|
+
- templates/github-actions.yml
|
113
|
+
- templates/gitlab-ci.yml
|
110
114
|
homepage: https://github.com/wilburhimself/gem_guard
|
111
115
|
licenses:
|
112
116
|
- MIT
|