New upstream version 0.0.40
Sophie Brun
5 years ago
0 | AllCops: | |
1 | TargetRubyVersion: 2.3 | |
2 | Exclude: | |
3 | - '*.gemspec' | |
4 | - 'vendor/**/*' | |
5 | - 'example/**/*' | |
6 | ClassVars: | |
7 | Enabled: false | |
8 | LineLength: | |
9 | Max: 120 | |
10 | MethodLength: | |
11 | Max: 18 | |
12 | Lint/UriEscapeUnescape: | |
13 | Enabled: false | |
14 | Metrics/AbcSize: | |
15 | Max: 25 | |
16 | Metrics/BlockLength: | |
17 | Exclude: | |
18 | - 'spec/**/*' | |
19 | Metrics/CyclomaticComplexity: | |
20 | Max: 10 | |
21 | Metrics/PerceivedComplexity: | |
22 | Max: 9 | |
23 | Style/FrozenStringLiteralComment: | |
24 | Enabled: false | |
25 | Style/FormatStringToken: | |
26 | Exclude: | |
27 | - lib/cms_scanner/finders/finder.rb | |
28 | Style/MixinUsage: | |
29 | Exclude: | |
30 | - lib/cms_scanner/formatter.rb |
0 | language: ruby | |
1 | sudo: false | |
2 | cache: bundler | |
3 | rvm: | |
4 | - 2.3.0 | |
5 | - 2.3.1 | |
6 | - 2.3.2 | |
7 | - 2.3.3 | |
8 | - 2.3.4 | |
9 | - 2.4.0 | |
10 | - 2.4.1 | |
11 | - 2.5.0 | |
12 | - ruby-head | |
13 | before_install: | |
14 | - "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc" | |
15 | - "gem update --system" | |
16 | matrix: | |
17 | allow_failures: | |
18 | - rvm: ruby-head | |
19 | script: | |
20 | - bundle exec rubocop | |
21 | - bundle exec rspec | |
22 | notifications: | |
23 | email: | |
24 | - [email protected] |
0 | Copyright (C) 2014-2015 - WPScanTeam | |
1 | ||
2 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
3 | of this software and associated documentation files (the "Software"), to deal | |
4 | in the Software without restriction, including without limitation the rights | |
5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
6 | copies of the Software, and to permit persons to whom the Software is | |
7 | furnished to do so, subject to the following conditions: | |
8 | ||
9 | The above copyright notice and this permission notice shall be included in all | |
10 | copies or substantial portions of the Software. | |
11 | ||
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
18 | SOFTWARE. |
0 | # CMSScanner | |
1 | ||
2 | [![Gem Version](https://badge.fury.io/rb/cms_scanner.svg)](https://badge.fury.io/rb/cms_scanner) | |
3 | [![Build Status](https://img.shields.io/travis/wpscanteam/CMSScanner.svg)](https://travis-ci.org/wpscanteam/CMSScanner) | |
4 | [![Coverage Status](https://img.shields.io/coveralls/wpscanteam/CMSScanner.svg)](https://coveralls.io/r/wpscanteam/CMSScanner) | |
5 | [![Code Climate](https://api.codeclimate.com/v1/badges/b90b7f9f6982792ef8d6/maintainability)](https://codeclimate.com/github/wpscanteam/CMSScanner/maintainability) | |
6 | [![Dependency Status](https://img.shields.io/gemnasium/wpscanteam/CMSScanner.svg)](https://gemnasium.com/wpscanteam/CMSScanner) | |
7 | ||
8 | The goal of this gem is to provide a quick and easy way to create a CMS/WebSite Scanner by acting like a Framework and providing classes, formatters etc. | |
9 | ||
10 | ## /!\ This gem is currently Experimental /!\ | |
11 | ||
12 | ## A basic implementation example is available in the example folder. | |
13 | ||
14 | To start to play with it, copy all its files and folders into a new git repository and run `bundle install && rake install` inside it. | |
15 | It will create a `cmsscan` command that you can run against a target, ie `cmsscan --url https://www.google.com` | |
16 | ||
17 | ||
18 | Install Dependencies: `bundle install` | |
19 | ||
20 | ## Contributing | |
21 | ||
22 | 1. Fork it ( https://github.com/wpscanteam/CMSScanner/fork ) | |
23 | 2. Create your feature branch (`git checkout -b my-new-feature`) | |
24 | 3. Commit your changes (`git commit -am 'Add some feature'`) | |
25 | 4. Push to the branch (`git push origin my-new-feature`) | |
26 | 5. Create new Pull Request |
0 | require 'bundler/gem_tasks' | |
1 | require 'rspec/core/rake_task' | |
2 | require 'rubocop/rake_task' | |
3 | ||
4 | RuboCop::RakeTask.new | |
5 | RSpec::Core::RakeTask.new(:spec) | |
6 | ||
7 | # Run rubocop & rspec before the build | |
8 | task build: %i[rubocop spec] |
0 | require_relative 'formatters' | |
1 | require_relative 'controllers' | |
2 | require_relative 'models' | |
3 | require_relative 'finders' |
0 | module CMSScanner | |
1 | module Controller | |
2 | # CLI Options for the Core Controller | |
3 | class Core < Base | |
4 | def cli_options | |
5 | formats = NS::Formatter.availables | |
6 | ||
7 | [ | |
8 | OptURL.new(['-u', '--url URL', 'The URL to scan'], | |
9 | required_unless: %i[help version], | |
10 | default_protocol: 'http') | |
11 | ] + mixed_cli_options + [ | |
12 | OptFilePath.new(['-o', '--output FILE', 'Output to FILE'], writable: true, exists: false), | |
13 | OptChoice.new(['-f', '--format FORMAT', | |
14 | 'Output results in the format supplied'], choices: formats), | |
15 | OptChoice.new(['--detection-mode MODE'], | |
16 | choices: %w[mixed passive aggressive], | |
17 | normalize: :to_sym, | |
18 | default: :mixed), | |
19 | OptArray.new(['--scope DOMAINS', | |
20 | 'Comma separated (sub-)domains to consider in scope. ', | |
21 | 'Wildcard(s) allowed in the trd of valid domains, e.g: *.target.tld']) | |
22 | ] + cli_browser_options | |
23 | end | |
24 | ||
25 | def mixed_cli_options | |
26 | [ | |
27 | OptBoolean.new(['-h', '--help', 'Display the help and exit']), | |
28 | OptBoolean.new(['--version', 'Display the version and exit']), | |
29 | OptBoolean.new(['--ignore-main-redirect', 'Ignore the main redirect (if any) and scan the target url']), | |
30 | OptBoolean.new(['-v', '--verbose', 'Verbose mode']), | |
31 | OptBoolean.new(['--[no-]banner', 'Whether or not to display the banner'], default: true) | |
32 | ] | |
33 | end | |
34 | ||
35 | # @return [ Array<OptParseValidator::OptBase> ] | |
36 | def cli_browser_options | |
37 | cli_browser_headers_options + [ | |
38 | OptBoolean.new(['--random-user-agent', '--rua', | |
39 | 'Use a random user-agent for each scan']), | |
40 | OptFilePath.new(['--user-agents-list FILE-PATH', | |
41 | 'List of agents to use with --random-user-agent'], exists: true), | |
42 | OptCredentials.new(['--http-auth login:password']), | |
43 | OptPositiveInteger.new(['--max-threads VALUE', '-t', 'The max threads to use'], | |
44 | default: 5), | |
45 | OptPositiveInteger.new(['--throttle MilliSeconds', 'Milliseconds to wait before doing another web request. ' \ | |
46 | 'If used, the max threads will be set to 1.']), | |
47 | OptPositiveInteger.new(['--request-timeout SECONDS', 'The request timeout in seconds'], | |
48 | default: 60), | |
49 | OptPositiveInteger.new(['--connect-timeout SECONDS', 'The connection timeout in seconds'], | |
50 | default: 30), | |
51 | OptBoolean.new(['--disable-tls-checks', 'Disables SSL/TLS certificate verification']) | |
52 | ] + cli_browser_proxy_options + cli_browser_cookies_options + cli_browser_cache_options | |
53 | end | |
54 | ||
55 | # @return [ Array<OptParseValidator::OptBase> ] | |
56 | def cli_browser_headers_options | |
57 | [ | |
58 | OptString.new(['--user-agent VALUE', '--ua']), | |
59 | OptHeaders.new(['--headers HEADERS', 'Additional headers to append in requests']), | |
60 | OptString.new(['--vhost VALUE', 'The virtual host (Host header) to use in requests']) | |
61 | ] | |
62 | end | |
63 | ||
64 | # @return [ Array<OptParseValidator::OptBase> ] | |
65 | def cli_browser_proxy_options | |
66 | [ | |
67 | OptProxy.new(['--proxy protocol://IP:port', | |
68 | 'Supported protocols depend on the cURL installed']), | |
69 | OptCredentials.new(['--proxy-auth login:password']) | |
70 | ] | |
71 | end | |
72 | ||
73 | # @return [ Array<OptParseValidator::OptBase> ] | |
74 | def cli_browser_cookies_options | |
75 | [ | |
76 | OptString.new(['--cookie-string COOKIE', | |
77 | 'Cookie string to use in requests, ' \ | |
78 | 'format: cookie1=value1[; cookie2=value2]']), | |
79 | OptFilePath.new(['--cookie-jar FILE-PATH', 'File to read and write cookies'], | |
80 | writable: true, | |
81 | readable: true, | |
82 | create: true, | |
83 | default: File.join(tmp_directory, 'cookie_jar.txt')) | |
84 | ] | |
85 | end | |
86 | ||
87 | # @return [ Array<OptParseValidator::OptBase> ] | |
88 | def cli_browser_cache_options | |
89 | [ | |
90 | OptInteger.new(['--cache-ttl TIME_TO_LIVE', 'The cache time to live in seconds'], default: 600), | |
91 | OptBoolean.new(['--clear-cache', 'Clear the cache before the scan']), | |
92 | OptDirectoryPath.new(['--cache-dir PATH'], | |
93 | readable: true, | |
94 | writable: true, | |
95 | create: true, | |
96 | default: File.join(tmp_directory, 'cache')) | |
97 | ] | |
98 | end | |
99 | end | |
100 | end | |
101 | end |
0 | require_relative 'core/cli_options' | |
1 | ||
2 | module CMSScanner | |
3 | module Controller | |
4 | # Core Controller | |
5 | class Core < Base | |
6 | def setup_cache | |
7 | return unless parsed_options[:cache_dir] | |
8 | ||
9 | storage_path = File.join(parsed_options[:cache_dir], Digest::MD5.hexdigest(target.url)) | |
10 | ||
11 | Typhoeus::Config.cache = Cache::Typhoeus.new(storage_path) | |
12 | Typhoeus::Config.cache.clean if parsed_options[:clear_cache] | |
13 | end | |
14 | ||
15 | def before_scan | |
16 | maybe_output_banner_help_and_version | |
17 | ||
18 | setup_cache | |
19 | check_target_availability | |
20 | end | |
21 | ||
22 | def maybe_output_banner_help_and_version | |
23 | output('banner') if parsed_options[:banner] | |
24 | output('help', help: option_parser.to_s) if parsed_options[:help] | |
25 | output('version') if parsed_options[:version] | |
26 | ||
27 | exit(NS::ExitCode::OK) if parsed_options[:help] || parsed_options[:version] | |
28 | end | |
29 | ||
30 | # Checks that the target is accessible, raises related errors otherwise | |
31 | # | |
32 | # @return [ Void ] | |
33 | def check_target_availability | |
34 | res = NS::Browser.get(target.url) | |
35 | ||
36 | case res.code | |
37 | when 0 | |
38 | raise TargetDownError, res | |
39 | when 401 | |
40 | raise HTTPAuthRequiredError | |
41 | when 403 | |
42 | raise AccessForbiddenError | |
43 | when 407 | |
44 | raise ProxyAuthRequiredError | |
45 | end | |
46 | ||
47 | # Checks for redirects | |
48 | # An out of scope redirect will raise an HTTPRedirectError | |
49 | effective_url = target.homepage_res.effective_url | |
50 | ||
51 | return if target.in_scope?(effective_url) | |
52 | ||
53 | raise HTTPRedirectError, effective_url unless parsed_options[:ignore_main_redirect] | |
54 | ||
55 | target.homepage_res = res | |
56 | end | |
57 | ||
58 | def run | |
59 | @start_time = Time.now | |
60 | @start_memory = memory_usage | |
61 | ||
62 | output('started', url: target.url, effective_url: target.homepage_url) | |
63 | end | |
64 | ||
65 | def after_scan | |
66 | @stop_time = Time.now | |
67 | @elapsed = @stop_time - @start_time | |
68 | @used_memory = memory_usage - @start_memory | |
69 | @requests_done = CMSScanner.total_requests | |
70 | ||
71 | output('finished') | |
72 | end | |
73 | end | |
74 | end | |
75 | end |
0 | module CMSScanner | |
1 | module Controller | |
2 | # InterestingFindings Controller | |
3 | class InterestingFindings < Base | |
4 | def cli_options | |
5 | [ | |
6 | OptChoice.new( | |
7 | ['--interesting-findings-detection MODE', | |
8 | 'Use the supplied mode for the interesting findings detection. '], | |
9 | choices: %w[mixed passive aggressive], | |
10 | normalize: :to_sym | |
11 | ) | |
12 | ] | |
13 | end | |
14 | ||
15 | def run | |
16 | mode = parsed_options[:interesting_findings_detection] || parsed_options[:detection_mode] | |
17 | findings = target.interesting_findings(mode: mode) | |
18 | ||
19 | output('findings', findings: findings) unless findings.empty? | |
20 | end | |
21 | end | |
22 | end | |
23 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | module InterestingFindings | |
3 | # FantasticoFileslist finder | |
4 | class FantasticoFileslist < Finder | |
5 | # @return [ String ] The url of the fantastico_fileslist.txt file | |
6 | def url | |
7 | target.url('fantastico_fileslist.txt') | |
8 | end | |
9 | ||
10 | # @return [ InterestingFinding ] | |
11 | def aggressive(_opts = {}) | |
12 | res = NS::Browser.get(url) | |
13 | ||
14 | return unless res&.code == 200 && !res.body.empty? | |
15 | return unless res.headers && res.headers['Content-Type'] =~ %r{\Atext/plain} | |
16 | ||
17 | NS::FantasticoFileslist.new(url, confidence: 70, found_by: found_by) | |
18 | end | |
19 | end | |
20 | end | |
21 | end | |
22 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | module InterestingFindings | |
3 | # Interesting Headers finder | |
4 | class Headers < Finder | |
5 | # @return [ InterestingFinding ] | |
6 | def passive(_opts = {}) | |
7 | r = NS::Headers.new(target.homepage_url, confidence: 100, found_by: found_by) | |
8 | ||
9 | r.interesting_entries.empty? ? nil : r | |
10 | end | |
11 | end | |
12 | end | |
13 | end | |
14 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | module InterestingFindings | |
3 | # Robots.txt finder | |
4 | class RobotsTxt < Finder | |
5 | # @return [ String ] The url of the robots.txt file | |
6 | def url | |
7 | target.url('robots.txt') | |
8 | end | |
9 | ||
10 | # @return [ InterestingFinding ] | |
11 | def aggressive(_opts = {}) | |
12 | res = NS::Browser.get(url) | |
13 | ||
14 | return unless res&.code == 200 && res.body =~ /(?:user-agent|(?:dis)?allow):/i | |
15 | ||
16 | NS::RobotsTxt.new(url, confidence: 100, found_by: found_by) | |
17 | end | |
18 | end | |
19 | end | |
20 | end | |
21 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | module InterestingFindings | |
3 | # SearchReplaceDB2 finder | |
4 | class SearchReplaceDB2 < Finder | |
5 | # @return [ String ] The url to the searchreplacedb2 PHP file | |
6 | def url | |
7 | target.url('searchreplacedb2.php') | |
8 | end | |
9 | ||
10 | # @return [ InterestingFinding ] | |
11 | def aggressive(_opts = {}) | |
12 | res = NS::Browser.get(url) | |
13 | ||
14 | return unless res&.code == 200 && res.body =~ /by interconnect/i | |
15 | ||
16 | NS::InterestingFinding.new(url, confidence: 100, | |
17 | found_by: found_by, | |
18 | references: references) | |
19 | end | |
20 | ||
21 | def references | |
22 | { url: 'https://interconnectit.com/products/search-and-replace-for-wordpress-databases/' } | |
23 | end | |
24 | end | |
25 | end | |
26 | end | |
27 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | module InterestingFindings | |
3 | # XML RPC finder | |
4 | class XMLRPC < Finder | |
5 | # @return [ Array<String> ] The potential urls to the XMl RPC file | |
6 | def potential_urls | |
7 | @potential_urls ||= [] | |
8 | end | |
9 | ||
10 | # @return [ Array<XMLRPC> ] | |
11 | def passive(opts = {}) | |
12 | [passive_headers(opts), passive_body(opts)].compact | |
13 | end | |
14 | ||
15 | # @return [ XMLRPC ] | |
16 | def passive_headers(_opts = {}) | |
17 | url = target.homepage_res.headers['X-Pingback'] | |
18 | ||
19 | return unless target.in_scope?(url) | |
20 | ||
21 | potential_urls << url | |
22 | ||
23 | NS::XMLRPC.new(url, confidence: 30, found_by: 'Headers (Passive Detection)') | |
24 | end | |
25 | ||
26 | # @return [ XMLRPC ] | |
27 | def passive_body(_opts = {}) | |
28 | target.homepage_res.html.css('link[rel="pingback"]').each do |tag| | |
29 | url = tag.attribute('href').to_s | |
30 | ||
31 | next unless target.in_scope?(url) | |
32 | ||
33 | potential_urls << url | |
34 | ||
35 | return NS::XMLRPC.new(url, confidence: 30, | |
36 | found_by: 'Link Tag (Passive Detection)') | |
37 | end | |
38 | nil | |
39 | end | |
40 | ||
41 | # @return [ XMLRPC ] | |
42 | def aggressive(_opts = {}) | |
43 | potential_urls << target.url('xmlrpc.php') | |
44 | ||
45 | potential_urls.uniq.each do |potential_url| | |
46 | next unless target.in_scope?(potential_url) | |
47 | ||
48 | res = NS::Browser.get(potential_url) | |
49 | ||
50 | next unless res&.body =~ /XML-RPC server accepts POST requests only/i | |
51 | ||
52 | return NS::XMLRPC.new(potential_url, | |
53 | confidence: 100, | |
54 | found_by: DIRECT_ACCESS) | |
55 | end | |
56 | nil | |
57 | end | |
58 | end | |
59 | end | |
60 | end | |
61 | end |
0 | require_relative 'interesting_findings/headers' | |
1 | require_relative 'interesting_findings/robots_txt' | |
2 | require_relative 'interesting_findings/fantastico_fileslist' | |
3 | require_relative 'interesting_findings/search_replace_db_2' | |
4 | require_relative 'interesting_findings/xml_rpc' | |
5 | ||
6 | module CMSScanner | |
7 | module Finders | |
8 | module InterestingFindings | |
9 | # Interesting Files Finder | |
10 | class Base | |
11 | include IndependentFinder | |
12 | ||
13 | # @param [ CMSScanner::Target ] target | |
14 | def initialize(target) | |
15 | %w[Headers RobotsTxt FantasticoFileslist SearchReplaceDB2 XMLRPC].each do |f| | |
16 | finders << NS::Finders::InterestingFindings.const_get(f).new(target) | |
17 | end | |
18 | end | |
19 | end | |
20 | end | |
21 | end | |
22 | end |
0 | require_relative 'finders/interesting_findings' |
0 | module CMSScanner | |
1 | module Formatter | |
2 | # CLI Formatter | |
3 | class Cli < Base | |
4 | # @return [ String ] | |
5 | def info_icon | |
6 | green('[+]') | |
7 | end | |
8 | ||
9 | # @return [ String ] | |
10 | def notice_icon | |
11 | blue('[i]') | |
12 | end | |
13 | ||
14 | # @return [ String ] | |
15 | def warning_icon | |
16 | amber('[!]') | |
17 | end | |
18 | ||
19 | # @return [ String ] | |
20 | def critical_icon | |
21 | red('[!]') | |
22 | end | |
23 | ||
24 | # @param [ String ] text | |
25 | # @return [ String ] | |
26 | def bold(text) | |
27 | colorize(text, 1) | |
28 | end | |
29 | ||
30 | # @param [ String ] text | |
31 | # @return [ String ] | |
32 | def red(text) | |
33 | colorize(text, 31) | |
34 | end | |
35 | ||
36 | # @param [ String ] text | |
37 | # @return [ String ] | |
38 | def green(text) | |
39 | colorize(text, 32) | |
40 | end | |
41 | ||
42 | # @param [ String ] text | |
43 | # @return [ String ] | |
44 | def amber(text) | |
45 | colorize(text, 33) | |
46 | end | |
47 | ||
48 | # @param [ String ] text | |
49 | # @return [ String ] | |
50 | def blue(text) | |
51 | colorize(text, 34) | |
52 | end | |
53 | ||
54 | # @param [ String ] text | |
55 | # @param [ Integer ] color_code | |
56 | # @return [ String ] | |
57 | def colorize(text, color_code) | |
58 | "\e[#{color_code}m#{text}\e[0m" | |
59 | end | |
60 | end | |
61 | end | |
62 | end |
0 | module CMSScanner | |
1 | module Formatter | |
2 | # Because Reason https://github.com/wpscanteam/CMSScanner/issues/56 | |
3 | class CliNoColor < CliNoColour | |
4 | end | |
5 | end | |
6 | end |
0 | module CMSScanner | |
1 | module Formatter | |
2 | # CLI No Colour Formatter | |
3 | class CliNoColour < Cli | |
4 | # Override to get the cli views | |
5 | def format | |
6 | 'cli' | |
7 | end | |
8 | ||
9 | def colorize(text, _color_code) | |
10 | text | |
11 | end | |
12 | end | |
13 | end | |
14 | end |
0 | module CMSScanner | |
1 | module Formatter | |
2 | # JSON Formatter | |
3 | class Json < Base | |
4 | include Buffer | |
5 | ||
6 | def beautify | |
7 | puts JSON.pretty_generate(JSON.parse("{#{buffer.chomp.chomp(',')}}")) | |
8 | end | |
9 | end | |
10 | end | |
11 | end |
0 | require_relative 'formatters/cli' | |
1 | require_relative 'formatters/cli_no_colour' | |
2 | require_relative 'formatters/cli_no_color' | |
3 | require_relative 'formatters/json' |
0 | module CMSScanner | |
1 | # FantasticoFileslist | |
2 | class FantasticoFileslist < InterestingFinding | |
3 | # @return [ Array<String> ] The interesting files/dirs detected | |
4 | def interesting_entries | |
5 | results = [] | |
6 | ||
7 | entries.each do |entry| | |
8 | next unless entry =~ /(?:admin|\.log|\.sql|\.db)/i | |
9 | ||
10 | results << entry | |
11 | end | |
12 | results | |
13 | end | |
14 | ||
15 | def references | |
16 | { url: ['http://www.acunetix.com/vulnerabilities/fantastico-fileslist/'] } | |
17 | end | |
18 | end | |
19 | end |
0 | module CMSScanner | |
1 | # Interesting Headers | |
2 | class Headers < InterestingFinding | |
3 | # @return [ Hash ] The headers | |
4 | def entries | |
5 | res = NS::Browser.get(url) | |
6 | return [] unless res&.headers | |
7 | ||
8 | res.headers | |
9 | end | |
10 | ||
11 | # @return [ Array<String> ] The interesting headers detected | |
12 | def interesting_entries | |
13 | results = [] | |
14 | ||
15 | entries.each do |header, value| | |
16 | next if known_headers.include?(header.downcase) | |
17 | ||
18 | results << "#{header}: #{[*value].join(', ')}" | |
19 | end | |
20 | results | |
21 | end | |
22 | ||
23 | # @return [ Array<String> ] Downcased known headers | |
24 | def known_headers | |
25 | %w[ | |
26 | age accept-ranges cache-control content-encoding content-length content-type connection date | |
27 | etag expires keep-alive location last-modified link pragma set-cookie strict-transport-security | |
28 | transfer-encoding vary x-cache x-content-security-policy x-content-type-options | |
29 | x-frame-options x-language x-permitted-cross-domain-policies x-pingback x-varnish | |
30 | x-webkit-csp x-xss-protection | |
31 | ] | |
32 | end | |
33 | end | |
34 | end |
0 | module CMSScanner | |
1 | # Interesting Finding | |
2 | class InterestingFinding | |
3 | include Finders::Finding | |
4 | ||
5 | attr_reader :url | |
6 | attr_writer :to_s | |
7 | ||
8 | # @param [ String ] url | |
9 | # @param [ Hash ] opts | |
10 | # :to_s (override the to_s method) | |
11 | # See Finders::Finding for other available options | |
12 | def initialize(url, opts = {}) | |
13 | @url = url | |
14 | @to_s = opts[:to_s] | |
15 | ||
16 | parse_finding_options(opts) | |
17 | end | |
18 | ||
19 | # @return [ Array<String> ] | |
20 | def entries | |
21 | res = NS::Browser.get(url) | |
22 | ||
23 | return [] unless res && res.headers['Content-Type'] =~ %r{\Atext/plain;}i | |
24 | ||
25 | res.body.split("\n").reject { |s| s.strip.empty? } | |
26 | end | |
27 | ||
28 | # @return [ String ] | |
29 | def to_s | |
30 | @to_s || url | |
31 | end | |
32 | ||
33 | # @return [ Boolean ] | |
34 | def ==(other) | |
35 | self.class == other.class && to_s == other.to_s | |
36 | end | |
37 | end | |
38 | end |
0 | module CMSScanner | |
1 | # Robots.txt | |
2 | class RobotsTxt < InterestingFinding | |
3 | # @todo Better detection, currently everything not empty or / is returned | |
4 | # | |
5 | # @return [ Array<String> ] The interesting Allow/Disallow rules detected | |
6 | def interesting_entries | |
7 | results = [] | |
8 | ||
9 | entries.each do |entry| | |
10 | next unless entry =~ /\A(?:dis)?allow:\s*(.+)\z/i | |
11 | ||
12 | match = Regexp.last_match(1) | |
13 | next if match == '/' | |
14 | ||
15 | results << match | |
16 | end | |
17 | ||
18 | results.uniq | |
19 | end | |
20 | end | |
21 | end |
0 | module CMSScanner | |
1 | # User | |
2 | class User | |
3 | include Finders::Finding | |
4 | ||
5 | attr_accessor :password | |
6 | attr_reader :id, :username | |
7 | ||
8 | # @param [ String ] username | |
9 | # @param [ Hash ] opts | |
10 | # @option opts [ Integer ] :id | |
11 | # @option opts [ String ] :password | |
12 | def initialize(username, opts = {}) | |
13 | @username = username | |
14 | @password = opts[:password] | |
15 | @id = opts[:id] | |
16 | ||
17 | parse_finding_options(opts) | |
18 | end | |
19 | ||
20 | def ==(other) | |
21 | return false unless self.class == other.class | |
22 | ||
23 | username == other.username | |
24 | end | |
25 | ||
26 | def to_s | |
27 | username | |
28 | end | |
29 | end | |
30 | end |
0 | module CMSScanner | |
1 | # Version | |
2 | class Version | |
3 | include Finders::Finding | |
4 | ||
5 | attr_reader :number | |
6 | ||
7 | def initialize(number, opts = {}) | |
8 | @number = number.to_s | |
9 | @number = "0#{number}" if @number[0, 1] == '.' | |
10 | ||
11 | parse_finding_options(opts) | |
12 | end | |
13 | ||
14 | # @param [ Version, String ] other | |
15 | # rubocop:disable Style/NumericPredicate | |
16 | def ==(other) | |
17 | (self <=> other) == 0 | |
18 | end | |
19 | # rubocop:enable all | |
20 | ||
21 | # @param [ Version, String ] other | |
22 | def <(other) | |
23 | (self <=> other) == -1 | |
24 | end | |
25 | ||
26 | # @param [ Version, String ] other | |
27 | def >(other) | |
28 | (self <=> other) == 1 | |
29 | end | |
30 | ||
31 | # @param [ Version, String ] other | |
32 | def <=>(other) | |
33 | other = self.class.new(other) unless other.is_a?(self.class) # handle potential '.1' version | |
34 | ||
35 | Gem::Version.new(number) <=> Gem::Version.new(other.number) | |
36 | rescue ArgumentError | |
37 | false | |
38 | end | |
39 | ||
40 | def to_s | |
41 | number | |
42 | end | |
43 | end | |
44 | end |
0 | module CMSScanner | |
1 | # XML RPC | |
2 | class XMLRPC < InterestingFinding | |
3 | # @return [ Browser ] | |
4 | def browser | |
5 | @browser ||= NS::Browser.instance | |
6 | end | |
7 | ||
8 | # @return [ Array<String> ] | |
9 | def available_methods | |
10 | return @available_methods if @available_methods | |
11 | ||
12 | @available_methods = [] | |
13 | ||
14 | res = method_call('system.listMethods').run | |
15 | doc = Nokogiri::XML.parse(res.body) | |
16 | ||
17 | doc.search('methodResponse params param value array data value string').each do |s| | |
18 | @available_methods << s.text | |
19 | end | |
20 | ||
21 | @available_methods | |
22 | end | |
23 | ||
24 | # @return [ Boolean ] Whether or not the XMLRPC is enabled | |
25 | def enabled? | |
26 | !available_methods.empty? | |
27 | end | |
28 | ||
29 | # @param [ String ] method_name | |
30 | # @param [ Array ] method_params | |
31 | # @param [ Hash ] request_params | |
32 | # | |
33 | # @return [ Typhoeus::Request ] | |
34 | def method_call(method_name, method_params = [], request_params = {}) | |
35 | browser.forge_request( | |
36 | url, | |
37 | request_params.merge( | |
38 | method: :post, | |
39 | body: ::XMLRPC::Create.new.methodCall(method_name, *method_params) | |
40 | ) | |
41 | ) | |
42 | end | |
43 | ||
44 | # @param [ Array<Array> ] methods_and_params | |
45 | # @param [ Hash ] request_params | |
46 | # | |
47 | # Example of methods_and_params: | |
48 | # [ | |
49 | # [method1, param1, param2], | |
50 | # [method2, param1], | |
51 | # [method3] | |
52 | # ] | |
53 | # | |
54 | # @return [ Typhoeus::Request ] | |
55 | def multi_call(methods_and_params = [], request_params = {}) | |
56 | browser.forge_request( | |
57 | url, | |
58 | request_params.merge( | |
59 | method: :post, | |
60 | body: ::XMLRPC::Create.new.methodCall( | |
61 | 'system.multicall', | |
62 | methods_and_params.collect { |m| { methodName: m[0], params: m[1..-1] } } | |
63 | ) | |
64 | ) | |
65 | ) | |
66 | end | |
67 | end | |
68 | end |
0 | require_relative 'models/interesting_finding' | |
1 | require_relative 'models/robots_txt' | |
2 | require_relative 'models/fantastico_fileslist' | |
3 | require_relative 'models/headers' | |
4 | require_relative 'models/xml_rpc' | |
5 | require_relative 'models/version' | |
6 | require_relative 'models/user' |
0 | # Windows | |
1 | Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5 | |
2 | Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14 | |
3 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27 | |
4 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1 | |
5 | Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E) | |
6 | Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 | |
7 | Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 | |
8 | Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 | |
9 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6 | |
10 | Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1 | |
11 | Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0 | |
12 | Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1 | |
13 | Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0) | |
14 | Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) | |
15 | Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0) | |
16 | Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00 | |
17 | Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 | |
18 | ||
19 | # MAC | |
20 | Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13 | |
21 | Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.5; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 | |
22 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 | |
23 | Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3 | |
24 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3 | |
25 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2; rv:10.0.1) Gecko/20100101 Firefox/10.0.1 | |
26 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10 | |
27 | ||
28 | # Linux | |
29 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1 | |
30 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.703.0 Chrome/12.0.703.0 Safari/534.24 | |
31 | Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.9) Gecko/20100915 Gentoo Firefox/3.6.9 | |
32 | Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Gecko Firefox/11.0 | |
33 | Mozilla/5.0 (X11; Linux i686; rv:12.0) Gecko/20100101 Firefox/12.0 | |
34 | Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00 | |
35 | Mozilla/5.0 (X11; U; Linux x86_64; us; rv:1.9.1.19) Gecko/20110430 shadowfox/7.0 (like Firefox/7.0 | |
36 | ||
37 | # iPad | |
38 | Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53 | |
39 | Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53 | |
40 | Mozilla/5.0 (iPad; CPU OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25 | |
41 | ||
42 | # iPhone | |
43 | Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 | |
44 | Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53 | |
45 | Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53 |
0 | <% # Empty file, the banner should be implemented in each scanner %>⏎ |
0 | <%= info_icon %> Finished: <%= @stop_time.asctime %> | |
1 | <%= info_icon %> Requests Done: <%= @requests_done %> | |
2 | <%= info_icon %> Memory used: <%= @used_memory.bytes_to_human %> | |
3 | <%= info_icon %> Elapsed time: <%= Time.at(@elapsed).utc.strftime('%H:%M:%S') %> |
0 | <%= @help %> |
0 | <%= info_icon %> URL: <%= @url %> | |
1 | <% if @url != @effective_url -%> | |
2 | <%= info_icon %> Effective URL: <%= @effective_url %> | |
3 | <% end -%> | |
4 | <%= info_icon %> Started: <%= @start_time.asctime %> | |
5 |
0 | Version: <%= NS::VERSION %> |
0 | <% unless @a.empty? -%> | |
1 | <% if @a.size == 1 -%> | |
2 | | <%= @s %>: <%= @a.first %> | |
3 | <% else -%> | |
4 | | <%= @p %>: | |
5 | <% @a.each do |line| -%> | |
6 | | - <%= line %> | |
7 | <% end -%> | |
8 | <% end -%> | |
9 | <% end -%>⏎ |
0 | <% unless @findings.empty? -%> | |
1 | Interesting Finding(s): | |
2 | <% @findings.each do |finding| -%> | |
3 | ||
4 | <%= info_icon %> <%= finding %> | |
5 | <%= render('_array', a: finding.interesting_entries, s: 'Interesting Entry', p: 'Interesting Entries') -%> | |
6 | | Found By: <%= finding.found_by %> | |
7 | <% if finding.confidence > 0 -%> | |
8 | | Confidence: <%= finding.confidence %>% | |
9 | <% end -%> | |
10 | <% unless (confirmed = finding.confirmed_by).empty? -%> | |
11 | <% if confirmed.size == 1 -%> | |
12 | | Confirmed By: <%= confirmed.first.found_by %><% if confirmed.first.confidence > 0 %>, <%= confirmed.first.confidence %>% confidence<% end %> | |
13 | <% else -%> | |
14 | | Confirmed By: | |
15 | <% confirmed.each do |c| -%> | |
16 | | - <%= c.found_by %><% if c.confidence > 0 %>, <%= c.confidence %>% confidence<% end %> | |
17 | <% end -%> | |
18 | <% end -%> | |
19 | <% end -%> | |
20 | <%= render('_array', a: finding.references_urls, s: 'Reference', p: 'References') -%> | |
21 | <% end -%> | |
22 | <% end %> |
0 | <% # Empty file, the banner should be implemented in each scanner %>⏎ |
0 | "stop_time": <%= @stop_time.to_i %>, | |
1 | "elapsed": <%= @elapsed.to_i %>, | |
2 | "requests_done": <%= @requests_done.to_i %>, | |
3 | "used_memory": <%= @used_memory.to_i %>,⏎ |
0 | "help": <%= @help.to_s.to_json %>,⏎ |
0 | "start_time": <%= @start_time.to_i %>, | |
1 | "start_memory": <%= @start_memory.to_i %>, | |
2 | "target_url": <%= @url.to_s.to_json %>, | |
3 | "effective_url": <%= @effective_url.to_s.to_json %>,⏎ |
0 | "version": <%= NS::VERSION.to_s.to_json %>,⏎ |
0 | "interesting_findings": [ | |
1 | <% unless @findings.empty? -%> | |
2 | <% last_index = @findings.size - 1 %> | |
3 | <% @findings.each.with_index do |finding, index| -%> | |
4 | { | |
5 | "url": <%= finding.url.to_s.to_json %>, | |
6 | "to_s": <%= finding.to_s.to_json %>, | |
7 | "found_by": <%= finding.found_by.to_s.to_json %>, | |
8 | "confidence": <%= finding.confidence.to_json %>, | |
9 | "confirmed_by": { | |
10 | <% unless (confirmed = finding.confirmed_by).empty? -%> | |
11 | <% c_last_index = confirmed.size - 1 %> | |
12 | <% confirmed.each.with_index do |c, i| -%> | |
13 | <%= c.found_by.to_s.to_json %>: { "confidence": <%= c.confidence.to_json %> }<% unless i == c_last_index %>,<% end %> | |
14 | <% end -%> | |
15 | <% end -%> | |
16 | }, | |
17 | "references": <%= finding.references.to_json %>, | |
18 | "interesting_entries": <%= finding.interesting_entries.to_json %> | |
19 | }<% unless index == last_index %>,<% end %> | |
20 | <% end -%> | |
21 | <% end -%> | |
22 | ],⏎ |
0 | "scan_aborted": <%= @reason.to_json %>, | |
1 | <% if @verbose -%> | |
2 | "trace": <%= @trace.to_json %>, | |
3 | <% end %>⏎ |
0 | lib = File.expand_path('../lib', __FILE__) | |
1 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) | |
2 | ||
3 | require 'cms_scanner/version' | |
4 | ||
5 | Gem::Specification.new do |s| | |
6 | s.name = 'cms_scanner' | |
7 | s.version = CMSScanner::VERSION | |
8 | s.platform = Gem::Platform::RUBY | |
9 | s.required_ruby_version = '>= 2.3' | |
10 | s.authors = ['WPScanTeam'] | |
11 | s.email = ['[email protected]'] | |
12 | s.summary = 'CMS Scanner Framework (experimental)' | |
13 | s.description = 'Framework to provide an easy way to implement CMS Scanners' | |
14 | s.homepage = 'https://github.com/wpscanteam/CMSScanner' | |
15 | s.license = 'MIT' | |
16 | ||
17 | s.files = `git ls-files -z`.split("\x0").reject do |file| | |
18 | file =~ %r{^(?: | |
19 | spec\/.* | |
20 | |Gemfile | |
21 | |Rakefile | |
22 | |\.rspec | |
23 | |\.gitignore | |
24 | |\.rubocop.yml | |
25 | |\.travis.yml | |
26 | )$}x | |
27 | end | |
28 | ||
29 | s.test_files = [] | |
30 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } | |
31 | s.require_path = 'lib' | |
32 | ||
33 | s.add_dependency 'nokogiri', '~> 1.8.0' | |
34 | s.add_dependency 'opt_parse_validator', '~> 0.0.16.2' | |
35 | s.add_dependency 'public_suffix', '~> 3.0.0' | |
36 | s.add_dependency 'ruby-progressbar', '~> 1.10.0' | |
37 | s.add_dependency 'typhoeus', '~> 1.3.0' | |
38 | s.add_dependency 'xmlrpc', '~> 0.3' | |
39 | s.add_dependency 'yajl-ruby', '~> 1.4.1' # Better JSON parser regarding memory usage | |
40 | ||
41 | # Already required by opt_parse_validator | |
42 | # so version restriction loosen to avoid potential future conflicts | |
43 | s.add_dependency 'activesupport', '~> 5.2' | |
44 | s.add_dependency 'addressable', '~> 2.5' | |
45 | ||
46 | s.add_development_dependency 'bundler', '~> 1.6' | |
47 | s.add_development_dependency 'coveralls', '~> 0.8.0' | |
48 | s.add_development_dependency 'rake', '~> 12.3' | |
49 | s.add_development_dependency 'rspec', '~> 3.8.0' | |
50 | s.add_development_dependency 'rspec-its', '~> 1.2.0' | |
51 | s.add_development_dependency 'rubocop', '~> 0.59.1' | |
52 | s.add_development_dependency 'simplecov', '~> 0.16.1' | |
53 | s.add_development_dependency 'webmock', '~> 3.4.2' | |
54 | end |
0 | *.gem | |
1 | *.rbc | |
2 | .bundle | |
3 | .config | |
4 | coverage | |
5 | pkg | |
6 | rdoc | |
7 | Gemfile.lock | |
8 | ||
9 | # YARD artifacts | |
10 | .yardoc | |
11 | _yardoc | |
12 | doc/ |
0 | AllCops: | |
1 | Exclude: | |
2 | - '*.gemspec' | |
3 | - 'vendor/**/*' | |
4 | LineLength: | |
5 | Max: 120 | |
6 | ClassVars: | |
7 | Enabled: false | |
8 | Style/RescueModifier: | |
9 | Enabled: false | |
10 | Style/SignalException: | |
11 | EnforcedStyle: semantic | |
12 | MethodLength: | |
13 | Max: 17 | |
14 | Metrics/AbcSize: | |
15 | Max: 25 | |
16 | Metrics/CyclomaticComplexity: | |
17 | Max: 10 | |
18 | Metrics/PerceivedComplexity: | |
19 | Max: 9 |
0 | language: ruby | |
1 | sudo: false | |
2 | cache: bundler | |
3 | rvm: | |
4 | - 2.3.0 | |
5 | - 2.3.1 | |
6 | - 2.3.2 | |
7 | - 2.3.3 | |
8 | - 2.3.4 | |
9 | - 2.4.0 | |
10 | - 2.4.1 | |
11 | - ruby-head | |
12 | before_install: | |
13 | - "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc" | |
14 | - "gem update --system" | |
15 | matrix: | |
16 | allow_failures: | |
17 | - rvm: ruby-head | |
18 | script: | |
19 | - bundle exec rspec | |
20 | - bundle exec rubocop |
0 | require 'bundler/gem_tasks' | |
1 | require 'rspec/core/rake_task' | |
2 | require 'rubocop/rake_task' | |
3 | ||
4 | RuboCop::RakeTask.new | |
5 | RSpec::Core::RakeTask.new(:spec) | |
6 | ||
7 | # Run rubocop & rspec before the build | |
8 | task build: %i[rubocop spec] |
0 | require_relative 'controllers' |
0 | module CMSScan | |
1 | module Controller | |
2 | # Example Controller | |
3 | class Example < CMSScanner::Controller::Core | |
4 | # @return [ Array<OptParseValidator::Opt> ] | |
5 | def cli_options | |
6 | [ | |
7 | OptString.new(['--dummy VALUE', 'Dummy CLI Option']) | |
8 | ] | |
9 | end | |
10 | ||
11 | def before_scan | |
12 | # Anything to do before ? | |
13 | end | |
14 | ||
15 | def run | |
16 | # Let's check and display whether or not the word 'scan' is present in the homepage of the target | |
17 | ||
18 | is_present = target.homepage_res.body =~ /scan/ ? true : false | |
19 | ||
20 | output('scan_word', is_present: is_present) | |
21 | end | |
22 | ||
23 | # Alternative way of doing it | |
24 | def run2 | |
25 | @is_present = Browser.get(target.homepage_url).body =~ /scan/ ? true : false | |
26 | ||
27 | output('scan_word') | |
28 | end | |
29 | ||
30 | def after_scan | |
31 | # Anything after ? | |
32 | end | |
33 | end | |
34 | end | |
35 | end |
0 | require_relative 'controllers/example' |
0 | <% if @is_present -%> | |
1 | <%= warning_icon %> The word 'scan' is present in the homepage | |
2 | <% else -%> | |
3 | <%= notice_icon %> The word 'scan' was not detected in the homepage | |
4 | <% end %> |
0 | "scan_word_present": <%= @is_present.to_json %>,⏎ |
0 | #!/usr/bin/env ruby | |
1 | ||
2 | require 'cmsscan' | |
3 | ||
4 | CMSScan::Scan.new do |s| | |
5 | s.controllers << CMSScan::Controller::Example.new | |
6 | ||
7 | s.run | |
8 | end |
0 | # coding: utf-8 | |
1 | ||
2 | lib = File.expand_path('../lib', __FILE__) | |
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) | |
4 | ||
5 | require 'cmsscan/version' | |
6 | ||
7 | Gem::Specification.new do |s| | |
8 | s.name = 'cmsscan' | |
9 | s.version = CMSScan::VERSION | |
10 | s.platform = Gem::Platform::RUBY | |
11 | s.required_ruby_version = '>= 2.1.0' | |
12 | s.authors = ['WPScanTeam'] | |
13 | s.date = Time.now.utc.strftime('%Y-%m-%d') | |
14 | s.email = ['[email protected]'] | |
15 | s.summary = 'CMSScan Gem Example' | |
16 | s.description = 'CMSScanner Implementation Example' | |
17 | s.homepage = 'https://github.com/wpscanteam/CMSScanner' | |
18 | s.license = 'MIT' | |
19 | ||
20 | s.files = `git ls-files -z`.split("\x0").reject do |file| | |
21 | file =~ %r{^(?: | |
22 | spec\/.* | |
23 | |Gemfile | |
24 | |Rakefile | |
25 | |\.rspec | |
26 | |\.gitignore | |
27 | |\.rubocop.yml | |
28 | |\.travis.yml | |
29 | )$}x | |
30 | end | |
31 | ||
32 | s.test_files = [] | |
33 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } | |
34 | s.require_path = 'lib' | |
35 | ||
36 | s.add_dependency 'cms_scanner', '~> 0.0.39.0' | |
37 | ||
38 | # Already required by CMSScanner, so version restrictions loosen | |
39 | s.add_dependency 'activesupport', '~> 5.1' | |
40 | s.add_dependency 'yajl-ruby', '~> 1.3' | |
41 | ||
42 | s.add_development_dependency 'bundler', '~> 1.6' | |
43 | s.add_development_dependency 'coveralls', '~> 0.8.0' | |
44 | s.add_development_dependency 'rake', '~> 12.0' | |
45 | s.add_development_dependency 'rspec', '~> 3.7.0' | |
46 | s.add_development_dependency 'rspec-its', '~> 1.2.0' | |
47 | s.add_development_dependency 'rubocop', '~> 0.52.0' | |
48 | s.add_development_dependency 'simplecov', '~> 0.14.0' # Can't update to 0.15 as it breaks coveralls dep | |
49 | s.add_development_dependency 'webmock', '~> 3.3.0' | |
50 | end |
0 | module CMSScan | |
1 | # Needed to load at least the Core controller | |
2 | # Otherwise, the following error will be raised: | |
3 | # `initialize': uninitialized constant CMSScan::Controller::Core (NameError) | |
4 | module Controller | |
5 | include CMSScanner::Controller | |
6 | end | |
7 | end |
0 | module CMSScan | |
1 | # Custom Target Class | |
2 | class Target < CMSScanner::Target | |
3 | # Put your own methods there | |
4 | end | |
5 | end |
0 | # Gems | |
1 | require 'cms_scanner' | |
2 | require 'yajl/json_gem' | |
3 | require 'addressable/uri' | |
4 | require 'active_support/all' | |
5 | # Standard Lib | |
6 | require 'uri' | |
7 | require 'time' | |
8 | require 'readline' | |
9 | require 'securerandom' | |
10 | # Custom Libs | |
11 | require 'cmsscan/target' | |
12 | require 'cmsscan/version' | |
13 | require 'cmsscan/controller' | |
14 | ||
15 | Encoding.default_external = Encoding::UTF_8 | |
16 | ||
17 | # CMSScan | |
18 | module CMSScan | |
19 | include CMSScanner | |
20 | ||
21 | APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path | |
22 | # Not needed in this example | |
23 | # DB_DIR = File.join(Dir.home, '.cmsscan', 'db') | |
24 | ||
25 | # Override, otherwise it would be returned as 'cms_scan' | |
26 | # doesn't really matter in this example. | |
27 | # | |
28 | # @return [ String ] | |
29 | def self.app_name | |
30 | 'cmsscan' | |
31 | end | |
32 | end | |
33 | ||
34 | require "#{CMSScan::APP_DIR}/app" |
0 | module CMSScanner | |
1 | class Browser | |
2 | # Browser Actions (get, post etc) | |
3 | module Actions | |
4 | # @param [ String ] url | |
5 | # @param [ Hash ] params | |
6 | # | |
7 | # @return [ Typhoeus::Response ] | |
8 | def get(url, params = {}) | |
9 | process(url, params.merge(method: :get)) | |
10 | end | |
11 | ||
12 | # @param [ String ] url | |
13 | # @param [ Hash ] params | |
14 | # | |
15 | # @return [ Typhoeus::Response ] | |
16 | def post(url, params = {}) | |
17 | process(url, params.merge(method: :post)) | |
18 | end | |
19 | ||
20 | # @param [ String ] url | |
21 | # @param [ Hash ] params | |
22 | # | |
23 | # @return [ Typhoeus::Response ] | |
24 | def head(url, params = {}) | |
25 | process(url, params.merge(method: :head)) | |
26 | end | |
27 | ||
28 | # @param [ String ] url | |
29 | # @param [ Hash ] params | |
30 | # | |
31 | # @return [ Typhoeus::Response ] | |
32 | def get_and_follow_location(url, params = {}) | |
33 | get(url, params.merge(followlocation: true)) | |
34 | end | |
35 | ||
36 | protected | |
37 | ||
38 | # @param [ String ] url | |
39 | # @param [ Hash ] params | |
40 | # | |
41 | # @return [ Typhoeus::Response ] | |
42 | def process(url, params) | |
43 | Typhoeus::Request.new(url, NS::Browser.instance.request_params(params)).run | |
44 | end | |
45 | end | |
46 | end | |
47 | end |
0 | module CMSScanner | |
1 | # Options available in the Browser | |
2 | class Browser | |
3 | OPTIONS = %i[ | |
4 | cache_ttl | |
5 | cookie_jar | |
6 | cookie_string | |
7 | connect_timeout | |
8 | disable_tls_checks | |
9 | headers | |
10 | http_auth | |
11 | max_threads | |
12 | proxy | |
13 | proxy_auth | |
14 | random_user_agent | |
15 | request_timeout | |
16 | throttle | |
17 | user_agent | |
18 | user_agents_list | |
19 | vhost | |
20 | ].freeze | |
21 | ||
22 | attr_accessor(*OPTIONS) | |
23 | ||
24 | # @return [ String ] | |
25 | def default_user_agent | |
26 | "#{NS} v#{NS::VERSION}" | |
27 | end | |
28 | ||
29 | def hydra | |
30 | @hydra ||= Typhoeus::Hydra.new(max_concurrency: max_threads || 1) | |
31 | end | |
32 | ||
33 | # @param [ Hash ] options | |
34 | def load_options(options = {}) | |
35 | OPTIONS.each do |sym| | |
36 | send("#{sym}=", options[sym]) if options.key?(sym) | |
37 | end | |
38 | end | |
39 | ||
40 | # Set the threads attribute and update | |
41 | # the max_concurrency of Typhoeus::Hydra | |
42 | # | |
43 | # If the throttle attribute is > 0, max_threads will be forced to 1 | |
44 | # | |
45 | # @param [ Integer ] number | |
46 | def max_threads=(number) | |
47 | @max_threads = number.to_i.positive? && throttle.zero? ? number.to_i : 1 | |
48 | ||
49 | hydra.max_concurrency = @max_threads | |
50 | end | |
51 | ||
52 | # @return [ String ] The user agent | |
53 | def user_agent | |
54 | @user_agent ||= random_user_agent ? user_agents.sample : default_user_agent | |
55 | end | |
56 | ||
57 | # @return [ Array<String> ] | |
58 | def user_agents | |
59 | return @user_agents if @user_agents | |
60 | ||
61 | @user_agents = [] | |
62 | ||
63 | File.open(user_agents_list).each do |line| | |
64 | next if line == "\n" || line[0, 1] == '#' | |
65 | ||
66 | @user_agents << line.chomp | |
67 | end | |
68 | ||
69 | @user_agents | |
70 | end | |
71 | ||
72 | # @return [ String ] The path to the user agents list | |
73 | def user_agents_list | |
74 | @user_agents_list ||= File.join(APP_DIR, 'user_agents.txt') | |
75 | end | |
76 | ||
77 | # @param [ value ] The throttle time in milliseconds | |
78 | # | |
79 | # if value > 0, the max_threads will be set to 1 | |
80 | def throttle=(value) | |
81 | @throttle = value.to_i.abs / 1000.0 | |
82 | ||
83 | self.max_threads = 1 if @throttle.positive? | |
84 | end | |
85 | ||
86 | def trottle! | |
87 | sleep(throttle) if throttle.positive? | |
88 | end | |
89 | end | |
90 | end |
0 | require 'cms_scanner/browser/actions' | |
1 | require 'cms_scanner/browser/options' | |
2 | ||
3 | module CMSScanner | |
4 | # Singleton used to perform HTTP/HTTPS request to the target | |
5 | class Browser | |
6 | extend Actions | |
7 | ||
8 | # @param [ Hash ] parsed_options | |
9 | # | |
10 | # @return [ Void ] | |
11 | def initialize(parsed_options = {}) | |
12 | self.throttle = 0 | |
13 | ||
14 | load_options(parsed_options) | |
15 | end | |
16 | ||
17 | private_class_method :new | |
18 | ||
19 | # @param [ Hash ] parsed_options | |
20 | # | |
21 | # @return [ Browser ] The instance | |
22 | def self.instance(parsed_options = {}) | |
23 | @@instance ||= new(parsed_options) | |
24 | end | |
25 | ||
26 | def self.reset | |
27 | @@instance = nil | |
28 | end | |
29 | ||
30 | # @param [ String ] url | |
31 | # @param [ Hash ] params | |
32 | # | |
33 | # @return [ Typhoeus::Request ] | |
34 | def forge_request(url, params = {}) | |
35 | Typhoeus::Request.new(url, request_params(params)) | |
36 | end | |
37 | ||
38 | # @return [ Hash ] | |
39 | def typhoeus_to_browser_opts | |
40 | { connecttimeout: :connect_timeout, cache_ttl: :cache_ttl, | |
41 | proxy: :proxy, timeout: :request_timeout, cookiejar: :cookie_jar, | |
42 | cookiefile: :cookie_jar, cookie: :cookie_string } | |
43 | end | |
44 | ||
45 | # @return [ Hash ] | |
46 | def default_request_params | |
47 | params = { | |
48 | headers: { 'User-Agent' => user_agent }.merge(headers || {}), | |
49 | accept_encoding: 'gzip, deflate', | |
50 | method: :get | |
51 | } | |
52 | ||
53 | if disable_tls_checks | |
54 | # See http://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html | |
55 | params[:ssl_verifypeer] = false | |
56 | params[:ssl_verifyhost] = 0 | |
57 | end | |
58 | ||
59 | typhoeus_to_browser_opts.each do |typhoeus_opt, browser_opt| | |
60 | attr_value = public_send(browser_opt) | |
61 | params[typhoeus_opt] = attr_value unless attr_value.nil? | |
62 | end | |
63 | ||
64 | params[:proxyuserpwd] = "#{proxy_auth[:username]}:#{proxy_auth[:password]}" if proxy_auth | |
65 | params[:userpwd] = "#{http_auth[:username]}:#{http_auth[:password]}" if http_auth | |
66 | ||
67 | params[:headers]['Host'] = vhost if vhost | |
68 | ||
69 | params | |
70 | end | |
71 | ||
72 | # @param [ Hash ] params | |
73 | # | |
74 | # @return [ Hash ] | |
75 | def request_params(params = {}) | |
76 | default_request_params.merge(params) do |key, oldval, newval| | |
77 | key == :headers ? oldval.merge(newval) : newval | |
78 | end | |
79 | end | |
80 | end | |
81 | end |
0 | module CMSScanner | |
1 | module Cache | |
2 | # Cache Implementation using files | |
3 | class FileStore | |
4 | attr_reader :storage_path, :serializer | |
5 | ||
6 | # The serializer must have the 2 methods #load and #dump | |
7 | # (Marshal and YAML have them) | |
8 | # YAML is Human Readable, contrary to Marshal which store in a binary format | |
9 | # Marshal does not need any "require" | |
10 | # | |
11 | # @param [ String ] storage_path | |
12 | # @param [ Constant ] serializer | |
13 | def initialize(storage_path, serializer = Marshal) | |
14 | @storage_path = File.expand_path(storage_path) | |
15 | @serializer = serializer | |
16 | ||
17 | FileUtils.mkdir_p(@storage_path) unless Dir.exist?(@storage_path) | |
18 | end | |
19 | ||
20 | # TODO: rename this to clear ? | |
21 | def clean | |
22 | Dir[File.join(storage_path, '*')].each do |f| | |
23 | File.delete(f) unless File.symlink?(f) | |
24 | end | |
25 | end | |
26 | ||
27 | # @param [ String ] key | |
28 | # | |
29 | # @return [ Mixed ] | |
30 | def read_entry(key) | |
31 | return if expired_entry?(key) | |
32 | ||
33 | serializer.load(File.read(entry_path(key))) | |
34 | rescue StandardError | |
35 | nil | |
36 | end | |
37 | ||
38 | # @param [ String ] key | |
39 | # @param [ Mixed ] data_to_store | |
40 | # @param [ Integer ] cache_ttl | |
41 | def write_entry(key, data_to_store, cache_ttl) | |
42 | return unless cache_ttl.to_i.positive? | |
43 | ||
44 | File.write(entry_path(key), serializer.dump(data_to_store)) | |
45 | File.write(entry_expiration_path(key), Time.now.to_i + cache_ttl) | |
46 | end | |
47 | ||
48 | # @param [ String ] key | |
49 | # | |
50 | # @return [ String ] The file path associated to the key | |
51 | def entry_path(key) | |
52 | File.join(storage_path, key) | |
53 | end | |
54 | ||
55 | # @param [ String ] key | |
56 | # | |
57 | # @return [ String ] The expiration file path associated to the key | |
58 | def entry_expiration_path(key) | |
59 | entry_path(key) + '.expiration' | |
60 | end | |
61 | ||
62 | private | |
63 | ||
64 | # @param [ String ] key | |
65 | # | |
66 | # @return [ Boolean ] | |
67 | def expired_entry?(key) | |
68 | File.read(entry_expiration_path(key)).to_i <= Time.now.to_i | |
69 | rescue StandardError | |
70 | true | |
71 | end | |
72 | end | |
73 | end | |
74 | end |
0 | require 'cms_scanner/cache/file_store' | |
1 | ||
2 | module CMSScanner | |
3 | module Cache | |
4 | # Cache implementation for Typhoeus | |
5 | class Typhoeus < FileStore | |
6 | # @param [ Typhoeus::Request ] request | |
7 | # | |
8 | # @return [ Typhoeus::Response ] | |
9 | def get(request) | |
10 | read_entry(request.hash.to_s) | |
11 | end | |
12 | ||
13 | # @param [ Typhoeus::Request ] request | |
14 | # @param [ Typhoeus::Response ] response | |
15 | def set(request, response) | |
16 | write_entry(request.hash.to_s, response, request.cache_ttl) | |
17 | end | |
18 | end | |
19 | end | |
20 | end |
0 | module CMSScanner | |
1 | module Controller | |
2 | # Base Controller | |
3 | class Base | |
4 | include OptParseValidator | |
5 | ||
6 | # @return [ Array<OptParseValidator::OptBase> ] | |
7 | def cli_options; end | |
8 | ||
9 | def before_scan; end | |
10 | ||
11 | def run; end | |
12 | ||
13 | def after_scan; end | |
14 | ||
15 | def ==(other) | |
16 | self.class == other.class | |
17 | end | |
18 | ||
19 | # Reset all the class attibutes | |
20 | # Currently only used in specs | |
21 | def self.reset | |
22 | @@target = nil | |
23 | @@parsed_options = nil | |
24 | @@datastore = nil | |
25 | @@formatter = nil | |
26 | end | |
27 | ||
28 | # @return [ Target ] | |
29 | def target | |
30 | @@target ||= NS::Target.new(parsed_options[:url], parsed_options) | |
31 | end | |
32 | ||
33 | # @param [ OptParsevalidator::OptParser ] parser | |
34 | def self.option_parser=(parser) | |
35 | @@option_parser = parser | |
36 | end | |
37 | ||
38 | # @return [ OptParsevalidator::OptParser ] | |
39 | def option_parser | |
40 | @@option_parser | |
41 | end | |
42 | ||
43 | # Set the parsed options and initialize the browser | |
44 | # with them | |
45 | # | |
46 | # @param [ Hash ] options | |
47 | def self.parsed_options=(options) | |
48 | @@parsed_options = options | |
49 | ||
50 | NS::Browser.instance(options) | |
51 | end | |
52 | ||
53 | # @return [ Hash ] | |
54 | def parsed_options | |
55 | @@parsed_options ||= {} | |
56 | end | |
57 | ||
58 | # @return [ Hash ] | |
59 | def datastore | |
60 | @@datastore ||= {} | |
61 | end | |
62 | ||
63 | # @return [ Formatter::Base ] | |
64 | def formatter | |
65 | @@formatter ||= NS::Formatter.load(parsed_options[:format], datastore[:views]) | |
66 | end | |
67 | ||
68 | # @see Formatter#output | |
69 | # | |
70 | # @return [ Void ] | |
71 | def output(tpl, vars = {}) | |
72 | formatter.output(*tpl_params(tpl, vars)) | |
73 | end | |
74 | ||
75 | # @see Formatter#render | |
76 | # | |
77 | # @return [ String ] | |
78 | def render(tpl, vars = {}) | |
79 | formatter.render(*tpl_params(tpl, vars)) | |
80 | end | |
81 | ||
82 | # @return [ Boolean ] | |
83 | def user_interaction? | |
84 | formatter.user_interaction? && !parsed_options[:output] | |
85 | end | |
86 | ||
87 | # @return [ String ] | |
88 | def tmp_directory | |
89 | File.join('/tmp', NS.app_name) | |
90 | end | |
91 | ||
92 | protected | |
93 | ||
94 | # @param [ String ] tpl | |
95 | # @param [ Hash ] vars | |
96 | # | |
97 | # @return [ Array<String> ] | |
98 | def tpl_params(tpl, vars) | |
99 | [ | |
100 | tpl, | |
101 | instance_variable_values.merge(vars), | |
102 | self.class.name.demodulize.underscore | |
103 | ] | |
104 | end | |
105 | ||
106 | # @return [ Hash ] All the instance variable keys (and their values) and the verbose value | |
107 | def instance_variable_values | |
108 | h = { verbose: parsed_options[:verbose] } | |
109 | instance_variables.each do |a| | |
110 | s = a.to_s | |
111 | n = s[1..s.size] | |
112 | h[n.to_sym] = instance_variable_get(a) | |
113 | end | |
114 | h | |
115 | end | |
116 | end | |
117 | end | |
118 | end |
0 | module CMSScanner | |
1 | # Controllers Container | |
2 | class Controllers < Array | |
3 | attr_reader :option_parser | |
4 | ||
5 | # @param [ OptParsevalidator::OptParser ] options_parser | |
6 | def initialize(option_parser = OptParseValidator::OptParser.new(nil, 40)) | |
7 | @option_parser = option_parser | |
8 | ||
9 | register_options_files | |
10 | end | |
11 | ||
12 | # Adds the potential option file paths to the option_parser | |
13 | def register_options_files | |
14 | [Dir.home, Dir.pwd].each do |dir| | |
15 | option_parser.options_files.supported_extensions.each do |ext| | |
16 | @option_parser.options_files << Pathname.new(dir).join(".#{NS.app_name}", "cli_options.#{ext}").to_s | |
17 | end | |
18 | end | |
19 | end | |
20 | ||
21 | # @param [ Controller::Base ] controller | |
22 | # | |
23 | # @retun [ Controllers ] self | |
24 | def <<(controller) | |
25 | options = controller.cli_options | |
26 | ||
27 | unless include?(controller) | |
28 | option_parser.add(*options) if options | |
29 | super(controller) | |
30 | end | |
31 | self | |
32 | end | |
33 | ||
34 | def run | |
35 | parsed_options = option_parser.results | |
36 | first.class.option_parser = option_parser | |
37 | first.class.parsed_options = parsed_options | |
38 | ||
39 | redirect_output_to_file(parsed_options[:output]) if parsed_options[:output] | |
40 | ||
41 | each(&:before_scan) | |
42 | each(&:run) | |
43 | # Reverse is used here as the app/controllers/core#after_scan finishes the output | |
44 | # and must be the last one to be executed | |
45 | reverse_each(&:after_scan) | |
46 | end | |
47 | end | |
48 | end |
0 | module CMSScanner | |
1 | class Error < StandardError | |
2 | end | |
3 | ||
4 | # Target Down Error | |
5 | class TargetDownError < Error | |
6 | attr_reader :response | |
7 | ||
8 | def initialize(response) | |
9 | @response = response | |
10 | end | |
11 | ||
12 | def to_s | |
13 | "The url supplied '#{response.request.url}' seems to be down (#{response.return_message})" | |
14 | end | |
15 | end | |
16 | ||
17 | # HTTP Authentication Required Error | |
18 | class HTTPAuthRequiredError < Error | |
19 | # :nocov: | |
20 | def to_s | |
21 | 'HTTP authentication required (or was invalid), please provide it with --http-auth' | |
22 | end | |
23 | # :nocov: | |
24 | end | |
25 | ||
26 | # Proxy Authentication Required Error | |
27 | class ProxyAuthRequiredError < Error | |
28 | # :nocov: | |
29 | def to_s | |
30 | 'Proxy authentication required (or was invalid), please provide it with --proxy-auth' | |
31 | end | |
32 | # :nocov: | |
33 | end | |
34 | ||
35 | # Access Forbidden Error | |
36 | class AccessForbiddenError < Error | |
37 | # :nocov: | |
38 | def to_s | |
39 | 'The target is responding with a 403, this might be due to a WAF. ' \ | |
40 | 'Please re-try with --random-user-agent' | |
41 | end | |
42 | # :nocov: | |
43 | end | |
44 | ||
45 | # HTTP Redirect Error | |
46 | class HTTPRedirectError < Error | |
47 | attr_reader :redirect_uri | |
48 | ||
49 | # @param [ String ] url | |
50 | def initialize(url) | |
51 | @redirect_uri = Addressable::URI.parse(url).normalize | |
52 | end | |
53 | ||
54 | def to_s | |
55 | "The URL supplied redirects to #{redirect_uri}. Use the --ignore-main-redirect "\ | |
56 | 'option to ignore the redirection and scan the target.' | |
57 | end | |
58 | end | |
59 | end |
0 | module CMSScanner | |
1 | # Exit Code Values | |
2 | module ExitCode | |
3 | # No error, scan finished w/o any vulnerabilies found | |
4 | OK = 0 | |
5 | ||
6 | # All exceptions raised by OptParseValidator and OptionParser | |
7 | CLI_OPTION_ERROR = 1 | |
8 | ||
9 | # Interrupt received | |
10 | INTERRUPTED = 2 | |
11 | ||
12 | # Exceptions | |
13 | ERROR = 3 | |
14 | ||
15 | # The target has at least one vulnerability. | |
16 | # Currently, the interesting findings do not count as vulnerable things | |
17 | VULNERABLE = 4 | |
18 | end | |
19 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # Base class container for the Finders (i.e IndependentFinders etc) | |
3 | class BaseFinders < Array | |
4 | # @return [ Findings ] | |
5 | def findings | |
6 | @findings ||= NS::Finders::Findings.new | |
7 | end | |
8 | ||
9 | # Should be implemented in child classes | |
10 | def run; end | |
11 | ||
12 | protected | |
13 | ||
14 | # @param [ Symbol ] mode :mixed, :passive or :aggressive | |
15 | # @return [ Array<Symbol> ] The symbols to call for the mode | |
16 | def symbols_from_mode(mode) | |
17 | symbols = %i[passive aggressive] | |
18 | ||
19 | return symbols if mode.nil? || mode == :mixed | |
20 | ||
21 | symbols.include?(mode) ? [*mode] : [] | |
22 | end | |
23 | ||
24 | # @param [ CMSScanner::Finders::Finder ] finder | |
25 | # @param [ Symbol ] symbol See return values of #symbols_from_mode | |
26 | # @param [ Hash ] opts | |
27 | def run_finder(finder, symbol, opts) | |
28 | [*finder.send(symbol, opts.merge(found: findings))].compact.each do |found| | |
29 | findings << found | |
30 | end | |
31 | end | |
32 | ||
33 | # Allow child classes to filter the findings, such as return the best one | |
34 | # or remove the low confidence ones. | |
35 | # | |
36 | # @return [ Findings ] | |
37 | def filter_findings | |
38 | findings | |
39 | end | |
40 | end | |
41 | end | |
42 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | class Finder | |
3 | # Module to provide an easy way to perform password attacks | |
4 | module BreadthFirstDictionaryAttack | |
5 | # @param [ Array<CMSScanner::User> ] users | |
6 | # @param [ Array<String> ] passwords | |
7 | # @param [ Hash ] opts | |
8 | # @option opts [ Boolean ] :show_progression | |
9 | # | |
10 | # @yield [ CMSScanner::User ] When a valid combination is found | |
11 | # | |
12 | # TODO: Make rubocop happy about metrics etc | |
13 | # | |
14 | # rubocop:disable all | |
15 | def attack(users, passwords, opts = {}) | |
16 | create_progress_bar(total: users.size * passwords.size, show_progression: opts[:show_progression]) | |
17 | queue_count = 0 | |
18 | ||
19 | passwords.each_with_index do |password, password_index| | |
20 | remaining_users = users.select { |u| u.password.nil? } | |
21 | ||
22 | break if remaining_users.empty? | |
23 | ||
24 | remaining_users.each do |user| | |
25 | request = login_request(user.username, password) | |
26 | ||
27 | request.on_complete do |res| | |
28 | progress_bar.title = "Trying #{user.username} / #{password}" | |
29 | progress_bar.increment | |
30 | ||
31 | if valid_credentials?(res) | |
32 | user.password = password | |
33 | ||
34 | yield user | |
35 | ||
36 | offset = progress_bar.total - progress_bar.progress < hydra.max_concurrency ? 2 : 1 | |
37 | ||
38 | progress_bar.total -= passwords.size - password_index - offset | |
39 | elsif errored_response?(res) | |
40 | output_error(res) | |
41 | end | |
42 | end | |
43 | ||
44 | hydra.queue(request) | |
45 | queue_count += 1 | |
46 | ||
47 | if queue_count >= hydra.max_concurrency | |
48 | hydra.run | |
49 | queue_count = 0 | |
50 | end | |
51 | end | |
52 | end | |
53 | ||
54 | hydra.run | |
55 | progress_bar.stop | |
56 | end | |
57 | # rubocop:enable all | |
58 | ||
59 | # @param [ String ] username | |
60 | # param [ String ] password | |
61 | # | |
62 | # @return [ Typhoeus::Request ] | |
63 | def login_request(username, password) | |
64 | # To Implement in the finder related to the attack | |
65 | end | |
66 | ||
67 | # @param [ Typhoeus::Response ] response | |
68 | # | |
69 | # @return [ Boolean ] Whether or not credentials related to the request are valid | |
70 | def valid_credentials?(response) | |
71 | # To Implement in the finder related to the attack | |
72 | end | |
73 | ||
74 | # @param [ Typhoeus::Response ] response | |
75 | # | |
76 | # @return [ Boolean ] Whether or not something wrong happened | |
77 | # other than wrong credentials | |
78 | def errored_response?(response) | |
79 | # To Implement in the finder related to the attack | |
80 | end | |
81 | ||
82 | protected | |
83 | ||
84 | # @param [ Typhoeus::Response ] response | |
85 | def output_error(response) | |
86 | error = if response.timed_out? | |
87 | 'Request timed out.' | |
88 | elsif response.code.zero? | |
89 | "No response from remote server. WAF/IPS? (#{response.return_message})" | |
90 | elsif response.code.to_s =~ /^50/ | |
91 | 'Server error, try reducing the number of threads.' | |
92 | else | |
93 | "Unknown response received Code: #{response.code}\nBody: #{response.body}" | |
94 | end | |
95 | ||
96 | progress_bar.log("Error: #{error}") | |
97 | end | |
98 | end | |
99 | end | |
100 | end | |
101 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | class Finder | |
3 | # Module to provide an easy way to enumerate items such as plugins, themes etc | |
4 | module Enumerator | |
5 | # @param [ Hash ] The target urls | |
6 | # @param [ Hash ] opts | |
7 | # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar | |
8 | # @option opts [ Regexp ] :exclude_content | |
9 | # | |
10 | # @yield [ Typhoeus::Response, String ] | |
11 | def enumerate(target_urls, opts = {}) | |
12 | create_progress_bar(opts.merge(total: target_urls.size)) | |
13 | ||
14 | target_urls.each do |url, id| | |
15 | request = browser.forge_request(url, request_params) | |
16 | ||
17 | request.on_complete do |res| | |
18 | progress_bar.increment | |
19 | ||
20 | next if target.homepage_or_404?(res) | |
21 | ||
22 | if opts[:exclude_content] | |
23 | next if res.response_headers&.match(opts[:exclude_content]) || res.body.match(opts[:exclude_content]) | |
24 | end | |
25 | ||
26 | yield res, id | |
27 | end | |
28 | ||
29 | hydra.queue(request) | |
30 | end | |
31 | ||
32 | hydra.run | |
33 | end | |
34 | ||
35 | # @return [ Hash ] | |
36 | def request_params | |
37 | # disabling the cache, as it causes a 'stack level too deep' exception | |
38 | # with a large number of requests :/ | |
39 | # See https://github.com/typhoeus/typhoeus/issues/408 | |
40 | { cache_ttl: 0 } | |
41 | end | |
42 | end | |
43 | end | |
44 | end | |
45 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | class Finder | |
3 | # Module to provide an easy way to fingerprint things such as versions | |
4 | module Fingerprinter | |
5 | # @param [ Hash ] fingerprints The fingerprints | |
6 | # Format should be like the following: | |
7 | # { | |
8 | # file_path_1: { | |
9 | # md5_hash_1: version_1, | |
10 | # md5_hash_2: [version_2] | |
11 | # }, | |
12 | # file_path_2: { | |
13 | # md5_hash_3: [version_1, version_2], | |
14 | # md5_hash_4: version_3 | |
15 | # } | |
16 | # } | |
17 | # Note that the version can either be an array or a string | |
18 | # | |
19 | # @param [ Hash ] opts | |
20 | # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar | |
21 | # | |
22 | # @yield [ Mixed, String, String ] version/s, url, hash The version associated to the | |
23 | # fingerprint of the url | |
24 | def fingerprint(fingerprints, opts = {}) | |
25 | create_progress_bar(opts.merge(total: fingerprints.size)) | |
26 | ||
27 | fingerprints.each do |path, f| | |
28 | url = target.url(path.dup) | |
29 | request = browser.forge_request(url, request_params) | |
30 | ||
31 | request.on_complete do |res| | |
32 | progress_bar.increment | |
33 | ||
34 | md5sum = hexdigest(res.body) | |
35 | ||
36 | next unless f.key?(md5sum) | |
37 | ||
38 | yield f[md5sum], url, md5sum | |
39 | end | |
40 | ||
41 | hydra.queue(request) | |
42 | end | |
43 | ||
44 | hydra.run | |
45 | end | |
46 | ||
47 | # @return [ Hash ] | |
48 | def request_params | |
49 | {} | |
50 | end | |
51 | ||
52 | # @return [ String ] The hashed value for the given body | |
53 | def hexdigest(body) | |
54 | Digest::MD5.hexdigest(body) | |
55 | end | |
56 | end | |
57 | end | |
58 | end | |
59 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | class Finder | |
3 | module SmartURLChecker | |
4 | # Findings | |
5 | class Findings < Array | |
6 | def <<(finding) | |
7 | return self unless finding | |
8 | ||
9 | each do |f| | |
10 | next unless f == finding && f.found_by == finding.found_by | |
11 | ||
12 | # This makes sure entries added are unique | |
13 | # and prevent pages redirecting to the same one to be added twice | |
14 | entries_to_add = finding.interesting_entries - f.interesting_entries | |
15 | return self if entries_to_add.empty? | |
16 | ||
17 | entries_to_add.each { |entry| f.interesting_entries << entry } | |
18 | ||
19 | f.confidence += finding.confidence | |
20 | ||
21 | return self | |
22 | end | |
23 | ||
24 | super(finding) | |
25 | end | |
26 | end | |
27 | end | |
28 | end | |
29 | end | |
30 | end |
0 | require 'cms_scanner/finders/finder/smart_url_checker/findings' | |
1 | ||
2 | module CMSScanner | |
3 | module Finders | |
4 | class Finder | |
5 | # Smart URL Checker | |
6 | # Typically used when some URLs are potentially in the homepage. If they are found | |
7 | # in it, they will be checked in the #passive (like a browser/client would do when loading the page). | |
8 | # Otherwise they will be checked in the #aggressive | |
9 | module SmartURLChecker | |
10 | # @param [ Array<String> ] urls | |
11 | # @param [ Hash ] opts | |
12 | # | |
13 | # @return [] | |
14 | def process_urls(_urls, _opts = {}) | |
15 | raise NotImplementedError | |
16 | end | |
17 | ||
18 | # @param [ Hash ] opts | |
19 | # | |
20 | # @return [ Array<Finding> ] | |
21 | def passive(opts = {}) | |
22 | process_urls(passive_urls(opts), opts) | |
23 | end | |
24 | ||
25 | # @param [ Hash ] opts | |
26 | # | |
27 | # @return [ Array<String> ] | |
28 | def passive_urls(_opts = {}) | |
29 | target.in_scope_urls(target.homepage_res, passive_urls_xpath) | |
30 | end | |
31 | ||
32 | # @return [ String ] | |
33 | def passive_urls_xpath | |
34 | raise NotImplementedError | |
35 | end | |
36 | ||
37 | # @param [ Hash ] opts | |
38 | # | |
39 | # @return [ Array<Finding> ] | |
40 | def aggressive(opts = {}) | |
41 | # To avoid scanning the same twice | |
42 | urls = aggressive_urls(opts) | |
43 | urls -= passive_urls(opts) if opts[:mode] == :mixed | |
44 | ||
45 | process_urls(urls, opts) | |
46 | end | |
47 | ||
48 | # @param [ Hash ] opts | |
49 | # | |
50 | # @return [ Array<String> ] | |
51 | def aggressive_urls(_opts = {}) | |
52 | raise NotImplementedError | |
53 | end | |
54 | end | |
55 | end | |
56 | end | |
57 | end |
0 | require 'cms_scanner/finders/finder/smart_url_checker' | |
1 | require 'cms_scanner/finders/finder/enumerator' | |
2 | require 'cms_scanner/finders/finder/fingerprinter' | |
3 | require 'cms_scanner/finders/finder/breadth_first_dictionary_attack' | |
4 | ||
5 | module CMSScanner | |
6 | module Finders | |
7 | # Finder | |
8 | class Finder | |
9 | # Constants for common found_by | |
10 | DIRECT_ACCESS = 'Direct Access (Aggressive Detection)'.freeze | |
11 | ||
12 | attr_accessor :target, :progress_bar | |
13 | ||
14 | def initialize(target) | |
15 | @target = target | |
16 | end | |
17 | ||
18 | # @return [ String ] The titleized name of the finder | |
19 | def titleize | |
20 | self.class.to_s.demodulize.underscore.titleize | |
21 | end | |
22 | ||
23 | # @param [ Hash ] _opts | |
24 | def passive(_opts = {}); end | |
25 | ||
26 | # @param [ Hash ] _opts | |
27 | def aggressive(_opts = {}); end | |
28 | ||
29 | # @param [ Hash ] opts See https://github.com/jfelchner/ruby-progressbar/wiki/Options | |
30 | # @option opts [ Boolean ] :show_progression | |
31 | # | |
32 | # @return [ ProgressBar::Base ] | |
33 | def create_progress_bar(opts = {}) | |
34 | bar_opts = { format: '%t %a <%B> (%c / %C) %P%% %e' } | |
35 | bar_opts[:output] = ProgressBarNullOutput unless opts[:show_progression] | |
36 | ||
37 | @progress_bar = ::ProgressBar.create(bar_opts.merge(opts)) | |
38 | end | |
39 | ||
40 | # @return [ Browser ] | |
41 | def browser | |
42 | @browser ||= NS::Browser.instance | |
43 | end | |
44 | ||
45 | # @return [ Typhoeus::Hydra ] | |
46 | def hydra | |
47 | @hydra ||= browser.hydra | |
48 | end | |
49 | ||
50 | # @param [ String, Symbol ] klass | |
51 | # @return [ String ] | |
52 | def found_by(klass = self) | |
53 | caller_locations.each do |call| | |
54 | label = call.label | |
55 | ||
56 | next unless %w[aggressive passive].include? label | |
57 | ||
58 | return "#{klass.titleize} (#{label.capitalize} Detection)" | |
59 | end | |
60 | nil | |
61 | end | |
62 | end | |
63 | end | |
64 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # Finding | |
3 | module Finding | |
4 | # Fix for "Double/Dynamic Inclusion Problem" | |
5 | def self.included(base) | |
6 | base.include References | |
7 | super(base) | |
8 | end | |
9 | ||
10 | FINDING_OPTS = %i[confidence confirmed_by references found_by interesting_entries].freeze | |
11 | ||
12 | attr_accessor(*FINDING_OPTS) | |
13 | ||
14 | # @return [ Array ] | |
15 | def confirmed_by | |
16 | @confirmed_by ||= [] | |
17 | end | |
18 | ||
19 | # Should be overriden in child classes | |
20 | # @return [ Array ] | |
21 | def interesting_entries | |
22 | @interesting_entries ||= [] | |
23 | end | |
24 | ||
25 | # @return [ Integer ] | |
26 | def confidence | |
27 | @confidence ||= 0 | |
28 | end | |
29 | ||
30 | # @param [ Integer ] value | |
31 | def confidence=(value) | |
32 | @confidence = value >= 100 ? 100 : value | |
33 | end | |
34 | ||
35 | # @param [ Hash ] opts | |
36 | def parse_finding_options(opts = {}) | |
37 | FINDING_OPTS.each { |opt| send("#{opt}=", opts[opt]) if opts.key?(opt) } | |
38 | end | |
39 | ||
40 | # TODO: maybe also check for interesting_entries and confirmed_by ? | |
41 | # So far this is used in specs only | |
42 | def eql?(other) | |
43 | self == other && confidence == other.confidence && found_by == other.found_by | |
44 | end | |
45 | ||
46 | def <=>(other) | |
47 | to_s.downcase <=> other.to_s.downcase | |
48 | end | |
49 | end | |
50 | end | |
51 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # Findings container | |
3 | class Findings < Array | |
4 | # Override to include the confirmed_by logic | |
5 | # | |
6 | # @param [ Finding ] finding | |
7 | def <<(finding) | |
8 | return self unless finding | |
9 | ||
10 | each do |found| | |
11 | next unless found == finding | |
12 | ||
13 | found.confirmed_by << finding | |
14 | found.confidence += finding.confidence | |
15 | ||
16 | return self | |
17 | end | |
18 | ||
19 | super(finding) | |
20 | end | |
21 | end | |
22 | end | |
23 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # Independent Finder | |
3 | module IndependentFinder | |
4 | extend ActiveSupport::Concern | |
5 | ||
6 | # See ActiveSupport::Concern | |
7 | module ClassMethods | |
8 | def find(target, opts = {}) | |
9 | new(target).find(opts) | |
10 | end | |
11 | end | |
12 | ||
13 | # @param [ Hash ] opts | |
14 | # @option opts [ Symbol ] mode (:mixed, :passive, :aggressive) | |
15 | # | |
16 | # @return [ Findings ] | |
17 | def find(opts = {}) | |
18 | finders.run(opts) | |
19 | end | |
20 | ||
21 | # @return [ Array ] | |
22 | def finders | |
23 | @finders ||= NS::Finders::IndependentFinders.new | |
24 | end | |
25 | end | |
26 | end | |
27 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # This class is designed to handle independent results | |
3 | # which are not related with each others | |
4 | # e.g: interesting files | |
5 | class IndependentFinders < BaseFinders | |
6 | # @param [ Hash ] opts | |
7 | # @option opts [ Symbol ] mode :mixed, :passive or :aggressive | |
8 | # | |
9 | # @return [ Findings ] | |
10 | def run(opts = {}) | |
11 | methods = symbols_from_mode(opts[:mode]) | |
12 | ||
13 | each do |finder| | |
14 | methods.each do |symbol| | |
15 | run_finder(finder, symbol, opts) | |
16 | end | |
17 | end | |
18 | ||
19 | filter_findings | |
20 | end | |
21 | end | |
22 | end | |
23 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # Same Type Finder | |
3 | module SameTypeFinder | |
4 | def self.included(klass) | |
5 | klass.class_eval do | |
6 | include IndependentFinder | |
7 | ||
8 | # @return [ Array ] | |
9 | def finders | |
10 | @finders ||= NS::Finders::SameTypeFinders.new | |
11 | end | |
12 | end | |
13 | end | |
14 | end | |
15 | end | |
16 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # This class is designed to handle same type results, such as enumeration of plugins, | |
3 | # themes etc. | |
4 | class SameTypeFinders < BaseFinders | |
5 | # @param [ Hash ] opts | |
6 | # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive | |
7 | # @option opts [ Boolean ] :sort Wether or not to sort the findings | |
8 | # | |
9 | # @return [ Findings ] | |
10 | def run(opts = {}) | |
11 | symbols_from_mode(opts[:mode]).each do |symbol| | |
12 | each do |finder| | |
13 | run_finder(finder, symbol, opts) | |
14 | end | |
15 | end | |
16 | ||
17 | findings.sort! if opts[:sort] | |
18 | ||
19 | filter_findings | |
20 | end | |
21 | end | |
22 | end | |
23 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # Unique Finder | |
3 | module UniqueFinder | |
4 | def self.included(klass) | |
5 | klass.class_eval do | |
6 | include IndependentFinder | |
7 | ||
8 | # @return [ Array ] | |
9 | def finders | |
10 | @finders ||= NS::Finders::UniqueFinders.new | |
11 | end | |
12 | end | |
13 | end | |
14 | end | |
15 | end | |
16 | end |
0 | module CMSScanner | |
1 | module Finders | |
2 | # This class is designed to return a unique result such as a version | |
3 | # Note: Finders contained can return multiple results but the #run will only | |
4 | # returned the best finding | |
5 | class UniqueFinders < BaseFinders | |
6 | # @param [ Hash ] opts | |
7 | # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive | |
8 | # @option opts [ Int ] :confidence_threshold If a finding's confidence reaches this value, | |
9 | # it will be returned as the best finding. | |
10 | # Default is 100. | |
11 | # If <= 0, all finders will be ran. | |
12 | # | |
13 | # @return [ Object, false ] The best finding or false if none | |
14 | def run(opts = {}) | |
15 | opts[:confidence_threshold] ||= 100 | |
16 | ||
17 | symbols_from_mode(opts[:mode]).each do |symbol| | |
18 | each do |finder| | |
19 | run_finder(finder, symbol, opts) | |
20 | ||
21 | next if opts[:confidence_threshold] <= 0 | |
22 | ||
23 | findings.each { |f| return f if f.confidence >= opts[:confidence_threshold] } | |
24 | end | |
25 | end | |
26 | ||
27 | filter_findings | |
28 | end | |
29 | ||
30 | protected | |
31 | ||
32 | # @return [ Object, false ] The best finding or false if none | |
33 | def filter_findings | |
34 | # results are sorted by confidence ASC | |
35 | findings.sort_by!(&:confidence) | |
36 | ||
37 | # If all findings have the same confidence, false is returned | |
38 | return false if findings.size > 1 && findings.first.confidence == findings.last.confidence | |
39 | ||
40 | findings.last || false | |
41 | end | |
42 | end | |
43 | end | |
44 | end |
0 | require 'cms_scanner/finders/finder' | |
1 | require 'cms_scanner/finders/finding' | |
2 | require 'cms_scanner/finders/findings' | |
3 | require 'cms_scanner/finders/base_finders' | |
4 | require 'cms_scanner/finders/independent_finders' | |
5 | require 'cms_scanner/finders/independent_finder' | |
6 | require 'cms_scanner/finders/unique_finders' | |
7 | require 'cms_scanner/finders/unique_finder' | |
8 | require 'cms_scanner/finders/same_type_finders' | |
9 | require 'cms_scanner/finders/same_type_finder' |
0 | module CMSScanner | |
1 | module Formatter | |
2 | # Module used to output the rendered views into a buffer | |
3 | # and beautify it a the end of the scan | |
4 | module Buffer | |
5 | def output(tpl, vars = {}, controller_name = nil) | |
6 | buffer << render(tpl, vars, controller_name).encode('UTF-8', invalid: :replace, undef: :replace) | |
7 | end | |
8 | ||
9 | def buffer | |
10 | @buffer ||= '' | |
11 | end | |
12 | end | |
13 | end | |
14 | end |
0 | require 'cms_scanner/formatter/buffer' | |
1 | ||
2 | module CMSScanner | |
3 | # Formatter | |
4 | module Formatter | |
5 | # Module to be able to do Formatter.load() & Formatter.availables | |
6 | # and do that as well when the Formatter is included in another module | |
7 | module ClassMethods | |
8 | # @param [ String ] format | |
9 | # @param [ Array<String> ] custom_views | |
10 | # | |
11 | # @return [ Formatter::Base ] | |
12 | def load(format = nil, custom_views = nil) | |
13 | format ||= 'cli' | |
14 | custom_views ||= [] | |
15 | ||
16 | f = const_get(format.tr('-', '_').camelize).new | |
17 | custom_views.each { |v| f.views_directories << v } | |
18 | f | |
19 | end | |
20 | ||
21 | # @return [ Array<String> ] The list of the available formatters (except the Base one) | |
22 | # @note: the #load method above should then be used to create the associated formatter | |
23 | def availables | |
24 | formatters = NS::Formatter.constants.select do |const| | |
25 | name = NS::Formatter.const_get(const) | |
26 | name.is_a?(Class) && name != NS::Formatter::Base | |
27 | end | |
28 | ||
29 | formatters.map { |sym| sym.to_s.underscore.dasherize } | |
30 | end | |
31 | end | |
32 | ||
33 | extend ClassMethods | |
34 | ||
35 | def self.included(base) | |
36 | base.extend(ClassMethods) | |
37 | end | |
38 | ||
39 | # This module should be implemented in the code which uses this Framework to | |
40 | # be able to override/implements instance methods for all the Formatters | |
41 | # w/o having to include/write the methods in each formatters. | |
42 | # | |
43 | # Example: to override the #views_directories (see the wpscan-v3/lib/wpscan/formatter.rb) | |
44 | module InstanceMethods | |
45 | end | |
46 | ||
47 | # Base Formatter | |
48 | class Base | |
49 | attr_reader :controller_name | |
50 | ||
51 | def initialize | |
52 | # Can't put this at the top level of the class, due to the NS:: | |
53 | extend NS::Formatter::InstanceMethods | |
54 | end | |
55 | ||
56 | # @return [ String ] The underscored name of the class | |
57 | def format | |
58 | self.class.name.demodulize.underscore | |
59 | end | |
60 | ||
61 | # @return [ Boolean ] | |
62 | def user_interaction? | |
63 | format == 'cli' | |
64 | end | |
65 | ||
66 | # @return [ String ] The underscored format to use as a base | |
67 | def base_format; end | |
68 | ||
69 | # @return [ Array<String> ] | |
70 | def formats | |
71 | [format, base_format].compact | |
72 | end | |
73 | ||
74 | # This is called after the scan | |
75 | # and used in some formatters (e.g JSON) | |
76 | # to indent results | |
77 | def beautify; end | |
78 | ||
79 | # @see #render | |
80 | def output(tpl, vars = {}, controller_name = nil) | |
81 | puts render(tpl, vars, controller_name) | |
82 | end | |
83 | ||
84 | # @param [ String ] tpl | |
85 | # @param [ Hash ] vars | |
86 | # @param [ String ] controller_name | |
87 | def render(tpl, vars = {}, controller_name = nil) | |
88 | template_vars(vars) | |
89 | @controller_name = controller_name if controller_name | |
90 | ||
91 | # '-' is used to disable new lines when -%> is used | |
92 | # See http://www.ruby-doc.org/stdlib-2.1.1/libdoc/erb/rdoc/ERB.html | |
93 | ERB.new(File.read(view_path(tpl)), nil, '-').result(binding) | |
94 | end | |
95 | ||
96 | # @param [ Hash ] vars | |
97 | # | |
98 | # @return [ Void ] | |
99 | def template_vars(vars) | |
100 | vars.each do |key, value| | |
101 | instance_variable_set("@#{key}", value) unless key == :views_directories | |
102 | end | |
103 | end | |
104 | ||
105 | # @param [ String ] tpl | |
106 | # | |
107 | # @return [ String ] The path of the view | |
108 | def view_path(tpl) | |
109 | if tpl[0, 1] == '@' # Global Template | |
110 | tpl = tpl.delete('@') | |
111 | else | |
112 | raise 'The controller_name can not be nil' unless controller_name | |
113 | ||
114 | tpl = "#{controller_name}/#{tpl}" | |
115 | end | |
116 | ||
117 | raise "Wrong tpl format: '#{tpl}'" unless tpl =~ %r{\A[\w/_]+\z} | |
118 | ||
119 | views_directories.reverse_each do |dir| | |
120 | formats.each do |format| | |
121 | potential_file = File.join(dir, format, "#{tpl}.erb") | |
122 | ||
123 | return potential_file if File.exist?(potential_file) | |
124 | end | |
125 | end | |
126 | ||
127 | raise "View not found for #{format}/#{tpl}" | |
128 | end | |
129 | ||
130 | # @return [ Array<String> ] The directories to look into for views | |
131 | def views_directories | |
132 | @views_directories ||= [ | |
133 | APP_DIR, NS::APP_DIR, | |
134 | File.join(Dir.home, ".#{NS.app_name}"), File.join(Dir.pwd, ".#{NS.app_name}") | |
135 | ].uniq.reduce([]) { |acc, elem| acc << Pathname.new(elem).join('views').to_s } | |
136 | end | |
137 | end | |
138 | end | |
139 | end |
0 | # @param [ String ] file The file path | |
1 | def redirect_output_to_file(file) | |
2 | $stdout.reopen(file, 'w') | |
3 | $stdout.sync = true | |
4 | $stderr.reopen($stdout) # Not sure if this is needed | |
5 | end | |
6 | ||
7 | # @return [ Integer ] The memory of the current process in Bytes | |
8 | def memory_usage | |
9 | `ps -o rss= -p #{Process.pid}`.to_i * 1024 # ps returns the value in KB | |
10 | end |
0 | # Hack of the Numeric class | |
1 | class Numeric | |
2 | # @return [ String ] A human readable string of the value | |
3 | def bytes_to_human | |
4 | units = %w[B KB MB GB TB] | |
5 | e = abs.zero? ? abs : (Math.log(abs) / Math.log(1024)).floor | |
6 | s = format('%.3f', (abs.to_f / 1024**e)) | |
7 | ||
8 | s.sub(/\.?0*$/, ' ' + units[e]) | |
9 | end | |
10 | end |
0 | require 'ruby-progressbar/outputs/null' | |
1 | ||
2 | module CMSScanner | |
3 | # Adds the feature to log message sent to #log | |
4 | # So they can be retrieved at some point, like after a password attack with a JSON output | |
5 | # which won't display the progressbar but still call #log for errors etc | |
6 | class ProgressBarNullOutput < ::ProgressBar::Outputs::Null | |
7 | # @retutn [ Array<String> ] | |
8 | def logs | |
9 | @logs ||= [] | |
10 | end | |
11 | ||
12 | # Override of parent method | |
13 | # @return [ Array<String> ] return the logs when no string provided | |
14 | def log(string = nil) | |
15 | return logs if string.nil? | |
16 | ||
17 | logs << string | |
18 | end | |
19 | end | |
20 | end |
0 | module PublicSuffix | |
1 | # Monkey Patch to include the match logic | |
2 | class Domain | |
3 | # For Sanity | |
4 | def ==(other) | |
5 | name == other.name | |
6 | end | |
7 | ||
8 | # @return [ Boolean ] | |
9 | # | |
10 | def match(pattern) | |
11 | pattern = PublicSuffix.parse(pattern) unless pattern.is_a?(PublicSuffix::Domain) | |
12 | ||
13 | return name == pattern.name unless pattern.trd | |
14 | return false unless tld == pattern.tld && sld == pattern.sld | |
15 | ||
16 | matching_pattern?(pattern) | |
17 | end | |
18 | ||
19 | protected | |
20 | ||
21 | # @rturn [ Boolean ] | |
22 | def matching_pattern?(pattern) | |
23 | pattern_trds = pattern.trd.split('.') | |
24 | domain_trds = trd.split('.') | |
25 | ||
26 | case pattern_trds.first | |
27 | when '*' | |
28 | pattern_trds[1..-1] == domain_trds[1..-1] | |
29 | when '**' | |
30 | pa = pattern_trds[1..-1] | |
31 | pa_size = pa.size | |
32 | ||
33 | domain_trds[domain_trds.size - pa_size, pa_size] == pa | |
34 | else | |
35 | name == pattern.name | |
36 | end | |
37 | end | |
38 | end | |
39 | end |
0 | module CMSScanner | |
1 | # References related to the issue | |
2 | module References | |
3 | extend ActiveSupport::Concern | |
4 | ||
5 | # See ActiveSupport::Concern | |
6 | module ClassMethods | |
7 | # @return [ Array<Symbol> ] | |
8 | def references_keys | |
9 | @references_keys ||= %i[cve secunia osvdb exploitdb url metasploit packetstorm securityfocus] | |
10 | end | |
11 | end | |
12 | ||
13 | # @param [ Hash ] refs | |
14 | def references=(refs) | |
15 | @references = {} | |
16 | ||
17 | self.class.references_keys.each do |key| | |
18 | @references[key] = [*refs[key]].map(&:to_s) if refs.key?(key) | |
19 | end | |
20 | end | |
21 | ||
22 | # @return [ Hash ] | |
23 | def references | |
24 | @references ||= {} | |
25 | end | |
26 | ||
27 | # @return [ Array<String> ] All the references URLs | |
28 | def references_urls | |
29 | cve_urls + secunia_urls + osvdb_urls + exploitdb_urls + urls + msf_urls + | |
30 | packetstorm_urls + securityfocus_urls | |
31 | end | |
32 | ||
33 | # @return [ Array<String> ] The CVEs | |
34 | def cves | |
35 | references[:cve] || [] | |
36 | end | |
37 | ||
38 | # @return [ Array<String> ] | |
39 | def cve_urls | |
40 | cves.reduce([]) { |acc, elem| acc << cve_url(elem) } | |
41 | end | |
42 | ||
43 | # @return [ String ] The URL to the CVE | |
44 | def cve_url(cve) | |
45 | "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{cve}" | |
46 | end | |
47 | ||
48 | # @return [ Array<String> ] The Secunia IDs | |
49 | def secunia_ids | |
50 | references[:secunia] || [] | |
51 | end | |
52 | ||
53 | # @return [ Array<String> ] | |
54 | def secunia_urls | |
55 | secunia_ids.reduce([]) { |acc, elem| acc << secunia_url(elem) } | |
56 | end | |
57 | ||
58 | # @return [ String ] The URL to the Secunia advisory | |
59 | def secunia_url(id) | |
60 | "https://secunia.com/advisories/#{id}/" | |
61 | end | |
62 | ||
63 | # @return [ Array<String> ] The OSVDB IDs | |
64 | def osvdb_ids | |
65 | references[:osvdb] || [] | |
66 | end | |
67 | ||
68 | # @return [ Array<String> ] | |
69 | def osvdb_urls | |
70 | osvdb_ids.reduce([]) { |acc, elem| acc << osvdb_url(elem) } | |
71 | end | |
72 | ||
73 | # @return [ String ] The URL to the ExploitDB advisory | |
74 | def osvdb_url(id) | |
75 | "http://osvdb.org/show/osvdb/#{id}" | |
76 | end | |
77 | ||
78 | # @return [ Array<String> ] The ExploitDB ID | |
79 | def exploitdb_ids | |
80 | references[:exploitdb] || [] | |
81 | end | |
82 | ||
83 | # @return [ Array<String> ] | |
84 | def exploitdb_urls | |
85 | exploitdb_ids.reduce([]) { |acc, elem| acc << exploitdb_url(elem) } | |
86 | end | |
87 | ||
88 | # @return [ String ] | |
89 | def exploitdb_url(id) | |
90 | "https://www.exploit-db.com/exploits/#{id}/" | |
91 | end | |
92 | ||
93 | # @return [ String<Array> ] | |
94 | def urls | |
95 | references[:url] || [] | |
96 | end | |
97 | ||
98 | # @return [ Array<String> ] The metasploit modules | |
99 | def msf_modules | |
100 | references[:metasploit] || [] | |
101 | end | |
102 | ||
103 | # @return [ Array<String> ] | |
104 | def msf_urls | |
105 | msf_modules.reduce([]) { |acc, elem| acc << msf_url(elem) } | |
106 | end | |
107 | ||
108 | # @return [ String ] The URL to the metasploit module page | |
109 | def msf_url(mod) | |
110 | "https://www.rapid7.com/db/modules/#{mod.sub(%r{^/}, '')}" | |
111 | end | |
112 | ||
113 | # @return [ Array<String> ] The Packetstormsecurity IDs | |
114 | def packetstorm_ids | |
115 | @packetstorm_ids ||= references[:packetstorm] || [] | |
116 | end | |
117 | ||
118 | # @return [ Array<String> ] | |
119 | def packetstorm_urls | |
120 | packetstorm_ids.reduce([]) { |acc, elem| acc << packetstorm_url(elem) } | |
121 | end | |
122 | ||
123 | # @return [ String ] | |
124 | def packetstorm_url(id) | |
125 | "http://packetstormsecurity.com/files/#{id}/" | |
126 | end | |
127 | ||
128 | # @return [ Array<String> ] The Security Focus IDs | |
129 | def securityfocus_ids | |
130 | references[:securityfocus] || [] | |
131 | end | |
132 | ||
133 | # @return [ Array<String> ] | |
134 | def securityfocus_urls | |
135 | securityfocus_ids.reduce([]) { |acc, elem| acc << securityfocus_url(elem) } | |
136 | end | |
137 | ||
138 | # @return [ String ] | |
139 | def securityfocus_url(id) | |
140 | "http://www.securityfocus.com/bid/#{id}/" | |
141 | end | |
142 | end | |
143 | end |
0 | module CMSScanner | |
1 | # Scope system logic | |
2 | class Target < WebSite | |
3 | # @note Comments are deleted to avoid cache generation details | |
4 | # | |
5 | # @param [ Typhoeus::Response, String ] page | |
6 | # | |
7 | # @return [ String ] The md5sum of the page | |
8 | def self.page_hash(page) | |
9 | page = NS::Browser.get(page, followlocation: true) unless page.is_a?(Typhoeus::Response) | |
10 | ||
11 | # Removes comments and script tags before computing the hash | |
12 | # to remove any potential cached stuff | |
13 | html = Nokogiri::HTML(page.body) | |
14 | html.xpath('//script|//comment()').each(&:remove) | |
15 | ||
16 | Digest::MD5.hexdigest(html) | |
17 | end | |
18 | ||
19 | # @return [ String ] The hash of the homepage | |
20 | def homepage_hash | |
21 | @homepage_hash ||= self.class.page_hash(url) | |
22 | end | |
23 | ||
24 | # @note This is used to detect potential custom 404 responding with a 200 | |
25 | # @return [ String ] The hash of a 404 | |
26 | def error_404_hash | |
27 | @error_404_hash ||= self.class.page_hash(non_existant_page_url) | |
28 | end | |
29 | ||
30 | # @return [ String ] The URL of an unlikely existant page | |
31 | def non_existant_page_url | |
32 | uri.join(Digest::MD5.hexdigest(rand(999_999_999).to_s) + '.html').to_s | |
33 | end | |
34 | ||
35 | # @param [ Typhoeus::Response, String ] page | |
36 | # @return [ Boolean ] Wether or not the page is a the homepage or a 404 based on its md5sum | |
37 | def homepage_or_404?(page) | |
38 | md5sum = self.class.page_hash(page) | |
39 | ||
40 | [homepage_hash, error_404_hash].include?(md5sum) | |
41 | end | |
42 | end | |
43 | end |
0 | module CMSScanner | |
1 | class Target < WebSite | |
2 | module Platform | |
3 | # Some PHP specific implementation | |
4 | module PHP | |
5 | DEBUG_LOG_PATTERN = /\[[^\]]+\] PHP (?:Warning|Error|Notice):/ | |
6 | FPD_PATTERN = /Fatal error:.+? in (.+?) on/ | |
7 | ERROR_LOG_PATTERN = /PHP Fatal error/i | |
8 | ||
9 | # @param [ String ] path | |
10 | # @param [ Regexp ] pattern | |
11 | # @param [ Hash ] params The request params | |
12 | # | |
13 | # @return [ Boolean ] | |
14 | def log_file?(path, pattern, params = {}) | |
15 | # Only the first 700 bytes of the file are retrieved to avoid getting enture log file | |
16 | # which can be huge (~ 2Go) | |
17 | res = NS::Browser.get(url(path), params.merge(headers: { 'range' => 'bytes=0-700' })) | |
18 | ||
19 | res.body =~ pattern ? true : false | |
20 | end | |
21 | ||
22 | # @param [ String ] path | |
23 | # @param [ Hash ] params The request params | |
24 | # | |
25 | # @return [ Boolean ] true if url(path) is a debug log, false otherwise | |
26 | def debug_log?(path, params = {}) | |
27 | log_file?(path, DEBUG_LOG_PATTERN, params) | |
28 | end | |
29 | ||
30 | # @param [ String ] path | |
31 | # @param [ Hash ] params The request params | |
32 | # | |
33 | # @return [ Boolean ] Wether or not url(path) is an error log file | |
34 | def error_log?(path, params = {}) | |
35 | log_file?(path, ERROR_LOG_PATTERN, params) | |
36 | end | |
37 | ||
38 | # @param [ String ] path | |
39 | # @param [ Hash ] params The request params | |
40 | # | |
41 | # @return [ Boolean ] true if url(path) contains a FPD, false otherwise | |
42 | def full_path_disclosure?(path = nil, params = {}) | |
43 | !full_path_disclosure_entries(path, params).empty? | |
44 | end | |
45 | ||
46 | # @param [ String ] path | |
47 | # @param [ Hash ] params The request params | |
48 | # | |
49 | # @return [ Array<String> ] The FPD found, or an empty array if none | |
50 | def full_path_disclosure_entries(path = nil, params = {}) | |
51 | res = NS::Browser.get(url(path), params) | |
52 | ||
53 | res.body.scan(FPD_PATTERN).flatten | |
54 | end | |
55 | end | |
56 | end | |
57 | end | |
58 | end |
0 | require 'cms_scanner/target/platform/php' |
0 | module CMSScanner | |
1 | # Scope system logic | |
2 | class Target < WebSite | |
3 | # @return [ Array<PublicSuffix::Domain, String> ] | |
4 | def scope | |
5 | @scope ||= Scope.new | |
6 | end | |
7 | ||
8 | # @param [ String ] url An absolute URL | |
9 | # | |
10 | # @return [ Boolean ] true if the url given is in scope | |
11 | def in_scope?(url) | |
12 | scope.include?(Addressable::URI.parse(url.strip).host) | |
13 | rescue StandardError | |
14 | false | |
15 | end | |
16 | ||
17 | # @param [ Typhoeus::Response ] res | |
18 | # @param [ String ] xpath | |
19 | # | |
20 | # @yield [ String, Nokogiri::XML::Element ] The in scope url and its associated tag | |
21 | # | |
22 | # @return [ Array<String> ] The in scope absolute URLs detected in the response's body | |
23 | def in_scope_urls(res, xpath = '//@href|//@src|//@data-src') | |
24 | found = [] | |
25 | ||
26 | urls_from_page(res, xpath) do |url, tag| | |
27 | next unless in_scope?(url) | |
28 | ||
29 | yield url, tag if block_given? | |
30 | ||
31 | found << url | |
32 | end | |
33 | ||
34 | found | |
35 | end | |
36 | ||
37 | # Scope Implementation | |
38 | class Scope | |
39 | # @return [ Array<PublicSuffix::Domain ] The valid domains in scope | |
40 | def domains | |
41 | @domains ||= [] | |
42 | end | |
43 | ||
44 | # @return [ Array<String> ] The invalid domains in scope (such as IP addresses etc) | |
45 | def invalid_domains | |
46 | @invalid_domains ||= [] | |
47 | end | |
48 | ||
49 | def <<(element) | |
50 | if PublicSuffix.valid?(element) | |
51 | domains << PublicSuffix.parse(element) | |
52 | else | |
53 | invalid_domains << element | |
54 | end | |
55 | end | |
56 | ||
57 | # @return [ Boolean ] Wether or not the host is in the scope | |
58 | def include?(host) | |
59 | if PublicSuffix.valid?(host) | |
60 | domain = PublicSuffix.parse(host) | |
61 | ||
62 | domains.each { |d| return true if domain.match(d) } | |
63 | else | |
64 | invalid_domains.each { |d| return true if host == d } | |
65 | end | |
66 | ||
67 | false | |
68 | end | |
69 | end | |
70 | end | |
71 | end |
0 | module CMSScanner | |
1 | class Target < WebSite | |
2 | module Server | |
3 | # Some Apche specific implementation | |
4 | module Apache | |
5 | # @param [ String ] path | |
6 | # @param [ Hash ] params The request params | |
7 | # | |
8 | # @return [ Symbol ] :Apache | |
9 | def server(_path = nil, _params = {}) | |
10 | :Apache | |
11 | end | |
12 | ||
13 | # @param [ String ] path | |
14 | # @param [ Hash ] params The request params | |
15 | # | |
16 | # @return [ Array<String> ] The first level of directories/files listed, | |
17 | # or an empty array if none | |
18 | def directory_listing_entries(path = nil, params = {}) | |
19 | super(path, params, 'td a') | |
20 | end | |
21 | end | |
22 | end | |
23 | end | |
24 | end |
0 | module CMSScanner | |
1 | class Target < WebSite | |
2 | module Server | |
3 | # Generic Server methods | |
4 | module Generic | |
5 | # @param [ String ] path | |
6 | # @param [ Hash ] params The request params | |
7 | # | |
8 | # @return [ Symbol ] The detected remote server (:Apache, :IIS, :Nginx) | |
9 | def server(path = nil, params = {}) | |
10 | headers = headers(path, params) | |
11 | ||
12 | return unless headers | |
13 | ||
14 | case headers[:server] | |
15 | when /\Aapache/i | |
16 | :Apache | |
17 | when /\AMicrosoft-IIS/i | |
18 | :IIS | |
19 | when /\Anginx/ | |
20 | :Nginx | |
21 | end | |
22 | end | |
23 | ||
24 | # @param [ String ] path | |
25 | # @param [ Hash ] params The request params | |
26 | # | |
27 | # @return [ Hash ] The headers | |
28 | def headers(path = nil, params = {}) | |
29 | # The HEAD method might be rejected by some servers ... maybe switch to GET ? | |
30 | NS::Browser.head(url(path), params).headers | |
31 | end | |
32 | ||
33 | # @param [ String ] path | |
34 | # @param [ Hash ] params The request params | |
35 | # | |
36 | # @return [ Boolean ] true if url(path) has the directory | |
37 | # listing enabled, false otherwise | |
38 | def directory_listing?(path = nil, params = {}) | |
39 | res = NS::Browser.get(url(path), params) | |
40 | ||
41 | res.code == 200 && res.body =~ /<h1>Index of/ ? true : false | |
42 | end | |
43 | ||
44 | # @param [ String ] path | |
45 | # @param [ Hash ] params The request params | |
46 | # @param [ String ] selector | |
47 | # @param [ Regexp ] ignore | |
48 | # | |
49 | # @return [ Array<String> ] The first level of directories/files listed, | |
50 | # or an empty array if none | |
51 | def directory_listing_entries( | |
52 | path = nil, params = {}, | |
53 | selector = 'pre a', ignore = /parent directory/i | |
54 | ) | |
55 | return [] unless directory_listing?(path, params) | |
56 | ||
57 | found = [] | |
58 | ||
59 | NS::Browser.get(url(path), params).html.css(selector).each do |node| | |
60 | entry = node.text.to_s | |
61 | ||
62 | next if entry =~ ignore | |
63 | ||
64 | found << entry | |
65 | end | |
66 | ||
67 | found | |
68 | end | |
69 | end | |
70 | end | |
71 | end | |
72 | end |
0 | module CMSScanner | |
1 | class Target < WebSite | |
2 | module Server | |
3 | # Some IIS specific implementation | |
4 | module IIS | |
5 | # @param [ String ] path | |
6 | # @param [ Hash ] params The request params | |
7 | # | |
8 | # @return [ Symbol ] :IIS | |
9 | def server(_path = nil, _params = {}) | |
10 | :IIS | |
11 | end | |
12 | ||
13 | # @param [ String ] path | |
14 | # @param [ Hash ] params The request params | |
15 | # | |
16 | # @return [ Boolean ] true if url(path) has the directory | |
17 | # listing enabled, false otherwise | |
18 | def directory_listing?(path = nil, params = {}) | |
19 | res = NS::Browser.get(url(path), params) | |
20 | ||
21 | res.code == 200 && res.body =~ %r{<H1>#{uri.host} - /} ? true : false | |
22 | end | |
23 | end | |
24 | end | |
25 | end | |
26 | end |
0 | module CMSScanner | |
1 | class Target < WebSite | |
2 | module Server | |
3 | # Some Nginx specific implementation | |
4 | module Nginx | |
5 | # @param [ String ] path | |
6 | # @param [ Hash ] params The request params | |
7 | # | |
8 | # @return [ Symbol ] :Nginx | |
9 | def server(_path = nil, _params = {}) | |
10 | :Nginx | |
11 | end | |
12 | ||
13 | # @param [ String ] path | |
14 | # @param [ Hash ] params The request params | |
15 | # | |
16 | # @return [ Array<String> ] The first level of directories/files listed, | |
17 | # or an empty array if none | |
18 | def directory_listing_entries(path = nil, params = {}) | |
19 | super(path, params, 'pre a', /\A\.\./i) | |
20 | end | |
21 | end | |
22 | end | |
23 | end | |
24 | end |
0 | require 'cms_scanner/target/server/generic' | |
1 | require 'cms_scanner/target/server/apache' | |
2 | require 'cms_scanner/target/server/iis' | |
3 | require 'cms_scanner/target/server/nginx' |
0 | require 'cms_scanner/web_site' | |
1 | require 'cms_scanner/target/platform' | |
2 | require 'cms_scanner/target/server' | |
3 | require 'cms_scanner/target/scope' | |
4 | require 'cms_scanner/target/hashes' | |
5 | ||
6 | module CMSScanner | |
7 | # Target to Scan | |
8 | class Target < WebSite | |
9 | include Server::Generic | |
10 | ||
11 | # @param [ String ] url | |
12 | # @param [ Hash ] opts | |
13 | # @option opts [ Array<PublicSuffix::Domain, String> ] :scope | |
14 | def initialize(url, opts = {}) | |
15 | super(url, opts) | |
16 | ||
17 | scope << uri.host | |
18 | [*opts[:scope]].each { |s| scope << s } | |
19 | end | |
20 | ||
21 | # @param [ Hash ] opts | |
22 | # | |
23 | # @return [ Findings ] | |
24 | def interesting_findings(opts = {}) | |
25 | @interesting_findings ||= NS::Finders::InterestingFindings::Base.find(self, opts) | |
26 | end | |
27 | ||
28 | # Weteher or not vulnerabilities have been found. | |
29 | # Used to set the exit code of the script | |
30 | # and it should be overriden in the implementation | |
31 | # | |
32 | # @return [ Boolean ] | |
33 | def vulnerable? | |
34 | false | |
35 | end | |
36 | ||
37 | # @param [ String ] xpath | |
38 | # @param [ Regexp ] pattern | |
39 | # @param [ Typhoeus::Response, String ] page | |
40 | # | |
41 | # @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ] | |
42 | # @yield [ MatchData, Nokogiri::XML::Element ] | |
43 | def xpath_pattern_from_page(xpath, pattern, page = nil) | |
44 | page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response) | |
45 | matches = [] | |
46 | ||
47 | page.html.xpath(xpath).each do |node| | |
48 | next unless node.text.strip =~ pattern | |
49 | ||
50 | yield Regexp.last_match, node if block_given? | |
51 | ||
52 | matches << [Regexp.last_match, node] | |
53 | end | |
54 | ||
55 | matches | |
56 | end | |
57 | ||
58 | # @param [ Regexp ] pattern | |
59 | # @param [ Typhoeus::Response, String ] page | |
60 | # | |
61 | # @return [ Array<Array<MatchData, Nokogiri::XML::Comment>> ] | |
62 | # @yield [ MatchData, Nokogiri::XML::Comment ] | |
63 | def comments_from_page(pattern, page = nil) | |
64 | xpath_pattern_from_page('//comment()', pattern, page) do |match, node| | |
65 | yield match, node if block_given? | |
66 | end | |
67 | end | |
68 | ||
69 | # @param [ Regexp ] pattern | |
70 | # @param [ Typhoeus::Response, String ] page | |
71 | # | |
72 | # @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ] | |
73 | # @yield [ MatchData, Nokogiri::XML::Element ] | |
74 | def javascripts_from_page(pattern, page = nil) | |
75 | xpath_pattern_from_page('//script', pattern, page) do |match, node| | |
76 | yield match, node if block_given? | |
77 | end | |
78 | end | |
79 | ||
80 | # @param [ Typhoeus::Response, String ] page | |
81 | # @param [ String ] xpath | |
82 | # | |
83 | # @yield [ String, Nokogiri::XML::Element ] The url and its associated tag | |
84 | # | |
85 | # @return [ Array<String> ] The absolute URLs detected in the response's body from the HTML tags | |
86 | def urls_from_page(page = nil, xpath = '//@href|//@src|//@data-src') | |
87 | page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response) | |
88 | found = [] | |
89 | ||
90 | page.html.xpath(xpath).each do |node| | |
91 | attr_value = node.text.to_s | |
92 | ||
93 | next unless attr_value && !attr_value.empty? | |
94 | ||
95 | node_uri = begin | |
96 | uri.join(attr_value.strip) | |
97 | rescue StandardError | |
98 | # Skip potential malformed URLs etc. | |
99 | next | |
100 | end | |
101 | ||
102 | node_uri_string = node_uri.to_s | |
103 | ||
104 | next unless node_uri.host | |
105 | ||
106 | yield node_uri_string, node.parent if block_given? && !found.include?(node_uri_string) | |
107 | ||
108 | found << node_uri_string | |
109 | end | |
110 | ||
111 | found.uniq | |
112 | end | |
113 | end | |
114 | end |
0 | module Typhoeus | |
1 | # Ensure a clean abort of hydra | |
2 | # See https://github.com/typhoeus/typhoeus/issues/439 | |
3 | class Hydra | |
4 | def abort | |
5 | super | |
6 | run | |
7 | end | |
8 | end | |
9 | end |
0 | module Typhoeus | |
1 | # Custom Response class | |
2 | class Response | |
3 | # @return [ Nokogiri::HTML ] The response's body parsed by Nokogiri::HTML | |
4 | def html | |
5 | @html ||= Nokogiri::HTML(body.encode('UTF-8', invalid: :replace, undef: :replace)) | |
6 | end | |
7 | ||
8 | # @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::XML | |
9 | def xml | |
10 | @xml ||= Nokogiri::XML(body.encode('UTF-8', invalid: :replace, undef: :replace)) | |
11 | end | |
12 | end | |
13 | end |
0 | module CMSScanner | |
1 | # Generic Vulnerability | |
2 | class Vulnerability | |
3 | include References | |
4 | ||
5 | attr_reader :title, :type, :fixed_in | |
6 | ||
7 | # @param [ String ] title | |
8 | # @param [ Hash ] references | |
9 | # @option references [ Array<String>, String ] cve | |
10 | # @option references [ Array<String>, String ] secunia | |
11 | # @option references [ Array<String>, String ] osvdb | |
12 | # @option references [ Array<String>, String ] exploitdb | |
13 | # @option references [ Array<String> ] url URL(s) to related advisories etc | |
14 | # @option references [ Array<String>, String ] metasploit The related metasploit module(s) | |
15 | # @param [ String ] type | |
16 | # @param [ String ] fixed_in | |
17 | def initialize(title, references = {}, type = nil, fixed_in = nil) | |
18 | @title = title | |
19 | @type = type | |
20 | @fixed_in = fixed_in | |
21 | ||
22 | self.references = references | |
23 | end | |
24 | ||
25 | # param [ Vulnerability ] other | |
26 | # | |
27 | # @return [ Boolean ] | |
28 | def ==(other) | |
29 | title == other.title && | |
30 | type == other.type && | |
31 | references == other.references && | |
32 | fixed_in == other.fixed_in | |
33 | end | |
34 | end | |
35 | end |
0 | module CMSScanner | |
1 | # WebSite Implementation | |
2 | class WebSite | |
3 | attr_reader :uri, :opts | |
4 | ||
5 | # @param [ String ] site_url | |
6 | # @param [ Hash ] opts | |
7 | def initialize(site_url, opts = {}) | |
8 | self.url = site_url.dup | |
9 | @opts = opts | |
10 | end | |
11 | ||
12 | def url=(site_url) | |
13 | # Add a trailing slash to the site url | |
14 | site_url << '/' if site_url[-1, 1] != '/' | |
15 | ||
16 | # Use the validator to ensure the site_url has a correct format | |
17 | OptParseValidator::OptURL.new([]).validate(site_url) | |
18 | ||
19 | @uri = Addressable::URI.parse(site_url).normalize | |
20 | end | |
21 | ||
22 | # Used for convenience | |
23 | # | |
24 | # URI.encode is preferered over Addressable::URI.encode as it will encode | |
25 | # leading # character: | |
26 | # URI.encode('#t#') => %23t%23 | |
27 | # Addressable::URI.encode('#t#') => #t%23 | |
28 | # | |
29 | # @param [ String ] path Optional path to merge with the uri | |
30 | # | |
31 | # @return [ String ] | |
32 | def url(path = nil) | |
33 | return @uri.to_s unless path | |
34 | ||
35 | @uri.join(URI.encode(path)).to_s | |
36 | end | |
37 | ||
38 | attr_writer :homepage_res | |
39 | ||
40 | # @return [ Typhoeus::Response ] | |
41 | # | |
42 | # As webmock does not support redirects mocking, coverage is ignored | |
43 | # :nocov: | |
44 | def homepage_res | |
45 | @homepage_res ||= NS::Browser.get_and_follow_location(url) | |
46 | end | |
47 | # :nocov: | |
48 | ||
49 | # @return [ String ] | |
50 | def homepage_url | |
51 | @homepage_url ||= homepage_res.effective_url | |
52 | end | |
53 | ||
54 | # Checks if the remote website is up. | |
55 | # | |
56 | # @param [ String ] path | |
57 | # | |
58 | # @return [ Boolean ] | |
59 | def online?(path = nil) | |
60 | NS::Browser.get(url(path)).code.nonzero? ? true : false | |
61 | end | |
62 | ||
63 | # @param [ String ] path | |
64 | # | |
65 | # @return [ Boolean ] | |
66 | def http_auth?(path = nil) | |
67 | NS::Browser.get(url(path)).code == 401 | |
68 | end | |
69 | ||
70 | # @param [ String ] path | |
71 | # | |
72 | # @return [ Boolean ] | |
73 | def access_forbidden?(path = nil) | |
74 | NS::Browser.get(url(path)).code == 403 | |
75 | end | |
76 | ||
77 | # @param [ String ] path | |
78 | # | |
79 | # @return [ Boolean ] | |
80 | def proxy_auth?(path = nil) | |
81 | NS::Browser.get(url(path)).code == 407 | |
82 | end | |
83 | ||
84 | # @param [ String ] url | |
85 | # | |
86 | # @return [ String ] The redirection url or nil | |
87 | # | |
88 | # As webmock does not support redirects mocking, coverage is ignored | |
89 | # :nocov: | |
90 | def redirection(url = nil) | |
91 | url ||= @uri.to_s | |
92 | ||
93 | return unless [301, 302].include?(NS::Browser.get(url).code) | |
94 | ||
95 | res = NS::Browser.get(url, followlocation: true) | |
96 | ||
97 | res.effective_url == url ? nil : res.effective_url | |
98 | end | |
99 | # :nocov: | |
100 | end | |
101 | end |
0 | # Gems | |
1 | require 'typhoeus' | |
2 | require 'nokogiri' | |
3 | require 'yajl/json_gem' | |
4 | require 'public_suffix' | |
5 | require 'addressable/uri' | |
6 | require 'ruby-progressbar' | |
7 | require 'opt_parse_validator' | |
8 | require 'active_support/concern' | |
9 | require 'active_support/inflector' | |
10 | # Standard Libs | |
11 | require 'erb' | |
12 | require 'uri' | |
13 | require 'fileutils' | |
14 | require 'pathname' | |
15 | require 'xmlrpc/client' | |
16 | # Monkey Patches | |
17 | require 'cms_scanner/typhoeus/response' # Adds a Response#html using Nokogiri to parse the body | |
18 | require 'cms_scanner/typhoeus/hydra' # https://github.com/typhoeus/typhoeus/issues/439 | |
19 | require 'cms_scanner/public_suffix/domain' # Adds a Domain#match method and logic, used in scope stuff | |
20 | require 'cms_scanner/numeric' # Adds a Numeric#bytes_to_human | |
21 | require 'cms_scanner/progressbar_null_output' | |
22 | # Custom Libs | |
23 | require 'cms_scanner/helper' | |
24 | require 'cms_scanner/exit_code' | |
25 | require 'cms_scanner/errors/http' | |
26 | require 'cms_scanner/cache/typhoeus' | |
27 | require 'cms_scanner/target' | |
28 | require 'cms_scanner/browser' | |
29 | require 'cms_scanner/version' | |
30 | require 'cms_scanner/controller' | |
31 | require 'cms_scanner/controllers' | |
32 | require 'cms_scanner/formatter' | |
33 | require 'cms_scanner/references' | |
34 | require 'cms_scanner/finders' | |
35 | require 'cms_scanner/vulnerability' | |
36 | ||
37 | # Module | |
38 | module CMSScanner | |
39 | APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path | |
40 | NS = self | |
41 | ||
42 | # Number of requests performed to display at the end of the scan | |
43 | Typhoeus.on_complete do |response| | |
44 | next if response.cached? | |
45 | ||
46 | self.total_requests += 1 | |
47 | ||
48 | NS::Browser.instance.trottle! | |
49 | end | |
50 | ||
51 | # Module to be able to use these class methods when the CMSScanner | |
52 | # is included in another module | |
53 | module ClassMethods | |
54 | # @return [ Integer ] | |
55 | def total_requests | |
56 | @@total_requests ||= 0 | |
57 | end | |
58 | ||
59 | # @param [ Integer ] | |
60 | def total_requests=(value) | |
61 | @@total_requests = value | |
62 | end | |
63 | ||
64 | # The lowercase name of the scanner | |
65 | # Mainly used in directory paths like the default cookie-jar file and | |
66 | # path to load the cli options from files | |
67 | # | |
68 | # @return [ String ] | |
69 | def app_name | |
70 | to_s.underscore | |
71 | end | |
72 | end | |
73 | ||
74 | extend ClassMethods | |
75 | ||
76 | def self.included(base) | |
77 | remove_const(:NS) | |
78 | const_set(:NS, base) | |
79 | ||
80 | base.extend(ClassMethods) | |
81 | super(base) | |
82 | end | |
83 | ||
84 | # Scan | |
85 | class Scan | |
86 | attr_reader :run_error | |
87 | ||
88 | def initialize | |
89 | controllers << NS::Controller::Core.new | |
90 | ||
91 | exit_hook | |
92 | ||
93 | yield self if block_given? | |
94 | end | |
95 | ||
96 | # @return [ Controllers ] | |
97 | def controllers | |
98 | @controllers ||= NS::Controllers.new | |
99 | end | |
100 | ||
101 | def run | |
102 | controllers.run | |
103 | rescue OptParseValidator::NoRequiredOption => e | |
104 | @run_error = e | |
105 | ||
106 | formatter.output('@usage', msg: e.message) | |
107 | rescue StandardError, SignalException => e | |
108 | @run_error = e | |
109 | ||
110 | formatter.output('@scan_aborted', | |
111 | reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message, | |
112 | trace: e.backtrace, | |
113 | verbose: controllers.first.parsed_options[:verbose]) | |
114 | ensure | |
115 | Browser.instance.hydra.abort | |
116 | ||
117 | formatter.beautify | |
118 | end | |
119 | ||
120 | # Used for convenience | |
121 | # @See Formatter | |
122 | def formatter | |
123 | controllers.first.formatter | |
124 | end | |
125 | ||
126 | # @return [ Hash ] | |
127 | def datastore | |
128 | controllers.first.datastore | |
129 | end | |
130 | ||
131 | # Hook to be able to have an exit code returned | |
132 | # depending on the findings / errors | |
133 | def exit_hook | |
134 | at_exit do | |
135 | exit(run_error_exit_code) if run_error | |
136 | ||
137 | controller = controllers.first | |
138 | ||
139 | # The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given | |
140 | exit(NS::ExitCode::VULNERABLE) if controller.parsed_options[:url] && controller.target.vulnerable? | |
141 | exit(NS::ExitCode::OK) | |
142 | end | |
143 | end | |
144 | ||
145 | # @return [ Integer ] The exit code related to the run_error | |
146 | def run_error_exit_code | |
147 | return NS::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) || | |
148 | run_error.is_a?(OptionParser::ParseError) | |
149 | ||
150 | return NS::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt) | |
151 | ||
152 | NS::ExitCode::ERROR | |
153 | end | |
154 | end | |
155 | end | |
156 | ||
157 | require "#{CMSScanner::APP_DIR}/app" |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Controller::Core do | |
3 | subject(:core) { described_class.new } | |
4 | let(:target_url) { 'http://example.com/' } | |
5 | let(:cli_args) { "--url #{target_url}" } | |
6 | let(:parsed_options) { rspec_parsed_options(cli_args) } | |
7 | ||
8 | before do | |
9 | CMSScanner::Browser.reset | |
10 | described_class.parsed_options = parsed_options | |
11 | end | |
12 | ||
13 | describe '#cli_options' do | |
14 | its(:cli_options) { should_not be_empty } | |
15 | its(:cli_options) { should be_a Array } | |
16 | ||
17 | it 'contaisn the expected options' do | |
18 | expect(core.cli_options.map(&:to_sym)).to match_array( | |
19 | %i[ | |
20 | banner cache_dir cache_ttl clear_cache connect_timeout cookie_jar cookie_string | |
21 | detection_mode disable_tls_checks format headers help http_auth ignore_main_redirect | |
22 | max_threads output proxy proxy_auth random_user_agent request_timeout scope | |
23 | throttle url user_agent user_agents_list verbose version vhost | |
24 | ] | |
25 | ) | |
26 | end | |
27 | end | |
28 | ||
29 | describe '#setup_cache' do | |
30 | context 'when no cache_dir supplied (or default)' do | |
31 | it 'returns nil' do | |
32 | expect(core.setup_cache).to eq nil | |
33 | end | |
34 | end | |
35 | ||
36 | context 'when cache_dir' do | |
37 | let(:cli_args) { "#{super()} --cache-dir #{CACHE}" } | |
38 | let(:cache) { Typhoeus::Config.cache } | |
39 | let(:storage) { File.join(parsed_options[:cache_dir], Digest::MD5.hexdigest(target_url)) } | |
40 | ||
41 | before { core.setup_cache } | |
42 | after { Typhoeus::Config.cache = nil } | |
43 | ||
44 | it 'sets up the cache' do | |
45 | expect(cache).to be_a CMSScanner::Cache::Typhoeus | |
46 | expect(cache.storage_path).to eq storage | |
47 | end | |
48 | end | |
49 | end | |
50 | ||
51 | describe 'maybe_output_banner_help_and_version' do | |
52 | before { described_class.option_parser = 'spec' } | |
53 | ||
54 | context 'when --no-banner' do | |
55 | let(:cli_args) { "#{super()} --no-banner" } | |
56 | ||
57 | it 'calls output' do | |
58 | expect(core.formatter).to_not receive(:output) | |
59 | ||
60 | expect { core.maybe_output_banner_help_and_version }.to_not raise_error | |
61 | end | |
62 | end | |
63 | ||
64 | context 'when --help' do | |
65 | let(:cli_args) { '--help' } | |
66 | ||
67 | it 'calls the output' do | |
68 | expect(core.formatter).to receive(:output).with('banner', { verbose: nil }, 'core') | |
69 | expect(core.formatter).to receive(:output).with('help', hash_including(help: 'spec'), 'core') | |
70 | ||
71 | expect { core.maybe_output_banner_help_and_version }.to raise_error(SystemExit) | |
72 | end | |
73 | end | |
74 | ||
75 | context 'when --version' do | |
76 | let(:cli_args) { "#{super()} --version" } | |
77 | ||
78 | it 'calls the output' do | |
79 | expect(core.formatter).to receive(:output).with('banner', { verbose: nil }, 'core') | |
80 | expect(core.formatter).to receive(:output).with('version', { verbose: nil }, 'core') | |
81 | ||
82 | expect { core.maybe_output_banner_help_and_version }.to raise_error(SystemExit) | |
83 | end | |
84 | end | |
85 | end | |
86 | ||
87 | describe '#before_scan' do | |
88 | context 'when --no-banner' do | |
89 | let(:cli_args) { "#{super()} --no-banner" } | |
90 | ||
91 | before { expect(core.formatter).to_not receive(:output) } | |
92 | ||
93 | it 'does not raise an error when everything is fine' do | |
94 | stub_request(:get, target_url).to_return(status: 200) | |
95 | ||
96 | expect { core.before_scan }.to_not raise_error | |
97 | end | |
98 | end | |
99 | ||
100 | context 'when --banner (default)' do | |
101 | before { expect(core.formatter).to receive(:output) } | |
102 | ||
103 | it 'does not raise an error when everything is fine' do | |
104 | stub_request(:get, target_url).to_return(status: 200) | |
105 | ||
106 | expect { core.before_scan }.to_not raise_error | |
107 | end | |
108 | ||
109 | it 'raise an error when the site is down' do | |
110 | stub_request(:get, target_url).to_return(status: 0) | |
111 | ||
112 | expect { core.before_scan } | |
113 | .to raise_error( | |
114 | CMSScanner::TargetDownError, | |
115 | "The url supplied '#{target_url}' seems to be down ()" | |
116 | ) | |
117 | end | |
118 | ||
119 | context 'when it redirects' do | |
120 | before do | |
121 | stub_request(:get, target_url).to_return(status: 301, headers: { location: redirection }) | |
122 | ||
123 | expect(core.target).to receive(:homepage_res).and_return(Typhoeus::Response.new(effective_url: redirection)) | |
124 | end | |
125 | ||
126 | context 'when out of scope' do | |
127 | let(:redirection) { 'http://somewhere.com/' } | |
128 | ||
129 | context 'when the --ignore-main-redirect is not supplied' do | |
130 | it 'raises an error' do | |
131 | expect { core.before_scan }.to raise_error( | |
132 | CMSScanner::HTTPRedirectError, | |
133 | "The URL supplied redirects to #{redirection}." \ | |
134 | ' Use the --ignore-main-redirect option to ignore the redirection and scan the target.' | |
135 | ) | |
136 | end | |
137 | end | |
138 | ||
139 | context 'when the --ignore-main-redirect is supplied' do | |
140 | let(:cli_args) { "#{super()} --ignore-main-redirect" } | |
141 | ||
142 | it 'does not raise any error' do | |
143 | expect { core.before_scan }.to_not raise_error | |
144 | expect(core.target.url).to eql target_url | |
145 | ||
146 | expect(core.target).to receive(:homepage_res).and_call_original | |
147 | expect(core.target.homepage_url).to eql target_url | |
148 | end | |
149 | end | |
150 | end | |
151 | ||
152 | context 'when in scope' do | |
153 | let(:redirection) { "#{target_url}home" } | |
154 | ||
155 | it 'does not raise any error' do | |
156 | expect { core.before_scan }.to_not raise_error | |
157 | expect(core.target.url).to eql target_url | |
158 | ||
159 | # expect(core.target).to receive(:homepage_res).and_call_original | |
160 | # expect(core.target.homepage_url).to eql redirection # Doesn't work, no idea why :x | |
161 | end | |
162 | end | |
163 | end | |
164 | ||
165 | context 'when access is forbidden' do | |
166 | before { stub_request(:get, target_url).to_return(status: 403) } | |
167 | ||
168 | it 'raises an error' do | |
169 | expect { core.before_scan }.to raise_error(CMSScanner::AccessForbiddenError) | |
170 | end | |
171 | end | |
172 | ||
173 | # This is quite a mess (as Webmock doesn't issue itself another 401 | |
174 | # when credential are incorrect :/) | |
175 | context 'when http authentication' do | |
176 | context 'when no credentials' do | |
177 | before { stub_request(:get, target_url).to_return(status: 401) } | |
178 | ||
179 | it 'raises an error' do | |
180 | expect { core.before_scan }.to raise_error(CMSScanner::HTTPAuthRequiredError) | |
181 | end | |
182 | end | |
183 | ||
184 | context 'when credentials' do | |
185 | context 'when valid' do | |
186 | before { stub_request(:get, 'http://example.com').with(basic_auth: %w[user pass]) } | |
187 | ||
188 | let(:cli_args) { "#{super()} --http-auth user:pass" } | |
189 | ||
190 | it 'does not raise any error' do | |
191 | expect { core.before_scan }.to_not raise_error | |
192 | end | |
193 | end | |
194 | ||
195 | context 'when invalid' do | |
196 | before do | |
197 | stub_request(:get, 'http://example.com') | |
198 | .with(basic_auth: %w[user p@ss]).to_return(status: 401) | |
199 | end | |
200 | ||
201 | let(:cli_args) { "#{super()} --http-auth user:p@ss" } | |
202 | ||
203 | it 'raises an error' do | |
204 | expect { core.before_scan }.to raise_error(CMSScanner::HTTPAuthRequiredError) | |
205 | end | |
206 | end | |
207 | end | |
208 | end | |
209 | ||
210 | context 'when proxy authentication' do | |
211 | before { stub_request(:get, target_url).to_return(status: 407) } | |
212 | ||
213 | context 'when no credentials' do | |
214 | it 'raises an error' do | |
215 | expect { core.before_scan }.to raise_error(CMSScanner::ProxyAuthRequiredError) | |
216 | end | |
217 | end | |
218 | ||
219 | context 'when invalid credentials' do | |
220 | let(:cli_args) { "#{super()} --proxy-auth user:p@ss" } | |
221 | ||
222 | it 'raises an error' do | |
223 | expect(CMSScanner::Browser.instance.proxy_auth).to eq(parsed_options[:proxy_auth]) | |
224 | ||
225 | expect { core.before_scan }.to raise_error(CMSScanner::ProxyAuthRequiredError) | |
226 | end | |
227 | end | |
228 | ||
229 | context 'when valid credentials' do | |
230 | before { stub_request(:get, target_url) } | |
231 | ||
232 | let(:cli_args) { "#{super()} --proxy-auth user:pass" } | |
233 | ||
234 | it 'raises an error' do | |
235 | expect(CMSScanner::Browser.instance.proxy_auth).to eq(parsed_options[:proxy_auth]) | |
236 | ||
237 | expect { core.before_scan }.to_not raise_error | |
238 | end | |
239 | end | |
240 | end | |
241 | end | |
242 | end | |
243 | ||
244 | describe '#run' do | |
245 | it 'calls the formatter with the correct parameters' do | |
246 | expect(core.formatter).to receive(:output) | |
247 | .with('started', | |
248 | hash_including(:start_memory, :start_time, :verbose, url: target_url), | |
249 | 'core') | |
250 | ||
251 | core.run | |
252 | end | |
253 | end | |
254 | ||
255 | describe '#after_scan' do | |
256 | let(:keys) { %i[verbose start_time stop_time start_memory elapsed used_memory] } | |
257 | ||
258 | it 'calls the formatter with the correct parameters' do | |
259 | # Call the #run once to ensure that @start_time & @start_memory are set | |
260 | expect(core).to receive(:output).with('started', hash_including(url: target_url)) | |
261 | core.run | |
262 | ||
263 | RSpec::Mocks.space.proxy_for(core).reset # Must reset due to the above statements | |
264 | ||
265 | expect(core.formatter).to receive(:output) | |
266 | .with('finished', hash_including(*keys), 'core') | |
267 | ||
268 | core.after_scan | |
269 | end | |
270 | end | |
271 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Controller::InterestingFindings do | |
3 | subject(:controller) { described_class.new } | |
4 | let(:target_url) { 'http://example.com/' } | |
5 | let(:cli_args) { "--url #{target_url}" } | |
6 | let(:parsed_options) { rspec_parsed_options(cli_args) } | |
7 | ||
8 | before do | |
9 | CMSScanner::Browser.reset | |
10 | described_class.parsed_options = parsed_options | |
11 | end | |
12 | ||
13 | its(:before_scan) { should be_nil } | |
14 | its(:after_scan) { should be_nil } | |
15 | ||
16 | describe '#cli_options' do | |
17 | its(:cli_options) { should_not be_empty } | |
18 | its(:cli_options) { should be_a Array } | |
19 | ||
20 | it 'contains to correct options' do | |
21 | expect(controller.cli_options.map(&:to_sym)).to eq [:interesting_findings_detection] | |
22 | end | |
23 | end | |
24 | ||
25 | describe '#run' do | |
26 | before do | |
27 | expect(controller.target).to receive(:interesting_findings) | |
28 | .with( | |
29 | mode: parsed_options[:interesting_findings_detection] || parsed_options[:detection_mode] | |
30 | ).and_return(stubbed) | |
31 | end | |
32 | ||
33 | after { controller.run } | |
34 | ||
35 | %i[mixed passive aggressive].each do |mode| | |
36 | context "when --detection-mode #{mode}" do | |
37 | let(:cli_args) { "#{super()} --detection-mode #{mode}" } | |
38 | ||
39 | context 'when no findings' do | |
40 | let(:stubbed) { [] } | |
41 | ||
42 | before { expect(controller.formatter).to_not receive(:output) } | |
43 | ||
44 | it 'does not call the formatter' do | |
45 | # Handled by the before statements above | |
46 | end | |
47 | ||
48 | context 'when --interesting-files-detection mode supplied' do | |
49 | let(:cli_args) { "#{super()} --interesting-findings-detection passive" } | |
50 | ||
51 | it 'gives the correct detection paramter' do | |
52 | # Handled by before/after statements | |
53 | end | |
54 | end | |
55 | end | |
56 | ||
57 | context 'when findings' do | |
58 | let(:stubbed) { ['yolo'] } | |
59 | ||
60 | it 'calls the formatter with the correct parameter' do | |
61 | expect(controller.formatter).to receive(:output) | |
62 | .with('findings', hash_including(findings: stubbed), 'interesting_findings') | |
63 | end | |
64 | end | |
65 | end | |
66 | end | |
67 | end | |
68 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::InterestingFindings::FantasticoFileslist do | |
3 | subject(:finder) { described_class.new(target) } | |
4 | let(:target) { CMSScanner::Target.new(url) } | |
5 | let(:url) { 'http://example.com/' } | |
6 | let(:file) { url + 'fantastico_fileslist.txt' } | |
7 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'fantastico_fileslist') } | |
8 | ||
9 | describe '#url' do | |
10 | its(:url) { should eq file } | |
11 | end | |
12 | ||
13 | describe '#aggressive' do | |
14 | after do | |
15 | stub_request(:get, file).to_return(status: status, body: body, headers: headers) | |
16 | ||
17 | result = finder.aggressive | |
18 | ||
19 | expect(result).to be_a CMSScanner::FantasticoFileslist if @expected | |
20 | expect(result).to eql @expected | |
21 | end | |
22 | ||
23 | let(:body) { '' } | |
24 | let(:headers) { { 'Content-Type' => 'text/html ' } } | |
25 | ||
26 | context 'when 404' do | |
27 | let(:status) { 404 } | |
28 | ||
29 | it 'returns nil' do | |
30 | @expected = nil | |
31 | end | |
32 | end | |
33 | ||
34 | context 'when 200' do | |
35 | let(:status) { 200 } | |
36 | ||
37 | context 'when the body is empty' do | |
38 | it 'returns nil' do | |
39 | @expected = nil | |
40 | end | |
41 | end | |
42 | ||
43 | context 'when not a text/plain Content-Type' do | |
44 | let(:body) { 'not an empty body' } | |
45 | ||
46 | it 'returns nil' do | |
47 | @expected = nil | |
48 | end | |
49 | end | |
50 | ||
51 | context 'when the body matches and Content-Type = text/plain' do | |
52 | let(:body) { File.read(fixtures.join('fantastico_fileslist.txt')) } | |
53 | let(:headers) { { 'Content-Type' => 'text/plain' } } | |
54 | ||
55 | it 'returns the InterestingFinding result' do | |
56 | @expected = CMSScanner::FantasticoFileslist.new( | |
57 | file, | |
58 | confidence: 70, | |
59 | found_by: 'Fantastico Fileslist (Aggressive Detection)' | |
60 | ) | |
61 | end | |
62 | end | |
63 | end | |
64 | end | |
65 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::InterestingFindings::Headers do | |
3 | subject(:finder) { described_class.new(target) } | |
4 | let(:target) { CMSScanner::Target.new(url) } | |
5 | let(:url) { 'http://example.com/' } | |
6 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'headers') } | |
7 | let(:fixture) { fixtures.join('interesting.txt') } | |
8 | let(:headers) { parse_headers_file(fixture) } | |
9 | ||
10 | describe '#passive' do | |
11 | before { stub_request(:get, url).to_return(headers: headers) } | |
12 | ||
13 | after do | |
14 | if @expected | |
15 | result = finder.passive | |
16 | ||
17 | expect(result).to be_a CMSScanner::Headers | |
18 | expect(result).to eql @expected | |
19 | end | |
20 | end | |
21 | ||
22 | context 'when no headers' do | |
23 | let(:headers) { {} } | |
24 | ||
25 | its(:passive) { should be nil } | |
26 | end | |
27 | ||
28 | context 'when headers' do | |
29 | it 'returns the result' do | |
30 | opts = { confidence: 100, found_by: 'Headers (Passive Detection)' } | |
31 | @expected = CMSScanner::Headers.new(url, opts) | |
32 | end | |
33 | end | |
34 | end | |
35 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::InterestingFindings::RobotsTxt do | |
3 | subject(:finder) { described_class.new(target) } | |
4 | let(:target) { CMSScanner::Target.new(url) } | |
5 | let(:url) { 'http://example.com/' } | |
6 | let(:robots_txt) { url + 'robots.txt' } | |
7 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'robots_txt') } | |
8 | ||
9 | describe '#url' do | |
10 | its(:url) { should eq robots_txt } | |
11 | end | |
12 | ||
13 | describe '#aggressive' do | |
14 | after do | |
15 | stub_request(:get, robots_txt).to_return(status: status, body: body) | |
16 | ||
17 | result = finder.aggressive | |
18 | ||
19 | expect(result).to be_a CMSScanner::RobotsTxt if @expected | |
20 | expect(finder.aggressive).to eql @expected | |
21 | end | |
22 | ||
23 | let(:body) { '' } | |
24 | ||
25 | context 'when 404' do | |
26 | let(:status) { 404 } | |
27 | ||
28 | it 'returns nil' do | |
29 | @expected = nil | |
30 | end | |
31 | end | |
32 | ||
33 | context 'when 200' do | |
34 | let(:status) { 200 } | |
35 | ||
36 | context 'when the body is empty' do | |
37 | it 'returns nil' do | |
38 | @expected = nil | |
39 | end | |
40 | end | |
41 | ||
42 | context 'when the body matches a robots.txt' do | |
43 | let(:body) { File.read(fixtures.join('robots.txt')) } | |
44 | ||
45 | it 'returns the InterestingFinding result' do | |
46 | @expected = CMSScanner::RobotsTxt.new(robots_txt, | |
47 | confidence: 100, | |
48 | found_by: 'Robots Txt (Aggressive Detection)') | |
49 | end | |
50 | end | |
51 | end | |
52 | end | |
53 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::InterestingFindings::SearchReplaceDB2 do | |
3 | subject(:finder) { described_class.new(target) } | |
4 | let(:target) { CMSScanner::Target.new(url) } | |
5 | let(:url) { 'http://example.com/' } | |
6 | let(:file) { url + 'searchreplacedb2.php' } | |
7 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'search_replace_db_2') } | |
8 | ||
9 | describe '#url' do | |
10 | its(:url) { should eq file } | |
11 | end | |
12 | ||
13 | describe '#aggressive' do | |
14 | after do | |
15 | stub_request(:get, file).to_return(status: status, body: body) | |
16 | ||
17 | expect(finder.aggressive).to eql @expected | |
18 | end | |
19 | ||
20 | let(:body) { '' } | |
21 | ||
22 | context 'when 404' do | |
23 | let(:status) { 404 } | |
24 | ||
25 | it 'returns nil' do | |
26 | @expected = nil | |
27 | end | |
28 | end | |
29 | ||
30 | context 'when 200' do | |
31 | let(:status) { 200 } | |
32 | ||
33 | context 'when the body is empty' do | |
34 | it 'returns nil' do | |
35 | @expected = nil | |
36 | end | |
37 | end | |
38 | ||
39 | context 'when the body matches' do | |
40 | let(:body) { File.read(fixtures.join('searchreplacedb2.php')) } | |
41 | ||
42 | it 'returns the InterestingFinding result' do | |
43 | @expected = CMSScanner::InterestingFinding.new( | |
44 | file, | |
45 | confidence: 100, | |
46 | found_by: 'Search Replace Db2 (Aggressive Detection)' | |
47 | ) | |
48 | end | |
49 | end | |
50 | end | |
51 | end | |
52 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::InterestingFindings::XMLRPC do | |
3 | subject(:finder) { described_class.new(target) } | |
4 | let(:target) { CMSScanner::Target.new(url) } | |
5 | let(:url) { 'http://e.org/' } | |
6 | let(:xml_rpc_url) { url + 'xmlrpc.php' } | |
7 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'xml_rpc') } | |
8 | ||
9 | describe '#potential_urls' do | |
10 | its(:potential_urls) { should be_empty } | |
11 | end | |
12 | ||
13 | describe '#passive' do | |
14 | before do | |
15 | expect(finder).to receive(:passive_headers).and_return(headers_stub) | |
16 | expect(finder).to receive(:passive_body).and_return(body_stub) | |
17 | end | |
18 | ||
19 | context 'when both passives return nil' do | |
20 | let(:headers_stub) { nil } | |
21 | let(:body_stub) { nil } | |
22 | ||
23 | its(:passive) { should be_empty } | |
24 | end | |
25 | ||
26 | context 'when one passive is not nil' do | |
27 | let(:headers_stub) { nil } | |
28 | let(:body_stub) { 'test' } | |
29 | ||
30 | its(:passive) { should eq %w[test] } | |
31 | end | |
32 | end | |
33 | ||
34 | describe '#passive_headers' do | |
35 | before { stub_request(:get, url).to_return(headers: headers) } | |
36 | ||
37 | let(:headers) { {} } | |
38 | ||
39 | context 'when no headers' do | |
40 | its(:passive_headers) { should be_nil } | |
41 | end | |
42 | ||
43 | context 'when headers' do | |
44 | context 'when URL is out of scope' do | |
45 | let(:headers) { { 'X-Pingback' => 'http://ex.org/yolo' } } | |
46 | ||
47 | its(:passive_headers) { should be_nil } | |
48 | end | |
49 | ||
50 | context 'when URL is in scope' do | |
51 | let(:headers) { { 'X-Pingback' => xml_rpc_url } } | |
52 | ||
53 | it 'adds the url to #potential_urls and returns the XMLRPC' do | |
54 | result = finder.passive_headers | |
55 | ||
56 | expect(finder.potential_urls).to eq [xml_rpc_url] | |
57 | ||
58 | expect(result).to be_a CMSScanner::XMLRPC | |
59 | expect(result).to eql CMSScanner::XMLRPC.new( | |
60 | xml_rpc_url, | |
61 | confidence: 30, | |
62 | found_by: 'Headers (Passive Detection)' | |
63 | ) | |
64 | end | |
65 | end | |
66 | end | |
67 | end | |
68 | ||
69 | describe '#passive_body' do | |
70 | before { stub_request(:get, url).to_return(body: body) } | |
71 | ||
72 | context 'when no link rel="pingback" tag' do | |
73 | let(:body) { '' } | |
74 | ||
75 | its(:passive_body) { should be_nil } | |
76 | end | |
77 | ||
78 | context 'when the tag is present' do | |
79 | context 'when the URL is out of scope' do | |
80 | let(:body) { File.read(fixtures.join('homepage_out_of_scope_pingback.html')) } | |
81 | ||
82 | its(:passive_body) { should be_nil } | |
83 | end | |
84 | ||
85 | context 'when URL is in scope' do | |
86 | let(:body) { File.read(fixtures.join('homepage_in_scope_pingback.html')) } | |
87 | let(:expected_url) { 'http://e.org/wp/xmlrpc.php' } | |
88 | ||
89 | it 'adds the URL to the #potential_urls and returns the XMLRPC' do | |
90 | result = finder.passive_body | |
91 | ||
92 | expect(finder.potential_urls).to eq [expected_url] | |
93 | ||
94 | expect(result).to be_a CMSScanner::XMLRPC | |
95 | expect(result).to eql CMSScanner::XMLRPC.new( | |
96 | expected_url, | |
97 | confidence: 30, | |
98 | found_by: 'Link Tag (Passive Detection)' | |
99 | ) | |
100 | end | |
101 | end | |
102 | end | |
103 | end | |
104 | ||
105 | describe '#aggressive' do | |
106 | # Adds an out of scope URL which should be ignored | |
107 | before { finder.potential_urls << 'htpp://ex.org' } | |
108 | ||
109 | after do | |
110 | stub_request(:get, xml_rpc_url).to_return(body: body) | |
111 | ||
112 | expect(finder.aggressive).to eql @expected | |
113 | end | |
114 | ||
115 | context 'when the body does not match' do | |
116 | let(:body) { '' } | |
117 | ||
118 | it 'returns nil' do | |
119 | @expected = nil | |
120 | end | |
121 | end | |
122 | ||
123 | context 'when the body matches' do | |
124 | let(:body) { File.read(fixtures.join('xmlrpc.php')) } | |
125 | ||
126 | it 'returns the InterestingFinding result' do | |
127 | @expected = CMSScanner::XMLRPC.new( | |
128 | xml_rpc_url, | |
129 | confidence: 100, | |
130 | found_by: described_class::DIRECT_ACCESS | |
131 | ) | |
132 | end | |
133 | end | |
134 | end | |
135 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::InterestingFindings::Base do | |
3 | it_behaves_like CMSScanner::Finders::IndependentFinder do | |
4 | let(:expected_finders) { %w[Headers RobotsTxt FantasticoFileslist SearchReplaceDB2 XMLRPC] } | |
5 | let(:expected_finders_class) { CMSScanner::Finders::IndependentFinders } | |
6 | end | |
7 | ||
8 | subject(:files) { described_class.new(target) } | |
9 | let(:target) { CMSScanner::Target.new(url) } | |
10 | let(:url) { 'http://example.com/' } | |
11 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Formatter::CliNoColour do | |
3 | subject(:formatter) { described_class.new } | |
4 | ||
5 | its(:format) { should eq 'cli' } | |
6 | its(:user_interaction?) { should be true } | |
7 | ||
8 | describe '#colorize' do | |
9 | it 'returns the text w/o any colour' do | |
10 | expect(formatter.red('Text')).to eq 'Text' | |
11 | end | |
12 | end | |
13 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Formatter::Cli do | |
3 | subject(:formatter) { described_class.new } | |
4 | ||
5 | its(:format) { should eq 'cli' } | |
6 | its(:user_interaction?) { should be true } | |
7 | ||
8 | describe '#bold, #red, #green, #amber, #blue, #colorize' do | |
9 | it 'returns the correct bold string' do | |
10 | expect(formatter.bold('Text')).to eq "\e[1mText\e[0m" | |
11 | end | |
12 | ||
13 | it 'returns the correct red string' do | |
14 | expect(formatter.red('Text')).to eq "\e[31mText\e[0m" | |
15 | end | |
16 | ||
17 | it 'returns the correct green string' do | |
18 | expect(formatter.green('Another Text')).to eq "\e[32mAnother Text\e[0m" | |
19 | end | |
20 | ||
21 | it 'returns the correct amber string' do | |
22 | expect(formatter.amber('Text')).to eq "\e[33mText\e[0m" | |
23 | end | |
24 | ||
25 | it 'returns the correct blue string' do | |
26 | expect(formatter.blue('Text')).to eq "\e[34mText\e[0m" | |
27 | end | |
28 | end | |
29 | ||
30 | describe '#*_icon' do | |
31 | { | |
32 | info: "\e[32m[+]\e[0m", | |
33 | notice: "\e[34m[i]\e[0m", | |
34 | warning: "\e[33m[!]\e[0m", | |
35 | critical: "\e[31m[!]\e[0m" | |
36 | }.each do |icon_type, expected| | |
37 | it "returns the correct #{icon_type} icon" do | |
38 | expect(formatter.send("#{icon_type}_icon")).to eql expected | |
39 | end | |
40 | end | |
41 | end | |
42 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Formatter::Json do | |
3 | it_behaves_like CMSScanner::Formatter::Buffer | |
4 | ||
5 | subject(:formatter) { described_class.new } | |
6 | let(:output_file) { FIXTURES.join('output.txt') } | |
7 | ||
8 | before { formatter.views_directories << FIXTURES_VIEWS } | |
9 | ||
10 | its(:format) { should eq 'json' } | |
11 | its(:user_interaction?) { should be false } | |
12 | ||
13 | describe '#output' do | |
14 | it 'puts the rendered text in the buffer' do | |
15 | 2.times { formatter.output('@render_me', test: 'Working') } | |
16 | ||
17 | expect(formatter.buffer).to eq "\"test\": \"Working\",\n" * 2 | |
18 | end | |
19 | end | |
20 | ||
21 | describe '#beautify' do | |
22 | it 'writes the buffer in the $stdout' do | |
23 | 2.times { formatter.output('@render_me', test: 'yolo') } | |
24 | ||
25 | expect($stdout).to receive(:puts).with(JSON.pretty_generate(JSON.parse('{"test": "yolo"}'))) | |
26 | formatter.beautify | |
27 | end | |
28 | ||
29 | context 'when invalid UTF-8 chars' do | |
30 | it 'tries to convert/replace them' do | |
31 | formatter.output('@render_me', test: 'it’s'.encode('CP1252')) | |
32 | ||
33 | expect($stdout).to receive(:puts).with(JSON.pretty_generate(JSON.parse('{"test": "it�s"}'))) | |
34 | formatter.beautify | |
35 | end | |
36 | end | |
37 | end | |
38 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::FantasticoFileslist do | |
3 | subject(:file) { described_class.new(url) } | |
4 | let(:url) { 'http://example.com/robots.txt' } | |
5 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'fantastico_fileslist') } | |
6 | ||
7 | describe '#interesting_entries' do | |
8 | let(:headers) { { 'Content-Type' => 'text/plain; charset=utf-8' } } | |
9 | ||
10 | after do | |
11 | body = File.read(fixtures.join(fixture)) | |
12 | ||
13 | stub_request(:get, file.url).to_return(headers: headers, body: body) | |
14 | ||
15 | expect(file.interesting_entries).to eq @expected | |
16 | end | |
17 | ||
18 | context 'when empty or / entries' do | |
19 | let(:fixture) { 'fantastico_fileslist.txt' } | |
20 | ||
21 | it 'ignores them and only returns the others' do | |
22 | @expected = %w[data.sql admin.txt] | |
23 | end | |
24 | end | |
25 | end | |
26 | ||
27 | describe '#references' do | |
28 | its(:references) { should_not be_nil } | |
29 | end | |
30 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Headers do | |
3 | subject(:file) { described_class.new(url) } | |
4 | let(:url) { 'http://example.com/' } | |
5 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'headers') } | |
6 | let(:fixture) { fixtures.join('interesting.txt') } | |
7 | let(:headers) { {} } | |
8 | ||
9 | before { stub_request(:get, file.url).to_return(headers: headers) } | |
10 | ||
11 | describe '#known_headers' do | |
12 | it 'does not contains dupliactes' do | |
13 | expect(file.known_headers).to eql file.known_headers.uniq | |
14 | end | |
15 | end | |
16 | ||
17 | describe '#entries' do | |
18 | after { expect(file.entries).to eq @expected if @expected } | |
19 | ||
20 | context 'when no headers' do | |
21 | its(:entries) { should eq({}) } | |
22 | end | |
23 | ||
24 | context 'when headers' do | |
25 | let(:headers) { parse_headers_file(fixture) } | |
26 | ||
27 | it 'returns the headers' do | |
28 | @expected = headers | |
29 | end | |
30 | end | |
31 | end | |
32 | ||
33 | describe '#interesting_entries' do | |
34 | after { expect(file.interesting_entries).to eq @expected if @expected } | |
35 | ||
36 | context 'when interesting headers' do | |
37 | let(:headers) { parse_headers_file(fixture) } | |
38 | ||
39 | it 'returns an array with the headers' do | |
40 | @expected = ['Server: nginx/1.1.19', 'X-Powered-By: ASP.NET, PHP', 'X-Article-Id: 12'] | |
41 | end | |
42 | end | |
43 | ||
44 | context 'when no interesting headers' do | |
45 | let(:headers) { parse_headers_file(fixtures.join('no_interesting.txt')) } | |
46 | ||
47 | its(:interesting_entries) { should eq [] } | |
48 | end | |
49 | end | |
50 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::InterestingFinding do | |
3 | it_behaves_like CMSScanner::Finders::Finding | |
4 | ||
5 | subject(:finding) { described_class.new(url, opts) } | |
6 | let(:opts) { {} } | |
7 | let(:url) { 'http://example.com/' } | |
8 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings') } | |
9 | ||
10 | describe '#to_s' do | |
11 | context 'when no opts[:to_s]' do | |
12 | its(:to_s) { should eql url } | |
13 | ||
14 | context 'when setter used' do | |
15 | it 'returns the value from the setter' do | |
16 | finding.to_s = 'also works' | |
17 | ||
18 | expect(finding.to_s).to eql 'also works' | |
19 | end | |
20 | end | |
21 | end | |
22 | ||
23 | context 'when opts[:to_s]' do | |
24 | let(:opts) { super().merge(to_s: 'works') } | |
25 | ||
26 | its(:to_s) { should eql 'works' } | |
27 | ||
28 | context 'when setter used' do | |
29 | it 'returns the value from the setter' do | |
30 | finding.to_s = 'also works' | |
31 | ||
32 | expect(finding.to_s).to eql 'also works' | |
33 | end | |
34 | end | |
35 | end | |
36 | end | |
37 | ||
38 | describe '#entries' do | |
39 | after do | |
40 | stub_request(:get, finding.url).to_return(headers: headers, body: @body) | |
41 | ||
42 | expect(finding.entries).to eq @expected | |
43 | end | |
44 | ||
45 | context 'when content-type matches text/plain' do | |
46 | let(:headers) { { 'Content-Type' => 'text/plain; charset=utf-8' } } | |
47 | ||
48 | it 'returns the finding content as an array w/o empty strings' do | |
49 | @body = File.read(fixtures.join('file.txt')) | |
50 | @expected = ['This is', 'a test file', 'with some content'] | |
51 | end | |
52 | end | |
53 | ||
54 | context 'when other content-type' do | |
55 | let(:headers) { { 'Content-Type' => 'text.html; charset=utf-8' } } | |
56 | ||
57 | it 'returns an empty array' do | |
58 | @expected = [] | |
59 | end | |
60 | end | |
61 | end | |
62 | ||
63 | describe '#==' do | |
64 | context 'when same URL' do | |
65 | context 'when the same #to_s' do | |
66 | it 'returns true' do | |
67 | expect(finding == described_class.new(url)).to be true | |
68 | end | |
69 | end | |
70 | ||
71 | context 'when different #to_s' do | |
72 | it 'returns false' do | |
73 | expect(finding == described_class.new(url, to_s: 'another')).to be false | |
74 | end | |
75 | end | |
76 | end | |
77 | ||
78 | context 'when not the same URL' do | |
79 | it 'returns false' do | |
80 | expect(finding == described_class.new('http://e.org')).to be false | |
81 | end | |
82 | end | |
83 | end | |
84 | ||
85 | describe '#<=>' do | |
86 | context 'when same URL' do | |
87 | it 'returns 0' do | |
88 | expect(finding <=> described_class.new(url)).to eql 0 | |
89 | end | |
90 | end | |
91 | ||
92 | context 'when the other URL <= current one' do | |
93 | it 'returns 1' do | |
94 | expect(finding <=> described_class.new('http://e.org')).to eql 1 | |
95 | end | |
96 | end | |
97 | ||
98 | context 'when the other URL >= current one' do | |
99 | it 'returns -1' do | |
100 | expect(finding <=> described_class.new('http://exi.org/')).to eql(-1) | |
101 | end | |
102 | end | |
103 | ||
104 | context 'when using capitals' do | |
105 | it 'returns ' do | |
106 | expect(finding <=> described_class.new('Sftp://a.org')).to eql(-1) | |
107 | end | |
108 | end | |
109 | end | |
110 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::RobotsTxt do | |
3 | subject(:file) { described_class.new(url) } | |
4 | let(:url) { 'http://example.com/robots.txt' } | |
5 | let(:fixtures) { FIXTURES_FINDERS.join('interesting_findings', 'robots_txt') } | |
6 | ||
7 | describe '#interesting_entries' do | |
8 | let(:headers) { { 'Content-Type' => 'text/plain; charset=utf-8' } } | |
9 | ||
10 | after do | |
11 | body = File.read(fixtures.join(fixture)) | |
12 | ||
13 | stub_request(:get, file.url).to_return(headers: headers, body: body) | |
14 | ||
15 | expect(file.interesting_entries).to eq @expected | |
16 | end | |
17 | ||
18 | context 'when empty or / entries' do | |
19 | let(:fixture) { 'robots.txt' } | |
20 | ||
21 | it 'ignores them and only returns the others w/o duplicate' do | |
22 | @expected = %w[/admin /public/home] | |
23 | end | |
24 | end | |
25 | end | |
26 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::User do | |
3 | subject(:user) { described_class.new(username, opts) } | |
4 | let(:username) { 'john' } | |
5 | let(:opts) { {} } | |
6 | ||
7 | describe '#new' do | |
8 | its(:username) { should eql username } | |
9 | ||
10 | context 'when opts' do | |
11 | let(:opts) { super().merge(id: 12, password: 'passwd') } | |
12 | ||
13 | its(:id) { should eql 12 } | |
14 | its(:password) { should eql 'passwd' } | |
15 | end | |
16 | end | |
17 | ||
18 | describe '#==' do | |
19 | context 'when another object' do | |
20 | it 'returns false' do | |
21 | expect(user == username).to be false | |
22 | end | |
23 | end | |
24 | ||
25 | context 'when the same username' do | |
26 | it 'return true' do | |
27 | expect(user == user.dup).to be true | |
28 | end | |
29 | end | |
30 | ||
31 | context 'when not the same username' do | |
32 | it 'returns false' do | |
33 | expect(user == CMSScanner::User.new('test')).to be false | |
34 | end | |
35 | end | |
36 | end | |
37 | ||
38 | describe '#to_s' do | |
39 | its(:to_s) { should eql username } | |
40 | end | |
41 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Version do | |
3 | it_behaves_like CMSScanner::Finders::Finding | |
4 | ||
5 | subject(:version) { described_class.new(number, opts) } | |
6 | let(:opts) { {} } | |
7 | let(:number) { '1.0' } | |
8 | ||
9 | its(:to_s) { should eql '1.0' } | |
10 | ||
11 | describe '#number' do | |
12 | its(:number) { should eql '1.0' } | |
13 | ||
14 | context 'when float number supplied' do | |
15 | let(:number) { 2.0 } | |
16 | ||
17 | its(:number) { should eql '2.0' } | |
18 | its(:to_s) { should eql '2.0' } | |
19 | end | |
20 | ||
21 | context 'when starting with a dot' do | |
22 | let(:number) { '.2' } | |
23 | ||
24 | its(:number) { should eql '0.2' } | |
25 | end | |
26 | end | |
27 | ||
28 | describe '#<=>, #==, #>, #<' do | |
29 | it 'returns true' do | |
30 | expect(version == '1.0').to be true | |
31 | expect(version == 1.0).to be true | |
32 | expect(version == described_class.new('1.0')).to be true | |
33 | expect(version > '0.9').to be true | |
34 | expect(version < '2').to be true | |
35 | ||
36 | expect(described_class.new('0.1') == '.1').to be true | |
37 | expect(described_class.new('.1') == '0.1').to be true | |
38 | end | |
39 | ||
40 | it 'returns false' do | |
41 | expect(version == '2.0').to be false | |
42 | expect(version == described_class.new('2')).to be false | |
43 | expect(version > '2.0').to be false | |
44 | expect(version < '1.0').to be false | |
45 | ||
46 | expect(version < 'gg').to be false | |
47 | expect(version == '').to be false | |
48 | expect(version == true).to be false | |
49 | end | |
50 | end | |
51 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::XMLRPC do | |
3 | subject(:xml_rpc) { described_class.new(url) } | |
4 | let(:url) { 'http://example.com/xmlrpc' } | |
5 | let(:fixtures) { FIXTURES_MODELS.join('xml_rpc') } | |
6 | ||
7 | describe '#method_call' do | |
8 | after do | |
9 | request = xml_rpc.method_call(method, method_params, request_params) | |
10 | ||
11 | expect(request).to be_a Typhoeus::Request | |
12 | expect(request.options[:body]).to eql @expected_body | |
13 | ||
14 | expect(request.options).to include(request_params) unless request_params.empty? | |
15 | end | |
16 | ||
17 | let(:method) { 'rpc-test' } | |
18 | let(:method_params) { [] } | |
19 | let(:request_params) { {} } | |
20 | ||
21 | context 'when no params' do | |
22 | it 'sets the correct body in the request' do | |
23 | @expected_body = '<?xml version="1.0" ?><methodCall>' | |
24 | @expected_body << "<methodName>#{method}</methodName><params/>" | |
25 | @expected_body << "</methodCall>\n" | |
26 | end | |
27 | end | |
28 | ||
29 | context 'when method_params and request_params' do | |
30 | let(:method_params) { %w[p1 p2] } | |
31 | let(:request_params) { { spec_key: 'yolo' } } | |
32 | ||
33 | it 'set the correct body in the request' do | |
34 | @expected_body = '<?xml version="1.0" ?><methodCall>' | |
35 | @expected_body << "<methodName>#{method}</methodName><params>" | |
36 | @expected_body << '<param><value><string>p1</string></value></param>' | |
37 | @expected_body << '<param><value><string>p2</string></value></param>' | |
38 | @expected_body << "</params></methodCall>\n" | |
39 | end | |
40 | end | |
41 | end | |
42 | ||
43 | describe '#multi_call' do | |
44 | after do | |
45 | request = xml_rpc.multi_call(methods_and_params, request_params) | |
46 | ||
47 | expect(request).to be_a Typhoeus::Request | |
48 | expect(request.options[:body]).to eql @expected_body | |
49 | ||
50 | expect(request.options).to include(request_params) unless request_params.empty? | |
51 | end | |
52 | ||
53 | let(:methods_and_params) { [%w[m1 p1 p2], %w[m2 p1], %w[m3]] } | |
54 | let(:request_params) { {} } | |
55 | ||
56 | it 'sets the correct body in the request' do | |
57 | @expected_body = File.read(fixtures.join('multi_call.xml')) | |
58 | end | |
59 | end | |
60 | ||
61 | describe '#available_methods' do | |
62 | after do | |
63 | expect(xml_rpc.available_methods).to eql @expected | |
64 | ||
65 | # When calling a second time, should not redo a request | |
66 | expect(xml_rpc).to_not receive(:method_call) | |
67 | expect(xml_rpc.available_methods).to eql @expected | |
68 | end | |
69 | ||
70 | context 'when an empty response' do | |
71 | it 'returns an empty array' do | |
72 | stub_request(:post, xml_rpc.url).and_return(body: '') | |
73 | ||
74 | @expected = [] | |
75 | end | |
76 | end | |
77 | ||
78 | context 'when a correct response' do | |
79 | it 'returns the expected array' do | |
80 | stub_request(:post, xml_rpc.url).and_return( | |
81 | body: '<?xml version="1.0" ?><methodResponse><params><param><value><array><data>'\ | |
82 | '<value><string>system.listMethods</string></value>'\ | |
83 | '<value><string>m1</string></value>'\ | |
84 | '</data></array></value></param></params></methodResponse>' | |
85 | ) | |
86 | ||
87 | @expected = %w[system.listMethods m1] | |
88 | end | |
89 | end | |
90 | end | |
91 | ||
92 | describe '#enabled?' do | |
93 | context 'when no methods available' do | |
94 | it 'returns false' do | |
95 | expect(xml_rpc).to receive(:available_methods).and_return([]) | |
96 | expect(xml_rpc.enabled?).to be false | |
97 | end | |
98 | end | |
99 | ||
100 | context 'when methods available' do | |
101 | it 'returns true' do | |
102 | expect(xml_rpc).to receive(:available_methods).and_return(%w[m1 m2]) | |
103 | expect(xml_rpc.enabled?).to be true | |
104 | end | |
105 | end | |
106 | end | |
107 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe 'App::Views' do | |
3 | let(:target_url) { 'http://e.org/' } | |
4 | let(:fixtures) { SPECS.join('output') } | |
5 | ||
6 | # CliNoColour is used to test the CLI output to avoid the painful colours | |
7 | # in the expected output. | |
8 | %i[JSON CliNoColour].each do |formatter| | |
9 | context "when #{formatter}" do | |
10 | it_behaves_like 'App::Views::Core' | |
11 | it_behaves_like 'App::Views::InterestingFindings' | |
12 | ||
13 | let(:parsed_options) { { url: target_url, format: formatter.to_s.underscore.dasherize } } | |
14 | ||
15 | before do | |
16 | controller.class.parsed_options = parsed_options | |
17 | # Resets the formatter to ensure the correct one is loaded | |
18 | controller.class.class_variable_set(:@@formatter, nil) | |
19 | end | |
20 | ||
21 | after do | |
22 | view_filename = defined?(expected_view) ? expected_view : view | |
23 | view_filename = "#{view_filename}.#{formatter.to_s.underscore.downcase}" | |
24 | controller_dir = controller.class.to_s.demodulize.underscore.downcase | |
25 | expected_output = @expected_output || File.read(fixtures.join(controller_dir, view_filename)) | |
26 | ||
27 | expect($stdout).to receive(:puts).with(expected_output) | |
28 | ||
29 | controller.output(view, @tpl_vars) | |
30 | controller.formatter.beautify # Mandatory to be able to test formatter such as JSON | |
31 | end | |
32 | end | |
33 | end | |
34 | end |
0 | module CMSScanner | |
1 | # Dummy Finding | |
2 | class DummyFinding | |
3 | include Finders::Finding | |
4 | ||
5 | attr_reader :r | |
6 | ||
7 | def initialize(finding, opts = {}) | |
8 | @r = finding | |
9 | parse_finding_options(opts) | |
10 | end | |
11 | ||
12 | def ==(other) | |
13 | r == other.r | |
14 | end | |
15 | ||
16 | def eql?(other) | |
17 | r == other.r && confidence == other.confidence && found_by == other.found_by | |
18 | end | |
19 | ||
20 | def to_s | |
21 | r | |
22 | end | |
23 | end | |
24 | end |
0 | require 'dummy_finding' | |
1 | ||
2 | module CMSScanner | |
3 | module Finders | |
4 | module Independent | |
5 | # Dummy Test Finder | |
6 | class DummyFinder < Finder | |
7 | def passive(_opts = {}) | |
8 | # the nil is there to ensure such value is ignored | |
9 | [DummyFinding.new('test', found_by: found_by), nil] | |
10 | end | |
11 | ||
12 | def aggressive(_opts = {}) | |
13 | DummyFinding.new('test', confidence: 100, found_by: 'override') | |
14 | end | |
15 | end | |
16 | ||
17 | # No aggressive result finder | |
18 | class NoAggressiveResult < Finder | |
19 | def passive(_opts = {}) | |
20 | DummyFinding.new('spotted', confidence: 10, found_by: found_by) | |
21 | end | |
22 | end | |
23 | end | |
24 | end | |
25 | end |
0 | require 'dummy_finding' | |
1 | ||
2 | module CMSScanner | |
3 | module Finders | |
4 | module Unique | |
5 | # Dummy Test Finder | |
6 | class Dummy < Finder | |
7 | def passive(_opts = {}) | |
8 | # the nil is there to ensure such value is ignored | |
9 | [DummyFinding.new('v1', found_by: found_by), nil] | |
10 | end | |
11 | ||
12 | def aggressive(_opts = {}) | |
13 | DummyFinding.new('v1', confidence: 100, found_by: 'override') | |
14 | end | |
15 | end | |
16 | ||
17 | # No aggressive result | |
18 | class NoAggressive < Finder | |
19 | def passive(_opts = {}) | |
20 | DummyFinding.new('v2', confidence: 10, found_by: found_by) | |
21 | end | |
22 | end | |
23 | ||
24 | # Dummy2 | |
25 | class Dummy2 < Finder | |
26 | def aggressive(_opts = {}) | |
27 | DummyFinding.new('v1', confidence: 90) | |
28 | end | |
29 | end | |
30 | end | |
31 | end | |
32 | end |
+12
-0
0 | includes | |
1 | misc | |
2 | modules | |
3 | .htaccess | |
4 | CHANGELOG.txt | |
5 | cron.php | |
6 | data.sql | |
7 | admin.txt | |
8 | robots.txt | |
9 | update.php | |
10 | UPGRADE.txt | |
11 | xmlrpc.php |
0 | HTTP/1.1 200 OK | |
1 | Server: nginx/1.1.19 | |
2 | X-Powered-By: ASP.NET | |
3 | X-Powered-By: PHP | |
4 | Date: Thu | |
5 | Content-Type: text/plain; charset=utf-8 | |
6 | Connection: keep-alive | |
7 | X-Content-Type-Options: nosniff | |
8 | Cache-Control: s-maxage=3600, must-revalidate, max-age=0 | |
9 | X-Article-Id: 12 | |
10 | X-Language: en | |
11 | Last-Modified: Tue, 26 Nov 2013 17:39:43 GMT | |
12 | Vary: X-Subdomain,X-Use-HHVM | |
13 | X-Varnish: 11545 | |
14 | Age: 206 | |
15 | Set-Cookie: GeoIP=; Path=/; Domain=.test.lo |
0 | HTTP/1.1 200 OK | |
1 | Date: Thu | |
2 | Content-Type: text/plain; charset=utf-8 | |
3 | Connection: keep-alive | |
4 | X-Content-Type-Options: nosniff | |
5 | Cache-Control: s-maxage=3600, must-revalidate, max-age=0 | |
6 | X-Language: en | |
7 | Last-Modified: Tue, 26 Nov 2013 17:39:43 GMT | |
8 | Vary: X-Subdomain,X-Use-HHVM | |
9 | X-Varnish: 15154 | |
10 | Age: 206 | |
11 | Set-Cookie: GeoIP=; Path=/; Domain=.test.lo |
0 | # advertising-related bots: | |
1 | User-agent: Mediapartners-Google* | |
2 | Disallow: / | |
3 | ||
4 | # Wikipedia work bots: | |
5 | User-agent: IsraBot | |
6 | Disallow: | |
7 | ||
8 | Disallow: /admin | |
9 | Allow: /public/home | |
10 | ||
11 | # Because why not :o | |
12 | Allow: /admin |
+188
-0
0 | <!DOCTYPE html> | |
1 | <html xmlns="http://www.w3.org/1999/xhtml" xmlns:dc="http://purl.org/dc/terms/" dir="ltr" lang="en-US"> | |
2 | <head profile="http://gmpg.org/xfn/11"> | |
3 | <title>Search and replace DB.</title> | |
4 | <style type="text/css"> | |
5 | body { | |
6 | background-color: #E5E5E5; | |
7 | color: #353231; | |
8 | font: 14px/18px "Gill Sans MT","Gill Sans",Calibri,sans-serif; | |
9 | } | |
10 | ||
11 | p { | |
12 | line-height: 18px; | |
13 | margin: 18px 0; | |
14 | max-width: 520px; | |
15 | } | |
16 | ||
17 | p.byline { | |
18 | margin: 0 0 18px 0; | |
19 | padding-bottom: 9px; | |
20 | border-bottom: 1px dashed #999999; | |
21 | max-width: 100%; | |
22 | } | |
23 | ||
24 | h1,h2,h3 { | |
25 | font-weight: normal; | |
26 | line-height: 36px; | |
27 | font-size: 24px; | |
28 | margin: 9px 0; | |
29 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.8); | |
30 | } | |
31 | ||
32 | h2 { | |
33 | font-weight: normal; | |
34 | line-height: 24px; | |
35 | font-size: 21px; | |
36 | margin: 9px 0; | |
37 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.8); | |
38 | } | |
39 | ||
40 | h3 { | |
41 | font-weight: normal; | |
42 | line-height: 18px; | |
43 | margin: 9px 0; | |
44 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.8); | |
45 | } | |
46 | ||
47 | a { | |
48 | -moz-transition: color 0.2s linear 0s; | |
49 | color: #DE1301; | |
50 | text-decoration: none; | |
51 | font-weight: normal; | |
52 | } | |
53 | ||
54 | a:visited { | |
55 | -moz-transition: color 0.2s linear 0s; | |
56 | color: #AE1301; | |
57 | } | |
58 | ||
59 | a:hover, a:visited:hover { | |
60 | -moz-transition: color 0.2s linear 0s; | |
61 | color: #FE1301; | |
62 | text-decoration: underline; | |
63 | } | |
64 | ||
65 | #container { | |
66 | display:block; | |
67 | width: 768px; | |
68 | padding: 10px; | |
69 | margin: 0px auto; | |
70 | border:solid 10px 0px 0px 0px #ccc; | |
71 | border-top: 18px solid #DE1301; | |
72 | background-color: #F5F5F5; | |
73 | } | |
74 | ||
75 | fieldset { | |
76 | border: 0 none; | |
77 | } | |
78 | ||
79 | .error { | |
80 | border: solid 1px #c00; | |
81 | padding: 5px; | |
82 | background-color: #FFEBE8; | |
83 | text-align: center; | |
84 | margin-bottom: 10px; | |
85 | } | |
86 | ||
87 | label { | |
88 | display:block; | |
89 | line-height: 18px; | |
90 | cursor: pointer; | |
91 | } | |
92 | ||
93 | select.multi, | |
94 | input.text { | |
95 | margin-bottom: 1em; | |
96 | display:block; | |
97 | width: 90%; | |
98 | } | |
99 | ||
100 | select.multi { | |
101 | height: 144px; | |
102 | } | |
103 | ||
104 | ||
105 | input.button { | |
106 | } | |
107 | ||
108 | div.help { | |
109 | border-top: 1px dashed #999999; | |
110 | margin-top: 9px; | |
111 | } | |
112 | ||
113 | </style> | |
114 | </head> | |
115 | <body> | |
116 | <div id="container"> | |
117 | ||
118 | <h1>Safe Search Replace</h1> | |
119 | <p class="byline">by interconnect/<strong>it</strong></p> | |
120 | <h2>Database details</h2> | |
121 | <form action="searchreplacedb2.php?step=3" method="post"> | |
122 | <fieldset> | |
123 | <p> | |
124 | <label for="host">Server Name:</label> | |
125 | <input class="text" type="text" name="host" id="host" value="localhost" /> | |
126 | </p> | |
127 | ||
128 | <p> | |
129 | <label for="data">Database Name:</label> | |
130 | <input class="text" type="text" name="data" id="data" value="" /> | |
131 | </p> | |
132 | ||
133 | <p> | |
134 | <label for="user">Username:</label> | |
135 | <input class="text" type="text" name="user" id="user" value="" /> | |
136 | </p> | |
137 | ||
138 | <p> | |
139 | <label for="pass">Password:</label> | |
140 | <input class="text" type="password" name="pass" id="pass" value="" /> | |
141 | </p> | |
142 | ||
143 | <p> | |
144 | <label for="pass">Charset:</label> | |
145 | <input class="text" type="text" name="char" id="char" value="" /> | |
146 | </p> | |
147 | <input type="submit" class="button" value="Submit DB details" /> </fieldset> | |
148 | </form> <div class="help"> | |
149 | <h4><a href="http://interconnectit.com/">interconnect/it</a> <a href="http://interconnectit.com/124/search-and-replace-for-wordpress-databases/">Safe Search and Replace on Database with Serialized Data v2.0.0</a></h4> | |
150 | <p>This developer/sysadmin tool helps solve the problem of doing a search and replace on a | |
151 | WordPress site when doing a migration to a domain name with a different length.</p> | |
152 | ||
153 | <p><style="color:red">WARNING!</strong> Take a backup first, and carefully test the results of this code. | |
154 | If you don't, and you vape your data then you only have yourself to blame. | |
155 | Seriously. And if you're English is bad and you don't fully understand the | |
156 | instructions then STOP. Right there. Yes. Before you do any damage. | |
157 | ||
158 | <h2>Don't Forget to Remove Me!</h3> | |
159 | ||
160 | <p style="color:red">Delete this utility from your | |
161 | server after use. It represents a major security threat to your database if | |
162 | maliciously used.</p> | |
163 | ||
164 | <h2>Use Of This Script Is Entirely At Your Own Risk</h2> | |
165 | ||
166 | <p> We accept no liability from the use of this tool.</p> | |
167 | ||
168 | <p>If you're not comfortable with this kind of stuff, get an expert, like us, to do | |
169 | this work for you. You do this ENTIRELY AT YOUR OWN RISK! We accept no responsibility | |
170 | if you mess up your data. There is NO UNDO here!</p> | |
171 | ||
172 | <p>The easiest way to use it is to copy your site's files and DB to the new location. | |
173 | You then, if required, fix up your .htaccess and wp-config.php appropriately. Once | |
174 | done, run this script, select your tables (in most cases all of them) and then | |
175 | enter the search replace strings. You can press back in your browser to do | |
176 | this several times, as may be required in some cases.</p> | |
177 | ||
178 | <p>Of course, you can use the script in many other ways - for example, finding | |
179 | all references to a company name and changing it when a rebrand comes along. Or | |
180 | perhaps you changed your name. Whatever you want to search and replace the code will help.</p> | |
181 | ||
182 | <p><a href="http://interconnectit.com/124/search-and-replace-for-wordpress-databases/">Got feedback on this script? Come tell us!</a> | |
183 | ||
184 | </div> | |
185 | </div> | |
186 | </body> | |
187 | </html> |
0 | <head> | |
1 | <meta charset="UTF-8"> | |
2 | <meta name="viewport" content="width=device-width"> | |
3 | <title>WordPress 4.0 | Just another WordPress site</title> | |
4 | <link rel="profile" href="http://gmpg.org/xfn/11"> | |
5 | <link rel="pingback" href="http://e.org/wp/xmlrpc.php"> | |
6 | </head> |
+7
-0
0 | <head> | |
1 | <meta charset="UTF-8"> | |
2 | <meta name="viewport" content="width=device-width"> | |
3 | <title>WordPress 4.0 | Just another WordPress site</title> | |
4 | <link rel="profile" href="http://gmpg.org/xfn/11"> | |
5 | <link rel="pingback" href="http://wp.lab/wordpress-4.0/xmlrpc.php"> | |
6 | </head> |
0 | XML-RPC server accepts POST requests only. |
0 | <?xml version="1.0" ?><methodCall><methodName>system.multicall</methodName><params><param><value><array><data><value><struct><member><name>methodName</name><value><string>m1</string></value></member><member><name>params</name><value><array><data><value><string>p1</string></value><value><string>p2</string></value></data></array></value></member></struct></value><value><struct><member><name>methodName</name><value><string>m2</string></value></member><member><name>params</name><value><array><data><value><string>p1</string></value></data></array></value></member></struct></value><value><struct><member><name>methodName</name><value><string>m3</string></value></member><member><name>params</name><value><array><data/></array></value></member></struct></value></data></array></value></param></params></methodCall> |
0 | <!DOCTYPE html> | |
1 | <html lang="en-US" class="no-js"> | |
2 | <head> | |
3 | <meta charset="UTF-8"> | |
4 | <meta name="viewport" content="width=device-width"> | |
5 | <link rel="profile" href="http://gmpg.org/xfn/11"> | |
6 | <link rel="pingback" href="http://wp.lab/wordpress-4.1.1/xmlrpc.php"> | |
7 | <!--[if lt IE 9]> | |
8 | <script src="http://wp.lab/wordpress-4.1.1/wp-content/themes/twentyfifteen/js/html5.js"></script> | |
9 | <![endif]--> | |
10 | <script>(function(){document.documentElement.className='js'})();</script> | |
11 | <title>WP 4.1.1 | Just another WordPress site</title> | |
12 | <meta name='robots' content='noindex,follow' /> | |
13 | ||
14 | <!-- All in One SEO Pack 2.2.5.1 by Michael Torbert of Semper Fi Web Design --> | |
15 | <link rel="canonical" href="http://wp.lab/wordpress-4.1.1/" /> | |
16 | <!-- /all in one seo pack --> | |
17 | <!--[if lt IE 9]> | |
18 | <link rel='stylesheet' id='twentyfifteen-ie-css' href='http://wp.lab/wordpress-4.1.1/wp-content/themes/twentyfifteen/css/ie.css?ver=20141010' type='text/css' media='all' /> | |
19 | <![endif]--> | |
20 | <!--[if lt IE 8]> | |
21 | <link rel='stylesheet' id='twentyfifteen-ie7-css' href='http://wp.lab/wordpress-4.1.1/wp-content/themes/twentyfifteen/css/ie7.css?ver=20141010' type='text/css' media='all' /> | |
22 | <![endif]--> | |
23 | ||
24 | <!-- .site-branding --> | |
25 | <!-- .site-header --> | |
26 | ||
27 | </body> | |
28 | </html> |
0 | <!DOCTYPE html> | |
1 | <html lang="en-US" class="no-js"> | |
2 | <head> | |
3 | <meta charset="UTF-8"> | |
4 | <meta name="viewport" content="width=device-width"> | |
5 | <link rel="profile" href="http://gmpg.org/xfn/11"> | |
6 | <link rel="pingback" href="http://wp.lab/wordpress-4.1.1/xmlrpc.php"> | |
7 | ||
8 | <script type="text/javascript">var _version = '1.2.4';</script> | |
9 |
0 | [11-Oct-2012 00:00:00] PHP Notice: Undefined index: ec_email in /var/www/wp/wp-content/plugins/easy-contact/econtact.php on line 33 | |
1 | [11-Oct-2012 00:00:00] PHP Notice: Undefined index: ec_url in /var/www/wp/wp-content/plugins/easy-contact/econtact.php on line 34 |
0 | [13-Jan-2009 01:53:25] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/null.php on line 19 | |
1 | [13-Jan-2009 01:55:58] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/file.php on line 20 | |
2 | [13-Jan-2009 02:13:34] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/error_log.php on line 19 | |
3 | [15-Feb-2009 10:47:54] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/error_log.php on line 19 | |
4 | [15-Feb-2009 11:36:15] PHP Fatal error: Class 'Log' not found in /home/****/public_html/lab/wp-content/plugins/fbconnect/Log/null.php on line 19 |
0 | <a href="http://e.org/f.txt">Link</a> | |
1 | <a href="http://e.org/f.txt">Link</a> <!-- Duplicates should be ignored --> | |
2 | ||
3 | <a href="mailto:[email protected]">eMail me!</a> | |
4 | <a href="jaVaScript:alert(2)">Click me Fool !</a> | |
5 | ||
6 | <script src=" https://cdn.e.org/f2.js "></script> <!-- head & tail spaces should be removed --> | |
7 | ||
8 | <script src="/script/s.js"></script> | |
9 | ||
10 | <link rel="alternate" type="application/rss+xml" title="Spec" href="http://wp-lamp/robots.txt" /> | |
11 | ||
12 | <link rel="canonical" href="https://duckduckgo.com/"> | |
13 | ||
14 | <img src="http://out.of.scope.com/img.jpg" width="1000" height="288" alt="" /> | |
15 | ||
16 | <a href="">Empty Link</a> | |
17 | ||
18 | <link rel="alternate" type="application/rss+xml" title="WordPress 4.1 » Feed" href="http://e.org/feed" /> | |
19 | ||
20 | <img src="//g.com/img.jpg" width="" height="" alt="" /> | |
21 | ||
22 | <img src="//out.of.scope.com/img.jpg" width="" height="" alt="" /> | |
23 |
0 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> | |
1 | <html> | |
2 | <head> | |
3 | <title>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</title> | |
4 | </head> | |
5 | <body> | |
6 | <h1>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</h1> | |
7 | <table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr> | |
8 | <tr><td valign="top"><img src="/icons/back.gif" alt="[DIR]"></td><td><a href="/wordpress-4.0/wp-content/plugins/">Parent Directory</a></td><td> </td><td align="right"> - </td><td> </td></tr> | |
9 | <tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="backup.php">backup.php</a></td><td align="right">07-Oct-2014 18:43 </td><td align="right"> 10K</td><td> </td></tr> | |
10 | <tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="database-empty.php">database-empty.php</a></td><td align="right">07-Oct-2014 18:43 </td><td align="right">3.9K</td><td> </td></tr> | |
11 | <tr><th colspan="5"><hr></th></tr> | |
12 | </table> | |
13 | <address>Apache/2.2.16 (Debian) Server at wp.lab Port 80</address> | |
14 | </body></html> |
0 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> | |
1 | <html> | |
2 | <head> | |
3 | <title>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</title> | |
4 | </head> | |
5 | <body> | |
6 | <h1>Index of /wordpress-4.0/wp-content/plugins/wp-dbmanager</h1> | |
7 | <table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr></tr> | |
8 | </table> | |
9 | <address>Apache/2.2.16 (Debian) Server at wp.lab Port 80</address> | |
10 | </body></html> |
0 | Date: Sun, 12 Oct 2014 19:44:42 GMT | |
1 | Server: Apache/2.2.16 (Debian) | |
2 | X-Powered-By: PHP/5.3.3-7+squeeze19 | |
3 | Vary: Accept-Encoding | |
4 | Content-Type: text/html |
0 | Content-Length: 1027 | |
1 | Content-Type: text/html; charset=UTF-8 | |
2 | Server: Microsoft-IIS/7.5 | |
3 | X-Powered-By: ASP.NET | |
4 | X-UA-Compatible: IE=EmulateIE7 | |
5 | Date: Sun, 12 Oct 2014 20:15:14 GMT |
0 | HTTP/1.1 200 OK | |
1 | Server: nginx | |
2 | Date: Sun, 15 Mar 2015 19:43:52 GMT | |
3 | Content-Type: text/html | |
4 | Connection: keep-alive |
0 | <html><head><title>e.org - /dir/</title></head><body><H1>e.org - /dir/</H1><hr> | |
1 | ||
2 | <pre>10/8/2014 11:00 PM <dir> <A HREF="/sub-dir/">sub-dir</A>10/10/2014 10:00 PM 168 <A HREF="/web.config">web.config</A><br></pre><hr></body></html> |
0 | <html><head><title>e.org - /dir/</title></head><body><H1>e.org - /dir/</H1><hr> | |
1 | ||
2 | <pre><A HREF="/">[To Parent Directory]</A><br><br> 10/8/2014 11:00 PM <dir> <A HREF="/sub-dir/">sub-dir</A>10/10/2014 10:00 PM 168 <A HREF="/web.config">web.config</A><br></pre><hr></body></html> |
0 | <html> | |
1 | <head><title>Index of /fanart/</title></head> | |
2 | <body bgcolor="white"> | |
3 | <h1>Index of /fanart/</h1><hr> | |
4 | <hr></body> | |
5 | </html> |
0 | <html> | |
1 | <head><title>Index of /fanart/</title></head> | |
2 | <body bgcolor="white"> | |
3 | <h1>Index of /fanart/</h1><hr><pre><a href="../">../</a> | |
4 | <a href="1931/">1931/</a> 16-Jun-2013 05:46 - | |
5 | <a href="720/">720/</a> 16-Jun-2013 05:46 - | |
6 | <a href="down">down</a> 16-Jun-2013 05:46 53 | |
7 | </pre><hr></body> | |
8 | </html> |
0 | <a href="http://e.org/f.txt">Link</a> | |
1 | Duplicates should be ignored | |
2 | <a href="http://e.org/f.txt">Link</a> | |
3 | ||
4 | <a href="mailto:[email protected]">eMail me!</a> | |
5 | <a href="jaVaScript:alert(2)">Click me Fool !</a> | |
6 | ||
7 | Head and tail spaces should be removed | |
8 | <script src=" https://cdn.e.org/f2.js "></script> | |
9 | ||
10 | <script src="/script/s.js"></script> | |
11 | ||
12 | <link rel="alternate" type="application/rss+xml" title="Spec" href="http://wp-lamp/feed.xml" /> | |
13 | ||
14 | <a href="">Empty Link should be ignored</a> | |
15 | ||
16 | <img src="//g.com/img.jpg" width="" height="" alt="" /> | |
17 | ||
18 | <a href="http://">no host, should be ignored</a> | |
19 | ||
20 | Don't parse that either | |
21 | <img src="data:image/jpeg;base64,/9j/4AAQ/" /> | |
22 | ||
23 | <img class="fl-photo-img wp-image-608 size-full" src="data:image/png;base64,SNIPPED" alt="XXX" itemprop="image" height="10" width="100" data-src="//g.org/logo.png" |
0 | Local View⏎ |
0 | Global View⏎ |
0 | Override the base/test.erb⏎ |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Browser do | |
3 | it_behaves_like described_class::Actions | |
4 | ||
5 | subject(:browser) { described_class.instance(options) } | |
6 | before { described_class.reset } | |
7 | let(:options) { {} } | |
8 | let(:default) do | |
9 | { | |
10 | headers: { 'User-Agent' => "CMSScanner v#{CMSScanner::VERSION}" }, | |
11 | accept_encoding: 'gzip, deflate', | |
12 | method: :get | |
13 | } | |
14 | end | |
15 | ||
16 | describe '#forge_request' do | |
17 | it 'returns a Typhoeus::Request' do | |
18 | expect(browser.forge_request('http://example.com')).to be_a Typhoeus::Request | |
19 | end | |
20 | end | |
21 | ||
22 | describe '#default_request_params' do | |
23 | its(:default_request_params) { should eq default } | |
24 | ||
25 | context 'when some attributes are set' do | |
26 | let(:options) do | |
27 | { | |
28 | cache_ttl: 200, connect_timeout: 10, | |
29 | http_auth: { username: 'log', password: 'pwd' }, | |
30 | cookie_jar: '/tmp/cookie_jar.txt', | |
31 | vhost: 'testing', | |
32 | headers: { 'Test' => 'aa' }, | |
33 | proxy_auth: { username: 'u', password: 'pwd' }, | |
34 | disable_tls_checks: true | |
35 | } | |
36 | end | |
37 | ||
38 | let(:expected) do | |
39 | default.merge( | |
40 | cache_ttl: 200, connecttimeout: 10, | |
41 | userpwd: 'log:pwd', proxyuserpwd: 'u:pwd', | |
42 | cookiejar: options[:cookie_jar], cookiefile: options[:cookie_jar], | |
43 | ssl_verifypeer: false, ssl_verifyhost: 0 | |
44 | ).merge(headers: default[:headers].merge('Host' => 'testing', 'Test' => 'aa')) | |
45 | end | |
46 | ||
47 | its(:default_request_params) { should eq expected } | |
48 | end | |
49 | end | |
50 | ||
51 | describe '#request_params' do | |
52 | context 'when no param is given' do | |
53 | its(:request_params) { should eq default } | |
54 | end | |
55 | ||
56 | context 'when params are supplied' do | |
57 | let(:params) { { another_param: true, headers: { 'Accept' => 'None' } } } | |
58 | ||
59 | it 'merges them (headers should be correctly merged)' do | |
60 | expect(browser.request_params(params)).to eq default | |
61 | .merge(params) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval } | |
62 | end | |
63 | ||
64 | context 'when browser options' do | |
65 | let(:options) { { proxy: 'http://127.0.0.1:8080', headers: { 'T' => 'a' } } } | |
66 | ||
67 | it 'returns the correct hash' do | |
68 | expect(browser.request_params(params)).to eq default | |
69 | .merge(options) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval } | |
70 | .merge(params) { |key, oldval, newval| key == :headers ? oldval.merge(newval) : newval } | |
71 | end | |
72 | end | |
73 | end | |
74 | end | |
75 | ||
76 | describe '#load_options' do | |
77 | context 'when no options' do | |
78 | it 'does not load anything' do | |
79 | described_class::OPTIONS.each do |sym| | |
80 | expected = case sym | |
81 | when :user_agent | |
82 | browser.default_user_agent | |
83 | when :user_agents_list | |
84 | File.join(CMSScanner::APP_DIR, 'user_agents.txt') | |
85 | when :throttle | |
86 | 0.0 | |
87 | end | |
88 | ||
89 | expect(browser.send(sym)).to eq expected | |
90 | end | |
91 | end | |
92 | end | |
93 | ||
94 | context 'when options are supplied' do | |
95 | module CMSScanner | |
96 | # Test accessor | |
97 | class Browser | |
98 | attr_accessor :test | |
99 | end | |
100 | end | |
101 | ||
102 | let(:options) do | |
103 | { | |
104 | cache_ttl: 200, max_threads: 10, test: 'should not be set', throttle: 0, | |
105 | user_agent: 'UA', proxy: false, user_agents_list: 'test.txt' | |
106 | } | |
107 | end | |
108 | ||
109 | it 'merges the browser options only' do | |
110 | described_class::OPTIONS.each do |sym| | |
111 | expected = options.key?(sym) ? options[sym] : nil | |
112 | ||
113 | expect(browser.send(sym)).to eq expected | |
114 | end | |
115 | ||
116 | expect(browser.test).to be nil | |
117 | end | |
118 | end | |
119 | end | |
120 | ||
121 | describe '#hydra' do | |
122 | context 'when #max_threads is nil' do | |
123 | its('hydra.max_concurrency') { should eq 1 } | |
124 | end | |
125 | ||
126 | context 'when #max_threads' do | |
127 | let(:options) { { max_threads: 20 } } | |
128 | ||
129 | its('hydra.max_concurrency') { should eq options[:max_threads] } | |
130 | end | |
131 | end | |
132 | ||
133 | describe '#max_threads=' do | |
134 | after do | |
135 | browser.max_threads = @threads | |
136 | ||
137 | expect(browser.max_threads).to eq @expected | |
138 | expect(browser.hydra.max_concurrency).to eq @expected | |
139 | end | |
140 | ||
141 | context 'when <= 0' do | |
142 | it 'sets max_threads to 1' do | |
143 | @threads = -2 | |
144 | @expected = 1 | |
145 | end | |
146 | end | |
147 | ||
148 | context 'when > 0' do | |
149 | it 'sets max_threads to 20' do | |
150 | @threads = 20 | |
151 | @expected = @threads | |
152 | end | |
153 | end | |
154 | ||
155 | context 'when throttle is used' do | |
156 | let(:options) { { throttle: 2000 } } | |
157 | ||
158 | it 'sets max_threads to 1' do | |
159 | @threads = 20 | |
160 | @expected = 1 | |
161 | end | |
162 | end | |
163 | end | |
164 | ||
165 | describe '#throttle=, #throttle' do | |
166 | context 'when not used' do | |
167 | let(:options) { { max_threads: 20 } } | |
168 | ||
169 | its(:throttle) { should eql 0.0 } | |
170 | its(:max_threads) { should eql 20 } | |
171 | end | |
172 | ||
173 | context 'when max_threads and throttle supplied as options' do | |
174 | let(:options) { { max_threads: 20, throttle: 200 } } | |
175 | ||
176 | its(:throttle) { should eql 0.2 } | |
177 | its(:max_threads) { should eql 1 } | |
178 | end | |
179 | ||
180 | context 'when used' do | |
181 | let(:options) { { max_threads: 10 } } | |
182 | ||
183 | after do | |
184 | browser.throttle = @throttle | |
185 | ||
186 | expect(browser.throttle).to eql @throttle.to_i.abs / 1000.0 # This one is in seconds | |
187 | expect(browser.max_threads).to eql 1 | |
188 | expect(browser.hydra.max_concurrency).to eql 1 | |
189 | end | |
190 | ||
191 | context 'when a negative value is supplied' do | |
192 | it 'uses the absolute value and set the max_threads to 1' do | |
193 | @throttle = -100 | |
194 | end | |
195 | end | |
196 | ||
197 | context 'when a positive value is supplied' do | |
198 | it 'sets and set the max_threads to 1' do | |
199 | @throttle = 1000 | |
200 | end | |
201 | end | |
202 | end | |
203 | end | |
204 | ||
205 | describe '#user_agent' do | |
206 | context 'when no --random-user-agent' do | |
207 | context 'when no --user-agent' do | |
208 | its(:user_agent) { should eql browser.default_user_agent } | |
209 | end | |
210 | ||
211 | context 'when --user-agent' do | |
212 | let(:options) { super().merge(user_agent: 'Test UA') } | |
213 | ||
214 | its(:user_agent) { should eql 'Test UA' } | |
215 | end | |
216 | end | |
217 | ||
218 | context 'when --random-user-agent' do | |
219 | let(:options) { super().merge(random_user_agent: true) } | |
220 | ||
221 | it 'select a random UA in the user_agents' do | |
222 | expect(browser.user_agent).to_not eql browser.default_user_agent | |
223 | # Should not pick up a random one each time | |
224 | expect(browser.user_agent).to eql browser.user_agent | |
225 | end | |
226 | end | |
227 | end | |
228 | ||
229 | describe '#user_agents' do | |
230 | let(:options) { { user_agents_list: FIXTURES.join('user_agents.txt') } } | |
231 | ||
232 | its(:user_agents) { should eql %w[UA-1 UA-2] } | |
233 | end | |
234 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Cache::FileStore do | |
3 | let(:cache_dir) { CACHE.join('cache_file_store').to_s } | |
4 | subject(:cache) { described_class.new(cache_dir) } | |
5 | ||
6 | before { FileUtils.rm_r(cache_dir, secure: true) if Dir.exist?(cache_dir) } | |
7 | after { cache.clean } | |
8 | ||
9 | describe '#new, #storage_path, #serializer' do | |
10 | its(:serializer) { should be Marshal } | |
11 | its(:storage_path) { should eq cache_dir } | |
12 | end | |
13 | ||
14 | describe '#clean' do | |
15 | it 'removes all files from the cache dir' do | |
16 | # let's create some files into the directory first | |
17 | (0..5).each do |i| | |
18 | File.new(File.join(cache.storage_path, "file_#{i}.txt"), File::CREAT) | |
19 | end | |
20 | ||
21 | expect(count_files_in_dir(cache.storage_path, 'file_*.txt')).to eq 6 | |
22 | cache.clean | |
23 | expect(count_files_in_dir(cache.storage_path)).to eq 0 | |
24 | end | |
25 | end | |
26 | ||
27 | describe '#read_entry?' do | |
28 | let(:key) { 'key1' } | |
29 | ||
30 | after do | |
31 | File.write(cache.entry_expiration_path(key), @expiration) if @expiration | |
32 | ||
33 | expect(cache.read_entry(key)).to eq @expected | |
34 | end | |
35 | ||
36 | context 'when the entry does not exists' do | |
37 | it 'returns nil' do | |
38 | @expected = nil | |
39 | end | |
40 | end | |
41 | ||
42 | context 'when the file is empty (marshal data too short error)' do | |
43 | it 'returns nil' do | |
44 | File.new(cache.entry_path(key), File::CREAT) | |
45 | ||
46 | @expiration = Time.now.to_i + 200 | |
47 | @expected = nil | |
48 | end | |
49 | end | |
50 | ||
51 | context 'when the entry has expired' do | |
52 | it 'returns nil' do | |
53 | @expiration = Time.now.to_i - 200 | |
54 | @expected = nil | |
55 | end | |
56 | end | |
57 | ||
58 | context 'when the entry has not expired' do | |
59 | it 'returns the entry' do | |
60 | File.write(cache.entry_path(key), cache.serializer.dump('testing data')) | |
61 | ||
62 | @expiration = Time.now.to_i + 600 | |
63 | @expected = 'testing data' | |
64 | end | |
65 | end | |
66 | end | |
67 | ||
68 | describe '#write_entry' do | |
69 | after do | |
70 | cache.write_entry(@key, @data, @ttl) | |
71 | expect(cache.read_entry(@key)).to eq @expected | |
72 | end | |
73 | ||
74 | it 'should get the correct entry (string)' do | |
75 | @ttl = 10 | |
76 | @key = 'some_key' | |
77 | @data = 'Hello World !' | |
78 | @expected = @data | |
79 | end | |
80 | ||
81 | context 'when cache_ttl <= 0' do | |
82 | it 'does not write the entry' do | |
83 | @ttl = 0 | |
84 | @key = 'another_key' | |
85 | @data = 'Another Hello World !' | |
86 | @expected = nil | |
87 | end | |
88 | end | |
89 | ||
90 | context 'when cache_ttl is nil' do | |
91 | it 'does not write the entry' do | |
92 | @ttl = nil | |
93 | @key = 'test' | |
94 | @data = 'test' | |
95 | @expected = nil | |
96 | end | |
97 | end | |
98 | end | |
99 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Cache::Typhoeus do | |
3 | subject(:cache) { described_class.new(cache_dir) } | |
4 | ||
5 | let(:cache_dir) { CACHE.join('typhoeus_cache') } | |
6 | let(:url) { 'http://example.com' } | |
7 | let(:request) { Typhoeus::Request.new(url, cache_ttl: 20) } | |
8 | let(:key) { request.hash.to_s } | |
9 | ||
10 | describe '#get' do | |
11 | it 'calls #read_entry' do | |
12 | expect(cache).to receive(:read_entry).with(key) | |
13 | ||
14 | cache.get(request) | |
15 | end | |
16 | end | |
17 | ||
18 | describe '#set' do | |
19 | let(:response) { Typhoeus::Response.new } | |
20 | ||
21 | it 'calls #write_entry' do | |
22 | expect(cache).to receive(:write_entry).with(key, response, request.cache_ttl) | |
23 | ||
24 | cache.set(request, response) | |
25 | end | |
26 | end | |
27 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner do | |
3 | let(:target_url) { 'http://wp.lab/' } | |
4 | ||
5 | before do | |
6 | scanner = CMSScanner::Scan.new | |
7 | scanner.controllers.first.class.parsed_options = rspec_parsed_options("--url #{target_url}") | |
8 | end | |
9 | ||
10 | describe 'typhoeus_on_complete' do | |
11 | before { CMSScanner.total_requests = 0 } | |
12 | ||
13 | # TODO: find a way to test the cached requests which should not be counted | |
14 | it 'returns the expected number of requests' do | |
15 | stub_request(:get, /.*/) | |
16 | ||
17 | CMSScanner::Browser.get(target_url) | |
18 | ||
19 | expect(CMSScanner.total_requests).to eql 1 | |
20 | end | |
21 | end | |
22 | ||
23 | describe '#app_name' do | |
24 | it 'returns the excpected string' do | |
25 | expect(CMSScanner.app_name).to eql 'cms_scanner' | |
26 | end | |
27 | end | |
28 | end | |
29 | ||
30 | describe CMSScanner::Scan do | |
31 | subject(:scanner) { described_class.new } | |
32 | let(:controller) { CMSScanner::Controller } | |
33 | ||
34 | before do | |
35 | Object.send(:remove_const, :ARGV) | |
36 | Object.const_set(:ARGV, []) | |
37 | end | |
38 | ||
39 | describe '#new, #controllers' do | |
40 | its(:controllers) { should eq([controller::Core.new]) } | |
41 | end | |
42 | ||
43 | describe '#run' do | |
44 | after do | |
45 | scanner.run | |
46 | ||
47 | if defined?(run_error) | |
48 | expect(scanner.run_error).to be_a run_error.class | |
49 | expect(scanner.run_error.message).to eql run_error.message | |
50 | end | |
51 | end | |
52 | ||
53 | it 'runs the controlllers and calls the formatter#beautify' do | |
54 | hydra = CMSScanner::Browser.instance.hydra | |
55 | ||
56 | expect(scanner.controllers).to receive(:run).ordered | |
57 | expect(hydra).to receive(:abort).ordered | |
58 | expect(scanner.formatter).to receive(:beautify).ordered | |
59 | end | |
60 | ||
61 | context 'when no required option supplied' do | |
62 | it 'calls the formatter to display the usage view' do | |
63 | expect(scanner.formatter).to receive(:output) | |
64 | .with('@usage', msg: 'One of the following options is required: url, help, version') | |
65 | end | |
66 | end | |
67 | ||
68 | context 'when an Interrupt is raised during the scan' do | |
69 | it 'aborts the scan with the correct output' do | |
70 | expect(scanner.controllers.option_parser).to receive(:results).and_return({}) | |
71 | ||
72 | expect(scanner.controllers.first) | |
73 | .to receive(:before_scan) | |
74 | .and_raise(Interrupt) | |
75 | ||
76 | expect(scanner.formatter).to receive(:output) | |
77 | .with('@scan_aborted', hash_including(reason: 'Canceled by User', trace: anything, verbose: nil)) | |
78 | end | |
79 | end | |
80 | ||
81 | [RuntimeError.new('error spotted'), SignalException.new('SIGTERM')].each do |error| | |
82 | context "when an/a #{error.class} is raised during the scan" do | |
83 | let(:run_error) { error } | |
84 | ||
85 | it 'aborts the scan with the associated output' do | |
86 | expect(scanner.controllers.option_parser).to receive(:results).and_return({}) | |
87 | ||
88 | expect(scanner.controllers.first) | |
89 | .to receive(:before_scan) | |
90 | .and_raise(run_error.class, run_error.message) | |
91 | ||
92 | expect(scanner.formatter).to receive(:output) | |
93 | .with('@scan_aborted', hash_including(reason: run_error.message, trace: anything, verbose: nil)) | |
94 | end | |
95 | end | |
96 | end | |
97 | end | |
98 | ||
99 | describe '#datastore' do | |
100 | its(:datastore) { should eq({}) } | |
101 | end | |
102 | ||
103 | describe '#exit_hook' do | |
104 | # No idea how to test that, maybe with another at_exit hook ? oO | |
105 | xit | |
106 | end | |
107 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Controller do | |
3 | subject(:controller) { described_class::Base.new } | |
4 | ||
5 | context 'when parsed_options' do | |
6 | before do | |
7 | described_class::Base.option_parser = nil | |
8 | described_class::Base.parsed_options = parsed_options | |
9 | end | |
10 | ||
11 | let(:parsed_options) { { url: 'http://example.com/' } } | |
12 | ||
13 | its(:option_parser) { should be nil } | |
14 | its(:parsed_options) { should eq(parsed_options) } | |
15 | its(:formatter) { should be_a CMSScanner::Formatter::Cli } | |
16 | its(:user_interaction?) { should be true } | |
17 | its(:tmp_directory) { should eql '/tmp/cms_scanner' } | |
18 | its(:target) { should be_a CMSScanner::Target } | |
19 | its('target.scope.domains') { should eq [PublicSuffix.parse('example.com')] } | |
20 | ||
21 | context 'when output option' do | |
22 | let(:parsed_options) { super().merge(output: '/tmp/spec.txt') } | |
23 | ||
24 | its(:user_interaction?) { should be false } | |
25 | end | |
26 | ||
27 | describe '#render' do | |
28 | it 'calls the formatter#render' do | |
29 | expect(controller.formatter).to receive(:render).with('test', { verbose: nil }, 'base') | |
30 | controller.render('test') | |
31 | end | |
32 | end | |
33 | end | |
34 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module CMSScanner | |
3 | module Controller | |
4 | class Spec < Base | |
5 | end | |
6 | end | |
7 | end | |
8 | ||
9 | describe CMSScanner::Controllers do | |
10 | subject(:controllers) { described_class.new } | |
11 | let(:controller_mod) { CMSScanner::Controller } | |
12 | ||
13 | describe '#<<' do | |
14 | its(:size) { should be 0 } | |
15 | ||
16 | context 'when controllers are added' do | |
17 | before { controllers << controller_mod::Spec.new << controller_mod::Base.new } | |
18 | ||
19 | its(:size) { should be 2 } | |
20 | end | |
21 | ||
22 | context 'when a controller is added twice' do | |
23 | before { 2.times { controllers << controller_mod::Spec.new } } | |
24 | ||
25 | its(:size) { should be 1 } | |
26 | end | |
27 | ||
28 | it 'returns self' do | |
29 | expect(controllers << controller_mod::Spec.new).to be_a described_class | |
30 | end | |
31 | end | |
32 | ||
33 | describe '#run' do | |
34 | it 'runs the before_scan, run and after_scan methods of each controller' do | |
35 | spec = controller_mod::Spec.new | |
36 | base = controller_mod::Base.new | |
37 | ||
38 | controllers << base << spec | |
39 | ||
40 | # Needed otherwise the default_argv is taken from rspec | |
41 | # (@default_argv=["--pattern", "spec/**{,/*/**}/*_spec.rb"]>) | |
42 | expect(controllers.option_parser).to receive(:results).and_return({}) | |
43 | ||
44 | [base, spec].each { |c| expect(c).to receive(:before_scan).ordered } | |
45 | [base, spec].each { |c| expect(c).to receive(:run).ordered } | |
46 | [spec, base].each { |c| expect(c).to receive(:after_scan).ordered } | |
47 | ||
48 | controllers.run | |
49 | end | |
50 | end | |
51 | ||
52 | describe '#register_options_files' do | |
53 | it 'register the correct files' do | |
54 | expect(File).to receive(:exist?).exactly(4).times.and_return(true) | |
55 | ||
56 | expected = [] | |
57 | option_parser = controllers.option_parser | |
58 | ||
59 | [Dir.home, Dir.pwd].each do |dir| | |
60 | option_parser.options_files.supported_extensions.each do |ext| | |
61 | expected << File.join(dir, '.cms_scanner', "cli_options.#{ext}") | |
62 | end | |
63 | end | |
64 | ||
65 | expect(option_parser.options_files.map(&:path)).to eql expected | |
66 | end | |
67 | end | |
68 | end |
0 | require 'spec_helper' | |
1 | require 'dummy_independent_finders' | |
2 | ||
3 | describe CMSScanner::Finders::BaseFinders do | |
4 | subject(:finders) { described_class.new } | |
5 | ||
6 | describe '#symbols_from_mode' do | |
7 | after { expect(finders.send(:symbols_from_mode, @mode)).to eq @expected } | |
8 | ||
9 | context 'when :mixed' do | |
10 | it 'returns [:passive, :aggressive]' do | |
11 | @mode = :mixed | |
12 | @expected = %i[passive aggressive] | |
13 | end | |
14 | end | |
15 | ||
16 | context 'when :passive or :aggresssive' do | |
17 | %i[passive aggressive].each do |symbol| | |
18 | it 'returns it in an array' do | |
19 | @mode = symbol | |
20 | @expected = [*symbol] | |
21 | end | |
22 | end | |
23 | end | |
24 | ||
25 | context 'otherwise' do | |
26 | it 'returns []' do | |
27 | @mode = :unallowed | |
28 | @expected = [] | |
29 | end | |
30 | end | |
31 | end | |
32 | ||
33 | describe '#run_finder' do | |
34 | # currently handled in independent_finders_spec | |
35 | end | |
36 | ||
37 | describe '#findings' do | |
38 | it 'returns a Findings object' do | |
39 | expect(finders.findings).to be_a CMSScanner::Finders::Findings | |
40 | end | |
41 | end | |
42 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack do | |
3 | # Dummy class to test the module | |
4 | class DummyBreadthFirstDictionaryAttack < CMSScanner::Finders::Finder | |
5 | include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack | |
6 | ||
7 | def login_request(username, password) | |
8 | Typhoeus::Request.new('http://e.org/login.php', | |
9 | method: :post, | |
10 | body: { username: username, pwd: password }) | |
11 | end | |
12 | ||
13 | def valid_credentials?(response) | |
14 | response.code == 302 | |
15 | end | |
16 | ||
17 | def errored_response?(response) | |
18 | response.timed_out? || response.body =~ /Error:/ | |
19 | end | |
20 | end | |
21 | ||
22 | subject(:finder) { DummyBreadthFirstDictionaryAttack.new(target) } | |
23 | let(:target) { CMSScanner::Target.new('http://e.org') } | |
24 | let(:login_url) { target.url('login.php') } | |
25 | ||
26 | describe '#attack' do | |
27 | let(:users) { %w[admin root user].map { |u| CMSScanner::User.new(u) } } | |
28 | let(:passwords) { %w[pwd admin P@ssw0rd] } | |
29 | ||
30 | before do | |
31 | # Mock all login requests to 401 | |
32 | passwords.each do |password| | |
33 | users.each do |user| | |
34 | stub_request(:post, login_url) | |
35 | .with(body: { username: user.username, pwd: password }) | |
36 | .to_return(status: 401) | |
37 | end | |
38 | end | |
39 | end | |
40 | ||
41 | context 'when no valid credentials' do | |
42 | it 'does not yield anything' do | |
43 | expect { |block| finder.attack(users, passwords, &block) }.not_to yield_control | |
44 | end | |
45 | end | |
46 | ||
47 | context 'when valid credentials' do | |
48 | before do | |
49 | stub_request(:post, login_url) | |
50 | .with(body: { username: 'admin', pwd: 'admin' }) | |
51 | .to_return(status: 302) | |
52 | end | |
53 | ||
54 | it 'yields the matching user' do | |
55 | expect { |block| finder.attack(users, passwords, &block) } | |
56 | .to yield_with_args(CMSScanner::User.new('admin', password: 'admin')) | |
57 | end | |
58 | end | |
59 | ||
60 | context 'when an error is present in a response' do | |
61 | before do | |
62 | if defined?(stub_params) | |
63 | stub_request(:post, login_url) | |
64 | .with(body: { username: 'admin', pwd: 'pwd' }) | |
65 | .to_return(stub_params) | |
66 | else | |
67 | stub_request(:post, login_url) | |
68 | .with(body: { username: 'admin', pwd: 'pwd' }) | |
69 | .to_timeout | |
70 | end | |
71 | ||
72 | finder.attack(users, passwords) | |
73 | end | |
74 | ||
75 | context 'when request timeout' do | |
76 | it 'logs to correct message' do | |
77 | expect(finder.progress_bar.log).to eql [ | |
78 | 'Error: Request timed out.' | |
79 | ] | |
80 | end | |
81 | end | |
82 | ||
83 | context 'when status/code = 0' do | |
84 | let(:stub_params) { { status: 0, body: 'Error: Down' } } | |
85 | ||
86 | it 'logs to correct message' do | |
87 | expect(finder.progress_bar.log).to eql [ | |
88 | 'Error: No response from remote server. WAF/IPS? ()' | |
89 | ] | |
90 | end | |
91 | end | |
92 | ||
93 | context 'when error 500' do | |
94 | let(:stub_params) { { status: 500, body: 'Error: 500' } } | |
95 | ||
96 | it 'logs to correct message' do | |
97 | expect(finder.progress_bar.log).to eql [ | |
98 | 'Error: Server error, try reducing the number of threads.' | |
99 | ] | |
100 | end | |
101 | end | |
102 | ||
103 | context 'when unknown error' do | |
104 | let(:stub_params) { { status: 200, body: 'Error: Something went wrong' } } | |
105 | ||
106 | it 'logs to correct message' do | |
107 | expect(finder.progress_bar.log).to eql [ | |
108 | "Error: Unknown response received Code: 200\nBody: Error: Something went wrong" | |
109 | ] | |
110 | end | |
111 | end | |
112 | end | |
113 | end | |
114 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::Finder::Enumerator do | |
3 | # Dummy class to test the module | |
4 | class DummyEnumeratorFinder < CMSScanner::Finders::Finder | |
5 | include CMSScanner::Finders::Finder::Enumerator | |
6 | end | |
7 | ||
8 | subject(:finder) { DummyEnumeratorFinder.new(target) } | |
9 | let(:target) { CMSScanner::Target.new('http://e.org') } | |
10 | ||
11 | its(:request_params) { should eql(cache_ttl: 0) } | |
12 | ||
13 | describe '#enumerate' do | |
14 | before do | |
15 | target_urls.each_key { |url| stub_request(:get, url).to_return(status: 200, body: 'rspec') } | |
16 | end | |
17 | ||
18 | let(:target_urls) do | |
19 | { | |
20 | target.url('1') => 1, | |
21 | target.url('2') => 2 | |
22 | } | |
23 | end | |
24 | ||
25 | context 'when no opts' do | |
26 | let(:opts) { {} } | |
27 | ||
28 | context 'when response are the homepage or custom 404' do | |
29 | before { expect(finder.target).to receive(:homepage_or_404?).twice.and_return(true) } | |
30 | ||
31 | it 'does not yield anything' do | |
32 | expect { |b| finder.enumerate(target_urls, opts, &b) }.to_not yield_control | |
33 | end | |
34 | end | |
35 | ||
36 | context 'when not the hompage or 404' do | |
37 | before { expect(finder.target).to receive(:homepage_or_404?).twice } | |
38 | ||
39 | it 'yield the expected items' do | |
40 | expect { |b| finder.enumerate(target_urls, opts, &b) }.to yield_successive_args( | |
41 | [Typhoeus::Response, 1], [Typhoeus::Response, 2] | |
42 | ) | |
43 | end | |
44 | end | |
45 | end | |
46 | ||
47 | context 'when opts' do | |
48 | context 'when :exclude_content' do | |
49 | before { expect(finder.target).to receive(:homepage_or_404?).twice } | |
50 | ||
51 | context 'when body matches' do | |
52 | let(:opts) { { exclude_content: /spec/i } } | |
53 | ||
54 | it 'does not yield anything' do | |
55 | expect { |b| finder.enumerate(target_urls, opts, &b) }.to_not yield_control | |
56 | end | |
57 | end | |
58 | ||
59 | context 'when body does not match' do | |
60 | let(:opts) { { exclude_content: /not/i } } | |
61 | ||
62 | it 'yield the expected items' do | |
63 | expect { |b| finder.enumerate(target_urls, opts, &b) }.to yield_successive_args( | |
64 | [Typhoeus::Response, 1], [Typhoeus::Response, 2] | |
65 | ) | |
66 | end | |
67 | end | |
68 | ||
69 | context 'when header matches' do | |
70 | let(:opts) { { exclude_content: %r{Location: /aa}i } } | |
71 | ||
72 | before do | |
73 | target_urls.each_key do |url| | |
74 | stub_request(:get, url).to_return(status: 301, | |
75 | headers: { 'Location' => '/aa' }) | |
76 | end | |
77 | end | |
78 | ||
79 | it 'does not yield anything' do | |
80 | expect { |b| finder.enumerate(target_urls, opts, &b) }.to_not yield_control | |
81 | end | |
82 | end | |
83 | ||
84 | context 'when header does not match' do | |
85 | let(:opts) { { exclude_content: /not 301/i } } | |
86 | ||
87 | it 'yield the expected items' do | |
88 | expect { |b| finder.enumerate(target_urls, opts, &b) }.to yield_successive_args( | |
89 | [Typhoeus::Response, 1], [Typhoeus::Response, 2] | |
90 | ) | |
91 | end | |
92 | end | |
93 | end | |
94 | end | |
95 | end | |
96 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::Finder::Fingerprinter do | |
3 | # Dummy class to test the module | |
4 | class DummyFingerprinterFinder < CMSScanner::Finders::Finder | |
5 | include CMSScanner::Finders::Finder::Fingerprinter | |
6 | end | |
7 | ||
8 | subject(:finder) { DummyFingerprinterFinder.new(target) } | |
9 | let(:target) { CMSScanner::Target.new('http://e.org/') } | |
10 | ||
11 | its(:request_params) { should eql({}) } | |
12 | ||
13 | describe '#fingerprint' do | |
14 | let(:fingerprints) do | |
15 | { | |
16 | target.url('f1.css') => { | |
17 | finder.hexdigest('f1_body') => 'v1' | |
18 | }, | |
19 | target.url('f2.js') => { | |
20 | finder.hexdigest('f2_body') => %w[v1 v2], | |
21 | finder.hexdigest('f2_2_body') => %w[v3] | |
22 | } | |
23 | } | |
24 | end | |
25 | ||
26 | context 'when no matches' do | |
27 | before { stub_request(:get, /.*/).to_return(body: '404') } | |
28 | ||
29 | it 'does not yield anything' do | |
30 | expect { |b| finder.fingerprint(fingerprints, &b) }.not_to yield_control | |
31 | end | |
32 | end | |
33 | ||
34 | context 'when matches' do | |
35 | before do | |
36 | stub_request(:get, target.url('f1.css')).to_return(body: 'f1_body') | |
37 | stub_request(:get, target.url('f2.js')).to_return(body: 'f2_body') | |
38 | end | |
39 | ||
40 | it 'yields the expected arguments' do | |
41 | expect { |b| finder.fingerprint(fingerprints, &b) }.to yield_successive_args( | |
42 | ['v1', target.url('f1.css'), finder.hexdigest('f1_body')], | |
43 | [%w[v1 v2], target.url('f2.js'), finder.hexdigest('f2_body')] | |
44 | ) | |
45 | end | |
46 | end | |
47 | end | |
48 | end |
0 | require 'spec_helper' | |
1 | require 'dummy_finding' | |
2 | ||
3 | describe CMSScanner::Finders::Finder::SmartURLChecker::Findings do | |
4 | subject(:findings) { described_class.new } | |
5 | let(:finding) { CMSScanner::DummyFinding } | |
6 | ||
7 | describe '#<<' do | |
8 | after { expect(findings).to match_array(@expected.map { |f| eql(f) }) } | |
9 | ||
10 | context 'when no findings already in' do | |
11 | it 'adds it' do | |
12 | findings << finding.new('empty-test') | |
13 | @expected = [finding.new('empty-test')] | |
14 | end | |
15 | end | |
16 | ||
17 | context 'when findings already in' do | |
18 | let(:confirmed) { finding.new('confirmed', interesting_entries: entries) } | |
19 | let(:entries) { %w[e1 e2] } | |
20 | ||
21 | before { findings << nil << nil << finding.new('test') << confirmed } | |
22 | ||
23 | it 'adds a confirmed result correctly and ignore nil values' do | |
24 | confirmed_dup = confirmed.dup | |
25 | confirmed_dup.confidence = 100 | |
26 | confirmed_dup.interesting_entries = %w[e2 e3] | |
27 | ||
28 | findings << confirmed_dup | |
29 | ||
30 | confirmed.confirmed_by = confirmed_dup | |
31 | ||
32 | @expected = [] << finding.new('test') << confirmed | |
33 | ||
34 | expect(findings[1].interesting_entries).to eql(%w[e1 e2 e3]) | |
35 | end | |
36 | end | |
37 | end | |
38 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::Finder::SmartURLChecker do | |
3 | # Dummy class to test the module | |
4 | class DummyFinder < CMSScanner::Finders::Finder | |
5 | include CMSScanner::Finders::Finder::SmartURLChecker | |
6 | end | |
7 | ||
8 | subject(:finder) { DummyFinder.new(target) } | |
9 | let(:target) { CMSScanner::Target.new('http://e.org') } | |
10 | ||
11 | before { stub_request(:get, target.url) } | |
12 | ||
13 | context 'when methods are not implemented' do | |
14 | it 'raises errors' do | |
15 | expect { finder.process_urls([]) }.to raise_error NotImplementedError | |
16 | expect { finder.passive }.to raise_error NotImplementedError | |
17 | expect { finder.aggressive_urls }.to raise_error NotImplementedError | |
18 | end | |
19 | end | |
20 | ||
21 | describe '#aggressive' do | |
22 | before { expect(finder).to receive(:aggressive_urls).and_return(%w[u1 u2 u3]) } | |
23 | ||
24 | after do | |
25 | expect(finder).to receive(:process_urls).with(@expected_urls, mode: mode) | |
26 | finder.aggressive(mode: mode) | |
27 | end | |
28 | ||
29 | context 'when :mode = :mixed' do | |
30 | before { expect(finder).to receive(:passive_urls).and_return(%w[u2]) } | |
31 | ||
32 | let(:mode) { :mixed } | |
33 | ||
34 | it 'calls #process_urls with the correct argument' do | |
35 | @expected_urls = %w[u1 u3] | |
36 | end | |
37 | end | |
38 | ||
39 | %i[passive aggressive].each do |m| | |
40 | context "when :mode = #{m}" do | |
41 | let(:mode) { m } | |
42 | ||
43 | it 'calls #process_urls with the correct argument' do | |
44 | @expected_urls = %w[u1 u2 u3] | |
45 | end | |
46 | end | |
47 | end | |
48 | end | |
49 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Finders::Finder do | |
3 | subject(:finder) { described_class.new('target') } | |
4 | ||
5 | describe '#create_progress_bar' do | |
6 | before { finder.create_progress_bar(opts) } | |
7 | ||
8 | context 'when opts[:show_progression] is true' do | |
9 | let(:opts) { { show_progression: true } } | |
10 | ||
11 | it 'uses the default progress-bar output' do | |
12 | expect(finder.progress_bar.send(:output)).to be_a ProgressBar::Outputs::Tty | |
13 | end | |
14 | end | |
15 | ||
16 | context 'when opts[:show_progression] is false' do | |
17 | let(:opts) { { show_progression: false } } | |
18 | ||
19 | it 'uses the null progress_bar outout' do | |
20 | expect(finder.progress_bar.send(:output)).to be_a ProgressBar::Outputs::Null | |
21 | end | |
22 | ||
23 | context 'when logging data' do | |
24 | context 'when no logs' do | |
25 | it 'returns an empty array' do | |
26 | expect(finder.progress_bar.log).to eql([]) | |
27 | end | |
28 | end | |
29 | ||
30 | context 'when adding messages' do | |
31 | it 'returns the messages' do | |
32 | finder.progress_bar.log 'Hello' | |
33 | finder.progress_bar.log 'World' | |
34 | ||
35 | expect(finder.progress_bar.log).to eql(%w[Hello World]) | |
36 | end | |
37 | end | |
38 | end | |
39 | end | |
40 | end | |
41 | ||
42 | its(:browser) { should be_a CMSScanner::Browser } | |
43 | ||
44 | its(:hydra) { should be_a Typhoeus::Hydra } | |
45 | ||
46 | describe '#found_by' do | |
47 | context 'when no klass supplied' do | |
48 | context 'when no passive or aggresive match' do | |
49 | it 'returns nil' do | |
50 | expect(finder).to receive(:caller_locations).and_return([]) | |
51 | ||
52 | expect(finder.found_by).to be_nil | |
53 | end | |
54 | end | |
55 | ||
56 | # TODO: make the below work | |
57 | # context 'when aggressive match' do | |
58 | # it 'returns the expected string' do | |
59 | # expect(finder).to receive(:caller_locations) | |
60 | # .and_return([Thread::Backtrace::Location.new("/aaaaa/file.rb:xx:in `aggressive'")]) | |
61 | # | |
62 | # expect(finder.found_by).to eql 'Finder (Aggressive Detection)' | |
63 | # end | |
64 | # end | |
65 | end | |
66 | ||
67 | # context 'when class supplied' do | |
68 | # it 'returns the expected string' do | |
69 | # expect(finder).to receive(:caller_locations) | |
70 | # .and_return(["/aaaaa/file.rb:xx:in `passive'"]) | |
71 | # | |
72 | # expect(finder.found_by('Rspec')).to eql 'Rspec (Passive Detection)' | |
73 | # end | |
74 | # end | |
75 | end | |
76 | end |
0 | require 'spec_helper' | |
1 | require 'dummy_finding' | |
2 | ||
3 | describe CMSScanner::Finders::Findings do | |
4 | subject(:findings) { described_class.new } | |
5 | let(:finding) { CMSScanner::DummyFinding } | |
6 | ||
7 | describe '#<<' do | |
8 | after { expect(findings).to match_array(@expected.map { |f| eql(f) }) } | |
9 | ||
10 | context 'when no findings already in' do | |
11 | it 'adds it' do | |
12 | findings << finding.new('empty-test', found_by: 'rspec', confidence: 20) | |
13 | @expected = [finding.new('empty-test', found_by: 'rspec', confidence: 20)] | |
14 | end | |
15 | end | |
16 | ||
17 | context 'when findings already in' do | |
18 | let(:confirmed) { finding.new('confirmed') } | |
19 | ||
20 | before { findings << nil << nil << finding.new('test') << confirmed } | |
21 | ||
22 | it 'adds a confirmed result correctly and ignore the nil values' do | |
23 | confirmed_dup = confirmed.dup | |
24 | confirmed_dup.confidence = 100 | |
25 | ||
26 | findings << finding.new('test2') | |
27 | findings << confirmed_dup | |
28 | ||
29 | confirmed.confirmed_by = confirmed_dup | |
30 | ||
31 | @expected = [] << finding.new('test') << confirmed << finding.new('test2') | |
32 | end | |
33 | end | |
34 | end | |
35 | end |
0 | require 'spec_helper' | |
1 | require 'dummy_independent_finders' | |
2 | ||
3 | describe CMSScanner::Finders::IndependentFinders do | |
4 | subject(:finders) { described_class.new } | |
5 | ||
6 | describe '#run' do | |
7 | let(:target) { 'target' } | |
8 | let(:finding) { CMSScanner::DummyFinding } | |
9 | let(:expected_aggressive) { [finding.new('test', found_by: 'override', confidence: 100)] } | |
10 | let(:expected_passive) do | |
11 | [ | |
12 | finding.new('test', found_by: 'Dummy Finder (Passive Detection)'), | |
13 | finding.new('spotted', found_by: 'No Aggressive Result (Passive Detection)', confidence: 10) | |
14 | ] | |
15 | end | |
16 | ||
17 | before do | |
18 | finders << | |
19 | CMSScanner::Finders::Independent::DummyFinder.new(target) << | |
20 | CMSScanner::Finders::Independent::NoAggressiveResult.new(target) | |
21 | end | |
22 | ||
23 | describe 'method calls order' do | |
24 | after { finders.run(mode: mode) } | |
25 | ||
26 | %i[passive aggressive].each do |current_mode| | |
27 | context "when #{current_mode} mode" do | |
28 | let(:mode) { current_mode } | |
29 | ||
30 | it "calls the #{current_mode} method on each finder" do | |
31 | finders.each do |f| | |
32 | expect(f).to receive(current_mode).with(hash_including(found: [])).ordered | |
33 | end | |
34 | end | |
35 | end | |
36 | end | |
37 | ||
38 | context 'when :mixed mode' do | |
39 | let(:mode) { :mixed } | |
40 | ||
41 | it 'calls :passive then :aggressive on each finder' do | |
42 | finders.each do |finder| | |
43 | %i[passive aggressive].each do |method| | |
44 | expect(finder).to receive(method).with(hash_including(found: [])).ordered | |
45 | end | |
46 | end | |
47 | end | |
48 | end | |
49 | end | |
50 | ||
51 | describe 'returned results' do | |
52 | before do | |
53 | @found = finders.run(mode: mode) | |
54 | ||
55 | expect(@found).to be_a(CMSScanner::Finders::Findings) | |
56 | ||
57 | @found.each { |f| expect(f).to be_a finding } | |
58 | end | |
59 | ||
60 | context 'when :passive mode' do | |
61 | let(:mode) { :passive } | |
62 | ||
63 | it 'returns 2 results' do | |
64 | expect(@found).to match_array(expected_passive.map { |f| eql(f) }) | |
65 | end | |
66 | end | |
67 | ||
68 | context 'when :aggressive mode' do | |
69 | let(:mode) { :aggressive } | |
70 | ||
71 | it 'returns 1 result' do | |
72 | expect(@found).to match_array(expected_aggressive.map { |f| eql(f) }) | |
73 | end | |
74 | end | |
75 | ||
76 | context 'when :mixed mode' do | |
77 | let(:mode) { :mixed } | |
78 | ||
79 | it 'returns 2 results' do | |
80 | # As the first passive is confirmed by the expected_aggressive, the confidence | |
81 | # increases and should be 100% due to the expected_aggressive.confidence | |
82 | first_passive = expected_passive.first.dup | |
83 | first_passive.confidence = 100 | |
84 | ||
85 | expect(@found.size).to eq 2 | |
86 | expect(@found.first).to eql first_passive | |
87 | expect(@found.first.confirmed_by).to eql expected_aggressive | |
88 | expect(@found.last).to eql expected_passive.last | |
89 | end | |
90 | end | |
91 | ||
92 | context 'when multiple results returned' do | |
93 | xit | |
94 | end | |
95 | end | |
96 | end | |
97 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module CMSScanner | |
3 | module Finders | |
4 | # Dummy Class to test the module | |
5 | class PluginsFinderSpec | |
6 | include SameTypeFinder | |
7 | ||
8 | def initialize(_target); end | |
9 | end | |
10 | end | |
11 | end | |
12 | ||
13 | describe CMSScanner::Finders::PluginsFinderSpec do | |
14 | it_behaves_like CMSScanner::Finders::IndependentFinder do | |
15 | let(:expected_finders) { [] } | |
16 | let(:expected_finders_class) { CMSScanner::Finders::SameTypeFinders } | |
17 | end | |
18 | ||
19 | subject(:plugins) { described_class.new(target) } | |
20 | let(:target) { CMSScanner::Target.new(url) } | |
21 | let(:url) { 'http://example.com/' } | |
22 | end |
0 | require 'spec_helper' | |
1 | require 'dummy_independent_finders' # will use those for convenience | |
2 | ||
3 | describe CMSScanner::Finders::SameTypeFinders do | |
4 | subject(:finders) { described_class.new } | |
5 | let(:independent_finders) { CMSScanner::Finders::Independent } | |
6 | ||
7 | describe '#run' do | |
8 | let(:target) { 'target' } | |
9 | let(:finding) { CMSScanner::DummyFinding } | |
10 | let(:opts) { {} } | |
11 | ||
12 | before do | |
13 | finders << | |
14 | independent_finders::DummyFinder.new(target) << | |
15 | independent_finders::NoAggressiveResult.new(target) | |
16 | end | |
17 | ||
18 | after do | |
19 | result = finders.run(opts) | |
20 | ||
21 | expect(result).to be_a CMSScanner::Finders::Findings | |
22 | expect(result).to match_array(@expected.map { |f| eql(f) }) | |
23 | end | |
24 | ||
25 | let(:dummy_passive) { independent_finders::DummyFinder.new(target).passive(opts) } | |
26 | let(:dummy_aggresssive) { independent_finders::DummyFinder.new(target).aggressive(opts) } | |
27 | let(:noaggressive) { independent_finders::NoAggressiveResult.new(target).passive(opts) } | |
28 | ||
29 | context 'when :mixed mode' do | |
30 | let(:opts) { super().merge(mode: :mixed) } | |
31 | ||
32 | it 'calls all #passive then #aggressive on finders and returns the results' do | |
33 | expect(finders[0]).to receive(:passive) | |
34 | .with(hash_including(found: [])).ordered.and_call_original | |
35 | ||
36 | expect(finders[1]).to receive(:passive) | |
37 | .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original | |
38 | ||
39 | expect(finders[0]).to receive(:aggressive) | |
40 | .with(hash_including(found: [dummy_passive.first, noaggressive])) | |
41 | .ordered.and_call_original | |
42 | ||
43 | expect(finders[1]).to receive(:aggressive) | |
44 | .with(hash_including(:found)) | |
45 | .ordered | |
46 | ||
47 | @expected = [] | |
48 | ||
49 | @expected << finding.new('test', confidence: 100, | |
50 | found_by: 'Dummy Finder (Passive Detection)') | |
51 | ||
52 | @expected.first.confirmed_by << finding.new('test', confidence: 100, found_by: 'override') | |
53 | ||
54 | @expected << finding.new('spotted', confidence: 10, | |
55 | found_by: 'No Aggressive Result (Passive Detection)') | |
56 | end | |
57 | end | |
58 | ||
59 | context 'when :passive mode' do | |
60 | let(:opts) { super().merge(mode: :passive) } | |
61 | ||
62 | before do | |
63 | expect(finders[0]).to receive(:passive) | |
64 | .with(hash_including(found: [])).ordered.and_call_original | |
65 | ||
66 | expect(finders[1]).to receive(:passive) | |
67 | .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original | |
68 | ||
69 | finders.each { |f| expect(f).to_not receive(:aggressive) } | |
70 | end | |
71 | ||
72 | it 'calls #passive on all finders and returns the results' do | |
73 | @expected = [] | |
74 | @expected << finding.new('test', found_by: 'Dummy Finder (Passive Detection)') | |
75 | @expected << finding.new('spotted', confidence: 10, | |
76 | found_by: 'No Aggressive Result (Passive Detection)') | |
77 | end | |
78 | ||
79 | context 'when :sort used' do | |
80 | let(:opts) { super().merge(sort: true) } | |
81 | ||
82 | it 'returns the sorted results' do | |
83 | @expected = [] | |
84 | @expected << finding.new('spotted', confidence: 10, | |
85 | found_by: 'No Aggressive Result (Passive Detection)') | |
86 | @expected << finding.new('test', found_by: 'Dummy Finder (Passive Detection)') | |
87 | end | |
88 | end | |
89 | ||
90 | # TODO: make this work | |
91 | # context 'when :vulnerable used' do | |
92 | # let(:opts) { super().merge(vulnerable: true) } | |
93 | ||
94 | # it 'returns the vulnerable results' do | |
95 | # expect(dummy_passive).to receive(:vulnerable?).and_return(true) | |
96 | # expect(noaggressive).to receive(:vulnerable?) | |
97 | ||
98 | # @expected = [finding.new('test', found_by: 'Dummy Finder (Passive Detection)')] | |
99 | # end | |
100 | # end | |
101 | end | |
102 | ||
103 | context 'when :aggressive mode' do | |
104 | let(:opts) { super().merge(mode: :aggressive) } | |
105 | ||
106 | it 'calls #aggressive on all finders and returns the results' do | |
107 | finders.each { |f| expect(f).to_not receive(:passive) } | |
108 | ||
109 | expect(finders[0]).to receive(:aggressive) | |
110 | .with(hash_including(found: [])).ordered.and_call_original | |
111 | ||
112 | expect(finders[1]).to receive(:aggressive) | |
113 | .with(hash_including(found: [dummy_aggresssive])).ordered | |
114 | ||
115 | @expected = [finding.new('test', confidence: 100, found_by: 'override')] | |
116 | end | |
117 | end | |
118 | end | |
119 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module CMSScanner | |
3 | module Finders | |
4 | # Dummy Class to test the module | |
5 | class VersionFinderSpec | |
6 | include UniqueFinder | |
7 | ||
8 | def initialize(_target); end | |
9 | end | |
10 | end | |
11 | end | |
12 | ||
13 | describe CMSScanner::Finders::VersionFinderSpec do | |
14 | it_behaves_like CMSScanner::Finders::IndependentFinder do | |
15 | let(:expected_finders) { [] } | |
16 | let(:expected_finders_class) { CMSScanner::Finders::UniqueFinders } | |
17 | end | |
18 | ||
19 | subject(:version) { described_class.new(target) } | |
20 | let(:target) { CMSScanner::Target.new(url) } | |
21 | let(:url) { 'http://example.com/' } | |
22 | end |
0 | require 'spec_helper' | |
1 | require 'dummy_unique_finders' | |
2 | ||
3 | describe CMSScanner::Finders::UniqueFinders do | |
4 | subject(:finders) { described_class.new } | |
5 | let(:unique_finders) { CMSScanner::Finders::Unique } | |
6 | ||
7 | describe '#filter_finding' do | |
8 | let(:findings) { [] } | |
9 | ||
10 | after do | |
11 | finders.instance_variable_set(:@findings, findings) | |
12 | ||
13 | expect(finders.send(:filter_findings)).to eql @expected | |
14 | end | |
15 | ||
16 | context 'when no findings' do | |
17 | it 'returns false' do | |
18 | @expected = false | |
19 | end | |
20 | end | |
21 | ||
22 | context 'when one finding' do | |
23 | let(:findings) { [CMSScanner::DummyFinding.new('one', confidence: 40)] } | |
24 | ||
25 | it 'returns it' do | |
26 | @expected = findings[0] | |
27 | end | |
28 | end | |
29 | ||
30 | context 'when multiple findings' do | |
31 | let(:findings) do | |
32 | (1..5).reduce([]) { |acc, elem| acc << CMSScanner::DummyFinding.new(elem, confidence: 20) } | |
33 | end | |
34 | ||
35 | context 'when they have the same confidence' do | |
36 | it 'returns false' do | |
37 | @expected = false | |
38 | end | |
39 | end | |
40 | ||
41 | context 'when there is a best confidence' do | |
42 | (0..4).each do |position| | |
43 | context "when at [#{position}]" do | |
44 | it 'returns it' do | |
45 | findings[position].confidence = 100 | |
46 | ||
47 | @expected = findings[position] | |
48 | end | |
49 | end | |
50 | end | |
51 | end | |
52 | end | |
53 | end | |
54 | ||
55 | describe '#run' do | |
56 | let(:target) { 'target' } | |
57 | let(:finding) { CMSScanner::DummyFinding } | |
58 | let(:opts) { {} } | |
59 | ||
60 | before do | |
61 | finders << | |
62 | unique_finders::Dummy.new(target) << | |
63 | unique_finders::NoAggressive.new(target) << | |
64 | unique_finders::Dummy2.new(target) | |
65 | end | |
66 | ||
67 | after do | |
68 | result = finders.run(opts) | |
69 | ||
70 | expect(result).to be_a finding if @expected | |
71 | expect(result).to eql @expected | |
72 | end | |
73 | ||
74 | let(:dummy_passive) { unique_finders::Dummy.new(target).passive(opts) } | |
75 | let(:dummy_aggresssive) { unique_finders::Dummy.new(target).aggressive(opts) } | |
76 | let(:noaggressive) { unique_finders::NoAggressive.new(target).passive(opts) } | |
77 | let(:dummy2_aggressive) { unique_finders::Dummy2.new(target).aggressive } | |
78 | ||
79 | context 'when :confidence_threshold <= 0' do | |
80 | let(:opts) { super().merge(confidence_threshold: 0) } | |
81 | ||
82 | context 'when :mixed mode' do | |
83 | let(:opts) { super().merge(mode: :mixed) } | |
84 | ||
85 | it 'calls all #passive then #aggressive on finders and returns the best result' do | |
86 | # Maybe there is a way to factorise this | |
87 | expect(finders[0]).to receive(:passive) | |
88 | .with(hash_including(found: [])).ordered.and_call_original | |
89 | ||
90 | expect(finders[1]).to receive(:passive) | |
91 | .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original | |
92 | ||
93 | expect(finders[2]).to receive(:passive) | |
94 | .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered | |
95 | ||
96 | expect(finders[0]).to receive(:aggressive).with(hash_including(:found)).ordered | |
97 | .and_call_original | |
98 | ||
99 | expect(finders[1]).to receive(:aggressive).with(hash_including(:found)).ordered | |
100 | expect(finders[2]).to receive(:aggressive).with(hash_including(:found)).ordered | |
101 | .and_call_original | |
102 | ||
103 | @expected = finding.new('v1', confidence: 100, found_by: 'Dummy (Passive Detection)') | |
104 | @expected.confirmed_by << finding.new('v1', confidence: 100, found_by: 'override') | |
105 | @expected.confirmed_by << finding.new('v1', confidence: 90) | |
106 | end | |
107 | end | |
108 | ||
109 | context 'when :passive mode' do | |
110 | let(:opts) { super().merge(mode: :passive) } | |
111 | ||
112 | it 'calls #passive on all finders and returns the best result' do | |
113 | expect(finders[0]).to receive(:passive) | |
114 | .with(hash_including(found: [])).ordered.and_call_original | |
115 | ||
116 | expect(finders[1]).to receive(:passive) | |
117 | .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original | |
118 | ||
119 | expect(finders[2]).to receive(:passive) | |
120 | .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered | |
121 | ||
122 | finders.each { |f| expect(f).to_not receive(:aggressive) } | |
123 | ||
124 | @expected = finding.new('v2', confidence: 10, | |
125 | found_by: 'No Aggressive (Passive Detection)') | |
126 | end | |
127 | end | |
128 | ||
129 | context 'when :aggressive mode' do | |
130 | let(:opts) { super().merge(mode: :aggressive) } | |
131 | ||
132 | it 'calls #aggressive on all finders and returns the best result' do | |
133 | finders.each { |f| expect(f).to_not receive(:passive) } | |
134 | ||
135 | expect(finders[0]).to receive(:aggressive) | |
136 | .with(hash_including(found: [])).ordered.and_call_original | |
137 | ||
138 | expect(finders[1]).to receive(:aggressive) | |
139 | .with(hash_including(found: [dummy_aggresssive])).ordered | |
140 | ||
141 | expect(finders[2]).to receive(:aggressive) | |
142 | .with(hash_including(:found)).ordered.and_call_original | |
143 | ||
144 | @expected = finding.new('v1', confidence: 100, found_by: 'override') | |
145 | @expected.confirmed_by << finding.new('v1', confidence: 90) | |
146 | end | |
147 | end | |
148 | end | |
149 | ||
150 | context 'when :confidence_threshold = 100 (default)' do | |
151 | context 'when :mixed mode' do | |
152 | let(:opts) { super().merge(mode: :mixed) } | |
153 | ||
154 | it 'calls all #passive then #aggressive methods on finders and returns the '\ | |
155 | 'result which reaches 100% confidence during the process' do | |
156 | expect(finders[0]).to receive(:passive) | |
157 | .with(hash_including(found: [])).ordered.and_call_original | |
158 | ||
159 | expect(finders[1]).to receive(:passive) | |
160 | .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original | |
161 | ||
162 | expect(finders[2]).to receive(:passive) | |
163 | .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered | |
164 | ||
165 | expect(finders[0]).to receive(:aggressive).with(hash_including(:found)).ordered | |
166 | .and_call_original | |
167 | ||
168 | expect(finders[1]).to_not receive(:aggressive) | |
169 | expect(finders[2]).to_not receive(:aggressive) | |
170 | ||
171 | @expected = finding.new('v1', confidence: 100, found_by: 'Dummy (Passive Detection)') | |
172 | @expected.confirmed_by << finding.new('v1', confidence: 100, found_by: 'override') | |
173 | end | |
174 | end | |
175 | ||
176 | context 'when :passive mode' do | |
177 | let(:opts) { super().merge(mode: :passive) } | |
178 | ||
179 | it 'calls all #passive and returns the best result' do | |
180 | expect(finders[0]).to receive(:passive) | |
181 | .with(hash_including(found: [])).ordered.and_call_original | |
182 | ||
183 | expect(finders[1]).to receive(:passive) | |
184 | .with(hash_including(found: [dummy_passive.first])).ordered.and_call_original | |
185 | ||
186 | expect(finders[2]).to receive(:passive) | |
187 | .with(hash_including(found: [dummy_passive.first, noaggressive])).ordered | |
188 | ||
189 | finders.each { |f| expect(f).to_not receive(:aggressive) } | |
190 | ||
191 | @expected = finding.new('v2', confidence: 10, | |
192 | found_by: 'No Aggressive (Passive Detection)') | |
193 | end | |
194 | end | |
195 | ||
196 | context 'when :aggressive mode' do | |
197 | let(:opts) { super().merge(mode: :aggressive) } | |
198 | ||
199 | it 'calls all #aggressive and returns the result which reaches 100% confidence' do | |
200 | finders.each { |f| expect(f).to_not receive(:passive) } | |
201 | ||
202 | expect(finders[0]).to receive(:aggressive) | |
203 | .with(hash_including(found: [])).ordered.and_call_original | |
204 | ||
205 | expect(finders[1]).to_not receive(:aggressive) | |
206 | expect(finders[2]).to_not receive(:aggressive) | |
207 | ||
208 | @expected = finding.new('v1', confidence: 100, found_by: 'override') | |
209 | end | |
210 | end | |
211 | end | |
212 | end | |
213 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module CMSScanner | |
3 | module Formatter | |
4 | module Spec | |
5 | # Base Format Test Class | |
6 | class BasedFormat < Base | |
7 | def base_format | |
8 | 'base' | |
9 | end | |
10 | end | |
11 | end | |
12 | end | |
13 | end | |
14 | ||
15 | describe CMSScanner::Formatter::Base do | |
16 | subject(:formatter) { described_class.new } | |
17 | ||
18 | describe '#format' do | |
19 | its(:format) { should eq 'base' } | |
20 | end | |
21 | ||
22 | describe '#user_interaction?' do | |
23 | context 'when not a cli format' do | |
24 | its(:user_interaction?) { should be false } | |
25 | end | |
26 | ||
27 | context 'when a cli format' do | |
28 | before { expect(formatter).to receive(:format).and_return('cli') } | |
29 | ||
30 | its(:user_interaction?) { should be true } | |
31 | end | |
32 | end | |
33 | ||
34 | describe '#render, output' do | |
35 | before { formatter.views_directories << FIXTURES_VIEWS } | |
36 | ||
37 | it 'renders the global template and does not override the @views_directories' do | |
38 | expect($stdout).to receive(:puts) | |
39 | .with("It Works!\nViews Dirs: #{formatter.views_directories}") | |
40 | ||
41 | formatter.output('@test', test: 'Works!', views_directories: 'owned') | |
42 | end | |
43 | ||
44 | context 'when global and local rendering are used inside a template' do | |
45 | it 'renders them correcly' do | |
46 | rendered = formatter.render('test', { var: 'Works' }, 'ctrl') | |
47 | ||
48 | expect(rendered).to eq "Test: Works\nLocal View\nGlobal View" | |
49 | end | |
50 | end | |
51 | ||
52 | it 'raises an error if the controller_name is nil and tpl is not a global one' do | |
53 | expect { formatter.output('test') }.to raise_error('The controller_name can not be nil') | |
54 | end | |
55 | end | |
56 | ||
57 | describe '#view_path' do | |
58 | before do | |
59 | formatter.views_directories << FIXTURES_VIEWS | |
60 | formatter.render('local', {}, 'ctrl') # Used to set the @controller_name | |
61 | end | |
62 | ||
63 | context 'when the tpl format is invalid' do | |
64 | let(:tpl) { '../try-this' } | |
65 | ||
66 | it 'raises an error' do | |
67 | expect { formatter.view_path(tpl) }.to raise_error("Wrong tpl format: 'ctrl/#{tpl}'") | |
68 | end | |
69 | end | |
70 | ||
71 | context 'when the tpl is not found' do | |
72 | let(:tpl) { 'not_there' } | |
73 | ||
74 | it 'raises an error' do | |
75 | expect { formatter.view_path(tpl) }.to raise_error("View not found for base/ctrl/#{tpl}") | |
76 | end | |
77 | end | |
78 | ||
79 | context 'when the tpl is found' do | |
80 | after { expect(formatter.view_path(@tpl)).to eq @expected.to_s } | |
81 | ||
82 | context 'if it\'s a global tpl' do | |
83 | it 'returns its path' do | |
84 | @expected = FIXTURES_VIEWS.join('base', 'test.erb') | |
85 | @tpl = '@test' | |
86 | end | |
87 | end | |
88 | ||
89 | context 'if it\s a local tpl' do | |
90 | it 'retuns its path' do | |
91 | @expected = FIXTURES_VIEWS.join('base', 'ctrl', 'local.erb') | |
92 | @tpl = 'local' | |
93 | end | |
94 | end | |
95 | end | |
96 | ||
97 | context 'when base_format' do | |
98 | subject(:formatter) { CMSScanner::Formatter::Spec::BasedFormat.new } | |
99 | ||
100 | after { expect(formatter.view_path(@tpl)).to eq @expected.to_s } | |
101 | ||
102 | context 'when the ovverided view exists' do | |
103 | it 'returns it' do | |
104 | @expected = FIXTURES_VIEWS.join('based_format', 'test.erb') | |
105 | @tpl = '@test' | |
106 | end | |
107 | end | |
108 | ||
109 | it 'returns the base views otherwise' do | |
110 | @expected = FIXTURES_VIEWS.join('base', 'ctrl', 'local.erb') | |
111 | @tpl = 'local' | |
112 | end | |
113 | end | |
114 | end | |
115 | ||
116 | describe '#views_directories' do | |
117 | let(:default_directories) do | |
118 | [Dir.home, Dir.pwd].reduce([APP_VIEWS]) do |a, e| | |
119 | a << Pathname.new(e).join(".#{CMSScanner.app_name}", 'views').to_s | |
120 | end | |
121 | end | |
122 | ||
123 | context 'when default directories' do | |
124 | its(:views_directories) { should eq(default_directories) } | |
125 | end | |
126 | ||
127 | context 'when adding directories' do | |
128 | it 'adds them' do | |
129 | formatter.views_directories << 'testing' | |
130 | ||
131 | expect(formatter.views_directories).to eq(default_directories << 'testing') | |
132 | end | |
133 | end | |
134 | end | |
135 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe Numeric do | |
3 | describe '#bytes_to_human' do | |
4 | context 'when positive' do | |
5 | it 'returns the expected value' do | |
6 | expect(100.bytes_to_human).to eql '100 B' | |
7 | expect(11_497_472.bytes_to_human).to eql '10.965 MB' | |
8 | end | |
9 | end | |
10 | ||
11 | context 'when negative' do | |
12 | it 'uses the absolute value' do | |
13 | expect(-11_497_472.bytes_to_human).to eql '10.965 MB' | |
14 | end | |
15 | end | |
16 | ||
17 | context 'when zero' do | |
18 | it 'returns zero' do | |
19 | expect(0.bytes_to_human).to eql '0 B' | |
20 | end | |
21 | end | |
22 | end | |
23 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::ProgressBarNullOutput do | |
3 | subject(:output) { described_class.new } | |
4 | ||
5 | describe '#log, #logs' do | |
6 | context 'when no log added' do | |
7 | its(:logs) { should eql([]) } | |
8 | end | |
9 | ||
10 | context 'when adding log' do | |
11 | it 'contains the added logs' do | |
12 | output.log 'M1' | |
13 | expect(output.logs).to eql(%w[M1]) | |
14 | ||
15 | output.log 'M2' | |
16 | expect(output.logs).to eql(%w[M1 M2]) | |
17 | ||
18 | expect(output.log).to eql(%w[M1 M2]) | |
19 | end | |
20 | end | |
21 | end | |
22 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe PublicSuffix::Domain do | |
3 | describe '#match' do | |
4 | it 'returns true' do | |
5 | expect(PublicSuffix.parse('g.com').match('g.com')).to eql true | |
6 | end | |
7 | ||
8 | it 'returns true' do | |
9 | expect(PublicSuffix.parse('s.g.com').match('*.g.com')).to eql true | |
10 | end | |
11 | ||
12 | it 'returns false' do | |
13 | expect(PublicSuffix.parse('a.b.g.com').match('*.g.com')).to eql false | |
14 | end | |
15 | ||
16 | it 'returns true' do | |
17 | expect(PublicSuffix.parse('a.b.g.com').match('*.b.g.com')).to eql true | |
18 | end | |
19 | ||
20 | it 'returns true' do | |
21 | expect(PublicSuffix.parse('a.b.g.com').match('**.g.com')).to eql true | |
22 | end | |
23 | ||
24 | it 'returns false' do | |
25 | expect(PublicSuffix.parse('a.b.y.g.com').match('**.b.g.com')).to eql false | |
26 | end | |
27 | ||
28 | it 'returns false' do | |
29 | expect(PublicSuffix.parse('w.g.com').match('*.g2.com')).to eql false | |
30 | end | |
31 | ||
32 | it 'returns true' do | |
33 | expect(PublicSuffix.parse('a.b.g.com').match('a.b.g.com')).to eql true | |
34 | end | |
35 | ||
36 | it 'returns false' do | |
37 | expect(PublicSuffix.parse('a.b.g.com').match('a.y.g.com')).to eql false | |
38 | end | |
39 | ||
40 | it 'returns true' do | |
41 | expect(PublicSuffix.parse('a.b.c.d.g.com').match('**.c.d.g.com')).to eql true | |
42 | end | |
43 | ||
44 | it 'returns true' do | |
45 | expect(PublicSuffix.parse('a.b.c.d.g.com').match('*.b.c.d.g.com')).to eql true | |
46 | end | |
47 | end | |
48 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe 'SubScanner' do | |
3 | before :all do | |
4 | # Module including the CMSScanner to test its correct inclusion | |
5 | module SubScanner | |
6 | include CMSScanner | |
7 | ||
8 | # Override to make sure it can be overriden | |
9 | def self.app_name | |
10 | 'subscanner' | |
11 | end | |
12 | ||
13 | VERSION = '1.0-Spec'.freeze | |
14 | APP_DIR = '/tmp/sub_scanner/spec'.freeze | |
15 | ||
16 | # This Target class should be called in the CMSScanner::Controller::Base | |
17 | # instead of the CMSScanner::Target | |
18 | class Target < CMSScanner::Target | |
19 | def new_method | |
20 | 'working' | |
21 | end | |
22 | end | |
23 | ||
24 | # Testing the override of the register_options_files | |
25 | class Controllers < CMSScanner::Controllers | |
26 | def register_options_files | |
27 | option_parser.options_files << File.join(".#{SubScanner.app_name}", 'rspec.yml') | |
28 | end | |
29 | end | |
30 | ||
31 | # Custom method for all formatters | |
32 | module Formatter | |
33 | include CMSScanner::Formatter | |
34 | ||
35 | # Implements a #custom method which should be available in all formatters | |
36 | module InstanceMethods | |
37 | def custom | |
38 | 'It Works!' | |
39 | end | |
40 | end | |
41 | end | |
42 | end | |
43 | ||
44 | CMSScanner::Controller::Base.reset | |
45 | CMSScanner::Browser.reset | |
46 | end | |
47 | ||
48 | after :all do | |
49 | CMSScanner.send(:remove_const, :NS) | |
50 | CMSScanner.const_set(:NS, CMSScanner) | |
51 | CMSScanner::Controller::Base.reset | |
52 | end | |
53 | ||
54 | subject(:scanner) { SubScanner::Scan.new } | |
55 | let(:formatter_class) { SubScanner::Formatter } | |
56 | let(:target_url) { 'http://ex.lo/' } | |
57 | ||
58 | before do | |
59 | scanner.controllers.first.class.parsed_options = { url: target_url } | |
60 | end | |
61 | ||
62 | describe '#app_name' do | |
63 | it 'returns the correct app_name' do | |
64 | expect(SubScanner.app_name).to eql 'subscanner' | |
65 | end | |
66 | end | |
67 | ||
68 | describe 'Browser#default_user_agent' do | |
69 | it 'returns the correct user_agent' do | |
70 | expect(SubScanner::Browser.instance.default_user_agent).to eql 'SubScanner v1.0-Spec' | |
71 | end | |
72 | end | |
73 | ||
74 | describe 'Controllers' do | |
75 | describe '#target' do | |
76 | it 'loads the overrided Target class' do | |
77 | target = scanner.controllers.first.target | |
78 | ||
79 | expect(target).to be_a SubScanner::Target | |
80 | expect(target).to respond_to(:new_method) | |
81 | expect(target.new_method).to eq 'working' | |
82 | expect(target.url).to eql target_url | |
83 | end | |
84 | end | |
85 | ||
86 | describe '#register_options_files' do | |
87 | let(:options_file_path) { '.subscanner/rspec.yml' } | |
88 | ||
89 | it 'register the correct file' do | |
90 | allow(File).to receive(:exist?).and_call_original | |
91 | allow(File).to receive(:exist?).with(options_file_path).and_return(true) | |
92 | ||
93 | option_parser = SubScanner::Scan.new.controllers.option_parser | |
94 | ||
95 | expect(option_parser.options_files.map(&:path)).to eql [options_file_path] | |
96 | end | |
97 | end | |
98 | end | |
99 | ||
100 | describe 'Controller::Base#tmp_directory' do | |
101 | it 'returns the expected value' do | |
102 | expect(scanner.controllers.first.tmp_directory).to eql '/tmp/subscanner' | |
103 | end | |
104 | end | |
105 | ||
106 | describe 'Formatter' do | |
107 | it_behaves_like CMSScanner::Formatter::ClassMethods do | |
108 | subject(:formatter) { formatter_class } | |
109 | end | |
110 | ||
111 | describe '.load' do | |
112 | it 'adds the #custom method for all formatters' do | |
113 | formatter_class.availables.each do |format| | |
114 | expect(formatter_class.load(format).custom).to eql 'It Works!' | |
115 | end | |
116 | end | |
117 | end | |
118 | ||
119 | describe '#views_directories' do | |
120 | it 'returns the expected paths' do | |
121 | expect(scanner.formatter.views_directories).to eql( | |
122 | [ | |
123 | CMSScanner::APP_DIR, SubScanner::APP_DIR, | |
124 | File.join(Dir.home, '.subscanner'), File.join(Dir.pwd, '.subscanner') | |
125 | ].reduce([]) do |a, e| | |
126 | a << File.join(e, 'views') | |
127 | end | |
128 | ) | |
129 | end | |
130 | end | |
131 | end | |
132 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Target do | |
3 | subject(:target) { described_class.new(url) } | |
4 | let(:url) { 'http://e.org' } | |
5 | ||
6 | def md5sum(body) | |
7 | Digest::MD5.hexdigest(body) | |
8 | end | |
9 | ||
10 | describe '#page_hash' do | |
11 | after { expect(described_class.page_hash(page)).to eql @expected } | |
12 | ||
13 | context 'when the page is an url' do | |
14 | let(:page) { 'http://e.org/somepage.php' } | |
15 | ||
16 | it 'returns the MD5 hash of the page' do | |
17 | body = 'Hello World !' | |
18 | ||
19 | stub_request(:get, page).to_return(body: body) | |
20 | ||
21 | @expected = md5sum(body) | |
22 | end | |
23 | end | |
24 | ||
25 | context 'when the page is a Typhoeus::Response' do | |
26 | let(:page) { Typhoeus::Response.new(body: 'Hello Example!') } | |
27 | ||
28 | it 'returns the correct hash' do | |
29 | @expected = md5sum('Hello Example!') | |
30 | end | |
31 | end | |
32 | ||
33 | context 'when there are comments' do | |
34 | let(:page) do | |
35 | body = "yolo\n\n<!--I should <script>no longer be</script> there -->\nworld!" | |
36 | Typhoeus::Response.new(body: body) | |
37 | end | |
38 | ||
39 | it 'removes them' do | |
40 | @expected = md5sum("yolo\n\n\nworld!") | |
41 | end | |
42 | end | |
43 | ||
44 | context 'when there are script tags' do | |
45 | let(:page) do | |
46 | body = "aa<script>var t = 'test';</script>bbb" | |
47 | ||
48 | Typhoeus::Response.new(body: body) | |
49 | end | |
50 | ||
51 | it 'removes them' do | |
52 | @expected = md5sum('aabbb') | |
53 | end | |
54 | end | |
55 | end | |
56 | ||
57 | describe '#homepage_hash' do | |
58 | it 'returns the MD5 hash of the homepage' do | |
59 | body = 'Hello World' | |
60 | ||
61 | stub_request(:get, target.url).to_return(body: body) | |
62 | ||
63 | expect(target.homepage_hash).to eql md5sum(body) | |
64 | end | |
65 | end | |
66 | ||
67 | describe '#error_404_hash' do | |
68 | it 'returns the md5sum of the 404 page' do | |
69 | stub_request(:any, /.*/).to_return(status: 404, body: '404 page !') | |
70 | ||
71 | expect(target.error_404_hash).to eql md5sum('404 page !') | |
72 | end | |
73 | end | |
74 | ||
75 | describe '#homepage_or_404?' do | |
76 | let(:page_url) { target.url('page') } | |
77 | ||
78 | before do | |
79 | expect(target).to receive(:homepage_hash).and_return(md5sum('Home')) | |
80 | expect(target).to receive(:error_404_hash).and_return(md5sum('Custom 404')) | |
81 | ||
82 | stub_request(:get, page_url).to_return(body: body) | |
83 | end | |
84 | ||
85 | context 'when hashes do not match' do | |
86 | let(:body) { 'Page!' } | |
87 | ||
88 | it 'returns false' do | |
89 | expect(target.homepage_or_404?(page_url)).to eql false | |
90 | end | |
91 | end | |
92 | ||
93 | context 'when hashes match' do | |
94 | let(:body) { 'Custom 404' } | |
95 | ||
96 | it 'returns true' do | |
97 | expect(target.homepage_or_404?(page_url)).to eql true | |
98 | end | |
99 | end | |
100 | end | |
101 | end |
0 | require 'spec_helper' | |
1 | ||
2 | [:PHP].each do |platform| | |
3 | describe CMSScanner::Target do | |
4 | subject(:target) do | |
5 | described_class.new(url).extend(described_class::Platform.const_get(platform)) | |
6 | end | |
7 | ||
8 | let(:url) { 'http://e.org' } | |
9 | let(:fixtures) { FIXTURES.join('target', 'platform', platform.to_s.downcase) } | |
10 | ||
11 | it_behaves_like described_class::Platform.const_get(platform) | |
12 | end | |
13 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Target do | |
3 | subject(:target) { described_class.new(url, opts) } | |
4 | let(:url) { 'http://e.org' } | |
5 | let(:fixtures) { FIXTURES.join('target', 'scope') } | |
6 | let(:opts) { { scope: nil } } | |
7 | ||
8 | describe '#scope' do | |
9 | let(:default_domains) { [PublicSuffix.parse('e.org')] } | |
10 | ||
11 | context 'when none supplied' do | |
12 | its('scope.domains') { should eq default_domains } | |
13 | end | |
14 | ||
15 | context 'when scope provided' do | |
16 | let(:opts) { super().merge(scope: ['*.e.org']) } | |
17 | ||
18 | its('scope.domains') { should eq default_domains << PublicSuffix.parse(opts[:scope].first) } | |
19 | ||
20 | context 'when invalid domains provided' do | |
21 | let(:opts) { super().merge(scope: ['wp-lamp']) } | |
22 | ||
23 | it 'adds them in the invalid_domains attribute' do | |
24 | expect(target.scope.domains).to eq default_domains | |
25 | expect(target.scope.invalid_domains).to eq opts[:scope] | |
26 | end | |
27 | end | |
28 | end | |
29 | end | |
30 | ||
31 | describe '#in_scope?' do | |
32 | context 'when default scope (target domain)' do | |
33 | [nil, '', 'http://out-of-scope.com', '//jquery.com/j.js', | |
34 | 'javascript:alert(3)', 'mailto:[email protected]'].each do |url| | |
35 | it "returns false for #{url}" do | |
36 | expect(target.in_scope?(url)).to eql false | |
37 | end | |
38 | end | |
39 | ||
40 | %w[https://e.org/file.txt http://e.org/ //e.org].each do |url| | |
41 | it "returns true for #{url}" do | |
42 | expect(target.in_scope?(url)).to eql true | |
43 | end | |
44 | end | |
45 | end | |
46 | ||
47 | context 'when custom scope' do | |
48 | let(:opts) { { scope: ['*.e.org', '192.168.1.12'] } } | |
49 | ||
50 | [nil, '', 'http://out-of-scope.com', '//jquery.com/j.js', 'http://192.168.1.2/'].each do |url| | |
51 | it "returns false for #{url}" do | |
52 | expect(target.in_scope?(url)).to eql false | |
53 | end | |
54 | end | |
55 | ||
56 | %w[http://e.org //cdn.e.org/f.txt http://s.e.org/ https://192.168.1.12/h].each do |url| | |
57 | it "returns true for #{url}" do | |
58 | expect(target.in_scope?(url)).to eql true | |
59 | end | |
60 | end | |
61 | end | |
62 | end | |
63 | ||
64 | describe '#in_scope_urls' do | |
65 | let(:res) { Typhoeus::Response.new(body: File.read(fixtures.join('index.html'))) } | |
66 | ||
67 | context 'when block given' do | |
68 | it 'yield the url' do | |
69 | expect { |b| target.in_scope_urls(res, &b) } | |
70 | .to yield_successive_args( | |
71 | ['http://e.org/f.txt', Nokogiri::XML::Element], | |
72 | ['http://e.org/script/s.js', Nokogiri::XML::Element], | |
73 | ['http://e.org/feed', Nokogiri::XML::Element] | |
74 | ) | |
75 | end | |
76 | end | |
77 | ||
78 | context 'when xpath argument given' do | |
79 | it 'returns the expected array' do | |
80 | xpath = '//link[@rel="alternate" and @type="application/rss+xml"]/@href' | |
81 | ||
82 | expect(target.in_scope_urls(res, xpath)).to eql(%w[http://e.org/feed]) | |
83 | end | |
84 | end | |
85 | ||
86 | context 'when no block given' do | |
87 | after { expect(target.in_scope_urls(res)).to eql @expected } | |
88 | ||
89 | context 'when default scope' do | |
90 | it 'returns the expected array' do | |
91 | @expected = %w[http://e.org/f.txt http://e.org/script/s.js http://e.org/feed] | |
92 | end | |
93 | end | |
94 | ||
95 | context 'when supplied scope' do | |
96 | let(:opts) { super().merge(scope: ['*.e.org', 'wp-lamp']) } | |
97 | ||
98 | it 'returns the expected array' do | |
99 | @expected = %w[http://e.org/f.txt https://cdn.e.org/f2.js http://e.org/script/s.js | |
100 | http://wp-lamp/robots.txt http://e.org/feed] | |
101 | end | |
102 | end | |
103 | end | |
104 | end | |
105 | end |
0 | require 'spec_helper' | |
1 | ||
2 | %i[Generic Apache IIS Nginx].each do |server| | |
3 | describe CMSScanner::Target do | |
4 | subject(:target) do | |
5 | described_class.new(url).extend(described_class::Server.const_get(server)) | |
6 | end | |
7 | ||
8 | let(:url) { 'http://e.org' } | |
9 | let(:fixtures) { FIXTURES.join('target', 'server', server.to_s.downcase) } | |
10 | ||
11 | it_behaves_like described_class::Server.const_get(server) | |
12 | end | |
13 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Target do | |
3 | subject(:target) { described_class.new(url) } | |
4 | let(:url) { 'http://e.org' } | |
5 | let(:fixtures) { FIXTURES.join('target') } | |
6 | ||
7 | describe '#interesting_findings' do | |
8 | before do | |
9 | expect(CMSScanner::Finders::InterestingFindings::Base).to receive(:find).and_return(stubbed) | |
10 | end | |
11 | ||
12 | context 'when no findings' do | |
13 | let(:stubbed) { [] } | |
14 | ||
15 | its(:interesting_findings) { should eq stubbed } | |
16 | end | |
17 | ||
18 | context 'when findings' do | |
19 | let(:stubbed) { ['yolo'] } | |
20 | ||
21 | it 'allows findings to be added with <<' do | |
22 | expect(target.interesting_findings).to eq stubbed | |
23 | ||
24 | target.interesting_findings << 'other-finding' | |
25 | ||
26 | expect(target.interesting_findings).to eq(stubbed << 'other-finding') | |
27 | end | |
28 | end | |
29 | end | |
30 | ||
31 | describe '#xpath_pattern_from_page' do | |
32 | # Handled in #comments_from_page & #javascripts_from_page | |
33 | end | |
34 | ||
35 | describe '#comments_from_page' do | |
36 | let(:fixture) { fixtures.join('comments.html') } | |
37 | let(:page) { Typhoeus::Response.new(body: File.read(fixture)) } | |
38 | ||
39 | context 'when the pattern does not match anything' do | |
40 | it 'returns an empty array' do | |
41 | expect(target.comments_from_page(/none/, page)).to eql([]) | |
42 | end | |
43 | end | |
44 | ||
45 | context 'when the pattern matches' do | |
46 | let(:pattern) { /all in one seo pack/i } | |
47 | let(:s1) { 'All in One SEO Pack 2.2.5.1 by Michael Torbert of Semper Fi Web Design' } | |
48 | let(:s2) { '/all in one seo pack' } | |
49 | ||
50 | context 'when no block given' do | |
51 | it 'returns the expected matches' do | |
52 | results = target.comments_from_page(pattern, page) | |
53 | ||
54 | [s1, s2].each_with_index do |s, i| | |
55 | expect(results[i].first).to eql s.match(pattern) | |
56 | expect(results[i].last.to_s).to eql "<!-- #{s} -->" | |
57 | end | |
58 | end | |
59 | end | |
60 | ||
61 | # The below doesn't work, dunno why | |
62 | # Would need to find a way to ensure the MatchData and XML::Comment are correct | |
63 | context 'when block given' do | |
64 | it 'yield the MatchData' do | |
65 | expect { |b| target.comments_from_page(pattern, page, &b) } | |
66 | .to yield_successive_args( | |
67 | [MatchData, Nokogiri::XML::Comment], | |
68 | [MatchData, Nokogiri::XML::Comment] | |
69 | ) | |
70 | end | |
71 | end | |
72 | end | |
73 | ||
74 | context 'when invalid byte sequence' do | |
75 | let(:page) { Typhoeus::Response.new(body: "<!-- \xEB -->") } | |
76 | ||
77 | it 'does not raise an error' do | |
78 | expect { target.comments_from_page(/none/, page) }.to_not raise_error | |
79 | end | |
80 | end | |
81 | end | |
82 | ||
83 | describe '#javascripts_from_page' do | |
84 | let(:fixture) { fixtures.join('javascripts.html') } | |
85 | let(:page) { Typhoeus::Response.new(body: File.read(fixture)) } | |
86 | ||
87 | context 'when the pattern does not match anything' do | |
88 | it 'returns an empty array' do | |
89 | expect(target.javascripts_from_page(/none/, page)).to eql([]) | |
90 | end | |
91 | end | |
92 | ||
93 | context 'when the pattern matches' do | |
94 | let(:pattern) { /_version =/i } | |
95 | let(:s) { "var _version = '1.2.4';" } | |
96 | ||
97 | context 'when no block given' do | |
98 | it 'returns the expected matches' do | |
99 | results = target.javascripts_from_page(pattern, page) | |
100 | ||
101 | expect(results[0].first).to eql s.match(pattern) | |
102 | expect(results[0].last.text.to_s).to eql s | |
103 | end | |
104 | end | |
105 | ||
106 | # The below doesn't work, dunno why | |
107 | # # Would need to find a way to ensure the MatchData and XML::Element are correct | |
108 | context 'when block given' do | |
109 | it 'yield the MatchData' do | |
110 | expect { |b| target.javascripts_from_page(pattern, page, &b) } | |
111 | .to yield_successive_args( | |
112 | [MatchData, Nokogiri::XML::Element] | |
113 | ) | |
114 | end | |
115 | end | |
116 | end | |
117 | end | |
118 | ||
119 | describe '#urls_from_page' do | |
120 | let(:page) { Typhoeus::Response.new(body: File.read(fixtures.join('urls_from_page.html'))) } | |
121 | ||
122 | context 'when block given' do | |
123 | it 'yield the url' do | |
124 | expect { |b| target.urls_from_page(page, &b) } | |
125 | .to yield_successive_args( | |
126 | ['http://e.org/f.txt', Nokogiri::XML::Element], | |
127 | ['https://cdn.e.org/f2.js', Nokogiri::XML::Element], | |
128 | ['http://e.org/script/s.js', Nokogiri::XML::Element], | |
129 | ['http://wp-lamp/feed.xml', Nokogiri::XML::Element], | |
130 | ['http://g.com/img.jpg', Nokogiri::XML::Element], | |
131 | ['http://g.org/logo.png', Nokogiri::XML::Element] | |
132 | ) | |
133 | end | |
134 | end | |
135 | ||
136 | context 'when no block given' do | |
137 | it 'returns the expected array' do | |
138 | expect(target.urls_from_page(page)).to eql( | |
139 | %w[ | |
140 | http://e.org/f.txt https://cdn.e.org/f2.js http://e.org/script/s.js | |
141 | http://wp-lamp/feed.xml http://g.com/img.jpg http://g.org/logo.png | |
142 | ] | |
143 | ) | |
144 | end | |
145 | ||
146 | context 'when xpath argument given' do | |
147 | it 'returns the expected array' do | |
148 | xpath = '//link[@rel="alternate" and @type="application/rss+xml"]/@href' | |
149 | ||
150 | expect(target.urls_from_page(page, xpath)).to eql(%w[http://wp-lamp/feed.xml]) | |
151 | end | |
152 | end | |
153 | end | |
154 | end | |
155 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::Vulnerability do | |
3 | subject(:vuln) { described_class.new(title, references) } | |
4 | let(:title) { 'Test Vuln' } | |
5 | let(:references) { {} } | |
6 | ||
7 | it_behaves_like CMSScanner::References | |
8 | ||
9 | describe '#new' do | |
10 | its(:title) { should eql title } | |
11 | its(:references) { should eql({}) } | |
12 | its(:type) { should eql nil } | |
13 | its(:fixed_in) { should eql nil } | |
14 | end | |
15 | ||
16 | describe '#==' do | |
17 | context 'when te same vuln' do | |
18 | it 'returns true' do | |
19 | expect(vuln).to eq vuln.dup | |
20 | end | |
21 | end | |
22 | ||
23 | context 'when not equal' do | |
24 | it 'returns false' do | |
25 | expect(vuln).to_not eq described_class.new('not eq') | |
26 | end | |
27 | end | |
28 | end | |
29 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe CMSScanner::WebSite do | |
3 | subject(:web_site) { described_class.new(url, opts) } | |
4 | let(:url) { 'http://e.org' } | |
5 | let(:opts) { {} } | |
6 | ||
7 | describe '#url=' do | |
8 | context 'when the url is incorrect' do | |
9 | after do | |
10 | expect { web_site.url = @url }.to raise_error Addressable::URI::InvalidURIError | |
11 | end | |
12 | ||
13 | it 'raises an error if empty' do | |
14 | @url = '' | |
15 | end | |
16 | ||
17 | it 'raises an error if wrong format' do | |
18 | @url = 'jj' | |
19 | end | |
20 | end | |
21 | ||
22 | context 'when valid' do | |
23 | it 'creates an Addressable object and adds a traling slash' do | |
24 | web_site.url = 'http://site.com' | |
25 | ||
26 | expect(web_site.url).to eq('http://site.com/') | |
27 | expect(web_site.uri).to be_a Addressable::URI | |
28 | end | |
29 | end | |
30 | ||
31 | context 'when non ascii chars' do | |
32 | it 'normalize it' do | |
33 | web_site.url = 'http://пример.испытание/' | |
34 | ||
35 | expect(web_site.url).to eql 'http://xn--e1afmkfd.xn--80akhbyknj4f/' | |
36 | end | |
37 | end | |
38 | end | |
39 | ||
40 | describe '#url' do | |
41 | context 'when no path argument' do | |
42 | its(:url) { should eql 'http://e.org/' } | |
43 | end | |
44 | ||
45 | context 'when a path argument' do | |
46 | it 'appends the path' do | |
47 | expect(web_site.url('file.txt')).to eql "#{url}/file.txt" | |
48 | end | |
49 | ||
50 | it 'encodes the path' do | |
51 | expect(web_site.url('f ile.txt')).to eql "#{url}/f%20ile.txt" | |
52 | expect(web_site.url('s/a%.txt')).to eql "#{url}/s/a%25.txt" | |
53 | expect(web_site.url('#file.txt#')).to eql "#{url}/%23file.txt%23" | |
54 | end | |
55 | ||
56 | context 'when relative path' do | |
57 | let(:url) { 'http://e.org/dir/' } | |
58 | ||
59 | it 'appends it from the host/domain' do | |
60 | expect(web_site.url('/sub/file.txt')).to eql 'http://e.org/sub/file.txt' | |
61 | end | |
62 | end | |
63 | end | |
64 | end | |
65 | ||
66 | describe '#opts' do | |
67 | its(:opts) { should eql({}) } | |
68 | ||
69 | context 'when opts' do | |
70 | let(:opts) { { test: 'mm' } } | |
71 | ||
72 | its(:opts) { should eql opts } | |
73 | end | |
74 | end | |
75 | ||
76 | describe '#online?, #http_auth?, #access_forbidden?, #proxy_auth?' do | |
77 | before { stub_request(:get, web_site.url(path)).to_return(status: status) } | |
78 | ||
79 | [nil, 'file-path.txt'].each do |p| | |
80 | context "when path = #{p}" do | |
81 | let(:path) { p } | |
82 | ||
83 | context 'when response status is a 200' do | |
84 | let(:status) { 200 } | |
85 | ||
86 | it 'is considered fine' do | |
87 | expect(web_site.online?(path)).to be true | |
88 | expect(web_site.http_auth?(path)).to be false | |
89 | expect(web_site.access_forbidden?(path)).to be false | |
90 | expect(web_site.proxy_auth?(path)).to be false | |
91 | end | |
92 | end | |
93 | ||
94 | context 'when offline' do | |
95 | let(:status) { 0 } | |
96 | ||
97 | it 'returns false' do | |
98 | expect(web_site.online?(path)).to be false | |
99 | end | |
100 | end | |
101 | ||
102 | context 'when http auth required' do | |
103 | let(:status) { 401 } | |
104 | ||
105 | it 'returns true' do | |
106 | expect(web_site.http_auth?(path)).to be true | |
107 | end | |
108 | end | |
109 | ||
110 | context 'when access is forbidden' do | |
111 | let(:status) { 403 } | |
112 | ||
113 | it 'return true' do | |
114 | expect(web_site.access_forbidden?(path)).to be true | |
115 | end | |
116 | end | |
117 | ||
118 | context 'when proxy auth required' do | |
119 | let(:status) { 407 } | |
120 | ||
121 | it 'returns true' do | |
122 | expect(web_site.proxy_auth?(path)).to be true | |
123 | end | |
124 | end | |
125 | end | |
126 | end | |
127 | end | |
128 | end |
0 | [+] Finished: Thu Oct 30 12:02:03 2014 | |
1 | [+] Requests Done: 10 | |
2 | [+] Memory used: 100 B | |
3 | [+] Elapsed time: 00:00:02 |
0 | Help Message from OptionParser |
0 | { | |
1 | "start_time": 1414670521, | |
2 | "start_memory": 10, | |
3 | "target_url": "http://e.org/", | |
4 | "effective_url": "http://e.org/" | |
5 | }⏎ |
0 | [+] URL: http://e.org/ | |
1 | [+] Effective URL: http://e.org/home | |
2 | [+] Started: Thu Oct 30 12:02:01 2014 | |
3 |
0 | { | |
1 | "start_time": 1414670521, | |
2 | "start_memory": 10, | |
3 | "target_url": "http://e.org/", | |
4 | "effective_url": "http://e.org/home" | |
5 | }⏎ |
0 | Interesting Finding(s): | |
1 | ||
2 | [+] F1_to_s | |
3 | | Found By: Spec | |
4 | | Confidence: 10% | |
5 | ||
6 | [+] F2 | |
7 | | Interesting Entry: IE1 | |
8 | | Found By: Spec | |
9 | | Confidence: 20% | |
10 | | Confirmed By: Spec2, 10% confidence | |
11 | | Reference: R1 | |
12 | ||
13 | [+] F3 | |
14 | | Interesting Entries: | |
15 | | - IE1 | |
16 | | - IE2 | |
17 | | Found By: Spec | |
18 | | Confidence: 100% | |
19 | | Confirmed By: | |
20 | | - Spec2, 100% confidence | |
21 | | - Spec3, 10% confidence | |
22 | | References: | |
23 | | - R1 | |
24 | | - R2 | |
25 | ||
26 | [+] F4 | |
27 | | Found By: Spec | |
28 | | Confirmed By: Spec2 | |
29 |
0 | { | |
1 | "interesting_findings": [ | |
2 | { | |
3 | "url": "F1", | |
4 | "to_s": "F1_to_s", | |
5 | "found_by": "Spec", | |
6 | "confidence": 10, | |
7 | "confirmed_by": { | |
8 | ||
9 | }, | |
10 | "references": { | |
11 | ||
12 | }, | |
13 | "interesting_entries": [ | |
14 | ||
15 | ] | |
16 | }, | |
17 | { | |
18 | "url": "F2", | |
19 | "to_s": "F2", | |
20 | "found_by": "Spec", | |
21 | "confidence": 20, | |
22 | "confirmed_by": { | |
23 | "Spec2": { | |
24 | "confidence": 10 | |
25 | } | |
26 | }, | |
27 | "references": { | |
28 | "url": [ | |
29 | "R1" | |
30 | ] | |
31 | }, | |
32 | "interesting_entries": [ | |
33 | "IE1" | |
34 | ] | |
35 | }, | |
36 | { | |
37 | "url": "F3", | |
38 | "to_s": "F3", | |
39 | "found_by": "Spec", | |
40 | "confidence": 100, | |
41 | "confirmed_by": { | |
42 | "Spec2": { | |
43 | "confidence": 100 | |
44 | }, | |
45 | "Spec3": { | |
46 | "confidence": 10 | |
47 | } | |
48 | }, | |
49 | "references": { | |
50 | "url": [ | |
51 | "R1", | |
52 | "R2" | |
53 | ] | |
54 | }, | |
55 | "interesting_entries": [ | |
56 | "IE1", | |
57 | "IE2" | |
58 | ] | |
59 | }, | |
60 | { | |
61 | "url": "F4", | |
62 | "to_s": "F4", | |
63 | "found_by": "Spec", | |
64 | "confidence": 0, | |
65 | "confirmed_by": { | |
66 | "Spec2": { | |
67 | "confidence": 0 | |
68 | } | |
69 | }, | |
70 | "references": { | |
71 | ||
72 | }, | |
73 | "interesting_entries": [ | |
74 | ||
75 | ] | |
76 | } | |
77 | ] | |
78 | }⏎ |
0 | shared_examples CMSScanner::Browser::Actions do | |
1 | let(:url) { 'http://example.com/file.txt' } | |
2 | let(:browser) { CMSScanner::Browser } | |
3 | ||
4 | describe '#get, #post, #head' do | |
5 | %i[get post head].each do |method| | |
6 | it 'calls the method and returns a Typhoeus::Response' do | |
7 | stub_request(method, url) | |
8 | ||
9 | expect(browser.send(method, url)).to be_a Typhoeus::Response | |
10 | end | |
11 | end | |
12 | end | |
13 | ||
14 | describe '#get_and_follow_location' do | |
15 | let(:redirection) { 'http://redirect.me' } | |
16 | ||
17 | it 'follows the location' do | |
18 | stub_request(:get, url).to_return(status: 301, headers: { location: redirection }) | |
19 | stub_request(:get, redirection).to_return(status: 200, body: 'Got me') | |
20 | ||
21 | response = browser.get_and_follow_location(url) | |
22 | expect(response).to be_a Typhoeus::Response | |
23 | # Line below is not working due to an issue in Typhoeus/Webmock | |
24 | # See https://github.com/typhoeus/typhoeus/issues/279 | |
25 | # expect(response.body).to eq 'Got me' | |
26 | end | |
27 | end | |
28 | end |
0 | shared_examples CMSScanner::Finders::Finding do | |
1 | it_behaves_like CMSScanner::References do | |
2 | let(:opts) { { references: references } } | |
3 | let(:references) { {} } | |
4 | end | |
5 | ||
6 | %i[confirmed_by interesting_entries].each do |opt| | |
7 | describe "##{opt}" do | |
8 | its(opt) { should eq [] } | |
9 | ||
10 | context 'when supplied in the #new' do | |
11 | let(:opts) { { opt => 'test' } } | |
12 | ||
13 | its(opt) { should eq 'test' } | |
14 | end | |
15 | end | |
16 | end | |
17 | ||
18 | describe '#confidence, #confidence=' do | |
19 | its(:confidence) { should eql 0 } | |
20 | ||
21 | context 'when already set' do | |
22 | before { subject.confidence = 10 } | |
23 | ||
24 | its(:confidence) { should eql 10 } | |
25 | end | |
26 | ||
27 | context 'when another confidence added' do | |
28 | it 'adds it the the actual' do | |
29 | subject.confidence += 30 | |
30 | expect(subject.confidence).to eql 30 | |
31 | end | |
32 | ||
33 | it 'sets it to 100 if >= 100' do | |
34 | subject.confidence += 120 | |
35 | expect(subject.confidence).to eql 100 | |
36 | end | |
37 | end | |
38 | end | |
39 | ||
40 | describe '#parse_finding_options' do | |
41 | xit | |
42 | end | |
43 | ||
44 | describe '#eql?' do | |
45 | before do | |
46 | subject.confidence = 10 | |
47 | subject.found_by = 'test' | |
48 | end | |
49 | ||
50 | context 'when eql' do | |
51 | it 'returns true' do | |
52 | expect(subject).to eql subject | |
53 | end | |
54 | end | |
55 | ||
56 | context 'when not eql' do | |
57 | it 'returns false' do | |
58 | other = subject.dup | |
59 | other.confidence = 20 | |
60 | ||
61 | expect(subject).to_not eql other | |
62 | end | |
63 | end | |
64 | end | |
65 | ||
66 | describe '#<=>' do | |
67 | # Handled in spec/app/models/interesting_findings_spec | |
68 | end | |
69 | end |
0 | shared_examples CMSScanner::Formatter::Buffer do | |
1 | describe '#buffer' do | |
2 | its(:buffer) { should be_empty } | |
3 | end | |
4 | end |
0 | shared_examples CMSScanner::Formatter::ClassMethods do | |
1 | describe '#load' do | |
2 | context 'w/o parameter' do | |
3 | it 'loads the default formatter' do | |
4 | expect(subject.load).to be_a subject::Cli | |
5 | end | |
6 | end | |
7 | ||
8 | it 'loads the correct formatter' do | |
9 | expect(subject.load('cli_no_colour')).to be_a subject::CliNoColour | |
10 | end | |
11 | ||
12 | it 'adds the custom_views' do | |
13 | formatter = subject.load(nil, %w[/path/views1 /path2/views]) | |
14 | ||
15 | expect(formatter.views_directories).to include('/path/views1', '/path2/views') | |
16 | end | |
17 | end | |
18 | ||
19 | describe '#availables' do | |
20 | it 'returns the right list' do | |
21 | expect(subject.availables).to match_array(%w[json cli-no-colour cli-no-color cli]) | |
22 | end | |
23 | end | |
24 | end |
0 | shared_examples CMSScanner::Finders::IndependentFinder do | |
1 | describe '::find' do | |
2 | it 'creates a new object and call finders#find' do | |
3 | created = described_class.new(target) | |
4 | ||
5 | expect(described_class).to receive(:new).and_return(created) | |
6 | expect(created).to receive(:find) | |
7 | ||
8 | described_class.find(target) | |
9 | end | |
10 | end | |
11 | ||
12 | describe '#find' do | |
13 | it 'calls finders#run' do | |
14 | expect(subject.finders).to receive(:run).with({}) | |
15 | subject.find | |
16 | end | |
17 | end | |
18 | ||
19 | describe '#finders' do | |
20 | its(:finders) { should be_a expected_finders_class } | |
21 | ||
22 | it 'returns the correct finders' do | |
23 | finders = subject.finders | |
24 | ||
25 | expect(finders.size).to eq expected_finders.size | |
26 | expect(finders.map { |f| f.class.to_s.demodulize }).to eq expected_finders | |
27 | end | |
28 | end | |
29 | end |
0 | shared_examples CMSScanner::References do | |
1 | describe '#references_keys' do | |
2 | it 'returns the expected array of symbols' do | |
3 | expect(subject.class.references_keys) | |
4 | .to eql %i[cve secunia osvdb exploitdb url metasploit packetstorm securityfocus] | |
5 | end | |
6 | end | |
7 | ||
8 | describe 'references' do | |
9 | context 'when no references' do | |
10 | %i[cves secunia_ids osvdb_ids exploitdb_ids urls | |
11 | msf_modules packetstorm_ids securityfocus_ids].each do |attribute| | |
12 | its(attribute) { should eql([]) } | |
13 | end | |
14 | ||
15 | %i[cve_urls secunia_urls osvdb_urls exploitdb_urls msf_urls | |
16 | packetstorm_urls secunia_urls].each do |attribute| | |
17 | its(attribute) { should eql([]) } | |
18 | end | |
19 | ||
20 | its(:references_urls) { should eql([]) } | |
21 | end | |
22 | ||
23 | context 'when an unknown reference key is provided' do | |
24 | let(:references) { { cve: 1, unknown: 12 } } | |
25 | ||
26 | its(:references) { should eql(cve: %w[1]) } | |
27 | end | |
28 | ||
29 | context 'when references provided as string' do | |
30 | let(:references) do | |
31 | { | |
32 | cve: 11, | |
33 | secunia: 12, | |
34 | osvdb: 13, | |
35 | exploitdb: 14, | |
36 | url: 'single-url', | |
37 | metasploit: '/exploit/yolo', | |
38 | packetstorm: 15, | |
39 | securityfocus: 16 | |
40 | } | |
41 | end | |
42 | ||
43 | its(:cves) { should eql %w[11] } | |
44 | its(:cve_urls) { should eql %w[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-11] } | |
45 | ||
46 | its(:secunia_ids) { should eql %w[12] } | |
47 | its(:secunia_urls) { should eql %w[https://secunia.com/advisories/12/] } | |
48 | ||
49 | its(:osvdb_ids) { should eql %w[13] } | |
50 | its(:osvdb_urls) { should eql %w[http://osvdb.org/show/osvdb/13] } | |
51 | ||
52 | its(:exploitdb_ids) { should eql %w[14] } | |
53 | its(:exploitdb_urls) { should eql %w[https://www.exploit-db.com/exploits/14/] } | |
54 | ||
55 | its(:urls) { should eql %w[single-url] } | |
56 | ||
57 | its(:msf_modules) { should eql %w[/exploit/yolo] } | |
58 | its(:msf_urls) { should eql %w[https://www.rapid7.com/db/modules/exploit/yolo] } | |
59 | ||
60 | its(:packetstorm_ids) { should eq %w[15] } | |
61 | its(:packetstorm_urls) { should eql %w[http://packetstormsecurity.com/files/15/] } | |
62 | ||
63 | its(:securityfocus_ids) { should eq %w[16] } | |
64 | its(:securityfocus_urls) { should eql %w[http://www.securityfocus.com/bid/16/] } | |
65 | ||
66 | its(:references_urls) do | |
67 | should eql [ | |
68 | 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-11', | |
69 | 'https://secunia.com/advisories/12/', | |
70 | 'http://osvdb.org/show/osvdb/13', | |
71 | 'https://www.exploit-db.com/exploits/14/', | |
72 | 'single-url', | |
73 | 'https://www.rapid7.com/db/modules/exploit/yolo', | |
74 | 'http://packetstormsecurity.com/files/15/', | |
75 | 'http://www.securityfocus.com/bid/16/' | |
76 | ] | |
77 | end | |
78 | end | |
79 | ||
80 | context 'when references provided as array' do | |
81 | xit | |
82 | end | |
83 | end | |
84 | end |
0 | shared_examples CMSScanner::Target::Platform::PHP do | |
1 | before { stub_request(:get, target.url(path)).to_return(body: body) } | |
2 | ||
3 | describe '#debug_log?' do | |
4 | let(:path) { 'd.log' } | |
5 | ||
6 | context 'when the body matches' do | |
7 | %w[debug.log].each do |file| | |
8 | context "when #{file} body" do | |
9 | let(:body) { File.read(fixtures.join('debug_log', file)) } | |
10 | ||
11 | it 'returns true' do | |
12 | expect(target.debug_log?(path)).to be true | |
13 | end | |
14 | end | |
15 | end | |
16 | end | |
17 | ||
18 | context 'when the body does not match' do | |
19 | let(:body) { '' } | |
20 | ||
21 | it 'returns false' do | |
22 | expect(target.debug_log?(path)).to be false | |
23 | end | |
24 | end | |
25 | end | |
26 | ||
27 | describe '#error_log?' do | |
28 | let(:path) { 'error.log' } | |
29 | ||
30 | context 'when the body matches' do | |
31 | %w[error.log].each do |file| | |
32 | context "when #{file} body" do | |
33 | let(:body) { File.read(fixtures.join('error_log', file)) } | |
34 | ||
35 | it 'returns true' do | |
36 | expect(target.error_log?(path)).to be true | |
37 | end | |
38 | end | |
39 | end | |
40 | end | |
41 | ||
42 | context 'when the body does not match' do | |
43 | let(:body) { '' } | |
44 | ||
45 | it 'returns false' do | |
46 | expect(target.error_log?(path)).to be false | |
47 | end | |
48 | end | |
49 | end | |
50 | ||
51 | describe '#full_path_disclosure?, #full_path_disclosure_entries' do | |
52 | let(:path) { 'p.php' } | |
53 | ||
54 | context 'when the body matches a FPD' do | |
55 | { | |
56 | 'wp_rss_functions.php' => %w[/short-path/rss-f.php] | |
57 | }.each do |file, expected| | |
58 | context "when #{file} body" do | |
59 | let(:body) { File.read(fixtures.join('fpd', file)) } | |
60 | ||
61 | it 'returns the expected array' do | |
62 | expect(target.full_path_disclosure_entries(path)).to eql expected | |
63 | expect(target.full_path_disclosure?(path)).to be true | |
64 | end | |
65 | end | |
66 | end | |
67 | end | |
68 | ||
69 | context 'when no FPD' do | |
70 | let(:body) { '' } | |
71 | ||
72 | it 'returns an empty array' do | |
73 | expect(target.full_path_disclosure_entries(path)).to eq [] | |
74 | expect(target.full_path_disclosure?(path)).to be false | |
75 | end | |
76 | end | |
77 | end | |
78 | end |
0 | require 'spec_helper' | |
1 | ||
2 | shared_examples CMSScanner::Target::Server::Apache do | |
3 | describe '#server' do | |
4 | its(:server) { should eq :Apache } | |
5 | end | |
6 | ||
7 | describe '#directory_listing?, #directory_listing_entries' do | |
8 | before { stub_request(:get, target.url(path)).to_return(body: body, status: status) } | |
9 | let(:path) { 'somedir' } | |
10 | ||
11 | context 'when not a 200' do | |
12 | let(:status) { 404 } | |
13 | let(:body) { '' } | |
14 | ||
15 | it 'returns false and an empty array' do | |
16 | expect(target.directory_listing?(path)).to be false | |
17 | expect(target.directory_listing_entries(path)).to eql [] | |
18 | end | |
19 | end | |
20 | ||
21 | context 'when 200' do | |
22 | let(:status) { 200 } | |
23 | let(:body) { File.read(fixtures.join('directory_listing', '2.2.16.html')) } | |
24 | ||
25 | it 'returns true and the expected array' do | |
26 | expect(target.directory_listing?(path)).to be true | |
27 | expect(target.directory_listing_entries(path)).to eq %w[backup.php database-empty.php] | |
28 | end | |
29 | end | |
30 | ||
31 | context 'when no files nor folders' do | |
32 | let(:status) { 200 } | |
33 | let(:body) { File.read(fixtures.join('directory_listing', 'empty.html')) } | |
34 | ||
35 | it 'returns true and the an empty array' do | |
36 | expect(target.directory_listing?(path)).to be true | |
37 | expect(target.directory_listing_entries(path)).to eql [] | |
38 | end | |
39 | end | |
40 | end | |
41 | end |
0 | require 'spec_helper' | |
1 | ||
2 | shared_examples CMSScanner::Target::Server::Generic do | |
3 | describe '#server' do | |
4 | before { stub_request(:head, target.url).to_return(headers: parse_headers_file(fixture)) } | |
5 | ||
6 | context 'when apache headers' do | |
7 | %w[basic.txt].each do |file| | |
8 | context "when #{file} headers" do | |
9 | let(:fixture) { fixtures.join('server', 'apache', file) } | |
10 | ||
11 | its(:server) { should eq :Apache } | |
12 | end | |
13 | end | |
14 | end | |
15 | ||
16 | context 'when iis headers' do | |
17 | %w[basic.txt].each do |file| | |
18 | context "when #{file} headers" do | |
19 | let(:fixture) { fixtures.join('server', 'iis', file) } | |
20 | ||
21 | its(:server) { should eq :IIS } | |
22 | end | |
23 | end | |
24 | end | |
25 | ||
26 | context 'when nginx headers' do | |
27 | %w[basic.txt].each do |file| | |
28 | context "when #{file} headers" do | |
29 | let(:fixture) { fixtures.join('server', 'nginx', file) } | |
30 | ||
31 | its(:server) { should eq :Nginx } | |
32 | end | |
33 | end | |
34 | end | |
35 | ||
36 | context 'not detected' do | |
37 | let(:fixture) { fixtures.join('server', 'not_detected.txt') } | |
38 | ||
39 | its(:server) { should be nil } | |
40 | end | |
41 | end | |
42 | ||
43 | describe '#directory_listing?' do | |
44 | # Handled in shared_examples/target/server/apache & nginx | |
45 | end | |
46 | end |
0 | require 'spec_helper' | |
1 | ||
2 | shared_examples CMSScanner::Target::Server::IIS do | |
3 | describe '#server' do | |
4 | its(:server) { should eq :IIS } | |
5 | end | |
6 | ||
7 | describe '#directory_listing?, #directory_listing_entries' do | |
8 | before { stub_request(:get, target.url(path)).to_return(body: body, status: status) } | |
9 | let(:path) { 'dir' } | |
10 | ||
11 | context 'when not a 200' do | |
12 | let(:status) { 404 } | |
13 | let(:body) { '' } | |
14 | ||
15 | it 'returns false and an empty array' do | |
16 | expect(target.directory_listing?(path)).to be false | |
17 | expect(target.directory_listing_entries(path)).to eql [] | |
18 | end | |
19 | end | |
20 | ||
21 | context 'when 200' do | |
22 | let(:status) { 200 } | |
23 | ||
24 | %w[with_parent.html no_parent.html].each do |file| | |
25 | context "when #{file} body" do | |
26 | let(:body) { File.read(fixtures.join('directory_listing', file)) } | |
27 | ||
28 | it 'returns true and the expected array' do | |
29 | expect(target.directory_listing?(path)).to be true | |
30 | expect(target.directory_listing_entries(path)).to eq %w[sub-dir web.config] | |
31 | end | |
32 | end | |
33 | end | |
34 | end | |
35 | end | |
36 | end |
0 | require 'spec_helper' | |
1 | ||
2 | shared_examples CMSScanner::Target::Server::Nginx do | |
3 | describe '#server' do | |
4 | its(:server) { should eq :Nginx } | |
5 | end | |
6 | ||
7 | describe '#directory_listing?, #directory_listing_entries' do | |
8 | before { stub_request(:get, target.url(path)).to_return(body: body, status: status) } | |
9 | let(:path) { 'somedir' } | |
10 | ||
11 | context 'when not a 200' do | |
12 | let(:status) { 404 } | |
13 | let(:body) { '' } | |
14 | ||
15 | it 'returns false and an empty array' do | |
16 | expect(target.directory_listing?(path)).to be false | |
17 | expect(target.directory_listing_entries(path)).to eql [] | |
18 | end | |
19 | end | |
20 | ||
21 | context 'when 200' do | |
22 | let(:status) { 200 } | |
23 | let(:body) { File.read(fixtures.join('directory_listing', 'fanart.html')) } | |
24 | ||
25 | it 'returns true and the expected array' do | |
26 | expect(target.directory_listing?(path)).to be true | |
27 | expect(target.directory_listing_entries(path)).to eql %w[1931/ 720/ down] | |
28 | end | |
29 | end | |
30 | ||
31 | context 'when no files nor folders' do | |
32 | let(:status) { 200 } | |
33 | let(:body) { File.read(fixtures.join('directory_listing', 'empty.html')) } | |
34 | ||
35 | it 'returns true and the an empty array' do | |
36 | expect(target.directory_listing?(path)).to be true | |
37 | expect(target.directory_listing_entries(path)).to eql [] | |
38 | end | |
39 | end | |
40 | end | |
41 | end |
0 | shared_examples 'App::Views::Core' do | |
1 | let(:controller) { CMSScanner::Controller::Core.new } | |
2 | let(:start) { Time.at(1_414_670_521).in_time_zone('Europe/London') } | |
3 | let(:tpl_vars) { { url: target_url, start_time: start } } | |
4 | ||
5 | describe 'version' do | |
6 | let(:view) { 'version' } | |
7 | ||
8 | it 'outputs the expected string' do | |
9 | @tpl_vars = {} | |
10 | ||
11 | @expected_output = if parsed_options[:format] == 'json' | |
12 | JSON.pretty_generate('version' => CMSScanner::VERSION) | |
13 | else | |
14 | "Version: #{CMSScanner::VERSION}\n" | |
15 | end | |
16 | end | |
17 | end | |
18 | ||
19 | describe 'help' do | |
20 | let(:view) { 'help' } | |
21 | ||
22 | it 'outputs the expected string' do | |
23 | @tpl_vars = { help: 'Help Message from OptionParser' } | |
24 | end | |
25 | end | |
26 | ||
27 | describe 'started' do | |
28 | let(:view) { 'started' } | |
29 | ||
30 | context 'when the target url and the effective_url are the same' do | |
31 | it 'outputs the expected string' do | |
32 | @tpl_vars = tpl_vars.merge(start_memory: 10, effective_url: target_url) | |
33 | end | |
34 | end | |
35 | ||
36 | context 'when target url != effective_url' do | |
37 | let(:expected_view) { 'started_effective_url' } | |
38 | ||
39 | it 'outputs the expected string' do | |
40 | @tpl_vars = tpl_vars.merge(start_memory: 10, effective_url: "#{target_url}home") | |
41 | end | |
42 | end | |
43 | end | |
44 | ||
45 | describe 'finished' do | |
46 | let(:view) { 'finished' } | |
47 | ||
48 | it 'outputs the expected string' do | |
49 | @tpl_vars = tpl_vars.merge( | |
50 | stop_time: Time.at(1_414_670_523).in_time_zone('Europe/London'), | |
51 | used_memory: 100, | |
52 | elapsed: 2, | |
53 | requests_done: 10 | |
54 | ) | |
55 | end | |
56 | end | |
57 | end |
0 | shared_examples 'App::Views::InterestingFindings' do | |
1 | let(:controller) { CMSScanner::Controller::InterestingFindings.new } | |
2 | let(:tpl_vars) { { url: target_url } } | |
3 | let(:interesting_file) { CMSScanner::InterestingFinding } | |
4 | ||
5 | describe 'findings' do | |
6 | let(:view) { 'findings' } | |
7 | let(:opts) { { confidence: 10, found_by: 'Spec' } } | |
8 | ||
9 | context 'when empty results' do | |
10 | let(:expected_view) { 'empty' } | |
11 | ||
12 | it 'outputs the expected string' do | |
13 | @tpl_vars = tpl_vars.merge(findings: []) | |
14 | end | |
15 | end | |
16 | ||
17 | it 'outputs the expected string' do | |
18 | findings = CMSScanner::Finders::Findings.new | |
19 | ||
20 | findings << | |
21 | interesting_file.new('F1', opts.merge(to_s: 'F1_to_s')) << | |
22 | interesting_file.new('F2', opts.merge(references: { url: 'R1' }, interesting_entries: %w[IE1])) << | |
23 | interesting_file.new('F2', opts.merge(found_by: 'Spec2')) << | |
24 | interesting_file.new('F3', | |
25 | opts.merge(references: { url: %w[R1 R2] }, interesting_entries: %w[IE1 IE2])) << | |
26 | interesting_file.new('F3', opts.merge(found_by: 'Spec2', confidence: 100)) << | |
27 | interesting_file.new('F3', opts.merge(found_by: 'Spec3')) << | |
28 | interesting_file.new('F4', opts.merge(confidence: 0)) << | |
29 | interesting_file.new('F4', opts.merge(confidence: 0, found_by: 'Spec2')) | |
30 | ||
31 | @tpl_vars = tpl_vars.merge(findings: findings) | |
32 | end | |
33 | end | |
34 | end |
0 | require 'shared_examples/browser_actions' | |
1 | require 'shared_examples/formatter_buffer' | |
2 | require 'shared_examples/formatter_class_methods' | |
3 | require 'shared_examples/finding' | |
4 | require 'shared_examples/independent_finder' | |
5 | require 'shared_examples/target/platform/php' | |
6 | require 'shared_examples/target/server/generic' | |
7 | require 'shared_examples/target/server/apache' | |
8 | require 'shared_examples/target/server/iis' | |
9 | require 'shared_examples/target/server/nginx' | |
10 | require 'shared_examples/views/core' | |
11 | require 'shared_examples/views/interesting_findings' | |
12 | require 'shared_examples/references' |
0 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) | |
1 | ||
2 | require 'simplecov' | |
3 | require 'rspec/its' | |
4 | require 'webmock/rspec' | |
5 | require 'active_support/time' | |
6 | ||
7 | if ENV['TRAVIS'] | |
8 | require 'coveralls' | |
9 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter | |
10 | end | |
11 | ||
12 | SimpleCov.start do | |
13 | add_filter '/example/' | |
14 | add_filter '/spec/' | |
15 | add_filter 'helper' | |
16 | end | |
17 | ||
18 | # See http://betterspecs.org/ | |
19 | RSpec.configure do |config| | |
20 | config.expect_with :rspec do |c| | |
21 | c.syntax = :expect | |
22 | end | |
23 | ||
24 | config.before(:each) do | |
25 | # Needed for rspec to run w/o error due to the at_exit hook calling the controller#target | |
26 | CMSScanner::Controller::Core.parsed_options = { url: 'http://ex.lo' } | |
27 | end | |
28 | end | |
29 | ||
30 | def count_files_in_dir(absolute_dir_path, files_pattern = '*') | |
31 | Dir.glob(File.join(absolute_dir_path, files_pattern)).count | |
32 | end | |
33 | ||
34 | # Parse a file containing raw headers and return the associated Hash | |
35 | # @return [ Hash ] | |
36 | def parse_headers_file(filepath) | |
37 | Typhoeus::Response::Header.new(File.read(filepath)) | |
38 | end | |
39 | ||
40 | require 'cms_scanner' | |
41 | require 'shared_examples' | |
42 | ||
43 | def rspec_parsed_options(args) | |
44 | controllers = CMSScanner::Controllers.new << | |
45 | CMSScanner::Controller::Core.new << | |
46 | CMSScanner::Controller::InterestingFindings.new | |
47 | ||
48 | controllers.option_parser.results(args.split(' ')) | |
49 | end | |
50 | ||
51 | # TODO: remove when https://github.com/bblimke/webmock/issues/552 fixed | |
52 | # rubocop:disable all | |
53 | module WebMock | |
54 | module HttpLibAdapters | |
55 | class TyphoeusAdapter < HttpLibAdapter | |
56 | def self.effective_url(effective_uri) | |
57 | effective_uri.port = nil if effective_uri.scheme == 'http' && effective_uri.port == 80 | |
58 | effective_uri.port = nil if effective_uri.scheme == 'https' && effective_uri.port == 443 | |
59 | ||
60 | effective_uri.to_s | |
61 | end | |
62 | ||
63 | def self.generate_typhoeus_response(request_signature, webmock_response) | |
64 | response = if webmock_response.should_timeout | |
65 | ::Typhoeus::Response.new( | |
66 | code: 0, | |
67 | status_message: '', | |
68 | body: '', | |
69 | headers: {}, | |
70 | return_code: :operation_timedout | |
71 | ) | |
72 | else | |
73 | ::Typhoeus::Response.new( | |
74 | code: webmock_response.status[0], | |
75 | status_message: webmock_response.status[1], | |
76 | body: webmock_response.body, | |
77 | headers: webmock_response.headers, | |
78 | effective_url: effective_url(request_signature.uri) | |
79 | ) | |
80 | end | |
81 | response.mock = :webmock | |
82 | response | |
83 | end | |
84 | end | |
85 | end | |
86 | end | |
87 | # rubocop:enabled all | |
88 | ||
89 | SPECS = Pathname.new(__FILE__).dirname | |
90 | CACHE = SPECS.join('cache') | |
91 | FIXTURES = SPECS.join('fixtures') | |
92 | FIXTURES_VIEWS = FIXTURES.join('views') | |
93 | FIXTURES_FINDERS = FIXTURES.join('finders') | |
94 | FIXTURES_MODELS = FIXTURES.join('models') | |
95 | APP_VIEWS = File.join(CMSScanner::APP_DIR, 'views') |