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.

711272a06d435ca5139b50874351cdbf?s=128

Justin Collins

January 07, 2016
Tweet

Transcript

  1. @presidentbeef Continuous Security with Practical Static Analysis Justin Collins @presidentbeef

    ^ tweet me questions CodeMash 2016
  2. @presidentbeef Definitions 2

  3. @presidentbeef Continuous Security Automated, always-on processes for detecting potential security

    vulnerabilities 3
  4. @presidentbeef Static Analysis Information determined about a program without actually

    running the code 4
  5. @presidentbeef Practical Static Analysis Things you can reasonably implement after

    this talk 5
  6. @presidentbeef Why Static Analysis? Fast Run anytime Easy to automate

    Points directly at suspect code 6
  7. @presidentbeef Background 7

  8. @presidentbeef Static analysis security tool for Ruby on Rails 8

  9. @presidentbeef Tool Cycle Run Tool Wait Interpret Results Fix Issues

    Repeat 9
  10. @presidentbeef SADB 10

  11. @presidentbeef 11

  12. @presidentbeef Getting Started 12

  13. @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
  14. @presidentbeef 2 - Identify a Solution Write a safer library

    Make security opt-out Detect unsafe calls/settings/API usage 14
  15. @presidentbeef 3 - Enforce the Solution Write tests Use static

    analysis Use dynamic analysis 15
  16. @presidentbeef 4 - Automate Enforcement Part of continuous integration Part

    of code review Standalone, continuously-running process Local scripts/hooks 16
  17. @presidentbeef Automation Strategies 17

  18. @presidentbeef Continuous Integration 18

  19. @presidentbeef Code Review 19

  20. @presidentbeef Deployment Gate 20

  21. @presidentbeef Separate Process 21

  22. @presidentbeef Local Script/Git Hook 22

  23. @presidentbeef Scenario 23

  24. @presidentbeef 1 - Identify a Problem get_survey(survey_id) 24

  25. @presidentbeef 2 - Identify a Solution get_survey(survey_id, user_id) 25

  26. @presidentbeef 3 - Enforce the Solution Use static analysis to

    check for use 26
  27. @presidentbeef Statically Analyzing 27

  28. @presidentbeef Regular Expressions • grep • ack • ag •

    …or build your own 28
  29. @presidentbeef grep/ack grep -R "get_survey([^,)]\+)" * ack "get_survey\([^,)]+\)" --type=python 29

  30. @presidentbeef Changed Files grep file1 file2 file3 ... Desired Flow

    30
  31. @presidentbeef Bash git checkout $@ --quiet files=$(git diff --name-status master

    HEAD | grep -E "^(A|M)" | cut -f 2) grep "get_survey([^,)]\+)" $files 31
  32. @presidentbeef Bash bash check_stuff.sh 9c2ca25 32

  33. @presidentbeef Changed Files Rules file1 - warning X file2 -

    warning Y file3 - warning X ... Multiple Rules 33
  34. @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
  35. @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
  36. @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
  37. @presidentbeef Ruby ruby check_stuff.rb 9c2ca25 37

  38. @presidentbeef False Positives # Remember not to use get_survey(survey_id)! function

    get_survey(node) {...} 38
  39. @presidentbeef False Negatives get_survey(input.split(",")[0]) 39

  40. @presidentbeef Abstract Syntax Trees 40

  41. @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
  42. @presidentbeef get_survey(survey_id) call args local nil "get_survey" "survey_id" 42

  43. @presidentbeef S-Expressions (call nil "get_survey" (local "survey_id")) get_survey(survey_id) 43

  44. @presidentbeef Ruby (RubyParser) s(:call, nil, :get_survey, s(:call, nil, :survey_id)) RubyParser.new.parse("get_survey(survey_id")

    44
  45. @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
  46. @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
  47. @presidentbeef Existing Tools 47

  48. @presidentbeef www.pylint.org 48

  49. @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
  50. @presidentbeef Pylint Custom Checker pylint --load-plugins custom my_file.py 50

  51. @presidentbeef Pylint Custom Warning ************* Module my_file ... E: 12,

    4: Call to get_survey without user ID (get_survey) ... 51
  52. @presidentbeef brakeman.org 52

  53. @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
  54. @presidentbeef Brakeman Custom Check brakeman --add-checks-path custom_checks/ 54

  55. @presidentbeef Brakeman Custom Warning 55

  56. @presidentbeef Custom Tools 56

  57. @presidentbeef Esprima esprima.org 57

  58. @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
  59. @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
  60. @presidentbeef RubyParser github.com/seattlerb/ruby_parser 60

  61. @presidentbeef Ruby (RubyParser) s(:call, nil, :get_survey, s(:call, nil, :survey_id)) RubyParser.new.parse("get_survey(survey_id")

    61
  62. @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
  63. @presidentbeef Summary Start small: identify single issue to solve Tailor

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

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