cvelist 0.1.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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.github/workflows/ruby.yml +29 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.md +10 -0
  8. data/Gemfile +13 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +82 -0
  11. data/Rakefile +13 -0
  12. data/benchmark.rb +38 -0
  13. data/cvelist.gemspec +61 -0
  14. data/gemspec.yml +21 -0
  15. data/lib/cvelist.rb +2 -0
  16. data/lib/cvelist/cve.rb +83 -0
  17. data/lib/cvelist/directory.rb +80 -0
  18. data/lib/cvelist/exceptions.rb +31 -0
  19. data/lib/cvelist/malformed_cve.rb +42 -0
  20. data/lib/cvelist/range_dir.rb +121 -0
  21. data/lib/cvelist/repository.rb +240 -0
  22. data/lib/cvelist/version.rb +4 -0
  23. data/lib/cvelist/year_dir.rb +178 -0
  24. data/spec/cve_methods_examples.rb +130 -0
  25. data/spec/cve_spec.rb +51 -0
  26. data/spec/cvelist_spec.rb +8 -0
  27. data/spec/directory_spec.rb +83 -0
  28. data/spec/fixtures/CVE-2020-1994.json +140 -0
  29. data/spec/fixtures/cvelist/.gitkeep +0 -0
  30. data/spec/fixtures/cvelist/1999/.gitkeep +0 -0
  31. data/spec/fixtures/cvelist/2000/.gitkeep +0 -0
  32. data/spec/fixtures/cvelist/2001/.gitkeep +0 -0
  33. data/spec/fixtures/cvelist/2002/.gitkeep +0 -0
  34. data/spec/fixtures/cvelist/2003/.gitkeep +0 -0
  35. data/spec/fixtures/cvelist/2004/.gitkeep +0 -0
  36. data/spec/fixtures/cvelist/2005/.gitkeep +0 -0
  37. data/spec/fixtures/cvelist/2006/.gitkeep +0 -0
  38. data/spec/fixtures/cvelist/2007/.gitkeep +0 -0
  39. data/spec/fixtures/cvelist/2008/.gitkeep +0 -0
  40. data/spec/fixtures/cvelist/2009/.gitkeep +0 -0
  41. data/spec/fixtures/cvelist/2010/.gitkeep +0 -0
  42. data/spec/fixtures/cvelist/2011/.gitkeep +0 -0
  43. data/spec/fixtures/cvelist/2012/.gitkeep +0 -0
  44. data/spec/fixtures/cvelist/2013/.gitkeep +0 -0
  45. data/spec/fixtures/cvelist/2014/.gitkeep +0 -0
  46. data/spec/fixtures/cvelist/2015/.gitkeep +0 -0
  47. data/spec/fixtures/cvelist/2016/.gitkeep +0 -0
  48. data/spec/fixtures/cvelist/2017/.gitkeep +0 -0
  49. data/spec/fixtures/cvelist/2018/.gitkeep +0 -0
  50. data/spec/fixtures/cvelist/2019/.gitkeep +0 -0
  51. data/spec/fixtures/cvelist/2020/.gitkeep +0 -0
  52. data/spec/fixtures/cvelist/2021/.gitkeep +0 -0
  53. data/spec/fixtures/cvelist/2021/0xxx/.gitkeep +0 -0
  54. data/spec/fixtures/cvelist/2021/1xxx/.gitkeep +0 -0
  55. data/spec/fixtures/cvelist/2021/20xxx/.gitkeep +0 -0
  56. data/spec/fixtures/cvelist/2021/21xxx/.gitkeep +0 -0
  57. data/spec/fixtures/cvelist/2021/2xxx/.gitkeep +0 -0
  58. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2000.json +18 -0
  59. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2001.json +18 -0
  60. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2002.json +18 -0
  61. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2003.json +18 -0
  62. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2004.json +18 -0
  63. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2005.json +18 -0
  64. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2006.json +18 -0
  65. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2007.json +18 -0
  66. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2008.json +18 -0
  67. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2009.json +18 -0
  68. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2998.json +3 -0
  69. data/spec/fixtures/cvelist/2021/2xxx/CVE-2021-2999.json +2 -0
  70. data/spec/range_dir_spec.rb +55 -0
  71. data/spec/repository_spec.rb +248 -0
  72. data/spec/spec_helper.rb +4 -0
  73. data/spec/year_dir_spec.rb +96 -0
  74. metadata +165 -0
@@ -0,0 +1,31 @@
1
+ require 'cve_schema/exceptions'
2
+
3
+ module CVEList
4
+ # Base class for all git related exceptions.
5
+ class GitError < RuntimeError
6
+ end
7
+
8
+ # Raised when a `git clone` failed.
9
+ class GitCloneFailed < GitError
10
+ end
11
+
12
+ # Raised when a `git pull` failed.
13
+ class GitPullFailed < GitError
14
+ end
15
+
16
+ class NotFoundError < RuntimeError
17
+ end
18
+
19
+ class YearDirNotFound < NotFoundError
20
+ end
21
+
22
+ class RangeDirNotFound < NotFoundError
23
+ end
24
+
25
+ class CVENotFound < NotFoundError
26
+ end
27
+
28
+ InvalidJSON = CVESchema::InvalidJSON
29
+ MissingJSONKey = CVESchema::MissingJSONKey
30
+ UnkownJSONValue = CVESchema::UnknownJSONValue
31
+ end
@@ -0,0 +1,42 @@
1
+ module CVEList
2
+ #
3
+ # Represents malformed/invalid CVE JSON that could not be loaded.
4
+ #
5
+ class MalformedCVE
6
+
7
+ # Path to the JSON file.
8
+ #
9
+ # @return [String]
10
+ attr_reader :path
11
+
12
+ # The exception encountered when parsing the JSON file.
13
+ #
14
+ # @return [StandardError]
15
+ attr_reader :exception
16
+
17
+ #
18
+ # Initializes the malformed json.
19
+ #
20
+ # @param [String] path
21
+ # Path to the JSON file.
22
+ #
23
+ # @param [StandardError] exception
24
+ # The exception encountered when parsing the JSON file.
25
+ #
26
+ def initialize(path,exception)
27
+ @path = path
28
+ @exception = exception
29
+ end
30
+
31
+ #
32
+ # Converts the malformed JSON back into a String.
33
+ #
34
+ # @return [String]
35
+ # The String containing the {#path}, the {#exception} class and message.
36
+ #
37
+ def to_s
38
+ "#{@path}: #{@exception.class}: #{@exception.message}"
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cvelist/directory'
4
+ require 'cvelist/exceptions'
5
+ require 'cvelist/cve'
6
+ require 'cvelist/malformed_cve'
7
+
8
+ module CVEList
9
+ class RangeDir < Directory
10
+
11
+ include Enumerable
12
+
13
+ # @return [String]
14
+ attr_reader :path
15
+
16
+ # @return [String]
17
+ attr_reader :range
18
+
19
+ #
20
+ # Initializes the range directory.
21
+ #
22
+ # @param [String] path
23
+ # The path to the range directory.
24
+ #
25
+ def initialize(path)
26
+ super(path)
27
+
28
+ @range = File.basename(@path)
29
+ end
30
+
31
+ #
32
+ # Determines whether the range directory contains the given CVE ID.
33
+ #
34
+ # @param [String] cve_id
35
+ # The given CVE ID.
36
+ #
37
+ # @return [Boolean]
38
+ #
39
+ def has_cve?(cve_id)
40
+ file?("#{cve_id}.json")
41
+ end
42
+
43
+ #
44
+ # Loads a CVE.
45
+ #
46
+ # @param [String] cve_id
47
+ # The CVE ID.
48
+ #
49
+ # @return [CVE, nil]
50
+ # The loaded CVE of `nil` if the CVE could not be found within the range
51
+ # directory.
52
+ #
53
+ def [](cve_id)
54
+ cve_file = "#{cve_id}.json"
55
+ cve_path = join(cve_file)
56
+
57
+ if File.file?(cve_path)
58
+ CVE.load(cve_path)
59
+ end
60
+ end
61
+
62
+ # `Dir.glob` pattern for all CVE `.json` files.
63
+ GLOB = 'CVE-[0-9][0-9][0-9][0-9]-*.json'
64
+
65
+ #
66
+ # The JSON files within the range directory.
67
+ #
68
+ # @return [Array<String>]
69
+ #
70
+ def files
71
+ glob(GLOB).sort
72
+ end
73
+
74
+ #
75
+ # Enumerates over the CVEs in the range directory.
76
+ #
77
+ # @yield [cve]
78
+ # The given block will be passed each CVE in the range directory.
79
+ #
80
+ # @yieldparam [CVE] cve
81
+ # A CVE within the range directory.
82
+ #
83
+ # @return [Enumerator]
84
+ # If no block is given, an Enumerator object will be returned.
85
+ #
86
+ def each
87
+ return enum_for(__method__) unless block_given?
88
+
89
+ files.each do |cve_path|
90
+ begin
91
+ yield CVE.load(cve_path)
92
+ rescue InvalidJSON
93
+ end
94
+ end
95
+ end
96
+
97
+ #
98
+ # Enumerates over the malformed CVEs within the range directory.
99
+ #
100
+ # @yield [malformed]
101
+ # The given block will be passed each malformed
102
+ #
103
+ # @yieldparam [MalformedCVE] malformed
104
+ #
105
+ # @return [Enumerator]
106
+ # If no block is given, an Enumerator object will be returned.
107
+ #
108
+ def each_malformed
109
+ return enum_for(__method__) unless block_given?
110
+
111
+ files.each do |cve_path|
112
+ begin
113
+ CVE.load(cve_path)
114
+ rescue InvalidJSON => error
115
+ yield MalformedCVE.new(cve_path,error)
116
+ end
117
+ end
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cvelist/directory'
4
+ require 'cvelist/exceptions'
5
+ require 'cvelist/year_dir'
6
+
7
+ module CVEList
8
+ class Repository < Directory
9
+
10
+ include Enumerable
11
+
12
+ # The default git URI for the cvelist repository
13
+ URL = 'https://github.com/CVEProject/cvelist.git'
14
+
15
+ #
16
+ # Clones a new repository.
17
+ #
18
+ # @param [#to_s] path
19
+ # The path to where the cvelist repository will be cloned to.
20
+ #
21
+ # @param [#to_s] url
22
+ # The URL for the cvelist repository.
23
+ #
24
+ # @param [#to_s] depth
25
+ # The depth of the git clone.
26
+ #
27
+ # @raise [CloneFailedError]
28
+ # The `git clone` command failed.
29
+ #
30
+ def self.clone(path, url: URL, depth: 1)
31
+ unless system 'git', 'clone', '--depth', depth.to_s, url.to_s, path.to_s
32
+ raise(GitCloneFailed,"failed to clone #{url.inspect} into #{path.inspect}")
33
+ end
34
+
35
+ return new(path)
36
+ end
37
+
38
+ class << self
39
+ alias download clone
40
+ end
41
+
42
+ #
43
+ # Determines whether the repository is a git repository.
44
+ #
45
+ # @return [Boolean]
46
+ # Specifies whether the repository is a git repository or not.
47
+ #
48
+ def git?
49
+ directory?('.git')
50
+ end
51
+
52
+ # The default git remote.
53
+ REMOTE = 'origin'
54
+
55
+ # The default git branch.
56
+ BRANCH = 'master'
57
+
58
+ #
59
+ # Pulls down new commits from the given remote/branch.
60
+ #
61
+ # @param [#to_s] remote
62
+ # The git remote.
63
+ #
64
+ # @param [#to_s] branch
65
+ # The git branch.
66
+ #
67
+ # @return [true, false]
68
+ # Returns `true` if the `git pull` succeeds.
69
+ # Returns `false` if the repository is not a git repository.
70
+ #
71
+ # @raise [PullFailedError]
72
+ # The `git pull` command faild.
73
+ #
74
+ def pull!(remote: REMOTE, branch: BRANCH)
75
+ return false unless git?
76
+
77
+ Dir.chdir(@path) do
78
+ unless system('git', 'pull', remote.to_s, branch.to_s)
79
+ raise(GitPullFailed,"failed to pull from remote #{remote.inspect} branch #{branch.inspect}")
80
+ end
81
+ end
82
+
83
+ return true
84
+ end
85
+
86
+ alias update! pull!
87
+
88
+ #
89
+ # Determines if the repository contains a directory for the given year.
90
+ #
91
+ # @param [#to_s] year
92
+ # The given year.
93
+ #
94
+ # @return [Boolean]
95
+ # Specifies whether the repository contains the directory for the year.
96
+ #
97
+ def has_year?(year)
98
+ directory?(year.to_s)
99
+ end
100
+
101
+ # `Dir.glob` for year directories.
102
+ GLOB = '[1-2][0-9][0-9][0-9]'
103
+
104
+ #
105
+ # The year directories within the repository.
106
+ #
107
+ # @return [Array<String>]
108
+ # The paths to the year directories.
109
+ #
110
+ def directories
111
+ glob(GLOB).sort
112
+ end
113
+
114
+ #
115
+ # The year directories contained within the repository.
116
+ #
117
+ # @return [Array<YearDir>]
118
+ # The year directories within the repository.
119
+ #
120
+ def years(&block)
121
+ directories.map { |dir| YearDir.new(dir) }
122
+ end
123
+
124
+ #
125
+ # Requests a year directory from the repository.
126
+ #
127
+ # @param [#to_s] year_number
128
+ # The given year number.
129
+ #
130
+ # @return [YearDir]
131
+ # The year directory.
132
+ #
133
+ # @raise [YearNotFound]
134
+ # There is no year directory within the repository for the given year.
135
+ #
136
+ def year(year_number)
137
+ year_dir = join(year_number.to_s)
138
+
139
+ unless File.directory?(year_dir)
140
+ raise(YearDirNotFound,"year #{year_number.inspect} not found within #{@path.inspect}")
141
+ end
142
+
143
+ return YearDir.new(year_dir)
144
+ end
145
+
146
+ alias / year
147
+
148
+ #
149
+ # Enumerates over every CVE withing the year directories.
150
+ #
151
+ # @yield [cve]
152
+ # The given block will be passed each CVE from within the repository.
153
+ #
154
+ # @yieldparam [CVE] cve
155
+ # A CVE from the repository.
156
+ #
157
+ # @return [Enumerator]
158
+ # If no block is given, an Enumerator will be returned.
159
+ #
160
+ def each(&block)
161
+ return enum_for(__method__) unless block_given?
162
+
163
+ years.each do |year_dir|
164
+ year_dir.each(&block)
165
+ end
166
+ end
167
+
168
+ #
169
+ # Enumerates over every malformed CVE within the repository.
170
+ #
171
+ # @yield [malformed_cve]
172
+ # The given block will be passed each malformed CVE from within the
173
+ # repository.
174
+ #
175
+ # @yieldparam [MalformedCVE] malformed_cve
176
+ # A malformed CVE from within the repository.
177
+ #
178
+ # @return [Enumerator]
179
+ # If no block is given, an Enumerator will be returned.
180
+ #
181
+ def each_malformed(&block)
182
+ return enum_for(__method__) unless block_given?
183
+
184
+ years.each do |year_dir|
185
+ year_dir.each_malformed(&block)
186
+ end
187
+ end
188
+
189
+ #
190
+ # Determines whether the repository contains the given CVE ID.
191
+ #
192
+ # @param [String] cve_id
193
+ # The given CVE ID.
194
+ #
195
+ # @return [Boolean]
196
+ #
197
+ def has_cve?(cve_id)
198
+ year_number = cve_to_year(cve_id)
199
+
200
+ return has_year?(year_number) && year(year_number).has_cve?(cve_id)
201
+ end
202
+
203
+ #
204
+ # Accesses a CVE from the repository with the given CVE ID.
205
+ #
206
+ # @param [String] cve_id
207
+ # The given CVE ID.
208
+ #
209
+ # @return [CVE, nil]
210
+ # The CVE with the given ID. If no CVE with the given ID could be found,
211
+ # `nil` will be returned.
212
+ #
213
+ # @raise [InvalidJSON, MissingJSONKey, UnknownJSONValue]
214
+ # The CVE's JSON is invalid or malformed.
215
+ #
216
+ def [](cve_id)
217
+ year_number = cve_to_year(cve_id)
218
+
219
+ if has_year?(year_number)
220
+ year(year_number)[cve_id]
221
+ end
222
+ end
223
+
224
+ #
225
+ # Calculates the total number of CVEs in the repository.
226
+ #
227
+ # @return [Integer]
228
+ #
229
+ def size
230
+ Dir[join(GLOB,YearDir::GLOB,RangeDir::GLOB)].length
231
+ end
232
+
233
+ private
234
+
235
+ def cve_to_year(cve_id)
236
+ cve_id[cve_id.index('-')+1 .. cve_id.rindex('-')-1]
237
+ end
238
+
239
+ end
240
+ end
@@ -0,0 +1,4 @@
1
+ module CVEList
2
+ # cvelist version
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cvelist/directory'
4
+ require 'cvelist/exceptions'
5
+ require 'cvelist/range_dir'
6
+
7
+ module CVEList
8
+ class YearDir < Directory
9
+
10
+ include Enumerable
11
+
12
+ # Path to the year directory.
13
+ #
14
+ # @return [String]
15
+ attr_reader :path
16
+
17
+ # The year of the directory.
18
+ #
19
+ # @return [Integer]
20
+ attr_reader :year
21
+
22
+ #
23
+ # Initializes the year dir.
24
+ #
25
+ # @param [String] path
26
+ # The path to the year directory.
27
+ #
28
+ def initialize(path)
29
+ super(path)
30
+
31
+ @year = File.basename(@path).to_i
32
+ end
33
+
34
+ #
35
+ # Determines if the year directory contains the given range directory.
36
+ #
37
+ # @param [String] xxx_range
38
+ # The given range directory ending in `xxx`.
39
+ #
40
+ # @return [Boolean]
41
+ #
42
+ def has_range?(xxx_range)
43
+ directory?(xxx_range)
44
+ end
45
+
46
+ #
47
+ # Access a range directory within the year directory.
48
+ #
49
+ # @param [String] xxx_range
50
+ # The "xxx" range.
51
+ #
52
+ # @return [RangeDir]
53
+ # The range directory.
54
+ #
55
+ # @raise [RangeDirNotFound]
56
+ # Could not find the given range directory within the year directory.
57
+ #
58
+ def range(xxx_range)
59
+ range_dir_path = join(xxx_range)
60
+
61
+ unless File.directory?(range_dir_path)
62
+ raise(RangeDirNotFound,"#{xxx_range.inspect} not found within #{@path.inspect}")
63
+ end
64
+
65
+ return RangeDir.new(range_dir_path)
66
+ end
67
+
68
+ alias / range
69
+
70
+ # `Dir.glob` pattern for `Nxxx` range directories.
71
+ GLOB = '*xxx'
72
+
73
+ #
74
+ # The `xxx` number ranges within the directory.
75
+ #
76
+ # @return [Array<String>]
77
+ #
78
+ def directories
79
+ glob(GLOB).sort
80
+ end
81
+
82
+ #
83
+ # The range directories within the year directory.
84
+ #
85
+ # @return [Enumerator]
86
+ # If no block is given, an Enumerator will be returned.
87
+ #
88
+ def ranges(&block)
89
+ directories.map { |dir| RangeDir.new(dir) }
90
+ end
91
+
92
+ #
93
+ # Enumerates over each CVE, in each range directory, within the year
94
+ # directory.
95
+ #
96
+ # @yield [cve]
97
+ # The given block will be passed each CVE in the year dir.
98
+ #
99
+ # @yieldparam [CVE] cve
100
+ # A CVE within one of the range directories.
101
+ #
102
+ # @return [Enumerator]
103
+ # If no block is given, an Enumerator will be returned.
104
+ #
105
+ def each(&block)
106
+ return enum_for(__method__) unless block_given?
107
+
108
+ ranges.each do |range_dir|
109
+ range_dir.each(&block)
110
+ end
111
+ end
112
+
113
+ #
114
+ # Enumerates over every malformed CVE within the year directories.
115
+ #
116
+ # @yield [malformed_cve]
117
+ # The given block will be passed each malformed CVE from within the
118
+ # year directory.
119
+ #
120
+ # @yieldparam [MalformedCVE] malformed_cve
121
+ # A malformed CVE from within the year directory.
122
+ #
123
+ # @return [Enumerator]
124
+ # If no block is given, an Enumerator will be returned.
125
+ #
126
+ def each_malformed(&block)
127
+ return enum_for(__method__) unless block_given?
128
+
129
+ ranges.each do |range_dir|
130
+ range_dir.each_malformed(&block)
131
+ end
132
+ end
133
+
134
+ #
135
+ # Determines whether a CVE exists with the given ID, within any of the range
136
+ # directories, within the year directory.
137
+ #
138
+ # @param [String] cve_id
139
+ # The given CVE ID.
140
+ #
141
+ # @return [Boolean]
142
+ # Specifies whether the CVE exists.
143
+ #
144
+ def has_cve?(cve_id)
145
+ xxx_range = cve_to_xxx_range(cve_id)
146
+
147
+ has_range?(xxx_range) && range(xxx_range).has_cve?(cve_id)
148
+ end
149
+
150
+ #
151
+ # Loads a CVE.
152
+ #
153
+ # @param [String] cve_id
154
+ # The CVE ID.
155
+ #
156
+ # @return [CVE, nil]
157
+ # The loaded CVE or `nil` if the accompaning range directory for the CVE
158
+ # could not be found.
159
+ #
160
+ def [](cve_id)
161
+ xxx_range = cve_to_xxx_range(cve_id)
162
+
163
+ if has_range?(xxx_range)
164
+ range(xxx_range)[cve_id]
165
+ end
166
+ end
167
+
168
+ private
169
+
170
+ def cve_to_xxx_range(cve_id)
171
+ cve_number = cve_id[cve_id.rindex('-')+1 ..]
172
+ cve_number[-3,3] = 'xxx'
173
+
174
+ return cve_number
175
+ end
176
+
177
+ end
178
+ end