Skip to content

Commit

Permalink
pre-commit hook blocks on old versions
Browse files Browse the repository at this point in the history
  • Loading branch information
Aaron Loo committed Jun 21, 2018
1 parent c802cec commit e4cf46a
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 138 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ exclude_lines =
# Don't complain if non-runnable code isn't run:
^if __name__ == ['"]__main__['"]:$
# Don't complain if performing logic for cross-version functionality
^\s*except ImportError:\b
# Don't complain if tests don't hit defensive assertion code:
^\s*raise AssertionError\b
^\s*raise NotImplementedError\b
Expand Down
35 changes: 7 additions & 28 deletions detect_secrets/core/secrets_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,15 @@ def __init__(self, plugins=(), exclude_regex=''):
:type exclude_regex: str
:param exclude_regex: for optional regex for ignored paths.
:type version: str
:param version: version of detect-secrets that SecretsCollection
is valid at.
"""
self.data = {}
self.plugins = plugins
self.exclude_regex = exclude_regex

@classmethod
def load_baseline_from_file(cls, filename):
"""Initialize a SecretsCollection object from file.
:param filename: string; name of file to load
:returns: SecretsCollection
:raises: IOError
"""
return cls.load_baseline_from_string(
cls._get_baseline_string_from_file(filename)
)

@classmethod
def _get_baseline_string_from_file(cls, filename):
"""Used for mocking, because we can't mock `open` (as it's also
used in `scan_file`."""
try:
with codecs.open(filename, encoding='utf-8') as f:
return f.read()

except (IOError, UnicodeDecodeError):
CustomLogObj.getLogger().error(
"Unable to open baseline file: %s.", filename
)

raise
self.version = VERSION

@classmethod
def load_baseline_from_string(cls, string):
Expand Down Expand Up @@ -103,6 +81,7 @@ def _load_baseline_from_dict(cls, data):
result.data[filename][secret] = secret

result.exclude_regex = data['exclude_regex']
result.version = data['version']

return result

Expand Down Expand Up @@ -246,7 +225,7 @@ def format_for_baseline_output(self):
'exclude_regex': self.exclude_regex,
'plugins_used': plugins_used,
'results': results,
'version': VERSION,
'version': self.version,
}

def _results_accumulator(self, filename):
Expand Down
106 changes: 87 additions & 19 deletions detect_secrets/pre_commit_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
import sys
import textwrap

try:
from functools import lru_cache
except ImportError:
from functools32 import lru_cache

from detect_secrets import VERSION
from detect_secrets.core.baseline import get_secrets_not_in_baseline
from detect_secrets.core.baseline import update_baseline_with_removed_secrets
from detect_secrets.core.log import CustomLog
Expand All @@ -28,7 +34,7 @@ def main(argv=None):
# it's valid, before doing any further computation.
baseline_collection = get_baseline(args.baseline[0])
except (IOError, ValueError):
# Error logs handled in load_baseline_from_file logic.
# Error logs handled within logic.
return 1

results = find_secrets_in_files(args)
Expand All @@ -53,14 +59,10 @@ def main(argv=None):
args.filenames,
)
if successful_update:
with open(args.baseline[0], 'w') as f:
f.write(
json.dumps(
baseline_collection.format_for_baseline_output(),
indent=2,
sort_keys=True
)
)
_write_to_baseline_file(
args.baseline[0],
baseline_collection.format_for_baseline_output(),
)

# The pre-commit framework should automatically detect a file change
# and print a relevant error message.
Expand All @@ -69,6 +71,18 @@ def main(argv=None):
return 0


def _write_to_baseline_file(filename, payload): # pragma: no cover
"""Breaking this function up for mockability."""
with open(filename, 'w') as f:
f.write(
json.dumps(
payload,
indent=2,
sort_keys=True,
)
)


def get_baseline(baseline_filename):
"""
:raises: IOError
Expand All @@ -79,7 +93,26 @@ def get_baseline(baseline_filename):

raise_exception_if_baseline_file_is_not_up_to_date(baseline_filename)

return SecretsCollection.load_baseline_from_file(baseline_filename)
baseline_string = _get_baseline_string_from_file(baseline_filename)
raise_exception_if_baseline_version_is_outdated(
json.loads(baseline_string).get('version')
)

return SecretsCollection.load_baseline_from_string(baseline_string)


def _get_baseline_string_from_file(filename): # pragma: no cover
"""Breaking this function up for mockability."""
try:
with open(filename) as f:
return f.read()

except IOError:
_get_custom_log().error(
'Unable to open baseline file: %s.', filename
)

raise


def raise_exception_if_baseline_file_is_not_up_to_date(filename):
Expand All @@ -98,15 +131,45 @@ def raise_exception_if_baseline_file_is_not_up_to_date(filename):
raise ValueError

if filename.encode() in files_changed_but_not_staged:
CustomLog(formatter='%(message)s').getLogger()\
.error((
'Your baseline file ({}) is unstaged.\n'
'`git add {}` to fix this.'
).format(
filename,
filename,
))
_get_custom_log().error((
'Your baseline file ({}) is unstaged.\n'
'`git add {}` to fix this.'
).format(
filename,
filename,
))

raise ValueError


def raise_exception_if_baseline_version_is_outdated(version):
"""
Version changes may cause breaking changes with past baselines.
Due to this, we want to make sure that the version that the
baseline was created with is compatible with the current version
of the scanner.
We use semantic versioning, and check for bumps in the MINOR
version (a good compromise, so we can release patches for other
non-baseline-related issues, without having all our users
recreate their baselines again).
:type version: str|None
:param version: version of baseline
:raises: ValueError
"""
if not version:
# Baselines created before this change, so by definition,
# would be outdated.
raise ValueError

baseline_version = version.split('.')
current_version = VERSION.split('.')

if int(current_version[0]) > int(baseline_version[0]):
raise ValueError
elif current_version[0] == baseline_version[0] and \
int(current_version[1]) > int(baseline_version[1]):
raise ValueError


Expand All @@ -129,13 +192,18 @@ def pretty_print_diagnostics(secrets):
:type secrets: SecretsCollection
"""
log = CustomLog(formatter='%(message)s').getLogger()
log = _get_custom_log()

_print_warning_header(log)
_print_secrets_found(log, secrets)
_print_mitigation_suggestions(log)


@lru_cache(maxsize=1)
def _get_custom_log():
return CustomLog(formatter='%(message)s').getLogger()


def _print_warning_header(log):
message = (
'Potential secrets about to be committed to git repo! Please rectify '
Expand Down
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@
'pyyaml',
'unidiff',
],
extras_require={':python_version=="2.7"': ['configparser', 'enum34']},
extras_require={
':python_version=="2.7"': [
'configparser',
'enum34',
'functools32',
]
},
entry_points={
'console_scripts': [
'detect-secrets = detect_secrets.main:main',
Expand Down
22 changes: 0 additions & 22 deletions tests/core/secrets_collection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,28 +279,6 @@ def test_load_baseline_with_invalid_input(self, mock_log):

assert mock_log.getLogger().error.called

def test_load_baseline_from_file(self, mock_gmtime):
original = self.get_baseline_dict(mock_gmtime)
with mock_open(json.dumps(original)):
secrets = SecretsCollection.load_baseline_from_file('does_not_matter')

self.assert_loaded_collection_is_original_collection(
original,
secrets.format_for_baseline_output()
)

def test_load_baseline_from_file_fails_early_on_bad_filename(self, mock_log):
with mock.patch.object(SecretsCollection, 'load_baseline_from_string') as \
mock_load_baseline_from_string, \
mock_open('will_throw_error') as mock_file:
mock_file().read.side_effect = MockUnicodeDecodeError

with pytest.raises(UnicodeDecodeError):
SecretsCollection.load_baseline_from_file('does_not_matter')

assert not mock_load_baseline_from_string.called
assert mock_log.getLogger().error.called

def get_baseline_dict(self, gmtime):
# They are all the same secret, so they should all have the same secret hash.
secret_hash = PotentialSecret.hash_secret('secret')
Expand Down
Loading

0 comments on commit e4cf46a

Please sign in to comment.