Project Migration to PyCQA

This change rehomes the project to PyCQA[1] as reported to the
openstack-dev mailing list [1]

[0] https://github.com/PyCQA/bandit
[1] http://lists.openstack.org/pipermail/openstack-dev/2018-April/129386.html

Change-Id: I6aad329a60799ea24a3d9bc49e35c3c35ed9dc3b
This commit is contained in:
lhinds 2018-04-26 11:21:32 +01:00 committed by Andreas Jaeger
parent 209182c3ee
commit 2d2170273b
251 changed files with 7 additions and 17068 deletions

View File

@ -1,3 +0,0 @@
[report]
include = bandit/*
omit = bandit/tests/functional/*

19
.gitignore vendored
View File

@ -1,19 +0,0 @@
env*
venv*
*.pyc
.DS_Store
*.egg
*.egg-info
.eggs/
.idea/
.tox
.stestr
build/*
cover/*
.coverage*
doc/build/*
ChangeLog
doc/source/api
.*.sw?
AUTHORS
releasenotes/build

View File

@ -1,4 +0,0 @@
[DEFAULT]
test_path=${OS_TEST_PATH:-./tests/unit}
top_dir=./
group_regex=.*(test_cert_setup)

View File

@ -1,179 +0,0 @@
- job:
name: bandit-integration-barbican
parent: legacy-base
run: playbooks/legacy/bandit-integration-barbican/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/barbican
- openstack/requirements
- job:
name: bandit-integration-glance
parent: legacy-base
run: playbooks/legacy/bandit-integration-glance/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/glance
- openstack/requirements
- job:
name: bandit-integration-glance_store
parent: legacy-base
run: playbooks/legacy/bandit-integration-glance_store/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/glance
- openstack/glance_store
- openstack/requirements
- job:
name: bandit-integration-keystone
parent: legacy-base
run: playbooks/legacy/bandit-integration-keystone/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/keystone
- openstack/requirements
- job:
name: bandit-integration-keystonemiddleware
parent: legacy-base
run: playbooks/legacy/bandit-integration-keystonemiddleware/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/keystone
- openstack/keystonemiddleware
- openstack/requirements
- job:
name: bandit-integration-magnum
parent: legacy-base
run: playbooks/legacy/bandit-integration-magnum/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/magnum
- openstack/requirements
- job:
name: bandit-integration-oslo.config
parent: legacy-base
run: playbooks/legacy/bandit-integration-oslo.config/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/oslo.config
- openstack/requirements
- job:
name: bandit-integration-oslo.log
parent: legacy-base
run: playbooks/legacy/bandit-integration-oslo.log/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/oslo.log
- openstack/requirements
- job:
name: bandit-integration-oslo.service
parent: legacy-base
run: playbooks/legacy/bandit-integration-oslo.service/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/oslo.service
- openstack/requirements
- job:
name: bandit-integration-oslo.utils
parent: legacy-base
run: playbooks/legacy/bandit-integration-oslo.utils/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/oslo.utils
- openstack/requirements
- job:
name: bandit-integration-oslo.vmware
parent: legacy-base
run: playbooks/legacy/bandit-integration-oslo.vmware/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/oslo.vmware
- openstack/requirements
- job:
name: bandit-integration-python-keystoneclient
parent: legacy-base
run: playbooks/legacy/bandit-integration-python-keystoneclient/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/keystone
- openstack/python-keystoneclient
- openstack/requirements
- job:
name: bandit-integration-python-magnumclient
parent: legacy-base
run: playbooks/legacy/bandit-integration-python-magnumclient/run.yaml
timeout: 1800
required-projects:
- openstack/bandit
- openstack/magnum
- openstack/python-magnumclient
- openstack/requirements
- job:
name: bandit-integration-sahara
parent: legacy-base
run: playbooks/legacy/bandit-integration-sahara/run.yaml
timeout: 1800
required-projects:
- openstack/ara
- openstack/bandit
- openstack/requirements
- openstack/sahara
- project:
check:
jobs:
- bandit-integration-barbican
- bandit-integration-glance
- bandit-integration-keystone
- bandit-integration-glance_store
- bandit-integration-keystonemiddleware
- bandit-integration-magnum
- bandit-integration-oslo.config
- bandit-integration-oslo.log
- bandit-integration-oslo.service
- bandit-integration-oslo.utils
- bandit-integration-oslo.vmware
- bandit-integration-python-keystoneclient
- bandit-integration-python-magnumclient
- bandit-integration-sahara
- openstack-tox-lower-constraints
gate:
jobs:
- bandit-integration-barbican
- bandit-integration-glance
- bandit-integration-keystone
- bandit-integration-glance_store
- bandit-integration-keystonemiddleware
- bandit-integration-magnum
- bandit-integration-oslo.config
- bandit-integration-oslo.log
- bandit-integration-oslo.service
- bandit-integration-oslo.utils
- bandit-integration-oslo.vmware
- bandit-integration-python-keystoneclient
- bandit-integration-python-magnumclient
- openstack-tox-lower-constraints

176
LICENSE
View File

@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

@ -1,428 +1,13 @@
Bandit
======
.. image:: https://governance.openstack.org/badges/bandit.svg
:target: https://governance.openstack.org/reference/tags/index.html
:alt: Bandit team and repository tags
This project is no longer maintained in OpenStack.
.. image:: https://img.shields.io/pypi/v/bandit.svg
:target: https://pypi.python.org/pypi/bandit/
:alt: Latest Version
Please visit PyCQA to raise issues or make contributions:
.. image:: https://img.shields.io/pypi/pyversions/bandit.svg
:target: https://pypi.python.org/pypi/bandit/
:alt: Python Versions
https://github.com/PyCQA/bandit
.. image:: https://img.shields.io/pypi/format/bandit.svg
:target: https://pypi.python.org/pypi/bandit/
:alt: Format
.. image:: https://img.shields.io/badge/license-Apache%202-blue.svg
:target: https://git.openstack.org/cgit/openstack/bandit/plain/LICENSE
:alt: License
A security linter from OpenStack Security
* Free software: Apache license
* Documentation: https://wiki.openstack.org/wiki/Security/Projects/Bandit
* Source: https://git.openstack.org/cgit/openstack/bandit
* Bugs: https://bugs.launchpad.net/bandit
Overview
--------
Bandit is a tool designed to find common security issues in Python code. To do
this Bandit processes each file, builds an AST from it, and runs appropriate
plugins against the AST nodes. Once Bandit has finished scanning all the files
it generates a report.
Installation
------------
Bandit is distributed on PyPI. The best way to install it is with pip:
Create a virtual environment (optional)::
virtualenv bandit-env
Install Bandit::
pip install bandit
# Or if you're working with a Python 3.5 project
pip3.5 install bandit
Run Bandit::
bandit -r path/to/your/code
Bandit can also be installed from source. To do so, download the source tarball
from PyPI, then install it::
python setup.py install
Usage
-----
Example usage across a code tree::
bandit -r ~/openstack-repo/keystone
Example usage across the ``examples/`` directory, showing three lines of
context and only reporting on the high-severity issues::
bandit examples/*.py -n 3 -lll
Bandit can be run with profiles. To run Bandit against the examples directory
using only the plugins listed in the ``ShellInjection`` profile::
bandit examples/*.py -p ShellInjection
Bandit also supports passing lines of code to scan using standard input. To
run Bandit with standard input::
cat examples/imports.py | bandit -
Usage::
$ bandit -h
usage: bandit [-h] [-r] [-a {file,vuln}] [-n CONTEXT_LINES] [-c CONFIG_FILE]
[-p PROFILE] [-t TESTS] [-s SKIPS] [-l] [-i]
[-f {csv,custom,html,json,screen,txt,xml,yaml}]
[--msg-template MSG_TEMPLATE] [-o [OUTPUT_FILE]] [-v] [-d]
[--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[--ini INI_PATH] [--version]
[targets [targets ...]]
Bandit - a Python source code security analyzer
positional arguments:
targets source file(s) or directory(s) to be tested
optional arguments:
-h, --help show this help message and exit
-r, --recursive find and process files in subdirectories
-a {file,vuln}, --aggregate {file,vuln}
aggregate output by vulnerability (default) or by
filename
-n CONTEXT_LINES, --number CONTEXT_LINES
maximum number of code lines to output for each issue
-c CONFIG_FILE, --configfile CONFIG_FILE
optional config file to use for selecting plugins and
overriding defaults
-p PROFILE, --profile PROFILE
profile to use (defaults to executing all tests)
-t TESTS, --tests TESTS
comma-separated list of test IDs to run
-s SKIPS, --skip SKIPS
comma-separated list of test IDs to skip
-l, --level report only issues of a given severity level or higher
(-l for LOW, -ll for MEDIUM, -lll for HIGH)
-i, --confidence report only issues of a given confidence level or
higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)
-f {csv,custom,html,json,screen,txt,xml,yaml}, --format {csv,custom,html,json,screen,txt,xml,yaml}
specify output format
--msg-template MSG_TEMPLATE
specify output message template (only usable with
--format custom), see CUSTOM FORMAT section for list
of available values
-o [OUTPUT_FILE], --output [OUTPUT_FILE]
write report to filename
-v, --verbose output extra information like excluded and included
files
-d, --debug turn on debug mode
--ignore-nosec do not skip lines with # nosec comments
-x EXCLUDED_PATHS, --exclude EXCLUDED_PATHS
comma-separated list of paths to exclude from scan
(note that these are in addition to the excluded paths
provided in the config file)
-b BASELINE, --baseline BASELINE
path of a baseline report to compare against (only
JSON-formatted files are accepted)
--ini INI_PATH path to a .bandit file that supplies command line
arguments
--version show program's version number and exit
CUSTOM FORMATTING
-----------------
Available tags:
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template:
bandit -r examples/ --format custom --msg-template \
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as:
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style:
bandit -r examples/ --format custom --msg-template \
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
The following tests were discovered and loaded:
-----------------------------------------------
B101 assert_used
B102 exec_used
B103 set_bad_file_permissions
B104 hardcoded_bind_all_interfaces
B105 hardcoded_password_string
B106 hardcoded_password_funcarg
B107 hardcoded_password_default
B108 hardcoded_tmp_directory
B109 password_config_option_not_marked_secret
B110 try_except_pass
B111 execute_with_run_as_root_equals_true
B112 try_except_continue
B201 flask_debug_true
B301 pickle
B302 marshal
B303 md5
B304 ciphers
B305 cipher_modes
B306 mktemp_q
B307 eval
B308 mark_safe
B309 httpsconnection
B310 urllib_urlopen
B311 random
B312 telnetlib
B313 xml_bad_cElementTree
B314 xml_bad_ElementTree
B315 xml_bad_expatreader
B316 xml_bad_expatbuilder
B317 xml_bad_sax
B318 xml_bad_minidom
B319 xml_bad_pulldom
B320 xml_bad_etree
B321 ftplib
B322 input
B323 unverified_context
B324 hashlib_new_insecure_functions
B401 import_telnetlib
B402 import_ftplib
B403 import_pickle
B404 import_subprocess
B405 import_xml_etree
B406 import_xml_sax
B407 import_xml_expat
B408 import_xml_minidom
B409 import_xml_pulldom
B410 import_lxml
B411 import_xmlrpclib
B412 import_httpoxy
B501 request_with_no_cert_validation
B502 ssl_with_bad_version
B503 ssl_with_bad_defaults
B504 ssl_with_no_version
B505 weak_cryptographic_key
B506 yaml_load
B601 paramiko_calls
B602 subprocess_popen_with_shell_equals_true
B603 subprocess_without_shell_equals_true
B604 any_other_function_with_shell_equals_true
B605 start_process_with_a_shell
B606 start_process_with_no_shell
B607 start_process_with_partial_path
B608 hardcoded_sql_expressions
B609 linux_commands_wildcard_injection
B701 jinja2_autoescape_false
B702 use_of_mako_templates
Configuration
-------------
An optional config file may be supplied and may include:
- lists of tests which should or shouldn't be run
- exclude_dirs - sections of the path, that if matched, will be excluded from
scanning
- overridden plugin settings - may provide different settings for some
plugins
Per Project Command Line Args
-----------------------------
Projects may include a `.bandit` file that specifies command line arguments
that should be supplied for that project. The currently supported arguments
are:
- targets: comma separated list of target dirs/files to run bandit on
- exclude: comma separated list of excluded paths
- skips: comma separated list of tests to skip
- tests: comma separated list of tests to run
To use this, put a .bandit file in your project's directory. For example:
::
[bandit]
exclude: /test
::
[bandit]
tests: B101,B102,B301
Exclusions
----------
In the event that a line of code triggers a Bandit issue, but that the line
has been reviewed and the issue is a false positive or acceptable for some
other reason, the line can be marked with a ``# nosec`` and any results
associated with it will not be reported.
For example, although this line may cause Bandit to report a potential
security issue, it will not be reported::
self.process = subprocess.Popen('/bin/echo', shell=True) # nosec
Vulnerability Tests
-------------------
Vulnerability tests or "plugins" are defined in files in the plugins directory.
Tests are written in Python and are autodiscovered from the plugins directory.
Each test can examine one or more type of Python statements. Tests are marked
with the types of Python statements they examine (for example: function call,
string, import, etc).
Tests are executed by the ``BanditNodeVisitor`` object as it visits each node
in the AST.
Test results are maintained in the ``BanditResultStore`` and aggregated for
output at the completion of a test run.
Writing Tests
-------------
To write a test:
- Identify a vulnerability to build a test for, and create a new file in
examples/ that contains one or more cases of that vulnerability.
- Consider the vulnerability you're testing for, mark the function with one
or more of the appropriate decorators:
- @checks('Call')
- @checks('Import', 'ImportFrom')
- @checks('Str')
- Create a new Python source file to contain your test, you can reference
existing tests for examples.
- The function that you create should take a parameter "context" which is
an instance of the context class you can query for information about the
current element being examined. You can also get the raw AST node for
more advanced use cases. Please see the context.py file for more.
- Extend your Bandit configuration file as needed to support your new test.
- Execute Bandit against the test file you defined in examples/ and ensure
that it detects the vulnerability. Consider variations on how this
vulnerability might present itself and extend the example file and the test
function accordingly.
Extending Bandit
----------------
Bandit allows users to write and register extensions for checks and formatters.
Bandit will load plugins from two entry-points:
- `bandit.formatters`
- `bandit.plugins`
Formatters need to accept 4 things:
- `result_store`: An instance of `bandit.core.BanditResultStore`
- `file_list`: The list of files which were inspected in the scope
- `scores`: The scores awarded to each file in the scope
- `excluded_files`: The list of files that were excluded from the scope
Plugins tend to take advantage of the `bandit.checks` decorator which allows
the author to register a check for a particular type of AST node. For example
::
@bandit.checks('Call')
def prohibit_unsafe_deserialization(context):
if 'unsafe_load' in context.call_function_name_qual:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="Unsafe deserialization detected."
)
To register your plugin, you have two options:
1. If you're using setuptools directly, add something like the following to
your ``setup`` call::
# If you have an imaginary bson formatter in the bandit_bson module
# and a function called `formatter`.
entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']}
# Or a check for using mako templates in bandit_mako that
entry_points={'bandit.plugins': ['mako = bandit_mako']}
2. If you're using pbr, add something like the following to your `setup.cfg`
file::
[entry_points]
bandit.formatters =
bson = bandit_bson:formatter
bandit.plugins =
mako = bandit_mako
Contributing
------------
Contributions to Bandit are always welcome! We can be found on
#openstack-security on Freenode IRC.
The best way to get started with Bandit is to grab the source::
git clone https://git.openstack.org/openstack/bandit.git
You can test any changes with tox::
pip install tox
tox -e pep8
tox -e py27
tox -e py35
tox -e docs
tox -e cover
Reporting Bugs
--------------
Bugs should be reported on Launchpad. To file a bug against Bandit, visit:
https://bugs.launchpad.net/bandit/+filebug
Under Which Version of Python Should I Install Bandit?
------------------------------------------------------
The answer to this question depends on the project(s) you will be running
Bandit against. If your project is only compatible with Python 2.7, you
should install Bandit to run under Python 2.7. If your project is only
compatible with Python 3.5, then use 3.5 respectively. If your project supports
both, you *could* run Bandit with both versions but you don't have to.
Bandit uses the `ast` module from Python's standard library in order to
analyze your Python code. The `ast` module is only able to parse Python code
that is valid in the version of the interpreter from which it is imported. In
other words, if you try to use Python 2.7's `ast` module to parse code written
for 3.5 that uses, for example, `yield from` with asyncio, then you'll have
syntax errors that will prevent Bandit from working properly. Alternatively,
if you are relying on 2.7's octal notation of `0777` then you'll have a syntax
error if you run Bandit on 3.x.
References
==========
Bandit wiki: https://wiki.openstack.org/wiki/Security/Projects/Bandit
Python AST module documentation: https://docs.python.org/2/library/ast.html
Green Tree Snakes - the missing Python AST docs:
https://greentreesnakes.readthedocs.org/en/latest/
Documentation of the various types of AST nodes that Bandit currently covers
or could be extended to cover:
https://greentreesnakes.readthedocs.org/en/latest/nodes.html
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".

View File

@ -1,31 +0,0 @@
# -*- 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 pbr.version
from bandit.core import config # noqa
from bandit.core import context # noqa
from bandit.core import manager # noqa
from bandit.core import meta_ast # noqa
from bandit.core import node_visitor # noqa
from bandit.core import test_set # noqa
from bandit.core import tester # noqa
from bandit.core import utils # noqa
from bandit.core.constants import * # noqa
from bandit.core.issue import * # noqa
from bandit.core.test_properties import * # noqa
__version__ = pbr.version.VersionInfo('bandit').version_string()

View File

@ -1,544 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 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.
r"""
====================================================
Blacklist various Python calls known to be dangerous
====================================================
This blacklist data checks for a number of Python calls known to have possible
security implications. The following blacklist tests are run against any
function calls encoutered in the scanned code base, triggered by encoutering
ast.Call nodes.
B301: pickle
------------
Pickle library appears to be in use, possible security issue.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B301 | pickle | - pickle.loads | Medium |
| | | - pickle.load | |
| | | - pickle.Unpickler | |
| | | - cPickle.loads | |
| | | - cPickle.load | |
| | | - cPickle.Unpickler | |
+------+---------------------+------------------------------------+-----------+
B302: marshal
-------------
Deserialization with the marshal module is possibly dangerous.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B302 | marshal | - marshal.load | Medium |
| | | - marshal.loads | |
+------+---------------------+------------------------------------+-----------+
B303: md5
---------
Use of insecure MD2, MD4, MD5, or SHA1 hash function.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B303 | md5 | - hashlib.md5 | Medium |
| | | - hashlib.sha1 | |
| | | - Crypto.Hash.MD2.new | |
| | | - Crypto.Hash.MD4.new | |
| | | - Crypto.Hash.MD5.new | |
| | | - Crypto.Hash.SHA.new | |
| | | - Cryptodome.Hash.MD2.new | |
| | | - Cryptodome.Hash.MD4.new | |
| | | - Cryptodome.Hash.MD5.new | |
| | | - Cryptodome.Hash.SHA.new | |
| | | - cryptography.hazmat.primitives | |
| | | .hashes.MD5 | |
| | | - cryptography.hazmat.primitives | |
| | | .hashes.SHA1 | |
+------+---------------------+------------------------------------+-----------+
B304 - B305: ciphers and modes
------------------------------
Use of insecure cipher or cipher mode. Replace with a known secure cipher such
as AES.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B304 | ciphers | - Crypto.Cipher.ARC2.new | High |
| | | - Crypto.Cipher.ARC4.new | |
| | | - Crypto.Cipher.Blowfish.new | |
| | | - Crypto.Cipher.DES.new | |
| | | - Crypto.Cipher.XOR.new | |
| | | - Cryptodome.Cipher.ARC2.new | |
| | | - Cryptodome.Cipher.ARC4.new | |
| | | - Cryptodome.Cipher.Blowfish.new | |
| | | - Cryptodome.Cipher.DES.new | |
| | | - Cryptodome.Cipher.XOR.new | |
| | | - cryptography.hazmat.primitives | |
| | | .ciphers.algorithms.ARC4 | |
| | | - cryptography.hazmat.primitives | |
| | | .ciphers.algorithms.Blowfish | |
| | | - cryptography.hazmat.primitives | |
| | | .ciphers.algorithms.IDEA | |
+------+---------------------+------------------------------------+-----------+
| B305 | cipher_modes | - cryptography.hazmat.primitives | Medium |
| | | .ciphers.modes.ECB | |
+------+---------------------+------------------------------------+-----------+
B306: mktemp_q
--------------
Use of insecure and deprecated function (mktemp).
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B306 | mktemp_q | - tempfile.mktemp | Medium |
+------+---------------------+------------------------------------+-----------+
B307: eval
----------
Use of possibly insecure function - consider using safer ast.literal_eval.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B307 | eval | - eval | Medium |
+------+---------------------+------------------------------------+-----------+
B308: mark_safe
---------------
Use of mark_safe() may expose cross-site scripting vulnerabilities and should
be reviewed.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B308 | mark_safe | - django.utils.safestring.mark_safe| Medium |
+------+---------------------+------------------------------------+-----------+
B309: httpsconnection
---------------------
Use of HTTPSConnection on older versions of Python prior to 2.7.9 and 3.4.3 do
not provide security, see https://wiki.openstack.org/wiki/OSSN/OSSN-0033
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B309 | httpsconnection | - httplib.HTTPSConnection | Medium |
| | | - http.client.HTTPSConnection | |
| | | - six.moves.http_client | |
| | | .HTTPSConnection | |
+------+---------------------+------------------------------------+-----------+
B310: urllib_urlopen
--------------------
Audit url open for permitted schemes. Allowing use of 'file:'' or custom
schemes is often unexpected.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B310 | urllib_urlopen | - urllib.urlopen | Medium |
| | | - urllib.request.urlopen | |
| | | - urllib.urlretrieve | |
| | | - urllib.request.urlretrieve | |
| | | - urllib.URLopener | |
| | | - urllib.request.URLopener | |
| | | - urllib.FancyURLopener | |
| | | - urllib.request.FancyURLopener | |
| | | - urllib2.urlopen | |
| | | - urllib2.Request | |
| | | - six.moves.urllib.request.urlopen | |
| | | - six.moves.urllib.request | |
| | | .urlretrieve | |
| | | - six.moves.urllib.request | |
| | | .URLopener | |
| | | - six.moves.urllib.request | |
| | | .FancyURLopener | |
+------+---------------------+------------------------------------+-----------+
B311: random
------------
Standard pseudo-random generators are not suitable for security/cryptographic
purposes.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B311 | random | - random.random | Low |
| | | - random.randrange | |
| | | - random.randint | |
| | | - random.choice | |
| | | - random.uniform | |
| | | - random.triangular | |
+------+---------------------+------------------------------------+-----------+
B312: telnetlib
---------------
Telnet-related functions are being called. Telnet is considered insecure. Use
SSH or some other encrypted protocol.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B312 | telnetlib | - telnetlib.\* | High |
+------+---------------------+------------------------------------+-----------+
B313 - B320: XML
----------------
Most of this is based off of Christian Heimes' work on defusedxml:
https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
Using various XLM methods to parse untrusted XML data is known to be vulnerable
to XML attacks. Methods should be replaced with their defusedxml equivalents.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B313 | xml_bad_cElementTree| - xml.etree.cElementTree.parse | Medium |
| | | - xml.etree.cElementTree.iterparse | |
| | | - xml.etree.cElementTree.fromstring| |
| | | - xml.etree.cElementTree.XMLParser | |
+------+---------------------+------------------------------------+-----------+
| B314 | xml_bad_ElementTree | - xml.etree.ElementTree.parse | Medium |
| | | - xml.etree.ElementTree.iterparse | |
| | | - xml.etree.ElementTree.fromstring | |
| | | - xml.etree.ElementTree.XMLParser | |
+------+---------------------+------------------------------------+-----------+
| B315 | xml_bad_expatreader | - xml.sax.expatreader.create_parser| Medium |
+------+---------------------+------------------------------------+-----------+
| B316 | xml_bad_expatbuilder| - xml.dom.expatbuilder.parse | Medium |
| | | - xml.dom.expatbuilder.parseString | |
+------+---------------------+------------------------------------+-----------+
| B317 | xml_bad_sax | - xml.sax.parse | Medium |
| | | - xml.sax.parseString | |
| | | - xml.sax.make_parser | |
+------+---------------------+------------------------------------+-----------+
| B318 | xml_bad_minidom | - xml.dom.minidom.parse | Medium |
| | | - xml.dom.minidom.parseString | |
+------+---------------------+------------------------------------+-----------+
| B319 | xml_bad_pulldom | - xml.dom.pulldom.parse | Medium |
| | | - xml.dom.pulldom.parseString | |
+------+---------------------+------------------------------------+-----------+
| B320 | xml_bad_etree | - lxml.etree.parse | Medium |
| | | - lxml.etree.fromstring | |
| | | - lxml.etree.RestrictedElement | |
| | | - lxml.etree.GlobalParserTLS | |
| | | - lxml.etree.getDefaultParser | |
| | | - lxml.etree.check_docinfo | |
+------+---------------------+------------------------------------+-----------+
B321: ftplib
------------
FTP-related functions are being called. FTP is considered insecure. Use
SSH/SFTP/SCP or some other encrypted protocol.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B321 | ftplib | - ftplib.\* | High |
+------+---------------------+------------------------------------+-----------+
B322: input
------------
The input method in Python 2 will read from standard input, evaluate and
run the resulting string as python source code. This is similar, though in
many ways worse, then using eval. On Python 2, use raw_input instead, input
is safe in Python 3.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B322 | input | - input | High |
+------+---------------------+------------------------------------+-----------+
B323: unverified_context
------------------------
By default, Python will create a secure, verified ssl context for use in such
classes as HTTPSConnection. However, it still allows using an insecure
context via the _create_unverified_context that reverts to the previous
behavior that does not validate certificates or perform hostname checks.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Calls | Severity |
+======+=====================+====================================+===========+
| B323 | unverified_context | - ssl._create_unverified_context | Medium |
+------+---------------------+------------------------------------+-----------+
"""
from bandit.blacklists import utils
def gen_blacklist():
"""Generate a list of items to blacklist.
Methods of this type, "bandit.blacklist" plugins, are used to build a list
of items that bandit's built in blacklisting tests will use to trigger
issues. They replace the older blacklist* test plugins and allow
blacklisted items to have a unique bandit ID for filtering and profile
usage.
:return: a dictionary mapping node types to a list of blacklist data
"""
sets = []
sets.append(utils.build_conf_dict(
'pickle', 'B301',
['pickle.loads',
'pickle.load',
'pickle.Unpickler',
'cPickle.loads',
'cPickle.load',
'cPickle.Unpickler'],
'Pickle library appears to be in use, possible security issue.'
))
sets.append(utils.build_conf_dict(
'marshal', 'B302', ['marshal.load', 'marshal.loads'],
'Deserialization with the marshal module is possibly dangerous.'
))
sets.append(utils.build_conf_dict(
'md5', 'B303',
['hashlib.md5',
'hashlib.sha1',
'Crypto.Hash.MD2.new',
'Crypto.Hash.MD4.new',
'Crypto.Hash.MD5.new',
'Crypto.Hash.SHA.new',
'Cryptodome.Hash.MD2.new',
'Cryptodome.Hash.MD4.new',
'Cryptodome.Hash.MD5.new',
'Cryptodome.Hash.SHA.new',
'cryptography.hazmat.primitives.hashes.MD5',
'cryptography.hazmat.primitives.hashes.SHA1'],
'Use of insecure MD2, MD4, MD5, or SHA1 hash function.'
))
sets.append(utils.build_conf_dict(
'ciphers', 'B304',
['Crypto.Cipher.ARC2.new',
'Crypto.Cipher.ARC4.new',
'Crypto.Cipher.Blowfish.new',
'Crypto.Cipher.DES.new',
'Crypto.Cipher.XOR.new',
'Cryptodome.Cipher.ARC2.new',
'Cryptodome.Cipher.ARC4.new',
'Cryptodome.Cipher.Blowfish.new',
'Cryptodome.Cipher.DES.new',
'Cryptodome.Cipher.XOR.new',
'cryptography.hazmat.primitives.ciphers.algorithms.ARC4',
'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish',
'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'],
'Use of insecure cipher {name}. Replace with a known secure'
' cipher such as AES.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'cipher_modes', 'B305',
['cryptography.hazmat.primitives.ciphers.modes.ECB'],
'Use of insecure cipher mode {name}.'
))
sets.append(utils.build_conf_dict(
'mktemp_q', 'B306', ['tempfile.mktemp'],
'Use of insecure and deprecated function (mktemp).'
))
sets.append(utils.build_conf_dict(
'eval', 'B307', ['eval'],
'Use of possibly insecure function - consider using safer '
'ast.literal_eval.'
))
sets.append(utils.build_conf_dict(
'mark_safe', 'B308', ['django.utils.safestring.mark_safe'],
'Use of mark_safe() may expose cross-site scripting '
'vulnerabilities and should be reviewed.'
))
sets.append(utils.build_conf_dict(
'httpsconnection', 'B309',
['httplib.HTTPSConnection',
'http.client.HTTPSConnection',
'six.moves.http_client.HTTPSConnection'],
'Use of HTTPSConnection on older versions of Python prior to 2.7.9 '
'and 3.4.3 do not provide security, see '
'https://wiki.openstack.org/wiki/OSSN/OSSN-0033'
))
sets.append(utils.build_conf_dict(
'urllib_urlopen', 'B310',
['urllib.urlopen',
'urllib.request.urlopen',
'urllib.urlretrieve',
'urllib.request.urlretrieve',
'urllib.URLopener',
'urllib.request.URLopener',
'urllib.FancyURLopener',
'urllib.request.FancyURLopener',
'urllib2.urlopen',
'urllib2.Request',
'six.moves.urllib.request.urlopen',
'six.moves.urllib.request.urlretrieve',
'six.moves.urllib.request.URLopener',
'six.moves.urllib.request.FancyURLopener'],
'Audit url open for permitted schemes. Allowing use of file:/ or '
'custom schemes is often unexpected.'
))
sets.append(utils.build_conf_dict(
'random', 'B311',
['random.random',
'random.randrange',
'random.randint',
'random.choice',
'random.uniform',
'random.triangular'],
'Standard pseudo-random generators are not suitable for '
'security/cryptographic purposes.',
'LOW'
))
sets.append(utils.build_conf_dict(
'telnetlib', 'B312', ['telnetlib.*'],
'Telnet-related functions are being called. Telnet is considered '
'insecure. Use SSH or some other encrypted protocol.',
'HIGH'
))
# Most of this is based off of Christian Heimes' work on defusedxml:
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
xml_msg = ('Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {name} with its '
'defusedxml equivalent function or make sure '
'defusedxml.defuse_stdlib() is called')
sets.append(utils.build_conf_dict(
'xml_bad_cElementTree', 'B313',
['xml.etree.cElementTree.parse',
'xml.etree.cElementTree.iterparse',
'xml.etree.cElementTree.fromstring',
'xml.etree.cElementTree.XMLParser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_ElementTree', 'B314',
['xml.etree.ElementTree.parse',
'xml.etree.ElementTree.iterparse',
'xml.etree.ElementTree.fromstring',
'xml.etree.ElementTree.XMLParser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_expatreader', 'B315', ['xml.sax.expatreader.create_parser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_expatbuilder', 'B316',
['xml.dom.expatbuilder.parse',
'xml.dom.expatbuilder.parseString'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_sax', 'B317',
['xml.sax.parse',
'xml.sax.parseString',
'xml.sax.make_parser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_minidom', 'B318',
['xml.dom.minidom.parse',
'xml.dom.minidom.parseString'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_pulldom', 'B319',
['xml.dom.pulldom.parse',
'xml.dom.pulldom.parseString'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_etree', 'B320',
['lxml.etree.parse',
'lxml.etree.fromstring',
'lxml.etree.RestrictedElement',
'lxml.etree.GlobalParserTLS',
'lxml.etree.getDefaultParser',
'lxml.etree.check_docinfo'],
('Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {name} with its '
'defusedxml equivalent function.')
))
# end of XML tests
sets.append(utils.build_conf_dict(
'ftplib', 'B321', ['ftplib.*'],
'FTP-related functions are being called. FTP is considered '
'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'input', 'B322', ['input'],
'The input method in Python 2 will read from standard input, '
'evaluate and run the resulting string as python source code. This '
'is similar, though in many ways worse, then using eval. On Python '
'2, use raw_input instead, input is safe in Python 3.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'unverified_context', 'B323', ['ssl._create_unverified_context'],
'By default, Python will create a secure, verified ssl context for '
'use in such classes as HTTPSConnection. However, it still allows '
'using an insecure context via the _create_unverified_context that '
'reverts to the previous behavior that does not validate certificates '
'or perform hostname checks.'
))
return {'Call': sets}

View File

@ -1,305 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 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.
r"""
======================================================
Blacklist various Python imports known to be dangerous
======================================================
This blacklist data checks for a number of Python modules known to have
possible security implications. The following blacklist tests are run against
any import statements or calls encountered in the scanned code base.
Note that the XML rules listed here are mostly based off of Christian Heimes'
work on defusedxml: https://pypi.python.org/pypi/defusedxml
B401: import_telnetlib
----------------------
A telnet-related module is being imported. Telnet is considered insecure. Use
SSH or some other encrypted protocol.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B401 | import_telnetlib | - telnetlib | high |
+------+---------------------+------------------------------------+-----------+
B402: import_ftplib
-------------------
A FTP-related module is being imported. FTP is considered insecure. Use
SSH/SFTP/SCP or some other encrypted protocol.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B402 | inport_ftplib | - ftplib | high |
+------+---------------------+------------------------------------+-----------+
B403: import_pickle
-------------------
Consider possible security implications associated with these modules.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B403 | import_pickle | - pickle | low |
| | | - cPickle | |
+------+---------------------+------------------------------------+-----------+
B404: import_subprocess
-----------------------
Consider possible security implications associated with these modules.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B404 | import_subprocess | - subprocess | low |
+------+---------------------+------------------------------------+-----------+
B405: import_xml_etree
----------------------
Using various methods to parse untrusted XML data is known to be vulnerable to
XML attacks. Replace vulnerable imports with the equivalent defusedxml package,
or make sure defusedxml.defuse_stdlib() is called.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B405 | import_xml_etree | - xml.etree.cElementTree | low |
| | | - xml.etree.ElementTree | |
+------+---------------------+------------------------------------+-----------+
B406: import_xml_sax
--------------------
Using various methods to parse untrusted XML data is known to be vulnerable to
XML attacks. Replace vulnerable imports with the equivalent defusedxml package,
or make sure defusedxml.defuse_stdlib() is called.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B406 | import_xml_sax | - xml.sax | low |
+------+---------------------+------------------------------------+-----------+
B407: import_xml_expat
----------------------
Using various methods to parse untrusted XML data is known to be vulnerable to
XML attacks. Replace vulnerable imports with the equivalent defusedxml package,
or make sure defusedxml.defuse_stdlib() is called.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B407 | import_xml_expat | - xml.dom.expatbuilder | low |
+------+---------------------+------------------------------------+-----------+
B408: import_xml_minidom
------------------------
Using various methods to parse untrusted XML data is known to be vulnerable to
XML attacks. Replace vulnerable imports with the equivalent defusedxml package,
or make sure defusedxml.defuse_stdlib() is called.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B408 | import_xml_minidom | - xml.dom.minidom | low |
+------+---------------------+------------------------------------+-----------+
B409: import_xml_pulldom
------------------------
Using various methods to parse untrusted XML data is known to be vulnerable to
XML attacks. Replace vulnerable imports with the equivalent defusedxml package,
or make sure defusedxml.defuse_stdlib() is called.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B409 | import_xml_pulldom | - xml.dom.pulldom | low |
+------+---------------------+------------------------------------+-----------+
B410: import_lxml
-----------------
Using various methods to parse untrusted XML data is known to be vulnerable to
XML attacks. Replace vulnerable imports with the equivalent defusedxml package.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B410 | import_lxml | - lxml | low |
+------+---------------------+------------------------------------+-----------+
B411: import_xmlrpclib
----------------------
XMLRPC is particularly dangerous as it is also concerned with communicating
data over a network. Use defused.xmlrpc.monkey_patch() function to monkey-patch
xmlrpclib and mitigate remote XML attacks.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B411 | import_xmlrpclib | - xmlrpclib | high |
+------+---------------------+------------------------------------+-----------+
B412: import_httpoxy
--------------------
httpoxy is a set of vulnerabilities that affect application code running in
CGI, or CGI-like environments. The use of CGI for web applications should be
avoided to prevent this class of attack. More details are available
at https://httpoxy.org/.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B412 | import_httpoxy | - wsgiref.handlers.CGIHandler | high |
| | | - twisted.web.twcgi.CGIScript | |
+------+---------------------+------------------------------------+-----------+
B413: import_pycrypto
---------------------
pycrypto library is known to have publicly disclosed buffer overflow
vulnerability https://github.com/dlitz/pycrypto/issues/176. It is no longer
actively maintained and has been deprecated in favor of pyca/cryptography
library.
+------+---------------------+------------------------------------+-----------+
| ID | Name | Imports | Severity |
+======+=====================+====================================+===========+
| B413 | import_pycrypto | - Crypto.Cipher | high |
| | | - Crypto.Hash | |
| | | - Crypto.IO | |
| | | - Crypto.Protocol | |
| | | - Crypto.PublicKey | |
| | | - Crypto.Random | |
| | | - Crypto.Signature | |
| | | - Crypto.Util | |
+------+---------------------+------------------------------------+-----------+
"""
from bandit.blacklists import utils
def gen_blacklist():
"""Generate a list of items to blacklist.
Methods of this type, "bandit.blacklist" plugins, are used to build a list
of items that bandit's built in blacklisting tests will use to trigger
issues. They replace the older blacklist* test plugins and allow
blacklisted items to have a unique bandit ID for filtering and profile
usage.
:return: a dictionary mapping node types to a list of blacklist data
"""
sets = []
sets.append(utils.build_conf_dict(
'import_telnetlib', 'B401', ['telnetlib'],
'A telnet-related module is being imported. Telnet is '
'considered insecure. Use SSH or some other encrypted protocol.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'import_ftplib', 'B402', ['ftplib'],
'A FTP-related module is being imported. FTP is considered '
'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'import_pickle', 'B403', ['pickle', 'cPickle'],
'Consider possible security implications associated with '
'{name} module.', 'LOW'
))
sets.append(utils.build_conf_dict(
'import_subprocess', 'B404', ['subprocess'],
'Consider possible security implications associated with '
'{name} module.', 'LOW'
))
# Most of this is based off of Christian Heimes' work on defusedxml:
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
xml_msg = ('Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {name} with the equivalent '
'defusedxml package, or make sure defusedxml.defuse_stdlib() '
'is called.')
lxml_msg = ('Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {name} with the '
'equivalent defusedxml package.')
sets.append(utils.build_conf_dict(
'import_xml_etree', 'B405',
['xml.etree.cElementTree', 'xml.etree.ElementTree'], xml_msg, 'LOW'))
sets.append(utils.build_conf_dict(
'import_xml_sax', 'B406', ['xml.sax'], xml_msg, 'LOW'))
sets.append(utils.build_conf_dict(
'import_xml_expat', 'B407', ['xml.dom.expatbuilder'], xml_msg, 'LOW'))
sets.append(utils.build_conf_dict(
'import_xml_minidom', 'B408', ['xml.dom.minidom'], xml_msg, 'LOW'))
sets.append(utils.build_conf_dict(
'import_xml_pulldom', 'B409', ['xml.dom.pulldom'], xml_msg, 'LOW'))
sets.append(utils.build_conf_dict(
'import_lxml', 'B410', ['lxml'], lxml_msg, 'LOW'))
sets.append(utils.build_conf_dict(
'import_xmlrpclib', 'B411', ['xmlrpclib'],
'Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() '
'function to monkey-patch xmlrpclib and mitigate XML '
'vulnerabilities.', 'HIGH'))
sets.append(utils.build_conf_dict(
'import_httpoxy', 'B412',
['wsgiref.handlers.CGIHandler', 'twisted.web.twcgi.CGIScript',
'twisted.web.twcgi.CGIDirectory'],
'Consider possible security implications associated with '
'{name} module.', 'HIGH'
))
sets.append(utils.build_conf_dict(
'import_pycrypto', 'B413',
['Crypto.Cipher',
'Crypto.Hash',
'Crypto.IO',
'Crypto.Protocol',
'Crypto.PublicKey',
'Crypto.Random',
'Crypto.Signature',
'Crypto.Util'],
'The pyCrypto library and its module {name} are no longer actively '
'maintained and have been deprecated. '
'Consider using pyca/cryptography library.', 'HIGH'))
return {'Import': sets, 'ImportFrom': sets, 'Call': sets}

View File

@ -1,22 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 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.
def build_conf_dict(name, bid, qualnames, message, level='MEDIUM'):
"""Build and return a blacklist configuration dict."""
return {'name': name, 'id': bid, 'message': message,
'qualnames': qualnames, 'level': level}

View File

View File

@ -1,224 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2015 Hewlett-Packard Enterprise
#
# 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.
# #############################################################################
# Bandit Baseline is a tool that runs Bandit against a Git commit, and compares
# the current commit findings to the parent commit findings.
# To do this it checks out the parent commit, runs Bandit (with any provided
# filters or profiles), checks out the current commit, runs Bandit, and then
# reports on any new findings.
# #############################################################################
import argparse
import contextlib
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import git
bandit_args = sys.argv[1:]
baseline_tmp_file = '_bandit_baseline_run.json_'
current_commit = None
default_output_format = 'terminal'
LOG = logging.getLogger(__name__)
repo = None
report_basename = 'bandit_baseline_result'
valid_baseline_formats = ['txt', 'html', 'json']
def main():
# our cleanup function needs this and can't be passed arguments
global current_commit
global repo
parent_commit = None
output_format = None
repo = None
report_fname = None
init_logger()
output_format, repo, report_fname = initialize()
if not repo:
sys.exit(2)
# #################### Find current and parent commits ####################
try:
commit = repo.commit()
current_commit = commit.hexsha
LOG.info('Got current commit: [%s]', commit.name_rev)
commit = commit.parents[0]
parent_commit = commit.hexsha
LOG.info('Got parent commit: [%s]', commit.name_rev)
except git.GitCommandError:
LOG.error("Unable to get current or parent commit")
sys.exit(2)
except IndexError:
LOG.error("Parent commit not available")
sys.exit(2)
# #################### Run Bandit against both commits ####################
output_type = (['-f', 'txt'] if output_format == default_output_format
else ['-o', report_fname])
with baseline_setup() as t:
bandit_tmpfile = "{}/{}".format(t, baseline_tmp_file)
steps = [{'message': 'Getting Bandit baseline results',
'commit': parent_commit,
'args': bandit_args + ['-f', 'json', '-o', bandit_tmpfile]},
{'message': 'Comparing Bandit results to baseline',
'commit': current_commit,
'args': bandit_args + ['-b', bandit_tmpfile] + output_type}]
return_code = None
for step in steps:
repo.head.reset(commit=step['commit'], working_tree=True)
LOG.info(step['message'])
bandit_command = ['bandit'] + step['args']
try:
output = subprocess.check_output(bandit_command)
except subprocess.CalledProcessError as e:
output = e.output
return_code = e.returncode
else:
return_code = 0
output = output.decode('utf-8') # subprocess returns bytes
if return_code not in [0, 1]:
LOG.error("Error running command: %s\nOutput: %s\n",
bandit_args, output)
# #################### Output and exit ####################################
# print output or display message about written report
if output_format == default_output_format:
print(output)
else:
LOG.info("Successfully wrote %s", report_fname)
# exit with the code the last Bandit run returned
sys.exit(return_code)
# #################### Clean up before exit ###################################
@contextlib.contextmanager
def baseline_setup():
d = tempfile.mkdtemp()
yield d
shutil.rmtree(d, True)
if repo:
repo.head.reset(commit=current_commit, working_tree=True)
# #################### Setup logging ##########################################
def init_logger():
LOG.handlers = []
log_level = logging.INFO
log_format_string = "[%(levelname)7s ] %(message)s"
logging.captureWarnings(True)
LOG.setLevel(log_level)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(log_format_string))
LOG.addHandler(handler)
# #################### Perform initialization and validate assumptions ########
def initialize():
valid = True
# #################### Parse Args #########################################
parser = argparse.ArgumentParser(
description='Bandit Baseline - Generates Bandit results compared to "'
'a baseline',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='Additional Bandit arguments such as severity filtering (-ll) '
'can be added and will be passed to Bandit.'
)
parser.add_argument('targets', metavar='targets', type=str, nargs='+',
help='source file(s) or directory(s) to be tested')
parser.add_argument('-f', dest='output_format', action='store',
default='terminal', help='specify output format',
choices=valid_baseline_formats)
args, unknown = parser.parse_known_args()
# #################### Setup Output #######################################
# set the output format, or use a default if not provided
output_format = (args.output_format if args.output_format
else default_output_format)
if output_format == default_output_format:
LOG.info("No output format specified, using %s", default_output_format)
# set the report name based on the output format
report_fname = "{}.{}".format(report_basename, output_format)
# #################### Check Requirements #################################
try:
repo = git.Repo(os.getcwd())
except git.exc.InvalidGitRepositoryError:
LOG.error("Bandit baseline must be called from a git project root")
valid = False
except git.exc.GitCommandNotFound:
LOG.error("Git command not found")
valid = False
else:
if repo.is_dirty():
LOG.error("Current working directory is dirty and must be "
"resolved")
valid = False
# if output format is specified, we need to be able to write the report
if output_format != default_output_format and os.path.exists(report_fname):
LOG.error("File %s already exists, aborting", report_fname)
valid = False
# Bandit needs to be able to create this temp file
if os.path.exists(baseline_tmp_file):
LOG.error("Temporary file %s needs to be removed prior to running",
baseline_tmp_file)
valid = False
# we must validate -o is not provided, as it will mess up Bandit baseline
if '-o' in bandit_args:
LOG.error("Bandit baseline must not be called with the -o option")
valid = False
return (output_format, repo, report_fname) if valid else (None, None, None)
if __name__ == '__main__':
main()

View File

@ -1,186 +0,0 @@
# Copyright 2015 Red Hat Inc.
#
# 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.
from __future__ import print_function
import argparse
import importlib
import logging
import os
import sys
import yaml
from bandit.core import extension_loader
PROG_NAME = 'bandit_conf_generator'
LOG = logging.getLogger(__name__)
template = """
### Bandit config file generated from:
# '{cli}'
### This config may optionally select a subset of tests to run or skip by
### filling out the 'tests' and 'skips' lists given below. If no tests are
### specified for inclusion then it is assumed all tests are desired. The skips
### set will remove specific tests from the include set. This can be controlled
### using the -t/-s CLI options. Note that the same test ID should not appear
### in both 'tests' and 'skips', this would be nonsensical and is detected by
### Bandit at runtime.
# Available tests:
{test_list}
# (optional) list included test IDs here, eg '[B101, B406]':
{test}
# (optional) list skipped test IDs here, eg '[B101, B406]':
{skip}
### (optional) plugin settings - some test plugins require configuration data
### that may be given here, per-plugin. All bandit test plugins have a built in
### set of sensible defaults and these will be used if no configuration is
### provided. It is not necessary to provide settings for every (or any) plugin
### if the defaults are acceptable.
{settings}
"""
def init_logger():
LOG.handlers = []
log_level = logging.INFO
log_format_string = "[%(levelname)5s]: %(message)s"
logging.captureWarnings(True)
LOG.setLevel(log_level)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(log_format_string))
LOG.addHandler(handler)
def parse_args():
help_description = """Bandit Config Generator
This tool is used to generate an optional profile. The profile may be used
to include or skip tests and override values for plugins.
When used to store an output profile, this tool will output a template that
includes all plugins and their default settings. Any settings which aren't
being overridden can be safely removed from the profile and default values
will be used. Bandit will prefer settings from the profile over the built
in values."""
parser = argparse.ArgumentParser(
description=help_description,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--show-defaults', dest='show_defaults',
action='store_true',
help='show the default settings values for each '
'plugin but do not output a profile')
parser.add_argument('-o', '--out', dest='output_file',
action='store',
help='output file to save profile')
parser.add_argument(
'-t', '--tests', dest='tests',
action='store', default=None, type=str,
help='list of test names to run')
parser.add_argument(
'-s', '--skip', dest='skips',
action='store', default=None, type=str,
help='list of test names to skip')
args = parser.parse_args()
if not args.output_file and not args.show_defaults:
parser.print_help()
parser.exit(1)
return args
def get_config_settings():
config = {}
for plugin in extension_loader.MANAGER.plugins:
fn_name = plugin.name
function = plugin.plugin
# if a function takes config...
if hasattr(function, '_takes_config'):
fn_module = importlib.import_module(function.__module__)
# call the config generator if it exists
if hasattr(fn_module, 'gen_config'):
config[fn_name] = fn_module.gen_config(function._takes_config)
return yaml.safe_dump(config, default_flow_style=False)
def main():
init_logger()
args = parse_args()
yaml_settings = get_config_settings()
if args.show_defaults:
print(yaml_settings)
if args.output_file:
if os.path.exists(os.path.abspath(args.output_file)):
LOG.error("File %s already exists, exiting", args.output_file)
sys.exit(2)
try:
with open(args.output_file, 'w') as f:
skips = args.skips.split(',') if args.skips else []
tests = args.tests.split(',') if args.tests else []
for skip in skips:
if not extension_loader.MANAGER.check_id(skip):
raise RuntimeError('unknown ID in skips: %s' % skip)
for test in tests:
if not extension_loader.MANAGER.check_id(test):
raise RuntimeError('unknown ID in tests: %s' % test)
tpl = "# {0} : {1}"
test_list = [tpl.format(t.plugin._test_id, t.name)
for t in extension_loader.MANAGER.plugins]
others = [tpl.format(k, v['name']) for k, v in (
extension_loader.MANAGER.blacklist_by_id.items())]
test_list.extend(others)
test_list.sort()
contents = template.format(
cli=" ".join(sys.argv),
settings=yaml_settings,
test_list="\n".join(test_list),
skip='skips: ' + str(skips) if skips else 'skips:',
test='tests: ' + str(tests) if tests else 'tests:')
f.write(contents)
except IOError:
LOG.error("Unable to open %s for writing", args.output_file)
except Exception as e:
LOG.error("Error: %s", e)
else:
LOG.info("Successfully wrote profile: %s", args.output_file)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,401 +0,0 @@
# -*- 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 argparse
import fnmatch
import logging
import os
import sys
import textwrap
import bandit
from bandit.core import config as b_config
from bandit.core import constants
from bandit.core import manager as b_manager
from bandit.core import utils
BASE_CONFIG = 'bandit.yaml'
LOG = logging.getLogger()
def _init_logger(debug=False, log_format=None):
'''Initialize the logger
:param debug: Whether to enable debug mode
:return: An instantiated logging instance
'''
LOG.handlers = []
log_level = logging.INFO
if debug:
log_level = logging.DEBUG
if not log_format:
# default log format
log_format_string = constants.log_format_string
else:
log_format_string = log_format
logging.captureWarnings(True)
LOG.setLevel(log_level)
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter(log_format_string))
LOG.addHandler(handler)
LOG.debug("logging initialized")
def _get_options_from_ini(ini_path, target):
"""Return a dictionary of config options or None if we can't load any."""
ini_file = None
if ini_path:
ini_file = ini_path
else:
bandit_files = []
for t in target:
for root, dirnames, filenames in os.walk(t):
for filename in fnmatch.filter(filenames, '.bandit'):
bandit_files.append(os.path.join(root, filename))
if len(bandit_files) > 1:
LOG.error('Multiple .bandit files found - scan separately or '
'choose one with --ini\n\t%s', ', '.join(bandit_files))
sys.exit(2)
elif len(bandit_files) == 1:
ini_file = bandit_files[0]
LOG.info('Found project level .bandit file: %s', bandit_files[0])
if ini_file:
return utils.parse_ini_file(ini_file)
else:
return None
def _init_extensions():
from bandit.core import extension_loader as ext_loader
return ext_loader.MANAGER
def _log_option_source(arg_val, ini_val, option_name):
"""It's useful to show the source of each option."""
if arg_val:
LOG.info("Using command line arg for %s", option_name)
return arg_val
elif ini_val:
LOG.info("Using ini file for %s", option_name)
return ini_val
else:
return None
def _running_under_virtualenv():
if hasattr(sys, 'real_prefix'):
return True
elif sys.prefix != getattr(sys, 'base_prefix', sys.prefix):
return True
def _get_profile(config, profile_name, config_path):
profile = {}
if profile_name:
profiles = config.get_option('profiles') or {}
profile = profiles.get(profile_name)
if profile is None:
raise utils.ProfileNotFound(config_path, profile_name)
LOG.debug("read in legacy profile '%s': %s", profile_name, profile)
else:
profile['include'] = set(config.get_option('tests') or [])
profile['exclude'] = set(config.get_option('skips') or [])
return profile
def _log_info(args, profile):
inc = ",".join([t for t in profile['include']]) or "None"
exc = ",".join([t for t in profile['exclude']]) or "None"
LOG.info("profile include tests: %s", inc)
LOG.info("profile exclude tests: %s", exc)
LOG.info("cli include tests: %s", args.tests)
LOG.info("cli exclude tests: %s", args.skips)
def main():
# bring our logging stuff up as early as possible
debug = ('-d' in sys.argv or '--debug' in sys.argv)
_init_logger(debug)
extension_mgr = _init_extensions()
baseline_formatters = [f.name for f in filter(lambda x:
hasattr(x.plugin,
'_accepts_baseline'),
extension_mgr.formatters)]
# now do normal startup
parser = argparse.ArgumentParser(
description='Bandit - a Python source code security analyzer',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'targets', metavar='targets', type=str, nargs='*',
help='source file(s) or directory(s) to be tested'
)
parser.add_argument(
'-r', '--recursive', dest='recursive',
action='store_true', help='find and process files in subdirectories'
)
parser.add_argument(
'-a', '--aggregate', dest='agg_type',
action='store', default='file', type=str,
choices=['file', 'vuln'],
help='aggregate output by vulnerability (default) or by filename'
)
parser.add_argument(
'-n', '--number', dest='context_lines',
action='store', default=3, type=int,
help='maximum number of code lines to output for each issue'
)
parser.add_argument(
'-c', '--configfile', dest='config_file',
action='store', default=None, type=str,
help='optional config file to use for selecting plugins and '
'overriding defaults'
)
parser.add_argument(
'-p', '--profile', dest='profile',
action='store', default=None, type=str,
help='profile to use (defaults to executing all tests)'
)
parser.add_argument(
'-t', '--tests', dest='tests',
action='store', default=None, type=str,
help='comma-separated list of test IDs to run'
)
parser.add_argument(
'-s', '--skip', dest='skips',
action='store', default=None, type=str,
help='comma-separated list of test IDs to skip'
)
parser.add_argument(
'-l', '--level', dest='severity', action='count',
default=1, help='report only issues of a given severity level or '
'higher (-l for LOW, -ll for MEDIUM, -lll for HIGH)'
)
parser.add_argument(
'-i', '--confidence', dest='confidence', action='count',
default=1, help='report only issues of a given confidence level or '
'higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)'
)
output_format = 'screen' if sys.stdout.isatty() else 'txt'
parser.add_argument(
'-f', '--format', dest='output_format', action='store',
default=output_format, help='specify output format',
choices=sorted(extension_mgr.formatter_names)
)
parser.add_argument(
'--msg-template', action='store',
default=None, help='specify output message template'
' (only usable with --format custom),'
' see CUSTOM FORMAT section'
' for list of available values',
)
parser.add_argument(
'-o', '--output', dest='output_file', action='store', nargs='?',
type=argparse.FileType('w'), default=sys.stdout,
help='write report to filename'
)
parser.add_argument(
'-v', '--verbose', dest='verbose', action='store_true',
help='output extra information like excluded and included files'
)
parser.add_argument(
'-d', '--debug', dest='debug', action='store_true',
help='turn on debug mode'
)
parser.add_argument(
'--ignore-nosec', dest='ignore_nosec', action='store_true',
help='do not skip lines with # nosec comments'
)
parser.add_argument(
'-x', '--exclude', dest='excluded_paths', action='store',
default='', help='comma-separated list of paths to exclude from scan '
'(note that these are in addition to the excluded '
'paths provided in the config file)'
)
parser.add_argument(
'-b', '--baseline', dest='baseline', action='store',
default=None, help='path of a baseline report to compare against '
'(only JSON-formatted files are accepted)'
)
parser.add_argument(
'--ini', dest='ini_path', action='store', default=None,
help='path to a .bandit file that supplies command line arguments'
)
parser.add_argument(
'--version', action='version',
version='%(prog)s {version}'.format(version=bandit.__version__)
)
parser.set_defaults(debug=False)
parser.set_defaults(verbose=False)
parser.set_defaults(ignore_nosec=False)
plugin_info = ["%s\t%s" % (a[0], a[1].name) for a in
extension_mgr.plugins_by_id.items()]
blacklist_info = []
for a in extension_mgr.blacklist.items():
for b in a[1]:
blacklist_info.append('%s\t%s' % (b['id'], b['name']))
plugin_list = '\n\t'.join(sorted(set(plugin_info + blacklist_info)))
dedent_text = textwrap.dedent('''
CUSTOM FORMATTING
-----------------
Available tags:
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template:
bandit -r examples/ --format custom --msg-template \\
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as:
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style:
bandit -r examples/ --format custom --msg-template \\
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
The following tests were discovered and loaded:
-----------------------------------------------
''')
parser.epilog = dedent_text + "\t{0}".format(plugin_list)
# setup work - parse arguments, and initialize BanditManager
args = parser.parse_args()
# Check if `--msg-template` is not present without custom formatter
if args.output_format != 'custom' and args.msg_template is not None:
parser.error("--msg-template can only be used with --format=custom")
try:
b_conf = b_config.BanditConfig(config_file=args.config_file)
except utils.ConfigError as e:
LOG.error(e)
sys.exit(2)
# Handle .bandit files in projects to pass cmdline args from file
ini_options = _get_options_from_ini(args.ini_path, args.targets)
if ini_options:
# prefer command line, then ini file
args.excluded_paths = _log_option_source(args.excluded_paths,
ini_options.get('exclude'),
'excluded paths')
args.skips = _log_option_source(args.skips, ini_options.get('skips'),
'skipped tests')
args.tests = _log_option_source(args.tests, ini_options.get('tests'),
'selected tests')
ini_targets = ini_options.get('targets')
if ini_targets:
ini_targets = ini_targets.split(',')
args.targets = _log_option_source(args.targets, ini_targets,
'selected targets')
# TODO(tmcpeak): any other useful options to pass from .bandit?
if not args.targets:
LOG.error("No targets found in CLI or ini files, exiting.")
sys.exit(2)
# if the log format string was set in the options, reinitialize
if b_conf.get_option('log_format'):
log_format = b_conf.get_option('log_format')
_init_logger(debug, log_format=log_format)
try:
profile = _get_profile(b_conf, args.profile, args.config_file)
_log_info(args, profile)
profile['include'].update(args.tests.split(',') if args.tests else [])
profile['exclude'].update(args.skips.split(',') if args.skips else [])
extension_mgr.validate_profile(profile)
except (utils.ProfileNotFound, ValueError) as e:
LOG.error(e)
sys.exit(2)
b_mgr = b_manager.BanditManager(b_conf, args.agg_type, args.debug,
profile=profile, verbose=args.verbose,
ignore_nosec=args.ignore_nosec)
if args.baseline is not None:
try:
with open(args.baseline) as bl:
data = bl.read()
b_mgr.populate_baseline(data)
except IOError:
LOG.warning("Could not open baseline report: %s", args.baseline)
sys.exit(2)
if args.output_format not in baseline_formatters:
LOG.warning('Baseline must be used with one of the following '
'formats: ' + str(baseline_formatters))
sys.exit(2)
if args.output_format != "json":
if args.config_file:
LOG.info("using config: %s", args.config_file)
LOG.info("running on Python %d.%d.%d", sys.version_info.major,
sys.version_info.minor, sys.version_info.micro)
# initiate file discovery step within Bandit Manager
b_mgr.discover_files(args.targets, args.recursive, args.excluded_paths)
if not b_mgr.b_ts.tests:
LOG.error('No tests would be run, please check the profile.')
sys.exit(2)
# initiate execution of tests within Bandit Manager
b_mgr.run_tests()
LOG.debug(b_mgr.b_ma)
LOG.debug(b_mgr.metrics)
# trigger output of results by Bandit Manager
sev_level = constants.RANKING[args.severity - 1]
conf_level = constants.RANKING[args.confidence - 1]
b_mgr.output_results(args.context_lines,
sev_level,
conf_level,
args.output_file,
args.output_format,
args.msg_template)
# return an exit code of 1 if there are results, 0 otherwise
if b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0:
sys.exit(1)
else:
sys.exit(0)
if __name__ == '__main__':
main()

View File

@ -1,27 +0,0 @@
# -*- 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.
from bandit.core import config # noqa
from bandit.core import context # noqa
from bandit.core import manager # noqa
from bandit.core import meta_ast # noqa
from bandit.core import node_visitor # noqa
from bandit.core import test_set # noqa
from bandit.core import tester # noqa
from bandit.core import utils # noqa
from bandit.core.constants import * # noqa
from bandit.core.issue import * # noqa
from bandit.core.test_properties import * # noqa

View File

@ -1,75 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 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 fnmatch
from bandit.core import issue
def report_issue(check, name):
return issue.Issue(
severity=check.get('level', 'MEDIUM'), confidence='HIGH',
text=check['message'].replace('{name}', name),
ident=name, test_id=check.get("id", 'LEGACY'))
def blacklist(context, config):
"""Generic blacklist test, B001.
This generic blacklist test will be called for any encountered node with
defined blacklist data available. This data is loaded via plugins using
the 'bandit.blacklists' entry point. Please see the documentation for more
details. Each blacklist datum has a unique bandit ID that may be used for
filtering purposes, or alternatively all blacklisting can be filtered using
the id of this built in test, 'B001'.
"""
blacklists = config
node_type = context.node.__class__.__name__
if node_type == 'Call':
func = context.node.func
if isinstance(func, ast.Name) and func.id == '__import__':
if len(context.node.args):
if isinstance(context.node.args[0], ast.Str):
name = context.node.args[0].s
else:
# TODO(??): import through a variable, need symbol tab
name = "UNKNOWN"
else:
name = "" # handle '__import__()'
else:
name = context.call_function_name_qual
# In the case the Call is an importlib.import, treat the first
# argument name as an actual import module name.
if name in ["importlib.import_module", "importlib.__import__"]:
name = context.call_args[0]
for check in blacklists[node_type]:
for qn in check['qualnames']:
if fnmatch.fnmatch(name, qn):
return report_issue(check, name)
if node_type.startswith('Import'):
prefix = ""
if node_type == "ImportFrom":
if context.node.module is not None:
prefix = context.node.module + "."
for check in blacklists[node_type]:
for name in context.node.names:
for qn in check['qualnames']:
if (prefix + name.name).startswith(qn):
return report_issue(check, name.name)

View File

@ -1,243 +0,0 @@
# -*- 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 logging
import yaml
from bandit.core import constants
from bandit.core import extension_loader
from bandit.core import utils
LOG = logging.getLogger(__name__)
class BanditConfig(object):
def __init__(self, config_file=None):
'''Attempt to initialize a config dictionary from a yaml file.
Error out if loading the yaml file fails for any reason.
:param config_file: The Bandit yaml config file
:raises bandit.utils.ConfigError: If the config is invalid or
unreadable.
'''
self.config_file = config_file
self._config = {}
if config_file:
try:
f = open(config_file, 'r')
except IOError:
raise utils.ConfigError("Could not read config file.",
config_file)
try:
self._config = yaml.safe_load(f)
self.validate(config_file)
except yaml.YAMLError as err:
LOG.error(err)
raise utils.ConfigError("Error parsing file.", config_file)
# valid config must be a dict
if not isinstance(self._config, dict):
raise utils.ConfigError("Error parsing file.", config_file)
self.convert_legacy_config()
else:
# use sane defaults
self._config['plugin_name_pattern'] = '*.py'
self._config['include'] = ['*.py', '*.pyw']
self._init_settings()
def get_option(self, option_string):
'''Returns the option from the config specified by the option_string.
'.' can be used to denote levels, for example to retrieve the options
from the 'a' profile you can use 'profiles.a'
:param option_string: The string specifying the option to retrieve
:return: The object specified by the option_string, or None if it can't
be found.
'''
option_levels = option_string.split('.')
cur_item = self._config
for level in option_levels:
if cur_item and (level in cur_item):
cur_item = cur_item[level]
else:
return None
return cur_item
def get_setting(self, setting_name):
if setting_name in self._settings:
return self._settings[setting_name]
else:
return None
@property
def config(self):
'''Property to return the config dictionary
:return: Config dictionary
'''
return self._config
def _init_settings(self):
'''This function calls a set of other functions (one per setting)
This function calls a set of other functions (one per setting) to build
out the _settings dictionary. Each other function will set values from
the config (if set), otherwise use defaults (from constants if
possible).
:return: -
'''
self._settings = {}
self._init_plugin_name_pattern()
def _init_plugin_name_pattern(self):
'''Sets settings['plugin_name_pattern'] from default or config file.'''
plugin_name_pattern = constants.plugin_name_pattern
if self.get_option('plugin_name_pattern'):
plugin_name_pattern = self.get_option('plugin_name_pattern')
self._settings['plugin_name_pattern'] = plugin_name_pattern
def convert_legacy_config(self):
updated_profiles = self.convert_names_to_ids()
bad_calls, bad_imports = self.convert_legacy_blacklist_data()
if updated_profiles:
self.convert_legacy_blacklist_tests(updated_profiles,
bad_calls, bad_imports)
self._config['profiles'] = updated_profiles
def convert_names_to_ids(self):
'''Convert test names to IDs, unknown names are left unchanged.'''
extman = extension_loader.MANAGER
updated_profiles = {}
for name, profile in (self.get_option('profiles') or {}).items():
# NOTE(tkelsey): can't use default of get() because value is
# sometimes explicity 'None', for example when the list if given in
# yaml but not populated with any values.
include = set((extman.get_plugin_id(i) or i)
for i in (profile.get('include') or []))
exclude = set((extman.get_plugin_id(i) or i)
for i in (profile.get('exclude') or []))
updated_profiles[name] = {'include': include, 'exclude': exclude}
return updated_profiles
def convert_legacy_blacklist_data(self):
'''Detect legacy blacklist data and convert it to new format.'''
bad_calls_list = []
bad_imports_list = []
bad_calls = self.get_option('blacklist_calls') or {}
bad_calls = bad_calls.get('bad_name_sets', {})
for item in bad_calls:
for key, val in item.items():
val['name'] = key
val['message'] = val['message'].replace('{func}', '{name}')
bad_calls_list.append(val)
bad_imports = self.get_option('blacklist_imports') or {}
bad_imports = bad_imports.get('bad_import_sets', {})
for item in bad_imports:
for key, val in item.items():
val['name'] = key
val['message'] = val['message'].replace('{module}', '{name}')
val['qualnames'] = val['imports']
del val['imports']
bad_imports_list.append(val)
if bad_imports_list or bad_calls_list:
LOG.warning('Legacy blacklist data found in config, overriding '
'data plugins')
return bad_calls_list, bad_imports_list
@staticmethod
def convert_legacy_blacklist_tests(profiles, bad_imports, bad_calls):
'''Detect old blacklist tests, convert to use new builtin.'''
def _clean_set(name, data):
if name in data:
data.remove(name)
data.add('B001')
for name, profile in profiles.items():
blacklist = {}
include = profile['include']
exclude = profile['exclude']
name = 'blacklist_calls'
if name in include and name not in exclude:
blacklist.setdefault('Call', []).extend(bad_calls)
_clean_set(name, include)
_clean_set(name, exclude)
name = 'blacklist_imports'
if name in include and name not in exclude:
blacklist.setdefault('Import', []).extend(bad_imports)
blacklist.setdefault('ImportFrom', []).extend(bad_imports)
blacklist.setdefault('Call', []).extend(bad_imports)
_clean_set(name, include)
_clean_set(name, exclude)
_clean_set('blacklist_import_func', include)
_clean_set('blacklist_import_func', exclude)
# This can happen with a legacy config that includes
# blacklist_calls but exclude blacklist_imports for example
if 'B001' in include and 'B001' in exclude:
exclude.remove('B001')
profile['blacklist'] = blacklist
def validate(self, path):
'''Validate the config data.'''
legacy = False
message = ("Config file has an include or exclude reference "
"to legacy test '{0}' but no configuration data for "
"it. Configuration data is required for this test. "
"Please consider switching to the new config file "
"format, the tool 'bandit-config-generator' can help "
"you with this.")
def _test(key, block, exclude, include):
if key in exclude or key in include:
if self._config.get(block) is None:
raise utils.ConfigError(message.format(key), path)
if 'profiles' in self._config:
legacy = True
for profile in self._config['profiles'].values():
inc = profile.get('include') or set()
exc = profile.get('exclude') or set()
_test('blacklist_imports', 'blacklist_imports', inc, exc)
_test('blacklist_import_func', 'blacklist_imports', inc, exc)
_test('blacklist_calls', 'blacklist_calls', inc, exc)
# show deprecation message
if legacy:
LOG.warning("Config file '%s' contains deprecated legacy config "
"data. Please consider upgrading to the new config "
"format. The tool 'bandit-config-generator' can help "
"you with this. Support for legacy configs will be "
"removed in a future bandit version.", path)

View File

@ -1,42 +0,0 @@
# -*- 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.
# default plugin name pattern
plugin_name_pattern = '*.py'
# default progress increment
progress_increment = 50
RANKING = ['UNDEFINED', 'LOW', 'MEDIUM', 'HIGH']
RANKING_VALUES = {'UNDEFINED': 1, 'LOW': 3, 'MEDIUM': 5, 'HIGH': 10}
CRITERIA = [('SEVERITY', 'UNDEFINED'), ('CONFIDENCE', 'UNDEFINED')]
# add each ranking to globals, to allow direct access in module name space
for rank in RANKING:
globals()[rank] = rank
CONFIDENCE_DEFAULT = 'UNDEFINED'
# A list of values Python considers to be False.
# These can be useful in tests to check if a value is True or False.
# We don't handle the case of user-defined classes being false.
# These are only useful when we have a constant in code. If we
# have a variable we cannot determine if False.
# See https://docs.python.org/2/library/stdtypes.html#truth-value-testing
FALSE_VALUES = [None, False, 'False', 0, 0.0, 0j, '', (), [], {}]
# override with "log_format" option in config file
log_format_string = '[%(module)s]\t%(levelname)s\t%(message)s'

View File

@ -1,339 +0,0 @@
# -*- 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 six
from bandit.core import utils
class Context(object):
def __init__(self, context_object=None):
'''Initialize the class with a context, empty dict otherwise
:param context_object: The context object to create class from
:return: -
'''
if context_object is not None:
self._context = context_object
else:
self._context = dict()
def __repr__(self):
'''Generate representation of object for printing / interactive use
Most likely only interested in non-default properties, so we return
the string version of _context.
Example string returned:
<Context {'node': <_ast.Call object at 0x110252510>, 'function': None,
'name': 'socket', 'imports': set(['socket']), 'module': None,
'filename': 'examples/binding.py',
'call': <_ast.Call object at 0x110252510>, 'lineno': 3,
'import_aliases': {}, 'qualname': 'socket.socket'}>
:return: A string representation of the object
'''
return "<Context %s>" % self._context
@property
def call_args(self):
'''Get a list of function args
:return: A list of function args
'''
args = []
for arg in self._context['call'].args:
if hasattr(arg, 'attr'):
args.append(arg.attr)
else:
args.append(self._get_literal_value(arg))
return args
@property
def call_args_count(self):
'''Get the number of args a function call has
:return: The number of args a function call has
'''
if 'call' in self._context and hasattr(self._context['call'], 'args'):
return len(self._context['call'].args)
else:
return None
@property
def call_function_name(self):
'''Get the name (not FQ) of a function call
:return: The name (not FQ) of a function call
'''
if 'name' in self._context:
return self._context['name']
else:
return None
@property
def call_function_name_qual(self):
'''Get the FQ name of a function call
:return: The FQ name of a function call
'''
if 'qualname' in self._context:
return self._context['qualname']
else:
return None
@property
def call_keywords(self):
'''Get a dictionary of keyword parameters
:return: A dictionary of keyword parameters for a call as strings
'''
if ('call' in self._context and
hasattr(self._context['call'], 'keywords')):
return_dict = {}
for li in self._context['call'].keywords:
if hasattr(li.value, 'attr'):
return_dict[li.arg] = li.value.attr
else:
return_dict[li.arg] = self._get_literal_value(li.value)
return return_dict
else:
return None
@property
def node(self):
'''Get the raw AST node associated with the context
:return: The raw AST node associated with the context
'''
if 'node' in self._context:
return self._context['node']
else:
return None
@property
def string_val(self):
'''Get the value of a standalone unicode or string object
:return: value of a standalone unicode or string object
'''
if 'str' in self._context:
return self._context['str']
else:
return None
@property
def bytes_val(self):
'''Get the value of a standalone bytes object (py3 only)
:return: value of a standalone bytes object
'''
return self._context.get('bytes')
@property
def string_val_as_escaped_bytes(self):
'''Get escaped value of the object.
Turn the value of a string or bytes object into byte sequence with
unknown, control, and \\ characters escaped.
This function should be used when looking for a known sequence in a
potentially badly encoded string in the code.
:return: sequence of printable ascii bytes representing original string
'''
val = self.string_val
if val is not None:
# it's any of str or unicode in py2, or str in py3
return val.encode('unicode_escape')
val = self.bytes_val
if val is not None:
return utils.escaped_bytes_representation(val)
return None
@property
def statement(self):
'''Get the raw AST for the current statement
:return: The raw AST for the current statement
'''
if 'statement' in self._context:
return self._context['statement']
else:
return None
@property
def function_def_defaults_qual(self):
'''Get a list of fully qualified default values in a function def
:return: List of defaults
'''
defaults = []
if 'node' in self._context:
for default in self._context['node'].args.defaults:
defaults.append(utils.get_qual_attr(
default,
self._context['import_aliases']))
return defaults
def _get_literal_value(self, literal):
'''Utility function to turn AST literals into native Python types
:param literal: The AST literal to convert
:return: The value of the AST literal
'''
if isinstance(literal, _ast.Num):
literal_value = literal.n
elif isinstance(literal, _ast.Str):
literal_value = literal.s
elif isinstance(literal, _ast.List):
return_list = list()
for li in literal.elts:
return_list.append(self._get_literal_value(li))
literal_value = return_list
elif isinstance(literal, _ast.Tuple):
return_tuple = tuple()
for ti in literal.elts:
return_tuple = return_tuple + (self._get_literal_value(ti),)
literal_value = return_tuple
elif isinstance(literal, _ast.Set):
return_set = set()
for si in literal.elts:
return_set.add(self._get_literal_value(si))
literal_value = return_set
elif isinstance(literal, _ast.Dict):
literal_value = dict(zip(literal.keys, literal.values))
elif isinstance(literal, _ast.Ellipsis):
# what do we want to do with this?
literal_value = None
elif isinstance(literal, _ast.Name):
literal_value = literal.id
# NOTE(sigmavirus24): NameConstants are only part of the AST in Python
# 3. NameConstants tend to refer to things like True and False. This
# prevents them from being re-assigned in Python 3.
elif six.PY3 and isinstance(literal, _ast.NameConstant):
literal_value = str(literal.value)
# NOTE(sigmavirus24): Bytes are only part of the AST in Python 3
elif six.PY3 and isinstance(literal, _ast.Bytes):
literal_value = literal.s
else:
literal_value = None
return literal_value
def get_call_arg_value(self, argument_name):
'''Gets the value of a named argument in a function call.
:return: named argument value
'''
kwd_values = self.call_keywords
if kwd_values is not None and argument_name in kwd_values:
return kwd_values[argument_name]
def check_call_arg_value(self, argument_name, argument_values=None):
'''Checks for a value of a named argument in a function call.
Returns none if the specified argument is not found.
:param argument_name: A string - name of the argument to look for
:param argument_values: the value, or list of values to test against
:return: Boolean True if argument found and matched, False if
found and not matched, None if argument not found at all
'''
arg_value = self.get_call_arg_value(argument_name)
if arg_value is not None:
if not isinstance(argument_values, list):
# if passed a single value, or a tuple, convert to a list
argument_values = list((argument_values,))
for val in argument_values:
if arg_value == val:
return True
return False
else:
# argument name not found, return None to allow testing for this
# eventuality
return None
def get_lineno_for_call_arg(self, argument_name):
'''Get the line number for a specific named argument
In case the call is split over multiple lines, get the correct one for
the argument.
:param argument_name: A string - name of the argument to look for
:return: Integer - the line number of the found argument, or -1
'''
for key in self.node.keywords:
if key.arg == argument_name:
return key.value.lineno
def get_call_arg_at_position(self, position_num):
'''Returns positional argument at the specified position (if it exists)
:param position_num: The index of the argument to return the value for
:return: Value of the argument at the specified position if it exists
'''
if ('call' in self._context and
hasattr(self._context['call'], 'args') and
position_num < len(self._context['call'].args)):
return self._get_literal_value(
self._context['call'].args[position_num]
)
else:
return None
def is_module_being_imported(self, module):
'''Check for the specified module is currently being imported
:param module: The module name to look for
:return: True if the module is found, False otherwise
'''
return 'module' in self._context and self._context['module'] == module
def is_module_imported_exact(self, module):
'''Check if a specified module has been imported; only exact matches.
:param module: The module name to look for
:return: True if the module is found, False otherwise
'''
return ('imports' in self._context and
module in self._context['imports'])
def is_module_imported_like(self, module):
'''Check if a specified module has been imported
Check if a specified module has been imported; specified module exists
as part of any import statement.
:param module: The module name to look for
:return: True if the module is found, False otherwise
'''
if 'imports' in self._context:
for imp in self._context['imports']:
if module in imp:
return True
return False

View File

@ -1,53 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 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.
# where our docs are hosted
BASE_URL = 'https://docs.openstack.org/bandit/latest/'
def get_url(bid):
# NOTE(tkelsey): for some reason this import can't be found when stevedore
# loads up the formatter plugin that imports this file. It is available
# later though.
from bandit.core import extension_loader
info = extension_loader.MANAGER.plugins_by_id.get(bid)
if info is not None:
return '%splugins/%s_%s.html' % (BASE_URL, bid.lower(),
info.plugin.__name__)
info = extension_loader.MANAGER.blacklist_by_id.get(bid)
if info is not None:
template = 'blacklists/blacklist_{kind}.html#{id}-{name}'
info['name'] = info['name'].replace('_', '-')
if info['id'].startswith('B3'): # B3XX
# Some of the links are combined, so we have exception cases
if info['id'] in ['B304', 'B305']:
info['id'] = 'b304-b305'
info['name'] = 'ciphers-and-modes'
elif info['id'] in ['B313', 'B314', 'B315', 'B316', 'B317',
'B318', 'B319', 'B320']:
info['id'] = 'b313-b320'
ext = template.format(
kind='calls', id=info['id'], name=info['name'])
else:
ext = template.format(
kind='imports', id=info['id'], name=info['name'])
return BASE_URL + ext.lower()
return BASE_URL # no idea, give the docs main page

View File

@ -1,118 +0,0 @@
# -*- coding:utf-8 -*-
#
# 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.
from __future__ import print_function
import sys
import six
from stevedore import extension
from bandit.core import utils
class Manager(object):
# These IDs are for bandit built in tests
builtin = [
'B001' # Built in blacklist test
]
def __init__(self, formatters_namespace='bandit.formatters',
plugins_namespace='bandit.plugins',
blacklists_namespace='bandit.blacklists'):
# Cache the extension managers, loaded extensions, and extension names
self.load_formatters(formatters_namespace)
self.load_plugins(plugins_namespace)
self.load_blacklists(blacklists_namespace)
def load_formatters(self, formatters_namespace):
self.formatters_mgr = extension.ExtensionManager(
namespace=formatters_namespace,
invoke_on_load=False,
verify_requirements=False,
)
self.formatters = list(self.formatters_mgr)
self.formatter_names = self.formatters_mgr.names()
def load_plugins(self, plugins_namespace):
self.plugins_mgr = extension.ExtensionManager(
namespace=plugins_namespace,
invoke_on_load=False,
verify_requirements=False,
)
def test_has_id(plugin):
if not hasattr(plugin.plugin, "_test_id"):
# logger not setup yet, so using print
print("WARNING: Test '%s' has no ID, skipping." % plugin.name,
file=sys.stderr)
return False
return True
self.plugins = list(filter(test_has_id, list(self.plugins_mgr)))
self.plugin_names = [plugin.name for plugin in self.plugins]
self.plugins_by_id = {p.plugin._test_id: p for p in self.plugins}
self.plugins_by_name = {p.name: p for p in self.plugins}
def get_plugin_id(self, plugin_name):
if plugin_name in self.plugins_by_name:
return self.plugins_by_name[plugin_name].plugin._test_id
return None
def load_blacklists(self, blacklist_namespace):
self.blacklists_mgr = extension.ExtensionManager(
namespace=blacklist_namespace,
invoke_on_load=False,
verify_requirements=False,
)
self.blacklist = {}
blacklist = list(self.blacklists_mgr)
for item in blacklist:
for key, val in item.plugin().items():
utils.check_ast_node(key)
self.blacklist.setdefault(key, []).extend(val)
self.blacklist_by_id = {}
self.blacklist_by_name = {}
for val in six.itervalues(self.blacklist):
for b in val:
self.blacklist_by_id[b['id']] = b
self.blacklist_by_name[b['name']] = b
def validate_profile(self, profile):
'''Validate that everything in the configured profiles looks good.'''
for inc in profile['include']:
if not self.check_id(inc):
raise ValueError('Unknown test found in profile: %s' % inc)
for exc in profile['exclude']:
if not self.check_id(exc):
raise ValueError('Unknown test found in profile: %s' % exc)
union = set(profile['include']) & set(profile['exclude'])
if len(union) > 0:
raise ValueError('Non-exclusive include/exclude test sets: %s' %
union)
def check_id(self, test):
return (
test in self.plugins_by_id or
test in self.blacklist_by_id or
test in self.builtin)
# Using entry-points and pkg_resources *can* be expensive. So let's load these
# once, store them on the object, and have a module global object for
# accessing them. After the first time this module is imported, it should save
# this attribute on the module and not have to reload the entry-points.
MANAGER = Manager()

View File

@ -1,139 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2015 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.
from __future__ import division
from __future__ import unicode_literals
import linecache
from six import moves
from bandit.core import constants
class Issue(object):
def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT,
text="", ident=None, lineno=None, test_id=""):
self.severity = severity
self.confidence = confidence
if isinstance(text, bytes):
text = text.decode('utf-8')
self.text = text
self.ident = ident
self.fname = ""
self.test = ""
self.test_id = test_id
self.lineno = lineno
self.linerange = []
def __str__(self):
return ("Issue: '%s' from %s:%s: Severity: %s Confidence: "
"%s at %s:%i") % (self.text, self.test_id,
(self.ident or self.test), self.severity,
self.confidence, self.fname, self.lineno)
def __eq__(self, other):
# if the issue text, severity, confidence, and filename match, it's
# the same issue from our perspective
match_types = ['text', 'severity', 'confidence', 'fname', 'test',
'test_id']
return all(getattr(self, field) == getattr(other, field)
for field in match_types)
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return id(self)
def filter(self, severity, confidence):
'''Utility to filter on confidence and severity
This function determines whether an issue should be included by
comparing the severity and confidence rating of the issue to minimum
thresholds specified in 'severity' and 'confidence' respectively.
Formatters should call manager.filter_results() directly.
This will return false if either the confidence or severity of the
issue are lower than the given threshold values.
:param severity: Severity threshold
:param confidence: Confidence threshold
:return: True/False depending on whether issue meets threshold
'''
rank = constants.RANKING
return (rank.index(self.severity) >= rank.index(severity) and
rank.index(self.confidence) >= rank.index(confidence))
def get_code(self, max_lines=3, tabbed=False):
'''Gets lines of code from a file the generated this issue.
:param max_lines: Max lines of context to return
:param tabbed: Use tabbing in the output
:return: strings of code
'''
lines = []
max_lines = max(max_lines, 1)
lmin = max(1, self.lineno - max_lines // 2)
lmax = lmin + len(self.linerange) + max_lines - 1
tmplt = "%i\t%s" if tabbed else "%i %s"
for line in moves.xrange(lmin, lmax):
text = linecache.getline(self.fname, line)
if isinstance(text, bytes):
text = text.decode('utf-8')
if not len(text):
break
lines.append(tmplt % (line, text))
return ''.join(lines)
def as_dict(self, with_code=True):
'''Convert the issue to a dict of values for outputting.'''
out = {
'filename': self.fname,
'test_name': self.test,
'test_id': self.test_id,
'issue_severity': self.severity,
'issue_confidence': self.confidence,
'issue_text': self.text.encode('utf-8').decode('utf-8'),
'line_number': self.lineno,
'line_range': self.linerange,
}
if with_code:
out['code'] = self.get_code()
return out
def from_dict(self, data, with_code=True):
self.code = data["code"]
self.fname = data["filename"]
self.severity = data["issue_severity"]
self.confidence = data["issue_confidence"]
self.text = data["issue_text"]
self.test = data["test_name"]
self.test_id = data["test_id"]
self.lineno = data["line_number"]
self.linerange = data["line_range"]
def issue_from_dict(data):
i = Issue(severity=data["issue_severity"])
i.from_dict(data)
return i

View File

@ -1,398 +0,0 @@
# -*- 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 collections
import fnmatch
import json
import logging
import os
import sys
import traceback
from bandit.core import constants as b_constants
from bandit.core import extension_loader
from bandit.core import issue
from bandit.core import meta_ast as b_meta_ast
from bandit.core import metrics
from bandit.core import node_visitor as b_node_visitor
from bandit.core import test_set as b_test_set
LOG = logging.getLogger(__name__)
class BanditManager(object):
scope = []
def __init__(self, config, agg_type, debug=False, verbose=False,
profile=None, ignore_nosec=False):
'''Get logger, config, AST handler, and result store ready
:param config: config options object
:type config: bandit.core.BanditConfig
:param agg_type: aggregation type
:param debug: Whether to show debug messages or not
:param verbose: Whether to show verbose output
:param profile_name: Optional name of profile to use (from cmd line)
:param ignore_nosec: Whether to ignore #nosec or not
:return:
'''
self.debug = debug
self.verbose = verbose
if not profile:
profile = {}
self.ignore_nosec = ignore_nosec
self.b_conf = config
self.files_list = []
self.excluded_files = []
self.b_ma = b_meta_ast.BanditMetaAst()
self.skipped = []
self.results = []
self.baseline = []
self.agg_type = agg_type
self.metrics = metrics.Metrics()
self.b_ts = b_test_set.BanditTestSet(config, profile)
# set the increment of after how many files to show progress
self.progress = b_constants.progress_increment
self.scores = []
def get_skipped(self):
ret = []
# "skip" is a tuple of name and reason, decode just the name
for skip in self.skipped:
if isinstance(skip[0], bytes):
ret.append((skip[0].decode('utf-8'), skip[1]))
else:
ret.append(skip)
return ret
def get_issue_list(self,
sev_level=b_constants.LOW,
conf_level=b_constants.LOW):
return self.filter_results(sev_level, conf_level)
def populate_baseline(self, data):
'''Populate a baseline set of issues from a JSON report
This will populate a list of baseline issues discovered from a previous
run of bandit. Later this baseline can be used to filter out the result
set, see filter_results.
'''
items = []
try:
jdata = json.loads(data)
items = [issue.issue_from_dict(j) for j in jdata["results"]]
except Exception as e:
LOG.warning("Failed to load baseline data: %s", e)
self.baseline = items
def filter_results(self, sev_filter, conf_filter):
'''Returns a list of results filtered by the baseline
This works by checking the number of results returned from each file we
process. If the number of results is different to the number reported
for the same file in the baseline, then we return all results for the
file. We can't reliably return just the new results, as line numbers
will likely have changed.
:param sev_filter: severity level filter to apply
:param conf_filter: confidence level filter to apply
'''
results = [i for i in self.results if
i.filter(sev_filter, conf_filter)]
if not self.baseline:
return results
unmatched = _compare_baseline_results(self.baseline, results)
# if it's a baseline we'll return a dictionary of issues and a list of
# candidate issues
return _find_candidate_matches(unmatched, results)
def results_count(self, sev_filter=b_constants.LOW,
conf_filter=b_constants.LOW):
'''Return the count of results
:param sev_filter: Severity level to filter lower
:param conf_filter: Confidence level to filter
:return: Number of results in the set
'''
return len(self.get_issue_list(sev_filter, conf_filter))
def output_results(self, lines, sev_level, conf_level, output_file,
output_format, template=None):
'''Outputs results from the result store
:param lines: How many surrounding lines to show per result
:param sev_level: Which severity levels to show (LOW, MEDIUM, HIGH)
:param conf_level: Which confidence levels to show (LOW, MEDIUM, HIGH)
:param output_file: File to store results
:param output_format: output format plugin name
:param template: Output template with non-terminal tags <N>
(default: {abspath}:{line}:
{test_id}[bandit]: {severity}: {msg})
:return: -
'''
try:
formatters_mgr = extension_loader.MANAGER.formatters_mgr
if output_format not in formatters_mgr:
output_format = 'screen' if sys.stdout.isatty() else 'txt'
formatter = formatters_mgr[output_format]
report_func = formatter.plugin
if output_format == 'custom':
report_func(self, fileobj=output_file, sev_level=sev_level,
conf_level=conf_level, lines=lines,
template=template)
else:
report_func(self, fileobj=output_file, sev_level=sev_level,
conf_level=conf_level, lines=lines)
except Exception as e:
raise RuntimeError("Unable to output report using '%s' formatter: "
"%s" % (output_format, str(e)))
def discover_files(self, targets, recursive=False, excluded_paths=''):
'''Add tests directly and from a directory to the test set
:param targets: The command line list of files and directories
:param recursive: True/False - whether to add all files from dirs
:return:
'''
# We'll mantain a list of files which are added, and ones which have
# been explicitly excluded
files_list = set()
excluded_files = set()
excluded_path_strings = self.b_conf.get_option('exclude_dirs') or []
included_globs = self.b_conf.get_option('include') or ['*.py']
# if there are command line provided exclusions add them to the list
if excluded_paths:
for path in excluded_paths.split(','):
excluded_path_strings.append(path)
# build list of files we will analyze
for fname in targets:
# if this is a directory and recursive is set, find all files
if os.path.isdir(fname):
if recursive:
new_files, newly_excluded = _get_files_from_dir(
fname,
included_globs=included_globs,
excluded_path_strings=excluded_path_strings
)
files_list.update(new_files)
excluded_files.update(newly_excluded)
else:
LOG.warning("Skipping directory (%s), use -r flag to "
"scan contents", fname)
else:
# if the user explicitly mentions a file on command line,
# we'll scan it, regardless of whether it's in the included
# file types list
if _is_file_included(fname, included_globs,
excluded_path_strings,
enforce_glob=False):
files_list.add(fname)
else:
excluded_files.add(fname)
self.files_list = sorted(files_list)
self.excluded_files = sorted(excluded_files)
def run_tests(self):
'''Runs through all files in the scope
:return: -
'''
# display progress, if number of files warrants it
if len(self.files_list) > self.progress:
sys.stderr.write("%s [" % len(self.files_list))
# if we have problems with a file, we'll remove it from the files_list
# and add it to the skipped list instead
new_files_list = list(self.files_list)
for count, fname in enumerate(self.files_list):
LOG.debug("working on file : %s", fname)
if len(self.files_list) > self.progress:
# is it time to update the progress indicator?
if count % self.progress == 0:
sys.stderr.write("%s.. " % count)
sys.stderr.flush()
try:
if fname == '-':
sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0)
self._parse_file('<stdin>', sys.stdin, new_files_list)
else:
with open(fname, 'rb') as fdata:
self._parse_file(fname, fdata, new_files_list)
except IOError as e:
self.skipped.append((fname, e.strerror))
new_files_list.remove(fname)
if len(self.files_list) > self.progress:
sys.stderr.write("]\n")
sys.stderr.flush()
# reflect any files which may have been skipped
self.files_list = new_files_list
# do final aggregation of metrics
self.metrics.aggregate()
def _parse_file(self, fname, fdata, new_files_list):
try:
# parse the current file
data = fdata.read()
lines = data.splitlines()
self.metrics.begin(fname)
self.metrics.count_locs(lines)
if self.ignore_nosec:
nosec_lines = set()
else:
nosec_lines = set(
lineno + 1 for
(lineno, line) in enumerate(lines)
if b'#nosec' in line or b'# nosec' in line)
score = self._execute_ast_visitor(fname, data, nosec_lines)
self.scores.append(score)
self.metrics.count_issues([score, ])
except KeyboardInterrupt as e:
sys.exit(2)
except SyntaxError as e:
self.skipped.append((fname,
"syntax error while parsing AST from file"))
new_files_list.remove(fname)
except Exception as e:
LOG.error("Exception occurred when executing tests against "
"%s. Run \"bandit --debug %s\" to see the full "
"traceback.", fname, fname)
self.skipped.append((fname, 'exception while scanning file'))
new_files_list.remove(fname)
LOG.debug(" Exception string: %s", e)
LOG.debug(" Exception traceback: %s", traceback.format_exc())
def _execute_ast_visitor(self, fname, data, nosec_lines):
'''Execute AST parse on each file
:param fname: The name of the file being parsed
:param data: Original file contents
:param lines: The lines of code to process
:return: The accumulated test score
'''
score = []
res = b_node_visitor.BanditNodeVisitor(fname, self.b_ma,
self.b_ts, self.debug,
nosec_lines, self.metrics)
score = res.process(data)
self.results.extend(res.tester.results)
return score
def _get_files_from_dir(files_dir, included_globs=None,
excluded_path_strings=None):
if not included_globs:
included_globs = ['*.py']
if not excluded_path_strings:
excluded_path_strings = []
files_list = set()
excluded_files = set()
for root, subdirs, files in os.walk(files_dir):
for filename in files:
path = os.path.join(root, filename)
if _is_file_included(path, included_globs, excluded_path_strings):
files_list.add(path)
else:
excluded_files.add(path)
return files_list, excluded_files
def _is_file_included(path, included_globs, excluded_path_strings,
enforce_glob=True):
'''Determine if a file should be included based on filename
This utility function determines if a file should be included based
on the file name, a list of parsed extensions, excluded paths, and a flag
specifying whether extensions should be enforced.
:param path: Full path of file to check
:param parsed_extensions: List of parsed extensions
:param excluded_paths: List of paths from which we should not include files
:param enforce_glob: Can set to false to bypass extension check
:return: Boolean indicating whether a file should be included
'''
return_value = False
# if this is matches a glob of files we look at, and it isn't in an
# excluded path
if _matches_glob_list(path, included_globs) or not enforce_glob:
if not any(x in path for x in excluded_path_strings):
return_value = True
return return_value
def _matches_glob_list(filename, glob_list):
for glob in glob_list:
if fnmatch.fnmatch(filename, glob):
return True
return False
def _compare_baseline_results(baseline, results):
"""Compare a baseline list of issues to list of results
This function compares a baseline set of issues to a current set of issues
to find results that weren't present in the baseline.
:param baseline: Baseline list of issues
:param results: Current list of issues
:return: List of unmatched issues
"""
return [a for a in results if a not in baseline]
def _find_candidate_matches(unmatched_issues, results_list):
"""Returns a dictionary with issue candidates
For example, let's say we find a new command injection issue in a file
which used to have two. Bandit can't tell which of the command injection
issues in the file are new, so it will show all three. The user should
be able to pick out the new one.
:param unmatched_issues: List of issues that weren't present before
:param results_list: Master list of current Bandit findings
:return: A dictionary with a list of candidates for each issue
"""
issue_candidates = collections.OrderedDict()
for unmatched in unmatched_issues:
issue_candidates[unmatched] = ([i for i in results_list if
unmatched == i])
return issue_candidates

View File

@ -1,57 +0,0 @@
# -*- 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 collections
import logging
LOG = logging.getLogger(__name__)
class BanditMetaAst(object):
nodes = collections.OrderedDict()
def __init__(self):
pass
def add_node(self, node, parent_id, depth):
'''Add a node to the AST node collection
:param node: The AST node to add
:param parent_id: The ID of the node's parent
:param depth: The depth of the node
:return: -
'''
node_id = hex(id(node))
LOG.debug('adding node : %s [%s]', node_id, depth)
self.nodes[node_id] = {
'raw': node, 'parent_id': parent_id, 'depth': depth
}
def __str__(self):
'''Dumps a listing of all of the nodes
Dumps a listing of all of the nodes for debugging purposes
:return: -
'''
tmpstr = ""
for k, v in self.nodes.items():
tmpstr += "Node: %s\n" % k
tmpstr += "\t%s\n" % str(v)
tmpstr += "Length: %s\n" % len(self.nodes)
return tmpstr

View File

@ -1,103 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2015 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 collections
from bandit.core import constants
class Metrics(object):
"""Bandit metric gathering.
This class is a singleton used to gather and process metrics collected when
processing a code base with bandit. Metric collection is stateful, that
is, an active metric block will be set when requested and all subsequent
operations will effect that metric block until it is replaced by a setting
a new one.
"""
def __init__(self):
self.data = dict()
self.data['_totals'] = {'loc': 0, 'nosec': 0}
# initialize 0 totals for criteria and rank; this will be reset later
for rank in constants.RANKING:
for criteria in constants.CRITERIA:
self.data['_totals']['{0}.{1}'.format(criteria[0], rank)] = 0
def begin(self, fname):
"""Begin a new metric block.
This starts a new metric collection name "fname" and makes is active.
:param fname: the metrics unique name, normally the file name.
"""
self.data[fname] = {'loc': 0, 'nosec': 0}
self.current = self.data[fname]
def note_nosec(self, num=1):
"""Note a "nosec" commnet.
Increment the currently active metrics nosec count.
:param num: number of nosecs seen, defaults to 1
"""
self.current['nosec'] += num
def count_locs(self, lines):
"""Count lines of code.
We count lines that are not empty and are not comments. The result is
added to our currently active metrics loc count (normally this is 0).
:param lines: lines in the file to process
"""
def proc(line):
tmp = line.strip()
return bool(tmp and not tmp.startswith(b'#'))
self.current['loc'] += sum(proc(line) for line in lines)
def count_issues(self, scores):
self.current.update(self._get_issue_counts(scores))
def aggregate(self):
"""Do final aggregation of metrics."""
c = collections.Counter()
for fname in self.data:
c.update(self.data[fname])
self.data['_totals'] = dict(c)
@staticmethod
def _get_issue_counts(scores):
"""Get issue counts aggregated by confidence/severity rankings.
:param scores: list of scores to aggregate / count
:return: aggregated total (count) of issues identified
"""
issue_counts = {}
for score in scores:
for (criteria, default) in constants.CRITERIA:
for i, rank in enumerate(constants.RANKING):
label = '{0}.{1}'.format(criteria, rank)
if label not in issue_counts:
issue_counts[label] = 0
count = (
score[criteria][i] /
constants.RANKING_VALUES[rank]
)
issue_counts[label] += count
return issue_counts

View File

@ -1,279 +0,0 @@
# -*- 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

View File

@ -1,85 +0,0 @@
# -*- 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 logging
from bandit.core import utils
LOG = logging.getLogger(__name__)
def checks(*args):
'''Decorator function to set checks to be run.'''
def wrapper(func):
if not hasattr(func, "_checks"):
func._checks = []
func._checks.extend(utils.check_ast_node(a) for a in args)
LOG.debug('checks() decorator executed')
LOG.debug(' func._checks: %s', func._checks)
return func
return wrapper
def takes_config(*args):
'''Test function takes config
Use of this delegate before a test function indicates that it should be
passed data from the config file. Passing a name parameter allows
aliasing tests and thus sharing config options.
'''
name = ""
def _takes_config(func):
if not hasattr(func, "_takes_config"):
func._takes_config = name
return func
if len(args) == 1 and callable(args[0]):
name = args[0].__name__
return _takes_config(args[0])
else:
name = args[0]
return _takes_config
def test_id(id_val):
'''Test function identifier
Use this decorator before a test function indicates its simple ID
'''
def _has_id(func):
if not hasattr(func, "_test_id"):
func._test_id = id_val
return func
return _has_id
def accepts_baseline(*args):
"""Decorator to indicate formatter accepts baseline results
Use of this decorator before a formatter indicates that it is able to deal
with baseline results. Specifically this means it has a way to display
candidate results and know when it should do so.
"""
def wrapper(func):
if not hasattr(func, '_accepts_baseline'):
func._accepts_baseline = True
LOG.debug('accepts_baseline() decorator executed on %s', func.__name__)
return func
return wrapper(args[0])

View File

@ -1,123 +0,0 @@
# -*- 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 importlib
import logging
from bandit.core import blacklisting
from bandit.core import extension_loader
LOG = logging.getLogger(__name__)
class BanditTestSet(object):
def __init__(self, config, profile=None):
if not profile:
profile = {}
extman = extension_loader.MANAGER
filtering = self._get_filter(config, profile)
self.plugins = [p for p in extman.plugins
if p.plugin._test_id in filtering]
self.plugins.extend(self._load_builtins(filtering, profile))
self._load_tests(config, self.plugins)
@staticmethod
def _get_filter(config, profile):
extman = extension_loader.MANAGER
inc = set(profile.get('include', []))
exc = set(profile.get('exclude', []))
all_blacklist_tests = set()
for _node, tests in extman.blacklist.items():
all_blacklist_tests.update(t['id'] for t in tests)
# this block is purely for backwards compatibility, the rules are as
# follows:
# B001,B401 means B401
# B401 means B401
# B001 means all blacklist tests
if 'B001' in inc:
if not inc.intersection(all_blacklist_tests):
inc.update(all_blacklist_tests)
inc.discard('B001')
if 'B001' in exc:
if not exc.intersection(all_blacklist_tests):
exc.update(all_blacklist_tests)
exc.discard('B001')
if inc:
filtered = inc
else:
filtered = set(extman.plugins_by_id.keys())
filtered.update(extman.builtin)
filtered.update(all_blacklist_tests)
return filtered - exc
def _load_builtins(self, filtering, profile):
'''loads up builtin functions, so they can be filtered.'''
class Wrapper(object):
def __init__(self, name, plugin):
self.name = name
self.plugin = plugin
extman = extension_loader.MANAGER
blacklist = profile.get('blacklist')
if not blacklist: # not overridden by legacy data
blacklist = {}
for node, tests in extman.blacklist.items():
values = [t for t in tests if t['id'] in filtering]
if values:
blacklist[node] = values
if not blacklist:
return []
# this dresses up the blacklist to look like a plugin, but
# the '_checks' data comes from the blacklist information.
# the '_config' is the filtered blacklist data set.
setattr(blacklisting.blacklist, "_test_id", 'B001')
setattr(blacklisting.blacklist, "_checks", blacklist.keys())
setattr(blacklisting.blacklist, "_config", blacklist)
return [Wrapper('blacklist', blacklisting.blacklist)]
def _load_tests(self, config, plugins):
'''Builds a dict mapping tests to node types.'''
self.tests = {}
for plugin in plugins:
if hasattr(plugin.plugin, '_takes_config'):
# TODO(??): config could come from profile ...
cfg = config.get_option(plugin.plugin._takes_config)
if cfg is None:
genner = importlib.import_module(plugin.plugin.__module__)
cfg = genner.gen_config(plugin.plugin._takes_config)
plugin.plugin._config = cfg
for check in plugin.plugin._checks:
self.tests.setdefault(check, []).append(plugin.plugin)
LOG.debug('added function %s (%s) targeting %s',
plugin.name, plugin.plugin._test_id, check)
def get_tests(self, checktype):
'''Returns all tests that are of type checktype
:param checktype: The type of test to filter on
:return: A list of tests which are of the specified type
'''
return self.tests.get(checktype) or []

View File

@ -1,111 +0,0 @@
# -*- 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 copy
import logging
import warnings
from bandit.core import constants
from bandit.core import context as b_context
from bandit.core import utils
warnings.formatwarning = utils.warnings_formatter
LOG = logging.getLogger(__name__)
class BanditTester(object):
def __init__(self, testset, debug, nosec_lines):
self.results = []
self.testset = testset
self.last_result = None
self.debug = debug
self.nosec_lines = nosec_lines
def run_tests(self, raw_context, checktype):
'''Runs all tests for a certain type of check, for example
Runs all tests for a certain type of check, for example 'functions'
store results in results.
:param raw_context: Raw context dictionary
:param checktype: The type of checks to run
:param nosec_lines: Lines which should be skipped because of nosec
:return: a score based on the number and type of test results
'''
scores = {
'SEVERITY': [0] * len(constants.RANKING),
'CONFIDENCE': [0] * len(constants.RANKING)
}
tests = self.testset.get_tests(checktype)
for test in tests:
name = test.__name__
# execute test with the an instance of the context class
temp_context = copy.copy(raw_context)
context = b_context.Context(temp_context)
try:
if hasattr(test, '_config'):
result = test(context, test._config)
else:
result = test(context)
# if we have a result, record it and update scores
if (result is not None and
result.lineno not in self.nosec_lines and
temp_context['lineno'] not in self.nosec_lines):
if isinstance(temp_context['filename'], bytes):
result.fname = temp_context['filename'].decode('utf-8')
else:
result.fname = temp_context['filename']
if result.lineno is None:
result.lineno = temp_context['lineno']
result.linerange = temp_context['linerange']
result.test = name
if result.test_id == "":
result.test_id = test._test_id
self.results.append(result)
LOG.debug("Issue identified by %s: %s", name, result)
sev = constants.RANKING.index(result.severity)
val = constants.RANKING_VALUES[result.severity]
scores['SEVERITY'][sev] += val
con = constants.RANKING.index(result.confidence)
val = constants.RANKING_VALUES[result.confidence]
scores['CONFIDENCE'][con] += val
except Exception as e:
self.report_error(name, context, e)
if self.debug:
raise
LOG.debug("Returning scores: %s", scores)
return scores
@staticmethod
def report_error(test, context, error):
what = "Bandit internal error running: "
what += "%s " % test
what += "on file %s at line %i: " % (
context._context['filename'],
context._context['lineno']
)
what += str(error)
import traceback
what += traceback.format_exc()
LOG.error(what)

View File

@ -1,336 +0,0 @@
# -*- 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 ast
import logging
import os.path
import sys
try:
import configparser
except ImportError:
import ConfigParser as configparser
LOG = logging.getLogger(__name__)
"""Various helper functions."""
def _get_attr_qual_name(node, aliases):
'''Get a the full name for the attribute node.
This will resolve a pseudo-qualified name for the attribute
rooted at node as long as all the deeper nodes are Names or
Attributes. This will give you how the code referenced the name but
will not tell you what the name actually refers to. If we
encounter a node without a static name we punt with an
empty string. If this encounters something more complex, such as
foo.mylist[0](a,b) we just return empty string.
:param node: AST Name or Attribute node
:param aliases: Import aliases dictionary
:returns: Qualified name referred to by the attribute or name.
'''
if isinstance(node, _ast.Name):
if node.id in aliases:
return aliases[node.id]
return node.id
elif isinstance(node, _ast.Attribute):
name = '%s.%s' % (_get_attr_qual_name(node.value, aliases), node.attr)
if name in aliases:
return aliases[name]
return name
else:
return ""
def get_call_name(node, aliases):
if isinstance(node.func, _ast.Name):
if deepgetattr(node, 'func.id') in aliases:
return aliases[deepgetattr(node, 'func.id')]
return deepgetattr(node, 'func.id')
elif isinstance(node.func, _ast.Attribute):
return _get_attr_qual_name(node.func, aliases)
else:
return ""
def get_func_name(node):
return node.name # TODO(tkelsey): get that qualname using enclosing scope
def get_qual_attr(node, aliases):
prefix = ""
if isinstance(node, _ast.Attribute):
try:
val = deepgetattr(node, 'value.id')
if val in aliases:
prefix = aliases[val]
else:
prefix = deepgetattr(node, 'value.id')
except Exception:
# NOTE(tkelsey): degrade gracefully when we can't get the fully
# qualified name for an attr, just return its base name.
pass
return "%s.%s" % (prefix, node.attr)
else:
return "" # TODO(tkelsey): process other node types
def deepgetattr(obj, attr):
"""Recurses through an attribute chain to get the ultimate value."""
for key in attr.split('.'):
obj = getattr(obj, key)
return obj
class InvalidModulePath(Exception):
pass
class ConfigError(Exception):
"""Raised when the config file fails validation."""
def __init__(self, message, config_file):
self.config_file = config_file
self.message = "{0} : {1}".format(config_file, message)
super(ConfigError, self).__init__(self.message)
class ProfileNotFound(Exception):
"""Raised when chosen profile cannot be found."""
def __init__(self, config_file, profile):
self.config_file = config_file
self.profile = profile
message = 'Unable to find profile (%s) in config file: %s' % (
self.profile, self.config_file)
super(ProfileNotFound, self).__init__(message)
def warnings_formatter(message, category=UserWarning, filename='', lineno=-1,
line=''):
'''Monkey patch for warnings.warn to suppress cruft output.'''
return "{0}\n".format(message)
def get_module_qualname_from_path(path):
'''Get the module's qualified name by analysis of the path.
Resolve the absolute pathname and eliminate symlinks. This could result in
an incorrect name if symlinks are used to restructure the python lib
directory.
Starting from the right-most directory component look for __init__.py in
the directory component. If it exists then the directory name is part of
the module name. Move left to the subsequent directory components until a
directory is found without __init__.py.
:param: Path to module file. Relative paths will be resolved relative to
current working directory.
:return: fully qualified module name
'''
(head, tail) = os.path.split(path)
if head == '' or tail == '':
raise InvalidModulePath('Invalid python file path: "%s"'
' Missing path or file name' % (path))
qname = [os.path.splitext(tail)[0]]
while head not in ['/', '.', '']:
if os.path.isfile(os.path.join(head, '__init__.py')):
(head, tail) = os.path.split(head)
qname.insert(0, tail)
else:
break
qualname = '.'.join(qname)
return qualname
def namespace_path_join(base, name):
'''Extend the current namespace path with an additional name
Take a namespace path (i.e., package.module.class) and extends it
with an additional name (i.e., package.module.class.subclass).
This is similar to how os.path.join works.
:param base: (String) The base namespace path.
:param name: (String) The new name to append to the base path.
:returns: (String) A new namespace path resulting from combination of
base and name.
'''
return '%s.%s' % (base, name)
def namespace_path_split(path):
'''Split the namespace path into a pair (head, tail).
Tail will be the last namespace path component and head will
be everything leading up to that in the path. This is similar to
os.path.split.
:param path: (String) A namespace path.
:returns: (String, String) A tuple where the first component is the base
path and the second is the last path component.
'''
return tuple(path.rsplit('.', 1))
def escaped_bytes_representation(b):
'''PY3 bytes need escaping for comparison with other strings.
In practice it turns control characters into acceptable codepoints then
encodes them into bytes again to turn unprintable bytes into printable
escape sequences.
This is safe to do for the whole range 0..255 and result matches
unicode_escape on a unicode string.
'''
return b.decode('unicode_escape').encode('unicode_escape')
def linerange(node):
"""Get line number range from a node."""
strip = {"body": None, "orelse": None,
"handlers": None, "finalbody": None}
for key in strip.keys():
if hasattr(node, key):
strip[key] = getattr(node, key)
setattr(node, key, [])
lines_min = 9999999999
lines_max = -1
for n in ast.walk(node):
if hasattr(n, 'lineno'):
lines_min = min(lines_min, n.lineno)
lines_max = max(lines_max, n.lineno)
for key in strip.keys():
if strip[key] is not None:
setattr(node, key, strip[key])
if lines_max > -1:
return list(range(lines_min, lines_max + 1))
return [0, 1]
def linerange_fix(node):
"""Try and work around a known Python bug with multi-line strings."""
# deal with multiline strings lineno behavior (Python issue #16806)
lines = linerange(node)
if hasattr(node, 'sibling') and hasattr(node.sibling, 'lineno'):
start = min(lines)
delta = node.sibling.lineno - start
if delta > 1:
return list(range(start, node.sibling.lineno))
return lines
def concat_string(node, stop=None):
'''Builds a string from a ast.BinOp chain.
This will build a string from a series of ast.Str nodes wrapped in
ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc.
The provided node can be any participant in the BinOp chain.
:param node: (ast.Str or ast.BinOp) The node to process
:param stop: (ast.Str or ast.BinOp) Optional base node to stop at
:returns: (Tuple) the root node of the expression, the string value
'''
def _get(node, bits, stop=None):
if node != stop:
bits.append(
_get(node.left, bits, stop)
if isinstance(node.left, ast.BinOp)
else node.left)
bits.append(
_get(node.right, bits, stop)
if isinstance(node.right, ast.BinOp)
else node.right)
bits = [node]
while isinstance(node.parent, ast.BinOp):
node = node.parent
if isinstance(node, ast.BinOp):
_get(node, bits, stop)
return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)]))
def get_called_name(node):
'''Get a function name from an ast.Call node.
An ast.Call node representing a method call with present differently to one
wrapping a function call: thing.call() vs call(). This helper will grab the
unqualified call name correctly in either case.
:param node: (ast.Call) the call node
:returns: (String) the function name
'''
func = node.func
try:
return func.attr if isinstance(func, ast.Attribute) else func.id
except AttributeError:
return ""
def get_path_for_function(f):
'''Get the path of the file where the function is defined.
:returns: the path, or None if one could not be found or f is not a real
function
'''
if hasattr(f, "__module__"):
module_name = f.__module__
elif hasattr(f, "im_func"):
module_name = f.im_func.__module__
else:
LOG.warning("Cannot resolve file where %s is defined", f)
return None
module = sys.modules[module_name]
if hasattr(module, "__file__"):
return module.__file__
else:
LOG.warning("Cannot resolve file path for module %s", module_name)
return None
def parse_ini_file(f_loc):
config = configparser.ConfigParser()
try:
config.read(f_loc)
return {k: v for k, v in config.items('bandit')}
except (configparser.Error, KeyError, TypeError):
LOG.warning("Unable to parse config file %s or missing [bandit] "
"section", f_loc)
return None
def check_ast_node(name):
'Check if the given name is that of a valid AST node.'
try:
node = getattr(ast, name)
if issubclass(node, ast.AST):
return name
except AttributeError: # nosec(tkelsey): catching expected exception
pass
raise TypeError("Error: %s is not a valid node type in AST" % name)

View File

@ -1,76 +0,0 @@
# -*- coding:utf-8 -*-
#
# 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.
r"""
=============
CSV Formatter
=============
This formatter outputs the issues in a comma separated values format.
:Example:
.. code-block:: none
filename,test_name,test_id,issue_severity,issue_confidence,issue_text,
line_number,line_range
examples/yaml_load.py,blacklist_calls,B301,MEDIUM,HIGH,"Use of unsafe yaml
load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().
",5,[5]
.. versionadded:: 0.11.0
"""
# Necessary for this formatter to work when imported on Python 2. Importing
# the standard library's csv module conflicts with the name of this module.
from __future__ import absolute_import
import csv
import logging
import sys
LOG = logging.getLogger(__name__)
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''Prints issues in CSV format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
'''
results = manager.get_issue_list(sev_level=sev_level,
conf_level=conf_level)
with fileobj:
fieldnames = ['filename',
'test_name',
'test_id',
'issue_severity',
'issue_confidence',
'issue_text',
'line_number',
'line_range']
writer = csv.DictWriter(fileobj, fieldnames=fieldnames,
extrasaction='ignore')
writer.writeheader()
for result in results:
writer.writerow(result.as_dict(with_code=False))
if fileobj.name != sys.stdout.name:
LOG.info("CSV output written to file: %s", fileobj.name)

View File

@ -1,163 +0,0 @@
# Copyright (c) 2017 Hewlett Packard Enterprise
# -*- coding:utf-8 -*-
#
# 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.
r"""
================
Custom Formatter
================
This formatter outputs the issues in custom machine-readable format.
default template: {abspath}:{line}: {test_id}[bandit]: {severity}: {msg}
:Example:
/usr/lib/python3.6/site-packages/openlp/core/utils/__init__.py: \
405: B310[bandit]: MEDIUM: Audit url open for permitted schemes. \
Allowing use of file:/ or custom schemes is often unexpected.
"""
import logging
import os
import re
import string
import sys
from bandit.core import test_properties
LOG = logging.getLogger(__name__)
class SafeMapper(dict):
"""Safe mapper to handle format key errors"""
@classmethod # To prevent PEP8 warnings in the test suite
def __missing__(cls, key):
return "{%s}" % key
@test_properties.accepts_baseline
def report(manager, fileobj, sev_level, conf_level, lines=-1, template=None):
"""Prints issues in custom format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
:param template: Output template with non-terminal tags <N>
(default: '{abspath}:{line}:
{test_id}[bandit]: {severity}: {msg}')
"""
machine_output = {'results': [], 'errors': []}
for (fname, reason) in manager.get_skipped():
machine_output['errors'].append({'filename': fname,
'reason': reason})
results = manager.get_issue_list(sev_level=sev_level,
conf_level=conf_level)
msg_template = template
if template is None:
msg_template = "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
# Dictionary of non-terminal tags that will be expanded
tag_mapper = {
'abspath': lambda issue: os.path.abspath(issue.fname),
'relpath': lambda issue: os.path.relpath(issue.fname),
'line': lambda issue: issue.lineno,
'test_id': lambda issue: issue.test_id,
'severity': lambda issue: issue.severity,
'msg': lambda issue: issue.text,
'confidence': lambda issue: issue.confidence,
'range': lambda issue: issue.linerange
}
# Create dictionary with tag sets to speed up search for similar tags
tag_sim_dict = dict(
[(tag, set(tag)) for tag, _ in tag_mapper.items()]
)
# Parse the format_string template and check the validity of tags
try:
parsed_template_orig = list(string.Formatter().parse(msg_template))
# of type (literal_text, field_name, fmt_spec, conversion)
# Check the format validity only, ignore keys
string.Formatter().vformat(msg_template, (), SafeMapper(line=0))
except ValueError as e:
LOG.error("Template is not in valid format: %s", e.args[0])
sys.exit(2)
tag_set = {t[1] for t in parsed_template_orig if t[1] is not None}
if not tag_set:
LOG.error("No tags were found in the template. Are you missing '{}'?")
sys.exit(2)
def get_similar_tag(tag):
similarity_list = [(len(set(tag) & t_set), t)
for t, t_set in tag_sim_dict.items()]
return sorted(similarity_list)[-1][1]
tag_blacklist = []
for tag in tag_set:
# check if the tag is in dictionary
if tag not in tag_mapper:
similar_tag = get_similar_tag(tag)
LOG.warning(
"Tag '%s' was not recognized and will be skipped, "
"did you mean to use '%s'?", tag, similar_tag
)
tag_blacklist += [tag]
# Compose the message template back with the valid values only
msg_parsed_template_list = []
for literal_text, field_name, fmt_spec, conversion in parsed_template_orig:
if literal_text:
# if there is '{' or '}', double it to prevent expansion
literal_text = re.sub('{', '{{', literal_text)
literal_text = re.sub('}', '}}', literal_text)
msg_parsed_template_list.append(literal_text)
if field_name is not None:
if field_name in tag_blacklist:
msg_parsed_template_list.append(field_name)
continue
# Append the fmt_spec part
params = [field_name, fmt_spec, conversion]
markers = ['', ':', '!']
msg_parsed_template_list.append(
['{'] +
["%s" % (m + p) if p else ''
for m, p in zip(markers, params)] +
['}']
)
msg_parsed_template = "".join([item for lst in msg_parsed_template_list
for item in lst]) + "\n"
limit = lines if lines > 0 else None
with fileobj:
for defect in results[:limit]:
evaluated_tags = SafeMapper(
(k, v(defect)) for k, v in tag_mapper.items()
)
output = msg_parsed_template.format(**evaluated_tags)
fileobj.write(output)
if fileobj.name != sys.stdout.name:
LOG.info("Result written to file: %s", fileobj.name)

View File

@ -1,387 +0,0 @@
# Copyright (c) 2015 Rackspace, Inc.
# Copyright (c) 2015 Hewlett Packard Enterprise
#
# 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.
r"""
==============
HTML formatter
==============
This formatter outputs the issues as HTML.
:Example:
.. code-block:: html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
Bandit Report
</title>
<style>
html * {
font-family: "Arial", sans-serif;
}
pre {
font-family: "Monaco", monospace;
}
.bordered-box {
border: 1px solid black;
padding-top:.5em;
padding-bottom:.5em;
padding-left:1em;
}
.metrics-box {
font-size: 1.1em;
line-height: 130%;
}
.metrics-title {
font-size: 1.5em;
font-weight: 500;
margin-bottom: .25em;
}
.issue-description {
font-size: 1.3em;
font-weight: 500;
}
.candidate-issues {
margin-left: 2em;
border-left: solid 1px; LightGray;
padding-left: 5%;
margin-top: .2em;
margin-bottom: .2em;
}
.issue-block {
border: 1px solid LightGray;
padding-left: .5em;
padding-top: .5em;
padding-bottom: .5em;
margin-bottom: .5em;
}
.issue-sev-high {
background-color: Pink;
}
.issue-sev-medium {
background-color: NavajoWhite;
}
.issue-sev-low {
background-color: LightCyan;
}
</style>
</head>
<body>
<div id="metrics">
<div class="metrics-box bordered-box">
<div class="metrics-title">
Metrics:<br>
</div>
Total lines of code: <span id="loc">9</span><br>
Total lines skipped (#nosec): <span id="nosec">0</span>
</div>
</div>
<br>
<div id="results">
<div id="issue-0">
<div class="issue-block issue-sev-medium">
<b>yaml_load: </b> Use of unsafe yaml load. Allows
instantiation of arbitrary objects. Consider yaml.safe_load().<br>
<b>Test ID:</b> B506<br>
<b>Severity: </b>MEDIUM<br>
<b>Confidence: </b>HIGH<br>
<b>File: </b><a href="examples/yaml_load.py"
target="_blank">examples/yaml_load.py</a> <br>
<b>More info: </b><a href="https://docs.openstack.org/bandit/latest/
plugins/yaml_load.html" target="_blank">
https://docs.openstack.org/bandit/latest/plugins/yaml_load.html</a>
<br>
<div class="code">
<pre>
5 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
6 y = yaml.load(ystr)
7 yaml.dump(y)
</pre>
</div>
</div>
</div>
</div>
</body>
</html>
.. versionadded:: 0.14.0
"""
import cgi
import logging
import sys
from bandit.core import docs_utils
from bandit.core import test_properties
from bandit.formatters import utils
LOG = logging.getLogger(__name__)
@test_properties.accepts_baseline
def report(manager, fileobj, sev_level, conf_level, lines=-1):
"""Writes issues to 'fileobj' in HTML format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
"""
header_block = u"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
Bandit Report
</title>
<style>
html * {
font-family: "Arial", sans-serif;
}
pre {
font-family: "Monaco", monospace;
}
.bordered-box {
border: 1px solid black;
padding-top:.5em;
padding-bottom:.5em;
padding-left:1em;
}
.metrics-box {
font-size: 1.1em;
line-height: 130%;
}
.metrics-title {
font-size: 1.5em;
font-weight: 500;
margin-bottom: .25em;
}
.issue-description {
font-size: 1.3em;
font-weight: 500;
}
.candidate-issues {
margin-left: 2em;
border-left: solid 1px; LightGray;
padding-left: 5%;
margin-top: .2em;
margin-bottom: .2em;
}
.issue-block {
border: 1px solid LightGray;
padding-left: .5em;
padding-top: .5em;
padding-bottom: .5em;
margin-bottom: .5em;
}
.issue-sev-high {
background-color: Pink;
}
.issue-sev-medium {
background-color: NavajoWhite;
}
.issue-sev-low {
background-color: LightCyan;
}
</style>
</head>
"""
report_block = u"""
<body>
{metrics}
{skipped}
<br>
<div id="results">
{results}
</div>
</body>
</html>
"""
issue_block = u"""
<div id="issue-{issue_no}">
<div class="issue-block {issue_class}">
<b>{test_name}: </b> {test_text}<br>
<b>Test ID:</b> {test_id}<br>
<b>Severity: </b>{severity}<br>
<b>Confidence: </b>{confidence}<br>
<b>File: </b><a href="{path}" target="_blank">{path}</a> <br>
<b>More info: </b><a href="{url}" target="_blank">{url}</a><br>
{code}
{candidates}
</div>
</div>
"""
code_block = u"""
<div class="code">
<pre>
{code}
</pre>
</div>
"""
candidate_block = u"""
<div class="candidates">
<br>
<b>Candidates: </b>
{candidate_list}
</div>
"""
candidate_issue = u"""
<div class="candidate">
<div class="candidate-issues">
<pre>{code}</pre>
</div>
</div>
"""
skipped_block = u"""
<br>
<div id="skipped">
<div class="bordered-box">
<b>Skipped files:</b><br><br>
{files_list}
</div>
</div>
"""
metrics_block = u"""
<div id="metrics">
<div class="metrics-box bordered-box">
<div class="metrics-title">
Metrics:<br>
</div>
Total lines of code: <span id="loc">{loc}</span><br>
Total lines skipped (#nosec): <span id="nosec">{nosec}</span>
</div>
</div>
"""
issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level)
baseline = not isinstance(issues, list)
# build the skipped string to insert in the report
skipped_str = ''.join('%s <b>reason:</b> %s<br>' % (fname, reason)
for fname, reason in manager.get_skipped())
if skipped_str:
skipped_text = skipped_block.format(files_list=skipped_str)
else:
skipped_text = ''
# build the results string to insert in the report
results_str = ''
for index, issue in enumerate(issues):
if not baseline or len(issues[issue]) == 1:
candidates = ''
safe_code = cgi.escape(issue.get_code(lines, True).
strip('\n').lstrip(' '))
code = code_block.format(code=safe_code)
else:
candidates_str = ''
code = ''
for candidate in issues[issue]:
candidate_code = cgi.escape(candidate.get_code(lines, True).
strip('\n').lstrip(' '))
candidates_str += candidate_issue.format(code=candidate_code)
candidates = candidate_block.format(candidate_list=candidates_str)
url = docs_utils.get_url(issue.test_id)
results_str += issue_block.format(issue_no=index,
issue_class='issue-sev-{}'.
format(issue.severity.lower()),
test_name=issue.test,
test_id=issue.test_id,
test_text=issue.text,
severity=issue.severity,
confidence=issue.confidence,
path=issue.fname, code=code,
candidates=candidates,
url=url)
# build the metrics string to insert in the report
metrics_summary = metrics_block.format(
loc=manager.metrics.data['_totals']['loc'],
nosec=manager.metrics.data['_totals']['nosec'])
# build the report and output it
report_contents = report_block.format(metrics=metrics_summary,
skipped=skipped_text,
results=results_str)
with fileobj:
wrapped_file = utils.wrap_file_object(fileobj)
wrapped_file.write(utils.convert_file_contents(header_block))
wrapped_file.write(utils.convert_file_contents(report_contents))
if fileobj.name != sys.stdout.name:
LOG.info("HTML output written to file: %s", fileobj.name)

View File

@ -1,152 +0,0 @@
# -*- coding:utf-8 -*-
#
# 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.
r"""
==============
JSON formatter
==============
This formatter outputs the issues in JSON.
:Example:
.. code-block:: javascript
{
"errors": [],
"generated_at": "2015-12-16T22:27:34Z",
"metrics": {
"_totals": {
"CONFIDENCE.HIGH": 1,
"CONFIDENCE.LOW": 0,
"CONFIDENCE.MEDIUM": 0,
"CONFIDENCE.UNDEFINED": 0,
"SEVERITY.HIGH": 0,
"SEVERITY.LOW": 0,
"SEVERITY.MEDIUM": 1,
"SEVERITY.UNDEFINED": 0,
"loc": 5,
"nosec": 0
},
"examples/yaml_load.py": {
"CONFIDENCE.HIGH": 1,
"CONFIDENCE.LOW": 0,
"CONFIDENCE.MEDIUM": 0,
"CONFIDENCE.UNDEFINED": 0,
"SEVERITY.HIGH": 0,
"SEVERITY.LOW": 0,
"SEVERITY.MEDIUM": 1,
"SEVERITY.UNDEFINED": 0,
"loc": 5,
"nosec": 0
}
},
"results": [
{
"code": "4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})\n5
y = yaml.load(ystr)\n6 yaml.dump(y)\n",
"filename": "examples/yaml_load.py",
"issue_confidence": "HIGH",
"issue_severity": "MEDIUM",
"issue_text": "Use of unsafe yaml load. Allows instantiation of
arbitrary objects. Consider yaml.safe_load().\n",
"line_number": 5,
"line_range": [
5
],
"more_info": "https://docs.openstack.org/bandit/latest/",
"test_name": "blacklist_calls",
"test_id": "B301"
}
]
}
.. versionadded:: 0.10.0
"""
# Necessary so we can import the standard library json module while continuing
# to name this file json.py. (Python 2 only)
from __future__ import absolute_import
import datetime
import json
import logging
import operator
import sys
from bandit.core import docs_utils
from bandit.core import test_properties
LOG = logging.getLogger(__name__)
@test_properties.accepts_baseline
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''''Prints issues in JSON format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
'''
machine_output = {'results': [], 'errors': []}
for (fname, reason) in manager.get_skipped():
machine_output['errors'].append({'filename': fname,
'reason': reason})
results = manager.get_issue_list(sev_level=sev_level,
conf_level=conf_level)
baseline = not isinstance(results, list)
if baseline:
collector = []
for r in results:
d = r.as_dict()
d['more_info'] = docs_utils.get_url(d['test_id'])
if len(results[r]) > 1:
d['candidates'] = [c.as_dict() for c in results[r]]
collector.append(d)
else:
collector = [r.as_dict() for r in results]
for elem in collector:
elem['more_info'] = docs_utils.get_url(elem['test_id'])
itemgetter = operator.itemgetter
if manager.agg_type == 'vuln':
machine_output['results'] = sorted(collector,
key=itemgetter('test_name'))
else:
machine_output['results'] = sorted(collector,
key=itemgetter('filename'))
machine_output['metrics'] = manager.metrics.data
# timezone agnostic format
TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
time_string = datetime.datetime.utcnow().strftime(TS_FORMAT)
machine_output['generated_at'] = time_string
result = json.dumps(machine_output, sort_keys=True,
indent=2, separators=(',', ': '))
with fileobj:
fileobj.write(result)
if fileobj.name != sys.stdout.name:
LOG.info("JSON output written to file: %s", fileobj.name)

View File

@ -1,182 +0,0 @@
# Copyright (c) 2015 Hewlett Packard Enterprise
# -*- coding:utf-8 -*-
#
# 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.
r"""
================
Screen formatter
================
This formatter outputs the issues as color coded text.
:Example:
.. code-block:: none
>> Issue: [B301:blacklist_calls] Use of unsafe yaml load. Allows
instantiation of arbitrary objects. Consider yaml.safe_load().
Severity: Medium Confidence: High
Location: examples/yaml_load.py:5
4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
5 y = yaml.load(ystr)
6 yaml.dump(y)
.. versionadded:: 0.9.0
"""
from __future__ import print_function
import datetime
import logging
import sys
from bandit.core import constants
from bandit.core import test_properties
LOG = logging.getLogger(__name__)
COLOR = {
'DEFAULT': '\033[0m',
'HEADER': '\033[95m',
'LOW': '\033[94m',
'MEDIUM': '\033[93m',
'HIGH': '\033[91m',
}
def header(text, *args):
return u'%s%s%s' % (COLOR['HEADER'], (text % args), COLOR['DEFAULT'])
def get_verbose_details(manager):
bits = []
bits.append(header(u'Files in scope (%i):', len(manager.files_list)))
tpl = u"\t%s (score: {SEVERITY: %i, CONFIDENCE: %i})"
bits.extend([tpl % (item, sum(score['SEVERITY']), sum(score['CONFIDENCE']))
for (item, score)
in zip(manager.files_list, manager.scores)])
bits.append(header(u'Files excluded (%i):', len(manager.excluded_files)))
bits.extend([u"\t%s" % fname for fname in manager.excluded_files])
return '\n'.join([str(bit) for bit in bits])
def get_metrics(manager):
bits = []
bits.append(header("\nRun metrics:"))
for (criteria, default) in constants.CRITERIA:
bits.append("\tTotal issues (by %s):" % (criteria.lower()))
for rank in constants.RANKING:
bits.append("\t\t%s: %s" % (
rank.capitalize(),
manager.metrics.data['_totals']['%s.%s' % (criteria, rank)]))
return '\n'.join([str(bit) for bit in bits])
def _output_issue_str(issue, indent, show_lineno=True, show_code=True,
lines=-1):
# returns a list of lines that should be added to the existing lines list
bits = []
bits.append("%s%s>> Issue: [%s:%s] %s" % (
indent, COLOR[issue.severity], issue.test_id, issue.test, issue.text))
bits.append("%s Severity: %s Confidence: %s" % (
indent, issue.severity.capitalize(), issue.confidence.capitalize()))
bits.append("%s Location: %s:%s%s" % (
indent, issue.fname,
issue.lineno if show_lineno else "",
COLOR['DEFAULT']))
if show_code:
bits.extend([indent + l for l in
issue.get_code(lines, True).split('\n')])
return '\n'.join([bit for bit in bits])
def get_results(manager, sev_level, conf_level, lines):
bits = []
issues = manager.get_issue_list(sev_level, conf_level)
baseline = not isinstance(issues, list)
candidate_indent = ' ' * 10
if not len(issues):
return u"\tNo issues identified."
for issue in issues:
# if not a baseline or only one candidate we know the issue
if not baseline or len(issues[issue]) == 1:
bits.append(_output_issue_str(issue, "", lines=lines))
# otherwise show the finding and the candidates
else:
bits.append(_output_issue_str(issue, "",
show_lineno=False,
show_code=False))
bits.append(u'\n-- Candidate Issues --')
for candidate in issues[issue]:
bits.append(_output_issue_str(candidate,
candidate_indent,
lines=lines))
bits.append('\n')
bits.append(u'-' * 50)
return '\n'.join([bit for bit in bits])
def do_print(bits):
# needed so we can mock this stuff
print('\n'.join([bit for bit in bits]))
@test_properties.accepts_baseline
def report(manager, fileobj, sev_level, conf_level, lines=-1):
"""Prints discovered issues formatted for screen reading
This makes use of VT100 terminal codes for colored text.
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
"""
bits = []
bits.append(header("Run started:%s", datetime.datetime.utcnow()))
if manager.verbose:
bits.append(get_verbose_details(manager))
bits.append(header("\nTest results:"))
bits.append(get_results(manager, sev_level, conf_level, lines))
bits.append(header("\nCode scanned:"))
bits.append('\tTotal lines of code: %i' %
(manager.metrics.data['_totals']['loc']))
bits.append('\tTotal lines skipped (#nosec): %i' %
(manager.metrics.data['_totals']['nosec']))
bits.append(get_metrics(manager))
skipped = manager.get_skipped()
bits.append(header("Files skipped (%i):", len(skipped)))
bits.extend(["\t%s (%s)" % skip for skip in skipped])
do_print(bits)
if fileobj.name != sys.stdout.name:
LOG.info("Screen formatter output was not written to file: %s, "
"consider '-f txt'", fileobj.name)

View File

@ -1,164 +0,0 @@
# Copyright (c) 2015 Hewlett Packard Enterprise
# -*- coding:utf-8 -*-
#
# 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.
r"""
==============
Text Formatter
==============
This formatter outputs the issues as plain text.
:Example:
.. code-block:: none
>> Issue: [B301:blacklist_calls] Use of unsafe yaml load. Allows
instantiation of arbitrary objects. Consider yaml.safe_load().
Severity: Medium Confidence: High
Location: examples/yaml_load.py:5
4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
5 y = yaml.load(ystr)
6 yaml.dump(y)
.. versionadded:: 0.9.0
"""
from __future__ import print_function
import datetime
import logging
import sys
from bandit.core import constants
from bandit.core import test_properties
from bandit.formatters import utils
LOG = logging.getLogger(__name__)
def get_verbose_details(manager):
bits = []
bits.append(u'Files in scope (%i):' % len(manager.files_list))
tpl = u"\t%s (score: {SEVERITY: %i, CONFIDENCE: %i})"
bits.extend([tpl % (item, sum(score['SEVERITY']), sum(score['CONFIDENCE']))
for (item, score)
in zip(manager.files_list, manager.scores)])
bits.append(u'Files excluded (%i):' % len(manager.excluded_files))
bits.extend([u"\t%s" % fname for fname in manager.excluded_files])
return '\n'.join([bit for bit in bits])
def get_metrics(manager):
bits = []
bits.append("\nRun metrics:")
for (criteria, default) in constants.CRITERIA:
bits.append("\tTotal issues (by %s):" % (criteria.lower()))
for rank in constants.RANKING:
bits.append("\t\t%s: %s" % (
rank.capitalize(),
manager.metrics.data['_totals']['%s.%s' % (criteria, rank)]))
return '\n'.join([bit for bit in bits])
def _output_issue_str(issue, indent, show_lineno=True, show_code=True,
lines=-1):
# returns a list of lines that should be added to the existing lines list
bits = []
bits.append("%s>> Issue: [%s:%s] %s" % (
indent, issue.test_id, issue.test, issue.text))
bits.append("%s Severity: %s Confidence: %s" % (
indent, issue.severity.capitalize(), issue.confidence.capitalize()))
bits.append("%s Location: %s:%s" % (
indent, issue.fname, issue.lineno if show_lineno else ""))
if show_code:
bits.extend([indent + l for l in
issue.get_code(lines, True).split('\n')])
return '\n'.join([bit for bit in bits])
def get_results(manager, sev_level, conf_level, lines):
bits = []
issues = manager.get_issue_list(sev_level, conf_level)
baseline = not isinstance(issues, list)
candidate_indent = ' ' * 10
if not len(issues):
return u"\tNo issues identified."
for issue in issues:
# if not a baseline or only one candidate we know the issue
if not baseline or len(issues[issue]) == 1:
bits.append(_output_issue_str(issue, "", lines=lines))
# otherwise show the finding and the candidates
else:
bits.append(_output_issue_str(issue, "",
show_lineno=False,
show_code=False))
bits.append(u'\n-- Candidate Issues --')
for candidate in issues[issue]:
bits.append(_output_issue_str(candidate,
candidate_indent,
lines=lines))
bits.append('\n')
bits.append(u'-' * 50)
return '\n'.join([bit for bit in bits])
@test_properties.accepts_baseline
def report(manager, fileobj, sev_level, conf_level, lines=-1):
"""Prints discovered issues in the text format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
"""
bits = []
bits.append("Run started:%s" % datetime.datetime.utcnow())
if manager.verbose:
bits.append(get_verbose_details(manager))
bits.append("\nTest results:")
bits.append(get_results(manager, sev_level, conf_level, lines))
bits.append("\nCode scanned:")
bits.append('\tTotal lines of code: %i' %
(manager.metrics.data['_totals']['loc']))
bits.append('\tTotal lines skipped (#nosec): %i' %
(manager.metrics.data['_totals']['nosec']))
skipped = manager.get_skipped()
bits.append(get_metrics(manager))
bits.append("Files skipped (%i):" % len(skipped))
bits.extend(["\t%s (%s)" % skip for skip in skipped])
result = '\n'.join([bit for bit in bits]) + '\n'
with fileobj:
wrapped_file = utils.wrap_file_object(fileobj)
wrapped_file.write(utils.convert_file_contents(result))
if fileobj.name != sys.stdout.name:
LOG.info("Text output written to file: %s", fileobj.name)

View File

@ -1,43 +0,0 @@
# Copyright (c) 2016 Rackspace, Inc.
#
# 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.
"""Utility functions for formatting plugins for Bandit."""
import io
import six
def wrap_file_object(fileobj):
"""Handle differences in Python 2 and 3 around writing bytes."""
# If it's not an instance of IOBase, we're probably using Python 2 and
# that is less finnicky about writing text versus bytes to a file.
if not isinstance(fileobj, io.IOBase):
return fileobj
# At this point we're using Python 3 and that will mangle text written to
# a file written in bytes mode. So, let's check if the file can handle
# text as opposed to bytes.
if isinstance(fileobj, io.TextIOBase):
return fileobj
# Finally, we've determined that the fileobj passed in cannot handle text,
# so we use TextIOWrapper to handle the conversion for us.
return io.TextIOWrapper(fileobj)
def convert_file_contents(text):
"""Convert text to built-in strings on Python 2."""
if not six.PY2:
return text
return str(text.encode('utf-8'))

View File

@ -1,91 +0,0 @@
# -*- coding:utf-8 -*-
#
# 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.
r"""
=============
XML Formatter
=============
This formatter outputs the issues as XML.
:Example:
.. code-block:: xml
<?xml version='1.0' encoding='utf-8'?>
<testsuite name="bandit" tests="1"><testcase
classname="examples/yaml_load.py" name="blacklist_calls"><error
message="Use of unsafe yaml load. Allows instantiation of arbitrary
objects. Consider yaml.safe_load().&#10;" type="MEDIUM">Test ID: B301
Severity: MEDIUM Confidence: HIGH Use of unsafe yaml load. Allows
instantiation of arbitrary objects. Consider yaml.safe_load().
Location examples/yaml_load.py:5</error></testcase></testsuite>
.. versionadded:: 0.12.0
"""
# This future import is necessary here due to the xml import below on Python
# 2.7
from __future__ import absolute_import
import logging
import sys
from xml.etree import cElementTree as ET
import six
LOG = logging.getLogger(__name__)
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''Prints issues in XML format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
'''
issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level)
root = ET.Element('testsuite', name='bandit', tests=str(len(issues)))
for issue in issues:
test = issue.test
testcase = ET.SubElement(root, 'testcase',
classname=issue.fname, name=test)
text = 'Test ID: %s Severity: %s Confidence: %s\n%s\nLocation %s:%s'
text = text % (issue.test_id, issue.severity, issue.confidence,
issue.text, issue.fname, issue.lineno)
ET.SubElement(testcase, 'error', type=issue.severity,
message=issue.text).text = text
tree = ET.ElementTree(root)
if fileobj.name == sys.stdout.name:
if six.PY2:
fileobj = sys.stdout
else:
fileobj = sys.stdout.buffer
elif fileobj.mode == 'w':
fileobj.close()
fileobj = open(fileobj.name, "wb")
with fileobj:
tree.write(fileobj, encoding='utf-8', xml_declaration=True)
if fileobj.name != sys.stdout.name:
LOG.info("XML output written to file: %s", fileobj.name)

View File

@ -1,131 +0,0 @@
# Copyright (c) 2017 VMware, Inc.
#
# 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.
r"""
==============
YAML Formatter
==============
This formatter outputs the issues in a yaml format.
:Example:
.. code-block:: none
errors: []
generated_at: '2017-03-09T22:29:30Z'
metrics:
_totals:
CONFIDENCE.HIGH: 1
CONFIDENCE.LOW: 0
CONFIDENCE.MEDIUM: 0
CONFIDENCE.UNDEFINED: 0
SEVERITY.HIGH: 0
SEVERITY.LOW: 0
SEVERITY.MEDIUM: 1
SEVERITY.UNDEFINED: 0
loc: 9
nosec: 0
examples/yaml_load.py:
CONFIDENCE.HIGH: 1
CONFIDENCE.LOW: 0
CONFIDENCE.MEDIUM: 0
CONFIDENCE.UNDEFINED: 0
SEVERITY.HIGH: 0
SEVERITY.LOW: 0
SEVERITY.MEDIUM: 1
SEVERITY.UNDEFINED: 0
loc: 9
nosec: 0
results:
- code: '5 ystr = yaml.dump({''a'' : 1, ''b'' : 2, ''c'' : 3})\n
6 y = yaml.load(ystr)\n7 yaml.dump(y)\n'
filename: examples/yaml_load.py
issue_confidence: HIGH
issue_severity: MEDIUM
issue_text: Use of unsafe yaml load. Allows instantiation of arbitrary
objects.
Consider yaml.safe_load().
line_number: 6
line_range:
- 6
more_info: https://docs.openstack.org/bandit/latest/
test_id: B506
test_name: yaml_load
.. versionadded:: 1.4.1
"""
# Necessary for this formatter to work when imported on Python 2. Importing
# the standard library's yaml module conflicts with the name of this module.
from __future__ import absolute_import
import datetime
import logging
import operator
import sys
import yaml
from bandit.core import docs_utils
LOG = logging.getLogger(__name__)
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''Prints issues in YAML format
:param manager: the bandit manager object
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
'''
machine_output = {'results': [], 'errors': []}
for (fname, reason) in manager.get_skipped():
machine_output['errors'].append({'filename': fname, 'reason': reason})
results = manager.get_issue_list(sev_level=sev_level,
conf_level=conf_level)
collector = [r.as_dict() for r in results]
for elem in collector:
elem['more_info'] = docs_utils.get_url(elem['test_id'])
itemgetter = operator.itemgetter
if manager.agg_type == 'vuln':
machine_output['results'] = sorted(collector,
key=itemgetter('test_name'))
else:
machine_output['results'] = sorted(collector,
key=itemgetter('filename'))
machine_output['metrics'] = manager.metrics.data
for result in machine_output['results']:
if 'code' in result:
code = result['code'].replace('\n', '\\n')
result['code'] = code
# timezone agnostic format
TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
time_string = datetime.datetime.utcnow().strftime(TS_FORMAT)
machine_output['generated_at'] = time_string
yaml.safe_dump(machine_output, fileobj, default_flow_style=False)
if fileobj.name != sys.stdout.name:
LOG.info("YAML output written to file: %s", fileobj.name)

View File

@ -1,69 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2015 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.
r"""
======================================================
B201: Test for use of flask app with debug set to true
======================================================
Running Flask applications in debug mode results in the Werkzeug debugger
being enabled. This includes a feature that allows arbitrary code execution.
Documentation for both Flask [1]_ and Werkzeug [2]_ strongly suggests that
debug mode should never be enabled on production systems.
Operating a production server with debug mode enabled was the probable cause
of the Patreon breach in 2015 [3]_.
:Example:
.. code-block:: none
>> Issue: A Flask app appears to be run with debug=True, which exposes
the Werkzeug debugger and allows the execution of arbitrary code.
Severity: High Confidence: High
Location: examples/flask_debug.py:10
9 #bad
10 app.run(debug=True)
11
.. seealso::
.. [1] http://flask.pocoo.org/docs/0.10/quickstart/#debug-mode
.. [2] http://werkzeug.pocoo.org/docs/0.10/debug/
.. [3] http://labs.detectify.com/post/130332638391/how-patreon-got-hacked-publicly-exposed-werkzeug # noqa
.. versionadded:: 0.15.0
"""
import bandit
from bandit.core import test_properties as test
@test.test_id('B201')
@test.checks('Call')
def flask_debug_true(context):
if context.is_module_imported_like('flask'):
if context.call_function_name_qual.endswith('.run'):
if context.check_call_arg_value('debug', 'True'):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
text="A Flask app appears to be run with debug=True, "
"which exposes the Werkzeug debugger and allows "
"the execution of arbitrary code.",
lineno=context.get_lineno_for_call_arg('debug'),
)

View File

@ -1,65 +0,0 @@
# -*- 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.
r"""
============================
B101: Test for use of assert
============================
This plugin test checks for the use of the Python ``assert`` keyword. It was
discovered that some projects used assert to enforce interface constraints.
However, assert is removed with compiling to optimised byte code (python -o
producing \*.pyo files). This caused various protections to be removed. The use
of assert is also considered as general bad practice in OpenStack codebases.
Please see
https://docs.python.org/2/reference/simple_stmts.html#the-assert-statement for
more info on ``assert``
:Example:
.. code-block:: none
>> Issue: Use of assert detected. The enclosed code will be removed when
compiling to optimised byte code.
Severity: Low Confidence: High
Location: ./examples/assert.py:1
1 assert logged_in
2 display_assets()
.. seealso::
- https://bugs.launchpad.net/juniperopenstack/+bug/1456193
- https://bugs.launchpad.net/heat/+bug/1397883
- https://docs.python.org/2/reference/simple_stmts.html#the-assert-statement
.. versionadded:: 0.11.0
"""
import bandit
from bandit.core import test_properties as test
@test.test_id('B101')
@test.checks('Assert')
def assert_used(context):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text=("Use of assert detected. The enclosed code "
"will be removed when compiling to optimised byte code.")
)

View File

@ -1,72 +0,0 @@
# -*- 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.
r"""
=============================================
B501: Test for missing certificate validation
=============================================
Encryption in general is typically critical to the security of many
applications. Using TLS can greatly increase security by guaranteeing the
identity of the party you are communicating with. This is accomplished by one
or both parties presenting trusted certificates during the connection
initialization phase of TLS.
When request methods are used certificates are validated automatically which is
the desired behavior. If certificate validation is explicitly turned off
Bandit will return a HIGH severity error.
:Example:
.. code-block:: none
>> Issue: [request_with_no_cert_validation] Requests call with verify=False
disabling SSL certificate checks, security issue.
Severity: High Confidence: High
Location: examples/requests-ssl-verify-disabled.py:4
3 requests.get('https://gmail.com', verify=True)
4 requests.get('https://gmail.com', verify=False)
5 requests.post('https://gmail.com', verify=True)
.. seealso::
- https://security.openstack.org/guidelines/dg_move-data-securely.html
- https://security.openstack.org/guidelines/dg_validate-certificates.html
.. versionadded:: 0.9.0
"""
import bandit
from bandit.core import test_properties as test
@test.checks('Call')
@test.test_id('B501')
def request_with_no_cert_validation(context):
http_verbs = ('get', 'options', 'head', 'post', 'put', 'patch', 'delete')
if ('requests' in context.call_function_name_qual and
context.call_function_name in http_verbs):
if context.check_call_arg_value('verify', 'False'):
issue = bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="Requests call with verify=False disabling SSL "
"certificate checks, security issue.",
lineno=context.get_lineno_for_call_arg('verify'),
)
return issue

View File

@ -1,67 +0,0 @@
# -*- 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.
r"""
==============================
B102: Test for the use of exec
==============================
This plugin test checks for the use of Python's `exec` method or keyword. The
Python docs succinctly describe why the use of `exec` is risky.
:Example:
.. code-block:: none
>> Issue: Use of exec detected.
Severity: Medium Confidence: High
Location: ./examples/exec-py2.py:2
1 exec("do evil")
2 exec "do evil"
.. seealso::
- https://docs.python.org/2.0/ref/exec.html
- TODO: add info on exec and similar to sec best practice and link here
.. versionadded:: 0.9.0
"""
import six
import bandit
from bandit.core import test_properties as test
def exec_issue():
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
text="Use of exec detected."
)
if six.PY2:
@test.checks('Exec')
@test.test_id('B102')
def exec_used(context):
return exec_issue()
else:
@test.checks('Call')
@test.test_id('B102')
def exec_used(context):
if context.call_function_name_qual == 'exec':
return exec_issue()

View File

@ -1,94 +0,0 @@
# Copyright (c) 2015 VMware, Inc.
#
# 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.
r"""
==================================================
B111: Test for the use of rootwrap running as root
==================================================
Running commands as root dramatically increase their potential risk. Running
commands with restricted user privileges provides defense in depth against
command injection attacks, or developer and configuration error. This plugin
test checks for specific methods being called with a keyword parameter
`run_as_root` set to True, a common OpenStack idiom.
**Config Options:**
This test plugin takes a similarly named configuration block,
`execute_with_run_as_root_equals_true`, providing a list, `function_names`, of
function names. A call to any of these named functions will be checked for a
`run_as_root` keyword parameter, and if True, will report a Low severity
issue.
.. code-block:: yaml
execute_with_run_as_root_equals_true:
function_names:
- ceilometer.utils.execute
- cinder.utils.execute
- neutron.agent.linux.utils.execute
- nova.utils.execute
- nova.utils.trycmd
:Example:
.. code-block:: none
>> Issue: Execute with run_as_root=True identified, possible security
issue.
Severity: Low Confidence: Medium
Location: ./examples/exec-as-root.py:26
25 nova_utils.trycmd('gcc --version')
26 nova_utils.trycmd('gcc --version', run_as_root=True)
27
.. seealso::
- https://security.openstack.org/guidelines/dg_rootwrap-recommendations-and-plans.html # noqa
- https://security.openstack.org/guidelines/dg_use-oslo-rootwrap-securely.html
.. versionadded:: 0.10.0
"""
import bandit
from bandit.core import test_properties as test
def gen_config(name):
if name == 'execute_with_run_as_root_equals_true':
return {'function_names':
['ceilometer.utils.execute',
'cinder.utils.execute',
'neutron.agent.linux.utils.execute',
'nova.utils.execute',
'nova.utils.trycmd']}
@test.takes_config
@test.checks('Call')
@test.test_id('B111')
def execute_with_run_as_root_equals_true(context, config):
if (context.call_function_name_qual in config['function_names']):
if context.check_call_arg_value('run_as_root', 'True'):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
text="Execute with run_as_root=True identified, possible "
"security issue.",
lineno=context.get_lineno_for_call_arg('run_as_root'),
)

View File

@ -1,89 +0,0 @@
# -*- 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.
r"""
==================================================
B103: Test for setting permissive file permissions
==================================================
POSIX based operating systems utilize a permissions model to protect access to
parts of the file system. This model supports three roles "owner", "group"
and "world" each role may have a combination of "read", "write" or "execute"
flags sets. Python provides ``chmod`` to manipulate POSIX style permissions.
This plugin test looks for the use of ``chmod`` and will alert when it is used
to set particularly permissive control flags. A MEDIUM warning is generated if
a file is set to group executable and a HIGH warning is reported if a file is
set world writable. Warnings are given with HIGH confidence.
:Example:
.. code-block:: none
>> Issue: Probable insecure usage of temp file/directory.
Severity: Medium Confidence: Medium
Location: ./examples/os-chmod-py2.py:15
14 os.chmod('/etc/hosts', 0o777)
15 os.chmod('/tmp/oh_hai', 0x1ff)
16 os.chmod('/etc/passwd', stat.S_IRWXU)
>> Issue: Chmod setting a permissive mask 0777 on file (key_file).
Severity: High Confidence: High
Location: ./examples/os-chmod-py2.py:17
16 os.chmod('/etc/passwd', stat.S_IRWXU)
17 os.chmod(key_file, 0o777)
18
.. seealso::
- https://security.openstack.org/guidelines/dg_apply-restrictive-file-permissions.html # noqa
- https://en.wikipedia.org/wiki/File_system_permissions
- https://security.openstack.org
.. versionadded:: 0.9.0
"""
import stat
import bandit
from bandit.core import test_properties as test
@test.checks('Call')
@test.test_id('B103')
def set_bad_file_permissions(context):
if 'chmod' in context.call_function_name:
if context.call_args_count == 2:
mode = context.get_call_arg_at_position(1)
if (mode is not None and isinstance(mode, int) and
(mode & stat.S_IWOTH or mode & stat.S_IXGRP)):
# world writable is an HIGH, group executable is a MEDIUM
if mode & stat.S_IWOTH:
sev_level = bandit.HIGH
else:
sev_level = bandit.MEDIUM
filename = context.get_call_arg_at_position(0)
if filename is None:
filename = 'NOT PARSED'
return bandit.Issue(
severity=sev_level,
confidence=bandit.HIGH,
text="Chmod setting a permissive mask %s on file (%s)." %
(oct(mode), filename)
)

View File

@ -1,59 +0,0 @@
# -*- 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.
r"""
========================================
B104: Test for binding to all interfaces
========================================
Binding to all network interfaces can potentially open up a service to traffic
on unintended interfaces, that may not be properly documented or secured. This
plugin test looks for a string pattern "0.0.0.0" that may indicate a hardcoded
binding to all network interfaces.
:Example:
.. code-block:: none
>> Issue: Possible binding to all interfaces.
Severity: Medium Confidence: Medium
Location: ./examples/binding.py:4
3 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4 s.bind(('0.0.0.0', 31137))
5 s.bind(('192.168.0.1', 8080))
.. seealso::
- __TODO__ : add best practice info on binding to all interfaces, and link
here.
.. versionadded:: 0.9.0
"""
import bandit
from bandit.core import test_properties as test
@test.checks('Str')
@test.test_id('B104')
def hardcoded_bind_all_interfaces(context):
if context.string_val == '0.0.0.0':
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
text="Possible binding to all interfaces."
)

View File

@ -1,215 +0,0 @@
# -*- 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 sys
import bandit
from bandit.core import test_properties as test
CANDIDATES = set(["password", "pass", "passwd", "pwd", "secret", "token",
"secrete"])
def _report(value):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
text=("Possible hardcoded password: '%s'" % value))
@test.checks('Str')
@test.test_id('B105')
def hardcoded_password_string(context):
"""**B105: Test for use of hard-coded password strings**
The use of hard-coded passwords increases the possibility of password
guessing tremendously. This plugin test looks for all string literals and
checks the following conditions:
- assigned to a variable that looks like a password
- assigned to a dict key that looks like a password
- used in a comparison with a variable that looks like a password
Variables are considered to look like a password if they have match any one
of:
- "password"
- "pass"
- "passwd"
- "pwd"
- "secret"
- "token"
- "secrete"
Note: this can be noisy and may generate false positives.
**Config Options:**
None
:Example:
.. code-block:: none
>> Issue: Possible hardcoded password '(root)'
Severity: Low Confidence: Low
Location: ./examples/hardcoded-passwords.py:5
4 def someFunction2(password):
5 if password == "root":
6 print("OK, logged in")
.. seealso::
- https://www.owasp.org/index.php/Use_of_hard-coded_password
.. versionadded:: 0.9.0
"""
node = context.node
if isinstance(node.parent, ast.Assign):
# looks for "candidate='some_string'"
for targ in node.parent.targets:
if isinstance(targ, ast.Name) and targ.id in CANDIDATES:
return _report(node.s)
elif isinstance(node.parent, ast.Index) and node.s in CANDIDATES:
# looks for "dict[candidate]='some_string'"
# assign -> subscript -> index -> string
assign = node.parent.parent.parent
if isinstance(assign, ast.Assign) and isinstance(assign.value,
ast.Str):
return _report(assign.value.s)
elif isinstance(node.parent, ast.Compare):
# looks for "candidate == 'some_string'"
comp = node.parent
if isinstance(comp.left, ast.Name) and comp.left.id in CANDIDATES:
if isinstance(comp.comparators[0], ast.Str):
return _report(comp.comparators[0].s)
@test.checks('Call')
@test.test_id('B106')
def hardcoded_password_funcarg(context):
"""**B106: Test for use of hard-coded password function arguments**
The use of hard-coded passwords increases the possibility of password
guessing tremendously. This plugin test looks for all function calls being
passed a keyword argument that is a string literal. It checks that the
assigned local variable does not look like a password.
Variables are considered to look like a password if they have match any one
of:
- "password"
- "pass"
- "passwd"
- "pwd"
- "secret"
- "token"
- "secrete"
Note: this can be noisy and may generate false positives.
**Config Options:**
None
:Example:
.. code-block:: none
>> Issue: [B106:hardcoded_password_funcarg] Possible hardcoded
password: 'blerg'
Severity: Low Confidence: Medium
Location: ./examples/hardcoded-passwords.py:16
15
16 doLogin(password="blerg")
.. seealso::
- https://www.owasp.org/index.php/Use_of_hard-coded_password
.. versionadded:: 0.9.0
"""
# looks for "function(candidate='some_string')"
for kw in context.node.keywords:
if isinstance(kw.value, ast.Str) and kw.arg in CANDIDATES:
return _report(kw.value.s)
@test.checks('FunctionDef')
@test.test_id('B107')
def hardcoded_password_default(context):
"""**B107: Test for use of hard-coded password argument defaults**
The use of hard-coded passwords increases the possibility of password
guessing tremendously. This plugin test looks for all function definitions
that specify a default string literal for some argument. It checks that
the argument does not look like a password.
Variables are considered to look like a password if they have match any one
of:
- "password"
- "pass"
- "passwd"
- "pwd"
- "secret"
- "token"
- "secrete"
Note: this can be noisy and may generate false positives.
**Config Options:**
None
:Example:
.. code-block:: none
>> Issue: [B107:hardcoded_password_default] Possible hardcoded
password: 'Admin'
Severity: Low Confidence: Medium
Location: ./examples/hardcoded-passwords.py:1
1 def someFunction(user, password="Admin"):
2 print("Hi " + user)
.. seealso::
- https://www.owasp.org/index.php/Use_of_hard-coded_password
.. versionadded:: 0.9.0
"""
# looks for "def function(candidate='some_string')"
# this pads the list of default values with "None" if nothing is given
defs = [None] * (len(context.node.args.args) -
len(context.node.args.defaults))
defs.extend(context.node.args.defaults)
# go through all (param, value)s and look for candidates
for key, val in zip(context.node.args.args, defs):
if isinstance(key, ast.Name) or isinstance(key, ast.arg):
check = key.arg if sys.version_info.major > 2 else key.id # Py3
if isinstance(val, ast.Str) and check in CANDIDATES:
return _report(val.s)

View File

@ -1,86 +0,0 @@
# -*- 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.
r"""
===================================================
B108: Test for insecure usage of tmp file/directory
===================================================
Safely creating a temporary file or directory means following a number of rules
(see the references for more details). This plugin test looks for strings
starting with (configurable) commonly used temporary paths, for example:
- /tmp
- /var/tmp
- /dev/shm
- etc
**Config Options:**
This test plugin takes a similarly named config block,
`hardcoded_tmp_directory`. The config block provides a Python list, `tmp_dirs`,
that lists string fragments indicating possible temporary file paths. Any
string starting with one of these fragments will report a MEDIUM confidence
issue.
.. code-block:: yaml
hardcoded_tmp_directory:
tmp_dirs: ['/tmp', '/var/tmp', '/dev/shm']
:Example:
.. code-block: none
>> Issue: Probable insecure usage of temp file/directory.
Severity: Medium Confidence: Medium
Location: ./examples/hardcoded-tmp.py:1
1 f = open('/tmp/abc', 'w')
2 f.write('def')
.. seealso::
- https://security.openstack.org/guidelines/dg_using-temporary-files-securely.html # noqa
.. versionadded:: 0.9.0
"""
import bandit
from bandit.core import test_properties as test
def gen_config(name):
if name == 'hardcoded_tmp_directory':
return {'tmp_dirs': ['/tmp', '/var/tmp', '/dev/shm']}
@test.takes_config
@test.checks('Str')
@test.test_id('B108')
def hardcoded_tmp_directory(context, config):
if config is not None and 'tmp_dirs' in config:
tmp_dirs = config['tmp_dirs']
else:
tmp_dirs = ['/tmp', '/var/tmp', '/dev/shm']
if any(context.string_val.startswith(s) for s in tmp_dirs):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
text="Probable insecure usage of temp file/directory."
)

View File

@ -1,63 +0,0 @@
# -*- coding:utf-8 -*-
#
# 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.
r"""
==========================================================================
B324: Test for use of insecure md4 and md5 hash functions in hashlib.new()
==========================================================================
This plugin checks for the usage of the insecure MD4 and MD5 hash functions
in ``hashlib.new`` function. The ``hashlib.new`` function provides the ability
to construct a new hashing object using the named algorithm. This can be used
to create insecure hash functions like MD4 and MD5 if they are passed as
algorithm names to this function.
This is similar to B303 blacklist check, except that this checks for insecure
hash functions created using ``hashlib.new`` function.
:Example:
>> Issue: [B324:hashlib_new] Use of insecure MD4 or MD5 hash function.
Severity: Medium Confidence: High
Location: examples/hashlib_new_insecure_funcs.py:3
2
3 md5_hash = hashlib.new('md5', string='test')
4 print(md5_hash)
.. versionadded:: 1.5.0
"""
import bandit
from bandit.core import test_properties as test
@test.test_id('B324')
@test.checks('Call')
def hashlib_new(context):
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split('.')
func = qualname_list[-1]
if 'hashlib' in qualname_list and func == 'new':
args = context.call_args
keywords = context.call_keywords
name = args[0] if args else keywords['name']
if name.lower() in ('md4', 'md5'):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
text="Use of insecure MD4 or MD5 hash function.",
lineno=context.node.lineno,
)

View File

@ -1,74 +0,0 @@
# -*- 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.
r"""
==============================================
B601: Test for shell injection within Paramiko
==============================================
Paramiko is a Python library designed to work with the SSH2 protocol for secure
(encrypted and authenticated) connections to remote machines. It is intended to
run commands on a remote host. These commands are run within a shell on the
target and are thus vulnerable to various shell injection attacks. Bandit
reports a MEDIUM issue when it detects the use of Paramiko's "exec_command" or
"invoke_shell" methods advising the user to check inputs are correctly
sanitized.
:Example:
.. code-block:: none
>> Issue: Possible shell injection via Paramiko call, check inputs are
properly sanitized.
Severity: Medium Confidence: Medium
Location: ./examples/paramiko_injection.py:4
3 # this is not safe
4 paramiko.exec_command('something; reallly; unsafe')
5
>> Issue: Possible shell injection via Paramiko call, check inputs are
properly sanitized.
Severity: Medium Confidence: Medium
Location: ./examples/paramiko_injection.py:10
9 # this is not safe
10 SSHClient.invoke_shell('something; bad; here\n')
11
.. seealso::
- https://security.openstack.org
- https://github.com/paramiko/paramiko
- https://www.owasp.org/index.php/Command_Injection
.. versionadded:: 0.12.0
"""
import bandit
from bandit.core import test_properties as test
@test.checks('Call')
@test.test_id('B601')
def paramiko_calls(context):
issue_text = ('Possible shell injection via Paramiko call, check inputs '
'are properly sanitized.')
for module in ['paramiko']:
if context.is_module_imported_like(module):
if context.call_function_name in ['exec_command', 'invoke_shell']:
return bandit.Issue(severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
text=issue_text)

View File

@ -1,634 +0,0 @@
# -*- 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 re
import bandit
from bandit.core import test_properties as test
# yuck, regex: starts with a windows drive letter (eg C:)
# or one of our path delimeter characters (/, \, .)
full_path_match = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])')
def _evaluate_shell_call(context):
no_formatting = isinstance(context.node.args[0], ast.Str)
if no_formatting:
return bandit.LOW
else:
return bandit.HIGH
def gen_config(name):
if name == 'shell_injection':
return {
# Start a process using the subprocess module, or one of its
# wrappers.
'subprocess':
['subprocess.Popen',
'subprocess.call',
'subprocess.check_call',
'subprocess.check_output',
'utils.execute',
'utils.execute_with_timeout'],
# Start a process with a function vulnerable to shell injection.
'shell':
['os.system',
'os.popen',
'os.popen2',
'os.popen3',
'os.popen4',
'popen2.popen2',
'popen2.popen3',
'popen2.popen4',
'popen2.Popen3',
'popen2.Popen4',
'commands.getoutput',
'commands.getstatusoutput'],
# Start a process with a function that is not vulnerable to shell
# injection.
'no_shell':
['os.execl',
'os.execle',
'os.execlp',
'os.execlpe',
'os.execv',
'os.execve',
'os.execvp',
'os.execvpe',
'os.spawnl',
'os.spawnle',
'os.spawnlp',
'os.spawnlpe',
'os.spawnv',
'os.spawnve',
'os.spawnvp',
'os.spawnvpe',
'os.startfile']
}
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B602')
def subprocess_popen_with_shell_equals_true(context, config):
"""**B602: Test for use of popen with shell equals true**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess using a command shell. This type of subprocess
invocation is dangerous as it is vulnerable to various shell injection
attacks. Great care should be taken to sanitize all input in order to
mitigate this risk. Calls of this type are identified by a parameter of
'shell=True' being given.
Additionally, this plugin scans the command string given and adjusts its
reported severity based on how it is presented. If the command string is a
simple static string containing no special shell characters, then the
resulting issue has low severity. If the string is static, but contains
shell formatting characters or wildcards, then the reported issue is
medium. Finally, if the string is computed using Python's string
manipulation or formatting operations, then the reported issue has high
severity. These severity levels reflect the likelihood that the code is
vulnerable to injection.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `subprocess` section
that have shell=True specified.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
:Example:
.. code-block:: none
>> Issue: subprocess call with shell=True seems safe, but may be
changed in the future, consider rewriting without shell
Severity: Low Confidence: High
Location: ./examples/subprocess_shell.py:21
20 subprocess.check_call(['/bin/ls', '-l'], shell=False)
21 subprocess.check_call('/bin/ls -l', shell=True)
22
>> Issue: call with shell=True contains special shell characters,
consider moving extra logic into Python code
Severity: Medium Confidence: High
Location: ./examples/subprocess_shell.py:26
25
26 subprocess.Popen('/bin/ls *', shell=True)
27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
>> Issue: subprocess call with shell=True identified, security issue.
Severity: High Confidence: High
Location: ./examples/subprocess_shell.py:27
26 subprocess.Popen('/bin/ls *', shell=True)
27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
28 subprocess.Popen('/bin/ls {}'.format('something'), shell=True)
.. seealso::
- https://security.openstack.org
- https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
.. versionadded:: 0.9.0
"""
if config and context.call_function_name_qual in config['subprocess']:
if context.check_call_arg_value('shell', 'True'):
if len(context.call_args) > 0:
sev = _evaluate_shell_call(context)
if sev == bandit.LOW:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text='subprocess call with shell=True seems safe, but '
'may be changed in the future, consider '
'rewriting without shell',
lineno=context.get_lineno_for_call_arg('shell'),
)
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text='subprocess call with shell=True identified, '
'security issue.',
lineno=context.get_lineno_for_call_arg('shell'),
)
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B603')
def subprocess_without_shell_equals_true(context, config):
"""**B603: Test for use of subprocess with shell equals true**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess without the use of a command shell. This type of
subprocess invocation is not vulnerable to shell injection attacks, but
care should still be taken to ensure validity of input.
Because this is a lesser issue than that described in
`subprocess_popen_with_shell_equals_true` a LOW severity warning is
reported.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `subprocess` section
that have shell=False specified.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
:Example:
.. code-block:: none
>> Issue: subprocess call - check for execution of untrusted input.
Severity: Low Confidence: High
Location: ./examples/subprocess_shell.py:23
22
23 subprocess.check_output(['/bin/ls', '-l'])
24
.. seealso::
- https://security.openstack.org
- https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
.. versionadded:: 0.9.0
"""
if config and context.call_function_name_qual in config['subprocess']:
if not context.check_call_arg_value('shell', 'True'):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text='subprocess call - check for execution of untrusted '
'input.',
lineno=context.get_lineno_for_call_arg('shell'),
)
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B604')
def any_other_function_with_shell_equals_true(context, config):
"""**B604: Test for any function with shell equals true**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this plugin test
interrogates method calls for the presence of a keyword parameter `shell`
equalling true. It is related to detection of shell injection issues and is
intended to catch custom wrappers to vulnerable methods that may have been
created.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
Specifically, this plugin excludes those functions listed under the
subprocess section, these methods are tested in a separate specific test
plugin and this exclusion prevents duplicate issue reporting.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess: [subprocess.Popen, subprocess.call,
subprocess.check_call, subprocess.check_output,
utils.execute, utils.execute_with_timeout]
:Example:
.. code-block:: none
>> Issue: Function call with shell=True parameter identified, possible
security issue.
Severity: Medium Confidence: High
Location: ./examples/subprocess_shell.py:9
8 pop('/bin/gcc --version', shell=True)
9 Popen('/bin/gcc --version', shell=True)
10
.. seealso::
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html # noqa
.. versionadded:: 0.9.0
"""
if config and context.call_function_name_qual not in config['subprocess']:
if context.check_call_arg_value('shell', 'True'):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.LOW,
text='Function call with shell=True parameter identified, '
'possible security issue.',
lineno=context.get_lineno_for_call_arg('shell'),
)
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B605')
def start_process_with_a_shell(context, config):
"""**B605: Test for starting a process with a shell**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess using a command shell. This type of subprocess
invocation is dangerous as it is vulnerable to various shell injection
attacks. Great care should be taken to sanitize all input in order to
mitigate this risk. Calls of this type are identified by the use of certain
commands which are known to use shells. Bandit will report a LOW
severity warning.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_no_shell`
- :doc:`../plugins/start_process_with_partial_path`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `shell` section.
.. code-block:: yaml
shell_injection:
shell:
- os.system
- os.popen
- os.popen2
- os.popen3
- os.popen4
- popen2.popen2
- popen2.popen3
- popen2.popen4
- popen2.Popen3
- popen2.Popen4
- commands.getoutput
- commands.getstatusoutput
:Example:
.. code-block:: none
>> Issue: Starting a process with a shell: check for injection.
Severity: Low Confidence: Medium
Location: examples/os_system.py:3
2
3 os.system('/bin/echo hi')
.. seealso::
- https://security.openstack.org
- https://docs.python.org/2/library/os.html#os.system
- https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
.. versionadded:: 0.10.0
"""
if config and context.call_function_name_qual in config['shell']:
if len(context.call_args) > 0:
sev = _evaluate_shell_call(context)
if sev == bandit.LOW:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text='Starting a process with a shell: '
'Seems safe, but may be changed in the future, '
'consider rewriting without shell'
)
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text='Starting a process with a shell, possible injection'
' detected, security issue.'
)
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B606')
def start_process_with_no_shell(context, config):
"""**B606: Test for starting a process with no shell**
Python possesses many mechanisms to invoke an external executable. However,
doing so may present a security issue if appropriate care is not taken to
sanitize any user provided or variable input.
This plugin test is part of a family of tests built to check for process
spawning and warn appropriately. Specifically, this test looks for the
spawning of a subprocess in a way that doesn't use a shell. Although this
is generally safe, it maybe useful for penetration testing workflows to
track where external system calls are used. As such a LOW severity message
is generated.
See also:
- :doc:`../plugins/linux_commands_wildcard_injection`
- :doc:`../plugins/subprocess_without_shell_equals_true`
- :doc:`../plugins/start_process_with_a_shell`
- :doc:`../plugins/start_process_with_partial_path`
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This plugin specifically scans for methods listed in `no_shell` section.
.. code-block:: yaml
shell_injection:
no_shell:
- os.execl
- os.execle
- os.execlp
- os.execlpe
- os.execv
- os.execve
- os.execvp
- os.execvpe
- os.spawnl
- os.spawnle
- os.spawnlp
- os.spawnlpe
- os.spawnv
- os.spawnve
- os.spawnvp
- os.spawnvpe
- os.startfile
:Example:
.. code-block:: none
>> Issue: [start_process_with_no_shell] Starting a process without a
shell.
Severity: Low Confidence: Medium
Location: examples/os-spawn.py:8
7 os.spawnv(mode, path, args)
8 os.spawnve(mode, path, args, env)
9 os.spawnvp(mode, file, args)
.. seealso::
- https://security.openstack.org
- https://docs.python.org/2/library/os.html#os.system
- https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
.. versionadded:: 0.10.0
"""
if config and context.call_function_name_qual in config['no_shell']:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
text='Starting a process without a shell.'
)
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B607')
def start_process_with_partial_path(context, config):
"""**B607: Test for starting a process with a partial path**
Python possesses many mechanisms to invoke an external executable. If the
desired executable path is not fully qualified relative to the filesystem
root then this may present a potential security risk.
In POSIX environments, the `PATH` environment variable is used to specify a
set of standard locations that will be searched for the first matching
named executable. While convenient, this behavior may allow a malicious
actor to exert control over a system. If they are able to adjust the
contents of the `PATH` variable, or manipulate the file system, then a
bogus executable may be discovered in place of the desired one. This
executable will be invoked with the user privileges of the Python process
that spawned it, potentially a highly privileged user.
This test will scan the parameters of all configured Python methods,
looking for paths that do not start at the filesystem root, that is, do not
have a leading '/' character.
**Config Options:**
This plugin test shares a configuration with others in the same family,
namely `shell_injection`. This configuration is divided up into three
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
that spawn subprocesses, invoke commands within a shell, or invoke commands
without a shell (by replacing the calling process) respectively.
This test will scan parameters of all methods in all sections. Note that
methods are fully qualified and de-aliased prior to checking.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its
wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
# Start a process with a function vulnerable to shell injection.
shell:
- os.system
- os.popen
- popen2.Popen3
- popen2.Popen4
- commands.getoutput
- commands.getstatusoutput
# Start a process with a function that is not vulnerable to shell
injection.
no_shell:
- os.execl
- os.execle
:Example:
.. code-block:: none
>> Issue: Starting a process with a partial executable path
Severity: Low Confidence: High
Location: ./examples/partial_path_process.py:3
2 from subprocess import Popen as pop
3 pop('gcc --version', shell=False)
.. seealso::
- https://security.openstack.org
- https://docs.python.org/2/library/os.html#process-management
.. versionadded:: 0.13.0
"""
if config and len(context.call_args):
if(context.call_function_name_qual in config['subprocess'] or
context.call_function_name_qual in config['shell'] or
context.call_function_name_qual in config['no_shell']):
node = context.node.args[0]
# some calls take an arg list, check the first part
if isinstance(node, ast.List):
node = node.elts[0]
# make sure the param is a string literal and not a var name
if isinstance(node, ast.Str) and not full_path_match.match(node.s):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text='Starting a process with a partial executable path'
)

View File

@ -1,115 +0,0 @@
# -*- 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.
r"""
============================
B608: Test for SQL injection
============================
An SQL injection attack consists of insertion or "injection" of a SQL query via
the input data given to an application. It is a very common attack vector. This
plugin test looks for strings that resemble SQL statements that are involved in
some form of string building operation. For example:
- "SELECT %s FROM derp;" % var
- "SELECT thing FROM " + tab
- "SELECT " + val + " FROM " + tab + ...
- "SELECT {} FROM derp;".format(var)
Unless care is taken to sanitize and control the input data when building such
SQL statement strings, an injection attack becomes possible. If strings of this
nature are discovered, a LOW confidence issue is reported. In order to boost
result confidence, this plugin test will also check to see if the discovered
string is in use with standard Python DBAPI calls `execute` or `executemany`.
If so, a MEDIUM issue is reported. For example:
- cursor.execute("SELECT %s FROM derp;" % var)
:Example:
.. code-block:: none
>> Issue: Possible SQL injection vector through string-based query
construction.
Severity: Medium Confidence: Low
Location: ./examples/sql_statements_without_sql_alchemy.py:4
3 query = "DELETE FROM foo WHERE id = '%s'" % identifier
4 query = "UPDATE foo SET value = 'b' WHERE id = '%s'" % identifier
5
.. seealso::
- https://www.owasp.org/index.php/SQL_Injection
- https://security.openstack.org/guidelines/dg_parameterize-database-queries.html # noqa
.. versionadded:: 0.9.0
"""
import ast
import re
import bandit
from bandit.core import test_properties as test
from bandit.core import utils
SIMPLE_SQL_RE = re.compile(
r'(select\s.*from\s|'
r'delete\s+from\s|'
r'insert\s+into\s.*values\s|'
r'update\s.*set\s)',
re.IGNORECASE | re.DOTALL,
)
def _check_string(data):
return SIMPLE_SQL_RE.search(data) is not None
def _evaluate_ast(node):
wrapper = None
statement = ''
if isinstance(node.parent, ast.BinOp):
out = utils.concat_string(node, node.parent)
wrapper = out[0].parent
statement = out[1]
elif (isinstance(node.parent, ast.Attribute)
and node.parent.attr == 'format'):
statement = node.s
# Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str
wrapper = node.parent.parent.parent
if isinstance(wrapper, ast.Call): # wrapped in "execute" call?
names = ['execute', 'executemany']
name = utils.get_called_name(wrapper)
return (name in names, statement)
else:
return (False, statement)
@test.checks('Str')
@test.test_id('B608')
def hardcoded_sql_expressions(context):
val = _evaluate_ast(context.node)
if _check_string(val[1]):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM if val[0] else bandit.LOW,
text="Possible SQL injection vector through string-based "
"query construction."
)

View File

@ -1,149 +0,0 @@
# -*- 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.
r"""
========================================
B609: Test for use of wildcard injection
========================================
Python provides a number of methods that emulate the behavior of standard Linux
command line utilities. Like their Linux counterparts, these commands may take
a wildcard "\*" character in place of a file system path. This is interpreted
to mean "any and all files or folders" and can be used to build partially
qualified paths, such as "/home/user/\*".
The use of partially qualified paths may result in unintended consequences if
an unexpected file or symlink is placed into the path location given. This
becomes particularly dangerous when combined with commands used to manipulate
file permissions or copy data off of a system.
This test plugin looks for usage of the following commands in conjunction with
wild card parameters:
- 'chown'
- 'chmod'
- 'tar'
- 'rsync'
As well as any method configured in the shell or subprocess injection test
configurations.
**Config Options:**
This plugin test shares a configuration with others in the same family, namely
`shell_injection`. This configuration is divided up into three sections,
`subprocess`, `shell` and `no_shell`. They each list Python calls that spawn
subprocesses, invoke commands within a shell, or invoke commands without a
shell (by replacing the calling process) respectively.
This test will scan parameters of all methods in all sections. Note that
methods are fully qualified and de-aliased prior to checking.
.. code-block:: yaml
shell_injection:
# Start a process using the subprocess module, or one of its wrappers.
subprocess:
- subprocess.Popen
- subprocess.call
# Start a process with a function vulnerable to shell injection.
shell:
- os.system
- os.popen
- popen2.Popen3
- popen2.Popen4
- commands.getoutput
- commands.getstatusoutput
# Start a process with a function that is not vulnerable to shell
injection.
no_shell:
- os.execl
- os.execle
:Example:
.. code-block:: none
>> Issue: Possible wildcard injection in call: subprocess.Popen
Severity: High Confidence: Medium
Location: ./examples/wildcard-injection.py:8
7 o.popen2('/bin/chmod *')
8 subp.Popen('/bin/chown *', shell=True)
9
>> Issue: subprocess call - check for execution of untrusted input.
Severity: Low Confidence: High
Location: ./examples/wildcard-injection.py:11
10 # Not vulnerable to wildcard injection
11 subp.Popen('/bin/rsync *')
12 subp.Popen("/bin/chmod *")
.. seealso::
- https://security.openstack.org
- https://en.wikipedia.org/wiki/Wildcard_character
- http://www.defensecode.com/public/DefenseCode_Unix_WildCards_Gone_Wild.txt
.. versionadded:: 0.9.0
"""
import bandit
from bandit.core import test_properties as test
from bandit.plugins import injection_shell # NOTE(tkelsey): shared config
gen_config = injection_shell.gen_config
@test.takes_config('shell_injection')
@test.checks('Call')
@test.test_id('B609')
def linux_commands_wildcard_injection(context, config):
if not ('shell' in config and 'subprocess' in config):
return
vulnerable_funcs = ['chown', 'chmod', 'tar', 'rsync']
if context.call_function_name_qual in config['shell'] or (
context.call_function_name_qual in config['subprocess'] and
context.check_call_arg_value('shell', 'True')):
if context.call_args_count >= 1:
call_argument = context.get_call_arg_at_position(0)
argument_string = ''
if isinstance(call_argument, list):
for li in call_argument:
argument_string = argument_string + ' %s' % li
elif isinstance(call_argument, str):
argument_string = call_argument
if argument_string != '':
for vulnerable_func in vulnerable_funcs:
if(
vulnerable_func in argument_string and
'*' in argument_string
):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
text="Possible wildcard injection in call: %s" %
context.call_function_name_qual,
lineno=context.get_lineno_for_call_arg('shell'),
)

View File

@ -1,274 +0,0 @@
# -*- 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 bandit
from bandit.core import test_properties as test
def get_bad_proto_versions(config):
return config['bad_protocol_versions']
def gen_config(name):
if name == 'ssl_with_bad_version':
return {'bad_protocol_versions':
['PROTOCOL_SSLv2',
'SSLv2_METHOD',
'SSLv23_METHOD',
'PROTOCOL_SSLv3', # strict option
'PROTOCOL_TLSv1', # strict option
'SSLv3_METHOD', # strict option
'TLSv1_METHOD']} # strict option
@test.takes_config
@test.checks('Call')
@test.test_id('B502')
def ssl_with_bad_version(context, config):
"""**B502: Test for SSL use with bad version used**
Several highly publicized exploitable flaws have been discovered
in all versions of SSL and early versions of TLS. It is strongly
recommended that use of the following known broken protocol versions be
avoided:
- SSL v2
- SSL v3
- TLS v1
- TLS v1.1
This plugin test scans for calls to Python methods with parameters that
indicate the used broken SSL/TLS protocol versions. Currently, detection
supports methods using Python's native SSL/TLS support and the pyOpenSSL
module. A HIGH severity warning will be reported whenever known broken
protocol versions are detected.
It is worth noting that native support for TLS 1.2 is only available in
more recent Python versions, specifically 2.7.9 and up, and 3.x
See also:
- :doc:`../plugins/ssl_with_bad_defaults`
- :doc:`../plugins/ssl_with_no_version`
A note on 'SSLv23':
Amongst the available SSL/TLS versions provided by Python/pyOpenSSL there
exists the option to use SSLv23. This very poorly named option actually
means "use the highest version of SSL/TLS supported by both the server and
client". This may (and should be) a version well in advance of SSL v2 or
v3. Bandit can scan for the use of SSLv23 if desired, but its detection
does not necessarily indicate a problem.
When using SSLv23 it is important to also provide flags to explicitly
exclude bad versions of SSL/TLS from the protocol versions considered. Both
the Python native and pyOpenSSL modules provide the ``OP_NO_SSLv2`` and
``OP_NO_SSLv3`` flags for this purpose.
**Config Options:**
.. code-block:: yaml
ssl_with_bad_version:
bad_protocol_versions:
- PROTOCOL_SSLv2
- SSLv2_METHOD
- SSLv23_METHOD
- PROTOCOL_SSLv3 # strict option
- PROTOCOL_TLSv1 # strict option
- SSLv3_METHOD # strict option
- TLSv1_METHOD # strict option
:Example:
.. code-block:: none
>> Issue: ssl.wrap_socket call with insecure SSL/TLS protocol version
identified, security issue.
Severity: High Confidence: High
Location: ./examples/ssl-insecure-version.py:13
12 # strict tests
13 ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3)
14 ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1)
.. seealso::
- http://heartbleed.com/
- https://poodlebleed.com/
- https://security.openstack.org/
- https://security.openstack.org/guidelines/dg_move-data-securely.html
.. versionadded:: 0.9.0
"""
bad_ssl_versions = get_bad_proto_versions(config)
if context.call_function_name_qual == 'ssl.wrap_socket':
if context.check_call_arg_value('ssl_version', bad_ssl_versions):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="ssl.wrap_socket call with insecure SSL/TLS protocol "
"version identified, security issue.",
lineno=context.get_lineno_for_call_arg('ssl_version'),
)
elif context.call_function_name_qual == 'pyOpenSSL.SSL.Context':
if context.check_call_arg_value('method', bad_ssl_versions):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="SSL.Context call with insecure SSL/TLS protocol "
"version identified, security issue.",
lineno=context.get_lineno_for_call_arg('method'),
)
elif (context.call_function_name_qual != 'ssl.wrap_socket' and
context.call_function_name_qual != 'pyOpenSSL.SSL.Context'):
if (context.check_call_arg_value('method', bad_ssl_versions) or
context.check_call_arg_value('ssl_version', bad_ssl_versions)):
lineno = (context.get_lineno_for_call_arg('method') or
context.get_lineno_for_call_arg('ssl_version'))
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
text="Function call with insecure SSL/TLS protocol "
"identified, possible security issue.",
lineno=lineno,
)
@test.takes_config("ssl_with_bad_version")
@test.checks('FunctionDef')
@test.test_id('B503')
def ssl_with_bad_defaults(context, config):
"""**B503: Test for SSL use with bad defaults specified**
This plugin is part of a family of tests that detect the use of known bad
versions of SSL/TLS, please see :doc:`../plugins/ssl_with_bad_version` for
a complete discussion. Specifically, this plugin test scans for Python
methods with default parameter values that specify the use of broken
SSL/TLS protocol versions. Currently, detection supports methods using
Python's native SSL/TLS support and the pyOpenSSL module. A MEDIUM severity
warning will be reported whenever known broken protocol versions are
detected.
See also:
- :doc:`../plugins/ssl_with_bad_version`
- :doc:`../plugins/ssl_with_no_version`
**Config Options:**
This test shares the configuration provided for the standard
:doc:`../plugins/ssl_with_bad_version` test, please refer to its
documentation.
:Example:
.. code-block:: none
>> Issue: Function definition identified with insecure SSL/TLS protocol
version by default, possible security issue.
Severity: Medium Confidence: Medium
Location: ./examples/ssl-insecure-version.py:28
27
28 def open_ssl_socket(version=SSL.SSLv2_METHOD):
29 pass
.. seealso::
- http://heartbleed.com/
- https://poodlebleed.com/
- https://security.openstack.org/
- https://security.openstack.org/guidelines/dg_move-data-securely.html
.. versionadded:: 0.9.0
"""
bad_ssl_versions = get_bad_proto_versions(config)
for default in context.function_def_defaults_qual:
val = default.split(".")[-1]
if val in bad_ssl_versions:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
text="Function definition identified with insecure SSL/TLS "
"protocol version by default, possible security "
"issue."
)
@test.checks('Call')
@test.test_id('B504')
def ssl_with_no_version(context):
"""**B504: Test for SSL use with no version specified**
This plugin is part of a family of tests that detect the use of known bad
versions of SSL/TLS, please see :doc:`../plugins/ssl_with_bad_version` for
a complete discussion. Specifically, This plugin test scans for specific
methods in Python's native SSL/TLS support and the pyOpenSSL module that
configure the version of SSL/TLS protocol to use. These methods are known
to provide default value that maximize compatibility, but permit use of the
aforementioned broken protocol versions. A LOW severity warning will be
reported whenever this is detected.
See also:
- :doc:`../plugins/ssl_with_bad_version`
- :doc:`../plugins/ssl_with_bad_defaults`
**Config Options:**
This test shares the configuration provided for the standard
:doc:`../plugins/ssl_with_bad_version` test, please refer to its
documentation.
:Example:
.. code-block:: none
>> Issue: ssl.wrap_socket call with no SSL/TLS protocol version
specified, the default SSLv23 could be insecure, possible security
issue.
Severity: Low Confidence: Medium
Location: ./examples/ssl-insecure-version.py:23
22
23 ssl.wrap_socket()
24
.. seealso::
- http://heartbleed.com/
- https://poodlebleed.com/
- https://security.openstack.org/
- https://security.openstack.org/guidelines/dg_move-data-securely.html
.. versionadded:: 0.9.0
"""
if context.call_function_name_qual == 'ssl.wrap_socket':
if context.check_call_arg_value('ssl_version') is None:
# check_call_arg_value() returns False if the argument is found
# but does not match the supplied value (or the default None).
# It returns None if the arg_name passed doesn't exist. This
# tests for that (ssl_version is not specified).
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
text="ssl.wrap_socket call with no SSL/TLS protocol version "
"specified, the default SSLv23 could be insecure, "
"possible security issue.",
lineno=context.get_lineno_for_call_arg('ssl_version'),
)

View File

@ -1,131 +0,0 @@
# -*- 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.
r"""
==========================================
B701: Test for not auto escaping in jinja2
==========================================
Jinja2 is a Python HTML templating system. It is typically used to build web
applications, though appears in other places well, notably the Ansible
automation system. When configuring the Jinja2 environment, the option to use
autoescaping on input can be specified. When autoescaping is enabled, Jinja2
will filter input strings to escape any HTML content submitted via template
variables. Without escaping HTML input the application becomes vulnerable to
Cross Site Scripting (XSS) attacks.
Unfortunately, autoescaping is False by default. Thus this plugin test will
warn on omission of an autoescape setting, as well as an explicit setting of
false. A HIGH severity warning is generated in either of these scenarios.
:Example:
.. code-block:: none
>> Issue: Using jinja2 templates with autoescape=False is dangerous and can
lead to XSS. Use autoescape=True to mitigate XSS vulnerabilities.
Severity: High Confidence: High
Location: ./examples/jinja2_templating.py:11
10 templateEnv = jinja2.Environment(autoescape=False,
loader=templateLoader)
11 Environment(loader=templateLoader,
12 load=templateLoader,
13 autoescape=False)
14
>> Issue: By default, jinja2 sets autoescape to False. Consider using
autoescape=True or use the select_autoescape function to mitigate XSS
vulnerabilities.
Severity: High Confidence: High
Location: ./examples/jinja2_templating.py:15
14
15 Environment(loader=templateLoader,
16 load=templateLoader)
17
18 Environment(autoescape=select_autoescape(['html', 'htm', 'xml']),
19 loader=templateLoader)
.. seealso::
- `OWASP XSS <https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)>`_
- https://realpython.com/blog/python/primer-on-jinja-templating/
- http://jinja.pocoo.org/docs/dev/api/#autoescaping
- https://security.openstack.org
- https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html
.. versionadded:: 0.10.0
"""
import ast
import bandit
from bandit.core import test_properties as test
@test.checks('Call')
@test.test_id('B701')
def jinja2_autoescape_false(context):
# check type just to be safe
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split('.')
func = qualname_list[-1]
if 'jinja2' in qualname_list and func == 'Environment':
for node in ast.walk(context.node):
if isinstance(node, ast.keyword):
# definite autoescape = False
if (getattr(node, 'arg', None) == 'autoescape' and
(getattr(node.value, 'id', None) == 'False' or
getattr(node.value, 'value', None) is False)):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="Using jinja2 templates with autoescape="
"False is dangerous and can lead to XSS. "
"Use autoescape=True or use the "
"select_autoescape function to mitigate XSS "
"vulnerabilities."
)
# found autoescape
if getattr(node, 'arg', None) == 'autoescape':
value = getattr(node, 'value', None)
if (getattr(value, 'id', None) == 'True' or
getattr(value, 'value', None) is True):
return
# Check if select_autoescape function is used.
elif isinstance(value, ast.Call) and getattr(
value.func, 'id', None) == 'select_autoescape':
return
else:
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
text="Using jinja2 templates with autoescape="
"False is dangerous and can lead to XSS. "
"Ensure autoescape=True or use the "
"select_autoescape function to mitigate "
"XSS vulnerabilities."
)
# We haven't found a keyword named autoescape, indicating default
# behavior
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="By default, jinja2 sets autoescape to False. Consider "
"using autoescape=True or use the select_autoescape "
"function to mitigate XSS vulnerabilities."
)

View File

@ -1,76 +0,0 @@
# -*- coding:utf-8 -*-
#
# 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.
r"""
====================================
B702: Test for use of mako templates
====================================
Mako is a Python templating system often used to build web applications. It is
the default templating system used in Pylons and Pyramid. Unlike Jinja2 (an
alternative templating system), Mako has no environment wide variable escaping
mechanism. Because of this, all input variables must be carefully escaped
before use to prevent possible vulnerabilities to Cross Site Scripting (XSS)
attacks.
:Example:
.. code-block:: none
>> Issue: Mako templates allow HTML/JS rendering by default and are
inherently open to XSS attacks. Ensure variables in all templates are
properly sanitized via the 'n', 'h' or 'x' flags (depending on context).
For example, to HTML escape the variable 'data' do ${ data |h }.
Severity: Medium Confidence: High
Location: ./examples/mako_templating.py:10
9
10 mako.template.Template("hern")
11 template.Template("hern")
.. seealso::
- http://www.makotemplates.org/
- `OWASP XSS <https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)>`_
- https://security.openstack.org
- https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html
.. versionadded:: 0.10.0
"""
import bandit
from bandit.core import test_properties as test
@test.checks('Call')
@test.test_id('B702')
def use_of_mako_templates(context):
# check type just to be safe
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split('.')
func = qualname_list[-1]
if 'mako' in qualname_list and func == 'Template':
# unlike Jinja2, mako does not have a template wide autoescape
# feature and thus each variable must be carefully sanitized.
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
text="Mako templates allow HTML/JS rendering by default and "
"are inherently open to XSS attacks. Ensure variables "
"in all templates are properly sanitized via the 'n', "
"'h' or 'x' flags (depending on context). For example, "
"to HTML escape the variable 'data' do ${ data |h }."
)

View File

@ -1,115 +0,0 @@
# Copyright (c) 2015 VMware, Inc.
#
# 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.
r"""
===============================================================
B109: Test for a password based config option not marked secret
===============================================================
Passwords are sensitive and must be protected appropriately. In OpenStack
Oslo there is an option to mark options "secret" which will ensure that they
are not logged. This plugin detects usages of oslo configuration functions
that appear to deal with strings ending in 'password' and flag usages where
they have not been marked secret.
If such a value is found a MEDIUM severity error is generated. If 'False' or
'None' are explicitly set, Bandit will return a MEDIUM confidence issue. If
Bandit can't determine the value of secret it will return a LOW confidence
issue.
**Config Options:**
.. code-block:: yaml
password_config_option_not_marked_secret:
function_names:
- oslo.config.cfg.StrOpt
- oslo_config.cfg.StrOpt
:Example:
.. code-block:: none
>> Issue: [password_config_option_not_marked_secret] oslo config option
possibly not marked secret=True identified.
Severity: Medium Confidence: Low
Location: examples/secret-config-option.py:12
11 help="User's password"),
12 cfg.StrOpt('nova_password',
13 secret=secret,
14 help="Nova user password"),
15 ]
>> Issue: [password_config_option_not_marked_secret] oslo config option not
marked secret=True identified, security issue.
Severity: Medium Confidence: Medium
Location: examples/secret-config-option.py:21
20 help="LDAP ubind ser name"),
21 cfg.StrOpt('ldap_password',
22 help="LDAP bind user password"),
23 cfg.StrOpt('ldap_password_attribute',
.. seealso::
- https://security.openstack.org/guidelines/dg_protect-sensitive-data-in-files.html # noqa
.. versionadded:: 0.10.0
"""
import bandit
from bandit.core import constants
from bandit.core import test_properties as test
def gen_config(name):
if name == 'password_config_option_not_marked_secret':
return {'function_names':
['oslo.config.cfg.StrOpt',
'oslo_config.cfg.StrOpt']}
@test.takes_config
@test.checks('Call')
@test.test_id('B109')
def password_config_option_not_marked_secret(context, config):
if(context.call_function_name_qual in config['function_names'] and
context.get_call_arg_at_position(0) is not None and
context.get_call_arg_at_position(0).endswith('password')):
# Checks whether secret=False or secret is not set (None).
# Returns True if argument found, and matches supplied values
# and None if argument not found at all.
if context.check_call_arg_value('secret',
constants.FALSE_VALUES) in [
True, None]:
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
text="oslo config option not marked secret=True "
"identified, security issue.",
lineno=context.get_lineno_for_call_arg('secret'),
)
# Checks whether secret is not True, for example when its set to a
# variable, secret=secret.
elif not context.check_call_arg_value('secret', 'True'):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.LOW,
text="oslo config option possibly not marked secret=True "
"identified.",
lineno=context.get_lineno_for_call_arg('secret'),
)

View File

@ -1,110 +0,0 @@
# Copyright 2016 IBM Corp.
# 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.
r"""
=============================================
B112: Test for a continue in the except block
=============================================
Errors in Python code bases are typically communicated using ``Exceptions``.
An exception object is 'raised' in the event of an error and can be 'caught' at
a later point in the program, typically some error handling or logging action
will then be performed.
However, it is possible to catch an exception and silently ignore it while in
a loop. This is illustrated with the following example
.. code-block:: python
while keep_going:
try:
do_some_stuff()
except Exception:
continue
This pattern is considered bad practice in general, but also represents a
potential security issue. A larger than normal volume of errors from a service
can indicate an attempt is being made to disrupt or interfere with it. Thus
errors should, at the very least, be logged.
There are rare situations where it is desirable to suppress errors, but this is
typically done with specific exception types, rather than the base Exception
class (or no type). To accommodate this, the test may be configured to ignore
'try, except, continue' where the exception is typed. For example, the
following would not generate a warning if the configuration option
``checked_typed_exception`` is set to False:
.. code-block:: python
while keep_going:
try:
do_some_stuff()
except ZeroDivisionError:
continue
**Config Options:**
.. code-block:: yaml
try_except_continue:
check_typed_exception: True
:Example:
.. code-block:: none
>> Issue: Try, Except, Continue detected.
Severity: Low Confidence: High
Location: ./examples/try_except_continue.py:5
4 a = i
5 except:
6 continue
.. seealso::
- https://security.openstack.org
.. versionadded:: 1.0.0
"""
import ast
import bandit
from bandit.core import test_properties as test
def gen_config(name):
if name == 'try_except_continue':
return {'check_typed_exception': False}
@test.takes_config
@test.checks('ExceptHandler')
@test.test_id('B112')
def try_except_continue(context, config):
node = context.node
if len(node.body) == 1:
if (not config['check_typed_exception'] and
node.type is not None and
getattr(node.type, 'id', None) != 'Exception'):
return
if isinstance(node.body[0], ast.Continue):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text=("Try, Except, Continue detected."))

View File

@ -1,110 +0,0 @@
# -*- 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.
r"""
=========================================
B110: Test for a pass in the except block
=========================================
Errors in Python code bases are typically communicated using ``Exceptions``.
An exception object is 'raised' in the event of an error and can be 'caught' at
a later point in the program, typically some error handling or logging action
will then be performed.
However, it is possible to catch an exception and silently ignore it. This is
illustrated with the following example
.. code-block:: python
try:
do_some_stuff()
except Exception:
pass
This pattern is considered bad practice in general, but also represents a
potential security issue. A larger than normal volume of errors from a service
can indicate an attempt is being made to disrupt or interfere with it. Thus
errors should, at the very least, be logged.
There are rare situations where it is desirable to suppress errors, but this is
typically done with specific exception types, rather than the base Exception
class (or no type). To accommodate this, the test may be configured to ignore
'try, except, pass' where the exception is typed. For example, the following
would not generate a warning if the configuration option
``checked_typed_exception`` is set to False:
.. code-block:: python
try:
do_some_stuff()
except ZeroDivisionError:
pass
**Config Options:**
.. code-block:: yaml
try_except_pass:
check_typed_exception: True
:Example:
.. code-block:: none
>> Issue: Try, Except, Pass detected.
Severity: Low Confidence: High
Location: ./examples/try_except_pass.py:4
3 a = 1
4 except:
5 pass
.. seealso::
- https://security.openstack.org
.. versionadded:: 0.13.0
"""
import ast
import bandit
from bandit.core import test_properties as test
def gen_config(name):
if name == 'try_except_pass':
return {'check_typed_exception': False}
@test.takes_config
@test.checks('ExceptHandler')
@test.test_id('B110')
def try_except_pass(context, config):
node = context.node
if len(node.body) == 1:
if (not config['check_typed_exception'] and
node.type is not None and
getattr(node.type, 'id', None) != 'Exception'):
return
if isinstance(node.body[0], ast.Pass):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.HIGH,
text=("Try, Except, Pass detected.")
)

View File

@ -1,140 +0,0 @@
# Copyright (c) 2015 VMware, Inc.
#
# 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.
r"""
=========================================
B505: Test for weak cryptographic key use
=========================================
As computational power increases, so does the ability to break ciphers with
smaller key lengths. The recommended key length size for RSA and DSA algorithms
is 2048 and higher. 1024 bits and below are now considered breakable. EC key
length sizes are recommended to be 224 and higher with 160 and below considered
breakable. This plugin test checks for use of any key less than those limits
and returns a high severity error if lower than the lower threshold and a
medium severity error for those lower than the higher threshold.
:Example:
.. code-block:: none
>> Issue: DSA key sizes below 1024 bits are considered breakable.
Severity: High Confidence: High
Location: examples/weak_cryptographic_key_sizes.py:36
35 # Also incorrect: without keyword args
36 dsa.generate_private_key(512,
37 backends.default_backend())
38 rsa.generate_private_key(3,
.. seealso::
- http://csrc.nist.gov/publications/nistpubs/800-131A/sp800-131A.pdf
- https://security.openstack.org/guidelines/dg_strong-crypto.html
.. versionadded:: 0.14.0
"""
import bandit
from bandit.core import test_properties as test
def gen_config(name):
if name == 'weak_cryptographic_key':
return {
'weak_key_size_dsa_high': 1024,
'weak_key_size_dsa_medium': 2048,
'weak_key_size_rsa_high': 1024,
'weak_key_size_rsa_medium': 2048,
'weak_key_size_ec_high': 160,
'weak_key_size_ec_medium': 224,
}
def _classify_key_size(config, key_type, key_size):
if isinstance(key_size, str):
# size provided via a variable - can't process it at the moment
return
key_sizes = {
'DSA': [(config['weak_key_size_dsa_high'], bandit.HIGH),
(config['weak_key_size_dsa_medium'], bandit.MEDIUM)],
'RSA': [(config['weak_key_size_rsa_high'], bandit.HIGH),
(config['weak_key_size_rsa_medium'], bandit.MEDIUM)],
'EC': [(config['weak_key_size_ec_high'], bandit.HIGH),
(config['weak_key_size_ec_medium'], bandit.MEDIUM)],
}
for size, level in key_sizes[key_type]:
if key_size < size:
return bandit.Issue(
severity=level,
confidence=bandit.HIGH,
text='%s key sizes below %d bits are considered breakable. ' %
(key_type, size))
def _weak_crypto_key_size_cryptography_io(context, config):
func_key_type = {
'cryptography.hazmat.primitives.asymmetric.dsa.'
'generate_private_key': 'DSA',
'cryptography.hazmat.primitives.asymmetric.rsa.'
'generate_private_key': 'RSA',
'cryptography.hazmat.primitives.asymmetric.ec.'
'generate_private_key': 'EC',
}
arg_position = {
'DSA': 0,
'RSA': 1,
'EC': 0,
}
key_type = func_key_type.get(context.call_function_name_qual)
if key_type in ['DSA', 'RSA']:
key_size = (context.get_call_arg_value('key_size') or
context.get_call_arg_at_position(arg_position[key_type]) or
2048)
return _classify_key_size(config, key_type, key_size)
elif key_type == 'EC':
curve_key_sizes = {
'SECP192R1': 192,
'SECT163K1': 163,
'SECT163R2': 163,
}
curve = (context.get_call_arg_value('curve') or
context.call_args[arg_position[key_type]])
key_size = curve_key_sizes[curve] if curve in curve_key_sizes else 224
return _classify_key_size(config, key_type, key_size)
def _weak_crypto_key_size_pycrypto(context, config):
func_key_type = {
'Crypto.PublicKey.DSA.generate': 'DSA',
'Crypto.PublicKey.RSA.generate': 'RSA',
'Cryptodome.PublicKey.DSA.generate': 'DSA',
'Cryptodome.PublicKey.RSA.generate': 'RSA',
}
key_type = func_key_type.get(context.call_function_name_qual)
if key_type:
key_size = (context.get_call_arg_value('bits') or
context.get_call_arg_at_position(0) or
2048)
return _classify_key_size(config, key_type, key_size)
@test.takes_config
@test.checks('Call')
@test.test_id('B505')
def weak_cryptographic_key(context, config):
return (_weak_crypto_key_size_cryptography_io(context, config) or
_weak_crypto_key_size_pycrypto(context, config))

View File

@ -1,69 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright (c) 2016 Rackspace, Inc.
#
# 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.
r"""
===============================
B506: Test for use of yaml load
===============================
This plugin test checks for the unsafe usage of the ``yaml.load`` function from
the PyYAML package. The yaml.load function provides the ability to construct
an arbitrary Python object, which may be dangerous if you receive a YAML
document from an untrusted source. The function yaml.safe_load limits this
ability to simple Python objects like integers or lists.
Please see
http://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML for more information
on ``yaml.load`` and yaml.safe_load
:Example:
>> Issue: [yaml_load] Use of unsafe yaml load. Allows instantiation of
arbitrary objects. Consider yaml.safe_load().
Severity: Medium Confidence: High
Location: examples/yaml_load.py:5
4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
5 y = yaml.load(ystr)
6 yaml.dump(y)
.. seealso::
- http://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML
.. versionadded:: 1.0.0
"""
import bandit
from bandit.core import test_properties as test
@test.test_id('B506')
@test.checks('Call')
def yaml_load(context):
if isinstance(context.call_function_name_qual, str):
qualname_list = context.call_function_name_qual.split('.')
func = qualname_list[-1]
if 'yaml' in qualname_list and func == 'load':
if not context.check_call_arg_value('Loader', 'SafeLoader'):
return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.HIGH,
text="Use of unsafe yaml load. Allows instantiation of"
" arbitrary objects. Consider yaml.safe_load().",
lineno=context.node.lineno,
)

View File

@ -1,2 +0,0 @@
# This is a cross-platform list tracking distribution packages needed by tests;
# see http://docs.openstack.org/infra/bindep/ for additional information.

View File

@ -1,7 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
openstackdocstheme>=1.18.1 # Apache-2.0
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
reno>=2.5.0 # Apache-2.0
oslosphinx>=4.7.0 # Apache-2.0

View File

@ -1,5 +0,0 @@
---------------
blacklist_calls
---------------
.. automodule:: bandit.blacklists.calls

View File

@ -1,5 +0,0 @@
-----------------
blacklist_imports
-----------------
.. automodule:: bandit.blacklists.imports

View File

@ -1,69 +0,0 @@
Bandit Blacklist Plugins
========================
Bandit supports built in functionality to implement blacklisting of imports and
function calls, this functionality is provided by built in test 'B001'. This
test may be filtered as per normal plugin filtering rules.
The exact calls and imports that are blacklisted, and the issues reported, are
controlled by plugin methods with the entry point 'bandit.blacklists' and can
be extended by third party plugins if desired. Blacklist plugins will be
discovered by Bandit at startup and called. The returned results are combined
into the final data set, subject to Bandit's normal test include/exclude rules
allowing for fine grained control over blacklisted items. By convention,
blacklisted calls should have IDs in the B3xx range and imports should have IDs
in the B4xx range.
Plugin functions should return a dictionary mapping AST node types to
lists of blacklist data. Currently the following node types are supported:
- Call, used for blacklisting calls.
- Import, used for blacklisting module imports (this also implicitly tests
ImportFrom and Call nodes where the invoked function is Pythons built in
'__import__()' method).
Items in the data lists are Python dictionaries with the following structure:
+-------------+----------------------------------------------------+
| key | data meaning |
+=============+====================================================+
| 'name' | The issue name string. |
+-------------+----------------------------------------------------+
| 'id' | The bandit ID of the check, this must be unique |
| | and is used for filtering blacklist checks. |
+-------------+----------------------------------------------------+
| 'qualnames' | A Python list of fully qualified name strings. |
+-------------+----------------------------------------------------+
| 'message' | The issue message reported, this is a string that |
| | may contain the token '{name}' that will be |
| | substituted with the matched qualname in the final |
| | report. |
+-------------+----------------------------------------------------+
| 'level' | The severity level reported. |
+-------------+----------------------------------------------------+
A utility method bandit.blacklists.utils.build_conf_dict is provided to aid
building these dictionaries.
:Example:
.. code-block:: none
>> Issue: [B317:blacklist] Using xml.sax.parse to parse untrusted XML data
is known to be vulnerable to XML attacks. Replace xml.sax.parse with its
defusedxml equivalent function.
Severity: Medium Confidence: High
Location: ./examples/xml_sax.py:24
23 sax.parseString(xmlString, ExampleContentHandler())
24 sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler)
25
Complete Plugin Listing
-----------------------
.. toctree::
:maxdepth: 1
:glob:
*
.. versionadded:: 0.17.0

View File

@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
# 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 os
import sys
sys.path.insert(0, os.path.abspath('../..'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
# 'sphinx.ext.intersphinx',
'oslosphinx'
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Bandit'
copyright = u'2016, OpenStack Foundation'
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
modindex_common_prefix = ['bandit.']
#-- Options for man page output --------------------------------------------
# Grouping the document tree for man pages.
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
man_pages = [
('man/bandit', 'bandit', u'Python source code security analyzer',
[u'OpenStack Security Group'], 1)
]
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
html_theme_options = {}
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'),
]
# Example configuration for intersphinx: refer to the Python standard library.
# intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -1,82 +0,0 @@
Configuration
=============
Bandit is designed to be configurable and cover a wide range of needs, it may
be used as either a local developer utility or as part of a full CI/CD
pipeline. To provide for these various usage scenarios bandit can be configured
via a `YAML <http://yaml.org/>`_ file. This file is completely optional and in
many cases not needed, it may be specified on the command line by using `-c`.
A bandit configuration file may choose the specific test plugins to run and
override the default configurations of those tests. An example config might
look like the following:
.. code-block:: yaml
### profile may optionally select or skip tests
# (optional) list included tests here:
tests: ['B201', 'B301']
# (optional) list skipped tests here:
skips: ['B101', 'B601']
### override settings - used to set settings for plugins to non-default values
any_other_function_with_shell_equals_true:
no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve,
os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, os.startfile]
shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4,
popen2.popen2, popen2.popen3, popen2.popen4, popen2.Popen3,
popen2.Popen4, commands.getoutput, commands.getstatusoutput]
subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call,
subprocess.check_output,
utils.execute, utils.execute_with_timeout]
If you require several sets of tests for specific tasks, then you should create
several config files and pick from them using `-c`. If you only wish to control
the specific tests that are to be run (and not their parameters) then using
`-s` or `-t` on the command line may be more appropriate.
Skipping Tests
--------------
The bandit config may contain optional lists of test IDs to either include
(`tests`) or exclude (`skips`). These lists are equivalent to using `-t` and
`-s` on the command line. If only `tests` is given then bandit will include
only those tests, effectively excluding all other tests. If only `skips`
is given then bandit will include all tests not in the skips list. If both are
given then bandit will include only tests in `tests` and then remove `skips`
from that set. It is an error to include the same test ID in both `tests` and
`skips`.
Note that command line options `-t`/`-s` can still be used in conjunction with
`tests` and `skips` given in a config. The result is to concatenate `-t` with
`tests` and likewise for `-s` and `skips` before working out the tests to run.
Generating a Config
-------------------
Bandit ships the tool `bandit-config-generator` designed to take the leg work
out of configuration. This tool can generate a configuration file
automatically. The generated configuration will include default config blocks
for all detected test and blacklist plugins. This data can then be deleted or
edited as needed to produce a minimal config as desired. The config generator
supports `-t` and `-s` command line options to specify a list of test IDs that
should be included or excluded respectively. If no options are given then the
generated config will not include `tests` or `skips` sections (but will provide
a complete list of all test IDs for reference when editing).
Configuring Test Plugins
------------------------
Bandit's configuration file is written in `YAML <http://yaml.org/>`_ and options
for each plugin test are provided under a section named to match the test
method. For example, given a test plugin called 'try_except_pass' its
configuration section might look like the following:
.. code-block:: yaml
try_except_pass:
check_typed_exception: True
The specific content of the configuration block is determined by the plugin
test itself. See the `plugin test list <plugins/index.html>`_ for complete
information on configuring each one.

View File

@ -1,5 +0,0 @@
---
csv
---
.. automodule:: bandit.formatters.csv

View File

@ -1,5 +0,0 @@
----
html
----
.. automodule:: bandit.formatters.html

View File

@ -1,42 +0,0 @@
Bandit Report Formatters
========================
Bandit supports many different formatters to output various security issues in
python code. These formatters are created as plugins and new ones can be
created to extend the functionality offered by bandit today.
Example Formatter
-----------------
.. code-block:: python
def report(manager, fileobj, sev_level, conf_level, lines=-1):
result = bson.dumps(issues)
with fileobj:
fileobj.write(result)
To register your plugin, you have two options:
1. If you're using setuptools directly, add something like the following to
your `setup` call::
# If you have an imaginary bson formatter in the bandit_bson module
# and a function called `formatter`.
entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']}
2. If you're using pbr, add something like the following to your `setup.cfg`
file::
[entry_points]
bandit.formatters =
bson = bandit_bson:formatter
Complete Formatter Listing
----------------------------
.. toctree::
:maxdepth: 1
:glob:
*

View File

@ -1,5 +0,0 @@
----
json
----
.. automodule:: bandit.formatters.json

View File

@ -1,5 +0,0 @@
------
screen
------
.. automodule:: bandit.formatters.screen

View File

@ -1,5 +0,0 @@
----
text
----
.. automodule:: bandit.formatters.text

View File

@ -1,5 +0,0 @@
---
xml
---
.. automodule:: bandit.formatters.xml

View File

@ -1,5 +0,0 @@
----
yaml
----
.. automodule:: bandit.formatters.yaml

View File

@ -1,27 +0,0 @@
Welcome to Bandit's developer documentation!
============================================
Bandit is a tool designed to find common security issues in Python code. To do
this, Bandit processes each file, builds an AST from it, and runs appropriate
plugins against the AST nodes. Once Bandit has finished scanning all the files,
it generates a report.
This documentation is generated by the Sphinx toolkit and lives in the source
tree.
Getting Started
===============
.. toctree::
:maxdepth: 1
config
plugins/index
blacklists/index
formatters/index
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,128 +0,0 @@
======
bandit
======
SYNOPSIS
========
bandit [-h] [-r] [-a {file,vuln}] [-n CONTEXT_LINES] [-c CONFIG_FILE]
[-p PROFILE] [-t TESTS] [-s SKIPS] [-l] [-i]
[-f {csv,custom,html,json,screen,txt,xml,yaml}]
[--msg-template MSG_TEMPLATE] [-o OUTPUT_FILE] [-v] [-d]
[--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[--ini INI_PATH] [--version]
targets [targets ...]
DESCRIPTION
===========
``bandit`` is a tool designed to find common security issues in Python code. To
do this Bandit processes each file, builds an AST from it, and runs appropriate
plugins against the AST nodes. Once Bandit has finished scanning all the files
it generates a report.
OPTIONS
=======
-h, --help show this help message and exit
-r, --recursive find and process files in subdirectories
-a {file,vuln}, --aggregate {file,vuln}
aggregate output by vulnerability (default) or by
filename
-n CONTEXT_LINES, --number CONTEXT_LINES
maximum number of code lines to output for each issue
-c CONFIG_FILE, --configfile CONFIG_FILE
optional config file to use for selecting plugins and
overriding defaults
-p PROFILE, --profile PROFILE
profile to use (defaults to executing all tests)
-t TESTS, --tests TESTS
comma-separated list of test IDs to run
-s SKIPS, --skip SKIPS
comma-separated list of test IDs to skip
-l, --level report only issues of a given severity level or higher
(-l for LOW, -ll for MEDIUM, -lll for HIGH)
-i, --confidence report only issues of a given confidence level or
higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)
-f {csv,custom,html,json,screen,txt,xml,yaml}, --format {csv,custom,html,json,screen,txt,xml,yaml}
specify output format
--msg-template MSG_TEMPLATE
specify output message template (only usable with
--format custom), see CUSTOM FORMAT section for list
of available values
-o OUTPUT_FILE, --output OUTPUT_FILE
write report to filename
-v, --verbose output extra information like excluded and included
files
-d, --debug turn on debug mode
--ignore-nosec do not skip lines with # nosec comments
-x EXCLUDED_PATHS, --exclude EXCLUDED_PATHS
comma-separated list of paths to exclude from scan
(note that these are in addition to the excluded paths
provided in the config file)
-b BASELINE, --baseline BASELINE
path of a baseline report to compare against (only
JSON-formatted files are accepted)
--ini INI_PATH path to a .bandit file that supplies command line
arguments
--version show program's version number and exit
CUSTOM FORMATTING
-----------------
Available tags:
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template:
bandit -r examples/ --format custom --msg-template \
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as:
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style:
bandit -r examples/ --format custom --msg-template \
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
FILES
=====
.bandit
file that supplies command line arguments
/etc/bandit/bandit.yaml
legacy bandit configuration file
EXAMPLES
========
Example usage across a code tree::
bandit -r ~/openstack-repo/keystone
Example usage across the ``examples/`` directory, showing three lines of
context and only reporting on the high-severity issues::
bandit examples/*.py -n 3 -lll
Bandit can be run with profiles. To run Bandit against the examples directory
using only the plugins listed in the ShellInjection profile::
bandit examples/*.py -p ShellInjection
Bandit also supports passing lines of code to scan using standard input. To
run Bandit with standard input::
cat examples/imports.py | bandit -
SEE ALSO
========
pylint(1)

View File

@ -1,5 +0,0 @@
-----------------
B101: assert_used
-----------------
.. automodule:: bandit.plugins.asserts

View File

@ -1,5 +0,0 @@
---------------
B102: exec_used
---------------
.. automodule:: bandit.plugins.exec

View File

@ -1,5 +0,0 @@
------------------------------
B103: set_bad_file_permissions
------------------------------
.. automodule:: bandit.plugins.general_bad_file_permissions

View File

@ -1,5 +0,0 @@
-----------------------------------
B104: hardcoded_bind_all_interfaces
-----------------------------------
.. automodule:: bandit.plugins.general_bind_all_interfaces

View File

@ -1,8 +0,0 @@
-------------------------------
B105: hardcoded_password_string
-------------------------------
.. currentmodule:: bandit.plugins.general_hardcoded_password
.. autofunction:: hardcoded_password_string
:noindex:

View File

@ -1,8 +0,0 @@
--------------------------------
B106: hardcoded_password_funcarg
--------------------------------
.. currentmodule:: bandit.plugins.general_hardcoded_password
.. autofunction:: hardcoded_password_funcarg
:noindex:

View File

@ -1,8 +0,0 @@
--------------------------------
B107: hardcoded_password_default
--------------------------------
.. currentmodule:: bandit.plugins.general_hardcoded_password
.. autofunction:: hardcoded_password_default
:noindex:

View File

@ -1,5 +0,0 @@
-----------------------------
B108: hardcoded_tmp_directory
-----------------------------
.. automodule:: bandit.plugins.general_hardcoded_tmp

View File

@ -1,5 +0,0 @@
----------------------------------------------
B109: password_config_option_not_marked_secret
----------------------------------------------
.. automodule:: bandit.plugins.secret_config_option

View File

@ -1,5 +0,0 @@
---------------------
B110: try_except_pass
---------------------
.. automodule:: bandit.plugins.try_except_pass

View File

@ -1,5 +0,0 @@
------------------------------------------
B111: execute_with_run_as_root_equals_true
------------------------------------------
.. automodule:: bandit.plugins.exec_as_root

View File

@ -1,5 +0,0 @@
-------------------------
B112: try_except_continue
-------------------------
.. automodule:: bandit.plugins.try_except_continue

View File

@ -1,5 +0,0 @@
----------------------
B201: flask_debug_true
----------------------
.. automodule:: bandit.plugins.app_debug

View File

@ -1,5 +0,0 @@
-------------------------------------
B501: request_with_no_cert_validation
-------------------------------------
.. automodule:: bandit.plugins.crypto_request_no_cert_validation

View File

@ -1,8 +0,0 @@
--------------------------
B502: ssl_with_bad_version
--------------------------
.. currentmodule:: bandit.plugins.insecure_ssl_tls
.. autofunction:: ssl_with_bad_version
:noindex:

View File

@ -1,8 +0,0 @@
---------------------------
B503: ssl_with_bad_defaults
---------------------------
.. currentmodule:: bandit.plugins.insecure_ssl_tls
.. autofunction:: ssl_with_bad_defaults
:noindex:

View File

@ -1,8 +0,0 @@
-------------------------
B504: ssl_with_no_version
-------------------------
.. currentmodule:: bandit.plugins.insecure_ssl_tls
.. autofunction:: ssl_with_no_version
:noindex:

View File

@ -1,5 +0,0 @@
----------------------------
B505: weak_cryptographic_key
----------------------------
.. automodule:: bandit.plugins.weak_cryptographic_key

View File

@ -1,5 +0,0 @@
---------------
B506: yaml_load
---------------
.. automodule:: bandit.plugins.yaml_load

Some files were not shown because too many files have changed in this diff Show More