cve_crawler 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cve_crawler/cve_core.rb +3 -3
- data/lib/cve_crawler/cve_crawler.rb +37 -37
- data/lib/cve_crawler/cve_filter.rb +35 -35
- data/lib/cve_crawler/cve_parser.rb +74 -74
- data/lib/cve_crawler/cve_vulnerability.rb +62 -62
- data/spec/crawler_spec.rb +15 -15
- data/spec/parser_spec.rb +38 -38
- data/spec/spec_helper.rb +84 -84
- data/spec/vulnerability_spec.rb +137 -137
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5bb15083b60b4fdb80817982843e88e41af0e47c
|
4
|
+
data.tar.gz: e6b894f461ed3d6d452cf60bdde31979cf5db551
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e7fde51cb8b3c79811d2df649cab980f87929babd1e0d3dbc3766edaa3366865135742c2ccc7a33fb7500c6f93dc76a6f804f54f99a0ab484d90b8c5dcc91ec
|
7
|
+
data.tar.gz: ffe61027502d9a44c491e30faa142548cc58f197aff9bab1eebf2908b1b13d085d6804abf350f4dfeffe23851437e0c341b254e7996f55d7434fa21e535292d1
|
data/lib/cve_crawler/cve_core.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require 'time'
|
2
|
-
require 'cve_crawler'
|
3
|
-
require 'cve_parser'
|
2
|
+
require 'cve_crawler/cve_crawler'
|
3
|
+
require 'cve_crawler/cve_parser'
|
4
4
|
|
5
5
|
module CVE
|
6
6
|
VERSION_MAJOR = 0
|
7
7
|
VERSION_MINOR = 1
|
8
|
-
VERSION_BUILD =
|
8
|
+
VERSION_BUILD = 1
|
9
9
|
VERSION = "#{VERSION_MAJOR}.#{VERSION_MINOR}.#{VERSION_BUILD}".freeze
|
10
10
|
|
11
11
|
class Core
|
@@ -1,37 +1,37 @@
|
|
1
|
-
require 'net/https'
|
2
|
-
require 'uri'
|
3
|
-
|
4
|
-
module CVE
|
5
|
-
class Crawler
|
6
|
-
DATA_FEED_DEFAULT = URI('https://nvd.nist.gov/download/nvd-rss.xml')
|
7
|
-
DATA_FEED_ANALYZED = URI('https://nvd.nist.gov/download/nvd-rss-analyzed.xml')
|
8
|
-
|
9
|
-
def initialize(type, verify_cert, user_agent)
|
10
|
-
@crawl_url = type.downcase == 'analyzed' ? DATA_FEED_ANALYZED : DATA_FEED_DEFAULT
|
11
|
-
@verify_cert = verify_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
12
|
-
@user_agent = user_agent
|
13
|
-
end
|
14
|
-
|
15
|
-
attr_reader :crawl_url, :verify_cert, :user_agent
|
16
|
-
|
17
|
-
def crawl
|
18
|
-
http = Net::HTTP.new(@crawl_url.host, @crawl_url.port)
|
19
|
-
http.use_ssl = @crawl_url.scheme == 'https'
|
20
|
-
|
21
|
-
if http.use_ssl?
|
22
|
-
http.verify_mode = @verify_cert
|
23
|
-
end
|
24
|
-
|
25
|
-
request = Net::HTTP::Get.new(@crawl_url, {'User-Agent' => @user_agent})
|
26
|
-
response = http.request(request)
|
27
|
-
|
28
|
-
response.value # Raise an error if status is not 200
|
29
|
-
|
30
|
-
response
|
31
|
-
end
|
32
|
-
|
33
|
-
def inspect
|
34
|
-
"#<CVE::Crawler url=#{@crawl_url.to_s}>"
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
1
|
+
require 'net/https'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module CVE
|
5
|
+
class Crawler
|
6
|
+
DATA_FEED_DEFAULT = URI('https://nvd.nist.gov/download/nvd-rss.xml')
|
7
|
+
DATA_FEED_ANALYZED = URI('https://nvd.nist.gov/download/nvd-rss-analyzed.xml')
|
8
|
+
|
9
|
+
def initialize(type, verify_cert, user_agent)
|
10
|
+
@crawl_url = type.downcase == 'analyzed' ? DATA_FEED_ANALYZED : DATA_FEED_DEFAULT
|
11
|
+
@verify_cert = verify_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
12
|
+
@user_agent = user_agent
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :crawl_url, :verify_cert, :user_agent
|
16
|
+
|
17
|
+
def crawl
|
18
|
+
http = Net::HTTP.new(@crawl_url.host, @crawl_url.port)
|
19
|
+
http.use_ssl = @crawl_url.scheme == 'https'
|
20
|
+
|
21
|
+
if http.use_ssl?
|
22
|
+
http.verify_mode = @verify_cert
|
23
|
+
end
|
24
|
+
|
25
|
+
request = Net::HTTP::Get.new(@crawl_url, {'User-Agent' => @user_agent})
|
26
|
+
response = http.request(request)
|
27
|
+
|
28
|
+
response.value # Raise an error if status is not 200
|
29
|
+
|
30
|
+
response
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<CVE::Crawler url=#{@crawl_url.to_s}>"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,35 +1,35 @@
|
|
1
|
-
module CVE
|
2
|
-
class Filter
|
3
|
-
def initialize(history_size=100)
|
4
|
-
@history = []
|
5
|
-
@history_size = history_size
|
6
|
-
end
|
7
|
-
|
8
|
-
attr_reader :history_size
|
9
|
-
|
10
|
-
def filter(contents)
|
11
|
-
return true unless contents.is_a?(Vulnerability)
|
12
|
-
return true if cve_exists?(contents.identifier)
|
13
|
-
|
14
|
-
history_check_limit
|
15
|
-
false
|
16
|
-
end
|
17
|
-
|
18
|
-
def cve_exists?(identifier)
|
19
|
-
return true if @history.include?(identifier)
|
20
|
-
|
21
|
-
@history << identifier
|
22
|
-
false
|
23
|
-
end
|
24
|
-
|
25
|
-
def history_check_limit
|
26
|
-
if @history.length >= @history_size
|
27
|
-
@history.drop((@history_size / 2).round)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def inspect
|
32
|
-
"#<CVE::Filter history=#{@history.count} limit=#{@history_size}>"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
1
|
+
module CVE
|
2
|
+
class Filter
|
3
|
+
def initialize(history_size=100)
|
4
|
+
@history = []
|
5
|
+
@history_size = history_size
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :history_size
|
9
|
+
|
10
|
+
def filter(contents)
|
11
|
+
return true unless contents.is_a?(Vulnerability)
|
12
|
+
return true if cve_exists?(contents.identifier)
|
13
|
+
|
14
|
+
history_check_limit
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def cve_exists?(identifier)
|
19
|
+
return true if @history.include?(identifier)
|
20
|
+
|
21
|
+
@history << identifier
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def history_check_limit
|
26
|
+
if @history.length >= @history_size
|
27
|
+
@history.drop((@history_size / 2).round)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#<CVE::Filter history=#{@history.count} limit=#{@history_size}>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,74 +1,74 @@
|
|
1
|
-
require 'rss'
|
2
|
-
require 'cve_filter'
|
3
|
-
require 'cve_vulnerability'
|
4
|
-
|
5
|
-
module CVE
|
6
|
-
class Parser
|
7
|
-
def initialize(filters=nil)
|
8
|
-
@filters = []
|
9
|
-
|
10
|
-
@filters << CVE::Filter.new
|
11
|
-
@filters << filters if filters
|
12
|
-
end
|
13
|
-
|
14
|
-
def parse(content)
|
15
|
-
results = parse_rss_feed(content)
|
16
|
-
|
17
|
-
filter(results)
|
18
|
-
end
|
19
|
-
|
20
|
-
def parse_rss_feed(content)
|
21
|
-
rss = RSS::Parser.parse(content)
|
22
|
-
|
23
|
-
parse_items(rss)
|
24
|
-
end
|
25
|
-
|
26
|
-
def parse_items(rss)
|
27
|
-
items = []
|
28
|
-
|
29
|
-
if rss.nil?
|
30
|
-
raise 'RSS object failed to parse, is it valid XML?'
|
31
|
-
end
|
32
|
-
|
33
|
-
rss.items.each do |item|
|
34
|
-
items << parse_item(item)
|
35
|
-
end
|
36
|
-
|
37
|
-
items
|
38
|
-
end
|
39
|
-
|
40
|
-
def parse_item(item)
|
41
|
-
CVE::Vulnerability.new({
|
42
|
-
:identifier => extract_cve_identifier(item.title),
|
43
|
-
:title => item.title,
|
44
|
-
:link => item.link,
|
45
|
-
:description => item.description,
|
46
|
-
:date => item.date
|
47
|
-
})
|
48
|
-
end
|
49
|
-
|
50
|
-
def extract_cve_identifier(title)
|
51
|
-
title.split(' ')[0]
|
52
|
-
end
|
53
|
-
|
54
|
-
def filter(results)
|
55
|
-
filtered_contents = []
|
56
|
-
|
57
|
-
results.each do |result|
|
58
|
-
result_ok = true
|
59
|
-
|
60
|
-
@filters.each do |filter|
|
61
|
-
result_ok = result_ok && !filter.filter(result)
|
62
|
-
end
|
63
|
-
|
64
|
-
filtered_contents << result if result_ok
|
65
|
-
end
|
66
|
-
|
67
|
-
filtered_contents
|
68
|
-
end
|
69
|
-
|
70
|
-
def inspect
|
71
|
-
"<CVE::Parser filters=#{@filters.inspect}>"
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
1
|
+
require 'rss'
|
2
|
+
require 'cve_crawler/cve_filter'
|
3
|
+
require 'cve_crawler/cve_vulnerability'
|
4
|
+
|
5
|
+
module CVE
|
6
|
+
class Parser
|
7
|
+
def initialize(filters=nil)
|
8
|
+
@filters = []
|
9
|
+
|
10
|
+
@filters << CVE::Filter.new
|
11
|
+
@filters << filters if filters
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(content)
|
15
|
+
results = parse_rss_feed(content)
|
16
|
+
|
17
|
+
filter(results)
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_rss_feed(content)
|
21
|
+
rss = RSS::Parser.parse(content)
|
22
|
+
|
23
|
+
parse_items(rss)
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_items(rss)
|
27
|
+
items = []
|
28
|
+
|
29
|
+
if rss.nil?
|
30
|
+
raise 'RSS object failed to parse, is it valid XML?'
|
31
|
+
end
|
32
|
+
|
33
|
+
rss.items.each do |item|
|
34
|
+
items << parse_item(item)
|
35
|
+
end
|
36
|
+
|
37
|
+
items
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_item(item)
|
41
|
+
CVE::Vulnerability.new({
|
42
|
+
:identifier => extract_cve_identifier(item.title),
|
43
|
+
:title => item.title,
|
44
|
+
:link => item.link,
|
45
|
+
:description => item.description,
|
46
|
+
:date => item.date
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
def extract_cve_identifier(title)
|
51
|
+
title.split(' ')[0]
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter(results)
|
55
|
+
filtered_contents = []
|
56
|
+
|
57
|
+
results.each do |result|
|
58
|
+
result_ok = true
|
59
|
+
|
60
|
+
@filters.each do |filter|
|
61
|
+
result_ok = result_ok && !filter.filter(result)
|
62
|
+
end
|
63
|
+
|
64
|
+
filtered_contents << result if result_ok
|
65
|
+
end
|
66
|
+
|
67
|
+
filtered_contents
|
68
|
+
end
|
69
|
+
|
70
|
+
def inspect
|
71
|
+
"<CVE::Parser filters=#{@filters.inspect}>"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -1,62 +1,62 @@
|
|
1
|
-
module CVE
|
2
|
-
class Vulnerability
|
3
|
-
SOFTWARE_EXTRACT_REGEXP = Regexp.new('[(, ]([^(), ]+)')
|
4
|
-
|
5
|
-
def initialize(data)
|
6
|
-
unless data.instance_of?(Hash)
|
7
|
-
raise 'CVE Vulnerability needs to be initialized with a hash'
|
8
|
-
end
|
9
|
-
|
10
|
-
if malformed?(data)
|
11
|
-
raise 'CVE Vulnerability data is malformed'
|
12
|
-
end
|
13
|
-
|
14
|
-
@identifier = data[:identifier]
|
15
|
-
@date = data[:date]
|
16
|
-
@description = data[:description]
|
17
|
-
@link = data[:link]
|
18
|
-
@title = data[:title]
|
19
|
-
@affected_software = extract_software_from_title(data[:title])
|
20
|
-
end
|
21
|
-
|
22
|
-
attr_reader :identifier, :date, :description, :link, :title, :affected_software
|
23
|
-
|
24
|
-
def malformed?(data)
|
25
|
-
!(data.has_key?(:identifier) && data.has_key?(:date) && data.has_key?(:description) &&
|
26
|
-
data.has_key?(:link) && data.has_key?(:title))
|
27
|
-
end
|
28
|
-
|
29
|
-
def extract_software_from_title(title)
|
30
|
-
software = []
|
31
|
-
|
32
|
-
title.scan(SOFTWARE_EXTRACT_REGEXP) do |scan|
|
33
|
-
software << scan[0]
|
34
|
-
end
|
35
|
-
|
36
|
-
software.count == 0 ? nil : software
|
37
|
-
end
|
38
|
-
|
39
|
-
def affected_count
|
40
|
-
@affected_software.nil? ? 0 : @affected_software.count
|
41
|
-
end
|
42
|
-
|
43
|
-
def equal?(cve_item, strict=false)
|
44
|
-
return false unless cve_item.is_a?(Vulnerability)
|
45
|
-
|
46
|
-
if strict
|
47
|
-
return @identifier == cve_item.identifier && @link == cve_item.link && @date.utc.iso8601 == cve_item.date.utc.iso8601 &&
|
48
|
-
@title == cve_item.title && @description == cve_item.description
|
49
|
-
end
|
50
|
-
|
51
|
-
@identifier == cve_item.identifier && @link == cve_item.link
|
52
|
-
end
|
53
|
-
|
54
|
-
def to_s
|
55
|
-
"#{@title} - #{@link}"
|
56
|
-
end
|
57
|
-
|
58
|
-
def inspect
|
59
|
-
"#<CVE::Vulnerability id=#{@identifier} affected=#{affected_count}>"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
1
|
+
module CVE
|
2
|
+
class Vulnerability
|
3
|
+
SOFTWARE_EXTRACT_REGEXP = Regexp.new('[(, ]([^(), ]+)')
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
unless data.instance_of?(Hash)
|
7
|
+
raise 'CVE Vulnerability needs to be initialized with a hash'
|
8
|
+
end
|
9
|
+
|
10
|
+
if malformed?(data)
|
11
|
+
raise 'CVE Vulnerability data is malformed'
|
12
|
+
end
|
13
|
+
|
14
|
+
@identifier = data[:identifier]
|
15
|
+
@date = data[:date]
|
16
|
+
@description = data[:description]
|
17
|
+
@link = data[:link]
|
18
|
+
@title = data[:title]
|
19
|
+
@affected_software = extract_software_from_title(data[:title])
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :identifier, :date, :description, :link, :title, :affected_software
|
23
|
+
|
24
|
+
def malformed?(data)
|
25
|
+
!(data.has_key?(:identifier) && data.has_key?(:date) && data.has_key?(:description) &&
|
26
|
+
data.has_key?(:link) && data.has_key?(:title))
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_software_from_title(title)
|
30
|
+
software = []
|
31
|
+
|
32
|
+
title.scan(SOFTWARE_EXTRACT_REGEXP) do |scan|
|
33
|
+
software << scan[0]
|
34
|
+
end
|
35
|
+
|
36
|
+
software.count == 0 ? nil : software
|
37
|
+
end
|
38
|
+
|
39
|
+
def affected_count
|
40
|
+
@affected_software.nil? ? 0 : @affected_software.count
|
41
|
+
end
|
42
|
+
|
43
|
+
def equal?(cve_item, strict=false)
|
44
|
+
return false unless cve_item.is_a?(Vulnerability)
|
45
|
+
|
46
|
+
if strict
|
47
|
+
return @identifier == cve_item.identifier && @link == cve_item.link && @date.utc.iso8601 == cve_item.date.utc.iso8601 &&
|
48
|
+
@title == cve_item.title && @description == cve_item.description
|
49
|
+
end
|
50
|
+
|
51
|
+
@identifier == cve_item.identifier && @link == cve_item.link
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"#{@title} - #{@link}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect
|
59
|
+
"#<CVE::Vulnerability id=#{@identifier} affected=#{affected_count}>"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/spec/crawler_spec.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
require_relative 'spec_helper'
|
2
|
-
|
3
|
-
describe CVE::Crawler do
|
4
|
-
it 'should crawl the analyzed file when constructed with the analyzed type' do
|
5
|
-
crawler = CVE::Crawler.new('analyzed', false, false)
|
6
|
-
|
7
|
-
expect(crawler.crawl_url).to equal(CVE::Crawler::DATA_FEED_ANALYZED)
|
8
|
-
end
|
9
|
-
|
10
|
-
it 'should crawl the default file when constructed any non-analyzed type' do
|
11
|
-
crawler = CVE::Crawler.new('anything', false, false)
|
12
|
-
|
13
|
-
expect(crawler.crawl_url).to equal(CVE::Crawler::DATA_FEED_DEFAULT)
|
14
|
-
end
|
15
|
-
end
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe CVE::Crawler do
|
4
|
+
it 'should crawl the analyzed file when constructed with the analyzed type' do
|
5
|
+
crawler = CVE::Crawler.new('analyzed', false, false)
|
6
|
+
|
7
|
+
expect(crawler.crawl_url).to equal(CVE::Crawler::DATA_FEED_ANALYZED)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should crawl the default file when constructed any non-analyzed type' do
|
11
|
+
crawler = CVE::Crawler.new('anything', false, false)
|
12
|
+
|
13
|
+
expect(crawler.crawl_url).to equal(CVE::Crawler::DATA_FEED_DEFAULT)
|
14
|
+
end
|
15
|
+
end
|
data/spec/parser_spec.rb
CHANGED
@@ -1,38 +1,38 @@
|
|
1
|
-
require_relative 'spec_helper'
|
2
|
-
|
3
|
-
describe CVE::Parser do
|
4
|
-
crawler_mock = CrawlerResultMock.new
|
5
|
-
parser = CVE::Parser.new
|
6
|
-
|
7
|
-
it 'should parse a full xml body with three items and return an array of CVE items' do
|
8
|
-
result = parser.parse(crawler_mock.xml_full)
|
9
|
-
|
10
|
-
expect(result).to be_a(Array)
|
11
|
-
expect(result.length).to eq(3)
|
12
|
-
|
13
|
-
all_cve_items = true
|
14
|
-
result.each do |value|
|
15
|
-
all_cve_items = all_cve_items && value.is_a?(CVE::Vulnerability)
|
16
|
-
end
|
17
|
-
|
18
|
-
expect(all_cve_items).to equal(true)
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'should raise an error when non-xml is passed' do
|
22
|
-
expect{ parser.parse('Not xml') }.to raise_error(RuntimeError)
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'should extract the CVE identifier from a title' do
|
26
|
-
title = VulnerabilityMock.new.title
|
27
|
-
|
28
|
-
expect(parser.extract_cve_identifier(title)).to match(/^CVE\-\d{4}\-\d+$/)
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'should accept a custom filter' do
|
32
|
-
CVE::Parser.new(CVE::Filter.new)
|
33
|
-
end
|
34
|
-
|
35
|
-
it 'should accept an array of custom filters' do
|
36
|
-
CVE::Parser.new([CVE::Filter.new, CVE::Filter.new])
|
37
|
-
end
|
38
|
-
end
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe CVE::Parser do
|
4
|
+
crawler_mock = CrawlerResultMock.new
|
5
|
+
parser = CVE::Parser.new
|
6
|
+
|
7
|
+
it 'should parse a full xml body with three items and return an array of CVE items' do
|
8
|
+
result = parser.parse(crawler_mock.xml_full)
|
9
|
+
|
10
|
+
expect(result).to be_a(Array)
|
11
|
+
expect(result.length).to eq(3)
|
12
|
+
|
13
|
+
all_cve_items = true
|
14
|
+
result.each do |value|
|
15
|
+
all_cve_items = all_cve_items && value.is_a?(CVE::Vulnerability)
|
16
|
+
end
|
17
|
+
|
18
|
+
expect(all_cve_items).to equal(true)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should raise an error when non-xml is passed' do
|
22
|
+
expect{ parser.parse('Not xml') }.to raise_error(RuntimeError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should extract the CVE identifier from a title' do
|
26
|
+
title = VulnerabilityMock.new.title
|
27
|
+
|
28
|
+
expect(parser.extract_cve_identifier(title)).to match(/^CVE\-\d{4}\-\d+$/)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should accept a custom filter' do
|
32
|
+
CVE::Parser.new(CVE::Filter.new)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should accept an array of custom filters' do
|
36
|
+
CVE::Parser.new([CVE::Filter.new, CVE::Filter.new])
|
37
|
+
end
|
38
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,84 +1,84 @@
|
|
1
|
-
require 'rspec'
|
2
|
-
require_relative File.join('..', 'lib', 'cve_crawler'
|
3
|
-
|
4
|
-
class CrawlerResultMock
|
5
|
-
def xml_full
|
6
|
-
<<-eos
|
7
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
8
|
-
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
9
|
-
<channel rdf:about="http://web.nvd.nist.gov/view/vuln/search">
|
10
|
-
<title>National Vulnerability Database</title>
|
11
|
-
<link>http://web.nvd.nist.gov/view/vuln/search</link>
|
12
|
-
<description>This feed contains the most recent CVE cyber vulnerabilities published within the National Vulnerability Database.</description>
|
13
|
-
<items>
|
14
|
-
<rdf:Seq>
|
15
|
-
<rdf:li rdf:resource="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4476" />
|
16
|
-
<rdf:li rdf:resource="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4500" />
|
17
|
-
<rdf:li rdf:resource="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4501" />
|
18
|
-
</rdf:Seq>
|
19
|
-
</items>
|
20
|
-
<dc:date>2015-09-27T04:50:00Z</dc:date>
|
21
|
-
<dc:language>en-us</dc:language>
|
22
|
-
<dc:rights>This material is not copywritten and may be freely used, however, attribution is requested.</dc:rights>
|
23
|
-
</channel>
|
24
|
-
<item rdf:about="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4476">
|
25
|
-
<title>CVE-2015-4476 (firefox)</title>
|
26
|
-
<link>http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4476</link>
|
27
|
-
<description>Mozilla Firefox before 41.0 on Android allows user-assisted remote attackers to spoof address-bar attributes by leveraging lack of navigation after a paste of a URL with a nonstandard scheme, as demonstrated by spoofing an SSL attribute.</description>
|
28
|
-
<dc:date>2015-09-24T04:59:00Z</dc:date>
|
29
|
-
</item>
|
30
|
-
<item rdf:about="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4500">
|
31
|
-
<title>CVE-2015-4500 (firefox, firefox_esr)</title>
|
32
|
-
<link>http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4500</link>
|
33
|
-
<description>Multiple unspecified vulnerabilities in the browser engine in Mozilla Firefox before 41.0 and Firefox ESR 38.x before 38.3 allow remote attackers to cause a denial of service (memory corruption and application crash) or possibly execute arbitrary code via unknown vectors.</description>
|
34
|
-
<dc:date>2015-09-24T04:59:02Z</dc:date>
|
35
|
-
</item>
|
36
|
-
<item rdf:about="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4501">
|
37
|
-
<title>CVE-2015-4501 (firefox)</title>
|
38
|
-
<link>http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4501</link>
|
39
|
-
<description>Multiple unspecified vulnerabilities in the browser engine in Mozilla Firefox before 41.0 allow remote attackers to cause a denial of service (memory corruption and application crash) or possibly execute arbitrary code via unknown vectors.</description>
|
40
|
-
<dc:date>2015-09-24T04:59:03Z</dc:date>
|
41
|
-
</item>
|
42
|
-
</rdf:RDF>
|
43
|
-
eos
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
class VulnerabilityMock
|
48
|
-
def self.generate
|
49
|
-
mocker = VulnerabilityMock.new
|
50
|
-
id = mocker.identifier
|
51
|
-
|
52
|
-
CVE::Vulnerability.new({
|
53
|
-
:identifier => id,
|
54
|
-
:title => mocker.title(id),
|
55
|
-
:link => mocker.link(id),
|
56
|
-
:description => mocker.description,
|
57
|
-
:date => mocker.date
|
58
|
-
})
|
59
|
-
end
|
60
|
-
|
61
|
-
def identifier
|
62
|
-
year = Time.now.year
|
63
|
-
"CVE-#{rand(2000..year)}-#{rand(0..9999)}"
|
64
|
-
end
|
65
|
-
|
66
|
-
def title(id=nil)
|
67
|
-
(id || identifier) + ' (' + ['Lorem Ipsum', 'Some vulnerability', 'Your favourite software',
|
68
|
-
'Firefox', 'Chromium', 'Thunderbird'].sample.split('').shuffle.join + ')'
|
69
|
-
end
|
70
|
-
|
71
|
-
def link(id=nil)
|
72
|
-
'http://web.nvd.nist.gov/view/vuln/detail?vulnId=' + (id || identifier)
|
73
|
-
end
|
74
|
-
|
75
|
-
def description
|
76
|
-
['Hakuna Matata', 'What a wonderful phrase', 'Ain\'t no passing phrase', 'It means no worries',
|
77
|
-
'for the rest of your days', 'It\'s our problem-free philosophy', 'Yeah. That\'s our motto',
|
78
|
-
'What\'s a motto?'].sample.split('').shuffle.join
|
79
|
-
end
|
80
|
-
|
81
|
-
def date
|
82
|
-
Time.now
|
83
|
-
end
|
84
|
-
end
|
1
|
+
require 'rspec'
|
2
|
+
require_relative File.join('..', 'lib', 'cve_crawler')
|
3
|
+
|
4
|
+
class CrawlerResultMock
|
5
|
+
def xml_full
|
6
|
+
<<-eos
|
7
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
8
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
9
|
+
<channel rdf:about="http://web.nvd.nist.gov/view/vuln/search">
|
10
|
+
<title>National Vulnerability Database</title>
|
11
|
+
<link>http://web.nvd.nist.gov/view/vuln/search</link>
|
12
|
+
<description>This feed contains the most recent CVE cyber vulnerabilities published within the National Vulnerability Database.</description>
|
13
|
+
<items>
|
14
|
+
<rdf:Seq>
|
15
|
+
<rdf:li rdf:resource="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4476" />
|
16
|
+
<rdf:li rdf:resource="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4500" />
|
17
|
+
<rdf:li rdf:resource="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4501" />
|
18
|
+
</rdf:Seq>
|
19
|
+
</items>
|
20
|
+
<dc:date>2015-09-27T04:50:00Z</dc:date>
|
21
|
+
<dc:language>en-us</dc:language>
|
22
|
+
<dc:rights>This material is not copywritten and may be freely used, however, attribution is requested.</dc:rights>
|
23
|
+
</channel>
|
24
|
+
<item rdf:about="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4476">
|
25
|
+
<title>CVE-2015-4476 (firefox)</title>
|
26
|
+
<link>http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4476</link>
|
27
|
+
<description>Mozilla Firefox before 41.0 on Android allows user-assisted remote attackers to spoof address-bar attributes by leveraging lack of navigation after a paste of a URL with a nonstandard scheme, as demonstrated by spoofing an SSL attribute.</description>
|
28
|
+
<dc:date>2015-09-24T04:59:00Z</dc:date>
|
29
|
+
</item>
|
30
|
+
<item rdf:about="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4500">
|
31
|
+
<title>CVE-2015-4500 (firefox, firefox_esr)</title>
|
32
|
+
<link>http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4500</link>
|
33
|
+
<description>Multiple unspecified vulnerabilities in the browser engine in Mozilla Firefox before 41.0 and Firefox ESR 38.x before 38.3 allow remote attackers to cause a denial of service (memory corruption and application crash) or possibly execute arbitrary code via unknown vectors.</description>
|
34
|
+
<dc:date>2015-09-24T04:59:02Z</dc:date>
|
35
|
+
</item>
|
36
|
+
<item rdf:about="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4501">
|
37
|
+
<title>CVE-2015-4501 (firefox)</title>
|
38
|
+
<link>http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-4501</link>
|
39
|
+
<description>Multiple unspecified vulnerabilities in the browser engine in Mozilla Firefox before 41.0 allow remote attackers to cause a denial of service (memory corruption and application crash) or possibly execute arbitrary code via unknown vectors.</description>
|
40
|
+
<dc:date>2015-09-24T04:59:03Z</dc:date>
|
41
|
+
</item>
|
42
|
+
</rdf:RDF>
|
43
|
+
eos
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class VulnerabilityMock
|
48
|
+
def self.generate
|
49
|
+
mocker = VulnerabilityMock.new
|
50
|
+
id = mocker.identifier
|
51
|
+
|
52
|
+
CVE::Vulnerability.new({
|
53
|
+
:identifier => id,
|
54
|
+
:title => mocker.title(id),
|
55
|
+
:link => mocker.link(id),
|
56
|
+
:description => mocker.description,
|
57
|
+
:date => mocker.date
|
58
|
+
})
|
59
|
+
end
|
60
|
+
|
61
|
+
def identifier
|
62
|
+
year = Time.now.year
|
63
|
+
"CVE-#{rand(2000..year)}-#{rand(0..9999)}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def title(id=nil)
|
67
|
+
(id || identifier) + ' (' + ['Lorem Ipsum', 'Some vulnerability', 'Your favourite software',
|
68
|
+
'Firefox', 'Chromium', 'Thunderbird'].sample.split('').shuffle.join + ')'
|
69
|
+
end
|
70
|
+
|
71
|
+
def link(id=nil)
|
72
|
+
'http://web.nvd.nist.gov/view/vuln/detail?vulnId=' + (id || identifier)
|
73
|
+
end
|
74
|
+
|
75
|
+
def description
|
76
|
+
['Hakuna Matata', 'What a wonderful phrase', 'Ain\'t no passing phrase', 'It means no worries',
|
77
|
+
'for the rest of your days', 'It\'s our problem-free philosophy', 'Yeah. That\'s our motto',
|
78
|
+
'What\'s a motto?'].sample.split('').shuffle.join
|
79
|
+
end
|
80
|
+
|
81
|
+
def date
|
82
|
+
Time.now
|
83
|
+
end
|
84
|
+
end
|
data/spec/vulnerability_spec.rb
CHANGED
@@ -1,137 +1,137 @@
|
|
1
|
-
require_relative 'spec_helper'
|
2
|
-
|
3
|
-
describe CVE::Vulnerability do
|
4
|
-
mocker = VulnerabilityMock.new
|
5
|
-
cve_vul_obj = VulnerabilityMock.generate
|
6
|
-
|
7
|
-
it 'should not error when passing a valid hash' do
|
8
|
-
id = mocker.identifier
|
9
|
-
|
10
|
-
item = CVE::Vulnerability.new({
|
11
|
-
:identifier => id,
|
12
|
-
:title => mocker.title(id),
|
13
|
-
:link => mocker.link(id),
|
14
|
-
:description => mocker.description,
|
15
|
-
:date => mocker.date
|
16
|
-
})
|
17
|
-
|
18
|
-
expect(item).to be_a(CVE::Vulnerability)
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'should compare to a CVE with the same identifier and link' do
|
22
|
-
id = mocker.identifier
|
23
|
-
link = mocker.link(id)
|
24
|
-
item = CVE::Vulnerability.new({
|
25
|
-
:identifier => id,
|
26
|
-
:title => mocker.title(id),
|
27
|
-
:link => link,
|
28
|
-
:description => mocker.description,
|
29
|
-
:date => mocker.date
|
30
|
-
})
|
31
|
-
|
32
|
-
item_roughly_identical = CVE::Vulnerability.new({
|
33
|
-
:identifier => id,
|
34
|
-
:title => mocker.title(id),
|
35
|
-
:link => link,
|
36
|
-
:description => mocker.description,
|
37
|
-
:date => mocker.date
|
38
|
-
})
|
39
|
-
|
40
|
-
expect(item.equal?(item_roughly_identical, false)).to equal(true)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'should compare strictly to an identical CVE' do
|
44
|
-
id = mocker.identifier
|
45
|
-
item = CVE::Vulnerability.new({
|
46
|
-
:identifier => id,
|
47
|
-
:title => mocker.title(id),
|
48
|
-
:link => mocker.link(id),
|
49
|
-
:description => mocker.description,
|
50
|
-
:date => mocker.date
|
51
|
-
})
|
52
|
-
|
53
|
-
item_identical = item.clone
|
54
|
-
|
55
|
-
expect(item.equal?(item_identical, true)).to equal(true)
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'should fail compare strictly to an unidentical CVE with the same ID' do
|
59
|
-
id = mocker.identifier
|
60
|
-
item = CVE::Vulnerability.new({
|
61
|
-
:identifier => id,
|
62
|
-
:title => mocker.title(id),
|
63
|
-
:link => mocker.link(id),
|
64
|
-
:description => mocker.description,
|
65
|
-
:date => mocker.date
|
66
|
-
})
|
67
|
-
|
68
|
-
item_unidentical = CVE::Vulnerability.new({
|
69
|
-
:identifier => id,
|
70
|
-
:title => mocker.title(id),
|
71
|
-
:link => mocker.link(id),
|
72
|
-
:description => mocker.description,
|
73
|
-
:date => mocker.date
|
74
|
-
})
|
75
|
-
|
76
|
-
expect(item.equal?(item_unidentical, true)).to equal(false)
|
77
|
-
end
|
78
|
-
|
79
|
-
it 'should fail compare to an unidentical CVE' do
|
80
|
-
id = mocker.identifier
|
81
|
-
item = CVE::Vulnerability.new({
|
82
|
-
:identifier => id,
|
83
|
-
:title => mocker.title(id),
|
84
|
-
:link => mocker.link(id),
|
85
|
-
:description => mocker.description,
|
86
|
-
:date => mocker.date
|
87
|
-
})
|
88
|
-
|
89
|
-
id = mocker.identifier
|
90
|
-
item_unidentical = CVE::Vulnerability.new({
|
91
|
-
:identifier => id,
|
92
|
-
:title => mocker.title(id),
|
93
|
-
:link => mocker.link(id),
|
94
|
-
:description => mocker.description,
|
95
|
-
:date => mocker.date
|
96
|
-
})
|
97
|
-
|
98
|
-
expect(item.equal?(item_unidentical, false)).to equal(false)
|
99
|
-
end
|
100
|
-
|
101
|
-
it 'should be able to extract a single software from the title' do
|
102
|
-
title = 'CVE-2015-123 (firefox)'
|
103
|
-
extract = cve_vul_obj.extract_software_from_title(title)
|
104
|
-
|
105
|
-
expect(extract.count).to eq(1)
|
106
|
-
expect(extract[0]).to eq('firefox')
|
107
|
-
end
|
108
|
-
|
109
|
-
it 'should be able to extract many software from the title' do
|
110
|
-
title = 'CVE-2015-123 (firefox, firefox_esr, flash)'
|
111
|
-
extract = cve_vul_obj.extract_software_from_title(title)
|
112
|
-
|
113
|
-
expect(extract.count).to eq(3)
|
114
|
-
expect(extract[0]).to eq('firefox')
|
115
|
-
expect(extract[1]).to eq('firefox_esr')
|
116
|
-
expect(extract[2]).to eq('flash')
|
117
|
-
end
|
118
|
-
|
119
|
-
it 'should return nil for affected software when not included' do
|
120
|
-
title = 'CVE-2015-123'
|
121
|
-
extract = cve_vul_obj.extract_software_from_title(title)
|
122
|
-
|
123
|
-
expect(extract).to eq(nil)
|
124
|
-
end
|
125
|
-
|
126
|
-
it 'should error when not passing a hash' do
|
127
|
-
expect{ CVE::Vulnerability.new('') }.to raise_error(RuntimeError)
|
128
|
-
end
|
129
|
-
|
130
|
-
it 'should error when passing an empty hash' do
|
131
|
-
expect{ CVE::Vulnerability.new({}) }.to raise_error(RuntimeError)
|
132
|
-
end
|
133
|
-
|
134
|
-
it 'should error when passing a hash with missing keys' do
|
135
|
-
expect{ CVE::Vulnerability.new({:identifier => 'abc', :title => 'title'}) }.to raise_error(RuntimeError)
|
136
|
-
end
|
137
|
-
end
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe CVE::Vulnerability do
|
4
|
+
mocker = VulnerabilityMock.new
|
5
|
+
cve_vul_obj = VulnerabilityMock.generate
|
6
|
+
|
7
|
+
it 'should not error when passing a valid hash' do
|
8
|
+
id = mocker.identifier
|
9
|
+
|
10
|
+
item = CVE::Vulnerability.new({
|
11
|
+
:identifier => id,
|
12
|
+
:title => mocker.title(id),
|
13
|
+
:link => mocker.link(id),
|
14
|
+
:description => mocker.description,
|
15
|
+
:date => mocker.date
|
16
|
+
})
|
17
|
+
|
18
|
+
expect(item).to be_a(CVE::Vulnerability)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should compare to a CVE with the same identifier and link' do
|
22
|
+
id = mocker.identifier
|
23
|
+
link = mocker.link(id)
|
24
|
+
item = CVE::Vulnerability.new({
|
25
|
+
:identifier => id,
|
26
|
+
:title => mocker.title(id),
|
27
|
+
:link => link,
|
28
|
+
:description => mocker.description,
|
29
|
+
:date => mocker.date
|
30
|
+
})
|
31
|
+
|
32
|
+
item_roughly_identical = CVE::Vulnerability.new({
|
33
|
+
:identifier => id,
|
34
|
+
:title => mocker.title(id),
|
35
|
+
:link => link,
|
36
|
+
:description => mocker.description,
|
37
|
+
:date => mocker.date
|
38
|
+
})
|
39
|
+
|
40
|
+
expect(item.equal?(item_roughly_identical, false)).to equal(true)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should compare strictly to an identical CVE' do
|
44
|
+
id = mocker.identifier
|
45
|
+
item = CVE::Vulnerability.new({
|
46
|
+
:identifier => id,
|
47
|
+
:title => mocker.title(id),
|
48
|
+
:link => mocker.link(id),
|
49
|
+
:description => mocker.description,
|
50
|
+
:date => mocker.date
|
51
|
+
})
|
52
|
+
|
53
|
+
item_identical = item.clone
|
54
|
+
|
55
|
+
expect(item.equal?(item_identical, true)).to equal(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should fail compare strictly to an unidentical CVE with the same ID' do
|
59
|
+
id = mocker.identifier
|
60
|
+
item = CVE::Vulnerability.new({
|
61
|
+
:identifier => id,
|
62
|
+
:title => mocker.title(id),
|
63
|
+
:link => mocker.link(id),
|
64
|
+
:description => mocker.description,
|
65
|
+
:date => mocker.date
|
66
|
+
})
|
67
|
+
|
68
|
+
item_unidentical = CVE::Vulnerability.new({
|
69
|
+
:identifier => id,
|
70
|
+
:title => mocker.title(id),
|
71
|
+
:link => mocker.link(id),
|
72
|
+
:description => mocker.description,
|
73
|
+
:date => mocker.date
|
74
|
+
})
|
75
|
+
|
76
|
+
expect(item.equal?(item_unidentical, true)).to equal(false)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should fail compare to an unidentical CVE' do
|
80
|
+
id = mocker.identifier
|
81
|
+
item = CVE::Vulnerability.new({
|
82
|
+
:identifier => id,
|
83
|
+
:title => mocker.title(id),
|
84
|
+
:link => mocker.link(id),
|
85
|
+
:description => mocker.description,
|
86
|
+
:date => mocker.date
|
87
|
+
})
|
88
|
+
|
89
|
+
id = mocker.identifier
|
90
|
+
item_unidentical = CVE::Vulnerability.new({
|
91
|
+
:identifier => id,
|
92
|
+
:title => mocker.title(id),
|
93
|
+
:link => mocker.link(id),
|
94
|
+
:description => mocker.description,
|
95
|
+
:date => mocker.date
|
96
|
+
})
|
97
|
+
|
98
|
+
expect(item.equal?(item_unidentical, false)).to equal(false)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should be able to extract a single software from the title' do
|
102
|
+
title = 'CVE-2015-123 (firefox)'
|
103
|
+
extract = cve_vul_obj.extract_software_from_title(title)
|
104
|
+
|
105
|
+
expect(extract.count).to eq(1)
|
106
|
+
expect(extract[0]).to eq('firefox')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should be able to extract many software from the title' do
|
110
|
+
title = 'CVE-2015-123 (firefox, firefox_esr, flash)'
|
111
|
+
extract = cve_vul_obj.extract_software_from_title(title)
|
112
|
+
|
113
|
+
expect(extract.count).to eq(3)
|
114
|
+
expect(extract[0]).to eq('firefox')
|
115
|
+
expect(extract[1]).to eq('firefox_esr')
|
116
|
+
expect(extract[2]).to eq('flash')
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should return nil for affected software when not included' do
|
120
|
+
title = 'CVE-2015-123'
|
121
|
+
extract = cve_vul_obj.extract_software_from_title(title)
|
122
|
+
|
123
|
+
expect(extract).to eq(nil)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should error when not passing a hash' do
|
127
|
+
expect{ CVE::Vulnerability.new('') }.to raise_error(RuntimeError)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should error when passing an empty hash' do
|
131
|
+
expect{ CVE::Vulnerability.new({}) }.to raise_error(RuntimeError)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'should error when passing a hash with missing keys' do
|
135
|
+
expect{ CVE::Vulnerability.new({:identifier => 'abc', :title => 'title'}) }.to raise_error(RuntimeError)
|
136
|
+
end
|
137
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cve_crawler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jos Ahrens
|
@@ -47,7 +47,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
47
|
version: '0'
|
48
48
|
requirements: []
|
49
49
|
rubyforge_project:
|
50
|
-
rubygems_version: 2.
|
50
|
+
rubygems_version: 2.2.2
|
51
51
|
signing_key:
|
52
52
|
specification_version: 4
|
53
53
|
summary: CVE Crawler
|