dradis-saint 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CONTRIBUTING.md +3 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE +339 -0
  7. data/README.md +25 -0
  8. data/Rakefile +1 -0
  9. data/dradis-saint.gemspec +28 -0
  10. data/lib/dradis-saint.rb +7 -0
  11. data/lib/dradis/plugins/saint.rb +11 -0
  12. data/lib/dradis/plugins/saint/engine.rb +13 -0
  13. data/lib/dradis/plugins/saint/field_processor.rb +31 -0
  14. data/lib/dradis/plugins/saint/gem_version.rb +18 -0
  15. data/lib/dradis/plugins/saint/importer.rb +130 -0
  16. data/lib/dradis/plugins/saint/version.rb +11 -0
  17. data/lib/saint/base.rb +29 -0
  18. data/lib/saint/evidence.rb +18 -0
  19. data/lib/saint/vulnerability.rb +15 -0
  20. data/lib/tasks/thorfile.rb +19 -0
  21. data/spec/dradis/plugins/saint/field_processor_spec.rb +39 -0
  22. data/spec/dradis/plugins/saint/importer_spec.rb +33 -0
  23. data/spec/fixtures/files/evidence-01.xml +8 -0
  24. data/spec/fixtures/files/full_report.xml +45 -0
  25. data/spec/fixtures/files/host-01.xml +5 -0
  26. data/spec/fixtures/files/saint_metasploitable_sample.xml +718 -0
  27. data/spec/fixtures/files/vulnerability-01.xml +17 -0
  28. data/spec/saint/evidence_spec.rb +8 -0
  29. data/spec/saint/host_spec.rb +8 -0
  30. data/spec/saint/vulnerability_spec.rb +8 -0
  31. data/spec/spec_helper.rb +10 -0
  32. data/spec/xml_element.rb +10 -0
  33. data/templates/evidence.fields +5 -0
  34. data/templates/evidence.sample +8 -0
  35. data/templates/evidence.template +14 -0
  36. data/templates/vulnerability.fields +14 -0
  37. data/templates/vulnerability.sample +35 -0
  38. data/templates/vulnerability.template +41 -0
  39. metadata +165 -0
@@ -0,0 +1,7 @@
1
+ require 'dradis/plugins'
2
+
3
+ require 'dradis/plugins/saint'
4
+
5
+ require 'saint/base'
6
+ require 'saint/evidence'
7
+ require 'saint/vulnerability'
@@ -0,0 +1,11 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Saint
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'dradis/plugins/saint/engine'
9
+ require 'dradis/plugins/saint/field_processor'
10
+ require 'dradis/plugins/saint/importer'
11
+ require 'dradis/plugins/saint/version'
@@ -0,0 +1,13 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Saint
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Dradis::Plugins::Saint
6
+
7
+ include ::Dradis::Plugins::Base
8
+ description 'Processes SAINT XML format'
9
+ provides :upload
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Saint
4
+
5
+ class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
6
+ ALLOWED_DATA_NAMES = %w{evidence vulnerability host}
7
+
8
+ def post_initialize(args={})
9
+ raise 'Unhandled data name!' unless ALLOWED_DATA_NAMES.include?(data.name)
10
+ @saint_object =
11
+ "::Saint::#{data.name.capitalize}".constantize.new(data)
12
+ end
13
+
14
+ def value(args={})
15
+ field = args[:field]
16
+ _, name = field.split('.')
17
+
18
+ # We cannot send the message 'class' to the saint_object because it
19
+ # evaluates to the object's Ruby class. We temporarily rename the
20
+ # field to 'vuln_class' and switch it back later when needed.
21
+ if name == 'class'
22
+ name = 'vuln_class'
23
+ end
24
+
25
+ @saint_object.try(name) || 'n/a'
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,18 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Saint
4
+ def self.gem_version
5
+ Gem::Version.new VERSION::STRING
6
+ end
7
+
8
+ module VERSION
9
+ MAJOR = 3
10
+ MINOR = 10
11
+ TINY = 0
12
+ PRE = nil
13
+
14
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,130 @@
1
+ module Dradis::Plugins::Saint
2
+ class Importer < Dradis::Plugins::Upload::Importer
3
+ def import(params={})
4
+ @issues = {}
5
+ @hosts = {}
6
+ file_content = File.read(params[:file])
7
+
8
+ logger.info {'Parsing SAINT output file...'}
9
+ doc = Nokogiri::XML( file_content )
10
+ logger.info{'Done.'}
11
+
12
+ if doc.xpath('/report').empty?
13
+ error = "No reports were detected in the uploaded file (/report). Ensure you uploaded a SAINT XML report."
14
+ logger.fatal{ error }
15
+ content_service.create_note text: error
16
+ return false
17
+ end
18
+
19
+ doc.xpath('/report').each do |xml_report|
20
+ logger.info {'Processing report...'}
21
+
22
+ # Process <host> tags
23
+ xml_report.xpath('./overview/hosts/host').each do |host|
24
+ process_host_item(host)
25
+ end
26
+
27
+ # Process <vulnerability> tags
28
+ xml_report.xpath('./details/vulnerability').each do |vuln|
29
+ process_vuln_issue(vuln)
30
+ end
31
+
32
+ # Process <vulnerabilities> tag
33
+ xml_report.xpath('./overview/vulnerabilities/host_info').each do |xml_host_info|
34
+ host_name = xml_host_info.xpath('./hostname').first.text
35
+ xml_host_info.xpath('./vulnerability').each do |evidence|
36
+ process_evidence(evidence, host_name)
37
+ end
38
+ end
39
+
40
+ logger.info {'Report processed...'}
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ private
47
+
48
+ def process_evidence(xml_evidence, host_node_name)
49
+ # Associate the xml tag as evidence
50
+ xml_evidence.name = 'evidence'
51
+ evidence_desc = xml_evidence.xpath('./description').first.text
52
+
53
+ # Find the host node
54
+ host_node = @hosts[host_node_name]
55
+
56
+ if !host_node
57
+ logger.error { "[WARNING] Cannot find an associated host for '#{evidence_desc}'." }
58
+ return
59
+ end
60
+
61
+ # Find the related issue
62
+ issue_plugin_id = Digest::SHA1.hexdigest(evidence_desc)
63
+ issue = @issues[issue_plugin_id]
64
+
65
+ evidence_text = template_service.process_template(template: 'evidence', data: xml_evidence)
66
+
67
+ if issue
68
+ # Create Dradis evidence
69
+ logger.info{ "\t\t => Creating new evidence..." }
70
+ content_service.create_evidence(issue: issue, node: host_node, content: evidence_text)
71
+ else
72
+ # Create Note in Host
73
+ logger.info{ "\t\t => Creating note for host node..." }
74
+ note_text = "#[Title]#\n#{evidence_desc}\n\n" + evidence_text
75
+ content_service.create_note(text: note_text, node: host_node)
76
+ end
77
+ end
78
+
79
+ def process_host_item(xml_host)
80
+ # Create Dradis node
81
+ host_name = xml_host.xpath('./hostname').first.text || "Unnamed host"
82
+ host_node = content_service.create_node(label: host_name, type: :host)
83
+ logger.info{ "\tHost: #{host_name}" }
84
+
85
+ # Save the host for later to be linked to evidences
86
+ @hosts[host_name] = host_node
87
+
88
+ # Add properties to the node
89
+ if xml_host.xpath('./ipaddr').first
90
+ host_node.set_property(:ip, xml_host.xpath('./ipaddr').first.text)
91
+ end
92
+ if xml_host.xpath('./hosttype').first
93
+ host_node.set_property(:os, xml_host.xpath('./hosttype').first.text)
94
+ end
95
+ host_node.set_property(:hostname, host_name)
96
+ host_node.save
97
+ end
98
+
99
+ def process_vuln_issue(xml_vuln)
100
+ element_desc = xml_vuln.xpath('./description').first.text
101
+
102
+ # Check if the vulnerability is a real issue or a service
103
+ if real_issue?(xml_vuln)
104
+ # Create Dradis Issue
105
+ logger.info{ "\t\t => Creating new issue..." }
106
+ plugin_id = Digest::SHA1.hexdigest(element_desc)
107
+
108
+ issue_text = template_service.process_template(template: 'vulnerability', data: xml_vuln)
109
+ issue = content_service.create_issue(text: issue_text, id: plugin_id)
110
+ else
111
+ # Create Note in Host
112
+ logger.info{ "\t\t => Creating note for host node..." }
113
+
114
+ note_details = xml_vuln.xpath('./vuln_details').first.text
115
+ note_text = "#[Title]#\n#{element_desc}\n\n" + note_details
116
+
117
+ host_name = xml_vuln.xpath('./hostname').first.text || "Unnamed host"
118
+ host_node = @hosts[host_name]
119
+ content_service.create_note(text: note_text, node: host_node)
120
+ end
121
+
122
+ # Save the issue for later to be linked to evidences
123
+ @issues[plugin_id] = issue
124
+ end
125
+
126
+ def real_issue?(xml_vuln)
127
+ xml_vuln.xpath('./severity').first.text != 'Service'
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Dradis
4
+ module Plugins
5
+ module Saint
6
+ def self.version
7
+ gem_version
8
+ end
9
+ end
10
+ end
11
+ end
data/lib/saint/base.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Saint
2
+ class Base
3
+ def initialize(xml_node)
4
+ @xml = xml_node
5
+ end
6
+
7
+ def supported_tags
8
+ []
9
+ end
10
+
11
+ def respond_to?(method, include_private=false)
12
+ return true if supported_tags.include?(method.to_sym)
13
+ super
14
+ end
15
+
16
+ def method_missing(method, *args)
17
+ unless supported_tags.include?(method)
18
+ super
19
+ return
20
+ end
21
+
22
+ return process_field_value(method)
23
+ end
24
+
25
+ def process_field_value(method)
26
+ raise "Method #process_field_value not overridden!"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ module Saint
2
+ class Evidence < Base
3
+ def supported_tags
4
+ [ :port, :severity, :vuln_class, :cve, :cvss_base_score ]
5
+ end
6
+
7
+ def process_field_value(method)
8
+ # We cannot send the message 'class' to the saint_object because it
9
+ # evaluates to the object's Ruby class. We temporarily rename the
10
+ # field to 'vuln_class' and switch it back later when needed.
11
+ if method == :vuln_class
12
+ method = :class
13
+ end
14
+
15
+ @xml.xpath("./#{method.to_s}").first.try(:text)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Saint
2
+ class Vulnerability < Base
3
+ def supported_tags
4
+ [
5
+ :description, :hostname, :ipaddr, :hosttype, :scan_time, :status,
6
+ :severity, :cve, :cvss_base_score, :impact, :background, :problem,
7
+ :resolution, :reference
8
+ ]
9
+ end
10
+
11
+ def process_field_value(method)
12
+ @xml.xpath("./#{method.to_s}").first.try(:text)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ class SaintTasks < Thor
2
+ include Rails.application.config.dradis.thor_helper_module
3
+
4
+ namespace "dradis:plugins:saint"
5
+
6
+ desc "upload FILE", "upload Saint XML file"
7
+ def upload(file_path)
8
+ require 'config/environment'
9
+
10
+ unless File.exists?(file_path)
11
+ $stderr.puts "** the file [#{file_path}] does not exist"
12
+ exit(-1)
13
+ end
14
+
15
+ detect_and_set_project_scope
16
+ importer = Dradis::Plugins::Saint::Importer.new(task_options)
17
+ importer.import(file: file_path)
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dradis::Plugins::Saint::FieldProcessor do
4
+ let (:xml_file) { File.expand_path('spec/fixtures/files/full_report.xml') }
5
+
6
+ before do
7
+ @doc = Nokogiri::XML(File.read(xml_file))
8
+ @doc = @doc.xpath('./report/overview').first
9
+ end
10
+
11
+ describe "#value" do
12
+ context "for hosts and vulnerabilities" do
13
+ before do
14
+ @test_xml = @doc.xpath('./hosts/host').first
15
+ end
16
+
17
+ it "returns the value of the item's tag" do
18
+ processor = described_class.new(data: @test_xml)
19
+ value = processor.value(field: 'host.hostname')
20
+
21
+ expect(value).to eq("Test Hostname")
22
+ end
23
+ end
24
+
25
+ context "for evidences" do
26
+ before do
27
+ @test_xml = @doc.xpath('./vulnerabilities/host_info/vulnerability').first
28
+ @test_xml.name = 'evidence'
29
+ end
30
+
31
+ it "returns the value for the class attribute" do
32
+ processor = described_class.new(data: @test_xml)
33
+ value = processor.value(field: 'evidence.class')
34
+
35
+ expect(value).to eq("Test Vuln class")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dradis::Plugins::Saint::Importer do
4
+ before(:each) do
5
+ # Stub template service
6
+ templates_dir = File.expand_path('../../../../../templates', __FILE__)
7
+ expect_any_instance_of(Dradis::Plugins::TemplateService)
8
+ .to receive(:default_templates_dir).and_return(templates_dir)
9
+
10
+ plugin = Dradis::Plugins::Saint
11
+
12
+ @content_service = Dradis::Plugins::ContentService::Base.new(
13
+ logger: Logger.new(STDOUT),
14
+ plugin: plugin
15
+ )
16
+
17
+ @importer = described_class.new(
18
+ content_service: @content_service
19
+ )
20
+ end
21
+
22
+ it "creates the appropriate Dradis items" do
23
+ allow(@content_service).to receive(:create_issue) do |args|
24
+ OpenStruct.new(args)
25
+ end
26
+ allow(@content_service).to receive(:create_note) do |args|
27
+ OpenStruct.new(args)
28
+ end
29
+ expect(@content_service).to receive(:create_node).with(hash_including label: '192.168.150.163').once
30
+
31
+ @importer.import(file: 'spec/fixtures/files/saint_metasploitable_sample.xml')
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ <evidence>
2
+ <port>Test Port</port>
3
+ <severity>Test Severity</severity>
4
+ <description>Test Description</description>
5
+ <class>Test Vuln class</class>
6
+ <cve>Test Cve</cve>
7
+ <cvss_base_score>Test Cvss base score</cvss_base_score>
8
+ </evidence>
@@ -0,0 +1,45 @@
1
+ <report>
2
+ <summary>
3
+ </summary>
4
+ <overview>
5
+ <hosts>
6
+ <host>
7
+ <hostname>Test Hostname</hostname>
8
+ <ipaddr>Test Ipaddr</ipaddr>
9
+ <hosttype>Test Hosttype</hosttype>
10
+ </host>
11
+ </hosts>
12
+ <vulnerabilities>
13
+ <host_info>
14
+ <hostname>Test Hostname</hostname>
15
+ <vulnerability>
16
+ <port>Test Port</port>
17
+ <severity>Test Severity</severity>
18
+ <description>Test Description</description>
19
+ <class>Test Vuln class</class>
20
+ <cve>Test Cve</cve>
21
+ <cvss_base_score>Test Cvss base score</cvss_base_score>
22
+ </vulnerability>
23
+ </host_info>
24
+ </vulnerabilities>
25
+ </overview>
26
+ <details>
27
+ <vulnerability>
28
+ <description>Test Description</description>
29
+ <hostname>Test Hostname</hostname>
30
+ <ipaddr>Test Ipaddr</ipaddr>
31
+ <hosttype>Test Hosttype</hosttype>
32
+ <scan_time>Test Scan time</scan_time>
33
+ <status>Test Status</status>
34
+ <severity>Test Severity</severity>
35
+ <cve>Test Cve</cve>
36
+ <cvss_base_score>Test Cvss base score</cvss_base_score>
37
+ <impact>Test Impact</impact>
38
+ <background>Test Background</background>
39
+ <problem>Test Problem</problem>
40
+ <resolution>Test Resolution</resolution>
41
+ <reference>Test Reference</reference>
42
+ <vuln_details></vuln_details>
43
+ </vulnerability>
44
+ </details>
45
+ </report>