Skip to content

Commit d700108

Browse files
committedJun 24, 2020
SoftLayerDetector (Yelp#169)
* SoftLayerDetector Detects Softlayer API Keys Supports git-defenders/detect-secrets-discuss#120 * Lines were too long * Address PR comments 1 * Address PR comments 2
1 parent b15fa55 commit d700108

File tree

2 files changed

+77
-207
lines changed

2 files changed

+77
-207
lines changed
 

‎detect_secrets/plugins/softlayer.py

+22-62
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,39 @@
11
import re
22

3-
import requests
4-
53
from .base import RegexBasedDetector
6-
from detect_secrets.core.constants import VerifiedResult
74

85

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

128
secret_type = 'SoftLayer Credentials'
139

1410
# opt means optional
15-
sl = r'(?:softlayer|sl)(?:_|-|)(?:api|)'
16-
key_or_pass = r'(?:key|pwd|password|pass|token)'
11+
opt_quote = r'(?:"|)'
12+
opt_dashes = r'(?:--|)'
13+
sl = r'(?:softlayer|sl)'
14+
opt_dash_undrscr = r'(?:_|-|)'
15+
opt_api = r'(?:api|)'
16+
key = r'key'
17+
opt_space = r'(?: |)'
18+
opt_equals = r'(?:=|:|:=|)'
1719
secret = r'([a-z0-9]{64})'
1820
denylist = [
19-
RegexBasedDetector.assign_regex_generator(
20-
prefix_regex=sl,
21-
secret_keyword_regex=key_or_pass,
22-
secret_regex=secret,
21+
re.compile(
22+
r'{opt_quote}{opt_dashes}{sl}{opt_dash_undrscr}{opt_api}{opt_dash_undrscr}{key}'
23+
'{opt_quote}{opt_space}{opt_equals}{opt_space}{opt_quote}{secret}{opt_quote}'.format(
24+
opt_quote=opt_quote,
25+
opt_dashes=opt_dashes,
26+
sl=sl,
27+
opt_dash_undrscr=opt_dash_undrscr,
28+
opt_api=opt_api,
29+
key=key,
30+
opt_space=opt_space,
31+
opt_equals=opt_equals,
32+
secret=secret,
33+
), flags=re.IGNORECASE,
2334
),
24-
2535
re.compile(
2636
r'(?:http|https)://api.softlayer.com/soap/(?:v3|v3.1)/([a-z0-9]{64})',
2737
flags=re.IGNORECASE,
2838
),
2939
]
30-
31-
def verify(self, token, content):
32-
usernames = find_username(content)
33-
if not usernames:
34-
return VerifiedResult.UNVERIFIED
35-
36-
for username in usernames:
37-
return verify_softlayer_key(username, token)
38-
39-
return VerifiedResult.VERIFIED_FALSE
40-
41-
42-
def find_username(content):
43-
# 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-
)
50-
username = r'(\w(?:\w|_|@|\.|-)+)'
51-
regex = re.compile(
52-
RegexBasedDetector.assign_regex_generator(
53-
prefix_regex=SoftlayerDetector.sl,
54-
secret_keyword_regex=username_keyword,
55-
secret_regex=username,
56-
),
57-
)
58-
59-
return [
60-
match
61-
for line in content.splitlines()
62-
for match in regex.findall(line)
63-
]
64-
65-
66-
def verify_softlayer_key(username, token):
67-
headers = {'Content-type': 'application/json'}
68-
try:
69-
response = requests.get(
70-
'https://api.softlayer.com/rest/v3/SoftLayer_Account.json',
71-
auth=(username, token), headers=headers,
72-
)
73-
except requests.exceptions.RequestException:
74-
return VerifiedResult.UNVERIFIED
75-
76-
if response.status_code == 200:
77-
return VerifiedResult.VERIFIED_TRUE
78-
else:
79-
return VerifiedResult.VERIFIED_FALSE

‎tests/plugins/softlayer_test.py

+55-145
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,65 @@
1-
import textwrap
1+
from __future__ import absolute_import
22

33
import pytest
4-
import responses
54

6-
from detect_secrets.core.constants import VerifiedResult
7-
from detect_secrets.plugins.softlayer import find_username
8-
from detect_secrets.plugins.softlayer import SoftlayerDetector
5+
from detect_secrets.plugins.softlayer import SoftLayerDetector
96

10-
SL_USERNAME = 'test@testy.test'
11-
SL_TOKEN = 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'
127

8+
class TestSoftLayerDetector(object):
139

14-
class TestSoftlayerDetector:
10+
sl_token = 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'
1511

1612
@pytest.mark.parametrize(
1713
'payload, should_flag',
1814
[
19-
('--softlayer-api-key "{sl_token}"'.format(sl_token=SL_TOKEN), True),
20-
('--softlayer-api-key="{sl_token}"'.format(sl_token=SL_TOKEN), True),
21-
('--softlayer-api-key {sl_token}'.format(sl_token=SL_TOKEN), True),
22-
('--softlayer-api-key={sl_token}'.format(sl_token=SL_TOKEN), True),
23-
('http://api.softlayer.com/soap/v3/{sl_token}'.format(sl_token=SL_TOKEN), True),
24-
('http://api.softlayer.com/soap/v3.1/{sl_token}'.format(sl_token=SL_TOKEN), True),
25-
('softlayer_api_key: {sl_token}'.format(sl_token=SL_TOKEN), True),
26-
('softlayer-key : {sl_token}'.format(sl_token=SL_TOKEN), True),
27-
('SOFTLAYER-API-KEY : "{sl_token}"'.format(sl_token=SL_TOKEN), True),
28-
('"softlayer_api_key" : "{sl_token}"'.format(sl_token=SL_TOKEN), True),
29-
('softlayer-api-key: "{sl_token}"'.format(sl_token=SL_TOKEN), True),
30-
('"softlayer_api_key": "{sl_token}"'.format(sl_token=SL_TOKEN), True),
31-
('SOFTLAYER_API_KEY:"{sl_token}"'.format(sl_token=SL_TOKEN), True),
32-
('softlayer-key:{sl_token}'.format(sl_token=SL_TOKEN), True),
33-
('softlayer_key:"{sl_token}"'.format(sl_token=SL_TOKEN), True),
34-
('"softlayer_api_key":"{sl_token}"'.format(sl_token=SL_TOKEN), True),
35-
('softlayerapikey= {sl_token}'.format(sl_token=SL_TOKEN), True),
36-
('softlayer_api_key= "{sl_token}"'.format(sl_token=SL_TOKEN), True),
37-
('SOFTLAYERAPIKEY={sl_token}'.format(sl_token=SL_TOKEN), True),
38-
('softlayer_api_key="{sl_token}"'.format(sl_token=SL_TOKEN), True),
39-
('sl_api_key: {sl_token}'.format(sl_token=SL_TOKEN), True),
40-
('SLAPIKEY : {sl_token}'.format(sl_token=SL_TOKEN), True),
41-
('sl_apikey : "{sl_token}"'.format(sl_token=SL_TOKEN), True),
42-
('"sl_api_key" : "{sl_token}"'.format(sl_token=SL_TOKEN), True),
43-
('sl-key: "{sl_token}"'.format(sl_token=SL_TOKEN), True),
44-
('"sl_api_key": "{sl_token}"'.format(sl_token=SL_TOKEN), True),
45-
('sl_api_key:"{sl_token}"'.format(sl_token=SL_TOKEN), True),
46-
('sl_api_key:{sl_token}'.format(sl_token=SL_TOKEN), True),
47-
('sl-api-key:"{sl_token}"'.format(sl_token=SL_TOKEN), True),
48-
('"sl_api_key":"{sl_token}"'.format(sl_token=SL_TOKEN), True),
49-
('sl_key= {sl_token}'.format(sl_token=SL_TOKEN), True),
50-
('sl_api_key= "{sl_token}"'.format(sl_token=SL_TOKEN), True),
51-
('sl-api-key={sl_token}'.format(sl_token=SL_TOKEN), True),
52-
('slapi_key="{sl_token}"'.format(sl_token=SL_TOKEN), True),
53-
('slapikey:= {sl_token}'.format(sl_token=SL_TOKEN), True),
54-
('softlayer_api_key := {sl_token}'.format(sl_token=SL_TOKEN), True),
55-
('sl_api_key := "{sl_token}"'.format(sl_token=SL_TOKEN), True),
56-
('"softlayer_key" := "{sl_token}"'.format(sl_token=SL_TOKEN), True),
57-
('sl_api_key: "{sl_token}"'.format(sl_token=SL_TOKEN), True),
58-
('"softlayer_api_key":= "{sl_token}"'.format(sl_token=SL_TOKEN), True),
59-
('sl-api-key:="{sl_token}"'.format(sl_token=SL_TOKEN), True),
60-
('softlayer_api_key:={sl_token}'.format(sl_token=SL_TOKEN), True),
61-
('slapikey:"{sl_token}"'.format(sl_token=SL_TOKEN), True),
62-
('"softlayer_api_key":="{sl_token}"'.format(sl_token=SL_TOKEN), True),
63-
('sl-api-key:= {sl_token}'.format(sl_token=SL_TOKEN), True),
64-
('softlayer_key:= "{sl_token}"'.format(sl_token=SL_TOKEN), True),
65-
('sl_api_key={sl_token}'.format(sl_token=SL_TOKEN), True),
66-
('softlayer_api_key:="{sl_token}"'.format(sl_token=SL_TOKEN), True),
67-
('softlayer_password = "{sl_token}"'.format(sl_token=SL_TOKEN), True),
68-
('sl_pass="{sl_token}"'.format(sl_token=SL_TOKEN), True),
69-
('softlayer-pwd = {sl_token}'.format(sl_token=SL_TOKEN), True),
15+
('--softlayer-api-key "{sl_token}"'.format(sl_token=sl_token), True,),
16+
('--softlayer-api-key="{sl_token}"'.format(sl_token=sl_token), True,),
17+
('--softlayer-api-key {sl_token}'.format(sl_token=sl_token), True,),
18+
('--softlayer-api-key={sl_token}'.format(sl_token=sl_token), True,),
19+
('http://api.softlayer.com/soap/v3/{sl_token}'.format(sl_token=sl_token), True,),
20+
('http://api.softlayer.com/soap/v3.1/{sl_token}'.format(sl_token=sl_token), True,),
21+
('softlayer_api_key: {sl_token}'.format(sl_token=sl_token), True,),
22+
('softlayer-key : {sl_token}'.format(sl_token=sl_token), True,),
23+
('SOFTLAYER-API-KEY : "{sl_token}"'.format(sl_token=sl_token), True,),
24+
('"softlayer_api_key" : "{sl_token}"'.format(sl_token=sl_token), True,),
25+
('softlayer-api-key: "{sl_token}"'.format(sl_token=sl_token), True,),
26+
('"softlayer_api_key": "{sl_token}"'.format(sl_token=sl_token), True,),
27+
('SOFTLAYER_API_KEY:"{sl_token}"'.format(sl_token=sl_token), True,),
28+
('softlayer-key:{sl_token}'.format(sl_token=sl_token), True,),
29+
('softlayer_key:"{sl_token}"'.format(sl_token=sl_token), True,),
30+
('"softlayer_api_key":"{sl_token}"'.format(sl_token=sl_token), True,),
31+
('softlayerapikey= {sl_token}'.format(sl_token=sl_token), True,),
32+
('softlayer_api_key= "{sl_token}"'.format(sl_token=sl_token), True,),
33+
('SOFTLAYERAPIKEY={sl_token}'.format(sl_token=sl_token), True,),
34+
('softlayer_api_key="{sl_token}"'.format(sl_token=sl_token), True,),
35+
('sl_api_key: {sl_token}'.format(sl_token=sl_token), True,),
36+
('SLAPIKEY : {sl_token}'.format(sl_token=sl_token), True,),
37+
('sl_apikey : "{sl_token}"'.format(sl_token=sl_token), True,),
38+
('"sl_api_key" : "{sl_token}"'.format(sl_token=sl_token), True,),
39+
('sl-key: "{sl_token}"'.format(sl_token=sl_token), True,),
40+
('"sl_api_key": "{sl_token}"'.format(sl_token=sl_token), True,),
41+
('sl_api_key:"{sl_token}"'.format(sl_token=sl_token), True,),
42+
('sl_api_key:{sl_token}'.format(sl_token=sl_token), True,),
43+
('sl-api-key:"{sl_token}"'.format(sl_token=sl_token), True,),
44+
('"sl_api_key":"{sl_token}"'.format(sl_token=sl_token), True,),
45+
('sl_key= {sl_token}'.format(sl_token=sl_token), True,),
46+
('sl_api_key= "{sl_token}"'.format(sl_token=sl_token), True,),
47+
('sl-api-key={sl_token}'.format(sl_token=sl_token), True,),
48+
('slapi_key="{sl_token}"'.format(sl_token=sl_token), True,),
49+
('slapikey:= {sl_token}'.format(sl_token=sl_token), True,),
50+
('softlayer_api_key := {sl_token}'.format(sl_token=sl_token), True,),
51+
('sl_api_key := "{sl_token}"'.format(sl_token=sl_token), True,),
52+
('"softlayer_key" := "{sl_token}"'.format(sl_token=sl_token), True,),
53+
('sl_api_key: "{sl_token}"'.format(sl_token=sl_token), True,),
54+
('"softlayer_api_key":= "{sl_token}"'.format(sl_token=sl_token), True,),
55+
('sl-api-key:="{sl_token}"'.format(sl_token=sl_token), True,),
56+
('softlayer_api_key:={sl_token}'.format(sl_token=sl_token), True,),
57+
('slapikey:"{sl_token}"'.format(sl_token=sl_token), True,),
58+
('"softlayer_api_key":="{sl_token}"'.format(sl_token=sl_token), True,),
59+
('sl-api-key:= {sl_token}'.format(sl_token=sl_token), True,),
60+
('softlayer_key:= "{sl_token}"'.format(sl_token=sl_token), True,),
61+
('sl_api_key={sl_token}'.format(sl_token=sl_token), True),
62+
('softlayer_api_key:="{sl_token}"'.format(sl_token=sl_token), True),
7063
('softlayer_api_key="%s" % SL_API_KEY_ENV', False),
7164
('sl_api_key: "%s" % <softlayer_api_key>', False),
7265
('SOFTLAYER_APIKEY: "insert_key_here"', False),
@@ -75,91 +68,8 @@ class TestSoftlayerDetector:
7568
('fake-softlayer-key= "not_long_enough"', False),
7669
],
7770
)
78-
def test_analyze_line(self, payload, should_flag):
79-
logic = SoftlayerDetector()
71+
def test_analyze_string(self, payload, should_flag):
72+
logic = SoftLayerDetector()
8073

81-
output = logic.analyze_line(payload, 1, 'mock_filename')
74+
output = logic.analyze_string(payload, 1, 'mock_filename')
8275
assert len(output) == (1 if should_flag else 0)
83-
84-
@responses.activate
85-
def test_verify_invalid_secret(self):
86-
responses.add(
87-
responses.GET, 'https://api.softlayer.com/rest/v3/SoftLayer_Account.json',
88-
json={'error': 'Access denied. '}, status=401,
89-
)
90-
91-
assert SoftlayerDetector().verify(
92-
SL_TOKEN,
93-
'softlayer_username={}'.format(SL_USERNAME),
94-
) == VerifiedResult.VERIFIED_FALSE
95-
96-
@responses.activate
97-
def test_verify_valid_secret(self):
98-
responses.add(
99-
responses.GET, 'https://api.softlayer.com/rest/v3/SoftLayer_Account.json',
100-
json={'id': 1}, status=200,
101-
)
102-
assert SoftlayerDetector().verify(
103-
SL_TOKEN,
104-
'softlayer_username={}'.format(SL_USERNAME),
105-
) == VerifiedResult.VERIFIED_TRUE
106-
107-
@responses.activate
108-
def test_verify_unverified_secret(self):
109-
assert SoftlayerDetector().verify(
110-
SL_TOKEN,
111-
'softlayer_username={}'.format(SL_USERNAME),
112-
) == VerifiedResult.UNVERIFIED
113-
114-
def test_verify_no_secret(self):
115-
assert SoftlayerDetector().verify(
116-
SL_TOKEN,
117-
'no_un={}'.format(SL_USERNAME),
118-
) == VerifiedResult.UNVERIFIED
119-
120-
@pytest.mark.parametrize(
121-
'content, expected_output',
122-
(
123-
(
124-
textwrap.dedent("""
125-
--softlayer-username = {}
126-
""")[1:-1].format(
127-
SL_USERNAME,
128-
),
129-
[SL_USERNAME],
130-
),
131-
132-
# With quotes
133-
(
134-
textwrap.dedent("""
135-
sl_user_id = "{}"
136-
""")[1:-1].format(
137-
SL_USERNAME,
138-
),
139-
[SL_USERNAME],
140-
),
141-
142-
# multiple candidates
143-
(
144-
textwrap.dedent("""
145-
softlayer_id = '{}'
146-
sl-user = '{}'
147-
SOFTLAYER_USERID = '{}'
148-
softlayer-uname: {}
149-
""")[1:-1].format(
150-
SL_USERNAME,
151-
'test2@testy.test',
152-
'test3@testy.testy',
153-
'notanemail',
154-
),
155-
[
156-
SL_USERNAME,
157-
'test2@testy.test',
158-
'test3@testy.testy',
159-
'notanemail',
160-
],
161-
),
162-
),
163-
)
164-
def test_find_username(self, content, expected_output):
165-
assert find_username(content) == expected_output

0 commit comments

Comments
 (0)
Please sign in to comment.