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.
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.
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.
%!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
[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.
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.
%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!
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()
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
Go back and review the other gists in the series