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

Adding cli functionality to check strings in an adhoc manner #52

Merged
merged 1 commit into from
Jul 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 16 additions & 1 deletion detect_secrets/core/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ def __init__(self, subparser):
)

def add_arguments(self):
self._add_initialize_baseline_argument()
self._add_initialize_baseline_argument()\
._add_adhoc_scanning_argument()

PluginOptions(self.parser).add_arguments()

return self
Expand Down Expand Up @@ -118,6 +120,19 @@ def _add_initialize_baseline_argument(self):

return self

def _add_adhoc_scanning_argument(self):
self.parser.add_argument(
'--string',
nargs='?',
const=True,
help=(
'Scans an individual string, and displays configured '
'plugins\' verdict.'
),
)

return self


class AuditOptions(object):

Expand Down
39 changes: 33 additions & 6 deletions detect_secrets/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,21 @@ def main(argv=None):
log.set_debug_level(args.verbose)

if args.action == 'scan':
# Plugins are *always* rescanned with fresh settings, because
# we want to get the latest updates.
plugins = initialize.from_parser_builder(args.plugins)

if args.string:
line = args.string
Copy link
Collaborator

Choose a reason for hiding this comment

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

Super nit: it's not a line if it is a bool, maybe else:?

if isinstance(args.string, bool):
line = sys.stdin.read().splitlines()[0]

_scan_string(line, plugins)
return 0

print(
json.dumps(
_perform_scan(args),
_perform_scan(args, plugins),
indent=2,
sort_keys=True,
),
Expand All @@ -40,12 +52,27 @@ def main(argv=None):
return 0


def _perform_scan(args):
old_baseline = _get_existing_baseline(args.import_filename)
def _scan_string(line, plugins):
longest_plugin_name_length = max(
map(
lambda x: len(x.__class__.__name__),
plugins,
),
)

# Plugins are *always* rescanned with fresh settings, because
# we want to get the latest updates.
plugins = initialize.from_parser_builder(args.plugins)
output = [
('{:%d}: {}' % longest_plugin_name_length).format(
plugin.__class__.__name__,
plugin.adhoc_scan(line),
)
for plugin in plugins
]

print('\n'.join(sorted(output)))


def _perform_scan(args, plugins):
old_baseline = _get_existing_baseline(args.import_filename)

# Favors --exclude argument over existing baseline's regex (if exists)
if args.exclude:
Expand Down
23 changes: 23 additions & 0 deletions detect_secrets/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ def secret_generator(self, string): # pragma: no cover
"""
pass

def adhoc_scan(self, string):
"""To support faster discovery, we want the ability to conveniently
check what different plugins say regarding a single line/secret. This
supports that.

This is very similar to self.analyze_string, but allows the flexibility
for subclasses to add any other notable info (rather than just a
PotentialSecret type). e.g. HighEntropyStrings adds their Shannon
entropy in which they made their decision.

:type string: str
:param string: the string to analyze
:rtype: str
:returns: descriptive string that fits the format
<classname>: <returned-value>
"""
# TODO: Handle multiple secrets on single line.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit in general: Maybe github issue instead of todo's in the code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

(To bring attention of short-comings to reviewers, I normally call it out in github PR comment.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#53

results = self.analyze_string(string, 0, 'does_not_matter')
if not results:
return 'False'
else:
return 'True'

@property
def __dict__(self):
return {
Expand Down
15 changes: 15 additions & 0 deletions detect_secrets/plugins/high_entropy_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ def secret_generator(self, string):
if entropy_value > self.entropy_limit:
yield result

def adhoc_scan(self, string):
# Since it's an individual string, it's just bad UX to require quotes
# around the expected secret.
with self.non_quoted_string_regex():
results = self.analyze_string(string, 0, 'does_not_matter')

# NOTE: Trailing space allows for nicer formatting
output = 'False' if not results else 'True '
if self.regex.search(string):
output += ' ({})'.format(
round(self.calculate_shannon_entropy(string), 3),
)

return output

@contextmanager
def non_quoted_string_regex(self, strict=True):
"""For certain file formats, strings need not necessarily follow the
Expand Down
28 changes: 28 additions & 0 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ def test_scan_with_excludes_flag(self, mock_baseline_initialize):
'.',
)

def test_scan_string_basic(self, mock_baseline_initialize):
with mock_stdin(
'0123456789a',
), mock_printer(
main_module,
) as printer_shim:
assert main('scan --string'.split()) == 0
assert printer_shim.message == textwrap.dedent("""
Base64HighEntropyString: False (3.459)
HexHighEntropyString : True (3.459)
PrivateKeyDetector : False
""")[1:]

mock_baseline_initialize.assert_not_called()

def test_scan_string_cli_overrides_stdin(self):
with mock_stdin(
'0123456789a',
), mock_printer(
main_module,
) as printer_shim:
assert main('scan --string 012345'.split()) == 0
assert printer_shim.message == textwrap.dedent("""
Base64HighEntropyString: False (2.585)
HexHighEntropyString : False (2.121)
PrivateKeyDetector : False
""")[1:]

def test_reads_from_stdin(self, mock_merge_baseline):
with mock_stdin(json.dumps({'key': 'value'})):
assert main(['scan']) == 0
Expand Down