nvd-json_feeds 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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.github/workflows/ruby.yml +29 -0
  4. data/.gitignore +9 -0
  5. data/.rspec +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.md +25 -0
  8. data/Gemfile +13 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +136 -0
  11. data/Rakefile +31 -0
  12. data/gemspec.yml +22 -0
  13. data/lib/nvd/json_feeds.rb +25 -0
  14. data/lib/nvd/json_feeds/exceptions.rb +15 -0
  15. data/lib/nvd/json_feeds/feed.rb +50 -0
  16. data/lib/nvd/json_feeds/feed_file.rb +95 -0
  17. data/lib/nvd/json_feeds/feed_uri.rb +131 -0
  18. data/lib/nvd/json_feeds/gz_feed_file.rb +60 -0
  19. data/lib/nvd/json_feeds/gz_feed_uri.rb +25 -0
  20. data/lib/nvd/json_feeds/json_feed_file.rb +21 -0
  21. data/lib/nvd/json_feeds/meta.rb +122 -0
  22. data/lib/nvd/json_feeds/meta_feed_uri.rb +22 -0
  23. data/lib/nvd/json_feeds/schema/configurations.rb +61 -0
  24. data/lib/nvd/json_feeds/schema/configurations/node.rb +98 -0
  25. data/lib/nvd/json_feeds/schema/cpe/has_uri.rb +66 -0
  26. data/lib/nvd/json_feeds/schema/cpe/match.rb +117 -0
  27. data/lib/nvd/json_feeds/schema/cpe/name.rb +67 -0
  28. data/lib/nvd/json_feeds/schema/cve_feed.rb +142 -0
  29. data/lib/nvd/json_feeds/schema/cve_item.rb +94 -0
  30. data/lib/nvd/json_feeds/schema/cvss_v2.rb +298 -0
  31. data/lib/nvd/json_feeds/schema/cvss_v3.rb +332 -0
  32. data/lib/nvd/json_feeds/schema/has_data_version.rb +54 -0
  33. data/lib/nvd/json_feeds/schema/impact.rb +73 -0
  34. data/lib/nvd/json_feeds/schema/impact/base_metric_v2.rb +132 -0
  35. data/lib/nvd/json_feeds/schema/impact/base_metric_v3.rb +79 -0
  36. data/lib/nvd/json_feeds/schema/timestamp.rb +9 -0
  37. data/lib/nvd/json_feeds/version.rb +6 -0
  38. data/lib/nvd/json_feeds/zip_feed_file.rb +64 -0
  39. data/lib/nvd/json_feeds/zip_feed_uri.rb +25 -0
  40. data/nvd-json_feeds.gemspec +61 -0
  41. data/spec/feed_file_examples.rb +27 -0
  42. data/spec/feed_file_spec.rb +42 -0
  43. data/spec/feed_spec.rb +56 -0
  44. data/spec/feed_uri_spec.rb +81 -0
  45. data/spec/fixtures/gz_feed_file/nvdcve-1.1-recent.json.gz +0 -0
  46. data/spec/fixtures/nvdcve-1.1-recent.json +180 -0
  47. data/spec/fixtures/zip_feed_file/nvdcve-1.1-recent.json.zip +0 -0
  48. data/spec/gz_feed_file_spec.rb +66 -0
  49. data/spec/gz_feed_uri_spec.rb +35 -0
  50. data/spec/json_feed_file_spec.rb +18 -0
  51. data/spec/json_feeds_spec.rb +8 -0
  52. data/spec/meta_spec.rb +141 -0
  53. data/spec/schema/configurations/node_spec.rb +87 -0
  54. data/spec/schema/configurations_spec.rb +57 -0
  55. data/spec/schema/cpe/match_spec.rb +188 -0
  56. data/spec/schema/cpe/name_spec.rb +54 -0
  57. data/spec/schema/cve_feed_spec.rb +162 -0
  58. data/spec/schema/cve_item_spec.rb +116 -0
  59. data/spec/schema/impact/base_metric_v2_spec.rb +183 -0
  60. data/spec/schema/impact/base_metric_v3_spec.rb +80 -0
  61. data/spec/schema/impact_spec.rb +53 -0
  62. data/spec/schema/shared_examples.rb +136 -0
  63. data/spec/schema/timestamp_spec.rb +8 -0
  64. data/spec/spec_helper.rb +8 -0
  65. data/spec/zip_feed_file_spec.rb +66 -0
  66. data/spec/zip_feed_uri_spec.rb +35 -0
  67. metadata +156 -0
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/https'
4
+
5
+ module NVD
6
+ module JSONFeeds
7
+ #
8
+ # Base class for all feed URIs.
9
+ #
10
+ class FeedURI
11
+
12
+ SCHEMA_VERSION = '1.1'
13
+
14
+ BASE_URI = "https://nvd.nist.gov/feeds/json/cve/#{SCHEMA_VERSION}"
15
+
16
+ # The feed name or year.
17
+ #
18
+ # @return [:modified, :recent, Integer]
19
+ attr_reader :name
20
+
21
+ # The feed file extension.
22
+ #
23
+ # @return [String]
24
+ attr_reader :ext
25
+
26
+ # The file name of the feed.
27
+ #
28
+ # @return [String]
29
+ attr_reader :filename
30
+
31
+ # The feed URI.
32
+ #
33
+ # @return [URI::HTTPS]
34
+ attr_reader :uri
35
+
36
+ #
37
+ # Initializes the feed URI.
38
+ #
39
+ # @param [:modified, :recent, Integer] name
40
+ # The feed name or year.
41
+ #
42
+ # @param [String] ext
43
+ # The feed file extension (ex: `.json.gz`).
44
+ #
45
+ def initialize(name,ext)
46
+ @name = name
47
+ @ext = ext
48
+
49
+ @filename = "nvdcve-#{SCHEMA_VERSION}-#{@name}#{@ext}"
50
+ @uri = URI("#{BASE_URI}/#{@filename}")
51
+ end
52
+
53
+ #
54
+ # Performs and HTTP GET request to the feed URI.
55
+ #
56
+ # @yield [chunk]
57
+ # If a block is given, it will be passed each chunk read from the
58
+ # response.
59
+ #
60
+ # @yieldparam [String] chunk
61
+ # A chunk of data read from the response body.
62
+ #
63
+ # @return [String]
64
+ # If no block is given, the full response body will be returned.
65
+ #
66
+ def get(&block)
67
+ if block
68
+ Net::HTTP.start(@uri.host,@uri.port, use_ssl: true) do |http|
69
+ request = Net::HTTP::Get.new(uri)
70
+
71
+ http.request(request) do |response|
72
+ response.read_body(&block)
73
+ end
74
+ end
75
+ else
76
+ Net::HTTP.get(@uri)
77
+ end
78
+ end
79
+
80
+ #
81
+ # Downloads the feed to the given destination.
82
+ #
83
+ # @param [String] dest
84
+ # Either a destination file or directory.
85
+ #
86
+ def download(dest)
87
+ dest_path = if File.directory?(dest) then File.join(dest,@filename)
88
+ else dest
89
+ end
90
+
91
+ File.open(dest_path,'w') do |file|
92
+ get do |chunk|
93
+ file.write(chunk)
94
+ end
95
+ end
96
+
97
+ return dest_path
98
+ end
99
+
100
+ #
101
+ # Converts the feed URI to a regular URI.
102
+ #
103
+ # @return [URI::HTTPS]
104
+ # The feed URI.
105
+ #
106
+ def to_uri
107
+ @uri
108
+ end
109
+
110
+ #
111
+ # Converts the feed URI to a String.
112
+ #
113
+ # @return [String]
114
+ # The String version of the feed URI.
115
+ #
116
+ def to_s
117
+ @uri.to_s
118
+ end
119
+
120
+ #
121
+ # Inspects the feed URI.
122
+ #
123
+ # @return [String]
124
+ #
125
+ def inspect
126
+ "#<#{self.class}: #{self}>"
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nvd/json_feeds/feed_file'
4
+ require 'nvd/json_feeds/json_feed_file'
5
+ require 'nvd/json_feeds/exceptions'
6
+
7
+ require 'shellwords'
8
+
9
+ module NVD
10
+ module JSONFeeds
11
+ class GzFeedFile < FeedFile
12
+
13
+ #
14
+ # The filename of the `.json` file within the `.gz` archive.
15
+ #
16
+ # @return [String]
17
+ #
18
+ def json_filename
19
+ File.basename(@path,'.gz')
20
+ end
21
+
22
+ #
23
+ # Reads the compressed feed file.
24
+ #
25
+ # @return [String]
26
+ #
27
+ # @raise [ReadFailed]
28
+ # The `gunzip` command is not installed.
29
+ #
30
+ def read
31
+ `gunzip -q -c -k #{Shellwords.escape(@path)}`
32
+ rescue Errno::ENOENT
33
+ raise(ReadFailed,"gunzip command is not installed")
34
+ end
35
+
36
+ #
37
+ # Extracts the feed file.
38
+ #
39
+ # @return [JSONFeedFile]
40
+ #
41
+ # @raise [ExtractFailed]
42
+ # The `gunzip` command failed or did the `.json` file was not extracted.
43
+ #
44
+ def extract
45
+ unless system('gunzip', '-q', '-k', @path)
46
+ raise(ExtractFailed,"gunzip command failed")
47
+ end
48
+
49
+ extracted_dir = File.dirname(@path)
50
+ extracted_path = File.join(extracted_dir,json_filename)
51
+
52
+ unless File.file?(extracted_path)
53
+ raise(ExtractFailed,"extraction failed: #{@path.inspect}")
54
+ end
55
+
56
+ return JSONFeedFile.new(extracted_path)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ require 'nvd/json_feeds/feed_uri'
2
+ require 'nvd/json_feeds/gz_feed_file'
3
+
4
+ module NVD
5
+ module JSONFeeds
6
+ #
7
+ # Represents a URI to a `.json.gz` feed file.
8
+ #
9
+ class GzFeedURI < FeedURI
10
+
11
+ #
12
+ # Downloads the compressed feed file.
13
+ #
14
+ # @param [String] dest
15
+ # The destination file or directory.
16
+ #
17
+ # @return [GzFeedFile]
18
+ #
19
+ def download(dest)
20
+ GzFeedFile.new(super(dest))
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ require 'nvd/json_feeds/feed_file'
2
+
3
+ module NVD
4
+ module JSONFeeds
5
+ #
6
+ # Represents a `.json` feed file.
7
+ #
8
+ class JSONFeedFile < FeedFile
9
+
10
+ #
11
+ # Reads the JSON feed file.
12
+ #
13
+ # @return [String]
14
+ #
15
+ def read
16
+ File.read(@path)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,122 @@
1
+ require 'nvd/json_feeds/exceptions'
2
+
3
+ require 'time'
4
+
5
+ module NVD
6
+ module JSONFeeds
7
+ #
8
+ # Represents the parsed contents of a `.meta` file.
9
+ #
10
+ class Meta
11
+
12
+ # The date the feed file was last updated.
13
+ #
14
+ # @return [DateTime]
15
+ attr_reader :last_modified_date
16
+
17
+ # The size of the uncompressed `.json` feed file.
18
+ #
19
+ # @return [Integer]
20
+ attr_reader :size
21
+
22
+ # The size of the `.zip` feed file.
23
+ #
24
+ # @return [Integer]
25
+ attr_reader :zip_size
26
+
27
+ # The size of the `.gz` feed file.
28
+ #
29
+ # @return [Integer]
30
+ attr_reader :gz_size
31
+
32
+ # The SHA256 checksum of the uncompressed `.json` feed file.
33
+ #
34
+ # @return [String]
35
+ # @note NVD uses all upper-case SHA256 checksums.
36
+ attr_reader :sha256
37
+
38
+ #
39
+ # Initializes the feed metadata.
40
+ #
41
+ # @param [DateTime] last_modified_date
42
+ # The parsed `lastModifiedDate` timestamp.
43
+ #
44
+ # @param [Integer] size
45
+ # The `size` value.
46
+ #
47
+ # @param [Integer] zip_size
48
+ # The `zipSize` value.
49
+ #
50
+ # @param [Integer] gz_size
51
+ # The `gzSize` value.
52
+ #
53
+ # @param [String] sha256
54
+ # The `sha256` value.
55
+ #
56
+ def initialize(last_modified_date,size,zip_size,gz_size,sha256)
57
+ @last_modified_date = last_modified_date
58
+
59
+ @size = size
60
+ @zip_size = zip_size
61
+ @gz_size = gz_size
62
+ @sha256 = sha256
63
+ end
64
+
65
+ #
66
+ # Parses the text.
67
+ #
68
+ # @param [String] string
69
+ # The raw meta string.
70
+ #
71
+ # @return [Meta]
72
+ # The parsed meta information.
73
+ #
74
+ # @raise [MetaParseError]
75
+ # The meta string was malformed.
76
+ #
77
+ def self.parse(string)
78
+ last_modified_date = nil
79
+ size = nil
80
+ zip_size = nil
81
+ gz_size = nil
82
+ sha256 = nil
83
+
84
+ string.each_line do |line|
85
+ name, value = line.chomp.split(':',2)
86
+
87
+ case name
88
+ when 'lastModifiedDate'
89
+ last_modified_date = DateTime.parse(value)
90
+ when 'size' then size = value.to_i
91
+ when 'zipSize' then zip_size = value.to_i
92
+ when 'gzSize' then gz_size = value.to_i
93
+ when 'sha256' then sha256 = value
94
+ end
95
+ end
96
+
97
+ unless last_modified_date
98
+ raise(MetaParseError,"lastModifiedDate missing")
99
+ end
100
+
101
+ unless size
102
+ raise(MetaParseError,"size entry missing")
103
+ end
104
+
105
+ unless zip_size
106
+ raise(MetaParseError,"zipSize entry missing")
107
+ end
108
+
109
+ unless gz_size
110
+ raise(MetaParseError,"gzSize entry missing")
111
+ end
112
+
113
+ unless sha256
114
+ raise(MetaParseError,"sha256 entry missing")
115
+ end
116
+
117
+ return new(last_modified_date,size,zip_size,gz_size,sha256)
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,22 @@
1
+ require 'nvd/json_feeds/feed_uri'
2
+ require 'nvd/json_feeds/meta'
3
+
4
+ module NVD
5
+ module JSONFeeds
6
+ #
7
+ # Represents a URI to the `.meta` feed file.
8
+ #
9
+ class MetaFeedURI < FeedURI
10
+
11
+ #
12
+ # Requests and parses the feed metadata.
13
+ #
14
+ # @return [FeedMeta]
15
+ #
16
+ def parse
17
+ Meta.parse(get)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,61 @@
1
+ require 'nvd/json_feeds/schema/configurations/node'
2
+ require 'nvd/json_feeds/schema/has_data_version'
3
+
4
+ module NVD
5
+ module JSONFeeds
6
+ module Schema
7
+ #
8
+ # Represents the `"configurations"` value.
9
+ #
10
+ class Configurations
11
+
12
+ include HasDataVersion
13
+
14
+ # The nodes of the configuration.
15
+ #
16
+ # @return [Array<Node>]
17
+ attr_reader :nodes
18
+
19
+ #
20
+ # Initializes the configuration.
21
+ #
22
+ # @param [Array<Node>] nodes
23
+ #
24
+ def initialize(nodes: [], **kwargs)
25
+ super(**kwargs)
26
+
27
+ @nodes = nodes
28
+ end
29
+
30
+ #
31
+ # Maps the parsed JSON to a Symbol Hash for {#initialize}.
32
+ #
33
+ # @param [Hash{String => Object}] json
34
+ #
35
+ # @return [Hash{Symbol => Object}]
36
+ #
37
+ def self.from_json(json)
38
+ {
39
+ **super(json),
40
+
41
+ nodes: Array(json['nodes']).map(&Node.method(:load))
42
+ }
43
+ end
44
+
45
+ #
46
+ # Loads the configuration from the parsed JSON.
47
+ #
48
+ # @param [Hash{String => Object}] json
49
+ # The parsed JSON.
50
+ #
51
+ # @return [Cojnfiguration]
52
+ # The loaded configuration object.
53
+ #
54
+ def self.load(json)
55
+ new(**from_json(json))
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+ end