pluginscan 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.gitlab-ci.yml +16 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +46 -0
  6. data/.rubocop_todo.yml +36 -0
  7. data/CHANGELOG.md +89 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +90 -0
  10. data/README.md +56 -0
  11. data/Rakefile +2 -0
  12. data/TODO.md +8 -0
  13. data/bin/pluginscan +53 -0
  14. data/lib/file_creator.rb +18 -0
  15. data/lib/pluginscan.rb +69 -0
  16. data/lib/pluginscan/error.rb +9 -0
  17. data/lib/pluginscan/error_printer.rb +17 -0
  18. data/lib/pluginscan/file_finder.rb +42 -0
  19. data/lib/pluginscan/printer.rb +14 -0
  20. data/lib/pluginscan/reports/cloc_report.rb +27 -0
  21. data/lib/pluginscan/reports/cloc_report/cloc.rb +21 -0
  22. data/lib/pluginscan/reports/cloc_report/cloc_printer.rb +42 -0
  23. data/lib/pluginscan/reports/cloc_report/cloc_scanner.rb +41 -0
  24. data/lib/pluginscan/reports/cloc_report/system_cloc.rb +33 -0
  25. data/lib/pluginscan/reports/issues_report.rb +24 -0
  26. data/lib/pluginscan/reports/issues_report/error_list_printer.rb +99 -0
  27. data/lib/pluginscan/reports/issues_report/issue_checks.rb +382 -0
  28. data/lib/pluginscan/reports/issues_report/issue_checks/check.rb +55 -0
  29. data/lib/pluginscan/reports/issues_report/issue_checks/comment_checker.rb +13 -0
  30. data/lib/pluginscan/reports/issues_report/issue_checks/function_check.rb +32 -0
  31. data/lib/pluginscan/reports/issues_report/issue_checks/variable_check.rb +14 -0
  32. data/lib/pluginscan/reports/issues_report/issue_checks/variable_safety_checker.rb +112 -0
  33. data/lib/pluginscan/reports/issues_report/issues_models/check_findings.rb +29 -0
  34. data/lib/pluginscan/reports/issues_report/issues_models/issues.rb +31 -0
  35. data/lib/pluginscan/reports/issues_report/issues_printer.rb +34 -0
  36. data/lib/pluginscan/reports/issues_report/issues_printer/check_findings_printer.rb +37 -0
  37. data/lib/pluginscan/reports/issues_report/issues_printer/file_issues_printer.rb +36 -0
  38. data/lib/pluginscan/reports/issues_report/issues_printer/finding_printer.rb +38 -0
  39. data/lib/pluginscan/reports/issues_report/issues_printer_factory.rb +19 -0
  40. data/lib/pluginscan/reports/issues_report/issues_scanner.rb +49 -0
  41. data/lib/pluginscan/reports/issues_report/issues_scanner/file_issues_scanner.rb +39 -0
  42. data/lib/pluginscan/reports/issues_report/issues_scanner/line_issues_scanner.rb +15 -0
  43. data/lib/pluginscan/reports/issues_report/issues_scanner/utf8_checker.rb +14 -0
  44. data/lib/pluginscan/reports/sloccount_report.rb +26 -0
  45. data/lib/pluginscan/reports/sloccount_report/sloccount.rb +19 -0
  46. data/lib/pluginscan/reports/sloccount_report/sloccount_printer.rb +22 -0
  47. data/lib/pluginscan/reports/sloccount_report/sloccount_scanner.rb +86 -0
  48. data/lib/pluginscan/reports/vulnerability_report.rb +28 -0
  49. data/lib/pluginscan/reports/vulnerability_report/advisories_api.rb +23 -0
  50. data/lib/pluginscan/reports/vulnerability_report/vulnerabilities_printer.rb +55 -0
  51. data/lib/pluginscan/reports/vulnerability_report/vulnerability_scanner.rb +17 -0
  52. data/lib/pluginscan/reports/vulnerability_report/wp_vuln_db_api.rb +77 -0
  53. data/lib/pluginscan/version.rb +3 -0
  54. data/pluginscan.gemspec +31 -0
  55. data/spec/acceptance/cloc_spec.rb +54 -0
  56. data/spec/acceptance/create_error_list_file_spec.rb +29 -0
  57. data/spec/acceptance/issues_spec.rb +197 -0
  58. data/spec/acceptance/pluginscan_spec.rb +18 -0
  59. data/spec/acceptance/sloccount_spec.rb +39 -0
  60. data/spec/acceptance/vulnerabilities_spec.rb +57 -0
  61. data/spec/acceptance_spec_helper.rb +10 -0
  62. data/spec/checks_examples_spec.rb +352 -0
  63. data/spec/file_creator_spec.rb +51 -0
  64. data/spec/pluginscan/cloc_scanner/cloc_scanner_spec.rb +64 -0
  65. data/spec/pluginscan/cloc_scanner/cloc_spec.rb +30 -0
  66. data/spec/pluginscan/file_finder_spec.rb +91 -0
  67. data/spec/pluginscan/issues_scanner/check_findings_spec.rb +22 -0
  68. data/spec/pluginscan/issues_scanner/error_list_printer_ignores_spec.rb +35 -0
  69. data/spec/pluginscan/issues_scanner/error_list_printer_spec.rb +42 -0
  70. data/spec/pluginscan/issues_scanner/file_issues_scanner_spec.rb +25 -0
  71. data/spec/pluginscan/issues_scanner/issues_printer_factory_spec.rb +9 -0
  72. data/spec/pluginscan/issues_scanner/issues_spec.rb +55 -0
  73. data/spec/pluginscan/issues_scanner/variable_check_spec.rb +13 -0
  74. data/spec/pluginscan/issues_scanner/variable_safety_checker_spec.rb +81 -0
  75. data/spec/pluginscan/issues_scanner_spec.rb +21 -0
  76. data/spec/pluginscan/sloccount_scanner/sloccount_scanner_spec.rb +95 -0
  77. data/spec/pluginscan/sloccount_scanner/sloccount_spec.rb +72 -0
  78. data/spec/pluginscan/vulnerability_scanner_spec.rb +96 -0
  79. data/spec/process_spec_helper.rb +6 -0
  80. data/spec/spec_helper.rb +70 -0
  81. data/spec/support/acceptance_helpers.rb +68 -0
  82. data/spec/support/file_helpers.rb +35 -0
  83. data/spec/support/heredoc_helper.rb +7 -0
  84. data/spec/support/process_helpers.rb +25 -0
  85. data/spec/support/shared_examples_for_issue_checks.rb +31 -0
  86. data/spec/support/vcr_helper.rb +6 -0
  87. data/vcr_cassettes/wpvulndb/relevanssi.yml +78 -0
  88. metadata +342 -0
@@ -0,0 +1,15 @@
1
+ module Pluginscan
2
+ # Responsible for scanning one line of a file for issues of a certain type
3
+ class LineIssuesScanner
4
+ def initialize(check)
5
+ @check = check
6
+ end
7
+
8
+ def scan(line, lineno)
9
+ @check.run(line).map do |match|
10
+ ignored = @check.ignore?(match, line)
11
+ Finding.new(lineno, line, match, ignored)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module Pluginscan
2
+ # Responsible for checking a file for utf-8 validity
3
+ class UTF8Checker
4
+ # TODO: This returns nil if the file was valid. Returning nil is bad.
5
+ def check(file_contents)
6
+ # Check file contents are valid UTF-8
7
+ file_contents.force_encoding('utf-8')
8
+ return if file_contents.valid_encoding?
9
+ CheckFindings.new(
10
+ Check.new(name: 'Encoding', message: 'invalid UTF-8')
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require 'pluginscan/reports/sloccount_report/sloccount'
2
+ require 'pluginscan/reports/sloccount_report/sloccount_scanner'
3
+ require 'pluginscan/reports/sloccount_report/sloccount_printer'
4
+ require 'pluginscan/error_printer'
5
+
6
+ module Pluginscan
7
+ module Reports
8
+ module SLOCCountReport
9
+ def self.print(plugin_directory, printer, error_printer)
10
+ sloccount = SLOCCountScanner.new.scan(plugin_directory)
11
+ printer.print(sloccount)
12
+
13
+ rescue SLOCCountScanner::Unavailable
14
+ error_printer.print("The 'sloccount' command is unavailable. Is it installed and in your path?")
15
+ rescue SLOCCountScanner::Exception => e
16
+ error_printer.print("An error occurred while calculating the SLOCCount:\n #{e.message}")
17
+ rescue StandardError => e
18
+ # We don't want an error with CLOC to interrupt the rest of pluginscan
19
+ # TODO: not sure this is sensible
20
+ error_printer.print("An error occurred while calculating the SLOCCount:\n #{e.message}")
21
+ else
22
+ return true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # Responsible for accessing specific data in the output of the `sloccount` command
2
+ class SLOCCount
3
+ def initialize(sloccount_output)
4
+ @sloccount_output = sloccount_output
5
+ end
6
+
7
+ def total
8
+ total_regex = /^Total Physical Source Lines of Code.*= ([\d\,]+)/
9
+ # Numbers in sloccount are displayed with commas e.g. 1,791
10
+ @sloccount_output.match(total_regex)[1].delete(',').to_i
11
+ end
12
+ end
13
+
14
+ # TODO: should be ZeroSLOCCount? More descriptive, but less obvious it's null object pattern
15
+ class NullSLOCCount
16
+ def total
17
+ 0
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'pluginscan/printer'
2
+
3
+ module Pluginscan
4
+ class SLOCCountPrinter < Printer
5
+ def print(sloccount)
6
+ print_headline
7
+ print_results(sloccount)
8
+ print_blank_line
9
+ end
10
+
11
+ private
12
+
13
+ def print_headline
14
+ @output.puts "Total sloccount (from David A. Wheeler's 'SLOCCount' tool):".color(:blue)
15
+ end
16
+
17
+ def print_results(sloccount)
18
+ total = sloccount.total.to_s.color(:red)
19
+ @output.puts " #{total} source lines of code"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,86 @@
1
+ require 'open3'
2
+
3
+ # Responsible for running the `sloccount` system command and handling any resulting errors
4
+ # The SLOCCount project is at http://www.dwheeler.com/sloccount/ and can be installed on OSX using homebrew:
5
+ # brew install sloccount
6
+ class SLOCCountScanner
7
+ class Exception < StandardError; end
8
+ class ArgumentError < RuntimeError; end
9
+ class Unavailable < RuntimeError; end
10
+ class NoInput < RuntimeError; end
11
+ class NoDirectory < RuntimeError; end
12
+
13
+ def initialize(system_sloccount = SystemSLOCCount.instance)
14
+ @system_sloccount = system_sloccount
15
+ end
16
+
17
+ def scan(directory)
18
+ fail ArgumentError, "directory must must be a string (or quack like a string)" unless directory.respond_to?(:to_str)
19
+ fail Unavailable, "The 'sloccount' command is unavailable. Is it installed and in your path?" unless @system_sloccount.available?
20
+ fail NoDirectory, "No such directory: '#{directory}'" unless Dir.exist?(directory)
21
+
22
+ result, status = @system_sloccount.call(directory)
23
+
24
+ if status.success?
25
+ SLOCCount.new(result)
26
+ else
27
+ check_for_errors(result) { NullSLOCCount.new }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def check_for_errors(result)
34
+ case result
35
+ when /SLOC total is zero, no further analysis performed/
36
+ yield
37
+
38
+ when /Error: You must provide a directory or directories of source code/
39
+ # Because of the check above, it shouldn't be possible to get here
40
+ raise NoInput, "sloccount requires a directory or directories of source code"
41
+ else
42
+ raise Exception, "sloccount raised an error we didn't recognise. Here's the output:\n#{result}"
43
+ end
44
+ end
45
+
46
+ # Responsible for isolating the system calls to run SLOCCount
47
+ class SystemSLOCCount
48
+ require 'singleton'
49
+ include Singleton
50
+
51
+ COMMAND_NAME = "sloccount".freeze
52
+
53
+ def available?
54
+ _result, status = system_which_sloccount
55
+ status.success?
56
+ end
57
+
58
+ def call(directory)
59
+ # DANGER!!! calling system command
60
+ # also: danger! not covered by specs
61
+
62
+ # Should be safe from injection because of the `Dir.exist?` check.
63
+ Open3.capture2("#{COMMAND_NAME} #{directory} 2> #{error_log_name}")
64
+
65
+ ensure
66
+ delete_empty_error_log
67
+ end
68
+
69
+ private
70
+
71
+ def system_which_sloccount
72
+ # DANGER!!! calling system command
73
+ # also: danger! not covered by specs
74
+
75
+ Open3.capture2("which #{COMMAND_NAME}")
76
+ end
77
+
78
+ def error_log_name
79
+ "#{COMMAND_NAME}_error.log"
80
+ end
81
+
82
+ def delete_empty_error_log
83
+ File.delete(error_log_name) if File.zero?(error_log_name)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,28 @@
1
+ require 'pluginscan/reports/vulnerability_report/advisories_api'
2
+ require 'pluginscan/reports/vulnerability_report/wp_vuln_db_api'
3
+ require 'pluginscan/reports/vulnerability_report/vulnerability_scanner'
4
+ require 'pluginscan/reports/vulnerability_report/vulnerabilities_printer'
5
+ require 'fileutils'
6
+
7
+ module Pluginscan
8
+ module Reports
9
+ module VulnerabilityReport
10
+ def self.print(plugin_directory, printer, error_printer)
11
+ plugin_slug = get_plugin_slug(plugin_directory)
12
+ advisories = VulnerabilityScanner.new.scan(plugin_slug)
13
+
14
+ printer.print(advisories, plugin_slug)
15
+ true
16
+ rescue WPVulnDB::APIError => e
17
+ error_printer.print(e.message)
18
+ false
19
+ end
20
+
21
+ def self.get_plugin_slug(plugin_directory)
22
+ # Expanding the path handles '.' and '..' etc.
23
+ full_path = File.expand_path(plugin_directory)
24
+ full_path.split('/').last
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ require 'httparty'
2
+
3
+ module Pluginscan
4
+ # Responsible for calling an api endpoint
5
+ # and re-raising ruby errors with more information
6
+ class AdvisoriesAPI
7
+ class Error < StandardError; end
8
+ class ConnectionError < Error; end
9
+
10
+ def initialize(api_name:, timeout:)
11
+ @api_name = api_name
12
+ @timeout = timeout
13
+ end
14
+
15
+ def get(uri)
16
+ HTTParty.get(uri, timeout: @timeout)
17
+ rescue SocketError
18
+ raise(ConnectionError, "Couldn't connect to #{@api_name} (SocketError)")
19
+ rescue Net::OpenTimeout
20
+ raise(ConnectionError, "Connection to #{@api_name} timed out after #{@timeout} seconds")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ module Pluginscan
2
+ class VulnerabilitiesPrinter < Printer
3
+ def print(advisories, plugin_slug)
4
+ raise ArgumentError, "Can't print a nil list of advisories" if advisories.nil?
5
+
6
+ print_headline(advisories, plugin_slug)
7
+
8
+ advisories.reverse.each do |advisory|
9
+ print_advisory(advisory)
10
+ end
11
+
12
+ print_blank_line
13
+ end
14
+
15
+ private def print_headline(advisories, plugin_slug)
16
+ if advisories.any?
17
+ @output.puts "#{advisories.count} advisories were found for '#{plugin_slug}':".color(:blue)
18
+ else
19
+ @output.puts "No advisories were found for '#{plugin_slug}'".color(:blue)
20
+ end
21
+ end
22
+
23
+ private def print_advisory(advisory)
24
+ printer = VulnerabilityPrinter.new(@output)
25
+ printer.print(advisory)
26
+ end
27
+ end
28
+
29
+ class VulnerabilityPrinter < Printer
30
+ def print(advisory)
31
+ title = highlight_version_number(advisory.title)
32
+ date = format_date(advisory.date)
33
+ fixed = fixed_data(advisory.fixed_in)
34
+ @output.puts " #{date} #{title} #{fixed}"
35
+ @output.puts " #{advisory.url}"
36
+ end
37
+
38
+ private def highlight_version_number(title)
39
+ version_number_regex = '([\<\>\=]+\ )?[\d\.]+'
40
+ title.gsub(
41
+ /(?<version>#{version_number_regex})/,
42
+ '\k<version>'.color(:yellow)
43
+ )
44
+ end
45
+
46
+ private def format_date(date)
47
+ date.strftime('%Y-%m-%d').color(:green)
48
+ end
49
+
50
+ private def fixed_data(fixed_version)
51
+ return "(no fixed version!)".color(:red) if fixed_version.nil?
52
+ "(fixed in #{fixed_version})".color(:red)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ module Pluginscan
2
+ # Responsible for calling out to an API to see if any advisories
3
+ # have been published about this plugin
4
+ class VulnerabilityScanner
5
+ class Error < StandardError; end
6
+
7
+ def initialize(advisories_api = WPVulnDB::API.new, response_handler = WPVulnDB::APIResponseHandler.new)
8
+ @advisories_api = advisories_api
9
+ @response_handler = response_handler
10
+ end
11
+
12
+ def scan(plugin_slug)
13
+ response = @advisories_api.get_plugin_advisories(plugin_slug)
14
+ @response_handler.call(response, plugin_slug)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,77 @@
1
+ module Pluginscan
2
+ # Responsible for calling the WPVulnDB API
3
+ module WPVulnDB
4
+ class APIError < StandardError; end
5
+ class AccessDeniedError < APIError; end
6
+ class UnexpectedJSONError < APIError; end
7
+
8
+ class API
9
+ def initialize
10
+ @api = AdvisoriesAPI.new(api_name: 'wpvulndb', timeout: 10)
11
+ end
12
+
13
+ def get_plugin_advisories(plugin_slug)
14
+ @api.get(uri(plugin_slug))
15
+ end
16
+
17
+ private
18
+
19
+ def uri(plugin_slug)
20
+ "#{base_uri}/plugins/#{plugin_slug}"
21
+ end
22
+
23
+ def base_uri
24
+ 'https://wpvulndb.com/api/v2'
25
+ end
26
+ end
27
+
28
+ class APIResponseHandler
29
+ def call(response, plugin_slug)
30
+ case response.code
31
+ when 200
32
+ DataMapper.new.call(response.parsed_response, plugin_slug)
33
+ when 404
34
+ []
35
+ when 403
36
+ raise(AccessDeniedError, "We got blocked by wpvulndb for suspicious activity :( Contact team@wpvulndb.com")
37
+ else
38
+ raise(APIError, "Something went wrong when calling wpvulndb - got a #{response.code} code: '#{response.body[0..50]}'")
39
+ end
40
+ end
41
+ end
42
+
43
+ class DataMapper
44
+ def call(response_data, plugin_slug)
45
+ plugin_data = response_data.fetch(plugin_slug) do
46
+ raise(UnexpectedJSONError, "Couldn't find data for '#{plugin_slug}' in api response")
47
+ end
48
+
49
+ vulns = plugin_data.fetch('vulnerabilities') do
50
+ raise(UnexpectedJSONError, "Couldn't find a list of vulnerabilities")
51
+ end
52
+
53
+ vulns.map{ |v| Advisory.new(v) }
54
+ end
55
+ end
56
+
57
+ class Advisory
58
+ attr_reader :title
59
+ attr_reader :fixed_in
60
+
61
+ def initialize(data)
62
+ @id = data.fetch('id')
63
+ @title = data.fetch('title')
64
+ @created_at = data.fetch('created_at')
65
+ @fixed_in = data.fetch('fixed_in')
66
+ end
67
+
68
+ def date
69
+ Date.parse(@created_at)
70
+ end
71
+
72
+ def url
73
+ "https://wpvulndb.com/vulnerabilities/#{@id}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Pluginscan
2
+ VERSION = "0.9.0".freeze
3
+ end
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'pluginscan/version'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "pluginscan"
6
+ spec.version = Pluginscan::VERSION
7
+ spec.authors = ["dxw"]
8
+ spec.email = ["security@dxw.com"]
9
+ spec.homepage = "https://twinkie.dxw.net/dxw/pluginscan"
10
+ spec.description = %q(Scans WordPress plugins for potential issues and vulnerabilities)
11
+ spec.summary = %q(Does stuff)
12
+
13
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
14
+ spec.executables = ['pluginscan']
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ["lib"]
17
+
18
+ spec.add_dependency "rainbow", "~> 2.0"
19
+ spec.add_dependency "httparty", "< 1"
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rspec", "~> 3.4.0", '>= 3.4.0'
23
+ spec.add_development_dependency "webmock", "< 3"
24
+ spec.add_development_dependency "vcr", "< 4"
25
+ spec.add_development_dependency "simplecov", "< 1"
26
+ spec.add_development_dependency "rubocop", "< 1"
27
+ spec.add_development_dependency "fuubar", "~> 2"
28
+ spec.add_development_dependency "pry", "~> 0"
29
+ spec.add_development_dependency "rake", ">= 10.0.0"
30
+ spec.add_development_dependency "geminabox-release", "~> 0.2", ">= 0.2.0"
31
+ end
@@ -0,0 +1,54 @@
1
+ require 'acceptance_spec_helper'
2
+ require 'support/heredoc_helper'
3
+
4
+ RSpec.describe Pluginscan::Scanner do
5
+ before do
6
+ # these are slow, so don't run them if we don't have to
7
+ stub_cloc
8
+ stub_sloccount
9
+ stub_vuln_check
10
+ end
11
+
12
+ describe '.scan' do
13
+ let(:output) { StringIO.new }
14
+
15
+ describe "CLOC Report", type: [:file, :process] do
16
+ subject(:scanner) { Pluginscan::Scanner.new(sloccount: false, output: output) }
17
+ before(:all) { setup_tempdir 'tmp' }
18
+
19
+ it "reports on sloc by language" do
20
+ LanguageCount = Struct.new :language, :sloc, :file_count
21
+
22
+ cloc_output = <<-EOS.heredoc_unindent
23
+ files,language,blank,comment,code,"github.com/AlDanial/cloc v 1.70 T=0.29 s (284.9 files/s, 65539.8 lines/s)"
24
+ 20,PHP,4584,0,6628
25
+ EOS
26
+
27
+ stub_cloc(result: cloc_output)
28
+
29
+ php = coloured_cyan('PHP')
30
+ count = coloured_red('6628')
31
+ scanner.scan 'tmp'
32
+ expect(output.string).to match(/#{php}\s*#{count} lines across 20 files/)
33
+ end
34
+
35
+ it "displays a message when there was no code" do
36
+ stub_cloc(result: "")
37
+ scanner.scan 'tmp'
38
+ expect(output.string).to match(/CLOC didn't find any code/)
39
+ end
40
+
41
+ it "displays a message when cloc was unavailable (doesn't error out)" do
42
+ stub_cloc(which_result: which_failure)
43
+ scanner.scan 'tmp'
44
+ expect(output.string).to match(/The 'cloc' command is unavailable/)
45
+ end
46
+
47
+ it "displays a message when cloc threw an error (doesn't error out)" do
48
+ stub_cloc(result: "Some nonsense", process_status: failed_process_status)
49
+ scanner.scan 'tmp'
50
+ expect(output.string).to match(/Some nonsense/)
51
+ end
52
+ end
53
+ end
54
+ end