cms_scanner 0.0.2

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.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +6 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +6 -0
  7. data/README.md +20 -0
  8. data/Rakefile +9 -0
  9. data/app/app.rb +4 -0
  10. data/app/controllers.rb +2 -0
  11. data/app/controllers/core.rb +46 -0
  12. data/app/controllers/core/cli_options.rb +68 -0
  13. data/app/controllers/interesting_files.rb +12 -0
  14. data/app/finders.rb +1 -0
  15. data/app/finders/interesting_files.rb +21 -0
  16. data/app/finders/interesting_files/fantastico_fileslist.rb +23 -0
  17. data/app/finders/interesting_files/headers.rb +15 -0
  18. data/app/finders/interesting_files/robots_txt.rb +22 -0
  19. data/app/finders/interesting_files/search_replace_db_2.rb +28 -0
  20. data/app/finders/interesting_files/xml_rpc.rb +62 -0
  21. data/app/formatters.rb +3 -0
  22. data/app/formatters/cli.rb +18 -0
  23. data/app/formatters/cli_no_colour.rb +15 -0
  24. data/app/formatters/json.rb +12 -0
  25. data/app/models.rb +5 -0
  26. data/app/models/fantastico_fileslist.rb +20 -0
  27. data/app/models/headers.rb +37 -0
  28. data/app/models/interesting_file.rb +30 -0
  29. data/app/models/robots_txt.rb +20 -0
  30. data/app/models/xml_rpc.rb +35 -0
  31. data/app/views/cli/core/finished.erb +4 -0
  32. data/app/views/cli/core/started.erb +3 -0
  33. data/app/views/cli/interesting_files/findings.erb +19 -0
  34. data/app/views/cli/scan_aborted.erb +4 -0
  35. data/app/views/json/core/finished.erb +3 -0
  36. data/app/views/json/core/started.erb +3 -0
  37. data/app/views/json/interesting_files/findings.erb +1 -0
  38. data/app/views/json/scan_aborted.erb +4 -0
  39. data/cms_scanner.gemspec +37 -0
  40. data/examples/views/cli/wp_custom/test.erb +1 -0
  41. data/examples/views/json/wp_custom/test.erb +1 -0
  42. data/examples/wpscan.rb +29 -0
  43. data/lib/cms_scanner.rb +71 -0
  44. data/lib/cms_scanner/browser.rb +68 -0
  45. data/lib/cms_scanner/browser/actions.rb +48 -0
  46. data/lib/cms_scanner/browser/options.rb +53 -0
  47. data/lib/cms_scanner/cache/file_store.rb +75 -0
  48. data/lib/cms_scanner/cache/typhoeus.rb +21 -0
  49. data/lib/cms_scanner/controller.rb +90 -0
  50. data/lib/cms_scanner/controllers.rb +34 -0
  51. data/lib/cms_scanner/errors/auth_errors.rb +15 -0
  52. data/lib/cms_scanner/finders.rb +5 -0
  53. data/lib/cms_scanner/finders/finder.rb +27 -0
  54. data/lib/cms_scanner/finders/finding.rb +32 -0
  55. data/lib/cms_scanner/finders/findings.rb +25 -0
  56. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  57. data/lib/cms_scanner/finders/independent_finders.rb +41 -0
  58. data/lib/cms_scanner/formatter.rb +118 -0
  59. data/lib/cms_scanner/formatter/buffer.rb +15 -0
  60. data/lib/cms_scanner/target.rb +33 -0
  61. data/lib/cms_scanner/target/platform.rb +2 -0
  62. data/lib/cms_scanner/target/platform/php.rb +39 -0
  63. data/lib/cms_scanner/target/platform/wordpress.rb +35 -0
  64. data/lib/cms_scanner/target/platform/wordpress/custom_directories.rb +62 -0
  65. data/lib/cms_scanner/target/server.rb +3 -0
  66. data/lib/cms_scanner/target/server/apache.rb +43 -0
  67. data/lib/cms_scanner/target/server/generic.rb +34 -0
  68. data/lib/cms_scanner/target/server/iis.rb +48 -0
  69. data/lib/cms_scanner/version.rb +4 -0
  70. data/lib/cms_scanner/web_site.rb +68 -0
  71. data/lib/helper.rb +24 -0
  72. data/spec/app/controllers/core_spec.rb +152 -0
  73. data/spec/app/controllers/interesting_files_spec.rb +50 -0
  74. data/spec/app/finders/interesting_files/fantastico_fileslist_spec.rb +68 -0
  75. data/spec/app/finders/interesting_files/headers_spec.rb +38 -0
  76. data/spec/app/finders/interesting_files/robots_txt_spec.rb +56 -0
  77. data/spec/app/finders/interesting_files/search_replace_db_2_spec.rb +55 -0
  78. data/spec/app/finders/interesting_files/xml_rpc_spec.rb +138 -0
  79. data/spec/app/finders/interesting_files_spec.rb +13 -0
  80. data/spec/app/formatters/cli_no_colour_spec.rb +17 -0
  81. data/spec/app/formatters/cli_spec.rb +21 -0
  82. data/spec/app/formatters/json_spec.rb +33 -0
  83. data/spec/app/models/fantastico_fileslist_spec.rb +32 -0
  84. data/spec/app/models/headers_spec.rb +52 -0
  85. data/spec/app/models/interesting_file_spec.rb +51 -0
  86. data/spec/app/models/robots_txt_spec.rb +28 -0
  87. data/spec/app/models/xml_rpc_spec.rb +47 -0
  88. data/spec/cache/.gitignore +4 -0
  89. data/spec/dummy_finders.rb +41 -0
  90. data/spec/fixtures/interesting_files/fantastico_fileslist/fantastico_fileslist.txt +12 -0
  91. data/spec/fixtures/interesting_files/file.txt +4 -0
  92. data/spec/fixtures/interesting_files/headers/interesting.txt +14 -0
  93. data/spec/fixtures/interesting_files/headers/no_interesting.txt +12 -0
  94. data/spec/fixtures/interesting_files/robots_txt/robots.txt +10 -0
  95. data/spec/fixtures/interesting_files/search_replace_db_2/searchreplacedb2.php +188 -0
  96. data/spec/fixtures/interesting_files/xml_rpc/homepage_in_scope_pingback.html +7 -0
  97. data/spec/fixtures/interesting_files/xml_rpc/homepage_out_of_scope_pingback.html +7 -0
  98. data/spec/fixtures/interesting_files/xml_rpc/xmlrpc.php +1 -0
  99. data/spec/fixtures/output.txt +0 -0
  100. data/spec/fixtures/target/platform/php/debug_log/debug.log +2 -0
  101. data/spec/fixtures/target/platform/php/fpd/wp_rss_functions.php +2 -0
  102. data/spec/fixtures/target/platform/wordpress/custom_directories/custom_w_spaces.html +10 -0
  103. data/spec/fixtures/target/platform/wordpress/custom_directories/default.html +14 -0
  104. data/spec/fixtures/target/platform/wordpress/custom_directories/https.html +12 -0
  105. data/spec/fixtures/target/platform/wordpress/detection/default.html +4 -0
  106. data/spec/fixtures/target/platform/wordpress/detection/not_wp.html +8 -0
  107. data/spec/fixtures/target/platform/wordpress/detection/wp_includes.html +3 -0
  108. data/spec/fixtures/target/server/apache/directory_listing/2.2.16.html +15 -0
  109. data/spec/fixtures/target/server/generic/server/apache/basic.txt +5 -0
  110. data/spec/fixtures/target/server/generic/server/iis/basic.txt +6 -0
  111. data/spec/fixtures/target/server/generic/server/not_detected.txt +3 -0
  112. data/spec/fixtures/target/server/iis/directory_listing/no_parent.html +3 -0
  113. data/spec/fixtures/target/server/iis/directory_listing/with_parent.html +3 -0
  114. data/spec/fixtures/views/base/ctrl/local.erb +1 -0
  115. data/spec/fixtures/views/base/ctrl/test.erb +3 -0
  116. data/spec/fixtures/views/base/global.erb +1 -0
  117. data/spec/fixtures/views/base/test.erb +2 -0
  118. data/spec/fixtures/views/based_format/test.erb +1 -0
  119. data/spec/fixtures/views/json/render_me.erb +4 -0
  120. data/spec/lib/browser_spec.rb +141 -0
  121. data/spec/lib/cache/file_store_spec.rb +101 -0
  122. data/spec/lib/cache/typhoeus_spec.rb +30 -0
  123. data/spec/lib/cms_scanner_spec.rb +45 -0
  124. data/spec/lib/controller_spec.rb +23 -0
  125. data/spec/lib/controllers_spec.rb +52 -0
  126. data/spec/lib/finders/findings_spec.rb +49 -0
  127. data/spec/lib/finders/independent_finders_spec.rb +98 -0
  128. data/spec/lib/formatter_spec.rb +136 -0
  129. data/spec/lib/sub_scanner_spec.rb +27 -0
  130. data/spec/lib/target/platforms_spec.rb +13 -0
  131. data/spec/lib/target/servers_spec.rb +13 -0
  132. data/spec/lib/target_spec.rb +50 -0
  133. data/spec/lib/web_site_spec.rb +124 -0
  134. data/spec/shared_examples.rb +11 -0
  135. data/spec/shared_examples/browser_actions.rb +32 -0
  136. data/spec/shared_examples/finding.rb +20 -0
  137. data/spec/shared_examples/formatter_buffer.rb +8 -0
  138. data/spec/shared_examples/formatter_class_methods.rb +26 -0
  139. data/spec/shared_examples/independent_finder.rb +33 -0
  140. data/spec/shared_examples/target/platform/php.rb +58 -0
  141. data/spec/shared_examples/target/platform/wordpress.rb +41 -0
  142. data/spec/shared_examples/target/platform/wordpress/custom_directories.rb +50 -0
  143. data/spec/shared_examples/target/server/apache.rb +33 -0
  144. data/spec/shared_examples/target/server/generic.rb +34 -0
  145. data/spec/shared_examples/target/server/iis.rb +38 -0
  146. data/spec/spec_helper.rb +41 -0
  147. metadata +432 -0
@@ -0,0 +1,4 @@
1
+ # Version
2
+ module CMSScanner
3
+ VERSION = '0.0.2'
4
+ end
@@ -0,0 +1,68 @@
1
+ module CMSScanner
2
+ # WebSite Implementation
3
+ class WebSite
4
+ attr_reader :uri
5
+
6
+ def initialize(site_url)
7
+ self.url = site_url.dup
8
+ end
9
+
10
+ def url=(site_url)
11
+ # Add a trailing slash to the site url
12
+ site_url << '/' if site_url[-1, 1] != '/'
13
+
14
+ # Use the validator to ensure the site_url has a correct format
15
+ OptParseValidator::OptURL.new([]).validate(site_url)
16
+
17
+ @uri = Addressable::URI.parse(site_url)
18
+ end
19
+
20
+ # Used for convenience
21
+ #
22
+ # @param [ String ] path Optional path to merge with the uri
23
+ #
24
+ # @return [ String ]
25
+ def url(path = nil)
26
+ @uri.join(path || '').to_s
27
+ end
28
+
29
+ # Checks if the remote website is up.
30
+ #
31
+ # @return [ Boolean ]
32
+ def online?
33
+ NS::Browser.get(url).code != 0
34
+ end
35
+
36
+ # @return [ Boolean ]
37
+ def http_auth?
38
+ NS::Browser.get(url).code == 401
39
+ end
40
+
41
+ # @return [ Boolean ]
42
+ def proxy_auth?
43
+ NS::Browser.get(url).code == 407
44
+ end
45
+
46
+ # See if the remote url returns 30x redirect
47
+ # This method is recursive
48
+ #
49
+ # @param [ String ] url
50
+ #
51
+ # @return [ String ] The redirection url or nil
52
+ def redirection(url = nil)
53
+ url ||= @uri.to_s
54
+ response = NS::Browser.get(url)
55
+
56
+ if response.code == 301 || response.code == 302
57
+ redirection = response.headers_hash['location']
58
+
59
+ # Let's check if there is a redirection in the redirection
60
+ if (other_redirection = redirection(redirection))
61
+ redirection = other_redirection
62
+ end
63
+ end
64
+
65
+ redirection
66
+ end
67
+ end
68
+ end
data/lib/helper.rb ADDED
@@ -0,0 +1,24 @@
1
+
2
+ # @param [ String ] file The file path
3
+ def redirect_output_to_file(file)
4
+ $stdout.reopen(file, 'w')
5
+ $stdout.sync = true
6
+ $stderr.reopen($stdout) # Not sure if this is needed
7
+ end
8
+
9
+ # @return [ Integer ] The memory of the current process in Bytes
10
+ def memory_usage
11
+ `ps -o rss= -p #{Process.pid}`.to_i * 1024 # ps returns the value in KB
12
+ end
13
+
14
+ # Hack of the Numeric class
15
+ class Numeric
16
+ # @return [ String ] A human readable string of the value
17
+ def bytes_to_human
18
+ units = %w(B KB MB GB TB)
19
+ e = self > 0 ? (Math.log(self) / Math.log(1024)).floor : 0
20
+ s = format('%.3f', (to_f / 1024**e))
21
+
22
+ s.sub(/\.?0*$/, ' ' + units[e])
23
+ end
24
+ end
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Controller::Core do
4
+
5
+ subject(:core) { described_class.new }
6
+ let(:target_url) { 'http://example.com/' }
7
+ let(:parsed_options) { { url: target_url } }
8
+
9
+ before do
10
+ CMSScanner::Browser.reset
11
+ described_class.parsed_options = parsed_options
12
+ end
13
+
14
+ its(:cli_options) { should_not be_empty }
15
+ its(:cli_options) { should be_a Array }
16
+
17
+ describe '#setup_cache' do
18
+ context 'when no cache_dir supplied (or default)' do
19
+ it 'returns nil' do
20
+ expect(core.setup_cache).to eq nil
21
+ end
22
+ end
23
+
24
+ context 'when cache_dir' do
25
+ let(:parsed_options) { super().merge(cache_dir: CACHE) }
26
+ let(:cache) { Typhoeus::Config.cache }
27
+ let(:storage) { File.join(parsed_options[:cache_dir], Digest::MD5.hexdigest(target_url)) }
28
+
29
+ before { core.setup_cache }
30
+ after { Typhoeus::Config.cache = nil }
31
+
32
+ it 'sets up the cache' do
33
+ expect(cache).to be_a CMSScanner::Cache::Typhoeus
34
+ expect(cache.storage_path).to eq storage
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#before_scan' do
40
+ it 'raise an error when the site is down' do
41
+ stub_request(:get, target_url).to_return(status: 0)
42
+
43
+ expect { core.before_scan }
44
+ .to raise_error("The url supplied '#{target_url}' seems to be down")
45
+ end
46
+
47
+ it 'raises an error when the site redirects' do
48
+ redirection = 'http://somewhere.com'
49
+
50
+ stub_request(:get, target_url).to_return(status: 301, headers: { location: redirection })
51
+ stub_request(:get, redirection).to_return(status: 200)
52
+
53
+ expect { core.before_scan }
54
+ .to raise_error("The url supplied redirects to #{redirection}")
55
+ end
56
+
57
+ # This is quite a mess (as Webmock doesn't issue itself another 401
58
+ # when credential are incorrect :/)
59
+ context 'when http authentication' do
60
+ context 'when no credentials' do
61
+ before { stub_request(:get, target_url).to_return(status: 401) }
62
+
63
+ it 'raises an error' do
64
+ expect { core.before_scan }.to raise_error(CMSScanner::HTTPAuthRequiredError)
65
+ end
66
+ end
67
+
68
+ context 'when credentials' do
69
+ context 'when valid' do
70
+ before { stub_request(:get, 'http://user:pass@example.com') }
71
+
72
+ let(:parsed_options) { super().merge(http_auth: { username: 'user', password: 'pass' }) }
73
+
74
+ it 'does not raise any error' do
75
+ expect { core.before_scan }.to_not raise_error
76
+ end
77
+ end
78
+
79
+ context 'when invalid' do
80
+ before { stub_request(:get, 'http://user:p@ss@example.com').to_return(status: 401) }
81
+
82
+ let(:parsed_options) { super().merge(http_auth: { username: 'user', password: 'p@ss' }) }
83
+
84
+ it 'raises an error' do
85
+ expect { core.before_scan }.to raise_error(CMSScanner::HTTPAuthRequiredError)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'when proxy authentication' do
92
+ before { stub_request(:get, target_url).to_return(status: 407) }
93
+
94
+ context 'when no credentials' do
95
+ it 'raises an error' do
96
+ expect { core.before_scan }.to raise_error(CMSScanner::ProxyAuthRequiredError)
97
+ end
98
+ end
99
+
100
+ context 'when invalid credentials' do
101
+ let(:parsed_options) { super().merge(proxy_auth: { username: 'user', password: 'p@ss' }) }
102
+
103
+ it 'raises an error' do
104
+ expect(CMSScanner::Browser.instance.proxy_auth).to eq(parsed_options[:proxy_auth])
105
+
106
+ expect { core.before_scan }.to raise_error(CMSScanner::ProxyAuthRequiredError)
107
+ end
108
+ end
109
+
110
+ context 'when valid credentials' do
111
+ before { stub_request(:get, target_url) }
112
+
113
+ let(:parsed_options) { super().merge(proxy_auth: { username: 'user', password: 'pass' }) }
114
+
115
+ it 'raises an error' do
116
+ expect(CMSScanner::Browser.instance.proxy_auth).to eq(parsed_options[:proxy_auth])
117
+
118
+ expect { core.before_scan }.to_not raise_error
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ describe '#run' do
125
+ it 'calls the formatter with the correct parameters' do
126
+ expect(core.formatter).to receive(:output)
127
+ .with('started',
128
+ hash_including(:start_memory, :start_time, :verbose, url: target_url),
129
+ 'core')
130
+
131
+ core.run
132
+ end
133
+ end
134
+
135
+ describe '#after_scan' do
136
+ let(:keys) { [:verbose, :start_time, :stop_time, :start_memory, :elapsed, :used_memory] }
137
+
138
+ it 'calles the formatter with the correct parameters' do
139
+ # Call the #run once to ensure that @start_time & @start_memory are set
140
+ expect(core).to receive(:output).with('started', url: target_url)
141
+ core.run
142
+
143
+ RSpec::Mocks.space.proxy_for(core).reset # Must reset due to the above statements
144
+
145
+ expect(core.formatter).to receive(:output)
146
+ .with('finished', hash_including(*keys), 'core')
147
+
148
+ core.after_scan
149
+ end
150
+ end
151
+
152
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Controller::InterestingFiles do
4
+
5
+ subject(:controller) { described_class.new }
6
+ let(:target_url) { 'http://example.com/' }
7
+ let(:parsed_options) { { url: target_url } }
8
+
9
+ before do
10
+ CMSScanner::Browser.reset
11
+ described_class.parsed_options = parsed_options
12
+ end
13
+
14
+ its(:cli_options) { should be_nil }
15
+ its(:before_scan) { should be_nil }
16
+ its(:after_scan) { should be_nil }
17
+
18
+ describe '#run' do
19
+ before do
20
+ expect(controller.target).to receive(:interesting_files)
21
+ .with(mode: parsed_options[:detection_mode]).and_return(stubbed)
22
+ end
23
+ after { controller.run }
24
+
25
+ [:mixed, :passive, :aggressive].each do |mode|
26
+ context "when #{mode} mode" do
27
+ let(:parsed_options) { super().merge(detection_mode: mode) }
28
+
29
+ context 'when no findings' do
30
+ let(:stubbed) { [] }
31
+
32
+ it 'does not call the formatter' do
33
+ expect(controller.formatter).to_not receive(:output)
34
+ end
35
+ end
36
+
37
+ # TODO: Test the output with a dummy finding ?
38
+ context 'when findings' do
39
+ let(:stubbed) { ['yolo'] }
40
+
41
+ it 'calls the formatter with the correct parameter' do
42
+ expect(controller.formatter).to receive(:output)
43
+ .with('findings', hash_including(findings: stubbed), 'interesting_files')
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Finders::InterestingFile::FantasticoFileslist do
4
+
5
+ subject(:finder) { described_class.new(target) }
6
+ let(:target) { CMSScanner::Target.new(url) }
7
+ let(:url) { 'http://example.com/' }
8
+ let(:file) { url + 'fantastico_fileslist.txt' }
9
+ let(:fixtures) { File.join(FIXTURES, 'interesting_files', 'fantastico_fileslist') }
10
+
11
+ describe '#url' do
12
+ its(:url) { should eq file }
13
+ end
14
+
15
+ describe '#aggressive' do
16
+ after do
17
+ stub_request(:get, file).to_return(status: status, body: body, headers: headers)
18
+
19
+ result = finder.aggressive
20
+
21
+ expect(result).to be_a CMSScanner::FantasticoFileslist if @expected
22
+ expect(result).to eql @expected
23
+ end
24
+
25
+ let(:body) { '' }
26
+ let(:headers) { { 'Content-Type' => 'text/html ' } }
27
+
28
+ context 'when 404' do
29
+ let(:status) { 404 }
30
+
31
+ it 'returns nil' do
32
+ @expected = nil
33
+ end
34
+ end
35
+
36
+ context 'when 200' do
37
+ let(:status) { 200 }
38
+
39
+ context 'when the body is empty' do
40
+ it 'returns nil' do
41
+ @expected = nil
42
+ end
43
+ end
44
+
45
+ context 'when not a text/plain Content-Type' do
46
+ let(:body) { 'not an empty body' }
47
+
48
+ it 'returns nil' do
49
+ @expected = nil
50
+ end
51
+ end
52
+
53
+ context 'when the body matches and Content-Type = text/plain' do
54
+ let(:body) { File.new(File.join(fixtures, 'fantastico_fileslist.txt')).read }
55
+ let(:headers) { { 'Content-Type' => 'text/plain' } }
56
+
57
+ it 'returns the InterestingFile result' do
58
+ @expected = CMSScanner::FantasticoFileslist.new(
59
+ file,
60
+ confidence: 100,
61
+ found_by: 'FantasticoFileslist (aggressive detection)'
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Finders::InterestingFile::Headers do
4
+
5
+ subject(:finder) { described_class.new(target) }
6
+ let(:target) { CMSScanner::Target.new(url) }
7
+ let(:url) { 'http://example.com/' }
8
+ let(:fixtures) { File.join(FIXTURES, 'interesting_files', 'headers') }
9
+ let(:fixture) { File.join(fixtures, 'interesting.txt') }
10
+ let(:headers) { parse_headers_file(fixture) }
11
+
12
+ describe '#passive' do
13
+ before { stub_request(:get, url).to_return(headers: headers) }
14
+
15
+ after do
16
+ if @expected
17
+ result = finder.passive
18
+
19
+ expect(result).to be_a CMSScanner::Headers
20
+ expect(result).to eql @expected
21
+ end
22
+ end
23
+
24
+ context 'when no headers' do
25
+ let(:headers) { {} }
26
+
27
+ its(:passive) { should be nil }
28
+ end
29
+
30
+ context 'when headers' do
31
+ it 'returns the result' do
32
+ opts = { confidence: 100, found_by: 'Headers (passive detection)' }
33
+ @expected = CMSScanner::Headers.new(url, opts)
34
+ end
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMSScanner::Finders::InterestingFile::RobotsTxt do
4
+
5
+ subject(:finder) { described_class.new(target) }
6
+ let(:target) { CMSScanner::Target.new(url) }
7
+ let(:url) { 'http://example.com/' }
8
+ let(:robots_txt) { url + 'robots.txt' }
9
+ let(:fixtures) { File.join(FIXTURES, 'interesting_files', 'robots_txt') }
10
+
11
+ describe '#url' do
12
+ its(:url) { should eq robots_txt }
13
+ end
14
+
15
+ describe '#aggressive' do
16
+ after do
17
+ stub_request(:get, robots_txt).to_return(status: status, body: body)
18
+
19
+ result = finder.aggressive
20
+
21
+ expect(result).to be_a CMSScanner::RobotsTxt if @expected
22
+ expect(finder.aggressive).to eql @expected
23
+ end
24
+
25
+ let(:body) { '' }
26
+
27
+ context 'when 404' do
28
+ let(:status) { 404 }
29
+
30
+ it 'returns nil' do
31
+ @expected = nil
32
+ end
33
+ end
34
+
35
+ context 'when 200' do
36
+ let(:status) { 200 }
37
+
38
+ context 'when the body is empty' do
39
+ it 'returns nil' do
40
+ @expected = nil
41
+ end
42
+ end
43
+
44
+ context 'when the body matches a robots.txt' do
45
+ let(:body) { File.new(File.join(fixtures, 'robots.txt')).read }
46
+
47
+ it 'returns the InterestingFile result' do
48
+ @expected = CMSScanner::RobotsTxt.new(robots_txt,
49
+ confidence: 100,
50
+ found_by: 'RobotsTxt (aggressive detection)')
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ end