cvelist 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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