dradis-saint 3.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.github/issue_template.md +16 -0
  3. data/.github/pull_request_template.md +36 -0
  4. data/.gitignore +12 -0
  5. data/CHANGELOG.md +39 -0
  6. data/CONTRIBUTING.md +3 -0
  7. data/Gemfile +20 -0
  8. data/LICENSE +339 -0
  9. data/README.md +25 -0
  10. data/Rakefile +1 -0
  11. data/dradis-saint.gemspec +28 -0
  12. data/lib/dradis-saint.rb +7 -0
  13. data/lib/dradis/plugins/saint.rb +11 -0
  14. data/lib/dradis/plugins/saint/engine.rb +13 -0
  15. data/lib/dradis/plugins/saint/field_processor.rb +31 -0
  16. data/lib/dradis/plugins/saint/gem_version.rb +18 -0
  17. data/lib/dradis/plugins/saint/importer.rb +130 -0
  18. data/lib/dradis/plugins/saint/version.rb +11 -0
  19. data/lib/saint/base.rb +29 -0
  20. data/lib/saint/evidence.rb +18 -0
  21. data/lib/saint/vulnerability.rb +15 -0
  22. data/lib/tasks/thorfile.rb +19 -0
  23. data/spec/dradis/plugins/saint/field_processor_spec.rb +39 -0
  24. data/spec/dradis/plugins/saint/importer_spec.rb +33 -0
  25. data/spec/fixtures/files/evidence-01.xml +8 -0
  26. data/spec/fixtures/files/full_report.xml +45 -0
  27. data/spec/fixtures/files/host-01.xml +5 -0
  28. data/spec/fixtures/files/saint_metasploitable_sample.xml +718 -0
  29. data/spec/fixtures/files/vulnerability-01.xml +17 -0
  30. data/spec/saint/evidence_spec.rb +8 -0
  31. data/spec/saint/host_spec.rb +8 -0
  32. data/spec/saint/vulnerability_spec.rb +8 -0
  33. data/spec/spec_helper.rb +10 -0
  34. data/spec/xml_element.rb +10 -0
  35. data/templates/evidence.fields +5 -0
  36. data/templates/evidence.sample +8 -0
  37. data/templates/evidence.template +14 -0
  38. data/templates/vulnerability.fields +14 -0
  39. data/templates/vulnerability.sample +35 -0
  40. data/templates/vulnerability.template +41 -0
  41. metadata +166 -0
@@ -0,0 +1,25 @@
1
+ # Saint add-on for Dradis
2
+
3
+ This add-on will enable the user to upload Saint output files in the XML format (.xml) to create a structure of Dradis nodes, issues, and evidences that contain the same information about the hosts and vulnerabilities in the original file.
4
+
5
+ The add-on requires Dradis 3.0 or higher.
6
+
7
+
8
+ ## More information
9
+
10
+ See the Dradis Framework's [README.md](https://github.com/dradis/dradis-ce/blob/master/README.md)
11
+
12
+
13
+ ## Contributing
14
+
15
+ See the Dradis Framework's [CONTRIBUTING.md](https://github.com/dradis/dradis-ce/blob/master/CONTRIBUTING.md)
16
+
17
+
18
+ ## License
19
+
20
+ Dradis Framework and all its components are released under [GNU General Public License version 2.0](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html) as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.
21
+
22
+
23
+ ## Feature requests and bugs
24
+
25
+ Please use the [Dradis Framework issue tracker](https://github.com/dradis/dradis-ce/issues) for add-on improvements and bug reports.
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,28 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+
3
+ # Maintain your gem's version:
4
+ require 'dradis/plugins/saint/version'
5
+ version = Dradis::Plugins::Saint::VERSION::STRING
6
+
7
+ # Describe your gem and declare its dependencies:
8
+ Gem::Specification.new do |s|
9
+ s.platform = Gem::Platform::RUBY
10
+ s.name = 'dradis-saint'
11
+ s.version = version
12
+ s.authors = ['Daniel Martin']
13
+ s.email = ['etd@nomejortu.com']
14
+ s.homepage = 'http://dradisframework.org'
15
+ s.summary = 'Saint upload add-on for Dradis Framework.'
16
+ s.description = 'This add-on allows you to upload and parse reports from Saint.'
17
+ s.license = 'GPL-2'
18
+
19
+ s.files = `git ls-files`.split($\)
20
+
21
+ s.add_dependency 'dradis-plugins', '~> 3.8'
22
+ s.add_dependency 'nokogiri'
23
+ s.add_dependency 'rake', '~> 13.0'
24
+
25
+ s.add_development_dependency 'bundler', '~> 1.6'
26
+ s.add_dependency 'combustion', '~> 0.6.0'
27
+ s.add_dependency 'rspec-rails'
28
+ end
@@ -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 = 18
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
@@ -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