Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Continuous Security with Practical Static Analysis

Continuous Security with Practical Static Analysis

Static code analysis tools, which attempt determine what code does without actually running the code, provide an excellent opportunity to perform lightweight security checks as part of the software development lifecycle. Unfortunately, building generic static analysis tools, especially for security, is a costly, time-consuming effort. As a result very few tools exist and commercial tools are very expensive – if they even support your programming language. The good news is building targeted static analysis tools for your own environment with rules specific to your needs is much easier! This talk will go through straight-forward options for static analysis, from grep to writing rules for existing tools through writing your very own static analysis tool. Since static analysis tools can be run at any point in the software development lifecycle, even simple tools enable powerful security assurance when added to continuous integration.

Justin Collins

January 07, 2016
Tweet

More Decks by Justin Collins

Other Decks in Programming

Transcript

  1. @presidentbeef 1 - Identify a Problem Repeated incidents with the

    same root cause Opt-in security, e.g. in APIs Unsafe calls/settings no one should use, ever 13
  2. @presidentbeef 2 - Identify a Solution Write a safer library

    Make security opt-out Detect unsafe calls/settings/API usage 14
  3. @presidentbeef 4 - Automate Enforcement Part of continuous integration Part

    of code review Standalone, continuously-running process Local scripts/hooks 16
  4. @presidentbeef Bash git checkout $@ --quiet files=$(git diff --name-status master

    HEAD | grep -E "^(A|M)" | cut -f 2) grep "get_survey([^,)]\+)" $files 31
  5. @presidentbeef Changed Files Rules file1 - warning X file2 -

    warning Y file3 - warning X ... Multiple Rules 33
  6. @presidentbeef Create a Rule class CheckSurvey < Rule def run(file_name,

    code) if code =~ /get_survey\([^,)]+\)/ warn file_name, "get_survey without user ID" end end end 34
  7. @presidentbeef Base Rule Class class Rule @rules = [] @warnings

    = [] def self.inherited klass @rules << klass end def self.run_rules files files.each do |name| code = File.read(name) @rules.each do |r| r.new.run(name, code) end end end def self.warnings @warnings end def warn file_name, msg Rule.warnings << [file_name, msg] end end 35
  8. @presidentbeef Code to Run It system "git checkout #{ARGV[0]}" files

    = `git diff --name-status master HEAD | grep -E "^(A|M)" | cut -f 2` Rule.run_rules(files.split) Rule.warnings.each do |file_name, message| puts "#{message} in #{file_name}" end if Rule.warnings.any? exit 1 end 36
  9. @presidentbeef Compilation vs. Static Analysis Lexical Analysis Parse into Abstract

    Syntax Tree Input Program Text Convert to Intermediate Form(s) Optimize Output Compiled Code Analyze! Output Finding Report Semantic Analysis 41
  10. @presidentbeef Python (Astroid) Module() body = [ Discard() value =

    CallFunc() func = Name(get_survey) args = [ Name(survey_id) ] starargs = kwargs = ] AstroidBuilder().string_build("get_survey(survey_id)") 45
  11. @presidentbeef JavaScript (Esprima) { "type": "Program", "body": [ { "type":

    "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "get_survey" }, "arguments": [ { "type": "Identifier", "name": "survey_id" } ] } } ], "sourceType": "script" } esprima.parse("get_survey(survey_id)") 46
  12. @presidentbeef Pylint Custom Checker import astroid from pylint.interfaces import IAstroidChecker

    from pylint.checkers import BaseChecker class GetSurveyChecker(BaseChecker): __implements__ = IAstroidChecker name = 'survey_checker' msgs = {'E9312': ('Call to get_survey without user ID', 'get_survey', 'Calls to get_survey must include user ID.') } priority = -1 def visit_callfunc(self, node): if (isinstance(node.func, astroid.Name) and node.func.name == 'get_survey' and len(node.args) == 1): self.add_message('get_survey', node=node) def register(linter): linter.register_checker(GetSurveyChecker(linter)) 49
  13. @presidentbeef Pylint Custom Warning ************* Module my_file ... E: 12,

    4: Call to get_survey without user ID (get_survey) ... 51
  14. @presidentbeef Brakeman Custom Check require 'brakeman/checks/base_check' class Brakeman::CheckGetSurvey < Brakeman::BaseCheck

    Brakeman::Checks.add self Brakeman::WarningCodes::Codes[:get_survey] = 2001 @description = "Finds get_survey calls without a user_id" def run_check @tracker.find_call(target: false, method: :get_survey).each do |result| next if duplicate? result add_result result if result[:call].second_arg.nil? warn :result => result, :warning_type => "Direct Object Reference", :warning_code => :get_survey, :message => "Use of get_survey without user_id", :confidence => CONFIDENCE[:high] end end end end 53
  15. @presidentbeef JavaScript (Esprima) { "type": "Program", "body": [ { "type":

    "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "get_survey" }, "arguments": [ { "type": "Identifier", "name": "survey_id" } ] } } ], "sourceType": "script" } esprima.parse("get_survey(survey_id)") 58
  16. @presidentbeef Walking Esprima AST var check_get_survey = function(ast) { if(ast.type

    == "CallExpression" && ast.callee.type == "Identifier" && ast.callee.name == "get_survey" && ast.arguments.length == 1) { console.log("get_survey called without user_id at line ", ast.loc.start) } } var walk = function(ast) { if(Array.isArray(ast)) { ast.forEach(walk); } else if(ast.type) { check_get_survey(ast) for (key in ast) { walk(ast[key]) } } } var esprima = require('esprima'); walk(esprima.parse('get_survey(survey_id)', { loc: true })) 59
  17. @presidentbeef Walking RubyParser AST require 'ruby_parser' require 'sexp_processor' class FindGetSurvey

    < SexpInterpreter def process_call exp if exp[1].nil? and exp[2] == :get_survey and exp[4].nil? puts "get_survey called without user_id at line #{exp.line}" end end end ast = RubyParser.new.parse "get_survey(survey_id)" FindGetSurvey.new.process ast 62
  18. @presidentbeef Summary Start small: identify single issue to solve Tailor

    solution to your environment Automate enforcement 63
  19. @presidentbeef Go Mash Some Code! @PresidentBeef / presidentbeef.com @Brakeman /

    brakeman.org @BrakemanPro / brakemanpro.com @MakotoTheCat 64