lib/bundler/audit/cli/formats/junit.rb
#
# Copyright (c) 2013-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# bundler-audit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# bundler-audit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with bundler-audit. If not, see <https://www.gnu.org/licenses/>.
#
require 'thor'
require 'cgi'
module Bundler
module Audit
class CLI < ::Thor
module Formats
module Junit
#
# Prints any findings as an XML junit report.
#
# @param [Report] report
# The results from the {Scanner}.
#
# @param [IO, File] output
# Optional output stream.
#
def print_report(report, output=$stdout)
original_stdout = $stdout
$stdout = output
print_xml_testsuite(report) do
report.each do |result|
print_xml_testcase(result)
end
end
$stdout = original_stdout
end
private
def say_xml(*lines)
say(lines.join($/))
end
def print_xml_testsuite(report)
say_xml(
%{<?xml version="1.0" encoding="UTF-8" ?>},
%{<testsuites id="#{Time.now.to_i}" name="Bundle Audit">},
%{ <testsuite id="Gemfile" name="Ruby Gemfile" failures="#{report.count}">}
)
yield
say_xml(
%{ </testsuite>},
%{</testsuites>}
)
end
def xml(string)
CGI.escapeHTML(string.to_s)
end
def print_xml_testcase(result)
case result
when Results::InsecureSource
say_xml(
%{ <testcase id="#{xml(result.source)}" name="Insecure Source URI found: #{xml(result.source)}">},
%{ <failure message="Insecure Source URI found: #{xml(result.source)}" type="Unknown"></failure>},
%{ </testcase>}
)
when Results::UnpatchedGem
say_xml(
%{ <testcase id="#{xml(result.gem.name)}" name="#{xml(bundle_title(result))}">},
%{ <failure message="#{xml(result.advisory.title)}" type="#{xml(result.advisory.criticality)}">},
%{ Name: #{xml(result.gem.name)}},
%{ Version: #{xml(result.gem.version)}},
%{ Advisory: #{xml(advisory_ref(result.advisory))}},
%{ Criticality: #{xml(advisory_criticality(result.advisory))}},
%{ URL: #{xml(result.advisory.url)}},
%{ Title: #{xml(result.advisory.title)}},
%{ Solution: #{xml(advisory_solution(result.advisory))}},
%{ </failure>},
%{ </testcase>}
)
end
end
def bundle_title(result)
"#{advisory_criticality(result.advisory).upcase} #{result.gem.name}(#{result.gem.version}) #{result.advisory.title}"
end
def advisory_solution(advisory)
unless advisory.patched_versions.empty?
"update to #{advisory.patched_versions.map { |v| "'#{v}'" }.join(', ')}"
else
"remove or disable this gem until a patch is available!"
end
end
def advisory_criticality(advisory)
if advisory.criticality
advisory.criticality.to_s.capitalize
else
"Unknown"
end
end
def advisory_ref(advisory)
advisory.identifiers.join(" ")
end
Formats.register :junit, Junit
end
end
end
end
end