require 'fileutils'
module Dawn
class Reporter
def initialize(options={})
@engine = nil
@ret = false
@filename = options[:filename]
@ret = options[:apply_all_code] unless options[:apply_all_code].nil?
@format = options[:format] unless options[:format].nil?
@engine = options[:engine] unless options[:engine].nil?
@format = :tabular unless is_valid_format?(@format)
end
def report
ascii_tabular_report if @format == :tabular
json_report if @format == :json
ascii_plain_report if @format == :console
html_report if @format == :html
end
private
def write(output)
puts output if @filename.nil?
unless @filename.nil?
# $logger.warn "I will use codesake.css, bootstrap.min.css and bootstrap.js stored in ./support/ directory" if @format == :html
File.open(@filename, "w") do |f|
f.puts output
end
$logger.info "#{@filename} created (#{output.length} bytes)"
end
end
def write_html(path, content)
css_path = File.join(path, 'css')
js_path = File.join(path, 'js')
support_path = File.join(File.dirname(__FILE__), '..', '..', 'support')
FileUtils.mkdir_p(File.join(path, 'css'))
FileUtils.mkdir_p(File.join(path, 'js'))
FileUtils.cp(File.join(support_path, 'bootstrap.js'), js_path)
FileUtils.cp(File.join(support_path, 'bootstrap.min.css'), css_path)
FileUtils.cp(File.join(support_path, 'codesake.css'), css_path)
File.open(File.join(path, 'report.html'), "w") do |f|
f.puts content
end
$logger.info "#{File.join(path, 'report.html')} created (#{File.stat(File.join(path, 'report.html')).size} bytes)"
end
def write_table(path, content)
File.open(path, "w") do |f|
f.puts content
end
$logger.info "#{path} created (#{File.stat(path).size} bytes)"
end
def is_valid_format?(format)
return false if format.nil?
return true if (format == :console) || (format == :tabular) || (format == :json) || (format == :html) || (format == :csv)
return false # otherwise
end
def html_report
output = @engine.create_output_dir if @filename.nil?
html_head = "\n\n
\nDawnscanner report for #{File.basename(@engine.target)}"
html_head +=" "
html_head += ""
html_head += ""
html_head += ""
html_head += ""
html_head += "\n"
html_body = "\n"
html_body += ""
html_body += "\n"
html_body += "
\n"
html_body += "
\n"
html_body += "
Security code review results for \"#{File.basename(@engine.target)}\"
\n"
html_body += "
\n"
html_body += "
Scan detail
\n"
html_body += "
\n"
html_body += "
The scan was last executed #{@engine.scan_start.strftime("%d %b %Y - %T")} and Dawn founds #{@engine.count_vulnerabilities} vulnerabilities
\n"
html_body += "
\n"
html_body += "
\n"
html_body += "
Scan details
\n"
html_body += "
\n"
html_body += "Key | Value |
\n"
html_body += "\n"
html_body += "Dawn version | #{Dawn::VERSION} |
" unless Dawn::RELEASE == "(development)\n"
html_body += "Dawn development version | #{Dawn::VERSION} |
" if Dawn::RELEASE == "(development)\n"
html_body += "Scan duration | #{@engine.scan_time.round(3)} sec |
\n"
html_body += "Target | #{@engine.target} |
\n"
html_body += "MVC detected framework | #{@engine.name} v#{@engine.get_mvc_version} |
" unless @engine.name == "Gemfile.lock\n"
html_body += "MVC detected framework | #{@engine.force} v#{@engine.get_mvc_version} |
" if @engine.name == "Gemfile.lock\n"
if @ret
html_body += "Applied checks | #{@engine.applied_checks} security checks |
\n"
html_body += "Skipped checks | #{@engine.skipped_checks} security checks |
\n"
else
html_body += "Applied checks | No security checks in the knowledge base |
\n"
end
html_body+="Vulnerabilities found | #{@engine.count_vulnerabilities} |
\n"
html_body+="Mitigated issues found | #{@engine.mitigated_issues.count} |
\n"
html_body+="Reflected XSS | #{@engine.reflected_xss.count} |
\n"
html_body += "\n"
html_body += "
\n"
if @engine.count_vulnerabilities > 0
html_body += "
\n"
html_body += "
Vulnerabilities found
\n"
html_body += "
\n"
html_body += "Name | Severity | CVSS score | Description | Remediation |
\n"
html_body += ""
@engine.vulnerabilities.each do |vuln|
html_body += "#{vuln[:name]} | #{vuln[:severity]} | #{vuln[:cvss_score]} | #{vuln[:message]} | #{vuln[:remediation]} |
\n"
end
html_body += "\n"
html_body += "
\n"
end
html_body += "
\n"
html_body += "\n"
html_body += "
\n"
html_body += "
\n"
html_body += "\n"
html_body += ""
html = html_head + html_body
unless @filename.nil?
write(html)
else
write_html(output, html)
end
true
end
def ascii_tabular_report
output = @engine.create_output_dir
# 0_First table: executive summary
rows = []
rows << ['Dawn version', Dawn::VERSION] unless Dawn::RELEASE == "(development)"
rows << ['Dawn development version', Dawn::VERSION] if Dawn::RELEASE == "(development)"
rows << ['Scan started', @engine.scan_start]
rows << ['Scan duration', "#{@engine.scan_time.round(3)} sec"]
rows << ['Target', @engine.target]
rows << ['Framework', "#{@engine.name} v#{@engine.get_mvc_version}" ] unless @engine.name == "Gemfile.lock"
rows << ['Framework', "#{@engine.force} v#{@engine.get_mvc_version}" ] if @engine.name == "Gemfile.lock"
if @ret
rows << ['Applied checks', "#{@engine.applied_checks} security checks"]
rows << ['Skipped checks', "#{@engine.skipped_checks} security checks"]
else
rows << ['Applied checks', "No security checks in the knowledge base"]
end
rows << ['Vulnerabilities found in dependencies', @engine.count_vulnerabilities]
rows << ['Vulnerabilities mitigated by external factors', @engine.mitigated_issues.count] unless @engine.mitigated_issues.count == 0
rows << ['Reflected XSS found', @engine.reflected_xss.count]
table = Terminal::Table.new :title=>'Scan summary', :rows => rows
write_table(File.join(output, 'summary.txt'), table)
# 0_a) Application structure
rows = []
rows << ['Lines of code', 'to be added soon']
rows << ['Cyclomatic complexity index', 'to be added soon']
rows << ['Models', @engine.models.count]
rows << ['Views', @engine.views.count]
rows << ['Controllers', @engine.controllers.count]
table = Terminal::Table.new :title=>'Application stats', :rows => rows
write_table(File.join(output, 'statistics.txt'), table)
if @engine.count_vulnerabilities > 0
# 1) Vulnerabilities
# 1_a) Third party gem vulnerabilities
rows = []
@engine.vulnerabilities.each do |vuln|
$logger.error(vuln)
rows << [vuln[:name]&.justify(10), vuln[:severity], vuln[:message]&.justify(30), vuln[:remediation]&.justify(15), vuln[:evidences].join&.justify(15)]
rows << :separator
end
table = Terminal::Table.new :title=>"Vulnerabilities", :headings=>['Issue', 'Severity', 'Description', 'Solution', 'Evidences'], :rows=>rows
write_table(File.join(output, 'third_party_vulnerabilities.txt'), table)
# 1_b) Refleted XXS
rows = []
if @engine.has_reflected_xss?
@engine.reflected_xss.each do |vuln|
rows << [vuln[:sink_source], vuln[:sink_view], "#{vuln[:sink_file]}@#{vuln[:sink_line]}",vuln[:sink_evidence]]
rows << :separator
end
table = Terminal::Table.new :title=>"Reflected Cross Site Scripting", :headings=>['Sink name', 'View', 'Location the sink was read', 'Evidences'], :rows=>rows
write_table(File.join(output, 'reflected_xss.txt'), table)
end
end
if @engine.mitigated_issues.count > 0
# 2_Mitigated issues
rows = []
@engine.mitigated_issues.each do |vuln|
rows << [vuln[:name].justify(10), vuln[:message].justify(50), vuln[:evidences].join.justify(15)]
rows << :separator
end
table = Terminal::Table.new :title=>"Mitigated issues", :headings=>['Issue', 'Description', 'Evidences'], :rows=>rows
write_table(File.join(output, 'mitigated_issues.txt'), table)
end
true
end
def json_report
result = {}
return {:status=>"KO", :message=>"BUG at #{__FILE__}@#{__LINE__}: target is empty or engine is nil."}.to_json if @engine.target.empty? or @engine.nil?
return {:status=>"KO", :message=>"#{target} doesn't exist"}.to_json if ! Dir.exist?(@engine.target)
return {:status=>"KO", :message=>"no security checks applied"}.to_json unless @ret
result[:status]="OK"
result[:dawn_version] = Dawn::VERSION
result[:dawn_status] = "Develoment version" if Dawn::RELEASE == "(development)"
result[:scan_started] = @engine.scan_start
result[:scan_duration] = "#{@engine.scan_time.round(3)} sec"
result[:target]=@engine.target
result[:mvc]=@engine.name unless @engine.name == "Gemfile.lock"
result[:mvc]=@engine.force if @engine.name == "Gemfile.lock"
result[:mvc_version]=@engine.get_mvc_version
result[:applied_checks_count] = @engine.applied_checks
result[:skipped_checks_count] = @engine.skipped_checks
result[:vulnerabilities_count]=@engine.count_vulnerabilities
result[:mitigated_issues_count] = @engine.mitigated_issues.count
result[:reflected_xss_count] = @engine.reflected_xss.count
result[:vulnerabilities]=[]
@engine.vulnerabilities.each do |v|
result[:vulnerabilities] << {
:name => v[:name],
:cve_link => v[:cve_link],
:severity => v[:severity],
:cvss_score => v[:cvss_score],
:message => v[:message],
:remediation => v[:remediation]
}
end
result[:mitigated_vuln] = @engine.mitigated_issues
result[:reflected_xss] = []
@engine.reflected_xss.each do |r|
result[:reflected_xss] << "request parameter \"#{r[:sink_source]}\""
end
write(result.to_json)
true
end
def ascii_plain_report
result = sprintf "%s\n", "scanning #{@engine.target}"
result += sprintf "%s\n", "#{@engine.name} v#{@engine.get_mvc_version} detected" unless @engine.name == "Gemfile.lock"
result += sprintf "%s\n", "#{@engine.force} v#{@engine.get_mvc_version} detected" if @engine.name == "Gemfile.lock"
result += sprintf "%s\n", "applying all security checks"
if @ret
result += sprintf "%s\n", "#{@engine.applied_checks} security checks applied - #{@engine.skipped_checks} security checks skipped"
else
result += sprintf "%s\n", "no security checks in the knowledge base"
end
if @engine.count_vulnerabilities != 0
result += sprintf "%s\n", "#{@engine.count_vulnerabilities} vulnerabilities found"
@engine.vulnerabilities.each do |vuln|
result += sprintf "%s\n", "#{vuln[:name]} check failed\n"
result += sprintf "%s\n", "#{vuln[:message].justify(70)}"
result += sprintf "%s\n", "Evidence:"
vuln[:evidences].each do |evidence|
result += sprintf "%s\n", "\t#{evidence}"
end
result += sprintf "\n\n"
end
if @engine.has_reflected_xss?
result += sprintf "%s\n", "#{@engine.reflected_xss.count} reflected XSS found"
@engine.reflected_xss.each do |vuln|
result += sprintf "%s\n", "request parameter \"#{vuln[:sink_source]}\" is used without escaping in #{vuln[:sink_view]}. It was read here: #{vuln[:sink_file]}@#{vuln[:sink_line]}"
result += sprintf "%s\n", "evidence: #{vuln[:sink_evidence]}"
end
end
else
result += sprintf "%s\n", "no vulnerabilities found."
end
if @engine.mitigated_issues.count != 0
result += sprintf "%s\n", "#{@engine.mitigated_issues.count} mitigated vulnerabilities found"
@engine.mitigated_issues.each do |vuln|
result += sprintf "%s\n", "#{vuln[:name]} mitigated"
vuln[:evidences].each do |evidence|
result += sprintf "%s\n", evidence
end
end
end
write(result)
true
end
end
end