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 += "\n" html_body += "

Dawnscanner

\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 += "\n" html_body += "\n" html_body += "" unless Dawn::RELEASE == "(development)\n" html_body += "" if Dawn::RELEASE == "(development)\n" html_body += "\n" html_body += "\n" html_body += "" unless @engine.name == "Gemfile.lock\n" html_body += "" if @engine.name == "Gemfile.lock\n" if @ret html_body += "\n" html_body += "\n" else html_body += "\n" end html_body+="\n" html_body+="\n" html_body+="\n" html_body += "\n" html_body += "
KeyValue
Dawn version#{Dawn::VERSION}
Dawn development version#{Dawn::VERSION}
Scan duration#{@engine.scan_time.round(3)} sec
Target#{@engine.target}
MVC detected framework#{@engine.name} v#{@engine.get_mvc_version}
MVC detected framework#{@engine.force} v#{@engine.get_mvc_version}
Applied checks#{@engine.applied_checks} security checks
Skipped checks#{@engine.skipped_checks} security checks
Applied checksNo security checks in the knowledge base
Vulnerabilities found#{@engine.count_vulnerabilities}
Mitigated issues found#{@engine.mitigated_issues.count}
Reflected XSS#{@engine.reflected_xss.count}
\n" if @engine.count_vulnerabilities > 0 html_body += "
\n" html_body += "

Vulnerabilities found

\n" html_body += "\n" html_body += "\n" html_body += "" @engine.vulnerabilities.each do |vuln| html_body += "\n" end html_body += "\n" html_body += "
NameSeverityCVSS scoreDescriptionRemediation
#{vuln[:name]}#{vuln[:severity]}#{vuln[:cvss_score]}#{vuln[:message]}#{vuln[:remediation]}
\n" end html_body += "
\n" html_body += "
\n" html_body += "
\n" html_body += "

Proudly generated with Dawnscanner — #{Time.now.strftime("%Y")} — engine v#{Dawn::VERSION} (#{Dawn::RELEASE})

\n" 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