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:
parent
209182c3ee
commit
2d2170273b
@ -1,3 +0,0 @@
|
||||
[report]
|
||||
include = bandit/*
|
||||
omit = bandit/tests/functional/*
|
19
.gitignore
vendored
19
.gitignore
vendored
@ -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
|
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=${OS_TEST_PATH:-./tests/unit}
|
||||
top_dir=./
|
||||
group_regex=.*(test_cert_setup)
|
179
.zuul.yaml
179
.zuul.yaml
@ -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
176
LICENSE
@ -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.
|
||||
|
429
README.rst
429
README.rst
@ -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".
|
||||
|
@ -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()
|
@ -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}
|
@ -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}
|
@ -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}
|
@ -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()
|
@ -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())
|
@ -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()
|
@ -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
|
@ -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)
|
@ -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)
|
@ -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'
|
@ -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
|
@ -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
|
@ -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()
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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])
|
@ -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 []
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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'))
|
@ -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(). " 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)
|
@ -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)
|
@ -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'),
|
||||
)
|
@ -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.")
|
||||
)
|
@ -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
|
@ -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()
|
@ -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'),
|
||||
)
|
@ -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)
|
||||
)
|
@ -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."
|
||||
)
|
@ -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)
|
@ -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."
|
||||
)
|
@ -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,
|
||||
)
|
@ -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)
|
@ -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'
|
||||
)
|
@ -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."
|
||||
)
|
@ -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'),
|
||||
)
|
@ -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'),
|
||||
)
|
@ -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."
|
||||
)
|
@ -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 }."
|
||||
)
|
@ -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'),
|
||||
)
|
@ -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."))
|
@ -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.")
|
||||
)
|
@ -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))
|
@ -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,
|
||||
)
|
@ -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.
|
@ -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
|
@ -1,5 +0,0 @@
|
||||
---------------
|
||||
blacklist_calls
|
||||
---------------
|
||||
|
||||
.. automodule:: bandit.blacklists.calls
|
@ -1,5 +0,0 @@
|
||||
-----------------
|
||||
blacklist_imports
|
||||
-----------------
|
||||
|
||||
.. automodule:: bandit.blacklists.imports
|
@ -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
|
@ -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}
|
@ -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.
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
csv
|
||||
---
|
||||
|
||||
.. automodule:: bandit.formatters.csv
|
@ -1,5 +0,0 @@
|
||||
----
|
||||
html
|
||||
----
|
||||
|
||||
.. automodule:: bandit.formatters.html
|
@ -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:
|
||||
|
||||
*
|
@ -1,5 +0,0 @@
|
||||
----
|
||||
json
|
||||
----
|
||||
|
||||
.. automodule:: bandit.formatters.json
|
@ -1,5 +0,0 @@
|
||||
------
|
||||
screen
|
||||
------
|
||||
|
||||
.. automodule:: bandit.formatters.screen
|
@ -1,5 +0,0 @@
|
||||
----
|
||||
text
|
||||
----
|
||||
|
||||
.. automodule:: bandit.formatters.text
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
xml
|
||||
---
|
||||
|
||||
.. automodule:: bandit.formatters.xml
|
@ -1,5 +0,0 @@
|
||||
----
|
||||
yaml
|
||||
----
|
||||
|
||||
.. automodule:: bandit.formatters.yaml
|
@ -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`
|
@ -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)
|
@ -1,5 +0,0 @@
|
||||
-----------------
|
||||
B101: assert_used
|
||||
-----------------
|
||||
|
||||
.. automodule:: bandit.plugins.asserts
|
@ -1,5 +0,0 @@
|
||||
---------------
|
||||
B102: exec_used
|
||||
---------------
|
||||
|
||||
.. automodule:: bandit.plugins.exec
|
@ -1,5 +0,0 @@
|
||||
------------------------------
|
||||
B103: set_bad_file_permissions
|
||||
------------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.general_bad_file_permissions
|
@ -1,5 +0,0 @@
|
||||
-----------------------------------
|
||||
B104: hardcoded_bind_all_interfaces
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.general_bind_all_interfaces
|
@ -1,8 +0,0 @@
|
||||
-------------------------------
|
||||
B105: hardcoded_password_string
|
||||
-------------------------------
|
||||
|
||||
.. currentmodule:: bandit.plugins.general_hardcoded_password
|
||||
|
||||
.. autofunction:: hardcoded_password_string
|
||||
:noindex:
|
@ -1,8 +0,0 @@
|
||||
--------------------------------
|
||||
B106: hardcoded_password_funcarg
|
||||
--------------------------------
|
||||
|
||||
.. currentmodule:: bandit.plugins.general_hardcoded_password
|
||||
|
||||
.. autofunction:: hardcoded_password_funcarg
|
||||
:noindex:
|
@ -1,8 +0,0 @@
|
||||
--------------------------------
|
||||
B107: hardcoded_password_default
|
||||
--------------------------------
|
||||
|
||||
.. currentmodule:: bandit.plugins.general_hardcoded_password
|
||||
|
||||
.. autofunction:: hardcoded_password_default
|
||||
:noindex:
|
@ -1,5 +0,0 @@
|
||||
-----------------------------
|
||||
B108: hardcoded_tmp_directory
|
||||
-----------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.general_hardcoded_tmp
|
@ -1,5 +0,0 @@
|
||||
----------------------------------------------
|
||||
B109: password_config_option_not_marked_secret
|
||||
----------------------------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.secret_config_option
|
@ -1,5 +0,0 @@
|
||||
---------------------
|
||||
B110: try_except_pass
|
||||
---------------------
|
||||
|
||||
.. automodule:: bandit.plugins.try_except_pass
|
@ -1,5 +0,0 @@
|
||||
------------------------------------------
|
||||
B111: execute_with_run_as_root_equals_true
|
||||
------------------------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.exec_as_root
|
@ -1,5 +0,0 @@
|
||||
-------------------------
|
||||
B112: try_except_continue
|
||||
-------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.try_except_continue
|
@ -1,5 +0,0 @@
|
||||
----------------------
|
||||
B201: flask_debug_true
|
||||
----------------------
|
||||
|
||||
.. automodule:: bandit.plugins.app_debug
|
@ -1,5 +0,0 @@
|
||||
-------------------------------------
|
||||
B501: request_with_no_cert_validation
|
||||
-------------------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.crypto_request_no_cert_validation
|
@ -1,8 +0,0 @@
|
||||
--------------------------
|
||||
B502: ssl_with_bad_version
|
||||
--------------------------
|
||||
|
||||
.. currentmodule:: bandit.plugins.insecure_ssl_tls
|
||||
|
||||
.. autofunction:: ssl_with_bad_version
|
||||
:noindex:
|
@ -1,8 +0,0 @@
|
||||
---------------------------
|
||||
B503: ssl_with_bad_defaults
|
||||
---------------------------
|
||||
|
||||
.. currentmodule:: bandit.plugins.insecure_ssl_tls
|
||||
|
||||
.. autofunction:: ssl_with_bad_defaults
|
||||
:noindex:
|
@ -1,8 +0,0 @@
|
||||
-------------------------
|
||||
B504: ssl_with_no_version
|
||||
-------------------------
|
||||
|
||||
.. currentmodule:: bandit.plugins.insecure_ssl_tls
|
||||
|
||||
.. autofunction:: ssl_with_no_version
|
||||
:noindex:
|
@ -1,5 +0,0 @@
|
||||
----------------------------
|
||||
B505: weak_cryptographic_key
|
||||
----------------------------
|
||||
|
||||
.. automodule:: bandit.plugins.weak_cryptographic_key
|
@ -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
Loading…
Reference in New Issue
Block a user