mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-07-01 14:41:01 +02:00
280 lines
10 KiB
Python
280 lines
10 KiB
Python
# -*- coding:utf-8 -*-
|
|
#
|
|
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import ast
|
|
import logging
|
|
import operator
|
|
|
|
from bandit.core import constants
|
|
from bandit.core import tester as b_tester
|
|
from bandit.core import utils as b_utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class BanditNodeVisitor(object):
|
|
def __init__(self, fname, metaast, testset,
|
|
debug, nosec_lines, metrics):
|
|
self.debug = debug
|
|
self.nosec_lines = nosec_lines
|
|
self.seen = 0
|
|
self.scores = {
|
|
'SEVERITY': [0] * len(constants.RANKING),
|
|
'CONFIDENCE': [0] * len(constants.RANKING)
|
|
}
|
|
self.depth = 0
|
|
self.fname = fname
|
|
self.metaast = metaast
|
|
self.testset = testset
|
|
self.imports = set()
|
|
self.import_aliases = {}
|
|
self.tester = b_tester.BanditTester(
|
|
self.testset, self.debug, nosec_lines)
|
|
|
|
# in some cases we can't determine a qualified name
|
|
try:
|
|
self.namespace = b_utils.get_module_qualname_from_path(fname)
|
|
except b_utils.InvalidModulePath:
|
|
LOG.info('Unable to find qualified name for module: %s',
|
|
self.fname)
|
|
self.namespace = ""
|
|
LOG.debug('Module qualified name: %s', self.namespace)
|
|
self.metrics = metrics
|
|
|
|
def visit_ClassDef(self, node):
|
|
'''Visitor for AST ClassDef node
|
|
|
|
Add class name to current namespace for all descendants.
|
|
:param node: Node being inspected
|
|
:return: -
|
|
'''
|
|
# For all child nodes, add this class name to current namespace
|
|
self.namespace = b_utils.namespace_path_join(self.namespace, node.name)
|
|
|
|
def visit_FunctionDef(self, node):
|
|
'''Visitor for AST FunctionDef nodes
|
|
|
|
add relevant information about the node to
|
|
the context for use in tests which inspect function definitions.
|
|
Add the function name to the current namespace for all descendants.
|
|
:param node: The node that is being inspected
|
|
:return: -
|
|
'''
|
|
|
|
self.context['function'] = node
|
|
qualname = self.namespace + '.' + b_utils.get_func_name(node)
|
|
name = qualname.split('.')[-1]
|
|
|
|
self.context['qualname'] = qualname
|
|
self.context['name'] = name
|
|
|
|
# For all child nodes and any tests run, add this function name to
|
|
# current namespace
|
|
self.namespace = b_utils.namespace_path_join(self.namespace, name)
|
|
self.update_scores(self.tester.run_tests(self.context, 'FunctionDef'))
|
|
|
|
def visit_Call(self, node):
|
|
'''Visitor for AST Call nodes
|
|
|
|
add relevant information about the node to
|
|
the context for use in tests which inspect function calls.
|
|
:param node: The node that is being inspected
|
|
:return: -
|
|
'''
|
|
|
|
self.context['call'] = node
|
|
qualname = b_utils.get_call_name(node, self.import_aliases)
|
|
name = qualname.split('.')[-1]
|
|
|
|
self.context['qualname'] = qualname
|
|
self.context['name'] = name
|
|
|
|
self.update_scores(self.tester.run_tests(self.context, 'Call'))
|
|
|
|
def visit_Import(self, node):
|
|
'''Visitor for AST Import nodes
|
|
|
|
add relevant information about node to
|
|
the context for use in tests which inspect imports.
|
|
:param node: The node that is being inspected
|
|
:return: -
|
|
'''
|
|
for nodename in node.names:
|
|
if nodename.asname:
|
|
self.import_aliases[nodename.asname] = nodename.name
|
|
self.imports.add(nodename.name)
|
|
self.context['module'] = nodename.name
|
|
self.update_scores(self.tester.run_tests(self.context, 'Import'))
|
|
|
|
def visit_ImportFrom(self, node):
|
|
'''Visitor for AST ImportFrom nodes
|
|
|
|
add relevant information about node to
|
|
the context for use in tests which inspect imports.
|
|
:param node: The node that is being inspected
|
|
:return: -
|
|
'''
|
|
module = node.module
|
|
if module is None:
|
|
return self.visit_Import(node)
|
|
|
|
for nodename in node.names:
|
|
# TODO(ljfisher) Names in import_aliases could be overridden
|
|
# by local definitions. If this occurs bandit will see the
|
|
# name in import_aliases instead of the local definition.
|
|
# We need better tracking of names.
|
|
if nodename.asname:
|
|
self.import_aliases[nodename.asname] = (
|
|
module + "." + nodename.name
|
|
)
|
|
else:
|
|
# Even if import is not aliased we need an entry that maps
|
|
# name to module.name. For example, with 'from a import b'
|
|
# b should be aliased to the qualified name a.b
|
|
self.import_aliases[nodename.name] = (module + '.' +
|
|
nodename.name)
|
|
self.imports.add(module + "." + nodename.name)
|
|
self.context['module'] = module
|
|
self.context['name'] = nodename.name
|
|
self.update_scores(self.tester.run_tests(self.context, 'ImportFrom'))
|
|
|
|
def visit_Str(self, node):
|
|
'''Visitor for AST String nodes
|
|
|
|
add relevant information about node to
|
|
the context for use in tests which inspect strings.
|
|
:param node: The node that is being inspected
|
|
:return: -
|
|
'''
|
|
self.context['str'] = node.s
|
|
if not isinstance(node.parent, ast.Expr): # docstring
|
|
self.context['linerange'] = b_utils.linerange_fix(node.parent)
|
|
self.update_scores(self.tester.run_tests(self.context, 'Str'))
|
|
|
|
def visit_Bytes(self, node):
|
|
'''Visitor for AST Bytes nodes
|
|
|
|
add relevant information about node to
|
|
the context for use in tests which inspect strings.
|
|
:param node: The node that is being inspected
|
|
:return: -
|
|
'''
|
|
self.context['bytes'] = node.s
|
|
if not isinstance(node.parent, ast.Expr): # docstring
|
|
self.context['linerange'] = b_utils.linerange_fix(node.parent)
|
|
self.update_scores(self.tester.run_tests(self.context, 'Bytes'))
|
|
|
|
def pre_visit(self, node):
|
|
self.context = {}
|
|
self.context['imports'] = self.imports
|
|
self.context['import_aliases'] = self.import_aliases
|
|
|
|
if self.debug:
|
|
LOG.debug(ast.dump(node))
|
|
self.metaast.add_node(node, '', self.depth)
|
|
|
|
if hasattr(node, 'lineno'):
|
|
self.context['lineno'] = node.lineno
|
|
|
|
if node.lineno in self.nosec_lines:
|
|
LOG.debug("skipped, nosec")
|
|
self.metrics.note_nosec()
|
|
return False
|
|
|
|
self.context['node'] = node
|
|
self.context['linerange'] = b_utils.linerange_fix(node)
|
|
self.context['filename'] = self.fname
|
|
|
|
self.seen += 1
|
|
LOG.debug("entering: %s %s [%s]", hex(id(node)), type(node),
|
|
self.depth)
|
|
self.depth += 1
|
|
LOG.debug(self.context)
|
|
return True
|
|
|
|
def visit(self, node):
|
|
name = node.__class__.__name__
|
|
method = 'visit_' + name
|
|
visitor = getattr(self, method, None)
|
|
if visitor is not None:
|
|
if self.debug:
|
|
LOG.debug("%s called (%s)", method, ast.dump(node))
|
|
visitor(node)
|
|
else:
|
|
self.update_scores(self.tester.run_tests(self.context, name))
|
|
|
|
def post_visit(self, node):
|
|
self.depth -= 1
|
|
LOG.debug("%s\texiting : %s", self.depth, hex(id(node)))
|
|
|
|
# HACK(tkelsey): this is needed to clean up post-recursion stuff that
|
|
# gets setup in the visit methods for these node types.
|
|
if isinstance(node, ast.FunctionDef) or isinstance(node, ast.ClassDef):
|
|
self.namespace = b_utils.namespace_path_split(self.namespace)[0]
|
|
|
|
def generic_visit(self, node):
|
|
"""Drive the visitor."""
|
|
for _, value in ast.iter_fields(node):
|
|
if isinstance(value, list):
|
|
max_idx = len(value) - 1
|
|
for idx, item in enumerate(value):
|
|
if isinstance(item, ast.AST):
|
|
if idx < max_idx:
|
|
setattr(item, 'sibling', value[idx + 1])
|
|
else:
|
|
setattr(item, 'sibling', None)
|
|
setattr(item, 'parent', node)
|
|
|
|
if self.pre_visit(item):
|
|
self.visit(item)
|
|
self.generic_visit(item)
|
|
self.post_visit(item)
|
|
|
|
elif isinstance(value, ast.AST):
|
|
setattr(value, 'sibling', None)
|
|
setattr(value, 'parent', node)
|
|
|
|
if self.pre_visit(value):
|
|
self.visit(value)
|
|
self.generic_visit(value)
|
|
self.post_visit(value)
|
|
|
|
def update_scores(self, scores):
|
|
'''Score updater
|
|
|
|
Since we moved from a single score value to a map of scores per
|
|
severity, this is needed to update the stored list.
|
|
:param score: The score list to update our scores with
|
|
'''
|
|
# we'll end up with something like:
|
|
# SEVERITY: {0, 0, 0, 10} where 10 is weighted by finding and level
|
|
for score_type in self.scores:
|
|
self.scores[score_type] = list(map(
|
|
operator.add, self.scores[score_type], scores[score_type]
|
|
))
|
|
|
|
def process(self, data):
|
|
'''Main process loop
|
|
|
|
Build and process the AST
|
|
:param lines: lines code to process
|
|
:return score: the aggregated score for the current file
|
|
'''
|
|
f_ast = ast.parse(data)
|
|
self.generic_visit(f_ast)
|
|
return self.scores
|