Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use different regexes in KeywordDetector to improve accuracy #86

Merged
merged 19 commits into from
Jan 3, 2019
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b9c80f9
Use different regexes in KeywordDetector to improve accuracy
KevinHock Oct 22, 2018
c4e7676
Trim regexes down, include secret 'foo'; as well.
KevinHock Oct 22, 2018
fce2837
Do not alert if whitespace is in secret
KevinHock Oct 22, 2018
3167cb1
[Keyword Plugin] Add 3 re's and their secret groups in dict
KevinHock Oct 31, 2018
b6dc9cc
Merge branch 'master' into upgrade_keyword_detector
KevinHock Oct 31, 2018
b89209f
Add a few more Keyword negative test-cases
KevinHock Oct 31, 2018
431ad12
Added a variety of accuracy improvements to Keyword Plugin (see tests)
KevinHock Nov 1, 2018
a27659a
:telescope:[Keyword Plugin] Filter false-positives
KevinHock Dec 14, 2018
a4c0432
Merge branch 'master' into upgrade_keyword_detector
KevinHock Dec 14, 2018
d8f4e29
:snake: Make tests pass
KevinHock Dec 14, 2018
ed6a374
:snake: Improve test coverage
KevinHock Dec 14, 2018
3fd9e87
:telescope:[Keyword Plugin] Precision improvements
KevinHock Dec 14, 2018
f15366e
:zap: Remove unnecessary wrapping parens
KevinHock Dec 21, 2018
e01d818
:telescope:[Keyword Plugin] Precision improvements
KevinHock Dec 28, 2018
4581aa8
:mortar_board: Eg. -> E.g.
KevinHock Dec 28, 2018
b7e48ab
:telescope:[Keyword Plugin] Handle dict['keyword']
KevinHock Dec 28, 2018
a37a9c9
:arrows_counterclockwise: Merge branch 'master' into upgrade_keyword_…
KevinHock Dec 28, 2018
d314550
:snake:[consistency] 2 spaces before pragma comment
KevinHock Dec 29, 2018
a29108b
:telescope:[Keyword Plugin] Precision improvements
KevinHock Jan 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions detect_secrets/core/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ..plugins.core import initialize
from ..plugins.high_entropy_strings import HighEntropyStringsPlugin
from ..plugins.keyword import KeywordDetector
from .baseline import format_baseline_for_output
from .baseline import merge_results
from .bidirectional_iterator import BidirectionalIterator
Expand Down Expand Up @@ -518,7 +519,13 @@ def _get_secret_with_context(
)


def _highlight_secret(secret_line, secret_lineno, secret, filename, plugin_settings):
def _highlight_secret(
secret_line,
secret_lineno,
secret,
filename,
plugin_settings,
):
"""
:type secret_line: str
:param secret_line: the line on which the secret is found
Expand All @@ -544,7 +551,11 @@ def _highlight_secret(secret_line, secret_lineno, secret, filename, plugin_setti
plugin_settings,
)

for raw_secret in _raw_secret_generator(plugin, secret_line):
for raw_secret in _raw_secret_generator(
plugin,
secret_line,
is_php_file=filename.endswith('.php'),
):
secret_obj = PotentialSecret(
plugin.secret_type,
filename,
Expand Down Expand Up @@ -572,10 +583,14 @@ def _highlight_secret(secret_line, secret_lineno, secret, filename, plugin_setti
)


def _raw_secret_generator(plugin, secret_line):
def _raw_secret_generator(plugin, secret_line, is_php_file):
"""Generates raw secrets by re-scanning the line, with the specified plugin"""
for raw_secret in plugin.secret_generator(secret_line):
yield raw_secret
if isinstance(plugin, KeywordDetector):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I felt :/ about writing it like this, but didn't see a better way.

Just wanted to put neon lights on it for the review 😅

for raw_secret in plugin.secret_generator(secret_line, is_php_file):
yield raw_secret
else:
for raw_secret in plugin.secret_generator(secret_line):
yield raw_secret

if issubclass(plugin.__class__, HighEntropyStringsPlugin):
with plugin.non_quoted_string_regex(strict=False):
Expand Down
4 changes: 1 addition & 3 deletions detect_secrets/core/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,7 @@ def _add_opt_out_options(self):
plugin.disable_flag_text,
action='store_true',
help=plugin.disable_help_text,
# Temporarily disabling the KeywordDetector
# Until we can test its effectiveness on more repositories
default=True if plugin.disable_flag_text == '--no-keyword-scan' else False,
default=False,
)

return self
Expand Down
3 changes: 3 additions & 0 deletions detect_secrets/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def secret_generator(self, string):

:type string: str
:param string: the secret to scan

:rtype: iter
:returns: Of all the identifiers found
"""
raise NotImplementedError

Expand Down
101 changes: 89 additions & 12 deletions detect_secrets/plugins/keyword.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,58 @@
"""
from __future__ import absolute_import

import re

from .base import BasePlugin
from detect_secrets.core.potential_secret import PotentialSecret


# Note: All values here should be lowercase
BLACKLIST = (
# NOTE all values here should be lowercase,
# otherwise _secret_generator can fail to match them
'pass =',
'apikey',
'api_key',
'pass',
'password',
'passwd',
'pwd',
'private_key',
'secret',
'secrete',
'token',
)
FALSE_POSITIVES = (
"''",
"''):",
'""',
'""):',
'false',
'none',
'not',
'password)',
'password},',
'true',
)
FOLLOWED_BY_COLON_RE = re.compile(
# e.g. token:
r'({})(("|\')?):(\s*?)(("|\')?)([^\s]+)(\5)'.format(
r'|'.join(BLACKLIST),
),
)
FOLLOWED_BY_EQUAL_SIGNS_RE = re.compile(
# e.g. my_password =
r'({})()(\s*?)=(\s*?)(("|\')?)([^\s]+)(\5)'.format(
r'|'.join(BLACKLIST),
),
)
FOLLOWED_BY_QUOTES_AND_SEMICOLON_RE = re.compile(
# e.g. private_key "something";
r'({})([^\s]*?)(\s*?)("|\')([^\s]+)(\4);'.format(
r'|'.join(BLACKLIST),
),
)
BLACKLIST_REGEX_TO_GROUP = {
FOLLOWED_BY_COLON_RE: 7,
FOLLOWED_BY_EQUAL_SIGNS_RE: 7,
FOLLOWED_BY_QUOTES_AND_SEMICOLON_RE: 5,
}


class KeywordDetector(BasePlugin):
Expand All @@ -53,7 +90,10 @@ class KeywordDetector(BasePlugin):
def analyze_string(self, string, line_num, filename):
output = {}

for identifier in self.secret_generator(string):
for identifier in self.secret_generator(
string,
is_php_file=filename.endswith('.php'),
):
secret = PotentialSecret(
self.secret_type,
filename,
Expand All @@ -64,10 +104,47 @@ def analyze_string(self, string, line_num, filename):

return output

def _secret_generator(self, lowercase_string):
for line in BLACKLIST:
if line in lowercase_string:
yield line
def secret_generator(self, string, is_php_file):
lowered_string = string.lower()

for REGEX, group_number in BLACKLIST_REGEX_TO_GROUP.items():
match = REGEX.search(lowered_string)
if match:
lowered_secret = match.group(group_number)

# ([^\s]+) guarantees lowered_secret is not ''
if not probably_false_positive(lowered_secret, is_php_file):
yield lowered_secret


def probably_false_positive(lowered_secret, is_php_file):
if (
'fake' in lowered_secret or
lowered_secret in FALSE_POSITIVES or
# If it is a .php file, do not report $variables
(
is_php_file and
lowered_secret[0] == '$'
)
):
return True

# No function calls
try:
if (
lowered_secret.index('(') < lowered_secret.index(')')
):
return True
except ValueError:
pass

# Skip over e.g. request.json_body['hey']
try:
if (
lowered_secret.index('[') < lowered_secret.index(']')
):
return True
except ValueError:
pass

def secret_generator(self, string):
return self._secret_generator(string.lower())
return False
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
secret = 'BEEF0123456789a'
seecret = 'BEEF0123456789a'
skipped_sequential_false_positive = '0123456789a'
print('second line')
var = 'third line'
14 changes: 5 additions & 9 deletions tests/core/audit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_making_decisions(self, mock_printer):
def test_quit_half_way(self, mock_printer):
modified_baseline = deepcopy(self.baseline)

for secrets in modified_baseline['results'].values():
for secrets in modified_baseline['results'].values(): # pragma: no cover
secrets[0]['is_secret'] = False
break

Expand Down Expand Up @@ -147,8 +147,7 @@ def test_go_back_several_steps(self, mock_printer):
for secrets in modified_baseline['results'].values():
for secret in secrets:
value = values_to_inject.pop(0)
if value is not None:
secret['is_secret'] = value
secret['is_secret'] = value

self.run_logic(
['s', 'y', 'b', 's', 'b', 'b', 'n', 'n', 'n'],
Expand All @@ -174,10 +173,7 @@ def run_logic(self, inputs, modified_baseline=None, input_baseline=None):
) as m:
audit.audit_baseline('will_be_mocked')

if not modified_baseline:
assert m.call_args[0][1] == self.baseline
else:
assert m.call_args[0][1] == modified_baseline
assert m.call_args[0][1] == modified_baseline

@contextmanager
def mock_env(self, user_inputs=None, baseline=None):
Expand Down Expand Up @@ -335,7 +331,7 @@ def test_compare(self, mock_printer):
# These files come after, because filenames are sorted first
assert headers[2] == textwrap.dedent("""
Secret: 3 of 4
Filename: test_data/short_files/first_line.py
Filename: test_data/short_files/first_line.php
Secret Type: Hex High Entropy String
Status: >> REMOVED <<
""")[1:]
Expand Down Expand Up @@ -406,7 +402,7 @@ def old_baseline(self):
],

# This entire file will be "removed"
'test_data/short_files/first_line.py': [
'test_data/short_files/first_line.php': [
{
'hashed_secret': '0de9a11b3f37872868ca49ecd726c955e25b6e21',
'line_number': 1,
Expand Down
2 changes: 1 addition & 1 deletion tests/core/secrets_collection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ def assert_loaded_collection_is_original_collection(self, original, new):
assert original[key] == new[key]


class MockBasePlugin(BasePlugin):
class MockBasePlugin(BasePlugin): # pragma: no cover
"""Abstract testing class, to implement abstract methods."""

def analyze_string(self, value):
Expand Down
1 change: 1 addition & 0 deletions tests/core/usage_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def test_consolidates_output_basic(self):
'Base64HighEntropyString': {
'base64_limit': 4.5,
},
'KeywordDetector': {},
'PrivateKeyDetector': {},
'AWSKeyDetector': {},
}
Expand Down
6 changes: 4 additions & 2 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def test_scan_string_basic(self, mock_baseline_initialize):
Base64HighEntropyString: False (3.459)
BasicAuthDetector : False
HexHighEntropyString : True (3.459)
KeywordDetector : False
PrivateKeyDetector : False
""")[1:]

Expand All @@ -108,6 +109,7 @@ def test_scan_string_cli_overrides_stdin(self):
Base64HighEntropyString: False (2.585)
BasicAuthDetector : False
HexHighEntropyString : False (2.121)
KeywordDetector : False
PrivateKeyDetector : False
""")[1:]

Expand Down Expand Up @@ -192,9 +194,9 @@ def test_old_baseline_ignored_with_update_flag(
'filename, expected_output',
[
(
'test_data/short_files/first_line.py',
'test_data/short_files/first_line.php',
textwrap.dedent("""
1:secret = 'BEEF0123456789a'
1:seecret = 'BEEF0123456789a'
2:skipped_sequential_false_positive = '0123456789a'
3:print('second line')
4:var = 'third line'
Expand Down
2 changes: 1 addition & 1 deletion tests/plugins/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


def test_fails_if_no_secret_type_defined():
class MockPlugin(BasePlugin):
class MockPlugin(BasePlugin): # pragma: no cover
def analyze_string(self, *args, **kwargs):
pass

Expand Down
Loading