dbotquery 0.9
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/.idea/.gitignore +10 -0
- data/.idea/dbotquery.iml +130 -0
- data/.idea/dictionaries/project.xml +9 -0
- data/.idea/modules.xml +8 -0
- data/.idea/runConfigurations/Specs.xml +33 -0
- data/.idea/vcs.xml +6 -0
- data/.reek.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/NOTES.md +38 -0
- data/README.md +34 -0
- data/Rakefile +32 -0
- data/Steepfile +25 -0
- data/exe/dbotquery +8 -0
- data/lib/d_bot_query/cli.rb +74 -0
- data/lib/d_bot_query/commands/command_base.rb +51 -0
- data/lib/d_bot_query/commands/package_search_command.rb +40 -0
- data/lib/d_bot_query/commands/repo_sla_statistics_command.rb +52 -0
- data/lib/d_bot_query/commands/sla_statistics_command.rb +62 -0
- data/lib/d_bot_query/commands/summary_command.rb +51 -0
- data/lib/d_bot_query/models/alert.rb +40 -0
- data/lib/d_bot_query/models/alert_collection.rb +89 -0
- data/lib/d_bot_query/models/base.rb +128 -0
- data/lib/d_bot_query/models/cvss.rb +11 -0
- data/lib/d_bot_query/models/cwe.rb +8 -0
- data/lib/d_bot_query/models/data_file.rb +30 -0
- data/lib/d_bot_query/models/dependency.rb +12 -0
- data/lib/d_bot_query/models/package.rb +23 -0
- data/lib/d_bot_query/models/reference.rb +11 -0
- data/lib/d_bot_query/models/repository.rb +42 -0
- data/lib/d_bot_query/models/security_advisory.rb +22 -0
- data/lib/d_bot_query/models/security_vulnerability.rb +19 -0
- data/lib/d_bot_query/models/vulnerability_identifier.rb +14 -0
- data/lib/d_bot_query/version.rb +6 -0
- data/lib/dbotquery.rb +79 -0
- data/lib/schema.json +1255 -0
- data/rbs_collection.lock.yaml +316 -0
- data/rbs_collection.yaml +19 -0
- data/sig/d_bot_query/cli.rbs +26 -0
- data/sig/d_bot_query/commands/command_base.rbs +18 -0
- data/sig/d_bot_query/commands/package_search_command.rbs +11 -0
- data/sig/d_bot_query/commands/repo_sla_statistics_command.rbs +9 -0
- data/sig/d_bot_query/commands/sla_statistics_command.rbs +11 -0
- data/sig/d_bot_query/commands/summary_command.rbs +15 -0
- data/sig/d_bot_query/models/alert.rbs +17 -0
- data/sig/d_bot_query/models/alert_collection.rbs +41 -0
- data/sig/d_bot_query/models/base.rbs +39 -0
- data/sig/d_bot_query/models/data_file.rbs +21 -0
- data/sig/d_bot_query/models/dependency.rbs +7 -0
- data/sig/d_bot_query/models/package.rbs +10 -0
- data/sig/d_bot_query/models/repository.rbs +11 -0
- data/sig/d_bot_query/models/security_advisory.rbs +9 -0
- data/sig/d_bot_query.rbs +14 -0
- data/sig/gems/json-schema.rbs +5 -0
- metadata +157 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module DBotQuery
|
|
5
|
+
module Commands
|
|
6
|
+
# # Service Level Agreement Stats
|
|
7
|
+
# As part of our vulnerability management program, we need to be able to understand how many
|
|
8
|
+
# open vulnerabilities there are, as well as how long they’ve been open for. For each severity, we
|
|
9
|
+
# have a “service level agreement” (SLA) that states the maximum acceptable time for a
|
|
10
|
+
# vulnerability is allowed to remain unresolved.
|
|
11
|
+
#
|
|
12
|
+
# The SLA for each severity is as follows:
|
|
13
|
+
#
|
|
14
|
+
# | Type | Days |
|
|
15
|
+
# |------|------|
|
|
16
|
+
# | Low | 60|
|
|
17
|
+
# | Medium | 30|
|
|
18
|
+
# | High | 15|
|
|
19
|
+
# | Critical | 5|
|
|
20
|
+
#
|
|
21
|
+
# For this subcommand, we should output the number of vulnerabilities that have been open
|
|
22
|
+
# longer than the SLA allows for, separated by severity.
|
|
23
|
+
#
|
|
24
|
+
# ## Inputs
|
|
25
|
+
# | Command Line Flag | Required? | Description | Example |
|
|
26
|
+
# | :--- | :--- | :--- | :--- |
|
|
27
|
+
# | -f/--file | yes | Path to JSON file exported from Dependabot | dependabot.json |
|
|
28
|
+
# | --time | no | The date/time for which the SLA is being calculated, in ISO8601 format | 2023-01-01T00:00:00Z |
|
|
29
|
+
#
|
|
30
|
+
# ### Example commands
|
|
31
|
+
# * `./your_program sla_stats -f dependabot.json`
|
|
32
|
+
# * A count of all findings that exceed the SLA, separated by severity. The current time is used
|
|
33
|
+
# when calculating age.
|
|
34
|
+
# * `./your_program sla_stats -f dependabot.json -t 2023-01-01T00:00:00Z`
|
|
35
|
+
# * A count of all findings that exceed the SLA, separated by severity. The time specified is
|
|
36
|
+
# used when calculating age.
|
|
37
|
+
#
|
|
38
|
+
# ### Outputs
|
|
39
|
+
# ```json
|
|
40
|
+
# {
|
|
41
|
+
# "low": $count,
|
|
42
|
+
# "medium": $count,
|
|
43
|
+
# "high": $count,
|
|
44
|
+
# "critical": $count,
|
|
45
|
+
# "total": $count
|
|
46
|
+
# }
|
|
47
|
+
# ```
|
|
48
|
+
#
|
|
49
|
+
# Where `$count` represents the number of findings that exceed the SLA for each severity.
|
|
50
|
+
class SLAStatisticsCommand < CommandBase
|
|
51
|
+
def initialize(time: nil)
|
|
52
|
+
super()
|
|
53
|
+
|
|
54
|
+
@time = time || Time.now
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def perform(input)
|
|
58
|
+
input.alerts.sla_stats @time
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module DBotQuery
|
|
5
|
+
module Commands
|
|
6
|
+
# # Summary
|
|
7
|
+
#
|
|
8
|
+
# This subcommand takes a JSON export from Dependabot and returns the number of findings in
|
|
9
|
+
# the file. This command should take an optional “state” parameter so that the only vulnerabilities
|
|
10
|
+
# that are counted are the ones that match its value (e.g. open, fixed, dismissed)
|
|
11
|
+
#
|
|
12
|
+
# ## Inputs
|
|
13
|
+
#
|
|
14
|
+
# | Command Line Flag | Required | Description | Example |
|
|
15
|
+
# |:--- | :--- | :--- | :--- |
|
|
16
|
+
# | -f/--file | yes | The path to the JSON file to be processed | dependabot.json |
|
|
17
|
+
# | --state | no | The state of the vulnerability to be counted | open, fixed, dismissed |
|
|
18
|
+
# | --severity | no | The severity of the vulnerability to be counted | critical, high, medium, low |
|
|
19
|
+
#
|
|
20
|
+
# ## Example commands
|
|
21
|
+
#
|
|
22
|
+
# * `./your_program summary -f dependabot.json`
|
|
23
|
+
# * Count of all findings in dependabot.json
|
|
24
|
+
# * `./your_program summary -f dependabot.json --state dismissed`
|
|
25
|
+
# * Count of all dismissed findings in dependabot.json
|
|
26
|
+
# * `./your_program summary -f dependabot.json --state open --severity critical`
|
|
27
|
+
# * Count of all open critical vulnerabilities
|
|
28
|
+
#
|
|
29
|
+
# ## Outputs
|
|
30
|
+
#
|
|
31
|
+
# ```json
|
|
32
|
+
# {“count”: $count}
|
|
33
|
+
# ```
|
|
34
|
+
#
|
|
35
|
+
# Where `$count` represents the count of findings in the JSON file. If --state or --severity are specified,
|
|
36
|
+
# the count should only include the findings whose
|
|
37
|
+
class SummaryCommand < CommandBase
|
|
38
|
+
attr_reader :state, :severity
|
|
39
|
+
|
|
40
|
+
def initialize(state: nil, severity: nil)
|
|
41
|
+
super()
|
|
42
|
+
@state = state
|
|
43
|
+
@severity = severity
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def perform(input)
|
|
47
|
+
input.alerts.summary @state, @severity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# A singular dependency alert
|
|
6
|
+
class Alert < Base
|
|
7
|
+
attribute :number, :integer
|
|
8
|
+
alias_attribute :id, :number
|
|
9
|
+
delegate :severity, to: :security_vulnerability
|
|
10
|
+
|
|
11
|
+
attribute :repository, Repository
|
|
12
|
+
attribute :dependency, Dependency
|
|
13
|
+
attribute :security_advisory, SecurityAdvisory
|
|
14
|
+
attribute :security_vulnerability, SecurityVulnerability
|
|
15
|
+
|
|
16
|
+
attribute :created_at, :datetime
|
|
17
|
+
attribute :updated_at, :datetime
|
|
18
|
+
attribute :dismissed_at, :datetime
|
|
19
|
+
attribute :fixed_at, :datetime
|
|
20
|
+
attribute :auto_dismissed_at, :datetime
|
|
21
|
+
attribute :state, :symbol
|
|
22
|
+
attribute :url, :url
|
|
23
|
+
attribute :html_url, :url
|
|
24
|
+
attribute :dismissed_by, :string
|
|
25
|
+
attribute :dismissed_comment, :string
|
|
26
|
+
attribute :dismissed_reason, :string
|
|
27
|
+
|
|
28
|
+
def days_open(as_of)
|
|
29
|
+
as_of = as_of&.to_date || Time.now.to_date
|
|
30
|
+
create_date = created_at.to_date
|
|
31
|
+
if (fix_date = fixed_at)
|
|
32
|
+
fix_date = fix_date.to_date
|
|
33
|
+
fix_date < as_of ? (fix_date - create_date).to_i : (as_of - create_date).to_i
|
|
34
|
+
else
|
|
35
|
+
(as_of - create_date).to_i
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Namespace containing models for DBotQuery when used solely
|
|
4
|
+
# as an API.
|
|
5
|
+
module DBotQuery
|
|
6
|
+
module Models
|
|
7
|
+
# Class representing a collection of alerts. This is usually the
|
|
8
|
+
# result of deserializing JSON data.
|
|
9
|
+
class AlertCollection
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
delegate :each, to: :@alerts
|
|
13
|
+
|
|
14
|
+
def initialize(alerts)
|
|
15
|
+
@alerts = alerts.map do |alert|
|
|
16
|
+
case alert
|
|
17
|
+
when Hash
|
|
18
|
+
Alert.new(alert)
|
|
19
|
+
when Alert
|
|
20
|
+
alert
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "Input of type #{alert.class} cannot be converted to Alert\n#{alert.inspect}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def package_search(package_name)
|
|
28
|
+
where_package(package_name).map { |alert| alert.repository.full_name }.uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sla_violations(as_of = nil)
|
|
32
|
+
# Here we select only those which are in violation of the SLA.
|
|
33
|
+
|
|
34
|
+
@alerts.select do |finding|
|
|
35
|
+
finding.days_open(as_of) > SLA_DEFINITION[finding.severity]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def repo_sla_stats(as_of)
|
|
40
|
+
violation_map = sla_violations(as_of)
|
|
41
|
+
|
|
42
|
+
# And finally group by the repo
|
|
43
|
+
violation_map.group_by(&:repository).to_h do |repo, findings|
|
|
44
|
+
sum_repo(repo, findings)
|
|
45
|
+
end.with_indifferent_access
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sla_stats(as_of)
|
|
49
|
+
by_severity = sla_violations(as_of).group_by(&:severity).transform_values(&:size)
|
|
50
|
+
# Because this is just a generalization of the RepoSLAStatisticsCommand, we can gather those results
|
|
51
|
+
# and combine them into a single hash
|
|
52
|
+
base = SLA_DEFINITION.to_h { |severity, _days| [severity, 0] }
|
|
53
|
+
base.merge! by_severity
|
|
54
|
+
totals = { total: base.values.sum } #: { total: Integer }
|
|
55
|
+
base.merge(totals).with_indifferent_access
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def summary(state, severity)
|
|
59
|
+
findings = severity_filter(severity).state_filter(state)
|
|
60
|
+
{ count: findings.count }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sum_repo(repo, findings)
|
|
64
|
+
by_level = findings.select { |alert| alert.repository == repo }.group_by(&:severity)
|
|
65
|
+
by_level = SLA_DEFINITION.to_h { |key, _count| [key, 0] }.merge(by_level.transform_values(&:count))
|
|
66
|
+
|
|
67
|
+
[repo.to_s, { **by_level, total: by_level.values.sum }]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def select(&)
|
|
71
|
+
self.class.new(@alerts.select(&))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def severity_filter(severity)
|
|
75
|
+
severity ? select { |alert| alert.security_advisory.severity == severity.to_sym } : self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def state_filter(state)
|
|
79
|
+
state ? select { |alert| alert.state == state.to_sym } : self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def where_package(package_name)
|
|
83
|
+
select do |alert|
|
|
84
|
+
alert.dependency.package == package_name
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# Generic base class for models. Provides common functionality for all models.
|
|
6
|
+
# This includes additional scalar type support.
|
|
7
|
+
class Base
|
|
8
|
+
# Provides additional scalar type support for casting to symbols
|
|
9
|
+
class SymbolType < ActiveModel::Type::Value
|
|
10
|
+
def cast(value)
|
|
11
|
+
return nil if value.nil?
|
|
12
|
+
|
|
13
|
+
value.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serialize(value)
|
|
17
|
+
value&.to_s
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Provides additional scalar type support for casting to URLs (including those with templates)
|
|
22
|
+
class URLType < ActiveModel::Type::Value
|
|
23
|
+
def cast(value)
|
|
24
|
+
return nil if value.nil?
|
|
25
|
+
|
|
26
|
+
Addressable::Template.new value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def serialize(value)
|
|
30
|
+
value.pattern.to_s
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Provides additional scalar type support for casting to arrays of objects (with the target class)
|
|
35
|
+
class ObjectArray < ActiveModel::Type::Value
|
|
36
|
+
attr_reader :target_class
|
|
37
|
+
|
|
38
|
+
# Initialize with the specific class you want the array to hold
|
|
39
|
+
def initialize(of:)
|
|
40
|
+
@target_class = of
|
|
41
|
+
super()
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Casts raw input data (e.g. an array of hashes) into an array of objects
|
|
45
|
+
def cast(value)
|
|
46
|
+
return [] if value.blank?
|
|
47
|
+
|
|
48
|
+
# Ensure it's handled like an array
|
|
49
|
+
Array(value).map do |item|
|
|
50
|
+
materialize item
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def serialize(value)
|
|
55
|
+
value.map(&:to_h)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def materialize(item)
|
|
61
|
+
case item
|
|
62
|
+
when Hash
|
|
63
|
+
target_class.new(item)
|
|
64
|
+
when target_class
|
|
65
|
+
item
|
|
66
|
+
else
|
|
67
|
+
raise "Unexpected data (not of target_type #{target_class} or a Hash)"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
ActiveModel::Type.register(:symbol, SymbolType)
|
|
73
|
+
ActiveModel::Type.register(:url, URLType)
|
|
74
|
+
ActiveModel::Type.register(:array, ObjectArray)
|
|
75
|
+
|
|
76
|
+
include ActiveModel::Model
|
|
77
|
+
include ActiveModel::Attributes
|
|
78
|
+
include ActiveModel::Validations
|
|
79
|
+
include ActiveModel::Serialization
|
|
80
|
+
|
|
81
|
+
# As the models can be modified and re-serialized, we permit the changing of the data.
|
|
82
|
+
def self.mutable?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Run the validations on the model
|
|
87
|
+
def self.assert_valid_value(value)
|
|
88
|
+
case value
|
|
89
|
+
when self
|
|
90
|
+
raise 'value is not valid' unless value.valid?
|
|
91
|
+
when Hash
|
|
92
|
+
invalid = validate_hash_attributes(value)
|
|
93
|
+
raise "#{self} has invalid hash attributes #{invalid.to_a.to_sentence}" unless invalid.empty?
|
|
94
|
+
else
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Permit the creation of a model from a hash
|
|
100
|
+
def self.cast(input_hash)
|
|
101
|
+
new input_hash
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_h
|
|
105
|
+
attributes.to_h do |key, value|
|
|
106
|
+
type_for = self.class.type_for_attribute key
|
|
107
|
+
result = type_for.respond_to?(:serialize) ? type_for.serialize(value) : value
|
|
108
|
+
[key, result]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Helper to define multiple attributes of a specific type
|
|
113
|
+
def self.attributes(type, *attributes)
|
|
114
|
+
suffix = type.to_s.downcase
|
|
115
|
+
attributes.each do |attribute|
|
|
116
|
+
self.attribute :"#{attribute}_#{suffix}", type
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.validate_hash_attributes(hash)
|
|
121
|
+
attributes = attribute_names.to_set
|
|
122
|
+
keys = hash.keys.to_set(&:to_s)
|
|
123
|
+
invalid_keys = attributes - keys
|
|
124
|
+
keys <= attributes ? [] : invalid_keys.to_a
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# Data file. Reads and parses JSON data.
|
|
6
|
+
class DataFile
|
|
7
|
+
attr_reader :alerts, :data
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
JSON::Validator.validate!(DBotQuery.schema, data)
|
|
11
|
+
@data = data
|
|
12
|
+
@alerts = AlertCollection.new(data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.load(json_data)
|
|
16
|
+
from_data json_data
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.load_file(file_path)
|
|
20
|
+
from_data file_path, loader_method: :load_file
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_data(data, loader_method: :load)
|
|
24
|
+
new JSON.send(loader_method, data, freeze: true)
|
|
25
|
+
rescue JSON::ParserError => e
|
|
26
|
+
raise DBotQuery::Error, "File #{data} is not valid JSON: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# A single dependency. Describes the package that has or had a vulnerability that triggered the alert.
|
|
6
|
+
class Dependency < Base
|
|
7
|
+
attribute :package, Package
|
|
8
|
+
attribute :manifest_path
|
|
9
|
+
attribute :scope
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# A singular package (such as a npm package, a gem, or a pip for python) that is the target of a dependency
|
|
6
|
+
# relationship from a repository. This is the code that would have a vulnerability.
|
|
7
|
+
class Package < Base
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :ecosystem, :string
|
|
10
|
+
|
|
11
|
+
def ==(other)
|
|
12
|
+
case other
|
|
13
|
+
when Package
|
|
14
|
+
name == other.name && ecosystem == other.ecosystem
|
|
15
|
+
when String
|
|
16
|
+
name == other
|
|
17
|
+
else
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# A repository, or source of a dependency, that is the code that is dependent and vulnerable due to a package
|
|
6
|
+
# on which it depends having a security vulnerability.
|
|
7
|
+
class Repository < Base
|
|
8
|
+
attribute :id, :integer
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
attribute :full_name, :string
|
|
11
|
+
attribute :owner, :string
|
|
12
|
+
attribute :node_id, :string
|
|
13
|
+
attribute :private, :boolean
|
|
14
|
+
attribute :url, :url
|
|
15
|
+
attribute :description, :string
|
|
16
|
+
attribute :fork, :boolean
|
|
17
|
+
attribute :created_at, :datetime
|
|
18
|
+
attribute :updated_at, :datetime
|
|
19
|
+
attributes :url, :teams, :notifications, :deployments, :labels, :releases, :milestones, :pulls
|
|
20
|
+
attributes :url, :issues, :downloads, :compare, :merges, :contents, :issue_comment
|
|
21
|
+
attributes :url, :subscription, :trees, :languages, :stargazers, :contributors, :subscribers
|
|
22
|
+
attributes :url, :git_refs, :git_tags, :tags, :blobs, :events, :branches, :assignees, :hooks
|
|
23
|
+
attributes :url, :issue_events, :html, :forks, :collaborators, :keys, :archive, :comments
|
|
24
|
+
attributes :url, :statuses, :git_commits, :commits
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
case other
|
|
28
|
+
when Repository
|
|
29
|
+
full_name == other.full_name
|
|
30
|
+
when String
|
|
31
|
+
full_name == other
|
|
32
|
+
else
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
full_name
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# The backing security advisory that triggered the alert
|
|
6
|
+
class SecurityAdvisory < Base
|
|
7
|
+
attribute :ghsa_id, :string
|
|
8
|
+
attribute :cve_id, :string
|
|
9
|
+
attribute :summary, :string
|
|
10
|
+
attribute :description, :string
|
|
11
|
+
attribute :severity, :symbol
|
|
12
|
+
attribute :published_at, :datetime
|
|
13
|
+
attribute :updated_at, :datetime
|
|
14
|
+
attribute :withdrawn_at, :datetime
|
|
15
|
+
attribute :identifiers, :array, of: VulnerabilityIdentifier
|
|
16
|
+
attribute :references, :array, of: Reference
|
|
17
|
+
attribute :vulnerabilities, :array, of: SecurityVulnerability
|
|
18
|
+
attribute :cvss, CVSS
|
|
19
|
+
attribute :cwes, :array, of: CWE
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# A security vulnerability that is discovered in a package. This occurs in a package and has various information
|
|
6
|
+
# such as the package name, severity, published date, updated date, HTML URL, vulnerable version range,
|
|
7
|
+
# and first patched version. It also has any information such as the references to the bulletin.
|
|
8
|
+
class SecurityVulnerability < Base
|
|
9
|
+
attribute :id, :integer
|
|
10
|
+
attribute :package, :string
|
|
11
|
+
attribute :severity, :symbol
|
|
12
|
+
attribute :published_at, :datetime
|
|
13
|
+
attribute :updated_at, :datetime
|
|
14
|
+
attribute :html_url, :url
|
|
15
|
+
attribute :vulnerable_version_range, :string
|
|
16
|
+
attribute :first_patched_version, :string
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBotQuery
|
|
4
|
+
module Models
|
|
5
|
+
# A singular identifier for a vulnerability to correlate across multiple repos. These identifiers are stable
|
|
6
|
+
# over type and could include a CVE, GHSA, or another unique stable identifier. One vulnerability often has
|
|
7
|
+
# many identifiers (as there are multiple authorities for tracking such).
|
|
8
|
+
class VulnerabilityIdentifier < Base
|
|
9
|
+
attribute :value, :string
|
|
10
|
+
attribute :type, :string
|
|
11
|
+
alias_attribute :id, :value
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/dbotquery.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'thor'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'date'
|
|
8
|
+
require 'uri'
|
|
9
|
+
require 'json-schema'
|
|
10
|
+
require 'active_model'
|
|
11
|
+
require 'active_support/all'
|
|
12
|
+
require 'addressable/template'
|
|
13
|
+
require_relative 'd_bot_query/version'
|
|
14
|
+
|
|
15
|
+
# # Dependabot Data Viewer
|
|
16
|
+
#
|
|
17
|
+
# ## Assignment
|
|
18
|
+
#
|
|
19
|
+
# Dependabot is GitHub’s product for surfacing information about vulnerable dependencies.
|
|
20
|
+
# GitHub maintains a database of known vulnerabilities mapped to specific versions of specific
|
|
21
|
+
# dependencies, and Dependabot compares this to your package manifest (`package.json`,
|
|
22
|
+
# `Gemfile.lock`, etc.).
|
|
23
|
+
#
|
|
24
|
+
# GitHub makes this data available in the web interface, but we want to send this data to be
|
|
25
|
+
# processed elsewhere so non-engineers who don’t have access to GitHub can also see our
|
|
26
|
+
# vulnerabilities. To do this, we want you to write a command line utility that takes an exported
|
|
27
|
+
# JSON file from Dependabot and prints out some basic reports, also in JSON format. The
|
|
28
|
+
# command line tool will not need to talk to GitHub directly. All it needs to do is take the
|
|
29
|
+
# exported Dependabot JSON file as input, some configuration options, and return a JSON
|
|
30
|
+
# document. Please feel free to consult GitHub’s documentation on this file.
|
|
31
|
+
#
|
|
32
|
+
module DBotQuery
|
|
33
|
+
# This is a generic wrapper error for those error types raised specifically by this gem.
|
|
34
|
+
class Error < StandardError; end
|
|
35
|
+
|
|
36
|
+
SCHEMA_PATH = File.join(File.dirname(__FILE__), 'schema.json')
|
|
37
|
+
|
|
38
|
+
SCHEMA = JSON.parse(File.read(SCHEMA_PATH))
|
|
39
|
+
|
|
40
|
+
autoload :CLI, 'd_bot_query/cli'
|
|
41
|
+
|
|
42
|
+
SLA_DEFINITION = {
|
|
43
|
+
low: 60,
|
|
44
|
+
medium: 30,
|
|
45
|
+
high: 15,
|
|
46
|
+
critical: 5
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Module that contains the abstract base class for all commands, as well as the implementations. These
|
|
50
|
+
# are separated in this way to allow for easier testing and maintenance.
|
|
51
|
+
module Commands
|
|
52
|
+
autoload :CommandBase, 'd_bot_query/commands/command_base'
|
|
53
|
+
autoload :PackageSearchCommand, 'd_bot_query/commands/package_search_command'
|
|
54
|
+
autoload :SummaryCommand, 'd_bot_query/commands/summary_command'
|
|
55
|
+
autoload :RepoSLAStatisticsCommand, 'd_bot_query/commands/repo_sla_statistics_command'
|
|
56
|
+
autoload :SLAStatisticsCommand, 'd_bot_query/commands/sla_statistics_command'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Collection of models used by the DBotQuery library providing API access to data files and their contents.
|
|
60
|
+
module Models
|
|
61
|
+
autoload :DataFile, 'd_bot_query/models/data_file'
|
|
62
|
+
autoload :Base, 'd_bot_query/models/base'
|
|
63
|
+
autoload :AlertCollection, 'd_bot_query/models/alert_collection'
|
|
64
|
+
autoload :Alert, 'd_bot_query/models/alert'
|
|
65
|
+
autoload :Dependency, 'd_bot_query/models/dependency'
|
|
66
|
+
autoload :Package, 'd_bot_query/models/package'
|
|
67
|
+
autoload :Repository, 'd_bot_query/models/repository'
|
|
68
|
+
autoload :SecurityAdvisory, 'd_bot_query/models/security_advisory'
|
|
69
|
+
autoload :SecurityVulnerability, 'd_bot_query/models/security_vulnerability'
|
|
70
|
+
autoload :VulnerabilityIdentifier, 'd_bot_query/models/vulnerability_identifier'
|
|
71
|
+
autoload :Reference, 'd_bot_query/models/reference'
|
|
72
|
+
autoload :CVSS, 'd_bot_query/models/cvss'
|
|
73
|
+
autoload :CWE, 'd_bot_query/models/cwe'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.schema
|
|
77
|
+
SCHEMA
|
|
78
|
+
end
|
|
79
|
+
end
|