dradis-saint 3.18.0

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 (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