-
Notifications
You must be signed in to change notification settings - Fork 496
Add Detector for Softlayer tokens #254
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
Changes from all commits
d584a71
efff32e
f949ae2
da3d9ca
111736c
25889a1
2d841c7
11b4169
8b8b9d7
5f29e67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from __future__ import absolute_import | ||
|
||
import re | ||
|
||
import requests | ||
|
||
from .base import RegexBasedDetector | ||
from detect_secrets.core.constants import VerifiedResult | ||
|
||
|
||
class SoftlayerDetector(RegexBasedDetector): | ||
"""Scans for Softlayer credentials.""" | ||
|
||
secret_type = 'SoftLayer Credentials' | ||
|
||
# opt means optional | ||
sl = r'(?:softlayer|sl)(?:_|-|)(?:api|)' | ||
key_or_pass = r'(?:key|pwd|password|pass|token)' | ||
secret = r'([a-z0-9]{64})' | ||
denylist = [ | ||
RegexBasedDetector.assign_regex_generator( | ||
prefix_regex=sl, | ||
secret_keyword_regex=key_or_pass, | ||
secret_regex=secret, | ||
), | ||
|
||
re.compile( | ||
r'(?:http|https)://api.softlayer.com/soap/(?:v3|v3.1)/([a-z0-9]{64})', | ||
flags=re.IGNORECASE, | ||
), | ||
] | ||
|
||
def verify(self, token, content): | ||
usernames = find_username(content) | ||
if not usernames: | ||
return VerifiedResult.UNVERIFIED | ||
|
||
for username in usernames: | ||
return verify_softlayer_key(username, token) | ||
|
||
return VerifiedResult.VERIFIED_FALSE | ||
|
||
|
||
def find_username(content): | ||
# opt means optional | ||
username_keyword = ( | ||
r'(?:' | ||
r'username|id|user|userid|user-id|user-name|' | ||
r'name|user_id|user_name|uname' | ||
r')' | ||
) | ||
username = r'(\w(?:\w|_|@|\.|-)+)' | ||
regex = re.compile( | ||
RegexBasedDetector.assign_regex_generator( | ||
prefix_regex=SoftlayerDetector.sl, | ||
secret_keyword_regex=username_keyword, | ||
secret_regex=username, | ||
), | ||
) | ||
|
||
return [ | ||
match | ||
for line in content.splitlines() | ||
for match in regex.findall(line) | ||
] | ||
|
||
|
||
def verify_softlayer_key(username, token): | ||
headers = {'Content-type': 'application/json'} | ||
try: | ||
response = requests.get( | ||
'https://api.softlayer.com/rest/v3/SoftLayer_Account.json', | ||
auth=(username, token), headers=headers, | ||
) | ||
except requests.exceptions.RequestException: | ||
return VerifiedResult.UNVERIFIED | ||
|
||
if response.status_code == 200: | ||
return VerifiedResult.VERIFIED_TRUE | ||
else: | ||
return VerifiedResult.VERIFIED_FALSE |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ pre-commit==1.11.2 | |
pyahocorasick | ||
pytest | ||
pyyaml | ||
tox>=3.8 | ||
responses | ||
tox-pip-extensions | ||
tox>=3.8 | ||
unidiff |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
from __future__ import absolute_import | ||
|
||
import textwrap | ||
|
||
import pytest | ||
import responses | ||
|
||
from detect_secrets.core.constants import VerifiedResult | ||
from detect_secrets.plugins.softlayer import find_username | ||
from detect_secrets.plugins.softlayer import SoftlayerDetector | ||
|
||
SL_USERNAME = '[email protected]' | ||
SL_TOKEN = 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234' | ||
|
||
|
||
class TestSoftlayerDetector(object): | ||
|
||
@pytest.mark.parametrize( | ||
'payload, should_flag', | ||
[ | ||
('--softlayer-api-key "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('--softlayer-api-key="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('--softlayer-api-key {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('--softlayer-api-key={sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('http://api.softlayer.com/soap/v3/{sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('http://api.softlayer.com/soap/v3.1/{sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key: {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer-key : {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('SOFTLAYER-API-KEY : "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"softlayer_api_key" : "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('softlayer-api-key: "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"softlayer_api_key": "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('SOFTLAYER_API_KEY:"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('softlayer-key:{sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_key:"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"softlayer_api_key":"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('softlayerapikey= {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key= "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('SOFTLAYERAPIKEY={sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key: {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('SLAPIKEY : {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('sl_apikey : "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"sl_api_key" : "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl-key: "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"sl_api_key": "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key:"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key:{sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('sl-api-key:"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"sl_api_key":"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_key= {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key= "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl-api-key={sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('slapi_key="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('slapikey:= {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key := {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key := "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"softlayer_key" := "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key: "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"softlayer_api_key":= "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl-api-key:="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key:={sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('slapikey:"{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('"softlayer_api_key":="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl-api-key:= {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_key:= "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_api_key={sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key:="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_password = "{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('sl_pass="{sl_token}"'.format(sl_token=SL_TOKEN), True), | ||
('softlayer-pwd = {sl_token}'.format(sl_token=SL_TOKEN), True), | ||
('softlayer_api_key="%s" % SL_API_KEY_ENV', False), | ||
('sl_api_key: "%s" % <softlayer_api_key>', False), | ||
('SOFTLAYER_APIKEY: "insert_key_here"', False), | ||
('sl-apikey: "insert_key_here"', False), | ||
('softlayer-key:=afakekey', False), | ||
('fake-softlayer-key= "not_long_enough"', False), | ||
], | ||
) | ||
def test_analyze_string(self, payload, should_flag): | ||
logic = SoftlayerDetector() | ||
|
||
output = logic.analyze_string(payload, 1, 'mock_filename') | ||
assert len(output) == (1 if should_flag else 0) | ||
|
||
@responses.activate | ||
def test_verify_invalid_secret(self): | ||
responses.add( | ||
responses.GET, 'https://api.softlayer.com/rest/v3/SoftLayer_Account.json', | ||
json={'error': 'Access denied. '}, status=401, | ||
) | ||
|
||
assert SoftlayerDetector().verify( | ||
SL_TOKEN, | ||
'softlayer_username={}'.format(SL_USERNAME), | ||
) == VerifiedResult.VERIFIED_FALSE | ||
|
||
@responses.activate | ||
def test_verify_valid_secret(self): | ||
responses.add( | ||
responses.GET, 'https://api.softlayer.com/rest/v3/SoftLayer_Account.json', | ||
json={'id': 1}, status=200, | ||
) | ||
assert SoftlayerDetector().verify( | ||
SL_TOKEN, | ||
'softlayer_username={}'.format(SL_USERNAME), | ||
) == VerifiedResult.VERIFIED_TRUE | ||
|
||
@responses.activate | ||
def test_verify_unverified_secret(self): | ||
assert SoftlayerDetector().verify( | ||
SL_TOKEN, | ||
'softlayer_username={}'.format(SL_USERNAME), | ||
) == VerifiedResult.UNVERIFIED | ||
|
||
def test_verify_no_secret(self): | ||
assert SoftlayerDetector().verify( | ||
SL_TOKEN, | ||
'no_un={}'.format(SL_USERNAME), | ||
) == VerifiedResult.UNVERIFIED | ||
|
||
@pytest.mark.parametrize( | ||
'content, expected_output', | ||
( | ||
( | ||
textwrap.dedent(""" | ||
--softlayer-username = {} | ||
""")[1:-1].format( | ||
SL_USERNAME, | ||
), | ||
[SL_USERNAME], | ||
), | ||
|
||
# With quotes | ||
( | ||
textwrap.dedent(""" | ||
sl_user_id = "{}" | ||
""")[1:-1].format( | ||
SL_USERNAME, | ||
), | ||
[SL_USERNAME], | ||
), | ||
|
||
# multiple candidates | ||
( | ||
textwrap.dedent(""" | ||
softlayer_id = '{}' | ||
sl-user = '{}' | ||
SOFTLAYER_USERID = '{}' | ||
softlayer-uname: {} | ||
""")[1:-1].format( | ||
SL_USERNAME, | ||
'[email protected]', | ||
'[email protected]', | ||
'notanemail', | ||
), | ||
[ | ||
SL_USERNAME, | ||
'[email protected]', | ||
'[email protected]', | ||
'notanemail', | ||
], | ||
), | ||
), | ||
) | ||
def test_find_username(self, content, expected_output): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just in case you find that this type of secret is used in function calls, the following commit may help you add detection for that. (Code still in review -- c6037d9) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the suggestions. @KevinHock. Unlike AWS tokens which has a specific format, the Softlayer username here is almost free format. It might be hard to match them in function calls. I will prefer to leave them out for now. |
||
assert find_username(content) == expected_output |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
++, great use of non-capturing groups! (https://stackoverflow.com/questions/3512471/what-is-a-non-capturing-group-in-regular-expressions)
I should refactor some of our existing code to use those