lib/brakeman/warning.rb
require 'json'
require 'digest/sha2'
require 'brakeman/warning_codes'
require 'brakeman/messages'
#The Warning class stores information about warnings
class Brakeman::Warning
attr_reader :called_from, :check, :class, :confidence, :controller, :cwe_id,
:line, :method, :model, :template, :user_input, :user_input_type,
:warning_code, :warning_set, :warning_type
attr_accessor :code, :context, :file, :message
TEXT_CONFIDENCE = {
0 => "High",
1 => "Medium",
2 => "Weak",
}
CONFIDENCE = {
:high => 0,
:med => 1,
:medium => 1,
:low => 2,
:weak => 2,
}
OPTIONS = {
:called_from => :@called_from,
:check => :@check,
:class => :@class,
:code => :@code,
:controller => :@controller,
:cwe_id => :@cwe_id,
:file => :@file,
:gem_info => :@gem_info,
:line => :@line,
:link => :@link,
:link_path => :@link_path,
:message => :@message,
:method => :@method,
:model => :@model,
:template => :@template,
:user_input => :@user_input,
:warning_set => :@warning_set,
:warning_type => :@warning_type,
}
#+options[:result]+ can be a result from Tracker#find_call. Otherwise, it can be +nil+.
def initialize options = {}
@view_name = nil
OPTIONS.each do |key, var|
self.instance_variable_set(var, options[key])
end
self.confidence = options[:confidence]
result = options[:result]
if result
@code ||= result[:call]
@file ||= result[:location][:file]
if result[:location][:type] == :template #template result
@template ||= result[:location][:template]
else
@class ||= result[:location][:class]
@method ||= result[:location][:method]
end
end
if @method.to_s =~ /^fake_filter\d+/
@method = :before_filter
end
if @user_input.is_a? Brakeman::BaseCheck::Match
@user_input_type = @user_input.type
@user_input = @user_input.match
elsif @user_input == false
@user_input = nil
end
if not @line
if @user_input and @user_input.respond_to? :line
@line = @user_input.line
elsif @code and @code.respond_to? :line
@line = @code.line
end
end
if @gem_info
if @gem_info.is_a? Hash
@line ||= @gem_info[:line]
@file ||= @gem_info[:file]
else
# Fallback behavior returns just a string for the file name
@file ||= @gem_info
end
end
unless @warning_set
if self.model
@warning_set = :model
@file ||= self.model.file
elsif self.template
@warning_set = :template
@called_from = self.template.render_path
@file ||= self.template.file
elsif self.controller
@warning_set = :controller
else
@warning_set = :warning
end
end
if options[:warning_code]
@warning_code = Brakeman::WarningCodes.code options[:warning_code]
else
@warning_code = nil
end
Brakeman.debug("Warning created without warning code: #{options[:warning_code]}") unless @warning_code
if options[:message].is_a? String
@message = Brakeman::Messages::Message.new(options[:message])
end
@format_message = nil
@row = nil
end
def hash
self.to_s.hash
end
def eql? other_warning
self.hash == other_warning.hash
end
def confidence= conf
@confidence = case conf
when Integer
conf
when Symbol
CONFIDENCE[conf]
else
raise "Could not set confidence to `#{conf}`"
end
raise "Could not set confidence to `#{conf}`" unless @confidence
raise "Invalid confidence: `#{@confidence}`" unless TEXT_CONFIDENCE[@confidence]
end
#Returns name of a view, including where it was rendered from
def view_name(include_renderer = true)
if called_from and include_renderer
@view_name = "#{template.name} (#{called_from.last})"
else
@view_name = template.name
end
end
#Return String of the code output from the OutputProcessor and
#stripped of newlines and tabs.
def format_code strip = true
format_ruby self.code, strip
end
#Return String of the user input formatted and
#stripped of newlines and tabs.
def format_user_input strip = true
format_ruby self.user_input, strip
end
def format_with_user_input strip = true, &block
if self.user_input
formatted = Brakeman::OutputProcessor.new.format(code, self.user_input, &block)
formatted.gsub!(/(\t|\r|\n)+/, " ") if strip
formatted
else
format_code
end
end
#Return formatted warning message
def format_message
return @format_message if @format_message
@format_message = self.message.to_s.dup
if self.line
@format_message << " near line #{self.line}"
end
if self.code
@format_message << ": #{format_code}"
end
@format_message
end
def link
return @link if @link
if @link_path
if @link_path.start_with? "http"
@link = @link_path
else
@link = "https://brakemanscanner.org/docs/warning_types/#{@link_path}"
end
else
warning_path = self.warning_type.to_s.downcase.gsub(/\s+/, '_') + "/"
@link = "https://brakemanscanner.org/docs/warning_types/#{warning_path}"
end
@link
end
#Generates a hash suitable for inserting into a table
def to_row type = :warning
@row = { "Confidence" => TEXT_CONFIDENCE[self.confidence],
"Warning Type" => self.warning_type.to_s,
"CWE ID" => self.cwe_id,
"Message" => self.message }
case type
when :template
@row["Template"] = self.view_name.to_s
when :model
@row["Model"] = self.model.name.to_s
when :controller
@row["Controller"] = self.controller.to_s
when :warning
@row["Class"] = self.class.to_s
@row["Method"] = self.method.to_s
end
@row
end
def to_s
output = "(#{TEXT_CONFIDENCE[self.confidence]}) #{self.warning_type} - #{self.message}"
output << " near line #{self.line}" if self.line
output << " in #{self.file.relative}" if self.file
output << ": #{self.format_code}" if self.code
output
end
def fingerprint
loc = self.location
location_string = loc && loc.sort_by { |k, v| k.to_s }.inspect
warning_code_string = sprintf("%03d", @warning_code)
code_string = @code.inspect
Digest::SHA2.new(256).update("#{warning_code_string}#{code_string}#{location_string}#{self.file.relative}#{self.confidence}").to_s
end
def location include_renderer = true
case @warning_set
when :template
{ :type => :template, :template => self.view_name(include_renderer) }
when :model
{ :type => :model, :model => self.model.name }
when :controller
{ :type => :controller, :controller => self.controller }
when :warning
if self.class
{ :type => :method, :class => self.class, :method => self.method }
else
nil
end
end
end
def relative_path
self.file.relative
end
def check_name
@check_name ||= self.check.sub(/^Brakeman::Check/, '')
end
def confidence_name
TEXT_CONFIDENCE[self.confidence]
end
def to_hash absolute_paths: true
if self.called_from and not absolute_paths
render_path = self.called_from.with_relative_paths
else
render_path = self.called_from
end
{ :warning_type => self.warning_type,
:warning_code => @warning_code,
:fingerprint => self.fingerprint,
:check_name => self.check_name,
:message => self.message.to_s,
:file => (absolute_paths ? self.file.absolute : self.file.relative),
:line => self.line,
:link => self.link,
:code => (@code && self.format_code(false)),
:render_path => render_path,
:location => self.location(false),
:user_input => (@user_input && self.format_user_input(false)),
:confidence => self.confidence_name,
:cwe_id => cwe_id
}
end
def to_json
JSON.generate self.to_hash
end
private
def format_ruby code, strip
formatted = Brakeman::OutputProcessor.new.format(code)
formatted.gsub!(/(\t|\r|\n)+/, " ") if strip
formatted
end
end