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

Add a (b)ack option to 'Is this a valid secret?' Closes Issue #63 #72

Merged
merged 8 commits into from
Sep 11, 2018
76 changes: 39 additions & 37 deletions detect_secrets/core/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ..plugins.core import initialize
from ..plugins.high_entropy_strings import HighEntropyStringsPlugin
from .baseline import merge_results
from .bidirectional_iterator import BidirectionalIterator
from .color import BashColor
from .color import Color
from .potential_secret import PotentialSecret
Expand All @@ -28,41 +29,50 @@ def audit_baseline(baseline_filename):
files_removed = _remove_nonexistent_files_from_baseline(original_baseline)

current_secret_index = 0
results = defaultdict(list)
for filename, secret, total in _secret_generator(original_baseline):
all_secrets = list(_secret_generator(original_baseline))
secrets_with_choices = [
(filename, secret) for filename, secret in all_secrets
if 'is_secret' not in secret
]
total_choices = len(secrets_with_choices)
secret_iterator = BidirectionalIterator(secrets_with_choices)

for filename, secret in secret_iterator:
_clear_screen()

if 'is_secret' not in secret:
current_secret_index += 1

try:
_print_context(
filename,
secret,
current_secret_index,
total,
original_baseline['plugins_used'],
)
decision = _get_user_decision()
except SecretNotFoundOnSpecifiedLineError:
decision = _get_user_decision(prompt_secret_decision=False)
else:
# Unfortunately, we need to add skipped secrets in results,
# otherwise merge_results won't know how to handle it.
decision = 's'
current_secret_index += 1

try:
_print_context(
filename,
secret,
current_secret_index,
total_choices,
original_baseline['plugins_used'],
)
decision = _get_user_decision(can_step_back=secret_iterator.can_step_back())
except SecretNotFoundOnSpecifiedLineError:
decision = _get_user_decision(prompt_secret_decision=False)

if decision == 'q':
print('Quitting...')
break

if decision == 'b':
current_secret_index -= 2
secret_iterator.step_back_on_next_iteration()

_handle_user_decision(decision, secret)
results[filename].append(secret)

if current_secret_index == 0 and not files_removed:
print('Nothing to audit!')
return

print('Saving progress...')
results = defaultdict(list)
for filename, secret in all_secrets:
results[filename].append(secret)

original_baseline['results'] = merge_results(
original_baseline['results'],
dict(results),
Expand Down Expand Up @@ -90,23 +100,9 @@ def _remove_nonexistent_files_from_baseline(baseline):

def _secret_generator(baseline):
"""Generates secrets to audit, from the baseline"""
num_secrets_to_parse = sum(
map(
lambda filename: len(
list(
filter(
lambda secret: 'is_secret' not in secret,
baseline['results'][filename],
),
),
),
baseline['results'],
),
)

for filename, secrets in baseline['results'].items():
for secret in secrets:
yield filename, secret, num_secrets_to_parse
yield filename, secret


def _clear_screen(): # pragma: no cover
Expand Down Expand Up @@ -174,14 +170,16 @@ def _print_context(filename, secret, count, total, plugin_settings): # pragma:
raise error_obj


def _get_user_decision(prompt_secret_decision=True):
def _get_user_decision(prompt_secret_decision=True, can_step_back=False):
"""
:type prompt_secret_decision: bool
:param prompt_secret_decision: if False, won't ask to label secret.
"""
allowable_user_input = ['s', 'q']
if prompt_secret_decision:
allowable_user_input.extend(['y', 'n'])
if can_step_back:
allowable_user_input.append('b')

user_input = None
while user_input not in allowable_user_input:
Expand All @@ -192,6 +190,8 @@ def _get_user_decision(prompt_secret_decision=True):
user_input_string = 'Is this a valid secret? (y)es, (n)o, '
else:
user_input_string = 'What would you like to do? '
if 'b' in allowable_user_input:
user_input_string += '(b)ack, '
user_input_string += '(s)kip, (q)uit: '

user_input = input(user_input_string)
Expand All @@ -206,6 +206,8 @@ def _handle_user_decision(decision, secret):
secret['is_secret'] = True
elif decision == 'n':
secret['is_secret'] = False
elif decision == 's' and 'is_secret' in secret:
del secret['is_secret']


def _save_baseline_to_file(filename, data): # pragma: no cover
Expand Down
31 changes: 31 additions & 0 deletions detect_secrets/core/bidirectional_iterator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class BidirectionalIterator(object):
def __init__(self, collection):
self.collection = collection
self.index = -1 # starts on -1, as index is increased _before_ getting result
self.step_back_once = False

def __next__(self):
if self.step_back_once:
self.index -= 1
self.step_back_once = False
else:
self.index += 1
if self.index < 0:
raise StopIteration
try:
result = self.collection[self.index]
except IndexError:
raise StopIteration
return result

def next(self):
Copy link
Collaborator

@KevinHock KevinHock Sep 4, 2018

Choose a reason for hiding this comment

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

Nit: Looping should call the __next__ method directly, so no need for a next method 👍

Copy link
Author

Choose a reason for hiding this comment

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

I added next to be python2 compatible (it was renamed form next to __next__ from python2 to 3).
However, this does not feel very clean - perhaps you know of a better way? :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Aha! Very good point, I only tested on Python 3. My bad. I am impressed by how thorough you are 👍

return self.__next__()

def step_back_on_next_iteration(self):
self.step_back_once = True

def can_step_back(self):
return self.index > 0

def __iter__(self):
return self
67 changes: 67 additions & 0 deletions tests/core/audit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,73 @@ def test_skip_decision(self, mock_printer):
'Saving progress...\n'
)

def test_go_back_and_change_yes_to_no(self, mock_printer):
modified_baseline = deepcopy(self.baseline)

values_to_inject = [None, False, True]
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

self.run_logic(['s', 'y', 'b', 'n', 'y'], modified_baseline)

assert mock_printer.message == (
'Saving progress...\n'
)

def test_go_back_and_change_no_to_yes(self, mock_printer):
modified_baseline = deepcopy(self.baseline)

values_to_inject = [None, True, True]
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

self.run_logic(['s', 'n', 'b', 'y', 'y'], modified_baseline)

assert mock_printer.message == (
'Saving progress...\n'
)

def test_go_back_and_change_yes_to_skip(self, mock_printer):
modified_baseline = deepcopy(self.baseline)

values_to_inject = [None, None, True]
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

self.run_logic(['s', 'y', 'b', 's', 'y'], modified_baseline)

assert mock_printer.message == (
'Saving progress...\n'
)

def test_go_back_several_steps(self, mock_printer):
modified_baseline = deepcopy(self.baseline)

values_to_inject = [False, False, False]
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

self.run_logic(
['s', 'y', 'b', 's', 'b', 'b', 'n', 'n', 'n'],
modified_baseline,
)

assert mock_printer.message == (
'Saving progress...\n'
)

def test_leapfrog_decision(self, mock_printer):
modified_baseline = deepcopy(self.leapfrog_baseline)
modified_baseline['results']['filenameA'][1]['is_secret'] = True
Expand Down
62 changes: 62 additions & 0 deletions tests/core/bidirectional_iterator_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from __future__ import absolute_import

import pytest

from detect_secrets.core import bidirectional_iterator


class TestBidirectionalIterator(object):

def test_no_input(self):
iterator = bidirectional_iterator.BidirectionalIterator([])
with pytest.raises(StopIteration):
iterator.__next__()

def test_cannot_step_back_too_far(self):
iterator = bidirectional_iterator.BidirectionalIterator([0])
iterator.step_back_on_next_iteration()
with pytest.raises(StopIteration):
iterator.__next__()

def test_cannot_step_back_too_far_after_stepping_in(self):
iterator = bidirectional_iterator.BidirectionalIterator([0, 1, 2])
for _ in range(3):
iterator.__next__()
for _ in range(2):
iterator.step_back_on_next_iteration()
iterator.__next__()
iterator.step_back_on_next_iteration()
with pytest.raises(StopIteration):
iterator.__next__()

def test_works_correctly_in_loop(self):
iterator = bidirectional_iterator.BidirectionalIterator([0, 1, 2, 3, 4, 5])
commands = [0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0]
command_count = 0
results = []
for index in iterator:
if commands[command_count]:
iterator.step_back_on_next_iteration()
results.append(index)
command_count += 1
assert results == [0, 1, 0, 1, 2, 1, 0, 1, 2, 3, 4, 3, 2, 3, 4, 5]

def test_normal_iterator_if_not_told_to_step_back(self):
input_list = [0, 1, 2, 3, 4, 5]
iterator = bidirectional_iterator.BidirectionalIterator(input_list)
results = []
for index in iterator:
results.append(index)
assert results == input_list

def test_knows_when_stepping_back_possible(self):
iterator = bidirectional_iterator.BidirectionalIterator([0, 1, 2, 3])
commands = [0, 1, 0, 0, 1, 1, 0, 0, 0, 0]
command_count = 0
results = []
for _ in iterator:
if commands[command_count]:
iterator.step_back_on_next_iteration()
results.append(iterator.can_step_back())
command_count += 1
assert results == [False, True, False, True, True, True, False, True, True, True]