new_cms_scanner 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +26 -0
  4. data/app/app.rb +24 -0
  5. data/app/controllers/core/cli_options.rb +117 -0
  6. data/app/controllers/core.rb +82 -0
  7. data/app/controllers/interesting_findings.rb +25 -0
  8. data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
  9. data/app/finders/interesting_findings/headers.rb +17 -0
  10. data/app/finders/interesting_findings/robots_txt.rb +20 -0
  11. data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
  12. data/app/finders/interesting_findings/xml_rpc.rb +61 -0
  13. data/app/finders/interesting_findings.rb +25 -0
  14. data/app/formatters/cli.rb +65 -0
  15. data/app/formatters/cli_no_color.rb +9 -0
  16. data/app/formatters/cli_no_colour.rb +17 -0
  17. data/app/formatters/json.rb +14 -0
  18. data/app/models/fantastico_fileslist.rb +34 -0
  19. data/app/models/headers.rb +44 -0
  20. data/app/models/interesting_finding.rb +48 -0
  21. data/app/models/robots_txt.rb +31 -0
  22. data/app/models/search_replace_db_2.rb +17 -0
  23. data/app/models/user.rb +35 -0
  24. data/app/models/version.rb +49 -0
  25. data/app/models/xml_rpc.rb +78 -0
  26. data/app/user_agents.txt +46 -0
  27. data/app/views/cli/core/banner.erb +1 -0
  28. data/app/views/cli/core/finished.erb +8 -0
  29. data/app/views/cli/core/help.erb +4 -0
  30. data/app/views/cli/core/started.erb +6 -0
  31. data/app/views/cli/core/version.erb +1 -0
  32. data/app/views/cli/interesting_findings/_array.erb +10 -0
  33. data/app/views/cli/interesting_findings/findings.erb +23 -0
  34. data/app/views/cli/scan_aborted.erb +5 -0
  35. data/app/views/cli/usage.erb +3 -0
  36. data/app/views/json/core/banner.erb +1 -0
  37. data/app/views/json/core/finished.erb +10 -0
  38. data/app/views/json/core/help.erb +4 -0
  39. data/app/views/json/core/started.erb +5 -0
  40. data/app/views/json/core/version.erb +1 -0
  41. data/app/views/json/interesting_findings/findings.erb +24 -0
  42. data/app/views/json/scan_aborted.erb +5 -0
  43. data/lib/cms_scanner/browser/actions.rb +48 -0
  44. data/lib/cms_scanner/browser/options.rb +90 -0
  45. data/lib/cms_scanner/browser.rb +96 -0
  46. data/lib/cms_scanner/cache/file_store.rb +77 -0
  47. data/lib/cms_scanner/cache/typhoeus.rb +25 -0
  48. data/lib/cms_scanner/controller.rb +105 -0
  49. data/lib/cms_scanner/controllers.rb +67 -0
  50. data/lib/cms_scanner/errors/http.rb +72 -0
  51. data/lib/cms_scanner/errors/scan.rb +14 -0
  52. data/lib/cms_scanner/errors.rb +11 -0
  53. data/lib/cms_scanner/exit_code.rb +25 -0
  54. data/lib/cms_scanner/finders/base_finders.rb +45 -0
  55. data/lib/cms_scanner/finders/finder/breadth_first_dictionary_attack.rb +121 -0
  56. data/lib/cms_scanner/finders/finder/enumerator.rb +77 -0
  57. data/lib/cms_scanner/finders/finder/fingerprinter.rb +48 -0
  58. data/lib/cms_scanner/finders/finder/smart_url_checker/findings.rb +33 -0
  59. data/lib/cms_scanner/finders/finder/smart_url_checker.rb +60 -0
  60. data/lib/cms_scanner/finders/finder.rb +75 -0
  61. data/lib/cms_scanner/finders/finding.rb +54 -0
  62. data/lib/cms_scanner/finders/findings.rb +26 -0
  63. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  64. data/lib/cms_scanner/finders/independent_finders.rb +26 -0
  65. data/lib/cms_scanner/finders/same_type_finder.rb +19 -0
  66. data/lib/cms_scanner/finders/same_type_finders.rb +26 -0
  67. data/lib/cms_scanner/finders/unique_finder.rb +19 -0
  68. data/lib/cms_scanner/finders/unique_finders.rb +47 -0
  69. data/lib/cms_scanner/finders.rb +12 -0
  70. data/lib/cms_scanner/formatter/buffer.rb +17 -0
  71. data/lib/cms_scanner/formatter.rb +149 -0
  72. data/lib/cms_scanner/helper.rb +7 -0
  73. data/lib/cms_scanner/numeric.rb +13 -0
  74. data/lib/cms_scanner/parsed_cli.rb +37 -0
  75. data/lib/cms_scanner/progressbar_null_output.rb +23 -0
  76. data/lib/cms_scanner/public_suffix/domain.rb +42 -0
  77. data/lib/cms_scanner/references.rb +132 -0
  78. data/lib/cms_scanner/scan.rb +88 -0
  79. data/lib/cms_scanner/target/hashes.rb +45 -0
  80. data/lib/cms_scanner/target/platform/php.rb +62 -0
  81. data/lib/cms_scanner/target/platform.rb +3 -0
  82. data/lib/cms_scanner/target/scope.rb +103 -0
  83. data/lib/cms_scanner/target/server/apache.rb +27 -0
  84. data/lib/cms_scanner/target/server/generic.rb +72 -0
  85. data/lib/cms_scanner/target/server/iis.rb +29 -0
  86. data/lib/cms_scanner/target/server/nginx.rb +27 -0
  87. data/lib/cms_scanner/target/server.rb +6 -0
  88. data/lib/cms_scanner/target.rb +124 -0
  89. data/lib/cms_scanner/typhoeus/hydra.rb +12 -0
  90. data/lib/cms_scanner/typhoeus/response.rb +27 -0
  91. data/lib/cms_scanner/version.rb +6 -0
  92. data/lib/cms_scanner/vulnerability.rb +46 -0
  93. data/lib/cms_scanner/web_site.rb +145 -0
  94. data/lib/cms_scanner.rb +141 -0
  95. metadata +426 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ class Finder
6
+ # Module to provide an easy way to enumerate items such as plugins, themes etc
7
+ module Enumerator
8
+ # @return [ Hash ]
9
+ def head_or_get_request_params
10
+ # Disabling the cache, as it causes a 'stack level too deep' exception
11
+ # with a large number of requests.
12
+ # See https://github.com/typhoeus/typhoeus/issues/408
13
+ @head_or_get_request_params ||= target.head_or_get_params.merge(cache_ttl: 0)
14
+ end
15
+
16
+ # @return [ Array<Integer> ]
17
+ def valid_response_codes
18
+ @valid_response_codes ||= [200]
19
+ end
20
+
21
+ # @param [ Hash ] The target urls
22
+ # @param [ Hash ] opts
23
+ # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
24
+ # @option opts [ Regexp ] :exclude_content
25
+ # @option opts [ Boolean, Array, String ] :check_full_response
26
+ #
27
+ # @yield [ Typhoeus::Response, String ]
28
+ def enumerate(urls, opts = {})
29
+ create_progress_bar(opts.merge(total: urls.size))
30
+
31
+ urls.each do |url, id|
32
+ request = browser.forge_request(url, head_or_get_request_params)
33
+
34
+ request.on_complete do |head_res|
35
+ progress_bar.increment
36
+
37
+ next unless valid_response_codes.include?(head_res.code)
38
+
39
+ next if opts[:exclude_content] && head_res.response_headers&.match(opts[:exclude_content])
40
+
41
+ head_or_full_res = maybe_get_full_response(head_res, opts)
42
+
43
+ yield head_or_full_res, id if head_or_full_res
44
+ end
45
+
46
+ hydra.queue(request)
47
+ end
48
+
49
+ hydra.run
50
+ end
51
+
52
+ # @param [ Typhoeus::Response ] head_res
53
+ # @param [ Hash ] opts
54
+ #
55
+ # @return [ Typhoeus::Response, nil ]
56
+ def maybe_get_full_response(head_res, opts)
57
+ return head_res unless opts[:check_full_response] == true ||
58
+ Array(opts[:check_full_response]).include?(head_res.code)
59
+
60
+ full_res = NS::Browser.get(head_res.effective_url, full_request_params)
61
+
62
+ return unless valid_response_codes.include?(full_res.code)
63
+
64
+ return if target.homepage_or_404?(full_res) ||
65
+ (opts[:exclude_content] && full_res.body&.match(opts[:exclude_content]))
66
+
67
+ full_res
68
+ end
69
+
70
+ # @return [ Hash ]
71
+ def full_request_params
72
+ @full_request_params ||= {}
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ class Finder
6
+ # Module to provide an easy way to fingerprint things such as versions
7
+ module Fingerprinter
8
+ include Enumerator
9
+
10
+ # @param [ Hash ] fingerprints The fingerprints
11
+ # Format should be like the following:
12
+ # {
13
+ # file_path_1: {
14
+ # md5_hash_1: version_1,
15
+ # md5_hash_2: [version_2]
16
+ # },
17
+ # file_path_2: {
18
+ # md5_hash_3: [version_1, version_2],
19
+ # md5_hash_4: version_3
20
+ # }
21
+ # }
22
+ # Note that the version can either be an array or a string
23
+ #
24
+ # @param [ Hash ] opts
25
+ # @option opts [ Boolean ] :show_progression Wether or not to display the progress bar
26
+ #
27
+ # @yield [ Mixed, String, String ] version/s, url, hash The version associated to the
28
+ # fingerprint of the url
29
+ def fingerprint(fingerprints, opts = {})
30
+ enum_opts = opts.merge(check_full_response: 200)
31
+
32
+ enumerate(fingerprints.transform_keys { |k| target.url(k) }, enum_opts) do |res, fingerprint|
33
+ md5sum = hexdigest(res.body)
34
+
35
+ next unless fingerprint.key?(md5sum)
36
+
37
+ yield fingerprint[md5sum], res.effective_url, md5sum
38
+ end
39
+ end
40
+
41
+ # @return [ String ] The hashed value for the given body
42
+ def hexdigest(body)
43
+ Digest::MD5.hexdigest(body)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ class Finder
6
+ module SmartURLChecker
7
+ # Findings
8
+ class Findings < Array
9
+ def <<(finding)
10
+ return self unless finding
11
+
12
+ each do |f|
13
+ next unless f == finding && f.found_by == finding.found_by
14
+
15
+ # This makes sure entries added are unique
16
+ # and prevent pages redirecting to the same one to be added twice
17
+ entries_to_add = finding.interesting_entries - f.interesting_entries
18
+ return self if entries_to_add.empty?
19
+
20
+ entries_to_add.each { |entry| f.interesting_entries << entry }
21
+
22
+ f.confidence += finding.confidence
23
+
24
+ return self
25
+ end
26
+
27
+ super(finding)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/finders/finder/smart_url_checker/findings'
4
+
5
+ module CMSScanner
6
+ module Finders
7
+ class Finder
8
+ # Smart URL Checker
9
+ # Typically used when some URLs are potentially in the homepage. If they are found
10
+ # in it, they will be checked in the #passive (like a browser/client would do when loading the page).
11
+ # Otherwise they will be checked in the #aggressive
12
+ module SmartURLChecker
13
+ # @param [ Array<String> ] urls
14
+ # @param [ Hash ] opts
15
+ #
16
+ # @return []
17
+ def process_urls(_urls, _opts = {})
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # @param [ Hash ] opts
22
+ #
23
+ # @return [ Array<Finding> ]
24
+ def passive(opts = {})
25
+ process_urls(passive_urls(opts), opts)
26
+ end
27
+
28
+ # @param [ Hash ] opts
29
+ #
30
+ # @return [ Array<String> ]
31
+ def passive_urls(_opts = {})
32
+ target.in_scope_uris(target.homepage_res, passive_urls_xpath).map(&:to_s)
33
+ end
34
+
35
+ # @return [ String ]
36
+ def passive_urls_xpath
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # @param [ Hash ] opts
41
+ #
42
+ # @return [ Array<Finding> ]
43
+ def aggressive(opts = {})
44
+ # To avoid scanning the same twice
45
+ urls = aggressive_urls(opts)
46
+ urls -= passive_urls(opts) if opts[:mode] == :mixed
47
+
48
+ process_urls(urls, opts)
49
+ end
50
+
51
+ # @param [ Hash ] opts
52
+ #
53
+ # @return [ Array<String> ]
54
+ def aggressive_urls(_opts = {})
55
+ raise NotImplementedError
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/finders/finder/smart_url_checker'
4
+ require 'cms_scanner/finders/finder/enumerator'
5
+ require 'cms_scanner/finders/finder/fingerprinter'
6
+ require 'cms_scanner/finders/finder/breadth_first_dictionary_attack'
7
+
8
+ module CMSScanner
9
+ module Finders
10
+ # Finder
11
+ class Finder
12
+ # Constants for common found_by
13
+ DIRECT_ACCESS = 'Direct Access (Aggressive Detection)'
14
+
15
+ attr_accessor :target, :progress_bar
16
+
17
+ def initialize(target)
18
+ @target = target
19
+ end
20
+
21
+ # @return [ String ] The titleized name of the finder
22
+ def titleize
23
+ # Put a _ char before any digits except those at the end, which will be replaced by a space
24
+ # Otherwise, class such as Error404Page are returned as Error404 Page instead of Error 404 page
25
+ # The keep_id_suffix is to concevert classes such as CssId to Css Id instead of Css
26
+
27
+ @titleize ||= self.class.to_s.demodulize.gsub(/(\d+)[a-z]+/i, '_\0').titleize(keep_id_suffix: true)
28
+ end
29
+
30
+ # @param [ Hash ] _opts
31
+ def passive(_opts = {}); end
32
+
33
+ # @param [ Hash ] _opts
34
+ def aggressive(_opts = {}); end
35
+
36
+ # @param [ Hash ] opts See https://github.com/jfelchner/ruby-progressbar/wiki/Options
37
+ # @option opts [ Boolean ] :show_progression
38
+ #
39
+ # @return [ ProgressBar::Base ]
40
+ def create_progress_bar(opts = {})
41
+ bar_opts = { format: '%t %a <%B> (%c / %C) %P%% %e' }
42
+ bar_opts[:output] = ProgressBarNullOutput unless opts[:show_progression]
43
+
44
+ @progress_bar = ::ProgressBar.create(bar_opts.merge(opts))
45
+ end
46
+
47
+ # @return [ Browser ]
48
+ def browser
49
+ @browser ||= NS::Browser.instance
50
+ end
51
+
52
+ # @return [ Typhoeus::Hydra ]
53
+ def hydra
54
+ @hydra ||= browser.hydra
55
+ end
56
+
57
+ # @param [String, Class ] klass
58
+ # @return [ String ]
59
+ def found_by(klass = self.class)
60
+ labels = %w[aggressive passive]
61
+
62
+ caller_locations.each do |call|
63
+ label = call.label
64
+
65
+ next unless labels.include? label
66
+
67
+ title = klass.to_s.demodulize.gsub(/(\d+)[a-z]+/i, '_\0').titleize(keep_id_suffix: true)
68
+
69
+ return "#{title} (#{label.capitalize} Detection)"
70
+ end
71
+ nil
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # Finding
6
+ module Finding
7
+ # Fix for "Double/Dynamic Inclusion Problem"
8
+ def self.included(base)
9
+ base.include References
10
+ super(base)
11
+ end
12
+
13
+ FINDING_OPTS = %i[confidence confirmed_by references found_by interesting_entries].freeze
14
+
15
+ attr_accessor(*FINDING_OPTS)
16
+
17
+ # @return [ Array ]
18
+ def confirmed_by
19
+ @confirmed_by ||= []
20
+ end
21
+
22
+ # Should be overriden in child classes
23
+ # @return [ Array ]
24
+ def interesting_entries
25
+ @interesting_entries ||= []
26
+ end
27
+
28
+ # @return [ Integer ]
29
+ def confidence
30
+ @confidence ||= 0
31
+ end
32
+
33
+ # @param [ Integer ] value
34
+ def confidence=(value)
35
+ @confidence = value >= 100 ? 100 : value
36
+ end
37
+
38
+ # @param [ Hash ] opts
39
+ def parse_finding_options(opts = {})
40
+ FINDING_OPTS.each { |opt| send("#{opt}=", opts[opt]) if opts.key?(opt) }
41
+ end
42
+
43
+ # TODO: maybe also check for interesting_entries and confirmed_by ?
44
+ # So far this is used in specs only
45
+ def eql?(other)
46
+ self == other && confidence == other.confidence && found_by == other.found_by
47
+ end
48
+
49
+ def <=>(other)
50
+ to_s.downcase <=> other.to_s.downcase
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # Findings container
6
+ class Findings < Array
7
+ # Override to include the confirmed_by logic
8
+ #
9
+ # @param [ Finding ] finding
10
+ def <<(finding)
11
+ return self unless finding
12
+
13
+ each do |found|
14
+ next unless found == finding
15
+
16
+ found.confirmed_by << finding
17
+ found.confidence += finding.confidence
18
+
19
+ return self
20
+ end
21
+
22
+ super(finding)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # Independent Finder
6
+ module IndependentFinder
7
+ extend ActiveSupport::Concern
8
+
9
+ # See ActiveSupport::Concern
10
+ module ClassMethods
11
+ def find(target, opts = {})
12
+ new(target).find(opts)
13
+ end
14
+ end
15
+
16
+ # @param [ Hash ] opts
17
+ # @option opts [ Symbol ] mode (:mixed, :passive, :aggressive)
18
+ #
19
+ # @return [ Findings ]
20
+ def find(opts = {})
21
+ finders.run(opts)
22
+ end
23
+
24
+ # @return [ Array ]
25
+ def finders
26
+ @finders ||= NS::Finders::IndependentFinders.new
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # This class is designed to handle independent results
6
+ # which are not related with each others
7
+ # e.g: interesting files
8
+ class IndependentFinders < BaseFinders
9
+ # @param [ Hash ] opts
10
+ # @option opts [ Symbol ] mode :mixed, :passive or :aggressive
11
+ #
12
+ # @return [ Findings ]
13
+ def run(opts = {})
14
+ methods = symbols_from_mode(opts[:mode])
15
+
16
+ each do |finder|
17
+ methods.each do |symbol|
18
+ run_finder(finder, symbol, opts)
19
+ end
20
+ end
21
+
22
+ filter_findings
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # Same Type Finder
6
+ module SameTypeFinder
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include IndependentFinder
10
+
11
+ # @return [ Array ]
12
+ def finders
13
+ @finders ||= NS::Finders::SameTypeFinders.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # This class is designed to handle same type results, such as enumeration of plugins,
6
+ # themes etc.
7
+ class SameTypeFinders < BaseFinders
8
+ # @param [ Hash ] opts
9
+ # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
10
+ # @option opts [ Boolean ] :sort Wether or not to sort the findings
11
+ #
12
+ # @return [ Findings ]
13
+ def run(opts = {})
14
+ symbols_from_mode(opts[:mode]).each do |symbol|
15
+ each do |finder|
16
+ run_finder(finder, symbol, opts)
17
+ end
18
+ end
19
+
20
+ findings.sort! if opts[:sort]
21
+
22
+ filter_findings
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # Unique Finder
6
+ module UniqueFinder
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include IndependentFinder
10
+
11
+ # @return [ Array ]
12
+ def finders
13
+ @finders ||= NS::Finders::UniqueFinders.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Finders
5
+ # This class is designed to return a unique result such as a version
6
+ # Note: Finders contained can return multiple results but the #run will only
7
+ # returned the best finding
8
+ class UniqueFinders < BaseFinders
9
+ # @param [ Hash ] opts
10
+ # @option opts [ Symbol ] :mode :mixed, :passive or :aggressive
11
+ # @option opts [ Int ] :confidence_threshold If a finding's confidence reaches this value,
12
+ # it will be returned as the best finding.
13
+ # Default is 100.
14
+ # If <= 0, all finders will be ran.
15
+ #
16
+ # @return [ Object, false ] The best finding or false if none
17
+ def run(opts = {})
18
+ opts[:confidence_threshold] ||= 100
19
+
20
+ symbols_from_mode(opts[:mode]).each do |symbol|
21
+ each do |finder|
22
+ run_finder(finder, symbol, opts)
23
+
24
+ next if opts[:confidence_threshold] <= 0
25
+
26
+ findings.each { |f| return f if f.confidence >= opts[:confidence_threshold] }
27
+ end
28
+ end
29
+
30
+ filter_findings
31
+ end
32
+
33
+ protected
34
+
35
+ # @return [ Object, false ] The best finding or false if none
36
+ def filter_findings
37
+ # results are sorted by confidence ASC
38
+ findings.sort_by!(&:confidence)
39
+
40
+ # If all findings have the same confidence, false is returned
41
+ return false if findings.size > 1 && findings.first.confidence == findings.last.confidence
42
+
43
+ findings.last || false
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/finders/finder'
4
+ require 'cms_scanner/finders/finding'
5
+ require 'cms_scanner/finders/findings'
6
+ require 'cms_scanner/finders/base_finders'
7
+ require 'cms_scanner/finders/independent_finders'
8
+ require 'cms_scanner/finders/independent_finder'
9
+ require 'cms_scanner/finders/unique_finders'
10
+ require 'cms_scanner/finders/unique_finder'
11
+ require 'cms_scanner/finders/same_type_finders'
12
+ require 'cms_scanner/finders/same_type_finder'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Formatter
5
+ # Module used to output the rendered views into a buffer
6
+ # and beautify it a the end of the scan
7
+ module Buffer
8
+ def output(tpl, vars = {}, controller_name = nil)
9
+ buffer << render(tpl, vars, controller_name).encode('UTF-8', invalid: :replace, undef: :replace)
10
+ end
11
+
12
+ def buffer
13
+ @buffer ||= +''
14
+ end
15
+ end
16
+ end
17
+ end