nessana 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +13 -0
- data/bin/nessana +5 -0
- data/bin/nessana_old +145 -0
- data/lib/nessana.rb +6 -0
- data/lib/nessana/detection.rb +45 -0
- data/lib/nessana/dump.rb +176 -0
- data/lib/nessana/executor.rb +100 -0
- data/lib/nessana/executor/execution_configuration.rb +88 -0
- data/lib/nessana/filter.rb +34 -0
- data/lib/nessana/version.rb +3 -0
- data/lib/nessana/vulnerability.rb +77 -0
- metadata +252 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 841b59ccd2b8e575f45f778d97a1b5c7b158c13fca2bf3bf554792ef25d931b7
|
4
|
+
data.tar.gz: 056feefeef224de161623a01593d0620def012e44346ab9d5cdae79174d41e29
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9b1d6e33a8fccc0a1b6685c5ee6f4d98ef24a1084bc7066138a851c700b886c422c79858cb80dc33eb45ae779ee50a2a09eb234e0eb683fd7ff78105addbaaed
|
7
|
+
data.tar.gz: 2012a1a887a6b291c1b82636504104808335f6901beb7deca7ed3acf8dc38b220a39bc251df8b49ba6ec8e5d50eb693dada3e4e99343c02ff5885b9544f9c2e7
|
data/README.md
ADDED
data/bin/nessana
ADDED
data/bin/nessana_old
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'asana'
|
4
|
+
require 'csv'
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
7
|
+
require 'pp'
|
8
|
+
require 'pry'
|
9
|
+
require 'ruby-prof'
|
10
|
+
require 'ruby-prof-flamegraph'
|
11
|
+
require 'yaml'
|
12
|
+
|
13
|
+
require 'nessana/vulnerability'
|
14
|
+
require 'nessana/vulnerability_list'
|
15
|
+
|
16
|
+
result = RubyProf.profile do
|
17
|
+
|
18
|
+
$logger = Logger.new(STDOUT)
|
19
|
+
$logger.level = Logger::DEBUG
|
20
|
+
|
21
|
+
$file_contents = open('secrets.yml', 'rb') do |io|
|
22
|
+
io.read
|
23
|
+
end
|
24
|
+
|
25
|
+
$asana_access_token = YAML.load($file_contents)['ASANA_PAT']
|
26
|
+
|
27
|
+
$client = Asana::Client.new do |c|
|
28
|
+
c.authentication :access_token, $asana_access_token
|
29
|
+
end
|
30
|
+
|
31
|
+
workspace = $client.workspaces.find_all.select do |workspace|
|
32
|
+
workspace.name == 'stolaf.org'
|
33
|
+
end.first
|
34
|
+
|
35
|
+
project = $client.projects.find_all(workspace: workspace.id).select do |project|
|
36
|
+
project.name == '[Sys] Security'
|
37
|
+
end.first
|
38
|
+
|
39
|
+
tag = $client.tags.find_all(workspace: workspace.id).select do |tag|
|
40
|
+
tag.name == 'Automated [Nessana]'
|
41
|
+
end.first
|
42
|
+
|
43
|
+
pp project.sections
|
44
|
+
|
45
|
+
tasks = $client.tasks.find_by_tag(tag: tag.id).map do |flat_task|
|
46
|
+
$logger.debug "Fetching task with id=#{flat_task.id}"
|
47
|
+
$client.tasks.find_by_id(flat_task.id).to_h
|
48
|
+
end.select do |task_hash|
|
49
|
+
!task_hash["completed"]
|
50
|
+
end
|
51
|
+
|
52
|
+
pp tasks
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
printer = RubyProf::FlameGraphPrinter.new(result)
|
57
|
+
printer.print(STDERR, {})
|
58
|
+
|
59
|
+
__END__
|
60
|
+
|
61
|
+
output = nil
|
62
|
+
|
63
|
+
result = RubyProf.profile do
|
64
|
+
vulnerabilities = VulnerabilityList.from_csv(ARGV[0])
|
65
|
+
|
66
|
+
$vulnerabilities = vulnerabilities.filter_risks.filter_not_accessible
|
67
|
+
|
68
|
+
# vuln_plugin_mapping = $vulnerabilities.each_with_object({}) do |vuln, hash|
|
69
|
+
# cve_string = vuln.cve ? " (#{vuln.cve})" : ""
|
70
|
+
# puts "Adding entry for #{vuln.plugin_id}#{cve_string} on host #{vuln.host}:#{vuln.port} (#{vuln.protocol})"
|
71
|
+
# hash[vuln.plugin_id] ||= []
|
72
|
+
# hash[vuln.plugin_id].push vuln
|
73
|
+
# end
|
74
|
+
|
75
|
+
vulns_by_plugin = $vulnerabilities.each_with_object({}) do |vuln, hash|
|
76
|
+
hash[vuln.plugin_id] ||= []
|
77
|
+
hash[vuln.plugin_id] << vuln
|
78
|
+
end
|
79
|
+
|
80
|
+
reports = vulns_by_plugin.map do |plugin_id, vulns|
|
81
|
+
uniqued_titles = vulns.map do |vuln|
|
82
|
+
vuln.name
|
83
|
+
end.uniq
|
84
|
+
|
85
|
+
uniqued_cves = vulns.map do |vuln|
|
86
|
+
vuln.cve
|
87
|
+
end.uniq
|
88
|
+
|
89
|
+
uniqued_cvsss = vulns.map do |vuln|
|
90
|
+
vuln.cvss
|
91
|
+
end.uniq
|
92
|
+
|
93
|
+
uniqued_risks = vulns.map do |vuln|
|
94
|
+
vuln.risk
|
95
|
+
end.uniq
|
96
|
+
|
97
|
+
throw "Plugin #{plugin_id} produced #{uniqued_titles.count} != 1 unique titles!" unless uniqued_titles.count == 1
|
98
|
+
throw "Plugin #{plugin_id} produced #{uniqued_cvsss.count} != 1 unique CVSS's!" unless uniqued_cvsss.count == 1
|
99
|
+
throw "Plugin #{plugin_id} produced #{uniqued_risks.count} != 1 unique risks!" unless uniqued_risks.count == 1
|
100
|
+
|
101
|
+
uniqued_hosts = vulns.map do |vuln|
|
102
|
+
vuln.readable_host
|
103
|
+
end.uniq
|
104
|
+
|
105
|
+
uniqued_synopses = vulns.map do |vuln|
|
106
|
+
vuln.synopsis
|
107
|
+
end.uniq
|
108
|
+
|
109
|
+
throw "More than one unique synopsis given?" unless uniqued_synopses.count == 1
|
110
|
+
|
111
|
+
uniqued_descriptions = vulns.map do |vuln|
|
112
|
+
vuln.description
|
113
|
+
end.uniq
|
114
|
+
|
115
|
+
throw "More than one unique description given?" unless uniqued_descriptions.count == 1
|
116
|
+
|
117
|
+
uniqued_solutions = vulns.map do |vuln|
|
118
|
+
vuln.solution
|
119
|
+
end.uniq
|
120
|
+
|
121
|
+
throw "More than one unique solution given?" unless uniqued_solutions.count == 1
|
122
|
+
|
123
|
+
{
|
124
|
+
cvss: uniqued_cvsss.first,
|
125
|
+
title: "[Nessus #{plugin_id}] #{uniqued_titles.join(', ')}",
|
126
|
+
body: "CVE: #{uniqued_cves.first || 'N/A'}\nCVSS: #{uniqued_cvsss.first || 'N/A'}\nRisk: #{uniqued_risks.first || 'N/A'}\n\nSYNOPSIS\n\n#{uniqued_synopses.first}\n\nDESCRIPTION\n\n#{uniqued_descriptions.first.join("\n\n")}\n\nSOLUTION\n\n#{uniqued_solutions.first.join("\n\n")}\n\nThis issue was detected on #{uniqued_hosts.count} hosts: #{uniqued_hosts.join(', ')}",
|
127
|
+
hosts: uniqued_hosts
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
output = reports.sort do |report_a, report_b|
|
132
|
+
report_b[:cvss] <=> report_a[:cvss]
|
133
|
+
end.map do |report|
|
134
|
+
[report[:title], report[:body]]
|
135
|
+
end.to_a
|
136
|
+
end
|
137
|
+
|
138
|
+
printer = RubyProf::GraphPrinter.new(result)
|
139
|
+
printer.print(STDOUT, {})
|
140
|
+
|
141
|
+
CSV.open(ARGV[1], 'wb') do |csv|
|
142
|
+
output.each do |row|
|
143
|
+
csv << row
|
144
|
+
end
|
145
|
+
end
|
data/lib/nessana.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Nessana
|
2
|
+
class Detection
|
3
|
+
attr_reader :host, :protocol, :port
|
4
|
+
attr_accessor :status
|
5
|
+
|
6
|
+
def initialize(host, protocol, port, status = nil)
|
7
|
+
@host = host
|
8
|
+
@protocol = protocol
|
9
|
+
@port = port
|
10
|
+
@status = status
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#{@status ? status_prefix : ''}#{@host}:#{@port}/#{@protocol}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def hash
|
18
|
+
"#{@host}:#{@port}/#{@protocol}".hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
@host == other.host &&
|
23
|
+
@protocol == other.protocol &&
|
24
|
+
@port == other.port &&
|
25
|
+
@status == other.status || false
|
26
|
+
end
|
27
|
+
|
28
|
+
alias eql? ==
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def status_prefix
|
33
|
+
case @status
|
34
|
+
when :added
|
35
|
+
'+ '
|
36
|
+
when :removed
|
37
|
+
'- '
|
38
|
+
when :present
|
39
|
+
' '
|
40
|
+
else
|
41
|
+
'? '
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/nessana/dump.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
require 'fastcsv'
|
5
|
+
require 'tty-spinner'
|
6
|
+
|
7
|
+
require 'nessana/detection'
|
8
|
+
require 'nessana/vulnerability'
|
9
|
+
|
10
|
+
module Nessana
|
11
|
+
class Dump < Hash
|
12
|
+
attr_reader :filters
|
13
|
+
attr_reader :filename
|
14
|
+
|
15
|
+
def initialize(filename = nil, filters = [])
|
16
|
+
@filename, @filters = filename, filters
|
17
|
+
|
18
|
+
if @filename
|
19
|
+
if File.readable?(@filename)
|
20
|
+
spinner_options = {
|
21
|
+
success_mark: "\u2713".encode('utf-8'),
|
22
|
+
format: :dots_3
|
23
|
+
}
|
24
|
+
spinner = TTY::Spinner.new("[:spinner] Loading #{@filename}...", **spinner_options)
|
25
|
+
spinner.auto_spin
|
26
|
+
|
27
|
+
read_csv!
|
28
|
+
|
29
|
+
spinner.success('done!')
|
30
|
+
else
|
31
|
+
throw 'file not readable; sad face'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def -(other)
|
37
|
+
spinner_options = {
|
38
|
+
success_mark: "\u2713".encode('utf-8'),
|
39
|
+
format: :dots_3
|
40
|
+
}
|
41
|
+
|
42
|
+
spinner = TTY::Spinner.new('[:spinner] :action...', **spinner_options)
|
43
|
+
spinner.update(action: 'Generating detections...')
|
44
|
+
|
45
|
+
other_plugin_ids = other.keys
|
46
|
+
self_plugin_ids = keys
|
47
|
+
|
48
|
+
spinner.update(action: 'Finding L detections')
|
49
|
+
|
50
|
+
other_detection_pairs = other.map do |plugin_id, vulnerability|
|
51
|
+
spinner.update(action: "Finding L detections (#{plugin_id})")
|
52
|
+
spinner.auto_spin
|
53
|
+
|
54
|
+
vulnerability.detections.map do |detection|
|
55
|
+
{ plugin_id => detection }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
other_detections = Set.new(other_detection_pairs.flatten)
|
60
|
+
|
61
|
+
spinner.update(action: 'Finding R detections')
|
62
|
+
|
63
|
+
detection_pairs = map do |plugin_id, vulnerability|
|
64
|
+
spinner.update(action: "Finding R detections (#{plugin_id})")
|
65
|
+
spinner.auto_spin
|
66
|
+
|
67
|
+
vulnerability.detections.map do |detection|
|
68
|
+
{ plugin_id => detection }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
self_detections = Set.new(detection_pairs.flatten)
|
73
|
+
|
74
|
+
spinner.update(action: 'Joining detection sets')
|
75
|
+
spinner.auto_spin
|
76
|
+
|
77
|
+
detections = Set.new([other_detections, self_detections]).flatten
|
78
|
+
|
79
|
+
spinner.update(action: 'Processing detections')
|
80
|
+
spinner.auto_spin
|
81
|
+
|
82
|
+
detections.each do |detection_entry|
|
83
|
+
in_self = self_detections.include? detection_entry
|
84
|
+
in_other = other_detections.include? detection_entry
|
85
|
+
|
86
|
+
detection = detection_entry.values.first
|
87
|
+
|
88
|
+
if in_self && in_other
|
89
|
+
detection.status = :present
|
90
|
+
elsif !in_self && in_other
|
91
|
+
detection.status = :removed
|
92
|
+
elsif in_self && !in_other
|
93
|
+
detection.status = :added
|
94
|
+
else
|
95
|
+
detection.status = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
spinner.success('done!')
|
100
|
+
|
101
|
+
added_plugin_ids = self_plugin_ids - other_plugin_ids
|
102
|
+
deleted_plugin_ids = other_plugin_ids - self_plugin_ids
|
103
|
+
all_plugin_ids = other_plugin_ids + added_plugin_ids
|
104
|
+
|
105
|
+
all_vulnerabilities = all_plugin_ids.map do |plugin_id|
|
106
|
+
vulnerability = nil
|
107
|
+
|
108
|
+
if !self[plugin_id]
|
109
|
+
vulnerability = other[plugin_id].clone
|
110
|
+
else
|
111
|
+
vulnerability = self[plugin_id].clone
|
112
|
+
end
|
113
|
+
|
114
|
+
plugin_detections = detections.select do |detection_entry|
|
115
|
+
detection_entry.keys.first == vulnerability.plugin_id
|
116
|
+
end.map do |detection_entry|
|
117
|
+
detection_entry.values.first
|
118
|
+
end
|
119
|
+
|
120
|
+
vulnerability.detections = plugin_detections.map do |detection|
|
121
|
+
detection.dup
|
122
|
+
end
|
123
|
+
|
124
|
+
vulnerability
|
125
|
+
end
|
126
|
+
|
127
|
+
all_vulnerabilities
|
128
|
+
end
|
129
|
+
|
130
|
+
def read_csv!
|
131
|
+
data = read_csv(filename)
|
132
|
+
|
133
|
+
filtered_data = data.select do |_, vulnerability|
|
134
|
+
!vulnerability.matches?(@filters)
|
135
|
+
end
|
136
|
+
|
137
|
+
merge!(filtered_data)
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
def read_csv(filename)
|
143
|
+
dump_data = {}
|
144
|
+
|
145
|
+
first_row = true
|
146
|
+
|
147
|
+
File.open(filename, 'rb') do |io|
|
148
|
+
io.advise(:willneed)
|
149
|
+
io.advise(:noreuse)
|
150
|
+
io.advise(:sequential)
|
151
|
+
|
152
|
+
FastCSV.raw_parse(io) do |row|
|
153
|
+
if first_row
|
154
|
+
first_row = false
|
155
|
+
next
|
156
|
+
end
|
157
|
+
|
158
|
+
row_nessus_data = row[0..3] + row[7..-1]
|
159
|
+
row_detection_data = row[4..6]
|
160
|
+
|
161
|
+
plugin_id = row[0]
|
162
|
+
|
163
|
+
unless dump_data[plugin_id]
|
164
|
+
dump_data[plugin_id] = Vulnerability.new(*row_nessus_data)
|
165
|
+
end
|
166
|
+
|
167
|
+
row_detection = Detection.new(*row_detection_data)
|
168
|
+
|
169
|
+
dump_data[plugin_id].add_detection(row_detection)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
dump_data
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'pp'
|
3
|
+
require 'ruby-prof'
|
4
|
+
require 'ruby-prof-flamegraph'
|
5
|
+
|
6
|
+
require 'nessana/executor/execution_configuration'
|
7
|
+
require 'nessana/filter'
|
8
|
+
require 'nessana/dump'
|
9
|
+
|
10
|
+
module Nessana
|
11
|
+
module Executor
|
12
|
+
def self.print_usage!
|
13
|
+
puts @parser
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.execute!(argv = ARGV)
|
17
|
+
parse!(*argv)
|
18
|
+
|
19
|
+
RubyProf.start if !!@configuration['performance']
|
20
|
+
|
21
|
+
unless @configuration['old_filename']
|
22
|
+
$stderr.puts 'No old dump filename given; will assume no prior knowledge.'
|
23
|
+
end
|
24
|
+
|
25
|
+
unless @configuration['new_filename']
|
26
|
+
puts 'No new dump filename given; cannot do anything.'
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
filters = @configuration['filters'].map do |filter_hash|
|
31
|
+
Filter.new(filter_hash)
|
32
|
+
end
|
33
|
+
|
34
|
+
old_dump = @configuration['old_filename'] ? Dump.new(@configuration['old_filename'], filters) : Dump.new
|
35
|
+
new_dump = Dump.new(@configuration['new_filename'], filters)
|
36
|
+
|
37
|
+
diff = new_dump - old_dump
|
38
|
+
|
39
|
+
diff.sort do |vulnerability_a, vulnerability_b|
|
40
|
+
vulnerability_a.plugin_id.to_i <=> vulnerability_b.plugin_id.to_i
|
41
|
+
end.sort do |vulnerability_a, vulnerability_b|
|
42
|
+
vulnerability_a.cvss.to_f <=> vulnerability_b.cvss.to_f
|
43
|
+
end.each do |v|
|
44
|
+
puts "#{v}
|
45
|
+
|
46
|
+
DISCOVERIES"
|
47
|
+
v.detections.sort_by(&:port).sort_by(&:host).each do |detection|
|
48
|
+
if detection.status && detection.status != true
|
49
|
+
puts detection.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
puts "\n" * 2
|
53
|
+
end
|
54
|
+
|
55
|
+
if !!@configuration['performance']
|
56
|
+
result = RubyProf.stop
|
57
|
+
printer = RubyProf::FlameGraphPrinter.new(result)
|
58
|
+
io = open(@configuration['performance'], 'wb')
|
59
|
+
printer.print(io, {})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.parse(*argv)
|
64
|
+
configuration = ExecutionConfiguration.new
|
65
|
+
|
66
|
+
option_parser = OptionParser.new do |parser|
|
67
|
+
configuration.add_parser_hooks(parser)
|
68
|
+
|
69
|
+
if argv.count == 0
|
70
|
+
puts parser
|
71
|
+
exit 1
|
72
|
+
end
|
73
|
+
|
74
|
+
parser.parse(*argv)
|
75
|
+
end
|
76
|
+
|
77
|
+
remaining_arguments = option_parser.order!(argv)
|
78
|
+
|
79
|
+
case remaining_arguments.count
|
80
|
+
when 2
|
81
|
+
configuration['old_filename'] = remaining_arguments[0]
|
82
|
+
configuration['new_filename'] = remaining_arguments[1]
|
83
|
+
when 1
|
84
|
+
configuration['old_filename'] = nil
|
85
|
+
configuration['new_filename'] = remaining_arguments[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
configuration.read_configuration_file!
|
89
|
+
|
90
|
+
configuration
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
def self.parse!(*argv)
|
96
|
+
@configuration = parse(*argv)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'mime-types'
|
2
|
+
|
3
|
+
require 'nessana/version'
|
4
|
+
|
5
|
+
module Nessana::Executor
|
6
|
+
class ExecutionConfiguration < ::Hash
|
7
|
+
def initialize
|
8
|
+
self['verbosity'] = 'info'
|
9
|
+
self['config'] = 'config.yml'
|
10
|
+
self['dump_filename'] = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# FIXME: too many lines
|
14
|
+
def add_parser_hooks(parser)
|
15
|
+
parser.banner = "Usage: #{$PROGRAM_NAME} [options] <filename.csv>"
|
16
|
+
|
17
|
+
parser.separator ''
|
18
|
+
parser.separator 'Execution Options'
|
19
|
+
|
20
|
+
add_config_option(parser)
|
21
|
+
|
22
|
+
parser.separator ''
|
23
|
+
parser.separator 'General Options'
|
24
|
+
|
25
|
+
add_usage_option(parser)
|
26
|
+
add_verbosity_option(parser)
|
27
|
+
|
28
|
+
parser.on_tail('-h', '--help', 'Show this message') do
|
29
|
+
puts parser
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
parser.on_tail('--version', 'Show version') do
|
34
|
+
puts Nessana::VERSION
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# TODO: deep merge?
|
40
|
+
def read_configuration_file!
|
41
|
+
merge!(read_configuration_file(self['config']))
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def add_config_option(parser)
|
47
|
+
parser.on('-c', '--config CONFIG', "Load configuration from CONFIG (default: #{self['config']})") do |config|
|
48
|
+
self['config'] = config
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_usage_option(parser)
|
53
|
+
parser.on('-h', '--help', 'Print usage summary.') do
|
54
|
+
puts parser
|
55
|
+
exit 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_verbosity_option(parser)
|
60
|
+
parser.on('-v', '--verbosity VERBOSITY', "The level of verbosity to use. (default: #{self['verbosity']})")
|
61
|
+
end
|
62
|
+
|
63
|
+
def infer_mime_type(filename)
|
64
|
+
MIME::Types.type_for(filename).first.content_type
|
65
|
+
end
|
66
|
+
|
67
|
+
# FIXME: too many lines
|
68
|
+
def read_configuration_file(filename)
|
69
|
+
raise ArgumentError, 'Must pass a valid filename' if filename.nil?
|
70
|
+
|
71
|
+
mime_type = infer_mime_type(filename)
|
72
|
+
parsed = nil
|
73
|
+
|
74
|
+
data = File.open(filename, 'rb', &:read)
|
75
|
+
|
76
|
+
case mime_type
|
77
|
+
when /ya?ml/
|
78
|
+
require 'yaml'
|
79
|
+
parsed = YAML.safe_load(data, [Regexp])
|
80
|
+
when /json/
|
81
|
+
require 'json'
|
82
|
+
parsed = JSON.parse(data)
|
83
|
+
end
|
84
|
+
|
85
|
+
parsed.to_h
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
require 'nessana/vulnerability'
|
4
|
+
|
5
|
+
module Nessana
|
6
|
+
class Filter < ::Hash
|
7
|
+
def initialize(hash)
|
8
|
+
super
|
9
|
+
|
10
|
+
fixed_hash = hash.map do |key, value|
|
11
|
+
[key.to_sym, value]
|
12
|
+
end.to_h
|
13
|
+
|
14
|
+
merge!(fixed_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def applies_to?(vulnerability)
|
18
|
+
each do |key, value|
|
19
|
+
method = key.to_sym
|
20
|
+
|
21
|
+
return false unless vulnerability.respond_to?(method)
|
22
|
+
|
23
|
+
case value
|
24
|
+
when Regexp
|
25
|
+
return true if vulnerability.send(method).to_s =~ value
|
26
|
+
else
|
27
|
+
return true if vulnerability.send(method).to_s == value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Nessana
|
2
|
+
class Vulnerability
|
3
|
+
attr_reader :plugin_id
|
4
|
+
attr_reader :cve, :cvss, :risk
|
5
|
+
attr_reader :name, :synopsis, :description, :solution
|
6
|
+
attr_reader :see_also, :plugin_output
|
7
|
+
attr_accessor :detections
|
8
|
+
|
9
|
+
def initialize(plugin_id, cve, cvss, risk, name, synopsis, description, solution, see_also, plugin_output)
|
10
|
+
@plugin_id = plugin_id
|
11
|
+
@cve = cve
|
12
|
+
@cvss = cvss
|
13
|
+
@risk = risk
|
14
|
+
@name = name
|
15
|
+
@synopsis = reformat_multiline(synopsis)
|
16
|
+
@description = reformat_multiline(description)
|
17
|
+
@solution = solution
|
18
|
+
@see_also = see_also
|
19
|
+
@plugin_output = plugin_output
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_detection(detection)
|
23
|
+
@detections ||= []
|
24
|
+
@detections.push(detection)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
[title_line, @synopsis, cve_s, cvss_s, description_s, solution_s, see_also_s].join("\n\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
def matches?(filters = [])
|
32
|
+
filters.each do |filter|
|
33
|
+
return true if filter.applies_to?(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def title_line
|
40
|
+
"[Nessus #{@plugin_id || "???"}] #{@name || "Unknown Vulnerability"} (#{@risk || "Unknown Risk"})"
|
41
|
+
end
|
42
|
+
|
43
|
+
def short_description
|
44
|
+
"#{title_line}
|
45
|
+
#{@synopsis}"
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def cve_s
|
51
|
+
@cve ? "CVE: " + @cve : "No CVE."
|
52
|
+
end
|
53
|
+
|
54
|
+
def cvss_s
|
55
|
+
@cvss ? "CVSS: " + @cvss : "No CVSS."
|
56
|
+
end
|
57
|
+
|
58
|
+
def description_s
|
59
|
+
"DESCRIPTION:\n#{@description}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def solution_s
|
63
|
+
"SOLUTION:\n#{@solution}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def see_also_s
|
67
|
+
"SEE ALSO:\n#{@see_also}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO Replace with something not O(2 n)? [n = line count]
|
71
|
+
def reformat_multiline(multiline)
|
72
|
+
multiline.split("\n").map do |line|
|
73
|
+
line.length == 0 ? "\n\n" : line
|
74
|
+
end.join(' ').gsub(/^ /,'').gsub(/ {3,}/,' ')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
metadata
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nessana
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kristofer Rye <kristofer.rye@gmail.com>
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: asana
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.6.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.6.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fastcsv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.0.6
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.0.6
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mime-types
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tty-spinner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.8.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.8.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: codecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.1.14
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.1.14
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: coveralls
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.8.22
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.8.22
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.14'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.14'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: guard-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.7'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.7'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.11.3
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.11.3
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rspec
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '3.7'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.7'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.57'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.57'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: ruby-prof
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 0.17.0
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: 0.17.0
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: ruby-prof-flamegraph
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 0.3.0
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 0.3.0
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: simplecov
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 0.16.1
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 0.16.1
|
209
|
+
description: 'A Nessus dump parser and differ which can create Asana tasks.
|
210
|
+
|
211
|
+
'
|
212
|
+
email:
|
213
|
+
executables:
|
214
|
+
- nessana
|
215
|
+
extensions: []
|
216
|
+
extra_rdoc_files: []
|
217
|
+
files:
|
218
|
+
- README.md
|
219
|
+
- bin/nessana
|
220
|
+
- bin/nessana_old
|
221
|
+
- lib/nessana.rb
|
222
|
+
- lib/nessana/detection.rb
|
223
|
+
- lib/nessana/dump.rb
|
224
|
+
- lib/nessana/executor.rb
|
225
|
+
- lib/nessana/executor/execution_configuration.rb
|
226
|
+
- lib/nessana/filter.rb
|
227
|
+
- lib/nessana/version.rb
|
228
|
+
- lib/nessana/vulnerability.rb
|
229
|
+
homepage: https://github.com/rye/nessana
|
230
|
+
licenses:
|
231
|
+
- MIT
|
232
|
+
metadata: {}
|
233
|
+
post_install_message:
|
234
|
+
rdoc_options: []
|
235
|
+
require_paths:
|
236
|
+
- lib
|
237
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
238
|
+
requirements:
|
239
|
+
- - "~>"
|
240
|
+
- !ruby/object:Gem::Version
|
241
|
+
version: '2.4'
|
242
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
243
|
+
requirements:
|
244
|
+
- - ">="
|
245
|
+
- !ruby/object:Gem::Version
|
246
|
+
version: '0'
|
247
|
+
requirements: []
|
248
|
+
rubygems_version: 3.0.2
|
249
|
+
signing_key:
|
250
|
+
specification_version: 4
|
251
|
+
summary: A Nessus dump parser and Asana task creator
|
252
|
+
test_files: []
|