Skip to content

Commit

Permalink
dynamic plugin usage
Browse files Browse the repository at this point in the history
This removes the hard-coded PluginDescriptors, as well as the associated
hard-coded tests that needed to be changed with the addition of a new
plugin.

In doing so, this also addresses #146.
  • Loading branch information
Aaron Loo committed Oct 3, 2019
1 parent f30c94d commit 9283d25
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 268 deletions.
2 changes: 1 addition & 1 deletion detect_secrets/core/baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 37 additions & 65 deletions detect_secrets/core/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -279,7 +280,6 @@ class PluginDescriptor(
],
),
):

def __new__(cls, related_args=None, **kwargs):
if not related_args:
related_args = []
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion detect_secrets/plugins/artifactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class ArtifactoryDetector(RegexBasedDetector):

"""Scans for Artifactory credentials."""
secret_type = 'Artifactory Credentials'

denylist = [
Expand Down
7 changes: 6 additions & 1 deletion detect_secrets/plugins/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
57 changes: 48 additions & 9 deletions detect_secrets/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": <hash>,
"line_number": 123,
"type": <secret_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):
"""
Expand All @@ -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.
Expand Down Expand Up @@ -213,10 +256,6 @@ class FooDetector(RegexBasedDetector):
"""
__metaclass__ = ABCMeta

@abstractproperty
def secret_type(self):
raise NotImplementedError

@abstractproperty
def denylist(self):
raise NotImplementedError
Expand Down
2 changes: 1 addition & 1 deletion detect_secrets/plugins/basic_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


class BasicAuthDetector(RegexBasedDetector):

"""Scans for Basic Auth formatted URIs."""
secret_type = 'Basic Auth Credentials'

denylist = [
Expand Down
11 changes: 7 additions & 4 deletions detect_secrets/plugins/common/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ def from_plugin_classname(
:type should_verify_secrets: bool
"""
klass = import_plugins()[plugin_classname]
try:
klass = import_plugins()[plugin_classname]
except KeyError:
log.warning('No such plugin to initialize.')
raise TypeError

try:
instance = klass(
exclude_lines_regex=exclude_lines_regex,
Expand All @@ -171,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
Expand Down
27 changes: 23 additions & 4 deletions detect_secrets/plugins/high_entropy_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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'

Expand All @@ -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__
Expand Down Expand Up @@ -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'

Expand All @@ -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__
Expand Down
6 changes: 6 additions & 0 deletions detect_secrets/plugins/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import re

from .base import classproperty
from .base import RegexBasedDetector

try:
Expand All @@ -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,
Expand Down
Loading

3 comments on commit 9283d25

@pedro-sage
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, is there a release date estimated that will include this commit? Detect-secrets-server is not using this latest version and consequently not being able to turn off high entropy strings plugins. Thanks!

@KevinHock
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @pedro-sage, I don’t think this commit is related to what you want, we have the same options on detect-secrets before and after, there’s no change in —help output.

@pedro-sage
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @KevinHock you are right, it's just that I was following the code because --no-hex-string-scan was not working and got confused because I pulled wrong version. Turns out it all depends on if the flag is used on add, not on scan. Thanks!

Please sign in to comment.