aquatone 0.1.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 +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +168 -0
- data/Rakefile +10 -0
- data/aquatone.gemspec +29 -0
- data/aquatone.js +164 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/aquatone-discover +129 -0
- data/exe/aquatone-gather +55 -0
- data/exe/aquatone-scan +76 -0
- data/lib/aquatone.rb +43 -0
- data/lib/aquatone/assessment.rb +40 -0
- data/lib/aquatone/browser.rb +18 -0
- data/lib/aquatone/browser/drivers/nightmare.rb +52 -0
- data/lib/aquatone/collector.rb +106 -0
- data/lib/aquatone/collectors/dictionary.rb +20 -0
- data/lib/aquatone/collectors/dnsdb.rb +45 -0
- data/lib/aquatone/collectors/gtr.rb +58 -0
- data/lib/aquatone/collectors/hackertarget.rb +24 -0
- data/lib/aquatone/collectors/netcraft.rb +48 -0
- data/lib/aquatone/collectors/shodan.rb +45 -0
- data/lib/aquatone/collectors/threatcrowd.rb +25 -0
- data/lib/aquatone/collectors/virustotal.rb +24 -0
- data/lib/aquatone/command.rb +152 -0
- data/lib/aquatone/commands/discover.rb +187 -0
- data/lib/aquatone/commands/gather.rb +167 -0
- data/lib/aquatone/commands/scan.rb +108 -0
- data/lib/aquatone/domain.rb +33 -0
- data/lib/aquatone/http_client.rb +5 -0
- data/lib/aquatone/key_store.rb +72 -0
- data/lib/aquatone/port_lists.rb +36 -0
- data/lib/aquatone/report.rb +88 -0
- data/lib/aquatone/resolver.rb +47 -0
- data/lib/aquatone/thread_pool.rb +31 -0
- data/lib/aquatone/url_maker.rb +27 -0
- data/lib/aquatone/validation.rb +22 -0
- data/lib/aquatone/version.rb +3 -0
- data/subdomains.lst +8214 -0
- data/templates/default.html.erb +225 -0
- metadata +159 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "aquatone"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "aquatone"
|
4
|
+
|
5
|
+
options = {
|
6
|
+
:fallback_nameservers => %w(8.8.8.8 8.8.4.4),
|
7
|
+
:threads => 5,
|
8
|
+
:ignore_private => true
|
9
|
+
}
|
10
|
+
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: aquatone-discover OPTIONS"
|
13
|
+
|
14
|
+
opts.on("-d", "--domain DOMAIN", "Domain name to assess") do |v|
|
15
|
+
if !Aquatone::Validation.valid_domain_name?(v)
|
16
|
+
puts "#{v} doesn't look like a valid domain name."
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
options[:domain] = v
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("--nameservers NAMESERVERS", "Nameservers to use") do |v|
|
23
|
+
ips = v.split(",").map(&:strip).uniq
|
24
|
+
if ips.empty?
|
25
|
+
puts "Nameservers can't be empty."
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
ips.each do |ip|
|
29
|
+
if !Aquatone::Validation.valid_ip?(ip)
|
30
|
+
puts "#{ip} is not a valid IP address."
|
31
|
+
exit 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
options[:nameservers] = ips
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("--fallback-nameservers NAMESERVERS", "Nameservers to fall back to") do |v|
|
38
|
+
ips = v.split(",").map(&:strip).uniq
|
39
|
+
if ips.empty?
|
40
|
+
puts "Fallback nameservers can't be empty."
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
ips.each do |ip|
|
44
|
+
if !Aquatone::Validation.valid_ip?(ip)
|
45
|
+
puts "#{ip} is not a valid IP address."
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
options[:fallback_nameservers] = ips
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on("--[no-]ignore-private", "Ignore hosts resolving to private IP addresses") do |v|
|
53
|
+
options[:ignore_private] = v
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on("--set-key KEY VALUE", "Save a key to key store") do |key|
|
57
|
+
if ARGV.empty?
|
58
|
+
puts "No key value given."
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
key = key.to_s.downcase
|
62
|
+
value = ARGV.join(" ").strip
|
63
|
+
Aquatone::KeyStore.set(key, value)
|
64
|
+
puts "Saved key #{key} with value #{value}."
|
65
|
+
exit
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on("--list-collectors", "See information on collectors") do
|
69
|
+
Aquatone::Collector.descendants.each do |collector|
|
70
|
+
puts "Name.......: #{collector.meta[:name]}"
|
71
|
+
puts "Description: #{collector.meta[:description]}" if collector.meta[:description]
|
72
|
+
puts "Author.....: #{collector.meta[:author]}"
|
73
|
+
puts "Key........: #{collector.sluggified_name}\n\n"
|
74
|
+
puts "--------------------------------------------------\n\n"
|
75
|
+
end
|
76
|
+
exit
|
77
|
+
end
|
78
|
+
|
79
|
+
opts.on("--only-collectors COLLECTORS", "Only run specified collectors") do |v|
|
80
|
+
known_collectors = Aquatone::Collector.descendants.map(&:sluggified_name)
|
81
|
+
collectors = v.split(",").map(&:strip).uniq
|
82
|
+
collectors.each do |collector|
|
83
|
+
if !known_collectors.include?(collector)
|
84
|
+
puts "Unknown collector key: #{collector}"
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
options[:only_collectors] = collectors
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on("--disable-collectors COLLECTORS", "Disable specified collectors") do |v|
|
92
|
+
known_collectors = Aquatone::Collector.descendants.map(&:sluggified_name)
|
93
|
+
collectors = v.split(",").map(&:strip).uniq
|
94
|
+
collectors.each do |collector|
|
95
|
+
if !known_collectors.include?(collector)
|
96
|
+
puts "Unknown collector key: #{collector}"
|
97
|
+
exit 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
options[:disable_collectors] = collectors
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on("-t", "--threads THREADS", "Number of concurrent threads to use") do |v|
|
104
|
+
options[:threads] = v.to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
opts.on("-s", "--sleep SECONDS", "Seconds to sleep between lookups") do |v|
|
108
|
+
if v.to_i < 1
|
109
|
+
puts "Sleep can't be less than 1"
|
110
|
+
exit 1
|
111
|
+
end
|
112
|
+
options[:sleep] = v.to_i
|
113
|
+
end
|
114
|
+
|
115
|
+
opts.on("-j", "--jitter PERCENTAGE", "Jitter factor for sleep intervals") do |v|
|
116
|
+
if !v.to_i.between?(1, 100)
|
117
|
+
puts "Jitter factor must be between 1 and 100"
|
118
|
+
exit 1
|
119
|
+
end
|
120
|
+
options[:jitter] = v.to_f
|
121
|
+
end
|
122
|
+
|
123
|
+
opts.on("-h", "--help", "Show help") do
|
124
|
+
puts opts
|
125
|
+
exit 0
|
126
|
+
end
|
127
|
+
end.parse!
|
128
|
+
|
129
|
+
Aquatone::Commands::Discover.run(options)
|
data/exe/aquatone-gather
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "aquatone"
|
4
|
+
|
5
|
+
options = {
|
6
|
+
:threads => 5,
|
7
|
+
:timeout => 15
|
8
|
+
}
|
9
|
+
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: aquatone-gather OPTIONS"
|
12
|
+
|
13
|
+
opts.on("-d", "--domain DOMAIN", "Domain name to assess") do |v|
|
14
|
+
if !Aquatone::Validation.valid_domain_name?(v)
|
15
|
+
puts "#{v} doesn't look like a valid domain name."
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
options[:domain] = v
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-t", "--threads THREADS", "Number of concurrent threads to use") do |v|
|
22
|
+
options[:threads] = v.to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("--timeout SECONDS", "Timeout for page renderings") do |v|
|
26
|
+
if v.to_i < 1
|
27
|
+
puts "Timeout must be at least 1 second"
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
options[:timeout] = v.to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-s", "--sleep SECONDS", "Seconds to sleep between lookups") do |v|
|
34
|
+
if v.to_i < 1
|
35
|
+
puts "Sleep can't be less than 1"
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
options[:sleep] = v.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-j", "--jitter PERCENTAGE", "Jitter factor for sleep intervals") do |v|
|
42
|
+
if !v.to_i.between?(1, 100)
|
43
|
+
puts "Jitter factor must be between 1 and 100"
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
options[:jitter] = v.to_f
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("-h", "--help", "Show help") do
|
50
|
+
puts opts
|
51
|
+
exit 0
|
52
|
+
end
|
53
|
+
end.parse!
|
54
|
+
|
55
|
+
Aquatone::Commands::Gather.run(options)
|
data/exe/aquatone-scan
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "aquatone"
|
4
|
+
|
5
|
+
options = {
|
6
|
+
:ports => Aquatone::PortLists::MEDIUM,
|
7
|
+
:threads => 5,
|
8
|
+
:timeout => 0.5
|
9
|
+
}
|
10
|
+
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: aquatone-scan OPTIONS"
|
13
|
+
|
14
|
+
opts.on("-d", "--domain DOMAIN", "Domain name to assess") do |v|
|
15
|
+
if !Aquatone::Validation.valid_domain_name?(v)
|
16
|
+
puts "#{v} doesn't look like a valid domain name."
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
options[:domain] = v
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("-p", "--ports PORTS", "Ports to scan") do |v|
|
23
|
+
if v =~ /\A[\d,\s]{1,}\z/
|
24
|
+
ports = v.split(",").map { |p| p.strip.to_i }
|
25
|
+
ports.each do |p|
|
26
|
+
if !Aquatone::Validation.valid_tcp_port?(p)
|
27
|
+
puts "#{p} is not a valid TCP port."
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
options[:ports] = ports
|
32
|
+
else
|
33
|
+
begin
|
34
|
+
options[:ports] = Aquatone::PortLists.port_list_by_name(v.downcase)
|
35
|
+
rescue Aquatone::PortLists::UnknownPortListName => e
|
36
|
+
puts e.message
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("--timeout SECONDS", "Timeout in seconds for port probes") do |v|
|
43
|
+
if v.to_f <= 0
|
44
|
+
puts "Timeout can't be zero"
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
options[:timeout] = v.to_f
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-t", "--threads THREADS", "Number of concurrent threads to use") do |v|
|
51
|
+
options[:threads] = v.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-s", "--sleep SECONDS", "Seconds to sleep between port probes") do |v|
|
55
|
+
if v.to_i < 1
|
56
|
+
puts "Sleep can't be less than 1"
|
57
|
+
exit 1
|
58
|
+
end
|
59
|
+
options[:sleep] = v.to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on("-j", "--jitter PERCENTAGE", "Jitter factor for sleep intervals") do |v|
|
63
|
+
if !v.to_i.between?(1, 100)
|
64
|
+
puts "Jitter factor must be between 1 and 100"
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
options[:jitter] = v.to_f
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on("-h", "--help", "Show help") do
|
71
|
+
puts opts
|
72
|
+
exit 0
|
73
|
+
end
|
74
|
+
end.parse!
|
75
|
+
|
76
|
+
Aquatone::Commands::Scan.run(options)
|
data/lib/aquatone.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "resolv"
|
2
|
+
require "ipaddr"
|
3
|
+
require "socket"
|
4
|
+
require "timeout"
|
5
|
+
require "shellwords"
|
6
|
+
require "optparse"
|
7
|
+
|
8
|
+
require "httparty"
|
9
|
+
require "childprocess"
|
10
|
+
|
11
|
+
require "aquatone/version"
|
12
|
+
require "aquatone/port_lists"
|
13
|
+
require "aquatone/url_maker"
|
14
|
+
require "aquatone/validation"
|
15
|
+
require "aquatone/thread_pool"
|
16
|
+
require "aquatone/http_client"
|
17
|
+
require "aquatone/browser"
|
18
|
+
require "aquatone/browser/drivers/nightmare"
|
19
|
+
require "aquatone/domain"
|
20
|
+
require "aquatone/resolver"
|
21
|
+
require "aquatone/assessment"
|
22
|
+
require "aquatone/report"
|
23
|
+
require "aquatone/command"
|
24
|
+
require "aquatone/collector"
|
25
|
+
|
26
|
+
module Aquatone
|
27
|
+
AQUATONE_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")).freeze
|
28
|
+
DEFAULT_AQUATONE_PATH = File.join(Dir.home, "aquatone").freeze
|
29
|
+
|
30
|
+
def self.aquatone_path
|
31
|
+
ENV['AQUATONEPATH'] || DEFAULT_AQUATONE_PATH
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
require "aquatone/key_store"
|
36
|
+
|
37
|
+
Dir[File.join(File.dirname(__FILE__), "aquatone", "collectors", "*.rb")].each do |collector|
|
38
|
+
require collector
|
39
|
+
end
|
40
|
+
|
41
|
+
require "aquatone/commands/discover"
|
42
|
+
require "aquatone/commands/scan"
|
43
|
+
require "aquatone/commands/gather"
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Aquatone
|
2
|
+
class Assessment
|
3
|
+
attr_reader :domain
|
4
|
+
|
5
|
+
def initialize(domain)
|
6
|
+
@domain = domain
|
7
|
+
initialize_assessment_directory
|
8
|
+
end
|
9
|
+
|
10
|
+
def has_file?(name)
|
11
|
+
File.exist?(File.join(path, name))
|
12
|
+
end
|
13
|
+
|
14
|
+
def read_file(name)
|
15
|
+
File.read(File.join(path, name))
|
16
|
+
end
|
17
|
+
|
18
|
+
def write_file(name, data, mode = "w")
|
19
|
+
File.open(File.join(path, name), mode) do |file|
|
20
|
+
file.write(data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def make_directory(name)
|
25
|
+
dir = File.join(path, name)
|
26
|
+
Dir.mkdir(dir) unless Dir.exist?(dir)
|
27
|
+
end
|
28
|
+
|
29
|
+
def path
|
30
|
+
File.join(Aquatone.aquatone_path, domain)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def initialize_assessment_directory
|
36
|
+
Dir.mkdir(Aquatone.aquatone_path) unless Dir.exist?(Aquatone.aquatone_path)
|
37
|
+
Dir.mkdir(path) unless Dir.exist?(path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Aquatone
|
2
|
+
class Browser
|
3
|
+
def self.visit(url, vhost, html_destination, headers_destination, screenshot_destination, options)
|
4
|
+
driver = make_driver(url, vhost, html_destination, headers_destination, screenshot_destination, options)
|
5
|
+
visit = driver.visit
|
6
|
+
if !visit["success"]
|
7
|
+
visit = driver.visit
|
8
|
+
end
|
9
|
+
visit
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def self.make_driver(url, vhost, html_destination, headers_destination, screenshot_destination, options)
|
15
|
+
Aquatone::Browser::Drivers::Nightmare.new(url, vhost, html_destination, headers_destination, screenshot_destination, options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Aquatone
|
2
|
+
class Browser
|
3
|
+
module Drivers
|
4
|
+
class Nightmare
|
5
|
+
attr_reader :url, :vhost, :html_destination, :headers_destination, :screenshot_destination, :options
|
6
|
+
|
7
|
+
def initialize(url, vhost, html_destination, headers_destination, screenshot_destination, options)
|
8
|
+
@url = url
|
9
|
+
@vhost = vhost
|
10
|
+
@html_destination = html_destination
|
11
|
+
@headers_destination = headers_destination
|
12
|
+
@screenshot_destination = screenshot_destination
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit
|
17
|
+
rout, wout = IO.pipe
|
18
|
+
process = ChildProcess.build(*construct_command)
|
19
|
+
process.cwd = Aquatone::AQUATONE_ROOT
|
20
|
+
process.io.stdout = wout
|
21
|
+
process.start
|
22
|
+
process.poll_for_exit(options[:timeout])
|
23
|
+
wout.close
|
24
|
+
command_output = rout.readlines.join("\n").strip
|
25
|
+
JSON.parse(command_output)
|
26
|
+
rescue => e
|
27
|
+
process.stop if process
|
28
|
+
return {
|
29
|
+
"success" => false,
|
30
|
+
"error" => e.is_a?(ChildProcess::TimeoutError) ? "Timeout" : "#{e.class}: #{e.message}",
|
31
|
+
"code" => 0,
|
32
|
+
"details" => ""
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def construct_command
|
39
|
+
[
|
40
|
+
"node",
|
41
|
+
File.join(Aquatone::AQUATONE_ROOT, "aquatone.js"),
|
42
|
+
Shellwords.escape(url),
|
43
|
+
Shellwords.escape(vhost),
|
44
|
+
Shellwords.escape(html_destination),
|
45
|
+
Shellwords.escape(headers_destination),
|
46
|
+
Shellwords.escape(screenshot_destination)
|
47
|
+
]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|