cve_crawler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9c5c9a85897586ed3cf7cc66e9e97b9b7b09269b
4
+ data.tar.gz: 14af3dafda87aef7f5697c5d0b080036a9dc2488
5
+ SHA512:
6
+ metadata.gz: 272da0467bb4e39f284a810e6d304929cb12085cb57e838387e6d2042613f75541b1ba8af882d004ef7bfc7be0c377dde054732d373488cb8f5fc0dd3e1a891c
7
+ data.tar.gz: d319e15080382fec62bf9a36452b94b35c0f9969687fcd19a23e45c29639e28b83b8733defce524cb347cee7c7a9c78a405b8faae4b9f4dbe49b77e4b3a0d8b6
@@ -0,0 +1,42 @@
1
+ require 'time'
2
+ require 'cve_crawler'
3
+ require 'cve_parser'
4
+
5
+ module CVE
6
+ VERSION_MAJOR = 0
7
+ VERSION_MINOR = 1
8
+ VERSION_BUILD = 0
9
+ VERSION = "#{VERSION_MAJOR}.#{VERSION_MINOR}.#{VERSION_BUILD}".freeze
10
+
11
+ class Core
12
+ def initialize(crawl_type='default', verify_cert=true, user_agent=nil, filters=nil)
13
+ unless user_agent
14
+ user_agent = create_user_agent
15
+ end
16
+
17
+ @crawler = Crawler.new(crawl_type, verify_cert, user_agent)
18
+ @parser = Parser.new(filters)
19
+ end
20
+
21
+ def fetch
22
+ body = crawl.body
23
+ parse(body)
24
+ end
25
+
26
+ def crawl
27
+ @crawler.crawl
28
+ end
29
+
30
+ def parse(data)
31
+ @parser.parse(data)
32
+ end
33
+
34
+ def create_user_agent
35
+ 'RubyCVECrawler/' + VERSION + ' (https://github.com/zarthus/ruby-cve-crawler)'
36
+ end
37
+
38
+ def inspect
39
+ "<CVE::Core crawler=#{@crawler.inspect} parser=#{@parser.inspect}>"
40
+ end
41
+ end
42
+ end
@@ -0,0 +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
@@ -0,0 +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
@@ -0,0 +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
@@ -0,0 +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
@@ -0,0 +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
@@ -0,0 +1,19 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe CVE::Filter do
4
+ it 'should filter repeated identifiers' do
5
+ filter = CVE::Filter.new
6
+ vulnerability = VulnerabilityMock.generate
7
+
8
+ expect(filter.filter(vulnerability)).to equal(false)
9
+ expect(filter.filter(vulnerability)).to equal(true)
10
+ end
11
+
12
+ it 'should not filter different identifiers' do
13
+ filter = CVE::Filter.new
14
+
15
+ 3.times do
16
+ expect(filter.filter(VulnerabilityMock.generate)).to equal(false)
17
+ end
18
+ end
19
+ end
@@ -0,0 +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
@@ -0,0 +1,84 @@
1
+ require 'rspec'
2
+ require_relative File.join('..', 'lib', 'cve_crawler', 'cve_core')
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
@@ -0,0 +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
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cve_crawler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jos Ahrens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A periodic crawler that fetches the latest CVE additions, parses them,
14
+ and filters them
15
+ email: gems@zarth.us
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/cve_crawler/cve_core.rb
21
+ - lib/cve_crawler/cve_crawler.rb
22
+ - lib/cve_crawler/cve_filter.rb
23
+ - lib/cve_crawler/cve_parser.rb
24
+ - lib/cve_crawler/cve_vulnerability.rb
25
+ - spec/crawler_spec.rb
26
+ - spec/filter_spec.rb
27
+ - spec/parser_spec.rb
28
+ - spec/spec_helper.rb
29
+ - spec/vulnerability_spec.rb
30
+ homepage: https://github.com/zarthus/ruby-cve-crawler
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.4.5
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: CVE Crawler
54
+ test_files: []