qualys 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -2
  3. data/.rubocop.yml +7 -0
  4. data/.rubocop_todo.yml +71 -0
  5. data/.travis.yml +13 -1
  6. data/Gemfile +3 -1
  7. data/Gemfile.lock +91 -0
  8. data/README.md +50 -10
  9. data/Rakefile +3 -4
  10. data/lib/qualys.rb +10 -11
  11. data/lib/qualys/api.rb +38 -47
  12. data/lib/qualys/auth.rb +11 -20
  13. data/lib/qualys/compliance.rb +3 -7
  14. data/lib/qualys/config.rb +3 -5
  15. data/lib/qualys/host.rb +17 -0
  16. data/lib/qualys/report.rb +90 -0
  17. data/lib/qualys/scans.rb +10 -22
  18. data/lib/qualys/version.rb +1 -1
  19. data/lib/qualys/vulnerability.rb +74 -0
  20. data/qualys.gemspec +18 -19
  21. data/spec/fixtures/vcr_cassettes/api_get.yml +52 -0
  22. data/spec/fixtures/vcr_cassettes/create_global_report.yml +68 -0
  23. data/spec/fixtures/vcr_cassettes/emptyscans.yml +35 -0
  24. data/spec/fixtures/vcr_cassettes/get.yml +107 -0
  25. data/spec/fixtures/vcr_cassettes/global_report.yml +17800 -0
  26. data/spec/fixtures/vcr_cassettes/load_global_report.yml +625 -0
  27. data/spec/fixtures/vcr_cassettes/login.yml +50 -0
  28. data/spec/fixtures/vcr_cassettes/logout.yml +50 -0
  29. data/spec/fixtures/vcr_cassettes/scan.yml +73 -0
  30. data/spec/fixtures/vcr_cassettes/scans.yml +89 -0
  31. data/spec/fixtures/vcr_cassettes/templates.yml +121 -0
  32. data/spec/fixtures/vcr_cassettes/try_load_not_existing_report.yml +63 -0
  33. data/spec/fixtures/vcr_cassettes/unlogged.yml +45 -0
  34. data/spec/fixtures/vcr_cassettes/wrong.yml +101 -0
  35. data/spec/qualys/api_spec.rb +27 -0
  36. data/spec/qualys/report_spec.rb +65 -0
  37. data/spec/qualys/scans_spec.rb +75 -0
  38. data/spec/qualys/version_spec.rb +11 -0
  39. data/spec/qualys/vulnerability_spec.rb +53 -0
  40. data/spec/qualys_spec.rb +20 -0
  41. data/spec/spec_helper.rb +37 -0
  42. metadata +61 -15
  43. data/.rock.yml +0 -17
  44. data/lib/qualys/reports.rb +0 -47
@@ -6,37 +6,28 @@ module Qualys
6
6
 
7
7
  # Do Login
8
8
  def self.login
9
-
10
9
  # Request a login
11
- response = self.api_post('session/', {
12
- :body => {
13
- :action => 'login',
14
- :username => Qualys::Config.username,
15
- :password => Qualys::Config.password
16
- }
17
- })
18
-
10
+ response = api_post('/session/', body: {
11
+ action: 'login',
12
+ username: Qualys::Config.username,
13
+ password: Qualys::Config.password
14
+ })
15
+
19
16
  # set the session key
20
17
  Qualys::Config.session_key = response.header['Set-Cookie']
21
18
  true
22
-
23
19
  end
24
20
 
25
21
  # Set Logout
26
22
  def self.logout
27
-
28
23
  # Request a login
29
- response = self.api_post('session/', {
30
- :body => {
31
- :action => 'logout',
32
- }
33
- })
34
-
24
+ api_post('/session/', body: {
25
+ action: 'logout'
26
+ })
27
+
35
28
  # set the session key
36
29
  Qualys::Config.session_key = nil
37
30
  true
38
-
39
31
  end
40
-
41
32
  end
42
- end
33
+ end
@@ -1,21 +1,17 @@
1
1
  module Qualys
2
2
  class Compliance < Api
3
-
4
3
  def self.all
5
- response = api_get("compliance/control/", { :query => { :action => 'list' }} )
4
+ response = api_get('/compliance/control/', query: { action: 'list' })
6
5
 
7
- unless response.parsed_response['<COMPLIANCE_SCAN_RESULT_OUTPUT']['RESPONSE'].has_key? 'COMPLIANCE_SCAN'
6
+ unless response.parsed_response['<COMPLIANCE_SCAN_RESULT_OUTPUT']['RESPONSE'].key? 'COMPLIANCE_SCAN'
8
7
  return []
9
8
  end
10
9
 
11
10
  response.parsed_response['COMPLIANCE_SCAN_RESULT_OUTPUT']['RESPONSE']['COMPLIANCE_SCAN']
12
-
13
11
  end
14
12
 
15
13
  def self.each
16
- self.all
14
+ all
17
15
  end
18
-
19
16
  end
20
-
21
17
  end
@@ -26,10 +26,8 @@ module Qualys
26
26
  #
27
27
  # @param [ String ] path The path to the file.
28
28
  def load!(path)
29
- settings = YAML.load(ERB.new(File.new(path).read).result)['api']
30
- if settings.is_a? Hash
31
- from_hash(settings)
32
- end
29
+ settings = YAML.safe_load(ERB.new(File.new(path).read).result)['api']
30
+ from_hash(settings) if settings.is_a? Hash
33
31
  end
34
32
  end
35
- end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qualys
4
+ # a scanned target
5
+ class Host
6
+ attr_accessor :ip, :tracking_method, :dns, :operating_system, :vuln_info_list, :vulnerabilities
7
+
8
+ def initialize(host, vulnerabilities = nil)
9
+ @ip = host['IP']
10
+ @tracking_method = host['TRACKING_METHOD']
11
+ @dns = host['DNS']
12
+ @operating_system = host['OPERATING_SYSTEM']
13
+ @vuln_info_list = host['VULN_INFO_LIST']
14
+ @vulnerabilities = vulnerabilities
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qualys
4
+ # Qualys reports
5
+ class Report < Api
6
+ attr_accessor :header, :host_list, :glossary, :appendices
7
+
8
+ # accepted timeout in seconds
9
+ TIMEOUT = 60.0
10
+
11
+ class << self
12
+ def find_by_id(id)
13
+ response = api_get('/report/', query: {
14
+ action: 'fetch',
15
+ id: id
16
+ })
17
+
18
+ # check if report exist
19
+ return unless response.parsed_response.keys.include?('ASSET_DATA_REPORT')
20
+
21
+ Report.new(response.parsed_response)
22
+ end
23
+
24
+ # returns the list of the templates
25
+ def templates
26
+ auth = { username: Qualys::Config.username, password: Qualys::Config.password }
27
+ response = HTTParty.get('https://qualysapi.qualys.eu/msp/report_template_list.php',
28
+ basic_auth: auth)
29
+ response.parsed_response['REPORT_TEMPLATE_LIST']['REPORT_TEMPLATE']
30
+ end
31
+
32
+ def delete(id)
33
+ api_post('/report/', query: {
34
+ action: 'delete',
35
+ id: id
36
+ })
37
+ end
38
+
39
+ # returns the id of the report
40
+ def create_global_report
41
+ scan_template = templates.detect { |template| template['TITLE'] == 'Technical Report' }
42
+ response = api_post('/report/', query: {
43
+ action: 'launch',
44
+ report_title: 'Generated_by_Ruby_Qualys_gem',
45
+ report_type: 'Scan',
46
+ output_format: 'xml',
47
+ template_id: scan_template['ID']
48
+ })
49
+
50
+ response.parsed_response['SIMPLE_RETURN']['RESPONSE']['ITEM_LIST']['ITEM']['VALUE']
51
+ end
52
+
53
+ # returns a report global report object.
54
+ # This method can be time consuming and times out after 64 s
55
+ def global_report
56
+ report_id = create_global_report
57
+ report = find_by_id(report_id)
58
+
59
+ 10.times do
60
+ sleep(TIMEOUT / 10)
61
+ report = find_by_id(report_id)
62
+ break unless report.nil?
63
+ end
64
+
65
+ raise Qualys::Report::Exception, 'Report generation timed out' if report.nil?
66
+
67
+ delete(report_id)
68
+ report
69
+ end
70
+ end
71
+
72
+ def initialize(report)
73
+ @header = report['ASSET_DATA_REPORT']['HEADER']
74
+ @host_list = report['ASSET_DATA_REPORT']['HOST_LIST']['HOST']
75
+ @glossary = report['ASSET_DATA_REPORT']['GLOSSARY']['VULN_DETAILS_LIST']['VULN_DETAILS']
76
+ @appendices = report['ASSET_DATA_REPORT']['APPENDICES']
77
+ end
78
+
79
+ def hosts
80
+ hosts ||= host_list.map do |xml_host|
81
+ vulnerabilities = xml_host['VULN_INFO_LIST']['VULN_INFO'].map do |vuln|
82
+ Qualys::Vulnerability.new(vuln, @glossary)
83
+ end
84
+ Qualys::Host.new(xml_host, vulnerabilities)
85
+ end
86
+
87
+ hosts
88
+ end
89
+ end
90
+ end
@@ -1,37 +1,28 @@
1
1
  module Qualys
2
2
  class Scans < Api
3
-
4
3
  def self.all
5
- response = api_get("scan/", { :query => { :action => 'list' }} )
6
- unless response.parsed_response['SCAN_LIST_OUTPUT']['RESPONSE'].has_key? 'SCAN_LIST'
4
+ response = api_get('/scan/', query: { action: 'list' })
5
+ unless response.parsed_response['SCAN_LIST_OUTPUT']['RESPONSE'].key? 'SCAN_LIST'
7
6
  return []
8
7
  end
9
8
 
10
9
  scanlist = response.parsed_response['SCAN_LIST_OUTPUT']['RESPONSE']['SCAN_LIST']['SCAN']
11
- scanlist.map!{|scan| Scan.new(scan)}
12
-
13
- end
14
-
15
- def self.each
16
- self.all
10
+ scanlist.map! { |scan| Scan.new(scan) }
17
11
  end
18
12
 
19
13
  def self.get(ref)
20
- response = api_get("scan/", {
21
- :query => {
22
- :action => 'fetch',
23
- :scan_ref => ref,
24
- :mode => 'extended',
25
- :output_format => 'json'
26
- }} )
14
+ response = api_get('/scan/', query: {
15
+ action: 'fetch',
16
+ scan_ref: ref,
17
+ mode: 'extended',
18
+ output_format: 'json'
19
+ })
27
20
 
28
21
  JSON.parse(response.parsed_response)
29
22
  end
30
-
31
23
  end
32
24
 
33
25
  class Scan
34
-
35
26
  attr_accessor :ref, :title, :type, :date, :duration, :status, :target, :user
36
27
 
37
28
  def initialize(scan)
@@ -50,15 +41,12 @@ module Qualys
50
41
  end
51
42
 
52
43
  def finished?
53
- if @status.eql? 'Finished'
54
- return true
55
- end
44
+ return true if @status.eql? 'Finished'
56
45
 
57
46
  false
58
47
  end
59
48
  end
60
49
 
61
50
  class Details
62
-
63
51
  end
64
52
  end
@@ -1,3 +1,3 @@
1
1
  module Qualys
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.5'.freeze
3
3
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qualys
4
+ # Qualys vulnerabilities from a report xml
5
+ class Vulnerability
6
+ attr_accessor :qid, :type, :port, :service, :protocol, :ssl, :result,
7
+ :first_found, :last_found, :times_found, :status, :details,
8
+ :cve_code_list, :title, :severity, :category, :threat, :impact,
9
+ :solution, :pci_flag, :correlation, :vendor_reference_list, :last_update,
10
+ :url
11
+
12
+ def initialize(vuln, glossary)
13
+ parse_vuln vuln
14
+
15
+ match_details glossary
16
+
17
+ parse_cve if @details['CVE_ID_LIST']
18
+ parse_details
19
+ # gives the url to the qualys knowledge base for this vulnerabitlty
20
+ parse_url
21
+ end
22
+
23
+ def to_s
24
+ "#{qid}, #{title}, severity : #{severity}, cves: #{cve_code_list&.join(', ') || 'no cve'}"
25
+ end
26
+
27
+ private
28
+
29
+ def parse_cve
30
+ cve_xlm_array = if @details['CVE_ID_LIST']['CVE_ID'].is_a?(Array)
31
+ @details['CVE_ID_LIST']['CVE_ID']
32
+ else
33
+ [@details['CVE_ID_LIST']['CVE_ID']]
34
+ end
35
+ @cve_code_list = cve_xlm_array.map { |cve| cve['ID'] }
36
+ end
37
+
38
+ # this methods finds the details for this qid in the report's glossary
39
+ def match_details(glossary)
40
+ @details = glossary.select { |detail| detail['id'] == @qid }[0]
41
+ end
42
+
43
+ def parse_details
44
+ @title = @details['TITLE']
45
+ @severity = @details['SEVERITY']
46
+ @category = @details['CATEGORY']
47
+ @threat = @details['THREAT']
48
+ @impact = @details['IMPACT']
49
+ @solution = @details['SOLUTION']
50
+ @pci_flag = @details['PCI_FLAG']
51
+ @correlation = @details['CORRELATION']
52
+ @vendor_reference_list = @details['VENDOR_REFERENCE_LIST']
53
+ @last_update = @details['BUGTRAQ_ID_LIST']
54
+ end
55
+
56
+ def parse_url
57
+ @url = 'https://qualysguard.qualys.eu/fo/common/vuln_info.php?id=' + @qid[4..-1]
58
+ end
59
+
60
+ def parse_vuln(vuln)
61
+ @qid = vuln['QID']['id']
62
+ @type = vuln['TYPE']
63
+ @port = vuln['PORT']
64
+ @service = vuln['SERVICE']
65
+ @protocol = vuln['PROTOCOL']
66
+ @ssl = vuln['SSL']
67
+ @result = vuln['RESULT']
68
+ @first_found = vuln['FIRST_FOUND']
69
+ @last_found = vuln['LAST_FOUND']
70
+ @times_found = vuln['TIMES_FOUND']
71
+ @status = vuln['VULN_STATUS']
72
+ end
73
+ end
74
+ end
@@ -1,37 +1,36 @@
1
1
  # Created by hand, like a real man
2
2
  # coding: utf-8
3
+
3
4
  lib = File.expand_path('../lib', __FILE__)
4
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
6
  require 'qualys/version'
6
7
 
7
8
  Gem::Specification.new do |s|
8
-
9
9
  s.name = 'qualys'
10
10
  s.version = Qualys::VERSION
11
- s.date = '2015-02-25'
12
- s.summary = "qualys API Client"
13
- s.description = "Easily interface with the qualys for consuming events"
14
- s.authors = ["Mike Mackintosh"]
11
+ s.date = '2017-12-17'
12
+ s.summary = 'qualys API Client'
13
+ s.description = 'Easily interface with the qualys for consuming events'
14
+ s.authors = ['Mike Mackintosh']
15
15
  s.email = 'm@zyp.io'
16
16
  s.homepage =
17
17
  'http://github.com/mikemackintosh/ruby-qualys'
18
18
 
19
19
  s.license = 'MIT'
20
-
21
- s.require_paths = ["lib"]
20
+
21
+ s.require_paths = ['lib']
22
22
  s.files = `git ls-files -z`.split("\x0")
23
- #s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
- #s.test_files = s.files.grep(%r{^(test|spec|features)/})
23
+ # s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
25
25
 
26
- s.add_dependency 'json'
27
26
  s.add_dependency 'erubis'
28
- s.add_dependency 'httparty'
29
-
30
- s.add_development_dependency "bundler"
31
- s.add_development_dependency "rake"
32
- s.add_development_dependency "rspec"
33
- s.add_development_dependency "vcr"
34
- s.add_development_dependency "webmock"
35
- s.add_development_dependency "rubocop"
27
+ s.add_dependency 'httparty', '~> 0.15'
28
+ s.add_dependency 'json'
36
29
 
37
- end
30
+ s.add_development_dependency 'bundler'
31
+ s.add_development_dependency 'rake'
32
+ s.add_development_dependency 'rspec'
33
+ s.add_development_dependency 'rubocop'
34
+ s.add_development_dependency 'vcr'
35
+ s.add_development_dependency 'webmock'
36
+ end
@@ -0,0 +1,52 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://qualysapi.qualys.eu/api/2.0/fo/scan/?action=list
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ response:
10
+ status:
11
+ code: 200
12
+ message: OK
13
+ body:
14
+ encoding: UTF-8
15
+ string: "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE SCAN_LIST_OUTPUT
16
+ SYSTEM \"https://qualysapi.qualys.eu/api/2.0/fo/scan/scan_list_output.dtd\">\n<!--
17
+ This report was generated with an evaluation version of Qualys //--> \n<SCAN_LIST_OUTPUT>\n
18
+ \ <RESPONSE>\n <DATETIME>2017-12-11T12:49:00Z</DATETIME>\n <SCAN_LIST>\n
19
+ \ <SCAN>\n <REF>scan/XXXX633905.79357</REF>\n <TYPE>On-Demand</TYPE>\n
20
+ \ <TITLE><![CDATA[shellshock 20171207]]></TITLE>\n <USER_LOGIN>Thomas</USER_LOGIN>\n
21
+ \ <LAUNCH_DATETIME>2017-12-07T08:05:05Z</LAUNCH_DATETIME>\n <DURATION>00:01:43</DURATION>\n
22
+ \ <PROCESSING_PRIORITY>0 - No Priority</PROCESSING_PRIORITY>\n <PROCESSED>1</PROCESSED>\n
23
+ \ <STATUS>\n <STATE>Error</STATE>\n </STATUS>\n <TARGET><![CDATA[47.69.112.62]]></TARGET>\n
24
+ \ </SCAN>\n <SCAN>\n <REF>scan/1512633883.79354</REF>\n <TYPE>On-Demand</TYPE>\n
25
+ \ <TITLE><![CDATA[shellshock 20171207]]></TITLE>\n <USER_LOGIN>Thomas</USER_LOGIN>\n
26
+ \ <LAUNCH_DATETIME>2017-12-07T08:04:43Z</LAUNCH_DATETIME>\n <DURATION>00:07:02</DURATION>\n
27
+ \ <PROCESSING_PRIORITY>0 - No Priority</PROCESSING_PRIORITY>\n <PROCESSED>1</PROCESSED>\n
28
+ \ <STATUS>\n <STATE>Finished</STATE>\n </STATUS>\n <TARGET><![CDATA[47.69.112.62]]></TARGET>\n
29
+ \ </SCAN>\n <SCAN>\n <REF>scan/1512633720.79248</REF>\n <TYPE>On-Demand</TYPE>\n
30
+ \ <TITLE><![CDATA[shellshock]]></TITLE>\n <USER_LOGIN>Thomas</USER_LOGIN>\n
31
+ \ <LAUNCH_DATETIME>2017-12-07T08:01:59Z</LAUNCH_DATETIME>\n <DURATION>00:00:06</DURATION>\n
32
+ \ <PROCESSING_PRIORITY>0 - No Priority</PROCESSING_PRIORITY>\n <PROCESSED>1</PROCESSED>\n
33
+ \ <STATUS>\n <STATE>Error</STATE>\n </STATUS>\n <TARGET><![CDATA[47.69.112.62]]></TARGET>\n
34
+ \ </SCAN>\n <SCAN>\n <REF>scan/1512469561.67379</REF>\n <TYPE>On-Demand</TYPE>\n
35
+ \ <TITLE><![CDATA[test_vuln]]></TITLE>\n <USER_LOGIN>Thomas</USER_LOGIN>\n
36
+ \ <LAUNCH_DATETIME>2017-12-05T10:26:01Z</LAUNCH_DATETIME>\n <DURATION>00:04:48</DURATION>\n
37
+ \ <PROCESSING_PRIORITY>1 - Emergency</PROCESSING_PRIORITY>\n <PROCESSED>1</PROCESSED>\n
38
+ \ <STATUS>\n <STATE>Finished</STATE>\n </STATUS>\n <TARGET><![CDATA[192.168.1.100]]></TARGET>\n
39
+ \ </SCAN>\n <SCAN>\n <REF>scan/1512466786.67046</REF>\n <TYPE>On-Demand</TYPE>\n
40
+ \ <TITLE><![CDATA[test]]></TITLE>\n <USER_LOGIN>Thomas</USER_LOGIN>\n
41
+ \ <LAUNCH_DATETIME>2017-12-05T09:39:46Z</LAUNCH_DATETIME>\n <DURATION>00:07:56</DURATION>\n
42
+ \ <PROCESSING_PRIORITY>0 - No Priority</PROCESSING_PRIORITY>\n <PROCESSED>1</PROCESSED>\n
43
+ \ <STATUS>\n <STATE>Finished</STATE>\n </STATUS>\n <TARGET><![CDATA[88.78.187.177]]></TARGET>\n
44
+ \ </SCAN>\n </SCAN_LIST>\n </RESPONSE>\n</SCAN_LIST_OUTPUT>\n<!--
45
+ This report was generated with an evaluation version of Qualys //--> \n<!--
46
+ CONFIDENTIAL AND PROPRIETARY INFORMATION. Qualys provides the QualysGuard
47
+ Service \"As Is,\" without any warranty of any kind. Qualys makes no warranty
48
+ that the information contained in this report is complete or error-free. Copyright
49
+ 2017, Qualys, Inc. //--> \n"
50
+ http_version:
51
+ recorded_at: Mon, 11 Dec 2017 12:49:00 GMT
52
+ recorded_with: VCR 4.0.0