Skip to content

Commit ca961f4

Browse files
committed
SoftLayerDetector (Yelp#169)
Detects Softlayer API Keys Supports git-defenders/detect-secrets-discuss#120
1 parent d5a74f3 commit ca961f4

File tree

11 files changed

+229
-165
lines changed

11 files changed

+229
-165
lines changed

detect_secrets/core/baseline.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import re
44
import subprocess
55

6-
from detect_secrets import util
76
from detect_secrets.core.log import get_logger
87
from detect_secrets.core.secrets_collection import SecretsCollection
98

10-
119
log = get_logger(format_string='%(message)s')
1210

1311

@@ -294,8 +292,8 @@ def _get_git_tracked_files(rootdir='.'):
294292
git_files = subprocess.check_output(
295293
[
296294
'git',
297-
'-C', rootdir,
298295
'ls-files',
296+
rootdir,
299297
],
300298
stderr=fnull,
301299
)

detect_secrets/core/usage.py

+6
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,12 @@ class PluginOptions:
498498
disable_help_text='Disable scanning for GH credentials',
499499
is_default=False,
500500
),
501+
PluginDescriptor(
502+
classname='SoftLayerDetector',
503+
disable_flag_text='--no-sl-scan',
504+
disable_help_text='Disable scanning for SoftLayer keys',
505+
is_default=True,
506+
),
501507
]
502508

503509
default_plugins_list = [

detect_secrets/plugins/common/initialize.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ..keyword import KeywordDetector # noqa: F401
1111
from ..private_key import PrivateKeyDetector # noqa: F401
1212
from ..slack import SlackDetector # noqa: F401
13+
from ..softlayer import SoftLayerDetector # noqa: F401
1314
from ..stripe import StripeDetector # noqa: F401
1415
from detect_secrets.core.log import log
1516
from detect_secrets.core.usage import PluginOptions

detect_secrets/plugins/softlayer.py

+54-31
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
11
import re
2-
32
import requests
43

4+
from .base import RegexBasedDetector
55
from detect_secrets.core.constants import VerifiedResult
6-
from detect_secrets.plugins.base import RegexBasedDetector
76

87

9-
class SoftlayerDetector(RegexBasedDetector):
10-
"""Scans for Softlayer credentials."""
8+
class SoftLayerDetector(RegexBasedDetector):
119

1210
secret_type = 'SoftLayer Credentials'
1311

1412
# opt means optional
15-
sl = r'(?:softlayer|sl)(?:_|-|)(?:api|)'
16-
key_or_pass = r'(?:key|pwd|password|pass|token)'
13+
opt_quote = r'(?:"|\'|)'
14+
opt_dashes = r'(?:--|)'
15+
sl = r'(?:softlayer|sl)'
16+
opt_dash_undrscr = r'(?:_|-|)'
17+
opt_api = r'(?:api|)'
18+
key_or_pass = r'(?:key|pwd|password|pass)'
19+
opt_space = r'(?: |)'
20+
opt_equals = r'(?:=|:|:=|=>|)'
1721
secret = r'([a-z0-9]{64})'
1822
denylist = [
19-
RegexBasedDetector.assign_regex_generator(
20-
prefix_regex=sl,
21-
secret_keyword_regex=key_or_pass,
22-
secret_regex=secret,
23+
re.compile(
24+
r'{opt_quote}{opt_dashes}{sl}{opt_dash_undrscr}{opt_api}{opt_dash_undrscr}{key_or_pass}'
25+
'{opt_quote}{opt_space}{opt_equals}{opt_space}{opt_quote}{secret}{opt_quote}'.format(
26+
opt_quote=opt_quote,
27+
opt_dashes=opt_dashes,
28+
sl=sl,
29+
opt_dash_undrscr=opt_dash_undrscr,
30+
opt_api=opt_api,
31+
key_or_pass=key_or_pass,
32+
opt_space=opt_space,
33+
opt_equals=opt_equals,
34+
secret=secret,
35+
), flags=re.IGNORECASE,
2336
),
24-
2537
re.compile(
2638
r'(?:http|https)://api.softlayer.com/soap/(?:v3|v3.1)/([a-z0-9]{64})',
2739
flags=re.IGNORECASE,
2840
),
2941
]
3042

3143
def verify(self, token, content):
32-
usernames = find_username(content)
44+
usernames = get_username(content)
3345
if not usernames:
3446
return VerifiedResult.UNVERIFIED
3547

@@ -39,21 +51,32 @@ def verify(self, token, content):
3951
return VerifiedResult.VERIFIED_FALSE
4052

4153

42-
def find_username(content):
54+
def get_username(content):
4355
# opt means optional
44-
username_keyword = (
45-
r'(?:'
46-
r'username|id|user|userid|user-id|user-name|'
47-
r'name|user_id|user_name|uname'
48-
r')'
49-
)
56+
opt_quote = r'(?:"|\'|)'
57+
opt_dashes = r'(?:--|)'
58+
opt_sl = r'(?:softlayer|sl|)'
59+
opt_dash_undrscr = r'(?:_|-|)'
60+
opt_api = r'(?:api|)'
61+
username_keyword = r'(?:username|id|user|userid|user-id|user-name|name|user_id|user_name|uname)'
62+
opt_space = r'(?: |)'
63+
opt_equals = r'(?:=|:|:=|=>|)'
64+
seperator = r'(?: |=|:|:=|=>)+'
5065
username = r'(\w(?:\w|_|@|\.|-)+)'
5166
regex = re.compile(
52-
RegexBasedDetector.assign_regex_generator(
53-
prefix_regex=SoftlayerDetector.sl,
54-
secret_keyword_regex=username_keyword,
55-
secret_regex=username,
56-
),
67+
r'{opt_quote}{opt_dashes}{opt_sl}{opt_dash_undrscr}{opt_api}{opt_dash_undrscr}'
68+
'{username_keyword}{opt_quote}{seperator}{opt_quote}{username}{opt_quote}'.format(
69+
opt_quote=opt_quote,
70+
opt_dashes=opt_dashes,
71+
opt_sl=opt_sl,
72+
opt_dash_undrscr=opt_dash_undrscr,
73+
opt_api=opt_api,
74+
username_keyword=username_keyword,
75+
opt_space=opt_space,
76+
opt_equals=opt_equals,
77+
username=username,
78+
seperator=seperator,
79+
), flags=re.IGNORECASE,
5780
)
5881

5982
return [
@@ -64,16 +87,16 @@ def find_username(content):
6487

6588

6689
def verify_softlayer_key(username, token):
67-
headers = {'Content-type': 'application/json'}
6890
try:
91+
headers = {'Content-type': 'application/json'}
6992
response = requests.get(
7093
'https://api.softlayer.com/rest/v3/SoftLayer_Account.json',
7194
auth=(username, token), headers=headers,
7295
)
73-
except requests.exceptions.RequestException:
74-
return VerifiedResult.UNVERIFIED
7596

76-
if response.status_code == 200:
77-
return VerifiedResult.VERIFIED_TRUE
78-
else:
79-
return VerifiedResult.VERIFIED_FALSE
97+
if response.status_code == 200:
98+
return VerifiedResult.VERIFIED_TRUE
99+
else:
100+
return VerifiedResult.VERIFIED_FALSE
101+
except Exception:
102+
return VerifiedResult.UNVERIFIED

requirements-dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ responses
1212
tox-pip-extensions
1313
tox>=3.8
1414
unidiff
15+
responses

testing/factories.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33

44

55
def potential_secret_factory(
6-
type_='type',
7-
filename='filename',
8-
secret='secret',
9-
lineno=1,
6+
type_='type', filename='filename', secret='secret',
7+
lineno=1, output_raw=False,
108
):
119
"""This is only marginally better than creating PotentialSecret objects directly,
1210
because of the default values.
1311
"""
14-
return PotentialSecret(type_, filename, secret, lineno)
12+
return PotentialSecret(type_, filename, secret, lineno, output_raw=output_raw)
1513

1614

1715
def secrets_collection_factory(

tests/core/potential_secret_test.py

+9
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@ def test_json(self):
3939
secret = potential_secret_factory(secret='blah')
4040
for value in secret.json().values():
4141
assert value != 'blah'
42+
43+
def test_json_output_raw(self):
44+
secret = potential_secret_factory(secret='blah', output_raw=True)
45+
assert 'blah' in secret.json().values()
46+
47+
def test_other_factors(self):
48+
secret = potential_secret_factory(secret='blah')
49+
secret.other_factors['second factor'] = 'another one'
50+
assert {'second factor': 'another one'} == secret.json()['other_factors']

tests/core/usage_test.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def test_consolidates_output_basic(self):
3535
'StripeDetector': {},
3636
'ArtifactoryDetector': {},
3737
'GHDetector': {},
38+
'SoftLayerDetector': {},
3839
}
3940

4041
def test_consolidates_removes_disabled_plugins(self):

tests/main_test.py

+38-27
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ def test_old_baseline_ignored_with_update_flag(
327327
{
328328
'name': 'SlackDetector',
329329
},
330+
{
331+
'name': 'SoftLayerDetector',
332+
},
330333
{
331334
'name': 'StripeDetector',
332335
},
@@ -373,6 +376,9 @@ def test_old_baseline_ignored_with_update_flag(
373376
{
374377
'name': 'SlackDetector',
375378
},
379+
{
380+
'name': 'SoftLayerDetector',
381+
},
376382
{
377383
'name': 'StripeDetector',
378384
},
@@ -481,6 +487,9 @@ def test_old_baseline_ignored_with_update_flag(
481487
{
482488
'name': 'SlackDetector',
483489
},
490+
{
491+
'name': 'SoftLayerDetector',
492+
},
484493
{
485494
'name': 'StripeDetector',
486495
},
@@ -535,6 +544,9 @@ def test_old_baseline_ignored_with_update_flag(
535544
{
536545
'name': 'SlackDetector',
537546
},
547+
{
548+
'name': 'SoftLayerDetector',
549+
},
538550
{
539551
'name': 'StripeDetector',
540552
},
@@ -653,33 +665,32 @@ def test_audit_short_file(self, filename, expected_output):
653665
expected_output,
654666
)
655667

656-
@pytest.mark.parametrize(
657-
'filename, expected_output',
658-
[
659-
(
660-
'test_data/short_files/first_line.php',
661-
{
662-
'KeywordDetector': {
663-
'config': {
664-
'name': 'KeywordDetector',
665-
'keyword_exclude': None,
666-
},
667-
'results': {
668-
'false-positives': {},
669-
'true-positives': {},
670-
'unknowns': {
671-
'test_data/short_files/first_line.php': [{
672-
'line': "secret = 'notHighEnoughEntropy'",
673-
'plaintext': 'nothighenoughentropy',
674-
}],
675-
},
676-
},
677-
},
678-
},
679-
),
680-
],
681-
)
682-
def test_audit_display_results(self, filename, expected_output):
668+
def test_scan_with_default_plugin(self):
669+
filename = 'test_data/short_files/last_line.ini'
670+
plugins_used = [
671+
{
672+
'name': 'AWSKeyDetector',
673+
},
674+
{
675+
'name': 'ArtifactoryDetector',
676+
},
677+
{
678+
'name': 'BasicAuthDetector',
679+
},
680+
{
681+
'name': 'PrivateKeyDetector',
682+
},
683+
{
684+
'name': 'SlackDetector',
685+
},
686+
{
687+
'name': 'SoftLayerDetector',
688+
},
689+
{
690+
'name': 'StripeDetector',
691+
},
692+
]
693+
683694
with mock_stdin(), mock_printer(
684695
main_module,
685696
) as printer_shim:

0 commit comments

Comments
 (0)