Skip to content

Commit b73c473

Browse files
author
Aaron Loo
committed
Merge remote-tracking branch nickiaconis/nextline-pragma into pre-v1-launch
2 parents da17012 + cf72d81 commit b73c473

File tree

5 files changed

+165
-57
lines changed

5 files changed

+165
-57
lines changed

Diff for: detect_secrets/core/scan.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,14 @@ def _scan_for_allowlisted_secrets_in_lines(
167167
get_settings().disable_filters('detect_secrets.filters.allowlist.is_line_allowlisted')
168168
get_filters.cache_clear()
169169

170-
for line_number, line in lines:
171-
line = line.rstrip()
172-
173-
if not is_line_allowlisted(filename, line):
170+
line_numbers, lines = zip(*lines)
171+
lines = [line.rstrip() for line in lines]
172+
for line_number, line in zip(line_numbers, lines):
173+
if not is_line_allowlisted(
174+
filename,
175+
line,
176+
context=get_code_snippet(lines, line_number),
177+
):
174178
continue
175179

176180
if any([
@@ -277,10 +281,19 @@ def _process_line_based_plugins(
277281
# filters return True.
278282
for line_number, line in lines:
279283
line = line.rstrip()
284+
code_snippet = get_code_snippet(
285+
lines=line_content,
286+
line_number=line_number,
287+
)
280288

281289
# We apply line-specific filters, and see whether that allows us to quit early.
282290
if any([
283-
inject_variables_into_function(filter_fn, filename=filename, line=line)
291+
inject_variables_into_function(
292+
filter_fn,
293+
filename=filename,
294+
line=line,
295+
context=code_snippet,
296+
)
284297
for filter_fn in get_filters_with_parameter('line')
285298
]):
286299
continue
@@ -294,10 +307,7 @@ def _process_line_based_plugins(
294307
secret=secret.secret_value,
295308
plugin=plugin,
296309
line=line,
297-
context=get_code_snippet(
298-
lines=line_content,
299-
line_number=line_number,
300-
),
310+
context=code_snippet,
301311
):
302312
log.debug(f'Skipping "{secret.secret_value}" due to `{filter_fn.path}`.')
303313
break

Diff for: detect_secrets/filters/allowlist.py

+65-32
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,87 @@
22
import re
33
from functools import lru_cache
44
from typing import Dict
5+
from typing import Iterable
56
from typing import List
67
from typing import Pattern
8+
from typing import Tuple
79

10+
from ..util.code_snippet import CodeSnippet
811

9-
def is_line_allowlisted(filename: str, line: str) -> bool:
10-
regexes = _get_allowlist_regexes()
1112

12-
_, ext = os.path.splitext(filename)
13-
if ext[1:] in _get_file_based_allowlist_regexes():
14-
regexes = _get_file_based_allowlist_regexes()[ext[1:]]
15-
16-
for regex in regexes:
17-
if regex.search(line):
18-
return True
13+
def is_line_allowlisted(filename: str, line: str, context: CodeSnippet) -> bool:
14+
for payload, regexes in zip(
15+
[line, context.previous_line],
16+
_get_allowlist_regexes_for_file(filename),
17+
):
18+
for regex in regexes:
19+
if regex.search(payload):
20+
return True
1921

2022
return False
2123

2224

2325
@lru_cache(maxsize=1)
24-
def _get_file_based_allowlist_regexes() -> Dict[str, List[Pattern]]:
26+
def _get_file_to_index_dict() -> Dict[str, int]:
2527
# Add to this mapping (and ALLOWLIST_REGEXES if applicable) lazily,
2628
# as more language specific file parsers are implemented.
2729
# Discussion: https://github.com/Yelp/detect-secrets/pull/105
2830
return {
29-
'yaml': [_get_allowlist_regexes()[0]],
31+
'yaml': 0,
3032
}
3133

3234

3335
@lru_cache(maxsize=1)
34-
def _get_allowlist_regexes() -> List[Pattern]:
35-
return [
36-
re.compile(r)
37-
for r in [
38-
# Note: Always use allowlist, whitelist will be deprecated in the future
39-
r'[ \t]+{} *pragma: ?(allow|white)list[ -]secret.*?{}[ \t]*$'.format(start, end)
40-
for start, end in (
41-
('#', ''), # e.g. python or yaml
42-
('//', ''), # e.g. golang
43-
(r'/\*', r' *\*/'), # e.g. c
44-
('\'', ''), # e.g. visual basic .net
45-
('--', ''), # e.g. sql
46-
(r'<!--[# \t]*?', ' *?-->'), # e.g. xml
47-
# many other inline comment syntaxes are not included,
48-
# because we want to be performant for
49-
# any(regex.search(line) for regex in ALLOWLIST_REGEXES)
50-
# calls. of course, this won't be a concern if detect-secrets
51-
# switches over to implementing file plugins for each supported
52-
# filetype.
53-
)
54-
]
36+
def _get_comment_tuples() -> Iterable[Tuple[str, str]]:
37+
return (
38+
('#', ''), # e.g. python or yaml
39+
('//', ''), # e.g. golang
40+
(r'/\*', r' *\*/'), # e.g. c
41+
('\'', ''), # e.g. visual basic .net
42+
('--', ''), # e.g. sql
43+
(r'<!--[# \t]*?', ' *?-->'), # e.g. xml
44+
# many other inline comment syntaxes are not included,
45+
# because we want to be performant for
46+
# any(regex.search(line) for regex in ALLOWLIST_REGEXES)
47+
# calls. of course, this won't be a concern if detect-secrets
48+
# switches over to implementing file plugins for each supported
49+
# filetype.
50+
)
51+
52+
53+
def _get_allowlist_regexes_for_file(filename: str) -> Iterable[List[Pattern]]:
54+
comment_tuples = _get_comment_tuples()
55+
56+
_, ext = os.path.splitext(filename)
57+
if ext[1:] in _get_file_to_index_dict():
58+
comment_tuples = (comment_tuples[_get_file_to_index_dict()[ext[1:]]],)
59+
60+
yield [
61+
_get_allowlist_regexes(comment_tuple=t, nextline=False)
62+
for t in comment_tuples
63+
]
64+
yield [
65+
_get_allowlist_regexes(comment_tuple=t, nextline=True)
66+
for t in comment_tuples
5567
]
68+
69+
70+
# Note: Cache size should be 2x the number of comment types
71+
@lru_cache(maxsize=12)
72+
def _get_allowlist_regexes(comment_tuple: Tuple[str, str], nextline: bool) -> Pattern:
73+
start = comment_tuple[0]
74+
end = comment_tuple[1]
75+
return re.compile(
76+
r'{}[ \t]*{} *pragma: ?{}{}[ -]secret.*?{}[ \t]*$'.format(
77+
# Note: No text can precede a nextline pragma, this prevents obscuring what is allowed
78+
# For instance, we want to prevent the following case from working:
79+
# foo = 'bar' # pragma: allowlist nextline secret
80+
# pass = 'hunter2'
81+
r'^' if nextline else '',
82+
start,
83+
# Note: Always use allowlist, whitelist will be deprecated in the future
84+
r'allowlist' if nextline else r'(allow|white)list',
85+
r'[ -]nextline' if nextline else '',
86+
end,
87+
),
88+
)

Diff for: detect_secrets/util/code_snippet.py

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def __init__(self, snippet: List[str], start_line: int, target_index: int) -> No
4949
def target_line(self) -> str:
5050
return self.lines[self.target_index]
5151

52+
@property
53+
def previous_line(self) -> str:
54+
if self.target_index == 0 or len(self.lines) < self.target_index:
55+
return ''
56+
return self.lines[self.target_index - 1]
57+
5258
@target_line.setter
5359
def target_line(self, value: str) -> None:
5460
self.lines[self.target_index] = value

Diff for: tests/filters/allowlist_filter_test.py

+67-16
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,68 @@
11
import pytest
22

33
from detect_secrets.filters.allowlist import is_line_allowlisted
4+
from detect_secrets.util.code_snippet import CodeSnippet
45

56

6-
@pytest.mark.parametrize(
7-
'prefix, suffix',
8-
(
9-
('#', ''),
10-
('# ', ' more text'),
7+
EXAMPLE_COMMENT_PARTS = (
8+
('#', ''),
9+
('# ', ' more text'),
1110

12-
('//', ''),
13-
('// ', ' more text'),
11+
('//', ''),
12+
('// ', ' more text'),
1413

15-
('/*', '*/'),
16-
('/* ', ' */'),
14+
('/*', '*/'),
15+
('/* ', ' */'),
1716

18-
('--', ''),
19-
('-- ', ' more text'),
17+
('--', ''),
18+
('-- ', ' more text'),
2019

21-
('<!--', '-->'),
22-
),
20+
('<!--', '-->'),
21+
)
22+
23+
24+
@pytest.mark.parametrize(
25+
'prefix, suffix',
26+
EXAMPLE_COMMENT_PARTS,
2327
)
2428
def test_basic(prefix, suffix):
29+
line = f'AKIAEXAMPLE {prefix}pragma: allowlist secret{suffix}'
2530
assert is_line_allowlisted(
2631
'filename',
27-
f'AKIAEXAMPLE {prefix}pragma: allowlist secret{suffix}',
32+
line,
33+
CodeSnippet([line], 0, 0),
2834
)
2935

3036

37+
@pytest.mark.parametrize(
38+
'prefix, suffix',
39+
EXAMPLE_COMMENT_PARTS,
40+
)
41+
def test_nextline(prefix, suffix):
42+
comment = f'{prefix}pragma: allowlist nextline secret{suffix}'
43+
line = 'AKIAEXAMPLE'
44+
assert is_line_allowlisted(
45+
'filename',
46+
line,
47+
CodeSnippet([comment, line], 0, 1),
48+
)
49+
50+
51+
def test_nextline_exclusivity():
52+
line = 'AKIAEXAMPLE # pragma: allowlist nextline secret'
53+
assert is_line_allowlisted(
54+
'filename',
55+
line,
56+
CodeSnippet([line], 0, 0),
57+
) is False
58+
59+
3160
def test_backwards_compatibility():
61+
line = 'AKIAEXAMPLE # pragma: whitelist secret'
3262
assert is_line_allowlisted(
3363
'filename',
34-
'AKIAEXAMPLE # pragma: whitelist secret',
64+
line,
65+
CodeSnippet([line], 0, 0),
3566
)
3667

3768

@@ -43,4 +74,24 @@ def test_backwards_compatibility():
4374
),
4475
)
4576
def test_file_based_regexes(line, expected_result):
46-
assert is_line_allowlisted('filename.yaml', line) is expected_result
77+
assert is_line_allowlisted(
78+
'filename.yaml',
79+
line,
80+
CodeSnippet([line], 0, 0),
81+
) is expected_result
82+
83+
84+
@pytest.mark.parametrize(
85+
'comment, expected_result',
86+
(
87+
('# pragma: allowlist nextline secret', True),
88+
('// pragma: allowlist nextline secret', False),
89+
),
90+
)
91+
def test_file_based_nextline_regexes(comment, expected_result):
92+
line = 'key: value'
93+
assert is_line_allowlisted(
94+
'filename.yaml',
95+
line,
96+
CodeSnippet([comment, line], 0, 1),
97+
) is expected_result

Diff for: tests/util/code_snippet_test.py

+8
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,11 @@ def test_basic(line_number, expected):
1616
assert ''.join(
1717
list(get_code_snippet(list('abcde'), line_number, lines_of_context=2)),
1818
) == expected
19+
20+
21+
def test_target_line():
22+
assert get_code_snippet(list('abcde'), 3, lines_of_context=2).target_line == 'c'
23+
24+
25+
def test_previous_line():
26+
assert get_code_snippet(list('abcde'), 3, lines_of_context=2).previous_line == 'b'

0 commit comments

Comments
 (0)