Skip to content

Commit

Permalink
Merge pull request Yelp#429 from pablosantiagolopez/feature/keyword-c
Browse files Browse the repository at this point in the history
Support for C, C++ and C# secrets in Keyword plugin
  • Loading branch information
domanchi authored Apr 9, 2021
2 parents e2a5e36 + 2880c52 commit 194f4c1
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 25 deletions.
51 changes: 29 additions & 22 deletions detect_secrets/plugins/keyword.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
# line breaks, comma, backticks or quotes. This allows to reduce the false
# positives number and to prevent errors in the code snippet highlighting.
SECRET = r'(?=[^\v\'\"]*)(?=\w+)[^\v\'\"]*[^\v,\'\"`]'
SQUARE_BRACKETS = r'(\[\])'
SQUARE_BRACKETS = r'(\[[0-9]*\])'

FOLLOWED_BY_COLON_EQUAL_SIGNS_REGEX = re.compile(
# e.g. my_password := "bar" or my_password := bar
Expand Down Expand Up @@ -123,14 +123,23 @@
# e.g. my_password = "bar"
# e.g. my_password = @"bar"
# e.g. my_password[] = "bar";
r'{denylist}({square_brackets})?{optional_whitespace}={optional_whitespace}(@)?(")({secret})(\5)'.format( # noqa: E501
# e.g. char my_password[25] = "bar";
r'{denylist}({square_brackets})?{optional_whitespace}[!=]{{1,2}}{optional_whitespace}(@)?(")({secret})(\5)'.format( # noqa: E501
denylist=DENYLIST_REGEX,
square_brackets=SQUARE_BRACKETS,
optional_whitespace=OPTIONAL_WHITESPACE,
secret=SECRET,
),
flags=re.IGNORECASE,
)
FOLLOWED_BY_OPTIONAL_ASSIGN_QUOTES_REQUIRED_REGEX = re.compile(
# e.g. std::string secret("bar");
# e.g. secret.assign("bar",17);
r'{denylist}(.assign)?\((")({secret})(\3)'.format(
denylist=DENYLIST_REGEX,
secret=SECRET,
),
)
FOLLOWED_BY_EQUAL_SIGNS_REGEX = re.compile(
# e.g. my_password = bar
# e.g. my_password == "bar" or my_password != "bar" or my_password === "bar"
Expand Down Expand Up @@ -161,7 +170,6 @@
),
flags=re.IGNORECASE,
)

PRECEDED_BY_EQUAL_COMPARISON_SIGNS_QUOTES_REQUIRED_REGEX = re.compile(
# e.g. "bar" == my_password or "bar" != my_password or "bar" === my_password
# or "bar" !== my_password
Expand All @@ -174,7 +182,6 @@
secret=SECRET,
),
)

FOLLOWED_BY_QUOTES_AND_SEMICOLON_REGEX = re.compile(
# e.g. private_key "something";
r'{denylist}{nonWhitespace}{whitespace}({quote})({secret})(\2);'.format(
Expand All @@ -198,22 +205,31 @@
FOLLOWED_BY_EQUAL_SIGNS_REGEX: 5,
FOLLOWED_BY_QUOTES_AND_SEMICOLON_REGEX: 3,
}
OBJECTIVE_C_DENYLIST_REGEX_TO_GROUP = {
COMMON_C_DENYLIST_REGEX_TO_GROUP = {
FOLLOWED_BY_EQUAL_SIGNS_OPTIONAL_BRACKETS_OPTIONAL_AT_SIGN_QUOTES_REQUIRED_REGEX: 6,
}
C_PLUS_PLUS_REGEX_TO_GROUP = {
FOLLOWED_BY_OPTIONAL_ASSIGN_QUOTES_REQUIRED_REGEX: 4,
FOLLOWED_BY_EQUAL_SIGNS_QUOTES_REQUIRED_REGEX: 5,
}
QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP = {
FOLLOWED_BY_COLON_QUOTES_REQUIRED_REGEX: 5,
PRECEDED_BY_EQUAL_COMPARISON_SIGNS_QUOTES_REQUIRED_REGEX: 2,
FOLLOWED_BY_EQUAL_SIGNS_QUOTES_REQUIRED_REGEX: 5,
FOLLOWED_BY_QUOTES_AND_SEMICOLON_REGEX: 3,
}
QUOTES_REQUIRED_FILETYPES = {
FileType.CLS,
FileType.JAVA,
FileType.JAVASCRIPT,
FileType.PYTHON,
FileType.SWIFT,
FileType.TERRAFORM,
REGEX_BY_FILETYPE = {
FileType.GO: GOLANG_DENYLIST_REGEX_TO_GROUP,
FileType.OBJECTIVE_C: COMMON_C_DENYLIST_REGEX_TO_GROUP,
FileType.C_SHARP: COMMON_C_DENYLIST_REGEX_TO_GROUP,
FileType.C: COMMON_C_DENYLIST_REGEX_TO_GROUP,
FileType.C_PLUS_PLUS: C_PLUS_PLUS_REGEX_TO_GROUP,
FileType.CLS: QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP,
FileType.JAVA: QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP,
FileType.JAVASCRIPT: QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP,
FileType.PYTHON: QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP,
FileType.SWIFT: QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP,
FileType.TERRAFORM: QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP,
}


Expand Down Expand Up @@ -268,16 +284,7 @@ def analyze_line(
**kwargs: Any,
) -> Set[PotentialSecret]:
filetype = determine_file_type(filename)

if filetype in QUOTES_REQUIRED_FILETYPES:
denylist_regex_to_group = QUOTES_REQUIRED_DENYLIST_REGEX_TO_GROUP
elif filetype == FileType.GO:
denylist_regex_to_group = GOLANG_DENYLIST_REGEX_TO_GROUP
elif filetype == FileType.OBJECTIVE_C:
denylist_regex_to_group = OBJECTIVE_C_DENYLIST_REGEX_TO_GROUP
else:
denylist_regex_to_group = DENYLIST_REGEX_TO_GROUP

denylist_regex_to_group = REGEX_BY_FILETYPE.get(filetype, DENYLIST_REGEX_TO_GROUP)
return super().analyze_line(
filename=filename,
line=line,
Expand Down
8 changes: 7 additions & 1 deletion detect_secrets/util/filetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ class FileType(Enum):
SWIFT = 8
TERRAFORM = 9
YAML = 10
OTHER = 11
C_SHARP = 11
C = 12
C_PLUS_PLUS = 13
OTHER = 14


def determine_file_type(filename: str) -> FileType:
Expand All @@ -34,4 +37,7 @@ def determine_file_type(filename: str) -> FileType:
'.tf': FileType.TERRAFORM,
'.yaml': FileType.YAML,
'.yml': FileType.YAML,
'.cs': FileType.C_SHARP,
'.c': FileType.C,
'.cpp': FileType.C_PLUS_PLUS
}.get(file_extension, FileType.OTHER)
23 changes: 21 additions & 2 deletions tests/plugins/keyword_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,14 @@
(LONG_LINE, None), # Long line test
]

OBJECTIVE_C_TEST_CASES = [
COMMON_C_TEST_CASES = [
('apikey = "{}";'.format(COMMON_SECRET), COMMON_SECRET),
('if (secret == "{}")'.format(COMMON_SECRET), COMMON_SECRET), # Comparison
('if (db_pass != "{}")'.format(COMMON_SECRET), COMMON_SECRET), # Comparison
('password = @"{}";'.format(COMMON_SECRET), COMMON_SECRET),
('my_password_secure = @"{}";'.format(COMMON_SECRET), COMMON_SECRET), # Prefix/suffix
('secrete[] = "{}";'.format(COMMON_SECRET), COMMON_SECRET),
('char secrete[25] = "{}";'.format(COMMON_SECRET), COMMON_SECRET),
('secrete = "{}"'.format(LETTER_SECRET), LETTER_SECRET), # All symbols are allowed
('password = "{}"'.format(SYMBOL_SECRET), None), # At least 1 alphanumeric char is required
("api_key = '{}';".format(COMMON_SECRET), None), # Double quotes required
Expand All @@ -106,6 +109,20 @@
(LONG_LINE, None), # Long line test
]

C_PLUS_PLUS_TEST_CASES = [
('apikey = "{}";'.format(COMMON_SECRET), COMMON_SECRET),
('my_password_secure = "{}";'.format(COMMON_SECRET), COMMON_SECRET), # Prefix and suffix
('password = {}'.format(COMMON_SECRET), None), # Secret without quotes
('if (secret == "{}")'.format(COMMON_SECRET), COMMON_SECRET), # Comparison
('if (db_pass != "{}")'.format(COMMON_SECRET), COMMON_SECRET), # Comparison
('std::string secret("{}");'.format(COMMON_SECRET), COMMON_SECRET),
('secrete.assign("{}",17);'.format(COMMON_SECRET), COMMON_SECRET),
('api_key = ""', None), # Nothing in the quotes
('password = "somefakekey"', None), # 'fake' in the secret
('password = ${link}', None), # Has a ${ followed by a }
('some_key = "real_secret"', None), # We cannot make 'key' a Keyword, too noisy)
]

QUOTES_REQUIRED_TEST_CASES = [
('apikey: "{}"'.format(COMMON_SECRET), COMMON_SECRET),
('apikey_myservice: "{}"'.format(COMMON_SECRET), COMMON_SECRET), # Suffix
Expand Down Expand Up @@ -148,7 +165,9 @@ def parse_test_cases(test_cases):
parse_test_cases([
(None, GENERIC_TEST_CASES),
('go', GOLANG_TEST_CASES),
('m', OBJECTIVE_C_TEST_CASES),
('m', COMMON_C_TEST_CASES),
('c', COMMON_C_TEST_CASES),
('cs', COMMON_C_TEST_CASES),
('cls', QUOTES_REQUIRED_TEST_CASES),
('java', QUOTES_REQUIRED_TEST_CASES),
('py', QUOTES_REQUIRED_TEST_CASES),
Expand Down

0 comments on commit 194f4c1

Please sign in to comment.