aquatone 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -1
  3. data/README.md +37 -0
  4. data/exe/aquatone-discover +5 -4
  5. data/exe/aquatone-takeover +113 -0
  6. data/lib/aquatone.rb +6 -0
  7. data/lib/aquatone/commands/takeover.rb +147 -0
  8. data/lib/aquatone/detector.rb +94 -0
  9. data/lib/aquatone/detectors/campaign_monitor.rb +23 -0
  10. data/lib/aquatone/detectors/cargo.rb +25 -0
  11. data/lib/aquatone/detectors/cloudfront.rb +23 -0
  12. data/lib/aquatone/detectors/desk.rb +23 -0
  13. data/lib/aquatone/detectors/fastly.rb +23 -0
  14. data/lib/aquatone/detectors/feedpress.rb +23 -0
  15. data/lib/aquatone/detectors/freshdesk.rb +23 -0
  16. data/lib/aquatone/detectors/ghost.rb +29 -0
  17. data/lib/aquatone/detectors/github_pages.rb +25 -0
  18. data/lib/aquatone/detectors/helpjuice.rb +23 -0
  19. data/lib/aquatone/detectors/helpscout.rb +23 -0
  20. data/lib/aquatone/detectors/heroku.rb +25 -0
  21. data/lib/aquatone/detectors/instapage.rb +21 -0
  22. data/lib/aquatone/detectors/pingdom.rb +21 -0
  23. data/lib/aquatone/detectors/s3.rb +23 -0
  24. data/lib/aquatone/detectors/shopify.rb +25 -0
  25. data/lib/aquatone/detectors/statuspage.rb +23 -0
  26. data/lib/aquatone/detectors/surveygizmo.rb +25 -0
  27. data/lib/aquatone/detectors/teamwork.rb +23 -0
  28. data/lib/aquatone/detectors/tictail.rb +25 -0
  29. data/lib/aquatone/detectors/tumblr.rb +25 -0
  30. data/lib/aquatone/detectors/unbounce.rb +25 -0
  31. data/lib/aquatone/detectors/uservoice.rb +29 -0
  32. data/lib/aquatone/detectors/wpengine.rb +25 -0
  33. data/lib/aquatone/detectors/zendesk.rb +23 -0
  34. data/lib/aquatone/port_lists.rb +1 -1
  35. data/lib/aquatone/resolver.rb +34 -0
  36. data/lib/aquatone/version.rb +1 -1
  37. metadata +31 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1572ba4c07838fd7294dfc001c8ff23b2f7c19fc
4
- data.tar.gz: 2c7fc94f9cef66e71fc8adeed1840a3170cf6367
3
+ metadata.gz: 89f0cbe4f00ec77bae2d323be2978261680a75d8
4
+ data.tar.gz: 32948fb248e6614400c878ea2ac4681a5da9558b
5
5
  SHA512:
6
- metadata.gz: d5086248f07db25eeead317a570ee56cf3d622eb72e471d85c1c33611f3cfc4bba0be721a9f65658e6e078b264bbbb2327157574070e235d1542c4a85ba4ec6e
7
- data.tar.gz: f81b9ab7409889e524c59fda6e53e7ce4e891a515f7492885f5fcd5e558ae8bdb187c2e6295ad5ee40324f51b12994dfbc39759a272a03fbcbd08b75e5f90fc6
6
+ metadata.gz: 941e0e136a451eb295184a6b35dde232cbcae72624a4e421a5c304f89f70c281744cc85c05eafe951f210fd744ec6b3406362b554a4cab9a46b666e58c7be254
7
+ data.tar.gz: 48ccd44ff3e7fa4cfd05bd7221e6287479f9b80b362342915789ffe673c12a9f8a25fc8ef63524286eee401f4fccddf0fb8395810ca1e352aa6b81c0445325df
@@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
10
10
  ### Changed
11
11
 
12
12
 
13
+ ## [0.3.0]
14
+ ### Added
15
+ - New Tool: aquatone-takeover: Check discovered hosts for subdomain takeover vulnerabilities
16
+
17
+ ### Changed
18
+ - Show key requirements in for collectors when issuing `aquatone-discover --list-collectors`
19
+ - Add alias `xlarge` to `huge` port list.
20
+
21
+
13
22
  ## [0.2.0]
14
23
  ### Added
15
24
  - New Collector: riddler.io (Thanks, [@jolle](https://github.com/jolle)!)
@@ -38,6 +47,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
38
47
 
39
48
  ### Changed
40
49
 
41
- [Unreleased]: https://github.com/michenriksen/aquatone/compare/v0.2.0...HEAD
50
+ [Unreleased]: https://github.com/michenriksen/aquatone/compare/v0.3.0...HEAD
51
+ [0.3.0]: https://github.com/michenriksen/aquatone/compare/v0.2.0...v0.3.0
42
52
  [0.2.0]: https://github.com/michenriksen/aquatone/compare/v0.1.1...v0.2.0
43
53
  [0.1.1]: https://github.com/michenriksen/aquatone/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -152,6 +152,43 @@ When aquatone-gather is finished, it will have created several directories in th
152
152
  * `screenshots/`: Contains PNG images of how each web page looks like in a browser
153
153
  * `report/` Contains report files in HTML displaying the gathered information for easy analysis
154
154
 
155
+ ### Subdomain Takeover
156
+
157
+ Subdomain takeover is a very prevalent and potentially critical security issue which commonly occurs when an organization assigns a subdomain to a third-party service provider and then later discontinues use, but forgets to remove the DNS configuration. This leaves the subdomain vulnerable to complete takover by attackers by signing up to the same service provider and claiming the dangling subdomain.
158
+
159
+ aquatone-takeover can be used to check hosts uncovered by aquatone-discover for potential domain takeover vulnerabilities:
160
+
161
+ $ aquatone-takeover --domain example.com
162
+
163
+ aquatone-takeover can detect potential subdomain takeover situations from 25 different service providers, including GitHub Pages, Heroku, Amazon S3, Desk and WPEngine.
164
+
165
+ #### Results
166
+
167
+ aquatone-takeover will create a `takeovers.json` file in the domain's assessment directory which will contain information in JSON format about any potential subdomain takeover vulnerabilities:
168
+
169
+ ```
170
+ {
171
+ "shop.example.com": {
172
+ "service": "Shopify",
173
+ "service_website": "https://www.shopify.com/",
174
+ "description": "Ecommerce platform",
175
+ "resource": {
176
+ "type": "CNAME",
177
+ "value": "shops.myshopify.com"
178
+ }
179
+ },
180
+ "help.example.com": {
181
+ "service": "Desk",
182
+ "service_website": "https://www.desk.com/",
183
+ "description": "Customer service and helpdesk ticket software",
184
+ "resource": {
185
+ "type": "CNAME",
186
+ "value": "example.desk.com"
187
+ }
188
+ },
189
+ ...
190
+ }
191
+ ```
155
192
 
156
193
  ## Contributing
157
194
 
@@ -67,10 +67,11 @@ OptionParser.new do |opts|
67
67
 
68
68
  opts.on("--list-collectors", "See information on collectors") do
69
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"
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 Requirements: #{collector.meta.key?(:require_keys) ? collector.meta[:require_keys].join(', ') : 'None'}"
74
+ puts "Key.............: #{collector.sluggified_name}\n\n"
74
75
  puts "--------------------------------------------------\n\n"
75
76
  end
76
77
  exit
@@ -0,0 +1,113 @@
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
+ }
9
+
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: aquatone-takeover 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("--nameservers NAMESERVERS", "Nameservers to use") do |v|
22
+ ips = v.split(",").map(&:strip).uniq
23
+ if ips.empty?
24
+ puts "Nameservers can't be empty."
25
+ exit 1
26
+ end
27
+ ips.each do |ip|
28
+ if !Aquatone::Validation.valid_ip?(ip)
29
+ puts "#{ip} is not a valid IP address."
30
+ exit 1
31
+ end
32
+ end
33
+ options[:nameservers] = ips
34
+ end
35
+
36
+ opts.on("--fallback-nameservers NAMESERVERS", "Nameservers to fall back to") do |v|
37
+ ips = v.split(",").map(&:strip).uniq
38
+ if ips.empty?
39
+ puts "Fallback nameservers can't be empty."
40
+ exit 1
41
+ end
42
+ ips.each do |ip|
43
+ if !Aquatone::Validation.valid_ip?(ip)
44
+ puts "#{ip} is not a valid IP address."
45
+ exit 1
46
+ end
47
+ end
48
+ options[:fallback_nameservers] = ips
49
+ end
50
+
51
+ opts.on("--list-detectors", "See information on subdomain takeover detectors") do
52
+ Aquatone::Detector.descendants.each do |detector|
53
+ puts "Service........: #{detector.meta[:service]}"
54
+ puts "Service Website: #{detector.meta[:service_website]}"
55
+ puts "Description....: #{detector.meta[:description]}" if detector.meta[:description]
56
+ puts "Author.........: #{detector.meta[:author]}"
57
+ puts "Key............: #{detector.sluggified_name}\n\n"
58
+ puts "--------------------------------------------------\n\n"
59
+ end
60
+ exit
61
+ end
62
+
63
+ opts.on("--only-detectors DETECTORS", "Only run specified takeover detectors") do |v|
64
+ known_detectors = Aquatone::Detector.descendants.map(&:sluggified_name)
65
+ detectors = v.split(",").map(&:strip).uniq
66
+ detectors.each do |detector|
67
+ if !known_detectors.include?(detector)
68
+ puts "Unknown takeover detector key: #{detector}"
69
+ exit 1
70
+ end
71
+ end
72
+ options[:only_detectors] = detectors
73
+ end
74
+
75
+ opts.on("--disable-detectors DETECTORS", "Disable specified takeover detectors") do |v|
76
+ known_detectors = Aquatone::Detector.descendants.map(&:sluggified_name)
77
+ detectors = v.split(",").map(&:strip).uniq
78
+ detectors.each do |detector|
79
+ if !known_detectors.include?(detector)
80
+ puts "Unknown detector key: #{detector}"
81
+ exit 1
82
+ end
83
+ end
84
+ options[:disable_detectors] = detectors
85
+ end
86
+
87
+ opts.on("-t", "--threads THREADS", "Number of concurrent threads to use") do |v|
88
+ options[:threads] = v.to_i
89
+ end
90
+
91
+ opts.on("-s", "--sleep SECONDS", "Seconds to sleep between checks") do |v|
92
+ if v.to_i < 1
93
+ puts "Sleep can't be less than 1"
94
+ exit 1
95
+ end
96
+ options[:sleep] = v.to_i
97
+ end
98
+
99
+ opts.on("-j", "--jitter PERCENTAGE", "Jitter factor for sleep intervals") do |v|
100
+ if !v.to_i.between?(1, 100)
101
+ puts "Jitter factor must be between 1 and 100"
102
+ exit 1
103
+ end
104
+ options[:jitter] = v.to_f
105
+ end
106
+
107
+ opts.on("-h", "--help", "Show help") do
108
+ puts opts
109
+ exit 0
110
+ end
111
+ end.parse!
112
+
113
+ Aquatone::Commands::Takeover.run(options)
@@ -22,6 +22,7 @@ require "aquatone/assessment"
22
22
  require "aquatone/report"
23
23
  require "aquatone/command"
24
24
  require "aquatone/collector"
25
+ require "aquatone/detector"
25
26
 
26
27
  module Aquatone
27
28
  AQUATONE_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")).freeze
@@ -38,6 +39,11 @@ Dir[File.join(File.dirname(__FILE__), "aquatone", "collectors", "*.rb")].each do
38
39
  require collector
39
40
  end
40
41
 
42
+ Dir[File.join(File.dirname(__FILE__), "aquatone", "detectors", "*.rb")].each do |detector|
43
+ require detector
44
+ end
45
+
41
46
  require "aquatone/commands/discover"
42
47
  require "aquatone/commands/scan"
43
48
  require "aquatone/commands/gather"
49
+ require "aquatone/commands/takeover"
@@ -0,0 +1,147 @@
1
+ module Aquatone
2
+ module Commands
3
+ class Takeover < Aquatone::Command
4
+ def execute!
5
+ if !options[:domain]
6
+ output("Please specify a domain to assess\n")
7
+ exit 1
8
+ end
9
+
10
+ @domain = Aquatone::Domain.new(options[:domain])
11
+ @assessment = Aquatone::Assessment.new(options[:domain])
12
+
13
+ banner("Takeover")
14
+ prepare_host_dictionary
15
+ prepare_detectors
16
+ setup_resolver
17
+ check_hosts
18
+ write_to_takeovers_file
19
+ end
20
+
21
+ private
22
+
23
+ def prepare_host_dictionary
24
+ if !@assessment.has_file?("hosts.json")
25
+ output(red("#{@assessment.path} does not contain hosts.json file\n\n"))
26
+ output("Did you run aquatone-discover first?\n")
27
+ exit 1
28
+ end
29
+ hosts = JSON.parse(@assessment.read_file("hosts.json"))
30
+ @hosts = hosts.keys
31
+ output("Loaded #{bold(@hosts.count)} hosts from #{bold(File.join(@assessment.path, 'hosts.json'))}\n")
32
+ end
33
+
34
+ def prepare_detectors
35
+ @detectors = Aquatone::Detector.descendants
36
+ output("Loaded #{bold(@detectors.count)} domain takeover detectors\n\n")
37
+ end
38
+
39
+ def setup_resolver
40
+ if options[:nameservers]
41
+ nameservers = options[:nameservers]
42
+ else
43
+ output("Identifying nameservers for #{@domain.name}... ")
44
+ nameservers = @domain.nameservers
45
+ output("Done\n")
46
+ if nameservers.empty?
47
+ output(yellow("#{@domain.name} has no nameservers. Using fallback nameservers.\n\n"))
48
+ nameservers = []
49
+ end
50
+ end
51
+
52
+ if !nameservers.empty?
53
+ output("Using nameservers:\n\n")
54
+ nameservers.each do |ns|
55
+ output(" - #{ns}\n")
56
+ end
57
+ output("\n")
58
+ end
59
+ @resolver = Aquatone::Resolver.new(
60
+ :nameservers => nameservers,
61
+ :fallback_nameservers => options[:fallback_nameservers]
62
+ )
63
+ end
64
+
65
+ def check_hosts
66
+ pool = thread_pool
67
+ @task_count = 1
68
+ @takeovers = {}
69
+ @takeovers_detected = 0
70
+ @start_time = Time.now.to_i
71
+ output("Checking hosts for domain takeover vulnerabilities...\n\n")
72
+ @hosts.each do |host|
73
+ resource = @resolver.resource(host)
74
+ next unless valid_resource?(resource)
75
+ pool.schedule do
76
+ output_progress if asked_for_progress?
77
+ @task_count += 1
78
+ @detectors.each do |detector|
79
+ next if skip_detector?(detector)
80
+ detector_instance = detector.new(host, resource)
81
+ if detector_instance.positive?
82
+ resource_type = resource.class.to_s.split("::").last
83
+ resource_value = resource.is_a?(Resolv::DNS::Resource::IN::CNAME) ? resource.name.to_s : resource.address.to_s
84
+ output(red("Potential domain takeover detected!\n"))
85
+ output("#{bold('Host...........:')} #{host}\n")
86
+ output("#{bold('Service........:')} #{detector.meta[:service]}\n")
87
+ output("#{bold('Service website:')} #{detector.meta[:service_website]}\n")
88
+ output("#{bold('Resource.......:')} #{resource_type} #{resource_value}\n")
89
+ output("\n")
90
+ @takeovers[host] = {
91
+ "service" => detector.meta[:service],
92
+ "service_website" => detector.meta[:service_website],
93
+ "description" => detector.meta[:description],
94
+ "resource" => {
95
+ "type" => resource_type,
96
+ "value" => resource_value
97
+ }
98
+ }
99
+ @takeovers_detected += 1
100
+ break
101
+ end
102
+ end
103
+ end
104
+ jitter_sleep
105
+ end
106
+ pool.shutdown
107
+ output("Finished checking hosts:\n\n")
108
+ output(" - Vulnerable : #{bold(red(@takeovers_detected))}\n")
109
+ output(" - Not Vulnerable : #{bold(green(@hosts.count - @takeovers_detected))}\n\n")
110
+ end
111
+
112
+ def write_to_takeovers_file
113
+ @assessment.write_file("takeovers.json", @takeovers.to_json)
114
+ output("Wrote #{bold(@takeovers.keys.count)} potential subdomain takeovers to:\n\n")
115
+ output(" - #{bold('file://' + File.join(@assessment.path, 'takeovers.json'))}\n\n")
116
+ end
117
+
118
+ def output_progress
119
+ output("Stats: #{seconds_to_time(Time.now.to_i - @start_time)} elapsed; " \
120
+ "#{@task_count} out of #{@hosts.count} hosts checked (#{@takeovers_detected} takeovers detected); " \
121
+ "#{(@task_count.to_f / @hosts.count.to_f * 100.00).round(1)}% done\n")
122
+ end
123
+
124
+ def valid_resource?(resource)
125
+ [Resolv::DNS::Resource::IN::CNAME, Resolv::DNS::Resource::IN::A].include?(resource.class)
126
+ end
127
+
128
+ def skip_detector?(detector)
129
+ if options[:only_detectors]
130
+ if options[:only_detectors].include?(detector.sluggified_name)
131
+ false
132
+ else
133
+ true
134
+ end
135
+ elsif options[:disable_detectors]
136
+ if options[:disable_detectors].include?(detector.sluggified_name)
137
+ true
138
+ else
139
+ false
140
+ end
141
+ else
142
+ false
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,94 @@
1
+ module Aquatone
2
+ class Detector
3
+ class Error < StandardError; end
4
+ class InvalidMetadataError < Error; end
5
+ class MetadataNotSetError < Error; end
6
+
7
+ attr_reader :host, :resource
8
+
9
+ def self.meta
10
+ @meta || fail(MetadataNotSetError, "Metadata has not been set")
11
+ end
12
+
13
+ def self.meta=(meta)
14
+ validate_metadata(meta)
15
+ @meta = meta
16
+ end
17
+
18
+ def self.descendants
19
+ detectors = ObjectSpace.each_object(Class).select { |klass| klass < self }
20
+ detectors.sort { |x, y| x.meta[:service] <=> y.meta[:service] }
21
+ end
22
+
23
+ def self.sluggified_name
24
+ return meta[:slug].downcase if meta[:slug]
25
+ meta[:service].strip.downcase.gsub(/[^a-z0-9]+/, '-').gsub("--", "-")
26
+ end
27
+
28
+ def initialize(host, resource)
29
+ @host = host
30
+ @resource = resource
31
+ end
32
+
33
+ def run
34
+ fail NotImplementedError
35
+ end
36
+
37
+ def positive?
38
+ run
39
+ rescue
40
+ false
41
+ end
42
+
43
+ protected
44
+
45
+ def cname_resource?
46
+ resource.is_a?(Resolv::DNS::Resource::IN::CNAME)
47
+ end
48
+
49
+ def apex_resource?
50
+ resource.is_a?(Resolv::DNS::Resource::IN::A)
51
+ end
52
+
53
+ def resource_value
54
+ cname_resource? ? resource.name.to_s : resource.address.to_s
55
+ end
56
+
57
+ def get_request(uri, options={})
58
+ options = {
59
+ :timeout => 10
60
+ }.merge(options)
61
+ Aquatone::HttpClient.get(uri, options)
62
+ end
63
+
64
+ def post_request(uri, body=nil, options={})
65
+ options = {
66
+ :body => body,
67
+ :timeout => 10
68
+ }.merge(options)
69
+ Aquatone::HttpClient.post(uri, options)
70
+ end
71
+
72
+ def url_escape(string)
73
+ CGI.escape(string)
74
+ end
75
+
76
+ def random_sleep(seconds)
77
+ random_sleep = ((1 - (rand(30) * 0.01)) * seconds.to_i)
78
+ sleep(random_sleep)
79
+ end
80
+
81
+ def failure(message)
82
+ fail Error, message
83
+ end
84
+
85
+ def self.validate_metadata(meta)
86
+ fail InvalidMetadataError, "Metadata is not a hash" unless meta.is_a?(Hash)
87
+ fail InvalidMetadataError, "Metadata is empty" if meta.empty?
88
+ fail InvalidMetadataError, "Metadata is missing key: service" unless meta.key?(:service)
89
+ fail InvalidMetadataError, "Metadata is missing key: service_website" unless meta.key?(:service_website)
90
+ fail InvalidMetadataError, "Metadata is missing key: author" unless meta.key?(:author)
91
+ fail InvalidMetadataError, "Metadata is missing key: description" unless meta.key?(:description)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class CampaignMonitor < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Campaign Monitor",
6
+ :service_website => "https://www.zendesk.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Email marketing"
9
+ }
10
+
11
+ CNAME_VALUE = "name.createsend.com".freeze
12
+ RESPONSE_FINGERPRINT = "<strong>Trying to access your account?</strong>".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value == CNAME_VALUE
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Cargo < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Cargo",
6
+ :service_website => "https://cargocollective.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Web publishing platform"
9
+ }
10
+
11
+ APEX_VALUE = "173.203.204.123".freeze
12
+ CNAME_VALUE = "cargocollective.com".freeze
13
+ RESPONSE_FINGERPRINT = "Use a personal domain name".freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless resource_value == APEX_VALUE
18
+ elsif cname_resource?
19
+ return false unless resource_value == CNAME_VALUE
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Cloudfront < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Cloudfront",
6
+ :service_website => "https://aws.amazon.com/cloudfront/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Content delivery network"
9
+ }
10
+
11
+ CNAME_VALUE = ".cloudfront.net".freeze
12
+ RESPONSE_FINGERPRINT = "The request could not be satisfied".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Desk < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Desk",
6
+ :service_website => "https://www.desk.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Customer service and helpdesk ticket software"
9
+ }
10
+
11
+ CNAME_VALUE = ".desk.com".freeze
12
+ RESPONSE_FINGERPRINT = "Sorry, We Couldn't Find That Page".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Fastly < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Fastly",
6
+ :service_website => "https://www.fastly.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Content delivery network"
9
+ }
10
+
11
+ CNAME_VALUE = ".fastly.net".freeze
12
+ RESPONSE_FINGERPRINT = "Fastly error: unknown domain".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Feedpress < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "FeedPress",
6
+ :service_website => "https://feed.press/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Feed analytics and Podcast hosting"
9
+ }
10
+
11
+ CNAME_VALUE = "redirect.feedpress.me".freeze
12
+ RESPONSE_FINGERPRINT = "The feed has not been found.".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value == CNAME_VALUE
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Freshdesk < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Freshdesk",
6
+ :service_website => "https://freshdesk.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Customer support software and ticketing system"
9
+ }
10
+
11
+ CNAME_VALUE = ".freshdesk.com".freeze
12
+ RESPONSE_FINGERPRINT = "You can claim it now at".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Ghost < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Ghost",
6
+ :service_website => "https://ghost.org/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Publishing platform"
9
+ }
10
+
11
+ CNAME_VALUE = ".ghost.io".freeze
12
+ RESPONSE_FINGERPRINT = "The thing you were looking for is no longer here, or never was".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ response = get_request("http://#{host}/",
18
+ # Set a proper User-Agent to avoid potential CloudFlare CAPTCHA wall
19
+ :headers => {
20
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
21
+ }
22
+ )
23
+ return response.body.include?(RESPONSE_FINGERPRINT)
24
+ end
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class GithubPages < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "GitHub Pages",
6
+ :service_website => "https://pages.github.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "GitHub static website hosting"
9
+ }
10
+
11
+ APEX_VALUES = %w(192.30.252.153 192.30.252.154).freeze
12
+ CNAME_VALUE = ".github.io".freeze
13
+ RESPONSE_FINGERPRINT = "There isn't a GitHub Pages site here.".freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless APEX_VALUES.include?(resource_value)
18
+ elsif cname_resource?
19
+ return false unless resource_value.end_with?(CNAME_VALUE)
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Helpjuice < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Helpjuice",
6
+ :service_website => "https://helpjuice.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Knowledge base software"
9
+ }
10
+
11
+ CNAME_VALUE = ".helpjuice.com".freeze
12
+ RESPONSE_FINGERPRINT = "<title>No such app</title>".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Helpscout < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Help Scout",
6
+ :service_website => "https://www.helpscout.net/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Customer service software and education platform"
9
+ }
10
+
11
+ CNAME_VALUE = ".helpscoutdocs.com".freeze
12
+ RESPONSE_FINGERPRINT = "No settings were found for this company".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Heroku < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Heroku",
6
+ :service_website => "https://www.heroku.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Cloud application platform"
9
+ }
10
+
11
+ CNAME_VALUES = %w(.herokudns.com .herokussl.com .herokuapp.com).freeze
12
+ RESPONSE_FINGERPRINT = "<title>No such app</title>".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ CNAME_VALUES.each do |cname_value|
17
+ if resource_value.end_with?(cname_value)
18
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
19
+ end
20
+ end
21
+ false
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Instapage < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Instapage",
6
+ :service_website => "https://instapage.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Landing page platform"
9
+ }
10
+
11
+ CNAME_VALUES = %w(pageserve.co secure.pageserve.co).freeze
12
+ RESPONSE_FINGERPRINT = "You've Discovered A Missing Link. Our Apologies!".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ return false unless CNAME_VALUES.include?(resource_value)
17
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Pingdom < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Pingdom",
6
+ :service_website => "https://www.pingdom.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Website and performance monitoring"
9
+ }
10
+
11
+ CNAME_VALUE = "stats.pingdom.com".freeze
12
+ RESPONSE_FINGERPRINT = "Sorry, couldn&rsquo;t find the status page".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ return false unless resource_value == CNAME_VALUE
17
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class S3 < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Amazon S3",
6
+ :service_website => "https://aws.amazon.com/s3/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Cloud storage"
9
+ }
10
+
11
+ CNAME_VALUE = ".amazonaws.com".freeze
12
+ RESPONSE_FINGERPRINT = "NoSuchBucket".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Shopify < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Shopify",
6
+ :service_website => "https://www.shopify.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Ecommerce platform"
9
+ }
10
+
11
+ APEX_VALUE = "23.227.38.32".freeze
12
+ CNAME_VALUE = "shops.myshopify.com".freeze
13
+ RESPONSE_FINGERPRINT = "Sorry, this shop is currently unavailable.".freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless resource_value == APEX_VALUE
18
+ elsif cname_resource?
19
+ return false unless resource_value == CNAME_VALUE
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Statuspage < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "StatusPage",
6
+ :service_website => "https://www.statuspage.io/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Status page hosting"
9
+ }
10
+
11
+ CNAME_VALUE = ".stspg-customer.com".freeze
12
+ RESPONSE_FINGERPRINT = "<title>Hosted Status Pages for Your Company</title>".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Surveygizmo < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "SurveyGizmo",
6
+ :service_website => "https://www.surveygizmo.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Online survey software"
9
+ }
10
+
11
+ CNAME_VALUES = %w(privatedomain.sgizmo.com privatedomain.surveygizmo.eu privatedomain.sgizmoca.com).freeze
12
+ RESPONSE_FINGERPRINT = 'data-html-name="Header Logo Link"'.freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ CNAME_VALUES.each do |cname_value|
17
+ if resource_value == cname_value
18
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
19
+ end
20
+ end
21
+ false
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Teamwork < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Teamwork",
6
+ :service_website => "https://www.teamwork.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Project management, help desk and chat software"
9
+ }
10
+
11
+ CNAME_VALUE = ".teamwork.com".freeze
12
+ RESPONSE_FINGERPRINT = "<title>Oops - We didn't find your site.</title>".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Tictail < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Tictail",
6
+ :service_website => "https://tictail.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Social shopping platform"
9
+ }
10
+
11
+ APEX_VALUE = "46.137.181.142".freeze
12
+ CNAME_VALUE = "domains.tictail.com".freeze
13
+ RESPONSE_FINGERPRINT = 'class="MarketplaceHeader__tictailLogo"'.freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless resource_value == APEX_VALUE
18
+ elsif cname_resource?
19
+ return false unless resource_value == CNAME_VALUE
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Tumblr < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Tumblr",
6
+ :service_website => "https://www.tumblr.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Microblogging and social networking platform"
9
+ }
10
+
11
+ APEX_VALUE = "66.6.44.4".freeze
12
+ CNAME_VALUE = "domains.tumblr.com".freeze
13
+ RESPONSE_FINGERPRINT = "Whatever you were looking for doesn't currently exist at this address.".freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless resource_value == APEX_VALUE
18
+ elsif cname_resource?
19
+ return false unless resource_value == CNAME_VALUE
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Unbounce < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Unbounce",
6
+ :service_website => "https://unbounce.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Landing page builder and conversion marketing platform"
9
+ }
10
+
11
+ APEX_VALUE = "54.84.104.245".freeze
12
+ CNAME_VALUE = "unbouncepages.com".freeze
13
+ RESPONSE_FINGERPRINT = "The requested URL was not found on this server.".freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless resource_value == APEX_VALUE
18
+ elsif cname_resource?
19
+ return false unless resource_value == CNAME_VALUE
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Uservoice < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "UserVoice",
6
+ :service_website => "https://www.uservoice.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Product management software"
9
+ }
10
+
11
+ CNAME_VALUE = ".uservoice.com".freeze
12
+ RESPONSE_FINGERPRINTS = [
13
+ "The page you have requested does not exist.",
14
+ "This UserVoice subdomain is currently available!"
15
+ ].freeze
16
+
17
+ def run
18
+ return false unless cname_resource?
19
+ if resource_value.end_with?(CNAME_VALUE)
20
+ response = get_request("http://#{host}/")
21
+ RESPONSE_FINGERPRINTS.each do |fingerprint|
22
+ return true if response.body.include?(fingerprint)
23
+ end
24
+ end
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Wpengine < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "WPEngine",
6
+ :service_website => "https://wpengine.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "WordPress blog hosting"
9
+ }
10
+
11
+ APEX_VALUE = "130.211.160.56".freeze
12
+ CNAME_VALUE = ".wpengine.com".freeze
13
+ RESPONSE_FINGERPRINT = "but is not configured for an account on our platform.".freeze
14
+
15
+ def run
16
+ if apex_resource?
17
+ return false unless resource_value == APEX_VALUE
18
+ elsif cname_resource?
19
+ return false unless resource_value.end_with?(CNAME_VALUE)
20
+ end
21
+ get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Aquatone
2
+ module Detectors
3
+ class Zendesk < Aquatone::Detector
4
+ self.meta = {
5
+ :service => "Zendesk",
6
+ :service_website => "https://www.zendesk.com/",
7
+ :author => "Michael Henriksen (@michenriksen)",
8
+ :description => "Customer service software and support ticket system"
9
+ }
10
+
11
+ CNAME_VALUE = ".zendesk.com".freeze
12
+ RESPONSE_FINGERPRINT = "<title>Help Center Closed | Zendesk</title>".freeze
13
+
14
+ def run
15
+ return false unless cname_resource?
16
+ if resource_value.end_with?(CNAME_VALUE)
17
+ return get_request("http://#{host}/").body.include?(RESPONSE_FINGERPRINT)
18
+ end
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -26,7 +26,7 @@ module Aquatone
26
26
  return self::MEDIUM
27
27
  when "large"
28
28
  return self::LARGE
29
- when "huge"
29
+ when "huge", "xlarge"
30
30
  return self::HUGE
31
31
  else
32
32
  fail UnknownPortListName, "Unknown port list name: #{name}"
@@ -22,6 +22,20 @@ module Aquatone
22
22
  nil
23
23
  end
24
24
 
25
+ def resource(host)
26
+ resource = resource_with_nameserver(host,
27
+ Resolv::DNS::Resource::IN::CNAME) ||
28
+ resource_with_fallback_nameserver(host,
29
+ Resolv::DNS::Resource::IN::CNAME)
30
+ if !resource
31
+ resource = resource_with_nameserver(host,
32
+ Resolv::DNS::Resource::IN::A) ||
33
+ resource_with_fallback_nameserver(host,
34
+ Resolv::DNS::Resource::IN::A)
35
+ end
36
+ resource
37
+ end
38
+
25
39
  private
26
40
 
27
41
  def resolve_with_nameserver(host)
@@ -32,6 +46,14 @@ module Aquatone
32
46
  _resolve(host, options[:fallback_nameservers].sample)
33
47
  end
34
48
 
49
+ def resource_with_nameserver(host, typeclass)
50
+ _resource(host, typeclass, options[:nameservers].sample)
51
+ end
52
+
53
+ def resource_with_fallback_nameserver(host, typeclass)
54
+ _resource(host, typeclass, options[:fallback_nameservers].sample)
55
+ end
56
+
35
57
  def _resolve(host, nameserver_ip)
36
58
  nameserver = Resolv::DNS.new(:nameserver => nameserver_ip).tap do |ns|
37
59
  ns.timeouts = options[:timeout]
@@ -43,5 +65,17 @@ module Aquatone
43
65
  nameserver.close
44
66
  nil
45
67
  end
68
+
69
+ def _resource(host, typeclass, nameserver_ip)
70
+ nameserver = Resolv::DNS.new(:nameserver => nameserver_ip).tap do |ns|
71
+ ns.timeouts = options[:timeout]
72
+ end
73
+ resource = nameserver.getresource(host, typeclass)
74
+ nameserver.close
75
+ resource
76
+ rescue Resolv::ResolvError
77
+ nameserver.close
78
+ nil
79
+ end
46
80
  end
47
81
  end
@@ -1,3 +1,3 @@
1
1
  module Aquatone
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aquatone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Henriksen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-25 00:00:00.000000000 Z
11
+ date: 2017-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -87,6 +87,7 @@ executables:
87
87
  - aquatone-discover
88
88
  - aquatone-gather
89
89
  - aquatone-scan
90
+ - aquatone-takeover
90
91
  extensions: []
91
92
  extra_rdoc_files: []
92
93
  files:
@@ -103,6 +104,7 @@ files:
103
104
  - exe/aquatone-discover
104
105
  - exe/aquatone-gather
105
106
  - exe/aquatone-scan
107
+ - exe/aquatone-takeover
106
108
  - lib/aquatone.rb
107
109
  - lib/aquatone/assessment.rb
108
110
  - lib/aquatone/browser.rb
@@ -124,6 +126,33 @@ files:
124
126
  - lib/aquatone/commands/discover.rb
125
127
  - lib/aquatone/commands/gather.rb
126
128
  - lib/aquatone/commands/scan.rb
129
+ - lib/aquatone/commands/takeover.rb
130
+ - lib/aquatone/detector.rb
131
+ - lib/aquatone/detectors/campaign_monitor.rb
132
+ - lib/aquatone/detectors/cargo.rb
133
+ - lib/aquatone/detectors/cloudfront.rb
134
+ - lib/aquatone/detectors/desk.rb
135
+ - lib/aquatone/detectors/fastly.rb
136
+ - lib/aquatone/detectors/feedpress.rb
137
+ - lib/aquatone/detectors/freshdesk.rb
138
+ - lib/aquatone/detectors/ghost.rb
139
+ - lib/aquatone/detectors/github_pages.rb
140
+ - lib/aquatone/detectors/helpjuice.rb
141
+ - lib/aquatone/detectors/helpscout.rb
142
+ - lib/aquatone/detectors/heroku.rb
143
+ - lib/aquatone/detectors/instapage.rb
144
+ - lib/aquatone/detectors/pingdom.rb
145
+ - lib/aquatone/detectors/s3.rb
146
+ - lib/aquatone/detectors/shopify.rb
147
+ - lib/aquatone/detectors/statuspage.rb
148
+ - lib/aquatone/detectors/surveygizmo.rb
149
+ - lib/aquatone/detectors/teamwork.rb
150
+ - lib/aquatone/detectors/tictail.rb
151
+ - lib/aquatone/detectors/tumblr.rb
152
+ - lib/aquatone/detectors/unbounce.rb
153
+ - lib/aquatone/detectors/uservoice.rb
154
+ - lib/aquatone/detectors/wpengine.rb
155
+ - lib/aquatone/detectors/zendesk.rb
127
156
  - lib/aquatone/domain.rb
128
157
  - lib/aquatone/http_client.rb
129
158
  - lib/aquatone/key_store.rb