Skip to content

Commit

Permalink
Add JUnit-XML format report (#1453)
Browse files Browse the repository at this point in the history
Add JUnit-XML format report
  • Loading branch information
naokikimura committed Feb 14, 2020
1 parent 2b0a101 commit a097acd
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 2 deletions.
4 changes: 4 additions & 0 deletions lib/brakeman.rb
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ def self.get_formats_from_output_format output_format
[:to_text]
when :table, :to_table
[:to_table]
when :junit, :to_junit
[:to_junit]
else
[:to_text]
end
Expand Down Expand Up @@ -258,6 +260,8 @@ def self.get_formats_from_output_files output_files
:to_text
when /\.table$/i
:to_table
when /\.junit$/i
:to_junit
else
:to_text
end
Expand Down
2 changes: 1 addition & 1 deletion lib/brakeman/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def create_option_parser options

opts.on "-f",
"--format TYPE",
[:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table],
[:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit],
"Specify output formats. Default is text" do |type|

type = "s" if type == :text
Expand Down
5 changes: 4 additions & 1 deletion lib/brakeman/report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class Brakeman::Report
attr_reader :tracker

VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown, :to_codeclimate, :to_plain, :to_text]
VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown, :to_codeclimate, :to_plain, :to_text, :to_junit]

def initialize tracker
@app_tree = tracker.app_tree
Expand Down Expand Up @@ -40,6 +40,9 @@ def format format
return self.to_table
when :to_pdf
raise "PDF output is not yet supported."
when :to_junit
require_report 'junit'
Brakeman::Report::JUnit
else
raise "Invalid format: #{format}. Should be one of #{VALID_FORMATS.inspect}"
end
Expand Down
104 changes: 104 additions & 0 deletions lib/brakeman/report/report_junit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
require 'time'
require "stringio"
require 'rexml/document'

class Brakeman::Report::JUnit < Brakeman::Report::Base
def generate_report
io = StringIO.new
doc = REXML::Document.new
doc.add REXML::XMLDecl.new '1.0', 'UTF-8'

test_suites = REXML::Element.new 'testsuites'
test_suites.add_attribute 'xmlns:brakeman', 'https://brakemanscanner.org/'
properties = test_suites.add_element 'brakeman:properties', { 'xml:id' => 'scan_info' }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'app_path', 'brakeman:value' => tracker.app_path }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'rails_version', 'brakeman:value' => rails_version }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'security_warnings', 'brakeman:value' => all_warnings.length }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'start_time', 'brakeman:value' => tracker.start_time.iso8601 }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'end_time', 'brakeman:value' => tracker.end_time.iso8601 }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'duration', 'brakeman:value' => tracker.duration }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'checks_performed', 'brakeman:value' => checks.checks_run.join(',') }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'number_of_controllers', 'brakeman:value' => tracker.controllers.length }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'number_of_models', 'brakeman:value' => tracker.models.length - 1 }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'ruby_version', 'brakeman:value' => number_of_templates(@tracker) }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'number_of_templates', 'brakeman:value' => RUBY_VERSION }
properties.add_element 'brakeman:property', { 'brakeman:name' => 'brakeman_version', 'brakeman:value' => Brakeman::Version }

errors = test_suites.add_element 'brakeman:errors'
tracker.errors.each { |e|
error = errors.add_element 'brakeman:error'
error.add_attribute 'brakeman:message', e[:error]
e[:backtrace].each { |b|
backtrace = error.add_element 'brakeman:backtrace'
backtrace.add_text b
}
}

obsolete = test_suites.add_element 'brakeman:obsolete'
tracker.unused_fingerprints.each { |fingerprint|
obsolete.add_element 'brakeman:warning', { 'brakeman:fingerprint' => fingerprint }
}

ignored = test_suites.add_element 'brakeman:ignored'
ignored_warnings.each { |w|
warning = ignored.add_element 'brakeman:warning'
warning.add_attribute 'brakeman:message', w.message
warning.add_attribute 'brakeman:category', w.warning_type
warning.add_attribute 'brakeman:file', warning_file(w)
warning.add_attribute 'brakeman:line', w.line
warning.add_attribute 'brakeman:fingerprint', w.fingerprint
warning.add_attribute 'brakeman:confidence', TEXT_CONFIDENCE[w.confidence]
warning.add_attribute 'brakeman:code', w.format_code
warning.add_text w.to_s
}

hostname = `hostname`.strip
i = 0
all_warnings
.map { |warning| [warning.file, [warning]] }
.reduce({}) { |entries, entry|
key, value = entry
entries[key] = entries[key] ? entries[key].concat(value) : value
entries
}
.each { |file, warnings|
i += 1
test_suite = test_suites.add_element 'testsuite'
test_suite.add_attribute 'id', i
test_suite.add_attribute 'package', 'brakeman'
test_suite.add_attribute 'name', file.relative
test_suite.add_attribute 'timestamp', tracker.start_time.strftime('%FT%T')
test_suite.add_attribute 'hostname', hostname == '' ? 'localhost' : hostname
test_suite.add_attribute 'tests', checks.checks_run.length
test_suite.add_attribute 'failures', warnings.length
test_suite.add_attribute 'errors', '0'
test_suite.add_attribute 'time', '0'

test_suite.add_element 'properties'

warnings.each { |warning|
test_case = test_suite.add_element 'testcase'
test_case.add_attribute 'name', 'run_check'
test_case.add_attribute 'classname', warning.check
test_case.add_attribute 'time', '0'

failure = test_case.add_element 'failure'
failure.add_attribute 'message', warning.message
failure.add_attribute 'type', warning.warning_type
failure.add_attribute 'brakeman:fingerprint', warning.fingerprint
failure.add_attribute 'brakeman:file', warning_file(warning)
failure.add_attribute 'brakeman:line', warning.line
failure.add_attribute 'brakeman:confidence', TEXT_CONFIDENCE[warning.confidence]
failure.add_attribute 'brakeman:code', warning.format_code
failure.add_text warning.to_s
}

test_suite.add_element 'system-out'
test_suite.add_element 'system-err'
}

doc.add test_suites
doc.write io
io.string
end
end
38 changes: 38 additions & 0 deletions test/tests/junit_output.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require_relative '../test'
require 'rexml/document'

class JUnitOutputTests < Minitest::Test
def setup
@@document ||= REXML::Document.new(Brakeman.run("#{TEST_PATH}/apps/rails3.2").report.to_junit)
end

def test_for_ignored_warnings
assert @@document.get_elements('/testsuites/brakeman:ignored/brakeman:warning').length == 0

document = REXML::Document.new(Brakeman.run("#{TEST_PATH}/apps/rails4").report.to_junit)
ignored_warnings = document.get_elements('/testsuites/brakeman:ignored/brakeman:warning')
assert_equal 1, ignored_warnings.length
end

def test_for_errors
assert @@document.get_elements('/testsuites/brakeman:errors/brakeman:error').length == 0

tracker = Brakeman.run("#{TEST_PATH}/apps/rails3.2")
tracker.error Exception.new "some message"
document = REXML::Document.new(tracker.report.to_junit)
elements = document.get_elements('/testsuites/brakeman:errors/brakeman:error')
assert_equal elements.map { |e| e.attribute('brakeman:message').to_s }, ["some message"]
end

def test_for_obsolete
document = REXML::Document.new(Brakeman.run("#{TEST_PATH}/apps/rails4").report.to_junit)
obsolete = document.get_elements('/testsuites/brakeman:obsolete/brakeman:warning')
.map { |e| e.attribute('brakeman:fingerprint').to_s }
assert_equal ["abcdef01234567890ba28050e7faf1d54f218dfa9435c3f65f47cb378c18cf98"], obsolete
end

def test_paths
elements = @@document.get_elements('/testsuites/testsuite/testcase/failure')
assert elements.all? { |e| not e.attribute('brakeman:file').to_s.start_with? "/" }
end
end
7 changes: 7 additions & 0 deletions test/tests/report_generation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,11 @@ def test_github_markdown_sanity
ensure
@@tracker.options[:github_url] = nil
end

def test_junit_sanity
report = @@report.to_junit

assert report.is_a? String
assert report.match(/\A.*<\/testsuites>\z/m)
end
end

0 comments on commit a097acd

Please sign in to comment.