dependaboat 0.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 +7 -0
- data/bin/dependaboat +8 -0
- data/lib/dependaboat/cli.rb +220 -0
- data/lib/dependaboat.rb +15 -0
- data/lib/version.rb +3 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 14a88024f9d6d27f1365bfbfe5575ffaf2d8f0b4e84b04eccbe1ae70489da90b
|
4
|
+
data.tar.gz: a5b7200f42a6658de3e5903c6f408097588bd486b1c09452b2762936f34ca691
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: edaddab17c68a16780fd21f7a5318fea7c23d09aad516bcc5bd5b957249ce7d78e344ba9ad12d6bfff6e1d45e2340ed835a890c32e437927c1b380b2bd255e03
|
7
|
+
data.tar.gz: 416daac0c5e9d9e125649c193022c8f7eef01843f3d42013a408c8bb729bc5810ba75e6273ae8226948fee8dd4a35447454fc7aef6a3e96a4be67099964106fc
|
data/bin/dependaboat
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
require "optionparser"
|
2
|
+
require "yaml"
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module Dependaboat
|
6
|
+
class Cli
|
7
|
+
attr_reader :project_id, :owner, :repo, :project, :config
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
def self.run
|
11
|
+
new(*ARGV).run
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(*argv)
|
15
|
+
@dry_run = false # Default
|
16
|
+
@logger = Dependaboat.logger
|
17
|
+
options_parser.parse(argv)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
initialize_project
|
22
|
+
fetch_and_process_alerts
|
23
|
+
logger.info "Run complete."
|
24
|
+
0
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def load_config(path)
|
30
|
+
config_file = File.expand_path(path)
|
31
|
+
raise "Could not find config file" unless File.exist?(config_file)
|
32
|
+
|
33
|
+
@config = YAML.load_file(config_file)
|
34
|
+
logger.info "Loaded config from #{config_file}"
|
35
|
+
logger.info @config
|
36
|
+
|
37
|
+
@project_id = config.dig("github", "project_id")
|
38
|
+
@owner = config.dig("github", "owner")
|
39
|
+
@repo = config.dig("github", "repo")
|
40
|
+
@assignees = config.dig("github", "issue", "assignees") || {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize_project
|
44
|
+
@project = GHX::Project.new(project_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch_and_process_alerts
|
48
|
+
@alerts = GHX::Dependabot.get_alerts(owner: owner, repo: repo)
|
49
|
+
logger.info("Found #{@alerts.size} Dependabot alerts")
|
50
|
+
|
51
|
+
@alerts.each do |alert|
|
52
|
+
process_alert(alert)
|
53
|
+
sleep 1 # Rate limiting
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_alert(alert)
|
58
|
+
return if issue_exists?(alert)
|
59
|
+
|
60
|
+
alert_details = extract_alert_details(alert)
|
61
|
+
create_github_issue(alert, alert_details)
|
62
|
+
rescue StandardError => e
|
63
|
+
logger.error "Error processing alert ##{alert.number}: #{e.message}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def issue_exists?(alert)
|
67
|
+
existing_issue = GHX::Issue.search(owner: owner, repo: repo, query: issue_lookup_key(alert)).any?
|
68
|
+
if existing_issue
|
69
|
+
logger.info " Issue already exists for alert ##{alert.number}. Skipping."
|
70
|
+
end
|
71
|
+
existing_issue
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_alert_details(alert)
|
75
|
+
alert_number = alert.number
|
76
|
+
alert_severity = alert.security_vulnerability.severity.capitalize
|
77
|
+
alert_package_name = alert.security_vulnerability.package.name
|
78
|
+
alert_package_ecosystem = alert.security_vulnerability.package.ecosystem
|
79
|
+
alert_created_at = alert.created_at.to_date rescue Date.today
|
80
|
+
|
81
|
+
remediation_deadline = alert_created_at + config.dig("remediation_sla", alert_severity.downcase)
|
82
|
+
|
83
|
+
{
|
84
|
+
number: alert_number,
|
85
|
+
severity: alert_severity,
|
86
|
+
package_name: alert_package_name,
|
87
|
+
package_ecosystem: alert_package_ecosystem,
|
88
|
+
created_at: alert_created_at,
|
89
|
+
remediation_deadline: remediation_deadline
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_github_issue(alert, details)
|
94
|
+
template_variable_map = build_template_variable_map(details)
|
95
|
+
|
96
|
+
logger.info "Processing alert ##{details[:number]} (#{details[:severity].upcase}) in #{details[:package_name]} (#{details[:package_ecosystem]}) created at #{details[:created_at]}"
|
97
|
+
|
98
|
+
logger.info " Creating new issue for this alert."
|
99
|
+
|
100
|
+
title = build_issue_title(alert, template_variable_map)
|
101
|
+
body = build_issue_body(template_variable_map)
|
102
|
+
labels = build_issue_labels(template_variable_map)
|
103
|
+
|
104
|
+
issue = build_github_issue(alert, title, body, labels)
|
105
|
+
|
106
|
+
if dry_run?
|
107
|
+
log_dry_run(issue)
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
save_issue_and_update_project(alert, issue, template_variable_map)
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_template_variable_map(details)
|
115
|
+
{
|
116
|
+
"alert_number" => details[:number],
|
117
|
+
"alert_severity" => details[:severity],
|
118
|
+
"alert_package_name" => details[:package_name],
|
119
|
+
"alert_package_ecosystem" => details[:package_ecosystem],
|
120
|
+
"alert_created_at" => details[:created_at],
|
121
|
+
"remediation_deadline" => details[:remediation_deadline].strftime("%Y-%m-%d")
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_issue_title(alert, template_variable_map)
|
126
|
+
issue_lookup_key(alert) + " " + process_templateable_string(config.dig("github", "issue", "title"), template_variable_map)
|
127
|
+
end
|
128
|
+
|
129
|
+
def build_issue_body(template_variable_map)
|
130
|
+
process_templateable_string(config.dig("github", "issue", "body"), template_variable_map)
|
131
|
+
end
|
132
|
+
|
133
|
+
def build_issue_labels(template_variable_map)
|
134
|
+
labels_config = config.dig("github", "issue", "labels") || []
|
135
|
+
labels_config.map { |template| process_templateable_string(template, template_variable_map) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_github_issue(alert, title, body, labels)
|
139
|
+
GHX::Issue.new(
|
140
|
+
owner: owner,
|
141
|
+
repo: repo,
|
142
|
+
title: title,
|
143
|
+
body: body,
|
144
|
+
labels: labels,
|
145
|
+
assignees: assignees_for_ecosystem(alert.security_vulnerability.package.ecosystem)
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def assignees_for_ecosystem(ecosystem)
|
150
|
+
assignees = Array(@assignees[ecosystem] || @assignees["other"])
|
151
|
+
assignees << @assignees["all"]
|
152
|
+
assignees.flatten.compact.uniq
|
153
|
+
end
|
154
|
+
|
155
|
+
def log_dry_run(issue)
|
156
|
+
logger.info " Dry Run: Would have created issue:"
|
157
|
+
logger.info issue.inspect
|
158
|
+
end
|
159
|
+
|
160
|
+
def save_issue_and_update_project(alert, issue, template_variable_map)
|
161
|
+
issue.save
|
162
|
+
logger.info " Created Github Issue ##{issue.number} for alert ##{alert.number}"
|
163
|
+
|
164
|
+
logger.info " Waiting for GH automation to run and create the associated GH Project Item..."
|
165
|
+
sleep 5
|
166
|
+
|
167
|
+
project_item = fetch_project_item(issue)
|
168
|
+
update_project_item(project_item, template_variable_map)
|
169
|
+
end
|
170
|
+
|
171
|
+
def fetch_project_item(issue)
|
172
|
+
project.find_item_by_issue_number(owner: owner, repo: repo, number: issue.number).tap do |project_item|
|
173
|
+
logger.info " Found project item #{project_item.id} for issue ##{issue.number}."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def update_project_item(project_item, template_variable_map)
|
178
|
+
logger.info " Updating Project Item with additional data..."
|
179
|
+
|
180
|
+
config.dig("github", "project_item", "field_map").each do |field_map|
|
181
|
+
field_name = field_map["field_name"]
|
182
|
+
field_value = field_map["field_value"]
|
183
|
+
|
184
|
+
logger.debug " #{field_name} => #{field_value}"
|
185
|
+
project_item.update(field_name => process_templateable_string(field_value, template_variable_map))
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def issue_lookup_key(alert)
|
190
|
+
"[#{@repo}/DB #{alert.number}]"
|
191
|
+
end
|
192
|
+
|
193
|
+
def options_parser
|
194
|
+
OptionParser.new do |opts|
|
195
|
+
opts.banner = "Usage: dependaboat [options]"
|
196
|
+
|
197
|
+
opts.on("-cCONFIG_FILE", "--config-file=CONFIG_FILE", "The path to the config file") do |config_file|
|
198
|
+
load_config(config_file)
|
199
|
+
end
|
200
|
+
|
201
|
+
opts.on("-d", "--dry-run", "Run in dry-run mode") do
|
202
|
+
@dry_run = true
|
203
|
+
end
|
204
|
+
|
205
|
+
opts.on("-h", "--help", "Prints this help") do
|
206
|
+
puts opts
|
207
|
+
exit
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def process_templateable_string(s, map)
|
213
|
+
map.reduce(s.dup) { |str, (key, value)| str.gsub!("{{#{key}}}", value.to_s); str }
|
214
|
+
end
|
215
|
+
|
216
|
+
def dry_run?
|
217
|
+
@dry_run
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/lib/dependaboat.rb
ADDED
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dependaboat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- CompanyCam
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-05-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ghx
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dotenv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.1.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.9.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.9.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: faraday-retry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.2.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.2.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: standardrb
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: debug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Create GitHub Issues and GitHub Project Items from Dependabot alerts
|
112
|
+
email: jeff.mcfadden@companycam.com
|
113
|
+
executables:
|
114
|
+
- dependaboat
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- bin/dependaboat
|
119
|
+
- lib/dependaboat.rb
|
120
|
+
- lib/dependaboat/cli.rb
|
121
|
+
- lib/version.rb
|
122
|
+
homepage: https://github.com/companycam/dependaboat
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubygems_version: 3.5.10
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Ferry Dependabot alerts to GitHub Issues and Projects
|
145
|
+
test_files: []
|