updates
This commit is contained in:
20
Backend/venv/lib/python3.12/site-packages/bandit/__init__.py
Normal file
20
Backend/venv/lib/python3.12/site-packages/bandit/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from importlib import metadata
|
||||
|
||||
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
|
||||
|
||||
__author__ = metadata.metadata("bandit")["Author"]
|
||||
__version__ = metadata.version("bandit")
|
||||
17
Backend/venv/lib/python3.12/site-packages/bandit/__main__.py
Normal file
17
Backend/venv/lib/python3.12/site-packages/bandit/__main__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bandit is a tool designed to find common security issues in Python code.
|
||||
|
||||
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.
|
||||
|
||||
Bandit was originally developed within the OpenStack Security Project and
|
||||
later rehomed to PyCQA.
|
||||
|
||||
https://bandit.readthedocs.io/
|
||||
"""
|
||||
from bandit.cli import main
|
||||
|
||||
main.main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,670 @@
|
||||
#
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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 encountered in the scanned code base, triggered by encountering
|
||||
ast.Call nodes.
|
||||
|
||||
B301: pickle
|
||||
------------
|
||||
|
||||
Pickle and modules that wrap it can be unsafe when used to
|
||||
deserialize untrusted data, possible security issue.
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Calls | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B301 | pickle | - pickle.loads | Medium |
|
||||
| | | - pickle.load | |
|
||||
| | | - pickle.Unpickler | |
|
||||
| | | - dill.loads | |
|
||||
| | | - dill.load | |
|
||||
| | | - dill.Unpickler | |
|
||||
| | | - shelve.open | |
|
||||
| | | - shelve.DbfilenameShelf | |
|
||||
| | | - jsonpickle.decode | |
|
||||
| | | - jsonpickle.unpickler.decode | |
|
||||
| | | - jsonpickle.unpickler.Unpickler | |
|
||||
| | | - pandas.read_pickle | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
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 | |
|
||||
| | | - cryptography.hazmat.primitives | |
|
||||
| | | .ciphers.algorithms.CAST5 | |
|
||||
| | | - cryptography.hazmat.primitives | |
|
||||
| | | .ciphers.algorithms.SEED | |
|
||||
| | | - cryptography.hazmat.primitives | |
|
||||
| | | .ciphers.algorithms.TripleDES | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| 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
|
||||
---------------------
|
||||
|
||||
The check for this call has been removed.
|
||||
|
||||
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. Consider using the secrets module instead:
|
||||
https://docs.python.org/library/secrets.html
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Calls | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B311 | random | - random.Random | Low |
|
||||
| | | - random.random | |
|
||||
| | | - random.randrange | |
|
||||
| | | - random.randint | |
|
||||
| | | - random.choice | |
|
||||
| | | - random.choices | |
|
||||
| | | - random.uniform | |
|
||||
| | | - random.triangular | |
|
||||
| | | - random.randbytes | |
|
||||
| | | - random.randrange | |
|
||||
| | | - random.sample | |
|
||||
| | | - random.getrandbits | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
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 - B319: XML
|
||||
----------------
|
||||
|
||||
Most of this is based off of Christian Heimes' work on defusedxml:
|
||||
https://pypi.org/project/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
|
||||
-------------------
|
||||
|
||||
The check for this call has been removed.
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Calls | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| 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 check for this call has been removed.
|
||||
|
||||
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, than 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 |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
B325: tempnam
|
||||
--------------
|
||||
|
||||
The check for this call has been removed.
|
||||
|
||||
Use of os.tempnam() and os.tmpnam() is vulnerable to symlink attacks. Consider
|
||||
using tmpfile() instead.
|
||||
|
||||
For further information:
|
||||
https://docs.python.org/2.7/library/os.html#os.tempnam
|
||||
https://docs.python.org/3/whatsnew/3.0.html?highlight=tempnam
|
||||
https://bugs.python.org/issue17880
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Calls | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B325 | tempnam | - os.tempnam | Medium |
|
||||
| | | - os.tmpnam | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
"""
|
||||
from bandit.blacklists import utils
|
||||
from bandit.core import issue
|
||||
|
||||
|
||||
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",
|
||||
issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA,
|
||||
[
|
||||
"pickle.loads",
|
||||
"pickle.load",
|
||||
"pickle.Unpickler",
|
||||
"dill.loads",
|
||||
"dill.load",
|
||||
"dill.Unpickler",
|
||||
"shelve.open",
|
||||
"shelve.DbfilenameShelf",
|
||||
"jsonpickle.decode",
|
||||
"jsonpickle.unpickler.decode",
|
||||
"jsonpickle.unpickler.Unpickler",
|
||||
"pandas.read_pickle",
|
||||
],
|
||||
"Pickle and modules that wrap it can be unsafe when used to "
|
||||
"deserialize untrusted data, possible security issue.",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"marshal",
|
||||
"B302",
|
||||
issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA,
|
||||
["marshal.load", "marshal.loads"],
|
||||
"Deserialization with the marshal module is possibly dangerous.",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"md5",
|
||||
"B303",
|
||||
issue.Cwe.BROKEN_CRYPTO,
|
||||
[
|
||||
"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",
|
||||
issue.Cwe.BROKEN_CRYPTO,
|
||||
[
|
||||
"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.CAST5",
|
||||
"cryptography.hazmat.primitives.ciphers.algorithms.IDEA",
|
||||
"cryptography.hazmat.primitives.ciphers.algorithms.SEED",
|
||||
"cryptography.hazmat.primitives.ciphers.algorithms.TripleDES",
|
||||
],
|
||||
"Use of insecure cipher {name}. Replace with a known secure"
|
||||
" cipher such as AES.",
|
||||
"HIGH",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"cipher_modes",
|
||||
"B305",
|
||||
issue.Cwe.BROKEN_CRYPTO,
|
||||
["cryptography.hazmat.primitives.ciphers.modes.ECB"],
|
||||
"Use of insecure cipher mode {name}.",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"mktemp_q",
|
||||
"B306",
|
||||
issue.Cwe.INSECURE_TEMP_FILE,
|
||||
["tempfile.mktemp"],
|
||||
"Use of insecure and deprecated function (mktemp).",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"eval",
|
||||
"B307",
|
||||
issue.Cwe.OS_COMMAND_INJECTION,
|
||||
["eval"],
|
||||
"Use of possibly insecure function - consider using safer "
|
||||
"ast.literal_eval.",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"mark_safe",
|
||||
"B308",
|
||||
issue.Cwe.XSS,
|
||||
["django.utils.safestring.mark_safe"],
|
||||
"Use of mark_safe() may expose cross-site scripting "
|
||||
"vulnerabilities and should be reviewed.",
|
||||
)
|
||||
)
|
||||
|
||||
# skipped B309 as the check for a call to httpsconnection has been removed
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"urllib_urlopen",
|
||||
"B310",
|
||||
issue.Cwe.PATH_TRAVERSAL,
|
||||
[
|
||||
"urllib.request.urlopen",
|
||||
"urllib.request.urlretrieve",
|
||||
"urllib.request.URLopener",
|
||||
"urllib.request.FancyURLopener",
|
||||
"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",
|
||||
issue.Cwe.INSUFFICIENT_RANDOM_VALUES,
|
||||
[
|
||||
"random.Random",
|
||||
"random.random",
|
||||
"random.randrange",
|
||||
"random.randint",
|
||||
"random.choice",
|
||||
"random.choices",
|
||||
"random.uniform",
|
||||
"random.triangular",
|
||||
"random.randbytes",
|
||||
"random.sample",
|
||||
"random.randrange",
|
||||
"random.getrandbits",
|
||||
],
|
||||
"Standard pseudo-random generators are not suitable for "
|
||||
"security/cryptographic purposes.",
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"telnetlib",
|
||||
"B312",
|
||||
issue.Cwe.CLEARTEXT_TRANSMISSION,
|
||||
["telnetlib.Telnet"],
|
||||
"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.org/project/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",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
[
|
||||
"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",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
[
|
||||
"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",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.sax.expatreader.create_parser"],
|
||||
xml_msg,
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"xml_bad_expatbuilder",
|
||||
"B316",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.dom.expatbuilder.parse", "xml.dom.expatbuilder.parseString"],
|
||||
xml_msg,
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"xml_bad_sax",
|
||||
"B317",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.sax.parse", "xml.sax.parseString", "xml.sax.make_parser"],
|
||||
xml_msg,
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"xml_bad_minidom",
|
||||
"B318",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.dom.minidom.parse", "xml.dom.minidom.parseString"],
|
||||
xml_msg,
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"xml_bad_pulldom",
|
||||
"B319",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.dom.pulldom.parse", "xml.dom.pulldom.parseString"],
|
||||
xml_msg,
|
||||
)
|
||||
)
|
||||
|
||||
# skipped B320 as the check for a call to lxml.etree has been removed
|
||||
|
||||
# end of XML tests
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"ftplib",
|
||||
"B321",
|
||||
issue.Cwe.CLEARTEXT_TRANSMISSION,
|
||||
["ftplib.FTP"],
|
||||
"FTP-related functions are being called. FTP is considered "
|
||||
"insecure. Use SSH/SFTP/SCP or some other encrypted protocol.",
|
||||
"HIGH",
|
||||
)
|
||||
)
|
||||
|
||||
# skipped B322 as the check for a call to input() has been removed
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"unverified_context",
|
||||
"B323",
|
||||
issue.Cwe.IMPROPER_CERT_VALIDATION,
|
||||
["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.",
|
||||
)
|
||||
)
|
||||
|
||||
# skipped B324 (used in bandit/plugins/hashlib_new_insecure_functions.py)
|
||||
|
||||
# skipped B325 as the check for a call to os.tempnam and os.tmpnam have
|
||||
# been removed
|
||||
|
||||
return {"Call": sets}
|
||||
@@ -0,0 +1,425 @@
|
||||
#
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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.org/project/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 | import_ftplib | - ftplib | high |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
B403: import_pickle
|
||||
-------------------
|
||||
|
||||
Consider possible security implications associated with these modules.
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Imports | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B403 | import_pickle | - pickle | low |
|
||||
| | | - cPickle | |
|
||||
| | | - dill | |
|
||||
| | | - shelve | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
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
|
||||
-----------------
|
||||
|
||||
This import blacklist has been removed. The information here has been
|
||||
left for historical purposes.
|
||||
|
||||
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 defusedxml.xmlrpc.monkey_patch() function to
|
||||
monkey-patch xmlrpclib and mitigate remote XML attacks.
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Imports | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B411 | import_xmlrpclib | - xmlrpc | 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 | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
B414: import_pycryptodome
|
||||
-------------------------
|
||||
This import blacklist has been removed. The information here has been
|
||||
left for historical purposes.
|
||||
|
||||
pycryptodome is a direct fork of pycrypto that has not fully addressed
|
||||
the issues inherent in PyCrypto. It seems to exist, mainly, as an API
|
||||
compatible continuation of pycrypto and should be deprecated in favor
|
||||
of pyca/cryptography which has more support among the Python community.
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Imports | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B414 | import_pycryptodome | - Cryptodome.Cipher | high |
|
||||
| | | - Cryptodome.Hash | |
|
||||
| | | - Cryptodome.IO | |
|
||||
| | | - Cryptodome.Protocol | |
|
||||
| | | - Cryptodome.PublicKey | |
|
||||
| | | - Cryptodome.Random | |
|
||||
| | | - Cryptodome.Signature | |
|
||||
| | | - Cryptodome.Util | |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
B415: import_pyghmi
|
||||
-------------------
|
||||
An IPMI-related module is being imported. IPMI is considered insecure. Use
|
||||
an encrypted protocol.
|
||||
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
| ID | Name | Imports | Severity |
|
||||
+======+=====================+====================================+===========+
|
||||
| B415 | import_pyghmi | - pyghmi | high |
|
||||
+------+---------------------+------------------------------------+-----------+
|
||||
|
||||
"""
|
||||
from bandit.blacklists import utils
|
||||
from bandit.core import issue
|
||||
|
||||
|
||||
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",
|
||||
issue.Cwe.CLEARTEXT_TRANSMISSION,
|
||||
["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",
|
||||
issue.Cwe.CLEARTEXT_TRANSMISSION,
|
||||
["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",
|
||||
issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA,
|
||||
["pickle", "cPickle", "dill", "shelve"],
|
||||
"Consider possible security implications associated with "
|
||||
"{name} module.",
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_subprocess",
|
||||
"B404",
|
||||
issue.Cwe.OS_COMMAND_INJECTION,
|
||||
["subprocess"],
|
||||
"Consider possible security implications associated with the "
|
||||
"subprocess module.",
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
# Most of this is based off of Christian Heimes' work on defusedxml:
|
||||
# https://pypi.org/project/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."
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_xml_etree",
|
||||
"B405",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.etree.cElementTree", "xml.etree.ElementTree"],
|
||||
xml_msg,
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_xml_sax",
|
||||
"B406",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.sax"],
|
||||
xml_msg,
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_xml_expat",
|
||||
"B407",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.dom.expatbuilder"],
|
||||
xml_msg,
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_xml_minidom",
|
||||
"B408",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.dom.minidom"],
|
||||
xml_msg,
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_xml_pulldom",
|
||||
"B409",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xml.dom.pulldom"],
|
||||
xml_msg,
|
||||
"LOW",
|
||||
)
|
||||
)
|
||||
|
||||
# skipped B410 as the check for import_lxml has been removed
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_xmlrpclib",
|
||||
"B411",
|
||||
issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
["xmlrpc"],
|
||||
"Using {name} to parse untrusted XML data is known to be "
|
||||
"vulnerable to XML attacks. Use defusedxml.xmlrpc.monkey_patch() "
|
||||
"function to monkey-patch xmlrpclib and mitigate XML "
|
||||
"vulnerabilities.",
|
||||
"HIGH",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_httpoxy",
|
||||
"B412",
|
||||
issue.Cwe.IMPROPER_ACCESS_CONTROL,
|
||||
[
|
||||
"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",
|
||||
issue.Cwe.BROKEN_CRYPTO,
|
||||
[
|
||||
"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",
|
||||
)
|
||||
)
|
||||
|
||||
sets.append(
|
||||
utils.build_conf_dict(
|
||||
"import_pyghmi",
|
||||
"B415",
|
||||
issue.Cwe.CLEARTEXT_TRANSMISSION,
|
||||
["pyghmi"],
|
||||
"An IPMI-related module is being imported. IPMI is considered "
|
||||
"insecure. Use an encrypted protocol.",
|
||||
"HIGH",
|
||||
)
|
||||
)
|
||||
|
||||
return {"Import": sets, "ImportFrom": sets, "Call": sets}
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""Utils module."""
|
||||
|
||||
|
||||
def build_conf_dict(name, bid, cwe, qualnames, message, level="MEDIUM"):
|
||||
"""Build and return a blacklist configuration dict."""
|
||||
return {
|
||||
"name": name,
|
||||
"id": bid,
|
||||
"cwe": cwe,
|
||||
"message": message,
|
||||
"qualnames": qualnames,
|
||||
"level": level,
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
252
Backend/venv/lib/python3.12/site-packages/bandit/cli/baseline.py
Normal file
252
Backend/venv/lib/python3.12/site-packages/bandit/cli/baseline.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#
|
||||
# Copyright 2015 Hewlett-Packard Enterprise
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# #############################################################################
|
||||
# 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.
|
||||
# #############################################################################
|
||||
"""Bandit is a tool designed to find common security issues in Python code."""
|
||||
import argparse
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # nosec: B404
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
import git
|
||||
except ImportError:
|
||||
git = None
|
||||
|
||||
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"]
|
||||
|
||||
"""baseline.py"""
|
||||
|
||||
|
||||
def main():
|
||||
"""Execute Bandit."""
|
||||
# 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 = f"{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) # nosec: B603
|
||||
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():
|
||||
"""Baseline setup by creating temp folder and resetting repo."""
|
||||
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():
|
||||
"""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():
|
||||
"""Initialize arguments and output formats."""
|
||||
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.",
|
||||
)
|
||||
if sys.version_info >= (3, 14):
|
||||
parser.suggest_on_error = True
|
||||
parser.color = False
|
||||
|
||||
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, _ = 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 = f"{report_basename}.{output_format}"
|
||||
|
||||
# #################### Check Requirements #################################
|
||||
if git is None:
|
||||
LOG.error("Git not available, reinstall with baseline extra")
|
||||
valid = False
|
||||
return (None, None, None)
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,207 @@
|
||||
# Copyright 2015 Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bandit is a tool designed to find common security issues in Python code."""
|
||||
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():
|
||||
"""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():
|
||||
"""Parse arguments."""
|
||||
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,
|
||||
)
|
||||
if sys.version_info >= (3, 14):
|
||||
parser.suggest_on_error = True
|
||||
parser.color = False
|
||||
|
||||
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():
|
||||
"""Get configuration 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():
|
||||
"""Config generator to write configuration file."""
|
||||
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(f"unknown ID in skips: {skip}")
|
||||
|
||||
for test in tests:
|
||||
if not extension_loader.MANAGER.check_id(test):
|
||||
raise RuntimeError(f"unknown ID in tests: {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 OSError:
|
||||
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())
|
||||
701
Backend/venv/lib/python3.12/site-packages/bandit/cli/main.py
Normal file
701
Backend/venv/lib/python3.12/site-packages/bandit/cli/main.py
Normal file
@@ -0,0 +1,701 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bandit is a tool designed to find common security issues in Python code."""
|
||||
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(log_level=logging.INFO, log_format=None):
|
||||
"""Initialize the logger.
|
||||
|
||||
:param debug: Whether to enable debug mode
|
||||
:return: An instantiated logging instance
|
||||
"""
|
||||
LOG.handlers = []
|
||||
|
||||
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, _, 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(default_val, arg_val, ini_val, option_name):
|
||||
"""It's useful to show the source of each option."""
|
||||
# When default value is not defined, arg_val and ini_val is deterministic
|
||||
if default_val is None:
|
||||
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
|
||||
# No value passed to commad line and default value is used
|
||||
elif default_val == arg_val:
|
||||
return ini_val if ini_val else arg_val
|
||||
# Certainly a value is passed to commad line
|
||||
else:
|
||||
return arg_val
|
||||
|
||||
|
||||
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():
|
||||
"""Bandit CLI."""
|
||||
# bring our logging stuff up as early as possible
|
||||
debug = (
|
||||
logging.DEBUG
|
||||
if "-d" in sys.argv or "--debug" in sys.argv
|
||||
else logging.INFO
|
||||
)
|
||||
_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,
|
||||
)
|
||||
if sys.version_info >= (3, 14):
|
||||
parser.suggest_on_error = True
|
||||
parser.color = False
|
||||
|
||||
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",
|
||||
)
|
||||
severity_group = parser.add_mutually_exclusive_group(required=False)
|
||||
severity_group.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)",
|
||||
)
|
||||
severity_group.add_argument(
|
||||
"--severity-level",
|
||||
dest="severity_string",
|
||||
action="store",
|
||||
help="report only issues of a given severity level or higher."
|
||||
' "all" and "low" are likely to produce the same results, but it'
|
||||
" is possible for rules to be undefined which will"
|
||||
' not be listed in "low".',
|
||||
choices=["all", "low", "medium", "high"],
|
||||
)
|
||||
confidence_group = parser.add_mutually_exclusive_group(required=False)
|
||||
confidence_group.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)",
|
||||
)
|
||||
confidence_group.add_argument(
|
||||
"--confidence-level",
|
||||
dest="confidence_string",
|
||||
action="store",
|
||||
help="report only issues of a given confidence level or higher."
|
||||
' "all" and "low" are likely to produce the same results, but it'
|
||||
" is possible for rules to be undefined which will"
|
||||
' not be listed in "low".',
|
||||
choices=["all", "low", "medium", "high"],
|
||||
)
|
||||
output_format = (
|
||||
"screen"
|
||||
if (
|
||||
sys.stdout.isatty()
|
||||
and os.getenv("NO_COLOR") is None
|
||||
and os.getenv("TERM") != "dumb"
|
||||
)
|
||||
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", encoding="utf-8"),
|
||||
default=sys.stdout,
|
||||
help="write report to filename",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=False)
|
||||
group.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",
|
||||
)
|
||||
group.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
"--silent",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="only show output in the case of an error",
|
||||
)
|
||||
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=",".join(constants.EXCLUDE),
|
||||
help="comma-separated list of paths (glob patterns "
|
||||
"supported) to exclude from scan "
|
||||
"(note that these are in addition to the excluded "
|
||||
"paths provided in the config file) (default: "
|
||||
+ ",".join(constants.EXCLUDE)
|
||||
+ ")",
|
||||
)
|
||||
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(
|
||||
"--exit-zero",
|
||||
action="store_true",
|
||||
dest="exit_zero",
|
||||
default=False,
|
||||
help="exit with 0, " "even with results found",
|
||||
)
|
||||
python_ver = sys.version.replace("\n", "")
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {bandit.__version__}\n"
|
||||
f" python version = {python_ver}",
|
||||
)
|
||||
|
||||
parser.set_defaults(debug=False)
|
||||
parser.set_defaults(verbose=False)
|
||||
parser.set_defaults(quiet=False)
|
||||
parser.set_defaults(ignore_nosec=False)
|
||||
|
||||
plugin_info = [
|
||||
f"{a[0]}\t{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(f"{b['id']}\t{b['name']}")
|
||||
|
||||
plugin_list = "\n\t".join(sorted(set(plugin_info + blacklist_info)))
|
||||
dedent_text = textwrap.dedent(
|
||||
"""
|
||||
CUSTOM FORMATTING
|
||||
-----------------
|
||||
|
||||
Available tags:
|
||||
|
||||
{abspath}, {relpath}, {line}, {col}, {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/library/string.html
|
||||
|
||||
The following tests were discovered and loaded:
|
||||
-----------------------------------------------
|
||||
"""
|
||||
)
|
||||
parser.epilog = dedent_text + f"\t{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")
|
||||
|
||||
# Check if confidence or severity level have been specified with strings
|
||||
if args.severity_string is not None:
|
||||
if args.severity_string == "all":
|
||||
args.severity = 1
|
||||
elif args.severity_string == "low":
|
||||
args.severity = 2
|
||||
elif args.severity_string == "medium":
|
||||
args.severity = 3
|
||||
elif args.severity_string == "high":
|
||||
args.severity = 4
|
||||
# Other strings will be blocked by argparse
|
||||
|
||||
if args.confidence_string is not None:
|
||||
if args.confidence_string == "all":
|
||||
args.confidence = 1
|
||||
elif args.confidence_string == "low":
|
||||
args.confidence = 2
|
||||
elif args.confidence_string == "medium":
|
||||
args.confidence = 3
|
||||
elif args.confidence_string == "high":
|
||||
args.confidence = 4
|
||||
# Other strings will be blocked by argparse
|
||||
|
||||
# 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.config_file = _log_option_source(
|
||||
parser.get_default("configfile"),
|
||||
args.config_file,
|
||||
ini_options.get("configfile"),
|
||||
"config file",
|
||||
)
|
||||
|
||||
args.excluded_paths = _log_option_source(
|
||||
parser.get_default("excluded_paths"),
|
||||
args.excluded_paths,
|
||||
ini_options.get("exclude"),
|
||||
"excluded paths",
|
||||
)
|
||||
|
||||
args.skips = _log_option_source(
|
||||
parser.get_default("skips"),
|
||||
args.skips,
|
||||
ini_options.get("skips"),
|
||||
"skipped tests",
|
||||
)
|
||||
|
||||
args.tests = _log_option_source(
|
||||
parser.get_default("tests"),
|
||||
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(
|
||||
parser.get_default("targets"),
|
||||
args.targets,
|
||||
ini_targets,
|
||||
"selected targets",
|
||||
)
|
||||
|
||||
# TODO(tmcpeak): any other useful options to pass from .bandit?
|
||||
|
||||
args.recursive = _log_option_source(
|
||||
parser.get_default("recursive"),
|
||||
args.recursive,
|
||||
ini_options.get("recursive"),
|
||||
"recursive scan",
|
||||
)
|
||||
|
||||
args.agg_type = _log_option_source(
|
||||
parser.get_default("agg_type"),
|
||||
args.agg_type,
|
||||
ini_options.get("aggregate"),
|
||||
"aggregate output type",
|
||||
)
|
||||
|
||||
args.context_lines = _log_option_source(
|
||||
parser.get_default("context_lines"),
|
||||
args.context_lines,
|
||||
int(ini_options.get("number") or 0) or None,
|
||||
"max code lines output for issue",
|
||||
)
|
||||
|
||||
args.profile = _log_option_source(
|
||||
parser.get_default("profile"),
|
||||
args.profile,
|
||||
ini_options.get("profile"),
|
||||
"profile",
|
||||
)
|
||||
|
||||
args.severity = _log_option_source(
|
||||
parser.get_default("severity"),
|
||||
args.severity,
|
||||
ini_options.get("level"),
|
||||
"severity level",
|
||||
)
|
||||
|
||||
args.confidence = _log_option_source(
|
||||
parser.get_default("confidence"),
|
||||
args.confidence,
|
||||
ini_options.get("confidence"),
|
||||
"confidence level",
|
||||
)
|
||||
|
||||
args.output_format = _log_option_source(
|
||||
parser.get_default("output_format"),
|
||||
args.output_format,
|
||||
ini_options.get("format"),
|
||||
"output format",
|
||||
)
|
||||
|
||||
args.msg_template = _log_option_source(
|
||||
parser.get_default("msg_template"),
|
||||
args.msg_template,
|
||||
ini_options.get("msg-template"),
|
||||
"output message template",
|
||||
)
|
||||
|
||||
args.output_file = _log_option_source(
|
||||
parser.get_default("output_file"),
|
||||
args.output_file,
|
||||
ini_options.get("output"),
|
||||
"output file",
|
||||
)
|
||||
|
||||
args.verbose = _log_option_source(
|
||||
parser.get_default("verbose"),
|
||||
args.verbose,
|
||||
ini_options.get("verbose"),
|
||||
"output extra information",
|
||||
)
|
||||
|
||||
args.debug = _log_option_source(
|
||||
parser.get_default("debug"),
|
||||
args.debug,
|
||||
ini_options.get("debug"),
|
||||
"debug mode",
|
||||
)
|
||||
|
||||
args.quiet = _log_option_source(
|
||||
parser.get_default("quiet"),
|
||||
args.quiet,
|
||||
ini_options.get("quiet"),
|
||||
"silent mode",
|
||||
)
|
||||
|
||||
args.ignore_nosec = _log_option_source(
|
||||
parser.get_default("ignore_nosec"),
|
||||
args.ignore_nosec,
|
||||
ini_options.get("ignore-nosec"),
|
||||
"do not skip lines with # nosec",
|
||||
)
|
||||
|
||||
args.baseline = _log_option_source(
|
||||
parser.get_default("baseline"),
|
||||
args.baseline,
|
||||
ini_options.get("baseline"),
|
||||
"path of a baseline report",
|
||||
)
|
||||
|
||||
try:
|
||||
b_conf = b_config.BanditConfig(config_file=args.config_file)
|
||||
except utils.ConfigError as e:
|
||||
LOG.error(e)
|
||||
sys.exit(2)
|
||||
|
||||
if not args.targets:
|
||||
parser.print_usage()
|
||||
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(log_level=logging.DEBUG, log_format=log_format)
|
||||
|
||||
if args.quiet:
|
||||
_init_logger(log_level=logging.WARN)
|
||||
|
||||
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,
|
||||
quiet=args.quiet,
|
||||
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 OSError:
|
||||
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,
|
||||
)
|
||||
|
||||
if (
|
||||
b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0
|
||||
and not args.exit_zero
|
||||
):
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
#
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
|
||||
from bandit.core import issue
|
||||
|
||||
|
||||
def report_issue(check, name):
|
||||
return issue.Issue(
|
||||
severity=check.get("level", "MEDIUM"),
|
||||
confidence="HIGH",
|
||||
cwe=check.get("cwe", issue.Cwe.NOTSET),
|
||||
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.Constant
|
||||
) and isinstance(context.node.args[0].value, str):
|
||||
name = context.node.args[0].value
|
||||
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.
|
||||
# Will produce None if argument is not a literal or identifier
|
||||
if name in ["importlib.import_module", "importlib.__import__"]:
|
||||
if context.call_args_count > 0:
|
||||
name = context.call_args[0]
|
||||
else:
|
||||
name = context.call_keywords["name"]
|
||||
for check in blacklists[node_type]:
|
||||
for qn in check["qualnames"]:
|
||||
if name is not None and 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)
|
||||
271
Backend/venv/lib/python3.12/site-packages/bandit/core/config.py
Normal file
271
Backend/venv/lib/python3.12/site-packages/bandit/core/config.py
Normal file
@@ -0,0 +1,271 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
try:
|
||||
import tomli as tomllib
|
||||
except ImportError:
|
||||
tomllib = None
|
||||
|
||||
from bandit.core import constants
|
||||
from bandit.core import extension_loader
|
||||
from bandit.core import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BanditConfig:
|
||||
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, "rb")
|
||||
except OSError:
|
||||
raise utils.ConfigError(
|
||||
"Could not read config file.", config_file
|
||||
)
|
||||
|
||||
if config_file.endswith(".toml"):
|
||||
if tomllib is None:
|
||||
raise utils.ConfigError(
|
||||
"toml parser not available, reinstall with toml extra",
|
||||
config_file,
|
||||
)
|
||||
|
||||
try:
|
||||
with f:
|
||||
self._config = (
|
||||
tomllib.load(f).get("tool", {}).get("bandit", {})
|
||||
)
|
||||
except tomllib.TOMLDecodeError as err:
|
||||
LOG.error(err)
|
||||
raise utils.ConfigError("Error parsing file.", config_file)
|
||||
else:
|
||||
try:
|
||||
with f:
|
||||
self._config = yaml.safe_load(f)
|
||||
except yaml.YAMLError as err:
|
||||
LOG.error(err)
|
||||
raise utils.ConfigError("Error parsing file.", config_file)
|
||||
|
||||
self.validate(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 explicitly 'None', for example when the list is given
|
||||
# in yaml but not populated with any values.
|
||||
include = {
|
||||
(extman.get_test_id(i) or i)
|
||||
for i in (profile.get("include") or [])
|
||||
}
|
||||
exclude = {
|
||||
(extman.get_test_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,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# default plugin name pattern
|
||||
plugin_name_pattern = "*.py"
|
||||
|
||||
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/3/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"
|
||||
|
||||
# Directories to exclude by default
|
||||
EXCLUDE = (
|
||||
".svn",
|
||||
"CVS",
|
||||
".bzr",
|
||||
".hg",
|
||||
".git",
|
||||
"__pycache__",
|
||||
".tox",
|
||||
".eggs",
|
||||
"*.egg",
|
||||
)
|
||||
316
Backend/venv/lib/python3.12/site-packages/bandit/core/context.py
Normal file
316
Backend/venv/lib/python3.12/site-packages/bandit/core/context.py
Normal file
@@ -0,0 +1,316 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
|
||||
from bandit.core import utils
|
||||
|
||||
|
||||
class Context:
|
||||
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 f"<Context {self._context}>"
|
||||
|
||||
@property
|
||||
def call_args(self):
|
||||
"""Get a list of function args
|
||||
|
||||
:return: A list of function args
|
||||
"""
|
||||
args = []
|
||||
if "call" in self._context and hasattr(self._context["call"], "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 or None
|
||||
"""
|
||||
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
|
||||
"""
|
||||
return self._context.get("name")
|
||||
|
||||
@property
|
||||
def call_function_name_qual(self):
|
||||
"""Get the FQ name of a function call
|
||||
|
||||
:return: The FQ name of a function call
|
||||
"""
|
||||
return self._context.get("qualname")
|
||||
|
||||
@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
|
||||
"""
|
||||
return self._context.get("node")
|
||||
|
||||
@property
|
||||
def string_val(self):
|
||||
"""Get the value of a standalone unicode or string object
|
||||
|
||||
:return: value of a standalone unicode or string object
|
||||
"""
|
||||
return self._context.get("str")
|
||||
|
||||
@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
|
||||
"""
|
||||
return self._context.get("statement")
|
||||
|
||||
@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
|
||||
and hasattr(self._context["node"], "args")
|
||||
and hasattr(self._context["node"].args, "defaults")
|
||||
):
|
||||
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.Constant):
|
||||
if isinstance(literal.value, bool):
|
||||
literal_value = str(literal.value)
|
||||
elif literal.value is None:
|
||||
literal_value = str(literal.value)
|
||||
else:
|
||||
literal_value = literal.value
|
||||
|
||||
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 += (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.Name):
|
||||
literal_value = literal.id
|
||||
|
||||
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
|
||||
"""
|
||||
if hasattr(self.node, "keywords"):
|
||||
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
|
||||
"""
|
||||
max_args = self.call_args_count
|
||||
if max_args and position_num < max_args:
|
||||
arg = self._context["call"].args[position_num]
|
||||
return getattr(arg, "attr", None) or self._get_literal_value(arg)
|
||||
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 self._context.get("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 module in self._context.get("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
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self._context.get("filename")
|
||||
|
||||
@property
|
||||
def file_data(self):
|
||||
return self._context.get("file_data")
|
||||
|
||||
@property
|
||||
def import_aliases(self):
|
||||
return self._context.get("import_aliases")
|
||||
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import bandit
|
||||
|
||||
|
||||
def get_url(bid):
|
||||
# where our docs are hosted
|
||||
base_url = f"https://bandit.readthedocs.io/en/{bandit.__version__}/"
|
||||
|
||||
# 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 f"{base_url}plugins/{bid.lower()}_{info.plugin.__name__}.html"
|
||||
|
||||
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 = info.copy()
|
||||
info["id"] = "b304-b305"
|
||||
info["name"] = "ciphers-and-modes"
|
||||
elif info["id"] in [
|
||||
"B313",
|
||||
"B314",
|
||||
"B315",
|
||||
"B316",
|
||||
"B317",
|
||||
"B318",
|
||||
"B319",
|
||||
"B320",
|
||||
]:
|
||||
info = info.copy()
|
||||
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
|
||||
@@ -0,0 +1,114 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
from bandit.core import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Manager:
|
||||
# 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(
|
||||
f"WARNING: Test '{plugin.name}' has no ID, skipping.",
|
||||
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_test_id(self, test_name):
|
||||
if test_name in self.plugins_by_name:
|
||||
return self.plugins_by_name[test_name].plugin._test_id
|
||||
if test_name in self.blacklist_by_name:
|
||||
return self.blacklist_by_name[test_name]["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 self.blacklist.values():
|
||||
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):
|
||||
LOG.warning(f"Unknown test found in profile: {inc}")
|
||||
|
||||
for exc in profile["exclude"]:
|
||||
if not self.check_id(exc):
|
||||
LOG.warning(f"Unknown test found in profile: {exc}")
|
||||
|
||||
union = set(profile["include"]) & set(profile["exclude"])
|
||||
if len(union) > 0:
|
||||
raise ValueError(
|
||||
f"Non-exclusive include/exclude test sets: {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()
|
||||
245
Backend/venv/lib/python3.12/site-packages/bandit/core/issue.py
Normal file
245
Backend/venv/lib/python3.12/site-packages/bandit/core/issue.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import linecache
|
||||
|
||||
from bandit.core import constants
|
||||
|
||||
|
||||
class Cwe:
|
||||
NOTSET = 0
|
||||
IMPROPER_INPUT_VALIDATION = 20
|
||||
PATH_TRAVERSAL = 22
|
||||
OS_COMMAND_INJECTION = 78
|
||||
XSS = 79
|
||||
BASIC_XSS = 80
|
||||
SQL_INJECTION = 89
|
||||
CODE_INJECTION = 94
|
||||
IMPROPER_WILDCARD_NEUTRALIZATION = 155
|
||||
HARD_CODED_PASSWORD = 259
|
||||
IMPROPER_ACCESS_CONTROL = 284
|
||||
IMPROPER_CERT_VALIDATION = 295
|
||||
CLEARTEXT_TRANSMISSION = 319
|
||||
INADEQUATE_ENCRYPTION_STRENGTH = 326
|
||||
BROKEN_CRYPTO = 327
|
||||
INSUFFICIENT_RANDOM_VALUES = 330
|
||||
INSECURE_TEMP_FILE = 377
|
||||
UNCONTROLLED_RESOURCE_CONSUMPTION = 400
|
||||
DOWNLOAD_OF_CODE_WITHOUT_INTEGRITY_CHECK = 494
|
||||
DESERIALIZATION_OF_UNTRUSTED_DATA = 502
|
||||
MULTIPLE_BINDS = 605
|
||||
IMPROPER_CHECK_OF_EXCEPT_COND = 703
|
||||
INCORRECT_PERMISSION_ASSIGNMENT = 732
|
||||
INAPPROPRIATE_ENCODING_FOR_OUTPUT_CONTEXT = 838
|
||||
|
||||
MITRE_URL_PATTERN = "https://cwe.mitre.org/data/definitions/%s.html"
|
||||
|
||||
def __init__(self, id=NOTSET):
|
||||
self.id = id
|
||||
|
||||
def link(self):
|
||||
if self.id == Cwe.NOTSET:
|
||||
return ""
|
||||
|
||||
return Cwe.MITRE_URL_PATTERN % str(self.id)
|
||||
|
||||
def __str__(self):
|
||||
if self.id == Cwe.NOTSET:
|
||||
return ""
|
||||
|
||||
return "CWE-%i (%s)" % (self.id, self.link())
|
||||
|
||||
def as_dict(self):
|
||||
return (
|
||||
{"id": self.id, "link": self.link()}
|
||||
if self.id != Cwe.NOTSET
|
||||
else {}
|
||||
)
|
||||
|
||||
def as_jsons(self):
|
||||
return str(self.as_dict())
|
||||
|
||||
def from_dict(self, data):
|
||||
if "id" in data:
|
||||
self.id = int(data["id"])
|
||||
else:
|
||||
self.id = Cwe.NOTSET
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.id != other.id
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
|
||||
class Issue:
|
||||
def __init__(
|
||||
self,
|
||||
severity,
|
||||
cwe=0,
|
||||
confidence=constants.CONFIDENCE_DEFAULT,
|
||||
text="",
|
||||
ident=None,
|
||||
lineno=None,
|
||||
test_id="",
|
||||
col_offset=-1,
|
||||
end_col_offset=0,
|
||||
):
|
||||
self.severity = severity
|
||||
self.cwe = Cwe(cwe)
|
||||
self.confidence = confidence
|
||||
if isinstance(text, bytes):
|
||||
text = text.decode("utf-8")
|
||||
self.text = text
|
||||
self.ident = ident
|
||||
self.fname = ""
|
||||
self.fdata = None
|
||||
self.test = ""
|
||||
self.test_id = test_id
|
||||
self.lineno = lineno
|
||||
self.col_offset = col_offset
|
||||
self.end_col_offset = end_col_offset
|
||||
self.linerange = []
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"Issue: '%s' from %s:%s: CWE: %s, Severity: %s Confidence: "
|
||||
"%s at %s:%i:%i"
|
||||
) % (
|
||||
self.text,
|
||||
self.test_id,
|
||||
(self.ident or self.test),
|
||||
str(self.cwe),
|
||||
self.severity,
|
||||
self.confidence,
|
||||
self.fname,
|
||||
self.lineno,
|
||||
self.col_offset,
|
||||
)
|
||||
|
||||
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",
|
||||
"cwe",
|
||||
"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
|
||||
|
||||
if self.fname == "<stdin>":
|
||||
self.fdata.seek(0)
|
||||
for line_num in range(1, lmin):
|
||||
self.fdata.readline()
|
||||
|
||||
tmplt = "%i\t%s" if tabbed else "%i %s"
|
||||
for line in range(lmin, lmax):
|
||||
if self.fname == "<stdin>":
|
||||
text = self.fdata.readline()
|
||||
else:
|
||||
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, max_lines=3):
|
||||
"""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_cwe": self.cwe.as_dict(),
|
||||
"issue_confidence": self.confidence,
|
||||
"issue_text": self.text.encode("utf-8").decode("utf-8"),
|
||||
"line_number": self.lineno,
|
||||
"line_range": self.linerange,
|
||||
"col_offset": self.col_offset,
|
||||
"end_col_offset": self.end_col_offset,
|
||||
}
|
||||
|
||||
if with_code:
|
||||
out["code"] = self.get_code(max_lines=max_lines)
|
||||
return out
|
||||
|
||||
def from_dict(self, data, with_code=True):
|
||||
self.code = data["code"]
|
||||
self.fname = data["filename"]
|
||||
self.severity = data["issue_severity"]
|
||||
self.cwe = cwe_from_dict(data["issue_cwe"])
|
||||
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"]
|
||||
self.col_offset = data.get("col_offset", 0)
|
||||
self.end_col_offset = data.get("end_col_offset", 0)
|
||||
|
||||
|
||||
def cwe_from_dict(data):
|
||||
cwe = Cwe()
|
||||
cwe.from_dict(data)
|
||||
return cwe
|
||||
|
||||
|
||||
def issue_from_dict(data):
|
||||
i = Issue(severity=data["issue_severity"])
|
||||
i.from_dict(data)
|
||||
return i
|
||||
499
Backend/venv/lib/python3.12/site-packages/bandit/core/manager.py
Normal file
499
Backend/venv/lib/python3.12/site-packages/bandit/core/manager.py
Normal file
@@ -0,0 +1,499 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import collections
|
||||
import fnmatch
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tokenize
|
||||
import traceback
|
||||
|
||||
from rich import progress
|
||||
|
||||
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__)
|
||||
NOSEC_COMMENT = re.compile(r"#\s*nosec:?\s*(?P<tests>[^#]+)?#?")
|
||||
NOSEC_COMMENT_TESTS = re.compile(r"(?:(B\d+|[a-z\d_]+),?)+", re.IGNORECASE)
|
||||
PROGRESS_THRESHOLD = 50
|
||||
|
||||
|
||||
class BanditManager:
|
||||
scope = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
agg_type,
|
||||
debug=False,
|
||||
verbose=False,
|
||||
quiet=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 quiet: Whether to only show output in the case of an error
|
||||
: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
|
||||
self.quiet = quiet
|
||||
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)
|
||||
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()
|
||||
and os.getenv("NO_COLOR") is None
|
||||
and os.getenv("TERM") != "dumb"
|
||||
)
|
||||
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,
|
||||
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(
|
||||
f"Unable to output report using "
|
||||
f"'{output_format}' formatter: {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_globs = 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(","):
|
||||
if os.path.isdir(path):
|
||||
path = os.path.join(path, "*")
|
||||
|
||||
excluded_path_globs.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_globs,
|
||||
)
|
||||
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_globs,
|
||||
enforce_glob=False,
|
||||
):
|
||||
if fname != "-":
|
||||
fname = os.path.join(".", fname)
|
||||
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: -
|
||||
"""
|
||||
# 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)
|
||||
if (
|
||||
len(self.files_list) > PROGRESS_THRESHOLD
|
||||
and LOG.getEffectiveLevel() <= logging.INFO
|
||||
):
|
||||
files = progress.track(self.files_list)
|
||||
else:
|
||||
files = self.files_list
|
||||
|
||||
for count, fname in enumerate(files):
|
||||
LOG.debug("working on file : %s", fname)
|
||||
|
||||
try:
|
||||
if fname == "-":
|
||||
open_fd = os.fdopen(sys.stdin.fileno(), "rb", 0)
|
||||
fdata = io.BytesIO(open_fd.read())
|
||||
new_files_list = [
|
||||
"<stdin>" if x == "-" else x for x in new_files_list
|
||||
]
|
||||
self._parse_file("<stdin>", fdata, new_files_list)
|
||||
else:
|
||||
with open(fname, "rb") as fdata:
|
||||
self._parse_file(fname, fdata, new_files_list)
|
||||
except OSError as e:
|
||||
self.skipped.append((fname, e.strerror))
|
||||
new_files_list.remove(fname)
|
||||
|
||||
# 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)
|
||||
# nosec_lines is a dict of line number -> set of tests to ignore
|
||||
# for the line
|
||||
nosec_lines = dict()
|
||||
try:
|
||||
fdata.seek(0)
|
||||
tokens = tokenize.tokenize(fdata.readline)
|
||||
|
||||
if not self.ignore_nosec:
|
||||
for toktype, tokval, (lineno, _), _, _ in tokens:
|
||||
if toktype == tokenize.COMMENT:
|
||||
nosec_lines[lineno] = _parse_nosec_comment(tokval)
|
||||
|
||||
except tokenize.TokenError:
|
||||
pass
|
||||
score = self._execute_ast_visitor(fname, fdata, data, nosec_lines)
|
||||
self.scores.append(score)
|
||||
self.metrics.count_issues([score])
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(2)
|
||||
except SyntaxError:
|
||||
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.", fname
|
||||
)
|
||||
if not LOG.isEnabledFor(logging.DEBUG):
|
||||
LOG.error(
|
||||
'Run "bandit --debug %s" to see the full traceback.', 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, fdata, 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,
|
||||
fdata,
|
||||
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, _, 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 (globbing supported) 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 _matches_glob_list(path, excluded_path_strings) and 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: main 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
|
||||
|
||||
|
||||
def _find_test_id_from_nosec_string(extman, match):
|
||||
test_id = extman.check_id(match)
|
||||
if test_id:
|
||||
return match
|
||||
# Finding by short_id didn't work, let's check the test name
|
||||
test_id = extman.get_test_id(match)
|
||||
if not test_id:
|
||||
# Name and short id didn't work:
|
||||
LOG.warning(
|
||||
"Test in comment: %s is not a test name or id, ignoring", match
|
||||
)
|
||||
return test_id # We want to return None or the string here regardless
|
||||
|
||||
|
||||
def _parse_nosec_comment(comment):
|
||||
found_no_sec_comment = NOSEC_COMMENT.search(comment)
|
||||
if not found_no_sec_comment:
|
||||
# there was no nosec comment
|
||||
return None
|
||||
|
||||
matches = found_no_sec_comment.groupdict()
|
||||
nosec_tests = matches.get("tests", set())
|
||||
|
||||
# empty set indicates that there was a nosec comment without specific
|
||||
# test ids or names
|
||||
test_ids = set()
|
||||
if nosec_tests:
|
||||
extman = extension_loader.MANAGER
|
||||
# lookup tests by short code or name
|
||||
for test in NOSEC_COMMENT_TESTS.finditer(nosec_tests):
|
||||
test_match = test.group(1)
|
||||
test_id = _find_test_id_from_nosec_string(extman, test_match)
|
||||
if test_id:
|
||||
test_ids.add(test_id)
|
||||
|
||||
return test_ids
|
||||
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import collections
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BanditMetaAst:
|
||||
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 += f"Node: {k}\n"
|
||||
tmpstr += f"\t{str(v)}\n"
|
||||
tmpstr += f"Length: {len(self.nodes)}\n"
|
||||
return tmpstr
|
||||
106
Backend/venv/lib/python3.12/site-packages/bandit/core/metrics.py
Normal file
106
Backend/venv/lib/python3.12/site-packages/bandit/core/metrics.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import collections
|
||||
|
||||
from bandit.core import constants
|
||||
|
||||
|
||||
class Metrics:
|
||||
"""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,
|
||||
"skipped_tests": 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"][f"{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,
|
||||
"skipped_tests": 0,
|
||||
}
|
||||
self.current = self.data[fname]
|
||||
|
||||
def note_nosec(self, num=1):
|
||||
"""Note a "nosec" comment.
|
||||
|
||||
Increment the currently active metrics nosec count.
|
||||
:param num: number of nosecs seen, defaults to 1
|
||||
"""
|
||||
self.current["nosec"] += num
|
||||
|
||||
def note_skipped_test(self, num=1):
|
||||
"""Note a "nosec BXXX, BYYY, ..." comment.
|
||||
|
||||
Increment the currently active metrics skipped_tests count.
|
||||
:param num: number of skipped_tests seen, defaults to 1
|
||||
"""
|
||||
self.current["skipped_tests"] += 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, _ in constants.CRITERIA:
|
||||
for i, rank in enumerate(constants.RANKING):
|
||||
label = f"{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
|
||||
@@ -0,0 +1,297 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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:
|
||||
def __init__(
|
||||
self, fname, fdata, metaast, testset, debug, nosec_lines, metrics
|
||||
):
|
||||
self.debug = debug
|
||||
self.nosec_lines = nosec_lines
|
||||
self.scores = {
|
||||
"SEVERITY": [0] * len(constants.RANKING),
|
||||
"CONFIDENCE": [0] * len(constants.RANKING),
|
||||
}
|
||||
self.depth = 0
|
||||
self.fname = fname
|
||||
self.fdata = fdata
|
||||
self.metaast = metaast
|
||||
self.testset = testset
|
||||
self.imports = set()
|
||||
self.import_aliases = {}
|
||||
self.tester = b_tester.BanditTester(
|
||||
self.testset, self.debug, nosec_lines, metrics
|
||||
)
|
||||
|
||||
# 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.warning(
|
||||
"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_Constant(self, node):
|
||||
"""Visitor for AST Constant nodes
|
||||
|
||||
call the appropriate method for the node type.
|
||||
this maintains compatibility with <3.6 and 3.8+
|
||||
|
||||
This code is heavily influenced by Anthony Sottile (@asottile) here:
|
||||
https://bugs.python.org/msg342486
|
||||
|
||||
:param node: The node that is being inspected
|
||||
:return: -
|
||||
"""
|
||||
if isinstance(node.value, str):
|
||||
self.visit_Str(node)
|
||||
elif isinstance(node.value, bytes):
|
||||
self.visit_Bytes(node)
|
||||
|
||||
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.value
|
||||
if not isinstance(node._bandit_parent, ast.Expr): # docstring
|
||||
self.context["linerange"] = b_utils.linerange(node._bandit_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.value
|
||||
if not isinstance(node._bandit_parent, ast.Expr): # docstring
|
||||
self.context["linerange"] = b_utils.linerange(node._bandit_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 hasattr(node, "col_offset"):
|
||||
self.context["col_offset"] = node.col_offset
|
||||
if hasattr(node, "end_col_offset"):
|
||||
self.context["end_col_offset"] = node.end_col_offset
|
||||
|
||||
self.context["node"] = node
|
||||
self.context["linerange"] = b_utils.linerange(node)
|
||||
self.context["filename"] = self.fname
|
||||
self.context["file_data"] = self.fdata
|
||||
|
||||
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, 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:
|
||||
item._bandit_sibling = value[idx + 1]
|
||||
else:
|
||||
item._bandit_sibling = None
|
||||
item._bandit_parent = node
|
||||
|
||||
if self.pre_visit(item):
|
||||
self.visit(item)
|
||||
self.generic_visit(item)
|
||||
self.post_visit(item)
|
||||
|
||||
elif isinstance(value, ast.AST):
|
||||
value._bandit_sibling = None
|
||||
value._bandit_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)
|
||||
# Run tests that do not require access to the AST,
|
||||
# but only to the whole file source:
|
||||
self.context = {
|
||||
"file_data": self.fdata,
|
||||
"filename": self.fname,
|
||||
"lineno": 0,
|
||||
"linerange": [0, 1],
|
||||
"col_offset": 0,
|
||||
}
|
||||
self.update_scores(self.tester.run_tests(self.context, "File"))
|
||||
return self.scores
|
||||
@@ -0,0 +1,83 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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 = []
|
||||
for arg in args:
|
||||
if arg == "File":
|
||||
func._checks.append("File")
|
||||
else:
|
||||
func._checks.append(utils.check_ast_node(arg))
|
||||
|
||||
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])
|
||||
@@ -0,0 +1,114 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from bandit.core import blacklisting
|
||||
from bandit.core import extension_loader
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BanditTestSet:
|
||||
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 _, 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:
|
||||
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.
|
||||
blacklisting.blacklist._test_id = "B001"
|
||||
blacklisting.blacklist._checks = blacklist.keys()
|
||||
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 []
|
||||
166
Backend/venv/lib/python3.12/site-packages/bandit/core/tester.py
Normal file
166
Backend/venv/lib/python3.12/site-packages/bandit/core/tester.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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:
|
||||
def __init__(self, testset, debug, nosec_lines, metrics):
|
||||
self.results = []
|
||||
self.testset = testset
|
||||
self.last_result = None
|
||||
self.debug = debug
|
||||
self.nosec_lines = nosec_lines
|
||||
self.metrics = metrics
|
||||
|
||||
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
|
||||
:return: a score based on the number and type of test results with
|
||||
extra metrics about nosec comments
|
||||
"""
|
||||
|
||||
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 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 result is not None:
|
||||
nosec_tests_to_skip = self._get_nosecs_from_contexts(
|
||||
temp_context, test_result=result
|
||||
)
|
||||
|
||||
if isinstance(temp_context["filename"], bytes):
|
||||
result.fname = temp_context["filename"].decode("utf-8")
|
||||
else:
|
||||
result.fname = temp_context["filename"]
|
||||
result.fdata = temp_context["file_data"]
|
||||
|
||||
if result.lineno is None:
|
||||
result.lineno = temp_context["lineno"]
|
||||
if result.linerange == []:
|
||||
result.linerange = temp_context["linerange"]
|
||||
if result.col_offset == -1:
|
||||
result.col_offset = temp_context["col_offset"]
|
||||
result.end_col_offset = temp_context.get(
|
||||
"end_col_offset", 0
|
||||
)
|
||||
result.test = name
|
||||
if result.test_id == "":
|
||||
result.test_id = test._test_id
|
||||
|
||||
# don't skip the test if there was no nosec comment
|
||||
if nosec_tests_to_skip is not None:
|
||||
# If the set is empty then it means that nosec was
|
||||
# used without test number -> update nosecs counter.
|
||||
# If the test id is in the set of tests to skip,
|
||||
# log and increment the skip by test count.
|
||||
if not nosec_tests_to_skip:
|
||||
LOG.debug("skipped, nosec without test number")
|
||||
self.metrics.note_nosec()
|
||||
continue
|
||||
if result.test_id in nosec_tests_to_skip:
|
||||
LOG.debug(
|
||||
f"skipped, nosec for test {result.test_id}"
|
||||
)
|
||||
self.metrics.note_skipped_test()
|
||||
continue
|
||||
|
||||
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
|
||||
else:
|
||||
nosec_tests_to_skip = self._get_nosecs_from_contexts(
|
||||
temp_context
|
||||
)
|
||||
if (
|
||||
nosec_tests_to_skip
|
||||
and test._test_id in nosec_tests_to_skip
|
||||
):
|
||||
LOG.warning(
|
||||
f"nosec encountered ({test._test_id}), but no "
|
||||
f"failed test on line {temp_context['lineno']}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.report_error(name, context, e)
|
||||
if self.debug:
|
||||
raise
|
||||
LOG.debug("Returning scores: %s", scores)
|
||||
return scores
|
||||
|
||||
def _get_nosecs_from_contexts(self, context, test_result=None):
|
||||
"""Use context and optional test result to get set of tests to skip.
|
||||
:param context: temp context
|
||||
:param test_result: optional test result
|
||||
:return: set of tests to skip for the line based on contexts
|
||||
"""
|
||||
nosec_tests_to_skip = set()
|
||||
base_tests = (
|
||||
self.nosec_lines.get(test_result.lineno, None)
|
||||
if test_result
|
||||
else None
|
||||
)
|
||||
context_tests = utils.get_nosec(self.nosec_lines, context)
|
||||
|
||||
# if both are none there were no comments
|
||||
# this is explicitly different from being empty.
|
||||
# empty set indicates blanket nosec comment without
|
||||
# individual test names or ids
|
||||
if base_tests is None and context_tests is None:
|
||||
nosec_tests_to_skip = None
|
||||
|
||||
# combine tests from current line and context line
|
||||
if base_tests is not None:
|
||||
nosec_tests_to_skip.update(base_tests)
|
||||
if context_tests is not None:
|
||||
nosec_tests_to_skip.update(context_tests)
|
||||
|
||||
return nosec_tests_to_skip
|
||||
|
||||
@staticmethod
|
||||
def report_error(test, context, error):
|
||||
what = "Bandit internal error running: "
|
||||
what += f"{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)
|
||||
398
Backend/venv/lib/python3.12/site-packages/bandit/core/utils.py
Normal file
398
Backend/venv/lib/python3.12/site-packages/bandit/core/utils.py
Normal file
@@ -0,0 +1,398 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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 = f"{_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):
|
||||
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.
|
||||
prefix = ""
|
||||
|
||||
return f"{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 = f"{config_file} : {message}"
|
||||
super().__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 ({}) in config file: {}".format(
|
||||
self.profile,
|
||||
self.config_file,
|
||||
)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def warnings_formatter(
|
||||
message, category=UserWarning, filename="", lineno=-1, line=""
|
||||
):
|
||||
"""Monkey patch for warnings.warn to suppress cruft output."""
|
||||
return f"{message}\n"
|
||||
|
||||
|
||||
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(
|
||||
f'Invalid python file path: "{path}" Missing path or file name'
|
||||
)
|
||||
|
||||
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 f"{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 calc_linerange(node):
|
||||
"""Calculate linerange for subtree"""
|
||||
if hasattr(node, "_bandit_linerange"):
|
||||
return node._bandit_linerange
|
||||
|
||||
lines_min = 9999999999
|
||||
lines_max = -1
|
||||
if hasattr(node, "lineno"):
|
||||
lines_min = node.lineno
|
||||
lines_max = node.lineno
|
||||
for n in ast.iter_child_nodes(node):
|
||||
lines_minmax = calc_linerange(n)
|
||||
lines_min = min(lines_min, lines_minmax[0])
|
||||
lines_max = max(lines_max, lines_minmax[1])
|
||||
|
||||
node._bandit_linerange = (lines_min, lines_max)
|
||||
|
||||
return (lines_min, lines_max)
|
||||
|
||||
|
||||
def linerange(node):
|
||||
"""Get line number range from a node."""
|
||||
if hasattr(node, "lineno"):
|
||||
return list(range(node.lineno, node.end_lineno + 1))
|
||||
else:
|
||||
if hasattr(node, "_bandit_linerange_stripped"):
|
||||
lines_minmax = node._bandit_linerange_stripped
|
||||
return list(range(lines_minmax[0], lines_minmax[1] + 1))
|
||||
|
||||
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
|
||||
if hasattr(node, "lineno"):
|
||||
lines_min = node.lineno
|
||||
lines_max = node.lineno
|
||||
for n in ast.iter_child_nodes(node):
|
||||
lines_minmax = calc_linerange(n)
|
||||
lines_min = min(lines_min, lines_minmax[0])
|
||||
lines_max = max(lines_max, lines_minmax[1])
|
||||
|
||||
for key in strip.keys():
|
||||
if strip[key] is not None:
|
||||
setattr(node, key, strip[key])
|
||||
|
||||
if lines_max == -1:
|
||||
lines_min = 0
|
||||
lines_max = 1
|
||||
|
||||
node._bandit_linerange_stripped = (lines_min, lines_max)
|
||||
|
||||
lines = list(range(lines_min, lines_max + 1))
|
||||
|
||||
"""Try and work around a known Python bug with multi-line strings."""
|
||||
# deal with multiline strings lineno behavior (Python issue #16806)
|
||||
if hasattr(node, "_bandit_sibling") and hasattr(
|
||||
node._bandit_sibling, "lineno"
|
||||
):
|
||||
start = min(lines)
|
||||
delta = node._bandit_sibling.lineno - start
|
||||
if delta > 1:
|
||||
return list(range(start, node._bandit_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.Constant 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.Constant or ast.BinOp) The node to process
|
||||
:param stop: (ast.Constant 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._bandit_parent, ast.BinOp):
|
||||
node = node._bandit_parent
|
||||
if isinstance(node, ast.BinOp):
|
||||
_get(node, bits, stop)
|
||||
return (
|
||||
node,
|
||||
" ".join(
|
||||
[
|
||||
x.value
|
||||
for x in bits
|
||||
if isinstance(x, ast.Constant) and isinstance(x.value, 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:
|
||||
# These ast Node types don't exist in Python 3.14, but plugins may
|
||||
# still check on them.
|
||||
if sys.version_info >= (3, 14) and name in (
|
||||
"Num",
|
||||
"Str",
|
||||
"Ellipsis",
|
||||
"NameConstant",
|
||||
"Bytes",
|
||||
):
|
||||
return name
|
||||
|
||||
node = getattr(ast, name)
|
||||
if issubclass(node, ast.AST):
|
||||
return name
|
||||
except AttributeError: # nosec(tkelsey): catching expected exception
|
||||
pass
|
||||
|
||||
raise TypeError(f"Error: {name} is not a valid node type in AST")
|
||||
|
||||
|
||||
def get_nosec(nosec_lines, context):
|
||||
for lineno in context["linerange"]:
|
||||
nosec = nosec_lines.get(lineno, None)
|
||||
if nosec is not None:
|
||||
return nosec
|
||||
return None
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,82 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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_cwe,
|
||||
issue_text,line_number,line_range,more_info
|
||||
examples/yaml_load.py,blacklist_calls,B301,MEDIUM,HIGH,
|
||||
https://cwe.mitre.org/data/definitions/20.html,"Use of unsafe yaml
|
||||
load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().
|
||||
",5,[5],https://bandit.readthedocs.io/en/latest/
|
||||
|
||||
.. versionadded:: 0.11.0
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
New field `more_info` added to output
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
# 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.
|
||||
import csv
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from bandit.core import docs_utils
|
||||
|
||||
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_cwe",
|
||||
"issue_text",
|
||||
"line_number",
|
||||
"col_offset",
|
||||
"end_col_offset",
|
||||
"line_range",
|
||||
"more_info",
|
||||
]
|
||||
|
||||
writer = csv.DictWriter(
|
||||
fileobj, fieldnames=fieldnames, extrasaction="ignore"
|
||||
)
|
||||
writer.writeheader()
|
||||
for result in results:
|
||||
r = result.as_dict(with_code=False)
|
||||
r["issue_cwe"] = r["issue_cwe"]["link"]
|
||||
r["more_info"] = docs_utils.get_url(r["test_id"])
|
||||
writer.writerow(r)
|
||||
|
||||
if fileobj.name != sys.stdout.name:
|
||||
LOG.info("CSV output written to file: %s", fileobj.name)
|
||||
@@ -0,0 +1,161 @@
|
||||
#
|
||||
# Copyright (c) 2017 Hewlett Packard Enterprise
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
================
|
||||
Custom Formatter
|
||||
================
|
||||
|
||||
This formatter outputs the issues in custom machine-readable format.
|
||||
|
||||
default template: ``{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}``
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
/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.
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
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, 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 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,
|
||||
"col": lambda issue: issue.col_offset,
|
||||
"end_col": lambda issue: issue.end_col_offset,
|
||||
"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,
|
||||
"cwe": lambda issue: issue.cwe,
|
||||
}
|
||||
|
||||
# Create dictionary with tag sets to speed up search for similar tags
|
||||
tag_sim_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(
|
||||
["{"]
|
||||
+ [f"{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"
|
||||
)
|
||||
with fileobj:
|
||||
for defect in results:
|
||||
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)
|
||||
@@ -0,0 +1,394 @@
|
||||
# Copyright (c) 2015 Rackspace, Inc.
|
||||
# Copyright (c) 2015 Hewlett Packard Enterprise
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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>CWE: </b>CWE-20 (https://cwe.mitre.org/data/definitions/20.html)<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://bandit.readthedocs.io/en/latest/
|
||||
plugins/yaml_load.html" target="_blank">
|
||||
https://bandit.readthedocs.io/en/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
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
New field `more_info` added to output
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from html import escape as html_escape
|
||||
|
||||
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 = """
|
||||
<!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 = """
|
||||
<body>
|
||||
{metrics}
|
||||
{skipped}
|
||||
|
||||
<br>
|
||||
<div id="results">
|
||||
{results}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
issue_block = """
|
||||
<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>CWE: </b><a href="{cwe_link}" target="_blank">CWE-{cwe.id}</a><br>
|
||||
<b>File: </b><a href="{path}" target="_blank">{path}</a><br>
|
||||
<b>Line number: </b>{line_number}<br>
|
||||
<b>More info: </b><a href="{url}" target="_blank">{url}</a><br>
|
||||
{code}
|
||||
{candidates}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
code_block = """
|
||||
<div class="code">
|
||||
<pre>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
"""
|
||||
|
||||
candidate_block = """
|
||||
<div class="candidates">
|
||||
<br>
|
||||
<b>Candidates: </b>
|
||||
{candidate_list}
|
||||
</div>
|
||||
"""
|
||||
|
||||
candidate_issue = """
|
||||
<div class="candidate">
|
||||
<div class="candidate-issues">
|
||||
<pre>{code}</pre>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
skipped_block = """
|
||||
<br>
|
||||
<div id="skipped">
|
||||
<div class="bordered-box">
|
||||
<b>Skipped files:</b><br><br>
|
||||
{files_list}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
metrics_block = """
|
||||
<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(
|
||||
f"{fname} <b>reason:</b> {reason}<br>"
|
||||
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 = html_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 = html_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=f"issue-sev-{issue.severity.lower()}",
|
||||
test_name=issue.test,
|
||||
test_id=issue.test_id,
|
||||
test_text=issue.text,
|
||||
severity=issue.severity,
|
||||
confidence=issue.confidence,
|
||||
cwe=issue.cwe,
|
||||
cwe_link=issue.cwe.link(),
|
||||
path=issue.fname,
|
||||
code=code,
|
||||
candidates=candidates,
|
||||
url=url,
|
||||
line_number=issue.lineno,
|
||||
)
|
||||
|
||||
# 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(header_block)
|
||||
wrapped_file.write(report_contents)
|
||||
|
||||
if fileobj.name != sys.stdout.name:
|
||||
LOG.info("HTML output written to file: %s", fileobj.name)
|
||||
@@ -0,0 +1,155 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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_cwe": {
|
||||
"id": 20,
|
||||
"link": "https://cwe.mitre.org/data/definitions/20.html"
|
||||
},
|
||||
"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://bandit.readthedocs.io/en/latest/",
|
||||
"test_name": "blacklist_calls",
|
||||
"test_id": "B301"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 0.10.0
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
New field `more_info` added to output
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
# Necessary so we can import the standard library json module while continuing
|
||||
# to name this file json.py. (Python 2 only)
|
||||
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(max_lines=lines)
|
||||
d["more_info"] = docs_utils.get_url(d["test_id"])
|
||||
if len(results[r]) > 1:
|
||||
d["candidates"] = [
|
||||
c.as_dict(max_lines=lines) for c in results[r]
|
||||
]
|
||||
collector.append(d)
|
||||
|
||||
else:
|
||||
collector = [r.as_dict(max_lines=lines) 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.now(datetime.timezone.utc).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)
|
||||
@@ -0,0 +1,374 @@
|
||||
# Copyright (c) Microsoft. All Rights Reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Note: this code mostly incorporated from
|
||||
# https://github.com/microsoft/bandit-sarif-formatter
|
||||
#
|
||||
r"""
|
||||
===============
|
||||
SARIF formatter
|
||||
===============
|
||||
|
||||
This formatter outputs the issues in SARIF formatted JSON.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
{
|
||||
"runs": [
|
||||
{
|
||||
"tool": {
|
||||
"driver": {
|
||||
"name": "Bandit",
|
||||
"organization": "PyCQA",
|
||||
"rules": [
|
||||
{
|
||||
"id": "B101",
|
||||
"name": "assert_used",
|
||||
"properties": {
|
||||
"tags": [
|
||||
"security",
|
||||
"external/cwe/cwe-703"
|
||||
],
|
||||
"precision": "high"
|
||||
},
|
||||
"helpUri": "https://bandit.readthedocs.io/en/1.7.8/plugins/b101_assert_used.html"
|
||||
}
|
||||
],
|
||||
"version": "1.7.8",
|
||||
"semanticVersion": "1.7.8"
|
||||
}
|
||||
},
|
||||
"invocations": [
|
||||
{
|
||||
"executionSuccessful": true,
|
||||
"endTimeUtc": "2024-03-05T03:28:48Z"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"metrics": {
|
||||
"_totals": {
|
||||
"loc": 1,
|
||||
"nosec": 0,
|
||||
"skipped_tests": 0,
|
||||
"SEVERITY.UNDEFINED": 0,
|
||||
"CONFIDENCE.UNDEFINED": 0,
|
||||
"SEVERITY.LOW": 1,
|
||||
"CONFIDENCE.LOW": 0,
|
||||
"SEVERITY.MEDIUM": 0,
|
||||
"CONFIDENCE.MEDIUM": 0,
|
||||
"SEVERITY.HIGH": 0,
|
||||
"CONFIDENCE.HIGH": 1
|
||||
},
|
||||
"./examples/assert.py": {
|
||||
"loc": 1,
|
||||
"nosec": 0,
|
||||
"skipped_tests": 0,
|
||||
"SEVERITY.UNDEFINED": 0,
|
||||
"SEVERITY.LOW": 1,
|
||||
"SEVERITY.MEDIUM": 0,
|
||||
"SEVERITY.HIGH": 0,
|
||||
"CONFIDENCE.UNDEFINED": 0,
|
||||
"CONFIDENCE.LOW": 0,
|
||||
"CONFIDENCE.MEDIUM": 0,
|
||||
"CONFIDENCE.HIGH": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"message": {
|
||||
"text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code."
|
||||
},
|
||||
"level": "note",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"region": {
|
||||
"snippet": {
|
||||
"text": "assert True\n"
|
||||
},
|
||||
"endColumn": 11,
|
||||
"endLine": 1,
|
||||
"startColumn": 0,
|
||||
"startLine": 1
|
||||
},
|
||||
"artifactLocation": {
|
||||
"uri": "examples/assert.py"
|
||||
},
|
||||
"contextRegion": {
|
||||
"snippet": {
|
||||
"text": "assert True\n"
|
||||
},
|
||||
"endLine": 1,
|
||||
"startLine": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"issue_confidence": "HIGH",
|
||||
"issue_severity": "LOW"
|
||||
},
|
||||
"ruleId": "B101",
|
||||
"ruleIndex": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "2.1.0",
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json"
|
||||
}
|
||||
|
||||
.. versionadded:: 1.7.8
|
||||
|
||||
""" # noqa: E501
|
||||
import datetime
|
||||
import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
|
||||
import sarif_om as om
|
||||
from jschema_to_python.to_json import to_json
|
||||
|
||||
import bandit
|
||||
from bandit.core import docs_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json"
|
||||
SCHEMA_VER = "2.1.0"
|
||||
TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||
|
||||
|
||||
def report(manager, fileobj, sev_level, conf_level, lines=-1):
|
||||
"""Prints issues in SARIF 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
|
||||
"""
|
||||
|
||||
log = om.SarifLog(
|
||||
schema_uri=SCHEMA_URI,
|
||||
version=SCHEMA_VER,
|
||||
runs=[
|
||||
om.Run(
|
||||
tool=om.Tool(
|
||||
driver=om.ToolComponent(
|
||||
name="Bandit",
|
||||
organization=bandit.__author__,
|
||||
semantic_version=bandit.__version__,
|
||||
version=bandit.__version__,
|
||||
)
|
||||
),
|
||||
invocations=[
|
||||
om.Invocation(
|
||||
end_time_utc=datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).strftime(TS_FORMAT),
|
||||
execution_successful=True,
|
||||
)
|
||||
],
|
||||
properties={"metrics": manager.metrics.data},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
run = log.runs[0]
|
||||
invocation = run.invocations[0]
|
||||
|
||||
skips = manager.get_skipped()
|
||||
add_skipped_file_notifications(skips, invocation)
|
||||
|
||||
issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level)
|
||||
|
||||
add_results(issues, run)
|
||||
|
||||
serializedLog = to_json(log)
|
||||
|
||||
with fileobj:
|
||||
fileobj.write(serializedLog)
|
||||
|
||||
if fileobj.name != sys.stdout.name:
|
||||
LOG.info("SARIF output written to file: %s", fileobj.name)
|
||||
|
||||
|
||||
def add_skipped_file_notifications(skips, invocation):
|
||||
if skips is None or len(skips) == 0:
|
||||
return
|
||||
|
||||
if invocation.tool_configuration_notifications is None:
|
||||
invocation.tool_configuration_notifications = []
|
||||
|
||||
for skip in skips:
|
||||
(file_name, reason) = skip
|
||||
|
||||
notification = om.Notification(
|
||||
level="error",
|
||||
message=om.Message(text=reason),
|
||||
locations=[
|
||||
om.Location(
|
||||
physical_location=om.PhysicalLocation(
|
||||
artifact_location=om.ArtifactLocation(
|
||||
uri=to_uri(file_name)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
invocation.tool_configuration_notifications.append(notification)
|
||||
|
||||
|
||||
def add_results(issues, run):
|
||||
if run.results is None:
|
||||
run.results = []
|
||||
|
||||
rules = {}
|
||||
rule_indices = {}
|
||||
for issue in issues:
|
||||
result = create_result(issue, rules, rule_indices)
|
||||
run.results.append(result)
|
||||
|
||||
if len(rules) > 0:
|
||||
run.tool.driver.rules = list(rules.values())
|
||||
|
||||
|
||||
def create_result(issue, rules, rule_indices):
|
||||
issue_dict = issue.as_dict()
|
||||
|
||||
rule, rule_index = create_or_find_rule(issue_dict, rules, rule_indices)
|
||||
|
||||
physical_location = om.PhysicalLocation(
|
||||
artifact_location=om.ArtifactLocation(
|
||||
uri=to_uri(issue_dict["filename"])
|
||||
)
|
||||
)
|
||||
|
||||
add_region_and_context_region(
|
||||
physical_location,
|
||||
issue_dict["line_range"],
|
||||
issue_dict["col_offset"],
|
||||
issue_dict["end_col_offset"],
|
||||
issue_dict["code"],
|
||||
)
|
||||
|
||||
return om.Result(
|
||||
rule_id=rule.id,
|
||||
rule_index=rule_index,
|
||||
message=om.Message(text=issue_dict["issue_text"]),
|
||||
level=level_from_severity(issue_dict["issue_severity"]),
|
||||
locations=[om.Location(physical_location=physical_location)],
|
||||
properties={
|
||||
"issue_confidence": issue_dict["issue_confidence"],
|
||||
"issue_severity": issue_dict["issue_severity"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def level_from_severity(severity):
|
||||
if severity == "HIGH":
|
||||
return "error"
|
||||
elif severity == "MEDIUM":
|
||||
return "warning"
|
||||
elif severity == "LOW":
|
||||
return "note"
|
||||
else:
|
||||
return "warning"
|
||||
|
||||
|
||||
def add_region_and_context_region(
|
||||
physical_location, line_range, col_offset, end_col_offset, code
|
||||
):
|
||||
if code:
|
||||
first_line_number, snippet_lines = parse_code(code)
|
||||
snippet_line = snippet_lines[line_range[0] - first_line_number]
|
||||
snippet = om.ArtifactContent(text=snippet_line)
|
||||
else:
|
||||
snippet = None
|
||||
|
||||
physical_location.region = om.Region(
|
||||
start_line=line_range[0],
|
||||
end_line=line_range[1] if len(line_range) > 1 else line_range[0],
|
||||
start_column=col_offset + 1,
|
||||
end_column=end_col_offset + 1,
|
||||
snippet=snippet,
|
||||
)
|
||||
|
||||
if code:
|
||||
physical_location.context_region = om.Region(
|
||||
start_line=first_line_number,
|
||||
end_line=first_line_number + len(snippet_lines) - 1,
|
||||
snippet=om.ArtifactContent(text="".join(snippet_lines)),
|
||||
)
|
||||
|
||||
|
||||
def parse_code(code):
|
||||
code_lines = code.split("\n")
|
||||
|
||||
# The last line from the split has nothing in it; it's an artifact of the
|
||||
# last "real" line ending in a newline. Unless, of course, it doesn't:
|
||||
last_line = code_lines[len(code_lines) - 1]
|
||||
|
||||
last_real_line_ends_in_newline = False
|
||||
if len(last_line) == 0:
|
||||
code_lines.pop()
|
||||
last_real_line_ends_in_newline = True
|
||||
|
||||
snippet_lines = []
|
||||
first_line_number = 0
|
||||
first = True
|
||||
for code_line in code_lines:
|
||||
number_and_snippet_line = code_line.split(" ", 1)
|
||||
if first:
|
||||
first_line_number = int(number_and_snippet_line[0])
|
||||
first = False
|
||||
|
||||
snippet_line = number_and_snippet_line[1] + "\n"
|
||||
snippet_lines.append(snippet_line)
|
||||
|
||||
if not last_real_line_ends_in_newline:
|
||||
last_line = snippet_lines[len(snippet_lines) - 1]
|
||||
snippet_lines[len(snippet_lines) - 1] = last_line[: len(last_line) - 1]
|
||||
|
||||
return first_line_number, snippet_lines
|
||||
|
||||
|
||||
def create_or_find_rule(issue_dict, rules, rule_indices):
|
||||
rule_id = issue_dict["test_id"]
|
||||
if rule_id in rules:
|
||||
return rules[rule_id], rule_indices[rule_id]
|
||||
|
||||
rule = om.ReportingDescriptor(
|
||||
id=rule_id,
|
||||
name=issue_dict["test_name"],
|
||||
help_uri=docs_utils.get_url(rule_id),
|
||||
properties={
|
||||
"tags": [
|
||||
"security",
|
||||
f"external/cwe/cwe-{issue_dict['issue_cwe'].get('id')}",
|
||||
],
|
||||
"precision": issue_dict["issue_confidence"].lower(),
|
||||
},
|
||||
)
|
||||
|
||||
index = len(rules)
|
||||
rules[rule_id] = rule
|
||||
rule_indices[rule_id] = index
|
||||
return rule, index
|
||||
|
||||
|
||||
def to_uri(file_path):
|
||||
pure_path = pathlib.PurePath(file_path)
|
||||
if pure_path.is_absolute():
|
||||
return pure_path.as_uri()
|
||||
else:
|
||||
# Replace backslashes with slashes.
|
||||
posix_path = pure_path.as_posix()
|
||||
# %-encode special characters.
|
||||
return urlparse.quote(posix_path)
|
||||
@@ -0,0 +1,244 @@
|
||||
# Copyright (c) 2015 Hewlett Packard Enterprise
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
================
|
||||
Screen formatter
|
||||
================
|
||||
|
||||
This formatter outputs the issues as color coded text to screen.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B506: yaml_load] Use of unsafe yaml load. Allows
|
||||
instantiation of arbitrary objects. Consider yaml.safe_load().
|
||||
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html)
|
||||
More Info: https://bandit.readthedocs.io/en/latest/
|
||||
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
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
New field `more_info` added to output
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from bandit.core import constants
|
||||
from bandit.core import docs_utils
|
||||
from bandit.core import test_properties
|
||||
|
||||
IS_WIN_PLATFORM = sys.platform.startswith("win32")
|
||||
COLORAMA = False
|
||||
|
||||
# This fixes terminal colors not displaying properly on Windows systems.
|
||||
# Colorama will intercept any ANSI escape codes and convert them to the
|
||||
# proper Windows console API calls to change text color.
|
||||
if IS_WIN_PLATFORM:
|
||||
try:
|
||||
import colorama
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
COLORAMA = True
|
||||
|
||||
|
||||
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 f"{COLOR['HEADER']}{text % args}{COLOR['DEFAULT']}"
|
||||
|
||||
|
||||
def get_verbose_details(manager):
|
||||
bits = []
|
||||
bits.append(header("Files in scope (%i):", len(manager.files_list)))
|
||||
tpl = "\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("Files excluded (%i):", len(manager.excluded_files)))
|
||||
bits.extend([f"\t{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, _ in constants.CRITERIA:
|
||||
bits.append(f"\tTotal issues (by {criteria.lower()}):")
|
||||
for rank in constants.RANKING:
|
||||
bits.append(
|
||||
"\t\t%s: %s"
|
||||
% (
|
||||
rank.capitalize(),
|
||||
manager.metrics.data["_totals"][f"{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(f"{indent} CWE: {str(issue.cwe)}")
|
||||
|
||||
bits.append(f"{indent} More Info: {docs_utils.get_url(issue.test_id)}")
|
||||
|
||||
bits.append(
|
||||
"%s Location: %s:%s:%s%s"
|
||||
% (
|
||||
indent,
|
||||
issue.fname,
|
||||
issue.lineno if show_lineno else "",
|
||||
issue.col_offset if show_lineno else "",
|
||||
COLOR["DEFAULT"],
|
||||
)
|
||||
)
|
||||
|
||||
if show_code:
|
||||
bits.extend(
|
||||
[indent + line for line 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 "\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("\n-- Candidate Issues --")
|
||||
for candidate in issues[issue]:
|
||||
bits.append(
|
||||
_output_issue_str(candidate, candidate_indent, lines=lines)
|
||||
)
|
||||
bits.append("\n")
|
||||
bits.append("-" * 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
|
||||
"""
|
||||
|
||||
if IS_WIN_PLATFORM and COLORAMA:
|
||||
colorama.init()
|
||||
|
||||
bits = []
|
||||
if not manager.quiet or manager.results_count(sev_level, conf_level):
|
||||
bits.append(
|
||||
header(
|
||||
"Run started:%s", datetime.datetime.now(datetime.timezone.utc)
|
||||
)
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if IS_WIN_PLATFORM and COLORAMA:
|
||||
colorama.deinit()
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2015 Hewlett Packard Enterprise
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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
|
||||
CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html)
|
||||
More Info: https://bandit.readthedocs.io/en/latest/
|
||||
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
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
New field `more_info` added to output
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from bandit.core import constants
|
||||
from bandit.core import docs_utils
|
||||
from bandit.core import test_properties
|
||||
from bandit.formatters import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_verbose_details(manager):
|
||||
bits = []
|
||||
bits.append(f"Files in scope ({len(manager.files_list)}):")
|
||||
tpl = "\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(f"Files excluded ({len(manager.excluded_files)}):")
|
||||
bits.extend([f"\t{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, _ in constants.CRITERIA:
|
||||
bits.append(f"\tTotal issues (by {criteria.lower()}):")
|
||||
for rank in constants.RANKING:
|
||||
bits.append(
|
||||
"\t\t%s: %s"
|
||||
% (
|
||||
rank.capitalize(),
|
||||
manager.metrics.data["_totals"][f"{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(
|
||||
f"{indent}>> Issue: [{issue.test_id}:{issue.test}] {issue.text}"
|
||||
)
|
||||
|
||||
bits.append(
|
||||
"%s Severity: %s Confidence: %s"
|
||||
% (
|
||||
indent,
|
||||
issue.severity.capitalize(),
|
||||
issue.confidence.capitalize(),
|
||||
)
|
||||
)
|
||||
|
||||
bits.append(f"{indent} CWE: {str(issue.cwe)}")
|
||||
|
||||
bits.append(f"{indent} More Info: {docs_utils.get_url(issue.test_id)}")
|
||||
|
||||
bits.append(
|
||||
"%s Location: %s:%s:%s"
|
||||
% (
|
||||
indent,
|
||||
issue.fname,
|
||||
issue.lineno if show_lineno else "",
|
||||
issue.col_offset if show_lineno else "",
|
||||
)
|
||||
)
|
||||
|
||||
if show_code:
|
||||
bits.extend(
|
||||
[indent + line for line 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 "\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("\n-- Candidate Issues --")
|
||||
for candidate in issues[issue]:
|
||||
bits.append(
|
||||
_output_issue_str(candidate, candidate_indent, lines=lines)
|
||||
)
|
||||
bits.append("\n")
|
||||
bits.append("-" * 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 = []
|
||||
|
||||
if not manager.quiet or manager.results_count(sev_level, conf_level):
|
||||
bits.append(
|
||||
f"Run started:{datetime.datetime.now(datetime.timezone.utc)}"
|
||||
)
|
||||
|
||||
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"])
|
||||
)
|
||||
bits.append(
|
||||
"\tTotal potential issues skipped due to specifically being "
|
||||
"disabled (e.g., #nosec BXXX): %i"
|
||||
% (manager.metrics.data["_totals"]["skipped_tests"])
|
||||
)
|
||||
|
||||
skipped = manager.get_skipped()
|
||||
bits.append(get_metrics(manager))
|
||||
bits.append(f"Files skipped ({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(result)
|
||||
|
||||
if fileobj.name != sys.stdout.name:
|
||||
LOG.info("Text output written to file: %s", fileobj.name)
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2016 Rackspace, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Utility functions for formatting plugins for Bandit."""
|
||||
import io
|
||||
|
||||
|
||||
def wrap_file_object(fileobj):
|
||||
"""If the fileobj passed in cannot handle text, use TextIOWrapper
|
||||
to handle the conversion.
|
||||
"""
|
||||
if isinstance(fileobj, io.TextIOBase):
|
||||
return fileobj
|
||||
return io.TextIOWrapper(fileobj)
|
||||
@@ -0,0 +1,97 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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"
|
||||
more_info="https://bandit.readthedocs.io/en/latest/">Test ID: B301
|
||||
Severity: MEDIUM Confidence: HIGH
|
||||
CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html) 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
|
||||
|
||||
.. versionchanged:: 1.5.0
|
||||
New field `more_info` added to output
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from xml.etree import ElementTree as ET # nosec: B405
|
||||
|
||||
from bandit.core import docs_utils
|
||||
|
||||
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\nCWE: %s\n%s\n"
|
||||
"Location %s:%s"
|
||||
)
|
||||
text %= (
|
||||
issue.test_id,
|
||||
issue.severity,
|
||||
issue.confidence,
|
||||
issue.cwe,
|
||||
issue.text,
|
||||
issue.fname,
|
||||
issue.lineno,
|
||||
)
|
||||
ET.SubElement(
|
||||
testcase,
|
||||
"error",
|
||||
more_info=docs_utils.get_url(issue.test_id),
|
||||
type=issue.severity,
|
||||
message=issue.text,
|
||||
).text = text
|
||||
|
||||
tree = ET.ElementTree(root)
|
||||
|
||||
if fileobj.name == sys.stdout.name:
|
||||
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)
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) 2017 VMware, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
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://bandit.readthedocs.io/en/latest/
|
||||
test_id: B506
|
||||
test_name: yaml_load
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
New field `CWE` added to output
|
||||
|
||||
"""
|
||||
# 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.
|
||||
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(max_lines=lines) 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.now(datetime.timezone.utc).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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user