Skip to content

Commit c233804

Browse files
authored
Merge pull request #73 from Yelp/adhoc-line-scanning-v2
(proper) Adding cli functionality to check strings in an adhoc manner
2 parents be0614b + e28021e commit c233804

File tree

5 files changed

+122
-14
lines changed

5 files changed

+122
-14
lines changed

detect_secrets/core/usage.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def __init__(self, subparser):
8383
)
8484

8585
def add_arguments(self):
86-
self._add_initialize_baseline_argument()
86+
self._add_initialize_baseline_argument()\
87+
._add_adhoc_scanning_argument()
88+
8789
PluginOptions(self.parser).add_arguments()
8890

8991
return self
@@ -124,6 +126,18 @@ def _add_initialize_baseline_argument(self):
124126

125127
return self
126128

129+
def _add_adhoc_scanning_argument(self):
130+
self.parser.add_argument(
131+
'--string',
132+
nargs='?',
133+
const=True,
134+
help=(
135+
'Scans an individual string, and displays configured '
136+
'plugins\' verdict.'
137+
),
138+
)
139+
return self
140+
127141

128142
class AuditOptions(object):
129143

detect_secrets/main.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,56 @@ def main(argv=None):
2626
log.set_debug_level(args.verbose)
2727

2828
if args.action == 'scan':
29-
output = json.dumps(
30-
_perform_scan(args),
31-
indent=2,
32-
sort_keys=True,
33-
)
29+
# Plugins are *always* rescanned with fresh settings, because
30+
# we want to get the latest updates.
31+
plugins = initialize.from_parser_builder(args.plugins)
32+
if args.string:
33+
line = args.string
34+
35+
if isinstance(args.string, bool):
36+
line = sys.stdin.read().splitlines()[0]
37+
38+
_scan_string(line, plugins)
3439

35-
if args.import_filename:
36-
_write_to_file(args.import_filename[0], output)
3740
else:
38-
print(output)
41+
output = json.dumps(
42+
_perform_scan(args, plugins),
43+
indent=2,
44+
sort_keys=True,
45+
)
46+
47+
if args.import_filename:
48+
_write_to_file(args.import_filename[0], output)
49+
else:
50+
print(output)
3951

4052
elif args.action == 'audit':
4153
audit.audit_baseline(args.filename[0])
4254

4355
return 0
4456

4557

46-
def _perform_scan(args):
47-
old_baseline = _get_existing_baseline(args.import_filename)
58+
def _scan_string(line, plugins):
59+
longest_plugin_name_length = max(
60+
map(
61+
lambda x: len(x.__class__.__name__),
62+
plugins,
63+
),
64+
)
4865

49-
# Plugins are *always* rescanned with fresh settings, because
50-
# we want to get the latest updates.
51-
plugins = initialize.from_parser_builder(args.plugins)
66+
output = [
67+
('{:%d}: {}' % longest_plugin_name_length).format(
68+
plugin.__class__.__name__,
69+
plugin.adhoc_scan(line),
70+
)
71+
for plugin in plugins
72+
]
73+
74+
print('\n'.join(sorted(output)))
75+
76+
77+
def _perform_scan(args, plugins):
78+
old_baseline = _get_existing_baseline(args.import_filename)
5279

5380
# Favors --exclude argument over existing baseline's regex (if exists)
5481
if args.exclude:

detect_secrets/plugins/base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@ def secret_generator(self, string):
5050
"""
5151
raise NotImplementedError
5252

53+
def adhoc_scan(self, string):
54+
"""To support faster discovery, we want the ability to conveniently
55+
check what different plugins say regarding a single line/secret. This
56+
supports that.
57+
58+
This is very similar to self.analyze_string, but allows the flexibility
59+
for subclasses to add any other notable info (rather than just a
60+
PotentialSecret type). e.g. HighEntropyStrings adds their Shannon
61+
entropy in which they made their decision.
62+
63+
:type string: str
64+
:param string: the string to analyze
65+
66+
:rtype: str
67+
:returns: descriptive string that fits the format
68+
<classname>: <returned-value>
69+
"""
70+
# TODO: Handle multiple secrets on single line.
71+
results = self.analyze_string(string, 0, 'does_not_matter')
72+
if not results:
73+
return 'False'
74+
else:
75+
return 'True'
76+
5377
@property
5478
def __dict__(self):
5579
return {

detect_secrets/plugins/high_entropy_strings.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ def secret_generator(self, string):
119119
if entropy_value > self.entropy_limit:
120120
yield result
121121

122+
def adhoc_scan(self, string):
123+
# Since it's an individual string, it's just bad UX to require quotes
124+
# around the expected secret.
125+
with self.non_quoted_string_regex():
126+
results = self.analyze_string(string, 0, 'does_not_matter')
127+
128+
# NOTE: Trailing space allows for nicer formatting
129+
output = 'False' if not results else 'True '
130+
if self.regex.search(string):
131+
output += ' ({})'.format(
132+
round(self.calculate_shannon_entropy(string), 3),
133+
)
134+
135+
return output
136+
122137
@contextmanager
123138
def non_quoted_string_regex(self, strict=True):
124139
"""For certain file formats, strings need not necessarily follow the

tests/main_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,34 @@ def test_scan_with_excludes_flag(self, mock_baseline_initialize):
7575
False,
7676
)
7777

78+
def test_scan_string_basic(self, mock_baseline_initialize):
79+
with mock_stdin(
80+
'012345678ab',
81+
), mock_printer(
82+
main_module,
83+
) as printer_shim:
84+
assert main('scan --string'.split()) == 0
85+
assert printer_shim.message == textwrap.dedent("""
86+
Base64HighEntropyString: False (3.459)
87+
HexHighEntropyString : True (3.459)
88+
PrivateKeyDetector : False
89+
""")[1:]
90+
91+
mock_baseline_initialize.assert_not_called()
92+
93+
def test_scan_string_cli_overrides_stdin(self):
94+
with mock_stdin(
95+
'012345678ab',
96+
), mock_printer(
97+
main_module,
98+
) as printer_shim:
99+
assert main('scan --string 012345'.split()) == 0
100+
assert printer_shim.message == textwrap.dedent("""
101+
Base64HighEntropyString: False (2.585)
102+
HexHighEntropyString : False (2.121)
103+
PrivateKeyDetector : False
104+
""")[1:]
105+
78106
def test_scan_with_all_files_flag(self, mock_baseline_initialize):
79107
with mock_stdin():
80108
assert main('scan --all-files'.split()) == 0

0 commit comments

Comments
 (0)