diff --git a/detect_secrets/plugins/cloudant.py b/detect_secrets/plugins/cloudant.py index 14192deec..77e5ffc61 100644 --- a/detect_secrets/plugins/cloudant.py +++ b/detect_secrets/plugins/cloudant.py @@ -7,51 +7,45 @@ class CloudantDetector(RegexBasedDetector): - """Scans for Cloudant credentials.""" secret_type = 'Cloudant Credentials' # opt means optional + opt_quote = r'(?:"|\'|)' + opt_dashes = r'(?:--|)' + opt_dot = r'(?:\.|)' dot = r'\.' - cl_account = r'[\w\-]+' - cl = r'(?:cloudant|cl|clou)' + cl_account = r'[0-9a-z\-\_]*' + cl = r'(cloudant|cl|clou)' + opt_dash_undrscr = r'(?:_|-|)' opt_api = r'(?:api|)' - cl_key_or_pass = opt_api + r'(?:key|pwd|pw|password|pass|token)' - cl_pw = r'([0-9a-f]{64})' - cl_api_key = r'([a-z]{24})' + cl_key_or_pass = cl + opt_dash_undrscr + r'(?:key|pwd|pw|password|pass|token)' + opt_space = r'(?: |)' + assignment = r'(?:=|:|:=|=>)' + cl_secret = r'[0-9a-f]{64}' colon = r'\:' at = r'\@' - http = r'(?:https?\:\/\/)' + http = r'(?:http\:\/\/|https\:\/\/)' cloudant_api_url = r'cloudant\.com' denylist = [ - RegexBasedDetector.assign_regex_generator( - prefix_regex=cl, - secret_keyword_regex=cl_key_or_pass, - secret_regex=cl_pw, - ), - RegexBasedDetector.assign_regex_generator( - prefix_regex=cl, - secret_keyword_regex=cl_key_or_pass, - secret_regex=cl_api_key, - ), re.compile( - r'{http}{cl_account}{colon}{cl_pw}{at}{cl_account}{dot}{cloudant_api_url}'.format( - http=http, - colon=colon, + r'{cl_key_or_pass}{opt_space}{assignment}{opt_space}{opt_quote}{cl_secret}'.format( + cl_key_or_pass=cl_key_or_pass, + opt_quote=opt_quote, cl_account=cl_account, - cl_pw=cl_pw, - at=at, - dot=dot, - cloudant_api_url=cloudant_api_url, - ), - flags=re.IGNORECASE, + opt_dash_undrscr=opt_dash_undrscr, + opt_api=opt_api, + opt_space=opt_space, + assignment=assignment, + cl_secret=cl_secret, + ), flags=re.IGNORECASE, ), re.compile( - r'{http}{cl_account}{colon}{cl_api_key}{at}{cl_account}{dot}{cloudant_api_url}'.format( + r'{http}{cl_account}{colon}{cl_secret}{at}{cl_account}{dot}{cloudant_api_url}'.format( http=http, colon=colon, cl_account=cl_account, - cl_api_key=cl_api_key, + cl_secret=cl_secret, at=at, dot=dot, cloudant_api_url=cloudant_api_url, @@ -60,70 +54,70 @@ class CloudantDetector(RegexBasedDetector): ), ] - def verify(self, token, content): + def verify(self, token, content, potential_secret=None): - hosts = find_account(content) + hosts = get_host(content) if not hosts: return VerifiedResult.UNVERIFIED for host in hosts: - return verify_cloudant_key(host, token) + return verify_cloudant_key(host, token, potential_secret) return VerifiedResult.VERIFIED_FALSE -def find_account(content): +def get_host(content): + + # opt means optional + opt_quote = r'(?:"|\'|)' + opt_cl = r'(?:cloudant|cl|)' + opt_dash_undrscr = r'(?:_|-|)' opt_hostname_keyword = r'(?:hostname|host|username|id|user|userid|user-id|user-name|' \ - 'name|user_id|user_name|uname|account)' - account = r'(\w[\w\-]*)' - opt_basic_auth = r'(?:[\w\-:%]*\@)?' - - regexes = ( - RegexBasedDetector.assign_regex_generator( - prefix_regex=CloudantDetector.cl, - secret_keyword_regex=opt_hostname_keyword, - secret_regex=account, - ), - re.compile( - r'{http}{opt_basic_auth}{cl_account}{dot}{cloudant_api_url}'.format( - http=CloudantDetector.http, - opt_basic_auth=opt_basic_auth, - cl_account=account, - cl_api_key=CloudantDetector.cl_api_key, - dot=CloudantDetector.dot, - cloudant_api_url=CloudantDetector.cloudant_api_url, - ), - flags=re.IGNORECASE, - ), + 'name|user_id|user_name|uname)' + opt_space = r'(?: |)' + assignment = r'(?:\=|:|:=|=>)+' + hostname = r'(\w(?:\w|_|-)+)' + regex = re.compile( + r'{opt_quote}{opt_cl}{opt_dash_undrscr}{opt_hostname_keyword}{opt_space}{opt_quote}' + '{assignment}{opt_space}{opt_quote}{hostname}{opt_quote}'.format( + opt_quote=opt_quote, + opt_cl=opt_cl, + opt_dash_undrscr=opt_dash_undrscr, + opt_hostname_keyword=opt_hostname_keyword, + opt_space=opt_space, + hostname=hostname, + assignment=assignment, + ), flags=re.IGNORECASE, ) return [ match for line in content.splitlines() - for regex in regexes for match in regex.findall(line) ] -def verify_cloudant_key(hostname, token): - headers = {'Content-type': 'application/json'} - request_url = 'https://{hostname}:' \ - '{token}' \ - '@{hostname}.' \ - 'cloudant.com'.format( - hostname=hostname, - token=token, - ) - +def verify_cloudant_key(hostname, token, potential_secret=None): try: + headers = {'Content-type': 'application/json'} + request_url = 'https://{hostname}:' \ + '{token}' \ + '@{hostname}.' \ + 'cloudant.com/_api/v2'.format( + hostname=hostname, + token=token, + ) + response = requests.get( request_url, headers=headers, ) - except requests.exceptions.RequestException: - return VerifiedResult.UNVERIFIED - if response.status_code == 200: - return VerifiedResult.VERIFIED_TRUE - else: - return VerifiedResult.VERIFIED_FALSE + if response.status_code == 200: + if potential_secret: + potential_secret.other_factors['hostname'] = hostname + return VerifiedResult.VERIFIED_TRUE + else: + return VerifiedResult.VERIFIED_FALSE + except Exception: + return VerifiedResult.UNVERIFIED diff --git a/tests/plugins/cloudant_test.py b/tests/plugins/cloudant_test.py index 68da513af..f741a9797 100644 --- a/tests/plugins/cloudant_test.py +++ b/tests/plugins/cloudant_test.py @@ -4,15 +4,13 @@ import responses from detect_secrets.core.constants import VerifiedResult +from detect_secrets.core.potential_secret import PotentialSecret from detect_secrets.plugins.cloudant import CloudantDetector -from detect_secrets.plugins.cloudant import find_account +from detect_secrets.plugins.cloudant import get_host -CL_ACCOUNT = 'testy_-test' # also called user -# only detecting 64 hex CL generated password -CL_PW = 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234' - -# detecting 24 alpha for CL generated API KEYS -CL_API_KEY = 'abcdefghijabcdefghijabcd' +CL_HOST = 'testy_test' # also called user +# only detecting 64 hex +CL_TOKEN = 'abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234' class TestCloudantDetector: @@ -21,149 +19,122 @@ class TestCloudantDetector: 'payload, should_flag', [ ( - 'https://{cl_account}:{cl_pw}@{cl_account}.cloudant.com"'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, - ), True, - ), - ( - 'https://{cl_account}:{cl_pw}@{cl_account}.cloudant.com/_api/v2/'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, - ), True, - ), - ( - 'https://{cl_account}:{cl_pw}@{cl_account}.cloudant.com/_api/v2/'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, + 'https://{cl_host}:{cl_token}@{cl_host}.cloudant.com"'.format( + cl_host=CL_HOST, cl_token=CL_TOKEN, ), True, ), ( - 'https://{cl_account}:{cl_pw}@{cl_account}.cloudant.com'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, + 'https://{cl_host}:{cl_token}@{cl_host}.cloudant.com/_api/v2/'.format( + cl_host=CL_HOST, cl_token=CL_TOKEN, ), True, ), ( - 'https://{cl_account}:{cl_api_key}@{cl_account}.cloudant.com'.format( - cl_account=CL_ACCOUNT, cl_api_key=CL_API_KEY, - ), True, - ), - ( - 'https://{cl_account}:{cl_pw}.cloudant.com'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, + 'https://{cl_host}:{cl_token}.cloudant.com'.format( + cl_host=CL_HOST, cl_token=CL_TOKEN, ), False, ), - ('cloudant_password=\'{cl_pw}\''.format(cl_pw=CL_PW), True), - ('cloudant_pw=\'{cl_pw}\''.format(cl_pw=CL_PW), True), - ('cloudant_pw="{cl_pw}"'.format(cl_pw=CL_PW), True), - ('clou_pw = "{cl_pw}"'.format(cl_pw=CL_PW), True), - ('cloudant_key = "{cl_api_key}"'.format(cl_api_key=CL_API_KEY), True), + ('cloudant_password=\'{cl_token}\''.format(cl_token=CL_TOKEN), True), + ('cloudant_pw=\'{cl_token}\''.format(cl_token=CL_TOKEN), True), + ('cloudant_pw="{cl_token}"'.format(cl_token=CL_TOKEN), True), + ('clou_pw = "{cl_token}"'.format(cl_token=CL_TOKEN), True), ('cloudant_password = "a-fake-tooshort-key"', False), - ('cl_api_key = "a-fake-api-key"', False), ], ) def test_analyze_string(self, payload, should_flag): logic = CloudantDetector() - output = logic.analyze_line(payload, 1, 'mock_filename') + 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): - cl_api_url = 'https://{cl_account}:{cl_pw}@{cl_account}.cloudant.com'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, + cl_api_url = 'https://{cl_host}:{cl_token}@{cl_host}.cloudant.com/_api/v2'.format( + cl_host=CL_HOST, cl_token=CL_TOKEN, ) responses.add( responses.GET, cl_api_url, - json={'error': 'unauthorized'}, status=401, + json={'error': 'Access denied. '}, status=401, ) assert CloudantDetector().verify( - CL_PW, - 'cloudant_host={}'.format(CL_ACCOUNT), + CL_TOKEN, + 'cloudant_host={}'.format(CL_HOST), ) == VerifiedResult.VERIFIED_FALSE @responses.activate def test_verify_valid_secret(self): - cl_api_url = 'https://{cl_account}:{cl_pw}@{cl_account}.cloudant.com'.format( - cl_account=CL_ACCOUNT, cl_pw=CL_PW, + cl_api_url = 'https://{cl_host}:{cl_token}@{cl_host}.cloudant.com/_api/v2'.format( + cl_host=CL_HOST, cl_token=CL_TOKEN, ) responses.add( responses.GET, cl_api_url, json={'id': 1}, status=200, ) + potential_secret = PotentialSecret('test cloudant', 'test filename', CL_TOKEN) assert CloudantDetector().verify( - CL_PW, - 'cloudant_host={}'.format(CL_ACCOUNT), + CL_TOKEN, + 'cloudant_host={}'.format(CL_HOST), + potential_secret, ) == VerifiedResult.VERIFIED_TRUE + assert potential_secret.other_factors['hostname'] == CL_HOST @responses.activate def test_verify_unverified_secret(self): assert CloudantDetector().verify( - CL_PW, - 'cloudant_host={}'.format(CL_ACCOUNT), + CL_TOKEN, + 'cloudant_host={}'.format(CL_HOST), ) == VerifiedResult.UNVERIFIED def test_verify_no_secret(self): assert CloudantDetector().verify( - CL_PW, - 'no_un={}'.format(CL_ACCOUNT), + CL_TOKEN, + 'no_un={}'.format(CL_HOST), ) == VerifiedResult.UNVERIFIED - @pytest.mark.parametrize( - 'content, expected_output', - ( - ( - textwrap.dedent(""" - --cloudant-hostname = {} - """)[1:-1].format( - CL_ACCOUNT, - ), - [CL_ACCOUNT], - ), - # With quotes - ( - textwrap.dedent(""" - cl_account = "{}" - """)[1:-1].format( - CL_ACCOUNT, - ), - [CL_ACCOUNT], +@pytest.mark.parametrize( + 'content, expected_output', + ( + ( + textwrap.dedent(""" + --cloudant-hostname = {} + """)[1:-1].format( + CL_HOST, ), + [CL_HOST], + ), - # multiple candidates - ( - textwrap.dedent(""" - cloudant_id = '{}' - cl-user = '{}' - CLOUDANT_USERID = '{}' - cloudant-uname: {} - """)[1:-1].format( - CL_ACCOUNT, - 'test2_testy_test', - 'test3-testy-testy', - 'notanemail', - ), - [ - CL_ACCOUNT, - 'test2_testy_test', - 'test3-testy-testy', - 'notanemail', - ], + # With quotes + ( + textwrap.dedent(""" + cl_host = "{}" + """)[1:-1].format( + CL_HOST, ), + [CL_HOST], + ), - # In URL - ( - 'https://{cl_account}:{cl_api_key}@{cl_account}.cloudant.com'.format( - cl_account=CL_ACCOUNT, cl_api_key=CL_API_KEY, - ), - [CL_ACCOUNT], - ), - ( - 'https://{cl_account}.cloudant.com'.format( - cl_account=CL_ACCOUNT, - ), - [CL_ACCOUNT], + # multiple candidates + ( + textwrap.dedent(""" + cloudant_id = '{}' + cl-user = '{}' + CLOUDANT_USERID = '{}' + cloudant-uname: {} + """)[1:-1].format( + CL_HOST, + 'test2_testy_test', + 'test3-testy-testy', + 'notanemail', ), + [ + CL_HOST, + 'test2_testy_test', + 'test3-testy-testy', + 'notanemail', + ], ), - ) - def test_find_account(self, content, expected_output): - assert find_account(content) == expected_output + ), +) +def test_get_host(content, expected_output): + assert get_host(content) == expected_output