Skip to content

Instantly share code, notes, and snippets.

@kyokley
Last active June 28, 2018 06:33
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save kyokley/3e868a6575d28cb4020694876d8f16b7 to your computer and use it in GitHub Desktop.
Checking for Python Security Vulnerabilities with VIM

Checking for Python Security Vulnerabilities with VIM (Buffer Pre-write Hook Part 3)

Introduction

To continue my series of vim pre-write hooks, I wanted to add a new check for security static analysis failures. To see the progression of the series, please check out my other gists.

Back to security... In my office, we use OpenStack's Bandit static analysis tool. If you're not familiar with it, you should check it out. It's pretty nifty.

Getting Started

My goal is that I would like VIM to automatically run bandit whenever I try to save a buffer. The basic concept here is similar to my method of invoking pyflakes before each write.

Step 1: Copy the current buffer into a temporary buffer

let s:file_name = expand('%:t')
%yank p
new
0put p
$,$d

This step is exactly the same as the pyflakes checking from my previous gist.

Step 2: Get the output from bandit

%!bandit -
exe '%s/<stdin>/' . s:file_name . '/e'
unlet! s:file_name

Still pretty similar to my pyflakes handling. So far, so good.

Update: It was pointed out to me that I'm cheating a little bit here by assuming bandit is installed globally. It should be trivial to update this with a copy from a virtualenv. Something like:

%!/path/to/virtualenv/bin/bandit -
exe '%s/<stdin>/' . s:file_name . '/e'
unlet! s:file_name

Step 3: Find badness

[main]  INFO    profile include tests: None
[main]  INFO    profile exclude tests: None
[main]  INFO    cli include tests: None
[main]  INFO    cli exclude tests: None
[main]  INFO    running on Python 2.7.12
[node_visitor]  INFO    Unable to find qualified name for module: main.py
Run started:2017-03-22 21:13:51.528702

Test results:
>> Issue: [B605:start_process_with_a_shell] Starting a process with a shell, possible injection detected, security issue.
   Severity: High   Confidence: High
   Location: main.py:19
18      arg = 'this is no good'
19      os.system('echo %s' % arg)
20      while pos < len(string):

--------------------------------------------------

Code scanned:
    Total lines of code: 41
    Total lines skipped (#nosec): 0

Run metrics:
    Total issues (by severity):
        Undefined: 0
        Low: 0
        Medium: 0
        High: 1
    Total issues (by confidence):
        Undefined: 0
        Low: 0
        Medium: 0
        High: 1
Files skipped (0):

The bandit output gives a good amount of info but I'd really like to focus on just what's pertinent. What I'd like to capture is just the issue, severity, confidence, and location.

This could look like the following:

let s:is_res = search('^>> Issue:', 'nw')
if s:is_res != 0
    let s:res_end = s:is_res + 2
    for item in getline(s:is_res, s:res_end)
        echohl ErrorMsg | echo item | echohl None
    endfor
...

The first line of that block is searching for ">> Issue:" and getting the line number where it occurs. Simple. The next trick is to capture the next two lines. The getline function in Vim can either take a single argument which returns the given line as a string, or as I'm using it here with two arguments, it returns a list of strings for that range. Since I now have a list, I loop over each of the items and print them.

    echohl ErrorMsg | echo item | echohl None

The echohl's that are bracketing the echo item are responsible for setting the message highlighting to ErrorMsg and then turning it off again.

Step 4: Clean up and raise

    bd!
    throw 'Bandit Error'

After printing the error information, I still need to do a little cleanup so I delete the temporary buffer I was using. Finally, to prevent the write, I throw an error and just generically call it a "Bandit Error" since the important stuff was already printed.

All Together

%yank p
new
0put p
$,$d
%!bandit -
exe '%s/<stdin>/' . s:file_name . '/e'

let s:is_res = search('^>> Issue:', 'nw')
if s:is_res != 0
    let s:res_end = s:is_res + 2
    for item in getline(s:is_res, s:res_end)
        echohl ErrorMsg | echo item | echohl None
    endfor

    bd!
    throw 'Bandit Error'
endif
bd!

Result

Over the course of my three gists, this is what my BufWritePre hook looks like now:

function! RaiseExceptionForUnresolvedErrors()
    let s:file_name = expand('%:t')

    let s:conflict_line = search('\v^[<=>]{7}( .*|$)', 'nw')
    if s:conflict_line != 0
        throw 'Found unresolved conflicts in ' . s:file_name . ':' . s:conflict_line
    endif

    let s:whitespace_line = search('\s\+$', 'nw')
    if s:whitespace_line != 0
        throw 'Found trailing whitespace in ' . s:file_name . ':' . s:whitespace_line
    endif

    if &filetype == 'python'
        silent %yank p
        new
        silent 0put p
        silent $,$d
        silent %!pyflakes
        silent exe '%s/<stdin>/' . s:file_name . '/e'

        let s:un_res = search('\(unable to detect \)\@<!undefined name', 'nw')
        if s:un_res != 0
            let s:message = 'Syntax error! ' . getline(s:un_res)
            bd!
            throw s:message
        endif

        let s:ui_res = search('unexpected indent', 'nw')
        if s:ui_res != 0
            let s:message = 'Syntax error! ' . getline(s:ui_res)
            bd!
            throw s:message
        endif

        let s:ui_res = search('expected an indented block', 'nw')
        if s:ui_res != 0
            let s:message = 'Syntax error! ' . getline(s:ui_res)
            bd!
            throw s:message
        endif

        let s:is_res = search('invalid syntax', 'nw')
        if s:is_res != 0
            let s:message = 'Syntax error! ' . getline(s:is_res)
            bd!
            throw s:message
        endif

        let s:is_res = search('unindent does not match any outer indentation level', 'nw')
        if s:is_res != 0
            let s:message = 'Syntax error! ' . getline(s:is_res)
            bd!
            throw s:message
        endif

        let s:is_res = search('EOL while scanning string literal', 'nw')
        if s:is_res != 0
            let s:message = 'Syntax error! ' . getline(s:is_res)
            bd!
            throw s:message
        endif

        let s:is_res = search('trailing comma not allowed without surrounding parentheses', 'nw')
        if s:is_res != 0
            let s:message = 'Syntax error! ' . getline(s:is_res)
            bd!
            throw s:message
        endif

        let s:is_res = search('problem decoding source', 'nw')
        if s:is_res != 0
            let s:message = 'pyflakes error! Check results manually! ' . getline(s:is_res)
            bd!
            throw s:message
        endif

        bd!

        silent %yank p
        new
        silent 0put p
        silent $,$d
        silent %!bandit -
        silent exe '%s/<stdin>/' . s:file_name . '/e'

        let s:is_res = search('^>> Issue:', 'nw')
        if s:is_res != 0
            let s:res_end = s:is_res + 2
            for item in getline(s:is_res, s:res_end)
                echohl ErrorMsg | echo item | echohl None
            endfor

            bd!
            throw 'Bandit Error'
        endif

        bd!
    endif
endfunction
autocmd BufWritePre * call RaiseExceptionForUnresolvedErrors()

Final Remarks

If you're like me and stuck maintaining a lot of code that you may not have been responsible for writing, it can be frustrating trying to save and constantly having to deal with the errors. Of course, the right answer is to fix the failures. However, we're sometimes forced to ignore things for a later date (live to fight another day?). Bandit has the ability to only report on high severity issues if you tell it to. So, from the above,

    silent %!bandit -

would become

    silent %!bandit -lll -

If that's still not enough, remember you can still force Vim to ignore the pre-write hook entirely by telling it not to process any autocommands.

    :noautocmd w

Or,

    :noa w

Next Steps

Go back and review the other gists in the series

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment