diff --git a/README.md b/README.md index 69da1ec..2c4e478 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,20 @@ Automated security testing built right into your workflow! You already use flake8 to lint all your code for errors, ensure docstrings are formatted correctly, sort your imports correctly, and much more... so why not ensure you are writing secure code while you're at it? If you already have flake8 installed all it takes is `pip install flake8-bandit`. +## Configuration + +To include or exclude tests, use the standard `.bandit` configuration file. An example valid `.bandit` config file: + +```text +[bandit] +exclude = /frontend,/scripts,/tests,/venv +tests: B101 +``` + +In this case, we've specified to ignore a number of paths, and to only test for B101. + +**Note:** flake8-bugbear uses bandit default prefix 'B' so this plugin replaces the 'B' with an 'S' for Security. For more information, see https://github.com/PyCQA/flake8-bugbear/issues/37 + ## How's it work? We use the [bandit](https://github.com/PyCQA/bandit) package from [PyCQA](http://meta.pycqa.org/en/latest/) for all the security testing. diff --git a/flake8_bandit.py b/flake8_bandit.py index 7f4a88c..117ca2c 100644 --- a/flake8_bandit.py +++ b/flake8_bandit.py @@ -1,8 +1,14 @@ """Implementation of bandit security testing in Flake8.""" import ast +import configparser +import sys +from functools import lru_cache +from pathlib import Path +from typing import Dict, NamedTuple, Set import pycodestyle from flake8.options.config import ConfigFileFinder +from flake8 import utils as stdin_utils from bandit.core.config import BanditConfig from bandit.core.meta_ast import BanditMetaAst @@ -10,18 +16,63 @@ from bandit.core.node_visitor import BanditNodeVisitor from bandit.core.test_set import BanditTestSet -try: - import configparser -except ImportError: - import ConfigParser as configparser -try: - from flake8.engine import pep8 as stdin_utils -except ImportError: - from flake8 import utils as stdin_utils +__version__ = "2.1.2" -__version__ = "2.1.2" +class Flake8BanditConfig(NamedTuple): + profile: Dict + target_paths: Set + excluded_paths: Set + + @classmethod + @lru_cache(maxsize=32) + def from_config_file(cls) -> "Flake8BanditConfig": + # set defaults + profile = {} + target_paths = set() + excluded_paths = set() + + # populate config from `.bandit` configuration file + ini_file = ConfigFileFinder("bandit", None, None).local_config_files() + config = configparser.ConfigParser() + try: + config.read(ini_file) + bandit_config = {k: v for k, v in config.items("bandit")} + + # test-set profile + if bandit_config.get("skips"): + profile["exclude"] = ( + bandit_config.get("skips").replace("S", "B").split(",") + ) + if bandit_config.get("tests"): + profile["include"] = ( + bandit_config.get("tests").replace("S", "B").split(",") + ) + + # file include/exclude + if bandit_config.get("targets"): + paths = bandit_config.get("targets").split(",") + for path in paths: + # convert absolute to relative + if path.startswith("/"): + path = "." + path + target_paths.add(Path(path)) + + if bandit_config.get("exclude"): + paths = bandit_config.get("exclude").split(",") + for path in paths: + # convert absolute to relative + if path.startswith("/"): + path = "." + path + excluded_paths.add(Path(path)) + + except (configparser.Error, KeyError, TypeError) as e: + profile = {} + if str(e) != "No section: 'bandit'": + sys.stderr.write(f"Unable to parse config file: {e}") + + return cls(profile, target_paths, excluded_paths) class BanditTester(object): @@ -41,25 +92,24 @@ def __init__(self, tree, filename, lines): self.lines = lines def _check_source(self): - ini_file = ConfigFileFinder("bandit", None, None).local_config_files() - config = configparser.ConfigParser() - try: - config.read(ini_file) - profile = {k: v.replace("S", "B") for k, v in config.items("bandit")} - if profile.get("skips"): - profile["exclude"] = profile.get("skips").split(",") - if profile.get("tests"): - profile["include"] = profile.get("tests").split(",") - except (configparser.Error, KeyError, TypeError) as e: - if str(e) != "No section: 'bandit'": - import sys - err = "Unable to parse config file: %s\n" % e - sys.stderr.write(err) - profile = {} + config = Flake8BanditConfig.from_config_file() + + # potentially exit early if bandit config tells us to + filepath = Path(self.filename) + filepaths = set(filepath.parents) + filepaths.add(filepath) + if ( + config.excluded_paths and config.excluded_paths.intersection(filepaths) + ) or ( + config.target_paths + and len(config.target_paths.intersection(filepaths)) == 0 + ): + return [] + bnv = BanditNodeVisitor( self.filename, BanditMetaAst(), - BanditTestSet(BanditConfig(), profile=profile), + BanditTestSet(BanditConfig(), profile=config.profile), False, [], Metrics(), diff --git a/setup.py b/setup.py index 1e6aa9a..3aa0781 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,6 @@ def run(self): "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Security", "Topic :: Software Development :: Libraries :: Python Modules", @@ -121,4 +120,5 @@ def run(self): ], # $ setup.py publish support. cmdclass={"upload": UploadCommand}, + python_requires=">=3.6", )