dradis-netsparker 3.8.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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/CHANGELOG.md +4 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +23 -0
- data/LICENSE +339 -0
- data/README.md +27 -0
- data/Rakefile +1 -0
- data/dradis-netsparker.gemspec +35 -0
- data/lib/dradis-netsparker.rb +8 -0
- data/lib/dradis/plugins/netsparker.rb +11 -0
- data/lib/dradis/plugins/netsparker/engine.rb +9 -0
- data/lib/dradis/plugins/netsparker/field_processor.rb +21 -0
- data/lib/dradis/plugins/netsparker/gem_version.rb +19 -0
- data/lib/dradis/plugins/netsparker/importer.rb +64 -0
- data/lib/dradis/plugins/netsparker/version.rb +13 -0
- data/lib/netsparker/vulnerability.rb +130 -0
- data/lib/tasks/thorfile.rb +20 -0
- data/spec/dradis-netsparker_spec.rb +85 -0
- data/spec/fixtures/files/example-evidence.xml +85 -0
- data/spec/fixtures/files/example.xml +2485 -0
- data/spec/fixtures/files/netsparker-localhost-demo.xml +2529 -0
- data/spec/spec_helper.rb +10 -0
- data/templates/evidence.fields +3 -0
- data/templates/evidence.sample +55 -0
- data/templates/evidence.template +8 -0
- data/templates/issue.fields +14 -0
- data/templates/issue.sample +55 -0
- data/templates/issue.template +15 -0
- metadata +162 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Netsparker
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'dradis/plugins/netsparker/engine'
|
9
|
+
require 'dradis/plugins/netsparker/field_processor'
|
10
|
+
require 'dradis/plugins/netsparker/importer'
|
11
|
+
require 'dradis/plugins/netsparker/version'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dradis::Plugins::Netsparker
|
2
|
+
# This processor defers to ::Netsparker::Vulnerability for the issue and
|
3
|
+
# evidence templates.
|
4
|
+
class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
|
5
|
+
|
6
|
+
def post_initialize(args={})
|
7
|
+
@netsparker_object = Netsparker::Vulnerability.new(data)
|
8
|
+
end
|
9
|
+
|
10
|
+
def value(args={})
|
11
|
+
field = args[:field]
|
12
|
+
|
13
|
+
# fields in the template are of the form <foo>.<field>, where <foo>
|
14
|
+
# is common across all fields for a given template (and meaningless).
|
15
|
+
_, name = field.split('.')
|
16
|
+
|
17
|
+
@netsparker_object.try(name) || 'n/a'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Netsparker
|
4
|
+
# Returns the version of the currently loaded Netsparker as a <tt>Gem::Version</tt>
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 3
|
11
|
+
MINOR = 8
|
12
|
+
TINY = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Dradis::Plugins::Netsparker
|
2
|
+
class Importer < Dradis::Plugins::Upload::Importer
|
3
|
+
|
4
|
+
# The framework will call this function if the user selects this plugin from
|
5
|
+
# the dropdown list and uploads a file.
|
6
|
+
# @returns true if the operation was successful, false otherwise
|
7
|
+
def import(params={})
|
8
|
+
file_content = File.read( params.fetch(:file) )
|
9
|
+
|
10
|
+
logger.info{'Parsing Netsparker output file...'}
|
11
|
+
@doc = Nokogiri::XML( file_content )
|
12
|
+
logger.info{'Done.'}
|
13
|
+
|
14
|
+
if @doc.xpath('/netsparker').empty?
|
15
|
+
error = "No scan results were detected in the uploaded file (/netsparker). Ensure you uploaded an Netsparker XML report."
|
16
|
+
logger.fatal{ error }
|
17
|
+
content_service.create_note text: error
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
@doc.xpath('/netsparker/target').each do |xml_host|
|
22
|
+
process_report_host(xml_host)
|
23
|
+
end
|
24
|
+
|
25
|
+
return true
|
26
|
+
end # /import
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def process_report_host(xml_host)
|
31
|
+
# Create Nodes from the <url> tags
|
32
|
+
host_node_label = xml_host.at_xpath('./url').text
|
33
|
+
host_node_label = URI.parse(host_node_label).host rescue host_node_label
|
34
|
+
logger.info{ "\t\t => Creating new host: #{host_node_label}" }
|
35
|
+
host_node = content_service.create_node(label: host_node_label, type: :host)
|
36
|
+
|
37
|
+
@doc.xpath('/netsparker/vulnerability').each do |xml_vuln|
|
38
|
+
process_vuln(xml_vuln, host_node)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_vuln(xml_vuln, host_node)
|
44
|
+
type = xml_vuln.at_xpath('./type').text()
|
45
|
+
|
46
|
+
# Create Issues using the Issue template
|
47
|
+
logger.info{ "\t\t => Creating new Issue: #{type}" }
|
48
|
+
|
49
|
+
issue_text = template_service.process_template(template: 'issue', data: xml_vuln)
|
50
|
+
issue = content_service.create_issue(text: issue_text, id: type)
|
51
|
+
|
52
|
+
# Create Evidence using the Evidence template
|
53
|
+
# Associate the Evidence with the Node and Issue
|
54
|
+
logger.info{ "\t\t => Creating new evidence" }
|
55
|
+
evidence_content = template_service.process_template(
|
56
|
+
template: 'evidence', data: xml_vuln
|
57
|
+
)
|
58
|
+
content_service.create_evidence(
|
59
|
+
issue: issue, node: host_node, content: evidence_content
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Netsparker
|
2
|
+
# This class represents each of the /netsparker/vulnerability elements in the
|
3
|
+
# Netsparker XML document.
|
4
|
+
#
|
5
|
+
# It provides a convenient way to access the information scattered all over
|
6
|
+
# the XML in attributes and nested tags.
|
7
|
+
#
|
8
|
+
# Instead of providing separate methods for each supported property we rely
|
9
|
+
# on Ruby's #method_missing to do most of the work.
|
10
|
+
class Vulnerability
|
11
|
+
attr_accessor :xml
|
12
|
+
|
13
|
+
# Accepts an XML node from Nokogiri::XML.
|
14
|
+
def initialize(xml_node)
|
15
|
+
@xml = xml_node
|
16
|
+
end
|
17
|
+
|
18
|
+
# List of supported tags. They can be attributes, simple descendans or
|
19
|
+
# collections.
|
20
|
+
def supported_tags
|
21
|
+
[
|
22
|
+
# made-up tags
|
23
|
+
:title,
|
24
|
+
|
25
|
+
# simple tags
|
26
|
+
:certainty, :description, :rawrequest, :rawresponse, :remedy, :severity,
|
27
|
+
:type, :url,
|
28
|
+
|
29
|
+
# tags that correspond to Evidence
|
30
|
+
|
31
|
+
# nested tags
|
32
|
+
:classification_capec, :classification_cwe, :classification_hipaa,
|
33
|
+
:classification_owasp2013, :classification_owasppc,
|
34
|
+
:classification_pci31, :classification_pci32, :classification_wasc,
|
35
|
+
|
36
|
+
# multiple tags
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
# This allows external callers (and specs) to check for implemented
|
41
|
+
# properties
|
42
|
+
def respond_to?(method, include_private=false)
|
43
|
+
return true if supported_tags.include?(method.to_sym)
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
# This method is invoked by Ruby when a method that is not defined in this
|
48
|
+
# instance is called.
|
49
|
+
#
|
50
|
+
# In our case we inspect the @method@ parameter and try to find the
|
51
|
+
# attribute, simple descendent or collection that it maps to in the XML
|
52
|
+
# tree.
|
53
|
+
def method_missing(method, *args)
|
54
|
+
|
55
|
+
# We could remove this check and return nil for any non-recognized tag.
|
56
|
+
# The problem would be that it would make tricky to debug problems with
|
57
|
+
# typos. For instance: <>.potr would return nil instead of raising an
|
58
|
+
# exception
|
59
|
+
unless supported_tags.include?(method)
|
60
|
+
super
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
# Any fields where a simple .camelcase() won't work we need to translate,
|
65
|
+
# this includes acronyms (e.g. :cwe would become 'Cwe') and simple nested
|
66
|
+
# tags.
|
67
|
+
translations_table = {
|
68
|
+
classification_capec: 'classification/CAPEC',
|
69
|
+
classification_cwe: 'classification/CWE',
|
70
|
+
classification_hipaa: 'classification/HIPAA',
|
71
|
+
classification_owasp2013: 'classification/OWASP2013',
|
72
|
+
classification_owasppc: 'classification/OWASPPC',
|
73
|
+
classification_pci31: 'classification/PCI31',
|
74
|
+
classification_pci32: 'classification/PCI32',
|
75
|
+
classification_wasc: 'classification/WASC'
|
76
|
+
}
|
77
|
+
method_name = translations_table.fetch(method, method.to_s)
|
78
|
+
|
79
|
+
# We've got a virtual method :title which isn't provided by Netsparker
|
80
|
+
# but that most users will be expecting.
|
81
|
+
return type.underscore.humanize if method == :title
|
82
|
+
|
83
|
+
# first we try the attributes:
|
84
|
+
# return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
|
85
|
+
|
86
|
+
# then we try the children tags
|
87
|
+
tag = xml.at_xpath("./#{method_name}")
|
88
|
+
if tag && !tag.text.blank?
|
89
|
+
text = tag.text
|
90
|
+
return tags_with_html_content.include?(method) ? cleanup_html(text) : text
|
91
|
+
else
|
92
|
+
return 'n/a'
|
93
|
+
end
|
94
|
+
|
95
|
+
# nothing found
|
96
|
+
return nil
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def cleanup_html(source)
|
102
|
+
result = source.dup
|
103
|
+
result.gsub!(/"/, '"')
|
104
|
+
result.gsub!(/&/, '&')
|
105
|
+
result.gsub!(/</, '<')
|
106
|
+
result.gsub!(/>/, '>')
|
107
|
+
|
108
|
+
result.gsub!(/<b>(.*?)<\/b>/) { "*#{$1.strip}*" }
|
109
|
+
result.gsub!(/<br\/>/, "\n")
|
110
|
+
result.gsub!(/<code><pre.*?>(.*?)<\/pre><\/code>/m){|m| "\n\nbc.. #{$1.strip}\n\np. \n" }
|
111
|
+
result.gsub!(/<div>(.*?)<\/div>/, '\1 ')
|
112
|
+
result.gsub!(/<em>(.*?)<\/em>/, '\1')
|
113
|
+
result.gsub!(/<font.*?>(.*?)<\/font>/m, '\1')
|
114
|
+
result.gsub!(/<h2>(.*?)<\/h2>/) { "*#{$1.strip}*" }
|
115
|
+
result.gsub!(/<i>(.*?)<\/i>/, '\1')
|
116
|
+
result.gsub!(/<li.*>(.*)?<\/li>/, '* \1')
|
117
|
+
result.gsub!(/<p>(.*?)<\/p>/, '\1 ')
|
118
|
+
result.gsub!(/<pre.*?>(.*?)<\/pre>/m){|m| "\n\nbc.. #{$1.strip}\n\np. \n" }
|
119
|
+
result.gsub!(/<span.*>(.*?)<\/span>/, '\1')
|
120
|
+
result.gsub!(/<ul>(.*?)<\/ul>/m, '\1')
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
# Some of the values have embedded HTML conent that we need to strip
|
126
|
+
def tags_with_html_content
|
127
|
+
[:description, :remedy]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class NetsparkerTasks < Thor
|
2
|
+
include Rails.application.config.dradis.thor_helper_module
|
3
|
+
|
4
|
+
namespace "dradis:plugins:netsparker"
|
5
|
+
|
6
|
+
desc "upload FILE", "upload Netsparker XML results"
|
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::Netsparker::Importer.new(task_options)
|
17
|
+
importer.import(file: file_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module Dradis::Plugins
|
5
|
+
describe 'Netsparker upload plugin' do
|
6
|
+
before(:each) do
|
7
|
+
# Stub template service
|
8
|
+
templates_dir = File.expand_path('../../templates', __FILE__)
|
9
|
+
expect_any_instance_of(TemplateService).to \
|
10
|
+
receive(:default_templates_dir).and_return(templates_dir)
|
11
|
+
|
12
|
+
plugin = Dradis::Plugins::Netsparker
|
13
|
+
|
14
|
+
@content_service = Dradis::Plugins::ContentService::Base.new(
|
15
|
+
logger: Logger.new(STDOUT),
|
16
|
+
plugin: plugin
|
17
|
+
)
|
18
|
+
|
19
|
+
@importer = Dradis::Plugins::Netsparker::Importer.new(
|
20
|
+
content_service: @content_service
|
21
|
+
)
|
22
|
+
|
23
|
+
# Stub dradis-plugins methods
|
24
|
+
#
|
25
|
+
# They return their argument hashes as objects mimicking
|
26
|
+
# Nodes, Issues, etc
|
27
|
+
allow(@content_service).to receive(:create_node) do |args|
|
28
|
+
OpenStruct.new(args)
|
29
|
+
end
|
30
|
+
allow(@content_service).to receive(:create_note) do |args|
|
31
|
+
OpenStruct.new(args)
|
32
|
+
end
|
33
|
+
allow(@content_service).to receive(:create_issue) do |args|
|
34
|
+
OpenStruct.new(args)
|
35
|
+
end
|
36
|
+
allow(@content_service).to receive(:create_evidence) do |args|
|
37
|
+
OpenStruct.new(args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "creates the expected Node, Issue, and Evidence from the example file" do
|
42
|
+
expect(@content_service).to receive(:create_node).with(hash_including label: 'localhost', type: :host)
|
43
|
+
|
44
|
+
expect(@content_service).to receive(:create_issue) do |args|
|
45
|
+
expect(args[:text]).to include("#[Title]#\nPassword over http")
|
46
|
+
expect(args[:id]).to eq("PasswordOverHttp")
|
47
|
+
OpenStruct.new(args)
|
48
|
+
end
|
49
|
+
|
50
|
+
expect(@content_service).to receive(:create_evidence) do |args|
|
51
|
+
expect(args[:content]).to include("#[Request]#\nbc.. GET /login HTTP/1.1")
|
52
|
+
expect(args[:issue].id).to eq("PasswordOverHttp")
|
53
|
+
expect(args[:node].label).to eq("localhost")
|
54
|
+
end
|
55
|
+
|
56
|
+
@importer.import(file: 'spec/fixtures/files/example.xml')
|
57
|
+
end
|
58
|
+
|
59
|
+
it "creates than one instance of Evidence for a single Issue" do
|
60
|
+
expect(@content_service).to receive(:create_node).with(hash_including label: 'snorby.org', type: :host)
|
61
|
+
|
62
|
+
expect(@content_service).to receive(:create_issue) do |args|
|
63
|
+
expect(args[:text]).to include("#[Title]#\nEmail disclosure")
|
64
|
+
expect(args[:id]).to eq("EmailDisclosure")
|
65
|
+
OpenStruct.new(args)
|
66
|
+
end
|
67
|
+
|
68
|
+
expect(@content_service).to receive(:create_evidence) do |args|
|
69
|
+
expect(args[:content]).to include("#[URL]#\nhttps://snorby.org/foo")
|
70
|
+
expect(args[:issue].id).to eq("EmailDisclosure")
|
71
|
+
expect(args[:node].label).to eq("snorby.org")
|
72
|
+
end.once
|
73
|
+
|
74
|
+
expect(@content_service).to receive(:create_evidence) do |args|
|
75
|
+
expect(args[:content]).to include("#[URL]#\nhttps://snorby.org/bar")
|
76
|
+
expect(args[:issue].id).to eq("EmailDisclosure")
|
77
|
+
expect(args[:node].label).to eq("snorby.org")
|
78
|
+
end.once
|
79
|
+
|
80
|
+
@importer.import(file: 'spec/fixtures/files/example-evidence.xml')
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
2
|
+
<?xml-stylesheet href="vulnerabilities-list.xsl" type="text/xsl" ?>
|
3
|
+
|
4
|
+
<netsparker generated="1.2.2017 12:34:45">
|
5
|
+
<target>
|
6
|
+
<url>https://snorby.org/</url>
|
7
|
+
<scantime>1470</scantime>
|
8
|
+
</target>
|
9
|
+
<vulnerability confirmed="False">
|
10
|
+
<url>https://snorby.org/foo</url>
|
11
|
+
<type>EmailDisclosure</type>
|
12
|
+
<severity>Information</severity>
|
13
|
+
<certainty>95</certainty>
|
14
|
+
|
15
|
+
<rawrequest><rawrequest><![CDATA[REDACTED}]]></rawrequest></rawrequest>
|
16
|
+
<rawresponse><rawrequest><![CDATA[REDACTED}]]></rawrequest></rawresponse>
|
17
|
+
<extrainformation>
|
18
|
+
</extrainformation>
|
19
|
+
|
20
|
+
<proofs></proofs>
|
21
|
+
|
22
|
+
|
23
|
+
<classification>
|
24
|
+
<OWASP2013></OWASP2013>
|
25
|
+
<WASC>13</WASC>
|
26
|
+
<CWE>200</CWE>
|
27
|
+
<CAPEC>118</CAPEC>
|
28
|
+
<PCI31></PCI31>
|
29
|
+
<PCI32></PCI32>
|
30
|
+
<HIPAA></HIPAA>
|
31
|
+
<OWASPPC>C7</OWASPPC>
|
32
|
+
|
33
|
+
<CVSS>
|
34
|
+
<vector>CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N</vector>
|
35
|
+
|
36
|
+
<score>
|
37
|
+
<type>Base</type>
|
38
|
+
<value>5.3</value>
|
39
|
+
<severity>Medium</severity>
|
40
|
+
</score>
|
41
|
+
<score>
|
42
|
+
<type>Temporal</type>
|
43
|
+
<value>5.3</value>
|
44
|
+
<severity>Medium</severity>
|
45
|
+
</score>
|
46
|
+
<score>
|
47
|
+
<type>Environmental</type>
|
48
|
+
<value>5.3</value>
|
49
|
+
<severity>Medium</severity>
|
50
|
+
</score>
|
51
|
+
|
52
|
+
</CVSS>
|
53
|
+
</classification>
|
54
|
+
|
55
|
+
</vulnerability>
|
56
|
+
<vulnerability confirmed="False">
|
57
|
+
<url>https://snorby.org/bar</url>
|
58
|
+
<type>EmailDisclosure</type>
|
59
|
+
<severity>Information</severity>
|
60
|
+
<certainty>95</certainty>
|
61
|
+
|
62
|
+
<rawrequest><rawrequest><![CDATA[REDACTED}]]></rawrequest></rawrequest>
|
63
|
+
<rawresponse><rawrequest><![CDATA[REDACTED}]]></rawrequest></rawresponse>
|
64
|
+
<extrainformation>
|
65
|
+
<info name="Email Address(es)"><![CDATA[info@snorby.org]]></info>
|
66
|
+
</extrainformation>
|
67
|
+
|
68
|
+
<proofs></proofs>
|
69
|
+
|
70
|
+
|
71
|
+
<classification>
|
72
|
+
<OWASP2013></OWASP2013>
|
73
|
+
<WASC>13</WASC>
|
74
|
+
<CWE>200</CWE>
|
75
|
+
<CAPEC>118</CAPEC>
|
76
|
+
<PCI31></PCI31>
|
77
|
+
<PCI32></PCI32>
|
78
|
+
<HIPAA></HIPAA>
|
79
|
+
<OWASPPC>C7</OWASPPC>
|
80
|
+
|
81
|
+
</classification>
|
82
|
+
|
83
|
+
</vulnerability>
|
84
|
+
|
85
|
+
</netsparker>
|