From c3500a37d0db389f6111c45d149fdd9f2e1792ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Ju=C3=A1rez?= Date: Fri, 18 Sep 2015 11:25:45 -0500 Subject: [PATCH] Initial commit --- .gitignore | 9 + .rspec | 3 + .ruby-gemset | 1 + .ruby-version | 1 + Capfile | 8 + Gemfile | 21 +++ Gemfile.lock | 96 ++++++++++ README.md | 55 ++++++ bin/nvd_downloader | 72 ++++++++ bin/seed | 18 ++ config.ru | 4 + config/database.yml | 17 ++ config/deploy.rb | 72 ++++++++ config/deploy/production.rb | 2 + config/puma.rb | 25 +++ lib/cve_server.rb | 4 + lib/cve_server/app.rb | 54 ++++++ lib/cve_server/boot.rb | 20 +++ lib/cve_server/config.rb | 40 +++++ lib/cve_server/cpe.rb | 14 ++ lib/cve_server/cve.rb | 56 ++++++ lib/cve_server/db/connection.rb | 39 ++++ lib/cve_server/db/document.rb | 22 +++ lib/cve_server/db/mongo.rb | 51 ++++++ lib/cve_server/helper.rb | 14 ++ lib/cve_server/nvd/cvss.rb | 52 ++++++ lib/cve_server/nvd/entry.rb | 62 +++++++ lib/cve_server/nvd/reader.rb | 26 +++ log/.gitkeep | 0 nvd_data/.gitkeep | 0 spec/cve_server/app_spec.rb | 168 ++++++++++++++++++ spec/cve_server/boot_spec.rb | 25 +++ spec/cve_server/config_spec.rb | 24 +++ spec/cve_server/db/document_spec.rb | 18 ++ spec/cve_server/helper_spec.rb | 111 ++++++++++++ spec/cve_server/nvd/cvss_spec.rb | 168 ++++++++++++++++++ spec/cve_server/nvd/entry_spec.rb | 26 +++ spec/cve_server/nvd/reader_spec.rb | 25 +++ .../nvd_data/partial-nvdcve-2.0-2014.xml.gz | Bin 0 -> 11179 bytes spec/spec_helper.rb | 26 +++ tmp/.gitkeep | 0 41 files changed, 1449 insertions(+) create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .ruby-gemset create mode 100644 .ruby-version create mode 100644 Capfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README.md create mode 100755 bin/nvd_downloader create mode 100755 bin/seed create mode 100644 config.ru create mode 100644 config/database.yml create mode 100644 config/deploy.rb create mode 100644 config/deploy/production.rb create mode 100644 config/puma.rb create mode 100644 lib/cve_server.rb create mode 100644 lib/cve_server/app.rb create mode 100644 lib/cve_server/boot.rb create mode 100644 lib/cve_server/config.rb create mode 100644 lib/cve_server/cpe.rb create mode 100644 lib/cve_server/cve.rb create mode 100644 lib/cve_server/db/connection.rb create mode 100644 lib/cve_server/db/document.rb create mode 100644 lib/cve_server/db/mongo.rb create mode 100644 lib/cve_server/helper.rb create mode 100644 lib/cve_server/nvd/cvss.rb create mode 100644 lib/cve_server/nvd/entry.rb create mode 100644 lib/cve_server/nvd/reader.rb create mode 100644 log/.gitkeep create mode 100644 nvd_data/.gitkeep create mode 100644 spec/cve_server/app_spec.rb create mode 100644 spec/cve_server/boot_spec.rb create mode 100644 spec/cve_server/config_spec.rb create mode 100644 spec/cve_server/db/document_spec.rb create mode 100644 spec/cve_server/helper_spec.rb create mode 100644 spec/cve_server/nvd/cvss_spec.rb create mode 100644 spec/cve_server/nvd/entry_spec.rb create mode 100644 spec/cve_server/nvd/reader_spec.rb create mode 100644 spec/fixtures/nvd_data/partial-nvdcve-2.0-2014.xml.gz create mode 100644 spec/spec_helper.rb create mode 100644 tmp/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9999d1f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +db/* +!db/.gitkeep +nvd_data/* +!nvd_data/.gitkeep +log/* +!log/.gitkeep +tmp/* +!tmp/.gitkeep +coverage diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c5b8214a --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--colour +--backtrace +--format documentation diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 00000000..d3ea42b6 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +cve_server diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..8274681f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.2.3 diff --git a/Capfile b/Capfile new file mode 100644 index 00000000..12995210 --- /dev/null +++ b/Capfile @@ -0,0 +1,8 @@ +require 'capistrano/setup' +require 'capistrano/deploy' +require 'capistrano/puma' +require 'capistrano/puma' +require 'capistrano/rvm' +require 'capistrano/bundler' + +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..21a56a4a --- /dev/null +++ b/Gemfile @@ -0,0 +1,21 @@ +source 'https://rubygems.org' + +gem 'sinatra', '~> 1.4.6' +gem 'sinatra-json', '~> 0.1.0' +gem 'nokogiri', '~> 1.6.6.2' +gem 'mongo', '~> 2.1.0' +gem 'puma', '~> 2.13.4' + +group :production do + gem 'capistrano', '~> 3.4.0' + gem 'capistrano-bundler', '~> 1.1.4' + gem 'capistrano-rvm', '~> 0.1.2' + gem 'capistrano3-puma', '~> 1.2.1' +end + +group :development, :test do + gem 'pry', '~> 0.10.1' + gem 'rspec', '~> 3.3.0' + gem 'rack-test', '~> 0.6.3' + gem 'simplecov', '~> 0.10.0', :require => false +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..8b37b435 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,96 @@ +GEM + remote: https://rubygems.org/ + specs: + bson (3.2.4) + capistrano (3.4.0) + i18n + rake (>= 10.0.0) + sshkit (~> 1.3) + capistrano-bundler (1.1.4) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-rvm (0.1.2) + capistrano (~> 3.0) + sshkit (~> 1.2) + capistrano3-puma (1.2.1) + capistrano (~> 3.0) + puma (>= 2.6) + coderay (1.1.0) + colorize (0.7.7) + diff-lcs (1.2.5) + docile (1.1.5) + i18n (0.7.0) + json (1.8.3) + method_source (0.8.2) + mini_portile (0.6.2) + mongo (2.1.0) + bson (~> 3.0) + multi_json (1.11.1) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (2.9.2) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + puma (2.13.4) + rack (1.6.2) + rack-protection (1.5.3) + rack + rack-test (0.6.3) + rack (>= 1.0) + rake (10.4.2) + rspec (3.3.0) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-core (3.3.1) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) + simplecov (0.10.0) + docile (~> 1.1.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) + sinatra (1.4.6) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + sinatra-json (0.1.0) + multi_json (~> 1.0) + sinatra (~> 1.0) + slop (3.6.0) + sshkit (1.7.1) + colorize (>= 0.7.0) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) + tilt (2.0.1) + +PLATFORMS + ruby + +DEPENDENCIES + capistrano (~> 3.4.0) + capistrano-bundler (~> 1.1.4) + capistrano-rvm (~> 0.1.2) + capistrano3-puma (~> 1.2.1) + mongo (~> 2.1.0) + nokogiri (~> 1.6.6.2) + pry (~> 0.10.1) + puma (~> 2.13.4) + rack-test (~> 0.6.3) + rspec (~> 3.3.0) + simplecov (~> 0.10.0) + sinatra (~> 1.4.6) + sinatra-json (~> 0.1.0) + +BUNDLED WITH + 1.10.6 diff --git a/README.md b/README.md new file mode 100644 index 00000000..74aea6a9 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# CVEServer + +Simple REST-style web service for the CVE searching + +# Requirements + + * Ruby 2.x.x + * Mongo + * Ruby bundler + +# Installation + + * Clone our repository + + $ git clone https://github.com/SpiderLabs/cve_server.git + + * Install the ruby dependencies + + $ bundle install + + * Download the raw data from the National Vulnerability Database + + $ ./bin/nvd_downloader + + * Configure your database + + $ vi config/database.yml + + * Create and populate the database for you environment + + $ RACK_ENV=development ./bin/seed + + * Create and populate the database + + $ RACK_ENV=development ./bin/seed + + * Start the server + + $ RACK_ENV=development puma + +# Using the API + + * Search for an specific CVE using its ID + + http://localhost:port/v1/cve/CVE-ID + + * Search for CVEs related to any cpe + + http://localhost:port/v1/cpe/php:php + + * List all the available CPEs + + http://localhost:port/v1/cpe/ + + http://localhost:port/v1/cpe/microsoft:windows diff --git a/bin/nvd_downloader b/bin/nvd_downloader new file mode 100755 index 00000000..84e75839 --- /dev/null +++ b/bin/nvd_downloader @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift File.expand_path(File.join('..', '..', '/lib'), __FILE__) +require 'nokogiri' +require 'net/http' +require 'cve_server' + +def fetch_page(url) + uri = URI.parse(url) + use_ssl = uri.scheme == 'https' + Net::HTTP.start(uri.host, uri.port, use_ssl: use_ssl) do |http| + response = http.request_get(uri) + case response + when Net::HTTPSuccess then + response + when Net::HTTPRedirection then + location = response['location'] + fail "redirected to #{location}" + else + response.value + end + end +end + +def download_file(url, dest_path) + uri = URI.parse(url) + use_ssl = uri.scheme == 'https' + Net::HTTP.start(uri.host, uri.port, use_ssl: use_ssl) do |http| + http.request_get(uri.path) do |response| + case response + when Net::HTTPSuccess then + f = File.open(dest_path, 'w') + response.read_body do |seg| + f << seg + sleep 0.005 + end + f.close + when Net::HTTPRedirection then + location = response['location'] + fail "redirected to #{location}" + else + fail "Unable to download #{url}" + end + end + end +end + +def dest_path(link) + filename = link.split('/').last + File.join(CVEServer::Boot.config.raw_data_path, filename) +end + +response = fetch_page('https://nvd.nist.gov/download.cfm') + +if response.is_a?(Net::HTTPSuccess) + @doc = Nokogiri::HTML(response.body) + xml_file_path = '//td[@class="xml-file-type file-20"]' + @doc.xpath('//html').xpath(xml_file_path).each do |td| + link = td.xpath('a').first['href'] + next unless link =~ /.gz$/ + + dest_path = dest_path(link) + downloaded_path = [dest_path, '.download'].join + + puts "Downloading file from #{link}.." + download_file(link, downloaded_path) + + if File.exist?(downloaded_path) && File.size?(downloaded_path) + File.delete(dest_path) if File.exist?(dest_path) + File.rename(downloaded_path, dest_path) + end + end +end diff --git a/bin/seed b/bin/seed new file mode 100755 index 00000000..39aa3f51 --- /dev/null +++ b/bin/seed @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift File.expand_path(File.join('..', '..', '/lib'), __FILE__) +require 'zlib' +require 'cve_server' +require 'cve_server/nvd/reader' + +files = File.join(CVEServer::Boot.config.raw_data_path, '*.xml.gz') + +CVEServer::Cve.drop_all +Dir.glob(files).sort.each do |infile| + puts "Uncompressing #{infile}" + input = Zlib::GzipReader.open(infile).read + @doc = CVEServer::NVD::Reader.new(input) + puts 'Exporting data into the CVE collection' + CVEServer::Cve.bulk_create(@doc.all_cve) +end +puts "Reducing the cpe list" +CVEServer::Cve.reduce_cpes diff --git a/config.ru b/config.ru new file mode 100644 index 00000000..c13e10d2 --- /dev/null +++ b/config.ru @@ -0,0 +1,4 @@ +$LOAD_PATH.unshift File.expand_path('../lib', __FILE__) +require 'cve_server/app' + +run CVEServer::App diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..8c3cce7b --- /dev/null +++ b/config/database.yml @@ -0,0 +1,17 @@ +production: + database: cves_production + adapter: mongo + host: 127.0.0.1 + port: 27017 + +development: + database: cves_development + adapter: mongo + host: 127.0.0.1 + port: 27017 + +test: + database: cves_test + adapter: mongo + host: 127.0.0.1 + port: 27017 diff --git a/config/deploy.rb b/config/deploy.rb new file mode 100644 index 00000000..6b196fb2 --- /dev/null +++ b/config/deploy.rb @@ -0,0 +1,72 @@ +lock '3.4.0' +set :application, 'cve_server' +set :repo_url, 'git@github.com:SpiderLabs/cve_server.git' +set :branch, 'master' # Default branch is :master +set :deploy_to, '/home/deployer/cve_server' +set :stage, :production +set :pty, false +set :linked_dirs, fetch(:linked_dirs, []).push('nvd_data', 'log', 'tmp') + +set :puma_rackup, -> { File.join(current_path, 'config.ru') } +set :puma_state, "#{shared_path}/tmp/pids/puma.state" +set :puma_pid, "#{shared_path}/tmp/pids/puma.pid" +set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock" #accept array for multi-bind +set :puma_default_control_app, "unix://#{shared_path}/tmp/sockets/pumactl.sock" +set :puma_conf, "#{shared_path}/puma.rb" +set :puma_access_log, "#{shared_path}/log/puma_access.log" +set :puma_error_log, "#{shared_path}/log/puma_error.log" +set :puma_role, :app +set :puma_env, fetch(:rack_env, fetch(:rails_env, 'production')) +set :puma_threads, [0, 16] +set :puma_workers, 0 +set :puma_worker_timeout, nil +set :puma_init_active_record, false +set :puma_preload_app, true + +namespace :deploy do + + namespace :symlink do + desc 'Symlink linked directories' + task :linked_dirs do + next unless any? :linked_dirs + on release_roles :all do + execute :mkdir, '-pv', linked_dir_parents(shared_path) + execute :mkdir, '-pv', shared_path.join('tmp/sockets') + execute :mkdir, '-pv', shared_path.join('tmp/pids') + + fetch(:linked_dirs).each do |dir| + target = release_path.join(dir) + source = shared_path.join(dir) + unless test "[ -L #{target} ]" + if Dir.exist?(target) + execute :rm, '-rf', target + end + execute :ln, '-s', source, target + end + end + end + end + end + + desc 'download the nvd reports' + task :download_nvd_reports do + on fetch(:bundle_servers) do + within release_path do + with fetch(:bundle_env_variables, {}) do + execute :bundle, 'exec', './bin/nvd_downloader' + end + end + end + end + + desc 'reload the database with seed data' + task :seed do + on fetch(:bundle_servers) do + within release_path do + with fetch(:bundle_env_variables, {}) do + execute :bundle, 'exec', "./bin/seed RACK_ENV=#{fetch(:rack_env,{})}" + end + end + end + end +end diff --git a/config/deploy/production.rb b/config/deploy/production.rb new file mode 100644 index 00000000..812946be --- /dev/null +++ b/config/deploy/production.rb @@ -0,0 +1,2 @@ +server 'YourIP', user: 'deployer', roles: %w{app} +set :rack_env, 'production' diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..cdb0cd00 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,25 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'cve_server' + +@config = CVEServer::Boot.config + +directory @config.root +rackup File.join(@config.root, 'config.ru') +environment @config.env +daemonize @config.env == 'production' + +pid_dir = File.join(@config.root, 'tmp', 'pids') +Dir.mkdir pid_dir unless Dir.exists? pid_dir + +pidfile File.join(pid_dir, 'puma.pid') +state_path File.join(pid_dir, 'puma.state') +stdout_redirect File.join(@config.root, 'log', 'puma.stdout.log'), File.join(@config.root, 'log', 'puma.stderr.log') + +quiet + +threads 0, 16 + +socket_dir = File.join(@config.root, 'tmp','sockets') +Dir.mkdir socket_dir unless Dir.exists? socket_dir +bind 'tcp://0.0.0.0:9292' +bind "unix://#{socket_dir}/puma.sock?umask=0111" diff --git a/lib/cve_server.rb b/lib/cve_server.rb new file mode 100644 index 00000000..d4669f67 --- /dev/null +++ b/lib/cve_server.rb @@ -0,0 +1,4 @@ +require 'cve_server/boot' +require 'cve_server/cve' +require 'cve_server/cpe' +require 'cve_server/helper' diff --git a/lib/cve_server/app.rb b/lib/cve_server/app.rb new file mode 100644 index 00000000..c1ac8813 --- /dev/null +++ b/lib/cve_server/app.rb @@ -0,0 +1,54 @@ +require 'sinatra' +require 'sinatra/json' +require 'cve_server' + +module CVEServer + # CVEServer::App is the CVE Micro Service API + class App < Sinatra::Base + include CVEServer::Helper + + before do + content_type 'application/json' + end + + get '/v1/cve/:cve' do |cve| + bad_request unless valid_cve?(cve) + + @cve = CVEServer::Cve.find(cve.upcase) + if @cve + json_resp @cve + else + not_found + end + end + + get '/v1/cpe/:cpe' do |cpe| + bad_request unless valid_cpe?(cpe) + + @cves = CVEServer::Cve.all_cpe_like(cpe.downcase) + if @cves.count > 0 + json_resp @cves + else + not_found + end + end + + get '/v1/cpe' do + json_resp CVEServer::Cpe.all + end + + private + + def json_resp(body) + json body, status: 200 + end + + def not_found + halt 404, json(error: 'not-found') + end + + def bad_request + halt 400, json(error: 'invalid-parameters') + end + end +end diff --git a/lib/cve_server/boot.rb b/lib/cve_server/boot.rb new file mode 100644 index 00000000..9f9e2ae4 --- /dev/null +++ b/lib/cve_server/boot.rb @@ -0,0 +1,20 @@ +require 'cve_server/config' +require 'cve_server/db/connection' + +module CVEServer + module Boot + extend self + + def config + @config ||= CVEServer::Config.new + end + + def connection + @conn ||= CVEServer::DB::Connection.new(config) + end + + def db + connection.db if connection.open? + end + end +end diff --git a/lib/cve_server/config.rb b/lib/cve_server/config.rb new file mode 100644 index 00000000..3b97fd37 --- /dev/null +++ b/lib/cve_server/config.rb @@ -0,0 +1,40 @@ +require 'yaml' +require 'bundler/setup' + +ENV['RACK_ENV'] ||= 'development' +Bundler.require(:default, ENV['RACK_ENV'].to_sym) + +module CVEServer + # CVEServer::Config helps to handle the configuration options + class Config + attr_reader :env, :root, :path, :db_settings + + def initialize + @env = ENV['RACK_ENV'] + @root = File.join(File.dirname(__FILE__), '..', '..') + @path = File.join(root, 'config', 'database.yml') + end + + def db_adapter + db_settings['adapter'] + end + + def db_options + db_settings.select { |k, _v| k != 'adapter' } + end + + def raw_data_path + if env == 'test' + File.join(root, 'spec', 'fixtures', 'nvd_data') + else + File.join(root, 'nvd_data') + end + end + + private + + def db_settings + @db_settings ||= YAML.load(File.read(path))[env] + end + end +end diff --git a/lib/cve_server/cpe.rb b/lib/cve_server/cpe.rb new file mode 100644 index 00000000..93b3f14b --- /dev/null +++ b/lib/cve_server/cpe.rb @@ -0,0 +1,14 @@ +require 'cve_server/db/document' + +module CVEServer + # CVEServer::Cve is a module to abstract the database table/collection + # to store and read the CVEs. + class Cpe + include CVEServer::DB::Document + collection_name :cpes + + def self.all + super.collect {|cpe| cpe['_id']}.sort + end + end +end diff --git a/lib/cve_server/cve.rb b/lib/cve_server/cve.rb new file mode 100644 index 00000000..6402ad4e --- /dev/null +++ b/lib/cve_server/cve.rb @@ -0,0 +1,56 @@ +require 'cve_server/db/document' + +module CVEServer + # CVEServer::Cve is a module to abstract the database table/collection + # to store and read the CVEs. + class Cve + include CVEServer::DB::Document + + collection_name :cves + + def self.find(cve) + remove_id(super(id: cve)) + end + + def self.all_cpe_like(cpe) + all(cpes: /#{cpe}/i).collect do |h| + h['id'] + end.uniq.sort + end + + def self.reduce_cpes + map_reduce(mapper, reducer, map_reducer_opts).count + end + + def self.mapper + %q( + function() { + var application_names = []; + this.cpes.forEach(function(raw_cpe, index) { + var re = /\bcpe:\/\w:?([a-z0-9_\%\~\.\-]+?:[a-z0-9_\%\~\.\-]+)?:*\b/; + var cpe = raw_cpe.match(re)[1]; + if ((application_names.indexOf(cpe) < 0) && (cpe)) + application_names.push(cpe); + }); + for (var i = 0; i < application_names.length; i++) { + emit(application_names[i], {count: 1}); + } + } + ) + end + + def self.reducer + %q( + function(key, values) { + var res = { count: 0 }; + values.forEach(function(v) { res.count += v.count; }); + return res; + } + ) + end + + def self.map_reducer_opts + { out: { replace: 'cpes' }, raw: true } + end + end +end diff --git a/lib/cve_server/db/connection.rb b/lib/cve_server/db/connection.rb new file mode 100644 index 00000000..077c0305 --- /dev/null +++ b/lib/cve_server/db/connection.rb @@ -0,0 +1,39 @@ +module CVEServer + module DB + # CVEServer::DB::Connection implements the functionality for + # the database connection handling + class Connection + attr_reader :adapter, :options + attr_accessor :db + + def initialize(config) + @adapter = config.db_adapter + @options = config.db_options + @db = open + end + + def open? + case adapter + when 'mongo' then !db.nil? + end + end + + private + + def open + case adapter + when 'mongo' then mongo_connection + else abort "#{adapter} is not supported" + end + end + + def mongo_connection + require adapter + host_port = [[options['host'], options['port']].join(':')] + database = { database: options['database'] } + ::Mongo::Logger.logger.level = Logger::WARN + ::Mongo::Client.new(host_port, database) + end + end + end +end diff --git a/lib/cve_server/db/document.rb b/lib/cve_server/db/document.rb new file mode 100644 index 00000000..5d5d0cc6 --- /dev/null +++ b/lib/cve_server/db/document.rb @@ -0,0 +1,22 @@ +require 'cve_server/boot' +module CVEServer + # CVEServer::DB::Document adds the database connection and the specific + # code for each database into a class + module DB + module Document + extend self + + def included(base) + base.extend(CVEServer::Boot) + adapter = CVEServer::Boot.config.db_adapter + require "cve_server/db/#{adapter}" + klass = adapter.capitalize + if CVEServer::DB.const_defined?(klass) + base.extend(CVEServer::DB.const_get(klass)) + else + abort "The #{adapter} is not supported" + end + end + end + end +end diff --git a/lib/cve_server/db/mongo.rb b/lib/cve_server/db/mongo.rb new file mode 100644 index 00000000..58425fdd --- /dev/null +++ b/lib/cve_server/db/mongo.rb @@ -0,0 +1,51 @@ +module CVEServer + module DB + # CVEServer::DB::Mongo contains the code for the mongo queries + module Mongo + extend self + + def collection_name(name) + @collection = name.to_sym + end + + def find(args) + db[@collection].find(args).first + end + + def all(pattern = {}) + db[@collection].find(pattern) + end + + def all_sorted_by(attr) + db[@collection].find.sort(attr.to_sym => ::Mongo::Index::ASCENDING) + end + + def exist?(args) + db[@collection].find(args).count > 0 + end + + def create(args) + db[@collection].insert_one(args) + end + + def bulk_create(data) + inserts = data.reduce([]) do |ops, chunk| + ops << { :insert_one => chunk } + end + db[@collection].bulk_write(inserts, ordered: true) + end + + def drop_all + db[@collection].drop + end + + def map_reduce(mapper, reducer, options = {}) + db[@collection].find.map_reduce(mapper, reducer, options) + end + + def remove_id(record) + record.delete_if { |k, _| k == '_id' } if record.is_a? Hash + end + end + end +end diff --git a/lib/cve_server/helper.rb b/lib/cve_server/helper.rb new file mode 100644 index 00000000..3d89aeb4 --- /dev/null +++ b/lib/cve_server/helper.rb @@ -0,0 +1,14 @@ +module CVEServer + module Helper + module_function + + def valid_cve?(cve) + # https://cve.mitre.org/cve/identifiers/syntaxchange.html + cve.match(/^CVE-\d{4}-\d{1,7}$/i) + end + + def valid_cpe?(cpe) + cpe.match(/^[a-z0-9_\%\~\.\-]+(:[a-z0-9_\%\~\.\-]+){0,1}$/i) + end + end +end diff --git a/lib/cve_server/nvd/cvss.rb b/lib/cve_server/nvd/cvss.rb new file mode 100644 index 00000000..2858d36d --- /dev/null +++ b/lib/cve_server/nvd/cvss.rb @@ -0,0 +1,52 @@ +module CVEServer + module NVD + # CVEServer::NVD::Cvss provides an easy way to calculate a vector + # using the metrics from the NVD reports. + class Cvss + def initialize(cvss) + @cvss = cvss || {} + @cvss = keys_to_sym! + end + + def to_hash + @cvss.merge!(vector: raw_vector) if valid_vector? + @cvss.to_hash + end + + protected + + def keys_to_sym! + @cvss.keys.each_with_object({}) { |k, h| h[k.to_sym] = @cvss[k]; h } + end + + def raw_vector + metrics_map.collect do |k, abbrev| + [abbrev, @cvss[k.to_sym].to_s.slice(0).upcase].join(':') + end.join('/') + end + + def valid_vector? + valid_metrics? && valid_score? + end + + def valid_metrics? + metrics_map.keys.none? { |k| @cvss[k.to_sym].nil? } + end + + def valid_score? + @cvss[:score].to_f.between?(0.0, 10.0) + end + + def metrics_map + { + access_vector: 'AV', + access_complexity: 'AC', + authentication: 'Au', + confidentiality_impact: 'C', + integrity_impact: 'I', + availability_impact: 'A' + } + end + end + end +end diff --git a/lib/cve_server/nvd/entry.rb b/lib/cve_server/nvd/entry.rb new file mode 100644 index 00000000..6e3d64ba --- /dev/null +++ b/lib/cve_server/nvd/entry.rb @@ -0,0 +1,62 @@ +require 'cve_server/nvd/cvss' +module CVEServer + module NVD + # CVEServer::NVD::Entry provides an easy way to parse entries from the + # reports from the National CVEServererability Database. + # + # https://nvd.nist.gov/download.cfm + class Entry + def initialize(entry) + @entry = entry + end + + def to_hash + { + id: @entry['id'], + summary: xpath_content('.//vuln:summary'), + cwe: cwe_id, + published_at: xpath_content('.//vuln:published-datetime'), + updated_at: xpath_content('.//vuln:last-modified-datetime'), + cvss: CVEServer::NVD::Cvss.new(cvss).to_hash, + references: references, + cpes: cpes + } + end + + private + + def xpath_content(path, node = @entry) + node.xpath(path)[0].content + end + + def cwe_id + cwe = @entry.xpath('.//vuln:cwe')[0] + cwe['id'] unless cwe.nil? + end + + def cvss + path = './/cvss:base_metrics/cvss:*' + @entry.search(path).each_with_object({}) do |cvss, h| + h[cvss.name.gsub(/-/, '_').to_sym] = cvss.content + h + end + end + + def references + @entry.xpath('.//vuln:references').collect do |node| + { + type: node['reference_type'], + name: xpath_content('.//vuln:source', node), + href: node.xpath('.//vuln:reference')[0]['href'], + content: xpath_content('.//vuln:reference', node) + } + end + end + + def cpes + path = './/vuln:vulnerable-software-list/vuln:product' + @entry.xpath(path).collect(&:content) + end + end + end +end diff --git a/lib/cve_server/nvd/reader.rb b/lib/cve_server/nvd/reader.rb new file mode 100644 index 00000000..f94f5672 --- /dev/null +++ b/lib/cve_server/nvd/reader.rb @@ -0,0 +1,26 @@ +require 'nokogiri' +require 'cve_server/nvd/entry' + +module CVEServer + module NVD + # CVEServer::NVD::Reader provides an easy way to read the reports + # (version 2.0) from the National CVEServererability Database. + # + # https://nvd.nist.gov/download.cfm + class Reader + def initialize(input) + @doc = Nokogiri::XML(input) + end + + def all_cve + @doc.xpath('//xmlns:entry').collect do |entry| + CVEServer::NVD::Entry.new(entry).to_hash + end + end + + def each_cve(&blk) + all_cve.each(&blk) + end + end + end +end diff --git a/log/.gitkeep b/log/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/nvd_data/.gitkeep b/nvd_data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/spec/cve_server/app_spec.rb b/spec/cve_server/app_spec.rb new file mode 100644 index 00000000..1208a8cc --- /dev/null +++ b/spec/cve_server/app_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' +require 'cve_server/app' + +describe CVEServer::App do + def app + @app ||= CVEServer::App + end + + describe 'Specs for /v1/cve/:cve' do + describe 'GET /v1/cve/CVE-2014-0001' do + it 'should be successful' do + get '/v1/cve/CVE-2014-0001' + expect(last_response).to be_ok + end + + it 'should be case insensitive' do + get '/v1/cve/cve-2014-0001' + expect(last_response).to be_ok + end + + it 'should return content-type as json' do + get '/v1/cve/CVE-2014-0001' + expect(response_content_type).to eq 'application/json' + end + + it 'shoud not be emtpy' do + get '/v1/cve/CVE-2014-0001' + expect(last_response).not_to be_empty + end + + it 'should return json with the CVE-2014-0001 ID' do + get '/v1/cve/CVE-2014-0001' + expect(json_response['id']).to eq 'CVE-2014-0001' + end + + it 'should return json with the CVE-2014-0001 ID' do + get '/v1/cve/cVe-2014-0001' + expect(json_response['id']).to eq 'CVE-2014-0001' + end + + it 'should expect status equal to 200' do + get '/v1/cve/CVE-2014-0001' + expect(last_response.status).to eq(200) + end + + it 'should expect status equal to 200 using downcase characters' do + get '/v1/cve/cve-2014-0001' + expect(last_response.status).to eq(200) + end + end + + describe 'GET /v1/cve/CVE-1000-0001' do + it 'should return not found error message' do + get '/v1/cve/CVE-1000-0001' + expect(json_response['error']).to eq 'not-found' + end + + it 'should return status equel to 404' do + get '/v1/cve/CVE-1000-0001' + expect(last_response.status).to eq(404) + end + end + + describe 'GET /v1/cve/bad-requests' do + it 'should return invalid parameters message' do + get '/v1/cve/bad-request' + expect(json_response['error']).to eq 'invalid-parameters' + end + + it 'should return status equal to 400' do + get '/v1/cve/bad-request' + expect(last_response.status).to eq(400) + end + end + end + + describe 'Specs for /v1/cve/:cve' do + describe 'GET /v1/cve/apache:camel' do + it 'should be successful' do + get '/v1/cpe/apache:camel' + expect(last_response).to be_ok + end + + it 'should be case insensitive' do + get '/v1/cpe/apache:CAMEL' + expect(last_response).to be_ok + end + + it 'should return content-type as json' do + get '/v1/cpe/apache:camel' + expect(response_content_type).to eq 'application/json' + end + + it 'shoud not be emtpy' do + get '/v1/cpe/apache:camel' + expect(last_response).not_to be_empty + end + + it 'should return json with a CVE array' do + get '/v1/cpe/apache:camel' + expect(json_response).to eq ['CVE-2014-0002', 'CVE-2014-0003'] + end + + it 'should expect status equal to 200' do + get '/v1/cpe/apache:camel' + expect(last_response.status).to eq(200) + end + + it 'should expect status equal to 200 using upcase characters' do + get '/v1/cpe/apache:CAMEL' + expect(last_response.status).to eq(200) + end + end + + describe 'GET /v1/cpe/oracle:mysql' do + it 'should return not found error message' do + get '/v1/cpe/oracle:mysql' + expect(json_response['error']).to eq 'not-found' + end + + it 'should return status equal to 404' do + get '/v1/cpe/oracle:mysql' + expect(last_response.status).to eq(404) + end + end + + describe 'GET /v1/cpe/bad$requests+' do + it 'should return invalid parameters message' do + get '/v1/cpe/bad$request+' + expect(json_response['error']).to eq 'invalid-parameters' + end + + it 'should return status equel to 400' do + get '/v1/cpe/bad$request+' + expect(last_response.status).to eq(400) + end + end + end + + describe 'Specs for /v1/cve' do + describe 'GET /v1/cve' do + it 'should be successful' do + get '/v1/cpe' + expect(last_response).to be_ok + end + + it 'should return content-type as json' do + get '/v1/cpe' + expect(response_content_type).to eq 'application/json' + end + + it 'should return json with an array including the CPE mysql:mysql, pocoo:jinja2, mariadb:mariadb' do + get '/v1/cpe' + expect(json_response).to include('mysql:mysql', 'pocoo:jinja2', 'mariadb:mariadb') + end + + it 'should return json with an array with 18 CPE strings' do + get '/v1/cpe' + expect(json_response.size).to eq 18 + end + + it 'should expect status equal to 200' do + get '/v1/cpe' + expect(last_response.status).to eq(200) + end + end + end +end diff --git a/spec/cve_server/boot_spec.rb b/spec/cve_server/boot_spec.rb new file mode 100644 index 00000000..824e3e11 --- /dev/null +++ b/spec/cve_server/boot_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require 'cve_server/boot' + +describe CVEServer::Boot do + describe 'CVEServer::Boot included in a class' do + before :all do + class BootTestClass + include CVEServer::Boot + end + @class = BootTestClass.new + end + + it 'should have a CveService::Config instance in the config method' do + expect(@class.config).to be_an_instance_of(CVEServer::Config) + end + + it 'should have a CveService::DB::Connection instance in the connection method' do + expect(@class.connection).to be_an_instance_of(CVEServer::DB::Connection) + end + + it 'should not have a nil db objectx' do + expect(@class.db).not_to be nil + end + end +end diff --git a/spec/cve_server/config_spec.rb b/spec/cve_server/config_spec.rb new file mode 100644 index 00000000..87822aef --- /dev/null +++ b/spec/cve_server/config_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require 'cve_server/config' + +describe CVEServer::Config do + before :all do + @conf = CVEServer::Config.new + end + + it 'should be a test environment' do + expect(@conf.env).to eq 'test' + end + + it 'should have the mongo database adapter' do + expect(@conf.db_adapter).to eq 'mongo' + end + + it 'should have mongo connection options' do + expect(@conf.db_options).to include({'database' => 'cves_test' }) + end + + it 'should not have a raw data path empty' do + expect(@conf.raw_data_path).not_to be_empty + end +end diff --git a/spec/cve_server/db/document_spec.rb b/spec/cve_server/db/document_spec.rb new file mode 100644 index 00000000..cd8296a9 --- /dev/null +++ b/spec/cve_server/db/document_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require 'cve_server/db/document' + +describe CVEServer::DB::Document do + describe 'CVEServer::DB::Document included in a class' do + before :all do + # Dummy class + class Cve + include CVEServer::DB::Document + collection_name :cves + end + end + + it 'should find one record' do + expect(Cve.find(id: 'CVE-2014-0001')).to have_key('id') + end + end +end diff --git a/spec/cve_server/helper_spec.rb b/spec/cve_server/helper_spec.rb new file mode 100644 index 00000000..7dc11f74 --- /dev/null +++ b/spec/cve_server/helper_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' +require 'cve_server/helper' + +module HelperSpec + module_function + + def cves + [ + 'CVE-2011-1529', + 'CVE-2011-1530', + 'CVE-2011-1624', + 'CVE-2011-1625', + 'CVE-2011-1640', + 'CVE-2011-1737', + 'CVE-2011-1738', + 'CVE-2012-0814', + 'cve-2013-2125', + 'cve-2013-4548', + 'CVE-2014-1692', + 'CVE-2014-2532', + 'CVE-2014-2653', + 'CVE-2014-7250', + 'CVE-2014-9278', + 'CVE-2014-9424' + ] + end + + def invalid_cves + [ + 'CVE20111529', + 'CVE_2011_1530', + 'CVE*2011*1624', + 'CVE?2011?1624', + 'CVE-\d{4}-\d{4}', + ] + end + + def cpes + [ + '1two:livre_d_or', + '1und1:1%261_online_storage', + '20_20_applications:20_20_auto_gallery', + 'amarok:amarok', + 'amarok:web_frontend', + 'amateras:amateras_sns', + 'apple:mac_os', + 'apple:mac_os_runtime_for_java', + 'apple:mac_os_x', + 'cisco:nac_manager', + 'cisco:netflow_collection_engine', + 'cisco:network_access_control', + 'cisco:network_admission_control', + 'cisco:network_analysis_module', + 'cisco:network_convergence_system_6000', + 'cisco:network_convergence_system_6008', + 'cisco:network_services_manager', + 'cisco:nexus_1000v', + 'cisco:nexus_2148t_fex_switch', + 'cisco:nexus_2224tp_fex_switch', + 'cisco:nexus_2232pp_fex_switch', + 'cisco:nexus_2232tm_fex_switch', + 'cisco:nexus_2248tp_e_fex_switch', + 'cisco:nexus_2248tp_fex_switch', + ] + end + + def invalid_cpes + [ + '::::::::::::', + '*:*:*', + 'amazon:*', + '*:openbsd', + '\*:linux', + '%\?pple:mac_os_x', + ] + end +end + +describe 'CVEServer::Helper' do + before :all do + @module = CVEServer::Helper + end + + describe 'CVE Validations' do + HelperSpec.cves.each do |cve| + it "should validates '#{cve}'" do + expect(@module.valid_cve?(cve)).not_to be eq(nil) + end + end + + HelperSpec.invalid_cves.each do |cve| + it "should not validates '#{cve}'" do + expect(@module.valid_cve?(cve)).to eq(nil) + end + end + end + + describe 'CPE Validations' do + HelperSpec.cpes.each do |cpe| + it "should validates '#{cpe}'" do + expect(@module.valid_cpe?(cpe)).not_to be eq(nil) + end + end + + HelperSpec.invalid_cpes.each do |cpe| + it "should not validates '#{cpe}'" do + expect(@module.valid_cpe?(cpe)).to eq(nil) + end + end + end +end diff --git a/spec/cve_server/nvd/cvss_spec.rb b/spec/cve_server/nvd/cvss_spec.rb new file mode 100644 index 00000000..b088e962 --- /dev/null +++ b/spec/cve_server/nvd/cvss_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' +require 'cve_server/nvd/cvss' + +module CvssSpec + module_function + + def cvss + [ + { + cvss: { + score: '7.5', + access_vector: 'NETWORK', + access_complexity: 'LOW', + authentication: 'NONE', + confidentiality_impact: 'PARTIAL', + integrity_impact: 'PARTIAL', + availability_impact: 'PARTIAL', + source: 'http://nvd.nist.gov', + generated_on_datetime: '2014-02-02T20:10:48.000-05:00' + }, + expected_vector: 'AV:N/AC:L/Au:N/C:P/I:P/A:P' + }, + { + cvss: { + score: '4.3', + access_vector: 'NETWORK', + access_complexity: 'MEDIUM', + authentication: 'NONE', + confidentiality_impact: 'NONE', + integrity_impact: 'NONE', + availability_impact: 'PARTIAL', + source: 'http://nvd.nist.gov', + generated_on_datetime: '2015-06-11T12:19:19.957-04:00', + }, + expected_vector: 'AV:N/AC:M/Au:N/C:N/I:N/A:P' + }, + { + cvss: { + 'score': '4.0', + 'access_vector': 'NETWORK', + 'access_complexity': 'LOW', + 'authentication': 'SINGLE_INSTANCE', + 'confidentiality_impact': 'PARTIAL', + 'integrity_impact': 'NONE', + 'availability_impact': 'NONE', + 'source': 'http://nvd.nist.gov', + 'generated_on_datetime': '2015-02-09T13:50:39.923-05:00' + }, + expected_vector: 'AV:N/AC:L/Au:S/C:P/I:N/A:N' + }, + { + cvss: { + 'score': '4.3', + 'access_vector': 'NETWORK', + 'access_complexity': 'MEDIUM', + 'authentication': 'NONE', + 'confidentiality_impact': 'NONE', + 'integrity_impact': 'PARTIAL', + 'availability_impact': 'NONE', + 'source': 'http://nvd.nist.gov', + 'generated_on_datetime': '2012-01-27T09:55:00.000-05:00', + }, + expected_vector: 'AV:N/AC:M/Au:N/C:N/I:P/A:N' + }, + { + cvss: { + 'score': '7.6', + 'access_vector': 'NETWORK', + 'access_complexity': 'HIGH', + 'authentication': 'NONE', + 'confidentiality_impact': 'COMPLETE', + 'integrity_impact': 'COMPLETE', + 'availability_impact': 'COMPLETE', + 'source': 'http://nvd.nist.gov', + 'generated_on_datetime': '2011-03-03T16:58:00.000-05:00' + }, + expected_vector: 'AV:N/AC:H/Au:N/C:C/I:C/A:C' + }, + ] + end + + def invalid_cvss + [ + { + cvss: { + 'score': '11.3', + 'access_vector': 'NETWORK', + 'access_complexity': 'MEDIUM', + 'confidentiality_impact': 'NONE', + 'integrity_impact': 'PARTIAL', + 'availability_impact': 'NONE', + 'source': 'http://nvd.nist.gov', + 'generated_on_datetime': '2012-01-27T09:55:00.000-05:00', + } + }, + { + cvss: { + 'score': '-7.6', + 'access_vector': 'NETWORK', + 'access_complexity': 'HIGH', + 'authentication': 'NONE', + 'availability_impact': 'COMPLETE', + 'source': 'http://nvd.nist.gov', + 'generated_on_datetime': '2011-03-03T16:58:00.000-05:00' + } + }, + ] + end +end + +describe 'CVEServer::NVD::Cvss' do + before :all do + @klass = CVEServer::NVD::Cvss + end + + CvssSpec.cvss.each do |entry| + context 'Passes valid cvss entry' do + before :each do + @cvss = @klass.new(entry[:cvss]) + end + + it "should have a valid vector" do + expect(@cvss.send('valid_vector?')).to be true + end + + it "should have a valid score" do + expect(@cvss.send('valid_score?')).to be true + end + + it "should have valid metrics" do + expect(@cvss.send('valid_metrics?')).to be true + end + + it "should expect the vector #{entry[:expected_vector]}" do + expect(@cvss.send('raw_vector')).to be == entry[:expected_vector] + end + + it "should expect a hash including the vector" do + h = entry[:cvss].merge!(vector: entry[:expected_vector]) + expect(@cvss.to_hash).to be == h + end + end + end + + CvssSpec.invalid_cvss.each do |entry| + context 'Should not validate invalid cvss entry ' do + before :each do + @cvss = @klass.new(entry[:cvss]) + end + + it "should not have a valid vector" do + expect(@cvss.send('valid_vector?')).to be false + end + + it "should not have a valid score" do + expect(@cvss.send('valid_score?')).to be false + end + + it "should not have valid metrics" do + expect(@cvss.send('valid_metrics?')).to be false + end + + it "should expect the original hash" do + expect(@cvss.to_hash).to be == entry[:cvss] + end + end + end +end diff --git a/spec/cve_server/nvd/entry_spec.rb b/spec/cve_server/nvd/entry_spec.rb new file mode 100644 index 00000000..6c5637c7 --- /dev/null +++ b/spec/cve_server/nvd/entry_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'cve_server/nvd/entry' +require 'zlib' +require 'nokogiri' + +describe CVEServer::NVD::Entry do + before :all do + @infile = File.expand_path('../../../fixtures/nvd_data/partial-nvdcve-2.0-2014.xml.gz', __FILE__) + @xml = Zlib::GzipReader.open(@infile).read + @doc = Nokogiri::XML(@xml) + @entry = @doc.xpath('//xmlns:entry').first + @nvd_entry = CVEServer::NVD::Entry.new(@entry) + end + + it 'should be an instance of CVEServer::NVD::Entry' do + expect(@nvd_entry).to be_an_instance_of(CVEServer::NVD::Entry) + end + + it 'should have the CVE attributes' do + expect(@nvd_entry.to_hash.keys).to include(:id, :summary, :cwe, :published_at, :updated_at, :cvss, :references, :cpes) + end + + it 'should have the CVE ID CVE-2014-0001' do + expect(@nvd_entry.to_hash[:id]).to eq 'CVE-2014-0001' + end +end diff --git a/spec/cve_server/nvd/reader_spec.rb b/spec/cve_server/nvd/reader_spec.rb new file mode 100644 index 00000000..20292ea8 --- /dev/null +++ b/spec/cve_server/nvd/reader_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require 'cve_server/nvd/reader' +require 'zlib' + +describe CVEServer::NVD::Reader do + before :all do + @infile = File.expand_path('../../../fixtures/nvd_data/partial-nvdcve-2.0-2014.xml.gz', __FILE__) + @xml = Zlib::GzipReader.open(@infile).read + @nvd_reader = CVEServer::NVD::Reader.new(@xml) + end + + it 'Should return an array with CVEs' do + expect(@nvd_reader.all_cve).to be_a(Array) + end + + it 'Should return 17 entries for CVEs' do + expect(@nvd_reader.all_cve.size).to eq 17 + end + + it 'Should pass each CVE as a Hash' do + @nvd_reader.each_cve do |cve| + expect(cve).to be_an_instance_of(Hash) + end + end +end diff --git a/spec/fixtures/nvd_data/partial-nvdcve-2.0-2014.xml.gz b/spec/fixtures/nvd_data/partial-nvdcve-2.0-2014.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..dff2511b4c8dcac488fecaa3bb5e6e51ce119b19 GIT binary patch literal 11179 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 literal 0 HcmV?d00001 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..996718da --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,26 @@ +require 'rspec' +require 'rack/test' +require 'json' +require 'simplecov' + +ENV['RACK_ENV'] = 'test' +$LOAD_PATH.unshift File.expand_path('../lib', __FILE__) + +SimpleCov.start + +module CVEServer + module TestHelper + def response_content_type + last_response.headers['Content-Type'] + end + + def json_response + JSON.parse(last_response.body) + end + end +end + +RSpec.configure do |conf| + conf.include Rack::Test::Methods + conf.include CVEServer::TestHelper +end diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 00000000..e69de29b