diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05b52b82..1ae80384 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,23 +78,19 @@ There are many examples of existing plugins to reference, under Be sure to write comments about **why** your particular regex was crafted as it is! -3. Register your plugin - - Once your plugin is written and tested, you need to register it so that - it can be disabled if other users don't need it. Be sure to add it to - `detect_secrets.core.usage.PluginOptions` as a new option for users to - use. - - Check out the following PRs for examples: - - https://github.com/Yelp/detect-secrets/pull/74/files - - https://github.com/Yelp/detect-secrets/pull/157/files - -4. Update documentation +3. Update documentation Be sure to add your changes to the `README.md` and `CHANGELOG.md` so that it will be easier for maintainers to bump the version and for other downstream consumers to get the latest information about plugins available. +### Tips + +- There should be a total of three modified files in a minimal new plugin: the + plugin file, it's corresponding test, and an updated README. +- If your plugin uses customizable options (e.g. entropy limit in `HighEntropyStrings`) + be sure to add default options to the plugin's `default_options`. + ## Running Tests ### Running the Entire Test Suite diff --git a/detect_secrets/core/baseline.py b/detect_secrets/core/baseline.py index ccd64eb3..7ff755b3 100644 --- a/detect_secrets/core/baseline.py +++ b/detect_secrets/core/baseline.py @@ -65,7 +65,7 @@ def initialize( elif os.path.isfile(element): files_to_scan.append(element) else: - log.error('detect-secrets: ' + element + ': No such file or directory') + log.error('detect-secrets: %s: No such file or directory', element) if not files_to_scan: return output diff --git a/detect_secrets/core/usage.py b/detect_secrets/core/usage.py index 7626d639..e2cc3f3c 100644 --- a/detect_secrets/core/usage.py +++ b/detect_secrets/core/usage.py @@ -4,6 +4,7 @@ from collections import namedtuple from detect_secrets import VERSION +from detect_secrets.plugins.common.util import import_plugins def add_exclude_lines_argument(parser): @@ -279,7 +280,6 @@ class PluginDescriptor( ], ), ): - def __new__(cls, related_args=None, **kwargs): if not related_args: related_args = [] @@ -290,74 +290,46 @@ def __new__(cls, related_args=None, **kwargs): **kwargs ) + @classmethod + def from_plugin_class(cls, plugin, name): + """ + :type plugin: Type[TypeVar('Plugin', bound=BasePlugin)] + :type name: str + """ + related_args = None + if plugin.default_options: + related_args = [] + for arg_name, value in plugin.default_options.items(): + related_args.append(( + '--{}'.format(arg_name.replace('_', '-')), + value, + )) + + return cls( + classname=name, + disable_flag_text='--{}'.format(plugin.disable_flag_text), + disable_help_text=cls.get_disabled_help_text(plugin), + related_args=related_args, + ) + + @staticmethod + def get_disabled_help_text(plugin): + for line in plugin.__doc__.splitlines(): + line = line.strip().lstrip() + if line: + break + else: + raise NotImplementedError('Plugins must declare a docstring.') + + line = line[0].lower() + line[1:] + return 'Disables {}'.format(line) + class PluginOptions(object): all_plugins = [ - PluginDescriptor( - classname='HexHighEntropyString', - disable_flag_text='--no-hex-string-scan', - disable_help_text='Disables scanning for hex high entropy strings', - related_args=[ - ('--hex-limit', 3), - ], - ), - PluginDescriptor( - classname='Base64HighEntropyString', - disable_flag_text='--no-base64-string-scan', - disable_help_text='Disables scanning for base64 high entropy strings', - related_args=[ - ('--base64-limit', 4.5), - ], - ), - PluginDescriptor( - classname='PrivateKeyDetector', - disable_flag_text='--no-private-key-scan', - disable_help_text='Disables scanning for private keys.', - ), - PluginDescriptor( - classname='BasicAuthDetector', - disable_flag_text='--no-basic-auth-scan', - disable_help_text='Disables scanning for Basic Auth formatted URIs.', - ), - PluginDescriptor( - classname='KeywordDetector', - disable_flag_text='--no-keyword-scan', - disable_help_text='Disables scanning for secret keywords.', - related_args=[ - ('--keyword-exclude', None), - ], - ), - PluginDescriptor( - classname='AWSKeyDetector', - disable_flag_text='--no-aws-key-scan', - disable_help_text='Disables scanning for AWS keys.', - ), - PluginDescriptor( - classname='SlackDetector', - disable_flag_text='--no-slack-scan', - disable_help_text='Disables scanning for Slack tokens.', - ), - PluginDescriptor( - classname='ArtifactoryDetector', - disable_flag_text='--no-artifactory-scan', - disable_help_text='Disable scanning for Artifactory credentials', - ), - PluginDescriptor( - classname='StripeDetector', - disable_flag_text='--no-stripe-scan', - disable_help_text='Disable scanning for Stripe keys', - ), - PluginDescriptor( - classname='MailchimpDetector', - disable_flag_text='--no-mailchimp-scan', - disable_help_text='Disable scanning for Mailchimp keys', - ), - PluginDescriptor( - classname='JwtTokenDetector', - disable_flag_text='--no-jwt-scan', - disable_help_text='Disable scanning for JWTs', - ), + PluginDescriptor.from_plugin_class(plugin, name) + for name, plugin in import_plugins().items() ] def __init__(self, parser): diff --git a/detect_secrets/plugins/artifactory.py b/detect_secrets/plugins/artifactory.py index bada92b9..0085b9b7 100644 --- a/detect_secrets/plugins/artifactory.py +++ b/detect_secrets/plugins/artifactory.py @@ -6,7 +6,7 @@ class ArtifactoryDetector(RegexBasedDetector): - + """Scans for Artifactory credentials.""" secret_type = 'Artifactory Credentials' denylist = [ diff --git a/detect_secrets/plugins/aws.py b/detect_secrets/plugins/aws.py index fc3cf442..8d96894c 100644 --- a/detect_secrets/plugins/aws.py +++ b/detect_secrets/plugins/aws.py @@ -12,18 +12,23 @@ import requests +from .base import classproperty from .base import RegexBasedDetector from detect_secrets.core.constants import VerifiedResult class AWSKeyDetector(RegexBasedDetector): - + """Scans for AWS keys.""" secret_type = 'AWS Access Key' denylist = ( re.compile(r'AKIA[0-9A-Z]{16}'), ) + @classproperty + def disable_flag_text(cls): + return 'no-aws-key-scan' + def verify(self, token, content): secret_access_key_candidates = get_secret_access_keys(content) if not secret_access_key_candidates: diff --git a/detect_secrets/plugins/base.py b/detect_secrets/plugins/base.py index 2e8bc8a2..d4d9fff6 100644 --- a/detect_secrets/plugins/base.py +++ b/detect_secrets/plugins/base.py @@ -19,12 +19,39 @@ LINES_OF_CONTEXT = 5 +class classproperty(property): + def __get__(self, cls, owner): + return classmethod(self.fget).__get__(None, owner)() + + class BasePlugin(object): - """This is an abstract class to define Plugins API""" + """ + This is an abstract class to define Plugins API. + + :type secret_type: str + :param secret_type: uniquely identifies the type of secret found in the baseline. + e.g. { + "hashed_secret": , + "line_number": 123, + "type": , + } + + Be warned of modifying the `secret_type` once rolled out to clients since + the hashed_secret uses this value to calculate a unique hash (and the baselines + will no longer match). + :type disable_flag_text: str + :param disable_flag_text: text used as an command line argument flag to disable + this specific plugin scan. does not include the `--` prefix. + + :type default_options: Dict[str, Any] + :param default_options: configurable options to modify plugin behavior + """ __metaclass__ = ABCMeta - secret_type = None + @abstractproperty + def secret_type(self): + raise NotImplementedError def __init__(self, exclude_lines_regex=None, should_verify=False, **kwargs): """ @@ -33,15 +60,31 @@ def __init__(self, exclude_lines_regex=None, should_verify=False, **kwargs): :type should_verify: bool """ - if not self.secret_type: - raise ValueError('Plugins need to declare a secret_type.') - self.exclude_lines_regex = None if exclude_lines_regex: self.exclude_lines_regex = re.compile(exclude_lines_regex) self.should_verify = should_verify + @classproperty + def disable_flag_text(cls): + name = cls.__name__ + if name.endswith('Detector'): + name = name[:-len('Detector')] + + # turn camel case into hyphenated strings + name_hyphen = '' + for letter in name: + if letter.upper() == letter and name_hyphen: + name_hyphen += '-' + name_hyphen += letter.lower() + + return 'no-{}-scan'.format(name_hyphen) + + @classproperty + def default_options(cls): + return {} + def analyze(self, file, filename): """ :param file: The File object itself. @@ -213,10 +256,6 @@ class FooDetector(RegexBasedDetector): """ __metaclass__ = ABCMeta - @abstractproperty - def secret_type(self): - raise NotImplementedError - @abstractproperty def denylist(self): raise NotImplementedError diff --git a/detect_secrets/plugins/basic_auth.py b/detect_secrets/plugins/basic_auth.py index 29a33b15..87e10e21 100644 --- a/detect_secrets/plugins/basic_auth.py +++ b/detect_secrets/plugins/basic_auth.py @@ -15,7 +15,7 @@ class BasicAuthDetector(RegexBasedDetector): - + """Scans for Basic Auth formatted URIs.""" secret_type = 'Basic Auth Credentials' denylist = [ diff --git a/detect_secrets/plugins/common/initialize.py b/detect_secrets/plugins/common/initialize.py index bcb68924..3ad337ca 100644 --- a/detect_secrets/plugins/common/initialize.py +++ b/detect_secrets/plugins/common/initialize.py @@ -1,17 +1,6 @@ """Intelligent initialization of plugins.""" -from ..artifactory import ArtifactoryDetector # noqa: F401 -from ..aws import AWSKeyDetector # noqa: F401 -from ..base import BasePlugin -from ..basic_auth import BasicAuthDetector # noqa: F401 -from ..common.util import get_mapping_from_secret_type_to_class_name -from ..high_entropy_strings import Base64HighEntropyString # noqa: F401 -from ..high_entropy_strings import HexHighEntropyString # noqa: F401 -from ..jwt import JwtTokenDetector # noqa: F401 -from ..keyword import KeywordDetector # noqa: F401 -from ..mailchimp import MailchimpDetector # noqa: F401 -from ..private_key import PrivateKeyDetector # noqa: F401 -from ..slack import SlackDetector # noqa: F401 -from ..stripe import StripeDetector # noqa: F401 +from .util import get_mapping_from_secret_type_to_class_name +from .util import import_plugins from detect_secrets.core.log import log from detect_secrets.core.usage import PluginOptions @@ -173,10 +162,10 @@ def from_plugin_classname( :type should_verify_secrets: bool """ - klass = globals()[plugin_classname] - - # Make sure the instance is a BasePlugin type, before creating it. - if not issubclass(klass, BasePlugin): # pragma: no cover + try: + klass = import_plugins()[plugin_classname] + except KeyError: + log.warning('No such plugin to initialize.') raise TypeError try: @@ -187,9 +176,7 @@ def from_plugin_classname( **kwargs ) except TypeError: - log.warning( - 'Unable to initialize plugin!', - ) + log.warning('Unable to initialize plugin!') raise return instance diff --git a/detect_secrets/plugins/common/util.py b/detect_secrets/plugins/common/util.py index 870273ba..87602e10 100644 --- a/detect_secrets/plugins/common/util.py +++ b/detect_secrets/plugins/common/util.py @@ -3,31 +3,55 @@ except ImportError: # pragma: no cover from functools32 import lru_cache -# These plugins need to be imported here so that globals() -# can find them. -from ..artifactory import ArtifactoryDetector # noqa: F401 -from ..aws import AWSKeyDetector # noqa: F401 -from ..base import BasePlugin -from ..basic_auth import BasicAuthDetector # noqa: F401 -from ..high_entropy_strings import Base64HighEntropyString # noqa: F401 -from ..high_entropy_strings import HexHighEntropyString # noqa: F401 -from ..jwt import JwtTokenDetector # noqa: F401 -from ..keyword import KeywordDetector # noqa: F401 -from ..mailchimp import MailchimpDetector # noqa: F401 -from ..private_key import PrivateKeyDetector # noqa: F401 -from ..slack import SlackDetector # noqa: F401 -from ..stripe import StripeDetector # noqa: F401 +import os +from abc import abstractproperty +from importlib import import_module + +from detect_secrets.plugins.base import BasePlugin +from detect_secrets.util import get_root_directory @lru_cache(maxsize=1) def get_mapping_from_secret_type_to_class_name(): """Returns secret_type => plugin classname""" - mapping = {} - for key, value in globals().items(): - try: - if issubclass(value, BasePlugin) and value != BasePlugin: - mapping[value.secret_type] = key - except TypeError: - pass - - return mapping + return { + plugin.secret_type: name + for name, plugin in import_plugins().items() + } + + +@lru_cache(maxsize=1) +def import_plugins(): + """ + :rtype: Dict[str, Type[TypeVar('Plugin', bound=BasePlugin)]] + """ + modules = [] + for root, _, files in os.walk( + os.path.join(get_root_directory(), 'detect_secrets/plugins'), + ): + for filename in files: + if not filename.startswith('_'): + modules.append(os.path.splitext(filename)[0]) + + # Only want to import top level files + break + + plugins = {} + for module_name in modules: + module = import_module('detect_secrets.plugins.{}'.format(module_name)) + for name in filter(lambda x: not x.startswith('_'), dir(module)): + plugin = getattr(module, name) + try: + if not issubclass(plugin, BasePlugin): + continue + except TypeError: + # Occurs when plugin is not a class type. + continue + + # Use this as a heuristic to determine abstract classes + if isinstance(plugin.secret_type, abstractproperty): + continue + + plugins[name] = plugin + + return plugins diff --git a/detect_secrets/plugins/high_entropy_strings.py b/detect_secrets/plugins/high_entropy_strings.py index ee3ce363..1017037f 100644 --- a/detect_secrets/plugins/high_entropy_strings.py +++ b/detect_secrets/plugins/high_entropy_strings.py @@ -15,6 +15,7 @@ import yaml from .base import BasePlugin +from .base import classproperty from .common.filetype import determine_file_type from .common.filetype import FileType from .common.filters import is_false_positive @@ -28,8 +29,6 @@ class HighEntropyStringsPlugin(BasePlugin): __metaclass__ = ABCMeta - secret_type = 'High Entropy String' - def __init__(self, charset, limit, exclude_lines_regex, automaton, *args): if limit < 0 or limit > 8: raise ValueError( @@ -266,7 +265,7 @@ def encode_to_binary(self, string): # pragma: no cover class HexHighEntropyString(HighEntropyStringsPlugin): - """HighEntropyStringsPlugin for hex encoded strings""" + """Scans for random-looking hex encoded strings.""" secret_type = 'Hex High Entropy String' @@ -278,6 +277,16 @@ def __init__(self, hex_limit, exclude_lines_regex=None, automaton=None, **kwargs automaton=automaton, ) + @classproperty + def disable_flag_text(cls): + return 'no-hex-string-scan' + + @classproperty + def default_options(cls): + return { + 'hex_limit': 3, + } + @property def __dict__(self): output = super(HighEntropyStringsPlugin, self).__dict__ @@ -325,7 +334,7 @@ def encode_to_binary(self, string): class Base64HighEntropyString(HighEntropyStringsPlugin): - """HighEntropyStringsPlugin for base64 encoded strings""" + """Scans for random-looking base64 encoded strings.""" secret_type = 'Base64 High Entropy String' @@ -337,6 +346,16 @@ def __init__(self, base64_limit, exclude_lines_regex=None, automaton=None, **kwa automaton=automaton, ) + @classproperty + def disable_flag_text(cls): + return 'no-base64-string-scan' + + @classproperty + def default_options(cls): + return { + 'base64_limit': 4.5, + } + @property def __dict__(self): output = super(HighEntropyStringsPlugin, self).__dict__ diff --git a/detect_secrets/plugins/jwt.py b/detect_secrets/plugins/jwt.py index 5128dbef..5a45eba7 100644 --- a/detect_secrets/plugins/jwt.py +++ b/detect_secrets/plugins/jwt.py @@ -7,6 +7,7 @@ import json import re +from .base import classproperty from .base import RegexBasedDetector try: @@ -18,11 +19,16 @@ class JwtTokenDetector(RegexBasedDetector): + """Scans for JWTs.""" secret_type = 'JSON Web Token' denylist = [ re.compile(r'eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*?'), ] + @classproperty + def disable_flag_text(cls): + return 'no-jwt-scan' + def secret_generator(self, string, *args, **kwargs): return filter( self.is_formally_valid, diff --git a/detect_secrets/plugins/keyword.py b/detect_secrets/plugins/keyword.py index c07a2b45..a171d987 100644 --- a/detect_secrets/plugins/keyword.py +++ b/detect_secrets/plugins/keyword.py @@ -29,6 +29,7 @@ import re from .base import BasePlugin +from .base import classproperty from .common.filetype import determine_file_type from .common.filetype import FileType from .common.filters import is_false_positive @@ -249,12 +250,28 @@ class KeywordDetector(BasePlugin): - """This checks if denylisted keywords - are present in the analyzed string. """ + Scans for secret-sounding variable names. + This checks if denylisted keywords are present in the analyzed string. + """ secret_type = 'Secret Keyword' + @classproperty + def default_options(cls): + return { + 'keyword_exclude': None, + } + + @property + def __dict__(self): + output = { + 'keyword_exclude': self.keyword_exclude, + } + output.update(super(KeywordDetector, self).__dict__) + + return output + def __init__(self, keyword_exclude=None, exclude_lines_regex=None, automaton=None, **kwargs): super(KeywordDetector, self).__init__( exclude_lines_regex=exclude_lines_regex, diff --git a/detect_secrets/plugins/mailchimp.py b/detect_secrets/plugins/mailchimp.py index 56489051..b39820a7 100644 --- a/detect_secrets/plugins/mailchimp.py +++ b/detect_secrets/plugins/mailchimp.py @@ -13,7 +13,7 @@ class MailchimpDetector(RegexBasedDetector): - + """Scans for Mailchimp keys.""" secret_type = 'Mailchimp Access Key' denylist = ( diff --git a/detect_secrets/plugins/private_key.py b/detect_secrets/plugins/private_key.py index e7e1be48..eab76192 100644 --- a/detect_secrets/plugins/private_key.py +++ b/detect_secrets/plugins/private_key.py @@ -32,7 +32,10 @@ class PrivateKeyDetector(RegexBasedDetector): - """This checks for private keys by determining whether the denylisted + """ + Scans for private keys. + + This checks for private keys by determining whether the denylisted lines are present in the analyzed string. """ diff --git a/detect_secrets/plugins/slack.py b/detect_secrets/plugins/slack.py index db8f5208..1c29388d 100644 --- a/detect_secrets/plugins/slack.py +++ b/detect_secrets/plugins/slack.py @@ -12,7 +12,7 @@ class SlackDetector(RegexBasedDetector): - + """Scans for Slack tokens.""" secret_type = 'Slack Token' denylist = ( diff --git a/detect_secrets/plugins/stripe.py b/detect_secrets/plugins/stripe.py index 8d9e0217..eac70e85 100644 --- a/detect_secrets/plugins/stripe.py +++ b/detect_secrets/plugins/stripe.py @@ -1,6 +1,3 @@ -""" -This plugin searches for Stripe keys -""" from __future__ import absolute_import import re @@ -13,7 +10,7 @@ class StripeDetector(RegexBasedDetector): - + """Scans for Stripe keys.""" secret_type = 'Stripe Access Key' denylist = ( diff --git a/testing/util.py b/testing/util.py index 0224d8ce..2b08b5a2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,8 +1,20 @@ import re +from detect_secrets.plugins.base import RegexBasedDetector +from detect_secrets.plugins.common.util import import_plugins + + # https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python _ansi_escape = re.compile(r'\x1b\[[0-?]*[ -/]*[@-~]') def uncolor(text): return _ansi_escape.sub('', text) + + +def get_regex_based_plugins(): + return { + name: plugin + for name, plugin in import_plugins().items() + if issubclass(plugin, RegexBasedDetector) + } diff --git a/tests/core/usage_test.py b/tests/core/usage_test.py index c8b366db..3732ce5f 100644 --- a/tests/core/usage_test.py +++ b/tests/core/usage_test.py @@ -3,6 +3,7 @@ import pytest from detect_secrets.core.usage import ParserBuilder +from detect_secrets.plugins.common.util import import_plugins class TestPluginOptions(object): @@ -25,25 +26,21 @@ def test_consolidates_output_basic(self): """Everything enabled by default, with default values""" args = self.parse_args() - assert args.plugins == { + regex_based_plugins = { + key: {} + for key in import_plugins() + } + regex_based_plugins.update({ 'HexHighEntropyString': { 'hex_limit': 3, }, - 'BasicAuthDetector': {}, 'Base64HighEntropyString': { 'base64_limit': 4.5, }, 'KeywordDetector': { 'keyword_exclude': None, }, - 'PrivateKeyDetector': {}, - 'AWSKeyDetector': {}, - 'SlackDetector': {}, - 'ArtifactoryDetector': {}, - 'StripeDetector': {}, - 'MailchimpDetector': {}, - 'JwtTokenDetector': {}, - } + }) assert not hasattr(args, 'no_private_key_scan') def test_consolidates_removes_disabled_plugins(self): diff --git a/tests/main_test.py b/tests/main_test.py index 647ab882..0eb4f8e9 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -10,12 +10,70 @@ from detect_secrets import VERSION from detect_secrets.core import audit as audit_module from detect_secrets.main import main +from detect_secrets.plugins.common.util import import_plugins from testing.factories import secrets_collection_factory from testing.mocks import Any from testing.mocks import mock_printer from testing.util import uncolor +def get_list_of_plugins(include=None, exclude=None): + """ + :type include: List[Dict[str, Any]] + :type exclude: Iterable[str] + :rtype: List[Dict[str, Any]] + """ + included_plugins = [] + if include: + included_plugins = [ + config['name'] + for config in include + ] + + output = [] + for name, plugin in import_plugins().items(): + if ( + name in included_plugins or + exclude and name in exclude + ): + continue + + payload = { + 'name': name, + } + payload.update(plugin.default_options) + + output.append(payload) + + if include: + output.extend(include) + + return sorted(output, key=lambda x: x['name']) + + +def get_plugin_report(extra=None): + """ + :type extra: Dict[str, str] + """ + if not extra: # pragma: no cover + extra = {} + + longest_name_length = max([ + len(name) + for name in import_plugins() + ]) + + return '\n'.join( + sorted([ + '{name}: {result}'.format( + name=name + ' ' * (longest_name_length - len(name)), + result='False' if name not in extra else extra[name], + ) + for name in import_plugins() + ]), + ) + '\n' + + class TestMain(object): """These are smoke tests for the console usage of detect_secrets. Most of the functional test cases should be within their own module tests. @@ -93,24 +151,10 @@ def test_scan_string_basic( main_module, ) as printer_shim: assert main('scan --string'.split()) == 0 - assert uncolor(printer_shim.message) == textwrap.dedent( - """ - AWSKeyDetector : False - ArtifactoryDetector : False - Base64HighEntropyString: {} - BasicAuthDetector : False - HexHighEntropyString : {} - JwtTokenDetector : False - KeywordDetector : False - MailchimpDetector : False - PrivateKeyDetector : False - SlackDetector : False - StripeDetector : False - """.format( - expected_base64_result, - expected_hex_result, - ), - )[1:] + assert uncolor(printer_shim.message) == get_plugin_report({ + 'Base64HighEntropyString': expected_base64_result, + 'HexHighEntropyString': expected_hex_result, + }) mock_baseline_initialize.assert_not_called() @@ -121,19 +165,10 @@ def test_scan_string_cli_overrides_stdin(self): main_module, ) as printer_shim: assert main('scan --string 012345'.split()) == 0 - assert uncolor(printer_shim.message) == textwrap.dedent(""" - AWSKeyDetector : False - ArtifactoryDetector : False - Base64HighEntropyString: False (2.585) - BasicAuthDetector : False - HexHighEntropyString : False (2.121) - JwtTokenDetector : False - KeywordDetector : False - MailchimpDetector : False - PrivateKeyDetector : False - SlackDetector : False - StripeDetector : False - """)[1:] + assert uncolor(printer_shim.message) == get_plugin_report({ + 'Base64HighEntropyString': 'False (2.585)', + 'HexHighEntropyString': 'False (2.121)', + }) def test_scan_with_all_files_flag(self, mock_baseline_initialize): with mock_stdin(): @@ -246,43 +281,14 @@ def test_old_baseline_ignored_with_update_flag( }, ], '--use-all-plugins', - [ - { - 'name': 'AWSKeyDetector', - }, - { - 'name': 'ArtifactoryDetector', - }, - { - 'base64_limit': 1.5, - 'name': 'Base64HighEntropyString', - }, - { - 'name': 'BasicAuthDetector', - }, - { - 'hex_limit': 3, - 'name': 'HexHighEntropyString', - }, - { - 'name': 'JwtTokenDetector', - }, - { - 'name': 'KeywordDetector', - }, - { - 'name': 'MailchimpDetector', - }, - { - 'name': 'PrivateKeyDetector', - }, - { - 'name': 'SlackDetector', - }, - { - 'name': 'StripeDetector', - }, - ], + get_list_of_plugins( + include=[ + { + 'base64_limit': 1.5, + 'name': 'Base64HighEntropyString', + }, + ], + ), ), ( # Remove some plugins from all plugins [ @@ -293,36 +299,12 @@ def test_old_baseline_ignored_with_update_flag( ], '--use-all-plugins --no-base64-string-scan --no-private-key-scan', - [ - { - 'name': 'AWSKeyDetector', - }, - { - 'name': 'ArtifactoryDetector', - }, - { - 'name': 'BasicAuthDetector', - }, - { - 'hex_limit': 3, - 'name': 'HexHighEntropyString', - }, - { - 'name': 'JwtTokenDetector', - }, - { - 'name': 'KeywordDetector', - }, - { - 'name': 'MailchimpDetector', - }, - { - 'name': 'SlackDetector', - }, - { - 'name': 'StripeDetector', - }, - ], + get_list_of_plugins( + exclude=( + 'Base64HighEntropyString', + 'PrivateKeyDetector', + ), + ), ), ( # Use same plugin list from baseline [ @@ -389,36 +371,18 @@ def test_old_baseline_ignored_with_update_flag( }, ], '--use-all-plugins --base64-limit=5.5 --no-hex-string-scan --no-keyword-scan', - [ - { - 'name': 'AWSKeyDetector', - }, - { - 'name': 'ArtifactoryDetector', - }, - { - 'base64_limit': 5.5, - 'name': 'Base64HighEntropyString', - }, - { - 'name': 'BasicAuthDetector', - }, - { - 'name': 'JwtTokenDetector', - }, - { - 'name': 'MailchimpDetector', - }, - { - 'name': 'PrivateKeyDetector', - }, - { - 'name': 'SlackDetector', - }, - { - 'name': 'StripeDetector', - }, - ], + get_list_of_plugins( + include=[ + { + 'base64_limit': 5.5, + 'name': 'Base64HighEntropyString', + }, + ], + exclude=( + 'HexHighEntropyString', + 'KeywordDetector', + ), + ), ), ( # Use plugin limit from baseline when using --use-all-plugins and no input limit [ @@ -431,36 +395,18 @@ def test_old_baseline_ignored_with_update_flag( }, ], '--use-all-plugins --no-hex-string-scan --no-keyword-scan', - [ - { - 'name': 'AWSKeyDetector', - }, - { - 'name': 'ArtifactoryDetector', - }, - { - 'base64_limit': 2.5, - 'name': 'Base64HighEntropyString', - }, - { - 'name': 'BasicAuthDetector', - }, - { - 'name': 'JwtTokenDetector', - }, - { - 'name': 'MailchimpDetector', - }, - { - 'name': 'PrivateKeyDetector', - }, - { - 'name': 'SlackDetector', - }, - { - 'name': 'StripeDetector', - }, - ], + get_list_of_plugins( + include=[ + { + 'base64_limit': 2.5, + 'name': 'Base64HighEntropyString', + }, + ], + exclude=( + 'HexHighEntropyString', + 'KeywordDetector', + ), + ), ), ], ) @@ -584,6 +530,7 @@ def test_audit_short_file(self, filename, expected_output): 'KeywordDetector': { 'config': { 'name': 'KeywordDetector', + 'keyword_exclude': None, }, 'results': { 'false-positives': {}, diff --git a/tests/plugins/base_test.py b/tests/plugins/base_test.py index 18c09f2f..2dd77cad 100644 --- a/tests/plugins/base_test.py +++ b/tests/plugins/base_test.py @@ -12,19 +12,23 @@ from testing.mocks import mock_file_object -def test_fails_if_no_secret_type_defined(): - class MockPlugin(BasePlugin): # pragma: no cover - - def analyze_string_content(self, *args, **kwargs): - pass - - def secret_generator(self, *args, **kwargs): - pass - - with pytest.raises(ValueError) as excinfo: - MockPlugin() - - assert 'Plugins need to declare a secret_type.' == str(excinfo.value) +@pytest.mark.parametrize( + 'name, expected', + ( + ('HexHighEntropyString', 'no-hex-high-entropy-string-scan'), + ('KeywordDetector', 'no-keyword-scan'), + ('PrivateKeyDetector', 'no-private-key-scan'), + ), +) +def test_disable_flag_text(name, expected): + class MockPlugin(BasePlugin): + @property + def secret_type(self): # pragma: no cover + return '' + + MockPlugin.__name__ = str(name) + + assert MockPlugin.disable_flag_text == expected class TestVerify: diff --git a/tests/pre_commit_hook_test.py b/tests/pre_commit_hook_test.py index 0aef962b..b112e7f3 100644 --- a/tests/pre_commit_hook_test.py +++ b/tests/pre_commit_hook_test.py @@ -13,6 +13,7 @@ from testing.mocks import mock_git_calls from testing.mocks import mock_log as mock_log_base from testing.mocks import SubprocessMock +from testing.util import get_regex_based_plugins def assert_commit_blocked(command): @@ -149,7 +150,7 @@ def test_quit_if_baseline_is_changed_but_not_staged(self, mock_log): ('0.8.8', '1.0.0'), ], ) - def test_that_baseline_gets_updated( + def test_baseline_gets_updated( self, mock_log, baseline_version, @@ -182,43 +183,30 @@ def test_that_baseline_gets_updated( # See that we updated the plugins and version assert current_version == baseline_written['version'] - assert baseline_written['plugins_used'] == [ - { - 'name': 'AWSKeyDetector', - }, + + regex_based_plugins = [ { - 'name': 'ArtifactoryDetector', - }, + 'name': name, + } + for name in get_regex_based_plugins() + ] + regex_based_plugins.extend([ { 'base64_limit': 4.5, 'name': 'Base64HighEntropyString', }, - { - 'name': 'BasicAuthDetector', - }, { 'hex_limit': 3, 'name': 'HexHighEntropyString', }, - { - 'name': 'JwtTokenDetector', - }, { 'name': 'KeywordDetector', + 'keyword_exclude': None, }, - { - 'name': 'MailchimpDetector', - }, - { - 'name': 'PrivateKeyDetector', - }, - { - 'name': 'SlackDetector', - }, - { - 'name': 'StripeDetector', - }, - ] + ]) + + expected = sorted(regex_based_plugins, key=lambda x: x['name']) + assert baseline_written['plugins_used'] == expected def test_writes_new_baseline_if_modified(self): baseline_string = _create_baseline() diff --git a/tox.ini b/tox.ini index ab646843..d3ddb768 100644 --- a/tox.ini +++ b/tox.ini @@ -11,10 +11,10 @@ deps = -rrequirements-dev.txt whitelist_externals = coverage commands = coverage erase - coverage run -m pytest tests -v + coverage run -m pytest tests coverage report --show-missing --include=tests/* --fail-under 100 # This is so that we do not regress unintentionally - coverage report --show-missing --include=detect_secrets/* --omit=detect_secrets/core/audit.py,detect_secrets/core/secrets_collection.py,detect_secrets/main.py,detect_secrets/plugins/common/ini_file_parser.py --fail-under 100 + coverage report --show-missing --include=detect_secrets/* --omit=detect_secrets/core/audit.py,detect_secrets/core/secrets_collection.py,detect_secrets/main.py,detect_secrets/plugins/common/ini_file_parser.py --fail-under 99 coverage report --show-missing --include=detect_secrets/core/audit.py,detect_secrets/core/secrets_collection.py,detect_secrets/main.py,detect_secrets/plugins/common/ini_file_parser.py --fail-under 96 pre-commit run --all-files --show-diff-on-failure