Skip to content

Commit eec4cf2

Browse files
author
Simon Siegert
committed
Add features: json output, stdin read, quiet output
1 parent 48a868a commit eec4cf2

File tree

4 files changed

+73
-35
lines changed

4 files changed

+73
-35
lines changed

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
1+
# pyInjection
12

2-
# Py_find_injection
3-
4-
Py_find_injection uses various heuristics to look for SQL injection vulnerabilities in python source code.
3+
pyInjection uses various heuristics to look for SQL injection vulnerabilities in python source code.
54

65
Based on https://github.com/uber/py-find-injection.
76

87

98
# Usage
109
``` shell
11-
python bin/py_find_injection.py tests/samples/bad_script.py
12-
```
10+
$ python bin/pyInjection.py --help
11+
usage: pyInjection.py [-h] [-v] [-j] [-q] [files [files ...]]
12+
13+
Look for patterns in python source files that might indicate SQL injection or
14+
other vulnerabilities
15+
16+
positional arguments:
17+
files files to check or '-' for standard in
18+
19+
optional arguments:
20+
-h, --help show this help message and exit
21+
-v, --version show program's version number and exit
22+
-j, --json print output in JSON
23+
-q, --quiet Do not print error statistics
24+
25+
Exit status is 0 if all files are okay, 1 if any files have an error. Errors
26+
are printed to standard out
27+
```

bin/py_find_injection.py renamed to bin/pyInjection.py

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
from __future__ import print_function
44

5-
import argparse
65
import ast
76
import sys
8-
9-
10-
version_info = (0, 1, 2)
7+
import fileinput
8+
import json
9+
version_info = (0, 1, 3)
1110
__version__ = '.'.join(map(str, version_info))
1211

1312

@@ -37,6 +36,9 @@ def __init__(self, reason, node, filename):
3736
self.filename = filename
3837
self.node = node
3938

39+
def toDict(self):
40+
return {'file': self.filename, 'line': self.lineno, 'message': self.reason}
41+
4042
def __str__(self):
4143
return "%s:%d\t%s" % (self.filename, self.lineno, self.reason)
4244

@@ -73,14 +75,12 @@ def check_execute(self, node):
7375
if node.func.attr == 'format':
7476
return IllegalLine('str.format called on SQL query', node, self.filename)
7577
elif isinstance(node, ast.Name):
76-
# now we need to figure out where that query is assigned. blargh.
7778
assignment = find_assignment_in_context(node.id, node)
7879
if assignment is not None:
7980
return self.check_execute(assignment.value)
8081

8182
def visit_Call(self, node):
8283
function_name = stringify(node.func)
83-
# catch and check aliases of session.execute and cursor.execute
8484
if function_name.lower().endswith('.execute'):
8585
try:
8686
node.args[0].parent = node
@@ -94,13 +94,11 @@ def visit_Call(self, node):
9494
self.generic_visit(node)
9595

9696
def visit(self, node):
97-
"""Visit a node."""
9897
method = 'visit_' + node.__class__.__name__
9998
visitor = getattr(self, method, self.generic_visit)
10099
return visitor(node)
101100

102101
def generic_visit(self, node):
103-
"""Called if no explicit visitor function exists for a node."""
104102
for field, value in ast.iter_fields(node):
105103
if isinstance(value, list):
106104
for item in value:
@@ -112,34 +110,56 @@ def generic_visit(self, node):
112110
self.visit(value)
113111

114112

115-
def check(filename):
113+
def check(filename, args):
116114
c = Checker(filename=filename)
117-
with open(filename, 'r') as fobj:
118-
try:
119-
parsed = ast.parse(fobj.read(), filename)
120-
c.visit(parsed)
121-
except Exception:
122-
raise
115+
if filename == '-':
116+
fobj = sys.stdin
117+
else:
118+
fobj = open(filename, 'r')
119+
120+
try:
121+
parsed = ast.parse(fobj.read(), filename)
122+
c.visit(parsed)
123+
except Exception:
124+
raise
123125
return c.errors
124126

125127

126-
def main():
128+
def create_parser():
129+
import argparse
127130
parser = argparse.ArgumentParser(
128-
description='Look for patterns in python source files that might indicate SQL injection vulnerabilities',
129-
epilog='Exit status is 0 if all files are okay, 1 if any files have an error. Errors are printed to stdout'
131+
description='Look for patterns in python source files that might indicate SQL injection or other vulnerabilities',
132+
epilog='Exit status is 0 if all files are okay, 1 if any files have an error. Found vulnerabilities are printed to standard out'
130133
)
131-
parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
132-
parser.add_argument('files', nargs='+', help='Files to check')
134+
parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__)
135+
parser.add_argument('files', nargs='*', help='files to check or \'-\' for standard in')
136+
parser.add_argument('-j', '--json', action='store_true', help='print output in JSON')
137+
parser.add_argument('-s', '--stdin', action='store_true', help='read from standard in, passed files are ignored')
138+
parser.add_argument('-q', '--quiet', action='store_true', help='do not print error statistics')
139+
140+
return parser
141+
142+
143+
def main():
144+
parser = create_parser()
133145
args = parser.parse_args()
134146

147+
if not (args.files or args.stdin):
148+
parser.error('incorrect number of arguments')
149+
if args.stdin:
150+
args.files = ['-']
151+
135152
errors = []
136153
for fname in args.files:
137-
these_errors = check(fname)
138-
if these_errors:
139-
print('\n'.join(str(e) for e in these_errors))
140-
errors.extend(these_errors)
154+
errors.extend(check(fname, args))
141155
if errors:
142-
print('%d total errors' % len(errors), file=sys.stderr)
156+
if args.json:
157+
print(json.dumps(map(lambda x: x.toDict(), errors),
158+
indent=2, sort_keys=True))
159+
else:
160+
print('\n'.join(str(e) for e in errors))
161+
if not args.quiet:
162+
print('Total errors: %d' % len(errors), file=sys.stderr)
143163
return 1
144164
else:
145165
return 0

setup.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33

44
setup(
5-
name="py-find-injection",
6-
version="0.1.2",
5+
name="pyInjection",
6+
version="0.1.3",
77
author="James Brown",
88
author_email="jbrown@uber.com",
9-
url="https://github.com/uber/py-find-injection",
9+
url="https://github.com/simsieg/pyInjection",
1010
description="simple python ast consumer which searches for common SQL injection attacks",
1111
license='MIT (Expat)',
1212
classifiers=[
@@ -22,12 +22,12 @@
2222
packages=find_packages(exclude=["tests"]),
2323
entry_points={
2424
"console_scripts": [
25-
"py-find-injection = py_find_injection:main",
25+
"pyInjection = pyInjection:main",
2626
]
2727
},
2828
tests_require=["nose==1.3.0", "mock==1.0.1"],
2929
test_suite="nose.collector",
30-
long_description="""py_find_injection
30+
long_description="pyInjection"
3131

3232
Walks the AST and looks for arguments to cursor.execute or session.execute; then
3333
determines whether string interpolation, concatenation or the .format() call is used

tests/samples/bad_script_3.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
username = 'testuser'
2+
query = 'ls -lsa ' + username
3+
result = eval(query)

0 commit comments

Comments
 (0)