Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 1 | from __future__ import annotations |
| 2 | |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 3 | import argparse |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 4 | import configparser |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 5 | import os |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 6 | from typing import NamedTuple |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 7 | from typing import Sequence |
Ara Hayrabedian | 02e8bdc | 2015-06-12 19:20:56 +0400 | [diff] [blame] | 8 | |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 9 | |
| 10 | class BadFile(NamedTuple): |
| 11 | filename: str |
| 12 | key: str |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 13 | |
| 14 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 15 | def get_aws_cred_files_from_env() -> set[str]: |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 16 | """Extract credential file paths from environment variables.""" |
Anthony Sottile | 4575652 | 2019-02-11 19:56:15 -0800 | [diff] [blame] | 17 | return { |
| 18 | os.environ[env_var] |
| 19 | for env_var in ( |
| 20 | 'AWS_CONFIG_FILE', 'AWS_CREDENTIAL_FILE', |
| 21 | 'AWS_SHARED_CREDENTIALS_FILE', 'BOTO_CONFIG', |
| 22 | ) |
| 23 | if env_var in os.environ |
| 24 | } |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 25 | |
| 26 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 27 | def get_aws_secrets_from_env() -> set[str]: |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 28 | """Extract AWS secrets from environment variables.""" |
| 29 | keys = set() |
Daniel Roschka | 3939aee | 2017-01-03 19:05:49 +0100 | [diff] [blame] | 30 | for env_var in ( |
Anthony Sottile | 2a902e0 | 2017-07-12 18:35:24 -0700 | [diff] [blame] | 31 | 'AWS_SECRET_ACCESS_KEY', 'AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN', |
Daniel Roschka | 3939aee | 2017-01-03 19:05:49 +0100 | [diff] [blame] | 32 | ): |
Alexander Demin | b3a28de | 2020-02-13 17:12:45 +0000 | [diff] [blame] | 33 | if os.environ.get(env_var): |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 34 | keys.add(os.environ[env_var]) |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 35 | return keys |
| 36 | |
| 37 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 38 | def get_aws_secrets_from_file(credentials_file: str) -> set[str]: |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 39 | """Extract AWS secrets from configuration files. |
| 40 | |
| 41 | Read an ini-style configuration file and return a set with all found AWS |
| 42 | secret access keys. |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 43 | """ |
| 44 | aws_credentials_file_path = os.path.expanduser(credentials_file) |
| 45 | if not os.path.exists(aws_credentials_file_path): |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 46 | return set() |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 47 | |
Ara Hayrabedian | 3078aec | 2015-06-12 16:24:01 +0400 | [diff] [blame] | 48 | parser = configparser.ConfigParser() |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 49 | try: |
| 50 | parser.read(aws_credentials_file_path) |
| 51 | except configparser.MissingSectionHeaderError: |
| 52 | return set() |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 53 | |
| 54 | keys = set() |
| 55 | for section in parser.sections(): |
Daniel Roschka | 3939aee | 2017-01-03 19:05:49 +0100 | [diff] [blame] | 56 | for var in ( |
| 57 | 'aws_secret_access_key', 'aws_security_token', |
Anthony Sottile | 2a902e0 | 2017-07-12 18:35:24 -0700 | [diff] [blame] | 58 | 'aws_session_token', |
Daniel Roschka | 3939aee | 2017-01-03 19:05:49 +0100 | [diff] [blame] | 59 | ): |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 60 | try: |
Pablo Vega | 83fca4c | 2018-01-26 00:28:39 -0800 | [diff] [blame] | 61 | key = parser.get(section, var).strip() |
| 62 | if key: |
| 63 | keys.add(key) |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 64 | except configparser.NoOptionError: |
| 65 | pass |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 66 | return keys |
| 67 | |
| 68 | |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 69 | def check_file_for_aws_keys( |
| 70 | filenames: Sequence[str], |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 71 | keys: set[bytes], |
| 72 | ) -> list[BadFile]: |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 73 | """Check if files contain AWS secrets. |
| 74 | |
| 75 | Return a list of all files containing AWS secrets and keys found, with all |
| 76 | but the first four characters obfuscated to ease debugging. |
| 77 | """ |
Dean Wilson | a666527 | 2015-10-28 05:13:37 +0000 | [diff] [blame] | 78 | bad_files = [] |
| 79 | |
| 80 | for filename in filenames: |
Anthony Sottile | 21553c2 | 2020-02-18 10:24:17 -0800 | [diff] [blame] | 81 | with open(filename, 'rb') as content: |
Dean Wilson | a666527 | 2015-10-28 05:13:37 +0000 | [diff] [blame] | 82 | text_body = content.read() |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 83 | for key in keys: |
| 84 | # naively match the entire file, low chance of incorrect |
| 85 | # collision |
Alexander Demin | 75d4832 | 2020-02-13 12:01:38 +0000 | [diff] [blame] | 86 | if key in text_body: |
Anthony Sottile | 21553c2 | 2020-02-18 10:24:17 -0800 | [diff] [blame] | 87 | key_hidden = key.decode()[:4].ljust(28, '*') |
| 88 | bad_files.append(BadFile(filename, key_hidden)) |
Dean Wilson | a666527 | 2015-10-28 05:13:37 +0000 | [diff] [blame] | 89 | return bad_files |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 90 | |
| 91 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 92 | def main(argv: Sequence[str] | None = None) -> int: |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 93 | parser = argparse.ArgumentParser() |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 94 | parser.add_argument('filenames', nargs='+', help='Filenames to run') |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 95 | parser.add_argument( |
Anthony Sottile | d444ab8 | 2016-02-08 17:05:39 -0800 | [diff] [blame] | 96 | '--credentials-file', |
Ryan Delaney | 2d4833c | 2018-10-28 15:58:14 -0700 | [diff] [blame] | 97 | dest='credentials_file', |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 98 | action='append', |
Anthony Sottile | 5da199b | 2017-01-03 13:13:44 -0800 | [diff] [blame] | 99 | default=[ |
| 100 | '~/.aws/config', '~/.aws/credentials', '/etc/boto.cfg', '~/.boto', |
| 101 | ], |
Anthony Sottile | d444ab8 | 2016-02-08 17:05:39 -0800 | [diff] [blame] | 102 | help=( |
Ryan Delaney | 0d83fed | 2018-10-28 15:59:39 -0700 | [diff] [blame] | 103 | 'Location of additional AWS credential file from which to get ' |
| 104 | 'secret keys. Can be passed multiple times.' |
Anthony Sottile | 2a902e0 | 2017-07-12 18:35:24 -0700 | [diff] [blame] | 105 | ), |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 106 | ) |
Mike Fiedler | 312e721 | 2017-02-10 08:26:26 -0500 | [diff] [blame] | 107 | parser.add_argument( |
| 108 | '--allow-missing-credentials', |
| 109 | dest='allow_missing_credentials', |
| 110 | action='store_true', |
Anthony Sottile | 2a902e0 | 2017-07-12 18:35:24 -0700 | [diff] [blame] | 111 | help='Allow hook to pass when no credentials are detected.', |
Mike Fiedler | 312e721 | 2017-02-10 08:26:26 -0500 | [diff] [blame] | 112 | ) |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 113 | args = parser.parse_args(argv) |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 114 | |
Ryan Delaney | 2d4833c | 2018-10-28 15:58:14 -0700 | [diff] [blame] | 115 | credential_files = set(args.credentials_file) |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 116 | |
| 117 | # Add the credentials files configured via environment variables to the set |
| 118 | # of files to to gather AWS secrets from. |
Anthony Sottile | 4575652 | 2019-02-11 19:56:15 -0800 | [diff] [blame] | 119 | credential_files |= get_aws_cred_files_from_env() |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 120 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 121 | keys: set[str] = set() |
Daniel Roschka | b0d4cdb | 2016-12-30 08:41:24 +0100 | [diff] [blame] | 122 | for credential_file in credential_files: |
| 123 | keys |= get_aws_secrets_from_file(credential_file) |
| 124 | |
| 125 | # Secrets might be part of environment variables, so add such secrets to |
| 126 | # the set of keys. |
| 127 | keys |= get_aws_secrets_from_env() |
| 128 | |
Mike Fiedler | 312e721 | 2017-02-10 08:26:26 -0500 | [diff] [blame] | 129 | if not keys and args.allow_missing_credentials: |
| 130 | return 0 |
| 131 | |
Ara Hayrabedian | 02e8bdc | 2015-06-12 19:20:56 +0400 | [diff] [blame] | 132 | if not keys: |
Daniel Roschka | 3939aee | 2017-01-03 19:05:49 +0100 | [diff] [blame] | 133 | print( |
| 134 | 'No AWS keys were found in the configured credential files and ' |
| 135 | 'environment variables.\nPlease ensure you have the correct ' |
Anthony Sottile | 2a902e0 | 2017-07-12 18:35:24 -0700 | [diff] [blame] | 136 | 'setting for --credentials-file', |
Daniel Roschka | 3939aee | 2017-01-03 19:05:49 +0100 | [diff] [blame] | 137 | ) |
Ara Hayrabedian | 02e8bdc | 2015-06-12 19:20:56 +0400 | [diff] [blame] | 138 | return 2 |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 139 | |
Anthony Sottile | 21553c2 | 2020-02-18 10:24:17 -0800 | [diff] [blame] | 140 | keys_b = {key.encode() for key in keys} |
| 141 | bad_filenames = check_file_for_aws_keys(args.filenames, keys_b) |
Dean Wilson | a666527 | 2015-10-28 05:13:37 +0000 | [diff] [blame] | 142 | if bad_filenames: |
| 143 | for bad_file in bad_filenames: |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 144 | print(f'AWS secret found in {bad_file.filename}: {bad_file.key}') |
Dean Wilson | a666527 | 2015-10-28 05:13:37 +0000 | [diff] [blame] | 145 | return 1 |
| 146 | else: |
| 147 | return 0 |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 148 | |
Anthony Sottile | 70e405e | 2016-11-30 09:56:42 -0800 | [diff] [blame] | 149 | |
Ara Hayrabedian | 95bf20d | 2015-05-31 23:50:49 +0400 | [diff] [blame] | 150 | if __name__ == '__main__': |
Anthony Sottile | 39ab2ed | 2021-10-23 13:23:50 -0400 | [diff] [blame] | 151 | raise SystemExit(main()) |