dradis-netsparker 3.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|