|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import argparse |
| 4 | +import re |
| 5 | +from collections.abc import Sequence |
| 6 | +from typing import NamedTuple |
| 7 | + |
| 8 | + |
| 9 | +class BadFile(NamedTuple): |
| 10 | + filename: str |
| 11 | + key: str |
| 12 | + pattern_name: str |
| 13 | + |
| 14 | + |
| 15 | +# Patterns based on https://github.com/gitleaks/gitleaks/pull/1291 |
| 16 | +# Azure Data Factory SHIR Key format: IR@{GUID}@{resource_name}@{location}@{base64} |
| 17 | +AZURE_DATAFACTORY_SHIR_PATTERN = re.compile( |
| 18 | + rb"IR@[0-9a-zA-Z-]{36}@[^@\s]+@[0-9a-zA-Z\-=]*@[A-Za-z0-9+/=]{44}", |
| 19 | +) |
| 20 | + |
| 21 | +# CSCAN0020, CSCAN0030 - App service deployment secrets |
| 22 | +AZURE_APP_SERVICE_DEPLOYMENT_PATTERN = re.compile( |
| 23 | + rb"MII[a-zA-Z0-9=_\-]{200,}", |
| 24 | +) |
| 25 | + |
| 26 | +# CSCAN0030, CSCAN0090, CSCAN0150 - Storage credentials (86 char) |
| 27 | +AZURE_STORAGE_86CHAR_PATTERN = re.compile( |
| 28 | + rb"[ \t]{0,10}[a-zA-Z0-9/+]{86}==", |
| 29 | +) |
| 30 | + |
| 31 | +# CSCAN0030, CSCAN0090, CSCAN0150 - Storage credentials (43 char) |
| 32 | +AZURE_STORAGE_43CHAR_PATTERN = re.compile( |
| 33 | + rb"[a-zA-Z0-9/+]{43}=[^{@\d%\s]", |
| 34 | +) |
| 35 | + |
| 36 | +# CSCAN0030, CSCAN0090, CSCAN0150 - SAS/sig tokens |
| 37 | +AZURE_STORAGE_SIG_PATTERN = re.compile( |
| 38 | + rb"(?:sig|sas|password)=[a-zA-Z0-9%]{43,53}%3[dD]", |
| 39 | + re.IGNORECASE, |
| 40 | +) |
| 41 | + |
| 42 | +# CSCAN0030 - Storage credential with userid/password |
| 43 | +AZURE_STORAGE_USERIDPW_PATTERN = re.compile( |
| 44 | + rb'(?:user ?(?:id|name)|uid)=.{2,128}?\s*;\s*(?:password|pwd)=[^\'\$%>@\'";\[{][^;"\']{2,350}?[;"\']', |
| 45 | + re.IGNORECASE, |
| 46 | +) |
| 47 | + |
| 48 | +# CSCAN0030 - AccountKey with MII prefix |
| 49 | +AZURE_STORAGE_ACCOUNTKEY_PATTERN = re.compile( |
| 50 | + rb"AccountKey\s*=\s*MII[a-zA-Z0-9/+]{43,}={0,2}", |
| 51 | + re.IGNORECASE, |
| 52 | +) |
| 53 | + |
| 54 | +# CSCAN0100 - Service Bus SharedAccessKey |
| 55 | +AZURE_STORAGE_SERVICEBUS_PATTERN = re.compile( |
| 56 | + rb'<ServiceBusAccountInfo.*?SharedAccessKey\s*=\s*["\'][a-zA-Z0-9/+]{10,}["\']', |
| 57 | + re.IGNORECASE | re.DOTALL, |
| 58 | +) |
| 59 | + |
| 60 | +# CSCAN0130 - Monitoring Agent credentials |
| 61 | +AZURE_STORAGE_MONIKER_PATTERN = re.compile( |
| 62 | + rb"Account Moniker\s*=.*?key\s*=", |
| 63 | + re.IGNORECASE, |
| 64 | +) |
| 65 | + |
| 66 | +# CSCAN0110 - Blob URL with SAS token |
| 67 | +AZURE_STORAGE_BLOBURL_PATTERN = re.compile( |
| 68 | + rb"https://[a-zA-Z0-9-]+\.(?:blob|file|queue|table|dfs|z\d+\.web)\.core\.windows\.net/.*?sig=[a-zA-Z0-9%]{30,}", |
| 69 | + re.IGNORECASE, |
| 70 | +) |
| 71 | + |
| 72 | +# CSCAN0090 - decryptionKey/validationKey |
| 73 | +AZURE_PASSWORD_MACHINEKEY_PATTERN = re.compile( |
| 74 | + rb'(?:decryptionKey|validationKey)\s*=\s*["\'][^"\']+["\']', |
| 75 | + re.IGNORECASE, |
| 76 | +) |
| 77 | + |
| 78 | +# CSCAN0090 - <add> elements with keys/secrets |
| 79 | +AZURE_PASSWORD_ADDKEY_PATTERN = re.compile( |
| 80 | + rb'<add\s+.*?(?:key(?:s|\d)?|credentials?|secrets?(?:S|\d)?|(?:password|token|key)(?:primary|secondary|orsas|sas|encrypted))\s*=\s*["\'][^"\']+["\'].*?value\s*=\s*["\'][^"\']+["\']', |
| 81 | + re.IGNORECASE, |
| 82 | +) |
| 83 | + |
| 84 | +# CSCAN0090 - Connection strings with password |
| 85 | +AZURE_PASSWORD_CONNSTRING_PATTERN = re.compile( |
| 86 | + rb'(?:connectionstring|connstring)[^=]*?=["\'][^"\']*?password=[^\$\s;][^"\'\s]*?[;"\']', |
| 87 | + re.IGNORECASE, |
| 88 | +) |
| 89 | + |
| 90 | +# CSCAN0090 - Base64 values |
| 91 | +AZURE_PASSWORD_VALUE_PATTERN = re.compile( |
| 92 | + rb'value\s*=\s*["\'](?:[A-Za-z0-9+/]{4}){1,200}==["\']', |
| 93 | + re.IGNORECASE, |
| 94 | +) |
| 95 | + |
| 96 | +# CSCAN0090, CSCAN0150 - uid/password pairs |
| 97 | +AZURE_PASSWORD_UIDPW_PATTERN = re.compile( |
| 98 | + rb'(?:user ?(?:id|name)|uid)=.{2,128}?\s*;\s*(?:password|pwd)=[^\'\$%@\'";\[{][^;"\']{2,350}?[;"\']', |
| 99 | + re.IGNORECASE, |
| 100 | +) |
| 101 | + |
| 102 | +# CSCAN0160 - NetworkCredential with domain |
| 103 | +AZURE_NETWORK_CREDENTIAL_PATTERN = re.compile( |
| 104 | + rb"NetworkCredential\([^)]*?(?:corp|europe|middleeast|northamerica|southpacific|southamerica|fareast|africa|redmond|exchange|extranet|partners|extranettest|parttest|noe|ntdev|ntwksta|sys-wingroup|windeploy|wingroup|winse|segroup|xcorp|xrep|phx|gme|usme|cdocidm|mslpa)\)", |
| 105 | + re.IGNORECASE, |
| 106 | +) |
| 107 | + |
| 108 | +# CSCAN0160 - schtasks with domain credentials |
| 109 | +AZURE_NETWORK_SCHTASKS_PATTERN = re.compile( |
| 110 | + rb"schtasks.*?/ru\s+(?:corp|europe|middleeast|northamerica|southpacific|southamerica|fareast|africa|redmond|exchange|extranet|partners|extranettest|parttest|noe|ntdev|ntwksta|sys-wingroup|windeploy|wingroup|winse|segroup|xcorp|xrep|phx|gme|usme|cdocidm|mslpa).*?/rp", |
| 111 | + re.IGNORECASE, |
| 112 | +) |
| 113 | + |
| 114 | +# CSCAN0160 - .NET NetworkCredential |
| 115 | +AZURE_NETWORK_DOTNET_PATTERN = re.compile( |
| 116 | + rb'new-object\s+System\.Net\.NetworkCredential\([^,]+,\s*["\'][^"\']+["\']', |
| 117 | + re.IGNORECASE, |
| 118 | +) |
| 119 | + |
| 120 | +# CSCAN0200 - DevDiv TFVC credentials |
| 121 | +AZURE_DEVTFVC_PATTERN = re.compile( |
| 122 | + rb"enc_username=.+[\n\r\s]+enc_password=.{3,}", |
| 123 | +) |
| 124 | + |
| 125 | +# CSCAN0240 - DevOps Personal Access Token |
| 126 | +AZURE_DEVOPS_PAT_PATTERN = re.compile( |
| 127 | + rb'access_token.*?[\'="][a-zA-Z0-9/+]{10,99}["\']', |
| 128 | + re.IGNORECASE, |
| 129 | +) |
| 130 | + |
| 131 | +# CSCAN0030 - PublishSettings userPWD |
| 132 | +PUBLISHSETTINGS_PWD_PATTERN = re.compile( |
| 133 | + rb'userPWD="[a-zA-Z0-9/\\+]{60}"', |
| 134 | +) |
| 135 | + |
| 136 | +# CSCAN0060 - PEM certificate files with private key |
| 137 | +PEM_PRIVATE_KEY_PATTERN = re.compile( |
| 138 | + rb"-{5}BEGIN( ([DR]SA|EC|OPENSSH))? PRIVATE KEY-{5}", |
| 139 | +) |
| 140 | + |
| 141 | +# CSCAN0080 - SecurityConfig XML passwords |
| 142 | +SECURITY_CONFIG_PASSWORD_PATTERN = re.compile( |
| 143 | + rb"<[pP]ass[wW]ord>[^<]+</[pP]ass[wW]ord>", |
| 144 | +) |
| 145 | + |
| 146 | +# CSCAN0110 - Script passwords in PowerShell/CMD |
| 147 | +SCRIPT_PASSWORD_PATTERN = re.compile( |
| 148 | + rb'\s-([pP]ass[wW]ord|PASSWORD)\s+(["\'][^"\'\r\n]*["\']|[^$\(\)\[\{<\-\r\n]+\s*(\r\n|\-))', |
| 149 | +) |
| 150 | + |
| 151 | +# CSCAN0111 - General password patterns |
| 152 | +GENERAL_PASSWORD_PATTERN = re.compile( |
| 153 | + rb'[a-zA-Z_\s](([pP]ass[wW]ord)|PASSWORD|([cC]lient|CLIENT|[aA]pp|APP)_?([sS]ecret|SECRET))\s{0,3}=\s{0,3}[\'"][^\s"\']{2,200}?[\'"][;\s]', |
| 154 | +) |
| 155 | + |
| 156 | +# CSCAN0210 - Git credentials |
| 157 | +GIT_CREDENTIALS_PATTERN = re.compile( |
| 158 | + rb"[hH][tT][tT][pP][sS]?://.+:.+@[^/]+\.[cC][oO][mM]", |
| 159 | +) |
| 160 | + |
| 161 | +# CSCAN0220 - Password contexts (ConvertTo-SecureString, X509Certificate2, etc.) |
| 162 | +PASSWORD_CONTEXT_PATTERN = re.compile( |
| 163 | + rb'([cC]onvert[tT]o-[sS]ecure[sS]tring(\s*-[sS]tring)?\s*"[^"\r\n]+"|new\sX509Certificate2\([^()]*,\s*"[^"\r\n]+"|<[pP]ass[wW]ord>(<[vV]alue>)?.+(</[vV]alue>)?</[pP]ass[wW]ord>|([cC]lear[tT]ext[pP]ass[wW]ord|CLEARTEXTPASSWORD)("?)?\s*[:=]\s*"[^"\r\n]+")', |
| 164 | +) |
| 165 | + |
| 166 | +# CSCAN0230 - Slack tokens |
| 167 | +SLACK_TOKEN_PATTERN = re.compile( |
| 168 | + rb"xoxp-[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+|xoxb-[a-zA-Z0-9]+-[a-zA-Z0-9]+", |
| 169 | +) |
| 170 | + |
| 171 | +# CSCAN0250 - OAuth/JWT tokens and refresh tokens |
| 172 | +JWT_TOKEN_PATTERN = re.compile( |
| 173 | + rb"eyJ[a-zA-Z0-9\-_%]+\.eyJ[a-zA-Z0-9\-_%]+\.[a-zA-Z0-9\-_%]+", |
| 174 | +) |
| 175 | + |
| 176 | +REFRESH_TOKEN_PATTERN = re.compile( |
| 177 | + rb'([rR]efresh_?[tT]oken|REFRESH_?TOKEN)["\']?\s*[:=]\s*["\']?([a-zA-Z0-9_]+-)+[a-zA-Z0-9_]+["\']?', |
| 178 | +) |
| 179 | + |
| 180 | +# CSCAN0260 - Ansible Vault (corrected from CSCAN0270) |
| 181 | +ANSIBLE_VAULT_PATTERN = re.compile( |
| 182 | + rb"\$ANSIBLE_VAULT;[0-9]\.[0-9];AES256[\r\n]+\d+", |
| 183 | +) |
| 184 | + |
| 185 | +# CSCAN0270 - Azure PowerShell Token Cache |
| 186 | +AZURE_POWERSHELL_TOKEN_PATTERN = re.compile( |
| 187 | + rb'["\']TokenCache["\']\s*:\s*\{\s*["\']CacheData["\']\s*:\s*["\'][a-zA-Z0-9/\+]{86}', |
| 188 | +) |
| 189 | + |
| 190 | +# CSCAN0140 - Default/known passwords |
| 191 | +DEFAULT_PASSWORDS_PATTERN = re.compile( |
| 192 | + rb"(T!T@n1130|[pP]0rsche911|[cC]o[mM][mM]ac\!12|[pP][aA]ss@[wW]or[dD]1|[rR]dP[aA]\$\$[wW]0r[dD]|iis6\!dfu|[pP]@ss[wW]or[dD]1|[pP][aA]\$\$[wW]or[dD]1|\!\!123ab|[aA]dmin123|[pP]@ss[wW]0r[dD]1|[uU]ser@123|[aA]bc@123|[pP][aA]ss[wW]or[dD]@123|homerrocks|[pP][aA]\$\$[wW]0r[dD]1?|Y29NbWFjITEy|[pP][aA]ss4Sales|WS2012R2R0cks\!|DSFS0319Test|March2010M2\!|[pP][aA]ss[wW]ord~1|[mM]icr0s0ft|test1test\!|123@tieorg|homerocks|[eE]lvis1)", |
| 193 | +) |
| 194 | + |
| 195 | + |
| 196 | +PATTERNS = [ |
| 197 | + ("datafactory-shir", AZURE_DATAFACTORY_SHIR_PATTERN), |
| 198 | + ("app-service-deployment", AZURE_APP_SERVICE_DEPLOYMENT_PATTERN), |
| 199 | + ("publishsettings-pwd", PUBLISHSETTINGS_PWD_PATTERN), |
| 200 | + ("storage-86char", AZURE_STORAGE_86CHAR_PATTERN), |
| 201 | + ("storage-43char", AZURE_STORAGE_43CHAR_PATTERN), |
| 202 | + ("storage-sig", AZURE_STORAGE_SIG_PATTERN), |
| 203 | + ("storage-useridpw", AZURE_STORAGE_USERIDPW_PATTERN), |
| 204 | + ("storage-accountkey", AZURE_STORAGE_ACCOUNTKEY_PATTERN), |
| 205 | + ("storage-servicebus", AZURE_STORAGE_SERVICEBUS_PATTERN), |
| 206 | + ("storage-moniker", AZURE_STORAGE_MONIKER_PATTERN), |
| 207 | + ("storage-bloburl", AZURE_STORAGE_BLOBURL_PATTERN), |
| 208 | + ("password-machinekey", AZURE_PASSWORD_MACHINEKEY_PATTERN), |
| 209 | + ("password-addkey", AZURE_PASSWORD_ADDKEY_PATTERN), |
| 210 | + ("password-connstring", AZURE_PASSWORD_CONNSTRING_PATTERN), |
| 211 | + ("password-value", AZURE_PASSWORD_VALUE_PATTERN), |
| 212 | + ("password-uidpw", AZURE_PASSWORD_UIDPW_PATTERN), |
| 213 | + ("network-credential", AZURE_NETWORK_CREDENTIAL_PATTERN), |
| 214 | + ("network-schtasks", AZURE_NETWORK_SCHTASKS_PATTERN), |
| 215 | + ("network-dotnet", AZURE_NETWORK_DOTNET_PATTERN), |
| 216 | + ("devtfvc-secrets", AZURE_DEVTFVC_PATTERN), |
| 217 | + ("devops-pat", AZURE_DEVOPS_PAT_PATTERN), |
| 218 | + ("pem-private-key", PEM_PRIVATE_KEY_PATTERN), |
| 219 | + ("security-config-password", SECURITY_CONFIG_PASSWORD_PATTERN), |
| 220 | + ("script-password", SCRIPT_PASSWORD_PATTERN), |
| 221 | + ("general-password", GENERAL_PASSWORD_PATTERN), |
| 222 | + ("git-credentials", GIT_CREDENTIALS_PATTERN), |
| 223 | + ("password-context", PASSWORD_CONTEXT_PATTERN), |
| 224 | + ("slack-token", SLACK_TOKEN_PATTERN), |
| 225 | + ("jwt-token", JWT_TOKEN_PATTERN), |
| 226 | + ("refresh-token", REFRESH_TOKEN_PATTERN), |
| 227 | + ("ansible-vault", ANSIBLE_VAULT_PATTERN), |
| 228 | + ("azure-powershell-token", AZURE_POWERSHELL_TOKEN_PATTERN), |
| 229 | + ("default-passwords", DEFAULT_PASSWORDS_PATTERN), |
| 230 | +] |
| 231 | + |
| 232 | + |
| 233 | +def check_file_for_azure_keys( |
| 234 | + filenames: Sequence[str], |
| 235 | +) -> list[BadFile]: |
| 236 | + """Check if files contain Azure credentials. |
| 237 | +
|
| 238 | + Return a list of all files containing Azure credentials with the keys |
| 239 | + obfuscated to ease debugging. |
| 240 | + """ |
| 241 | + bad_files = [] |
| 242 | + |
| 243 | + for filename in filenames: |
| 244 | + with open(filename, "rb") as content: |
| 245 | + text_body = content.read() |
| 246 | + |
| 247 | + # Check all Azure credential patterns |
| 248 | + for pattern_name, pattern in PATTERNS: |
| 249 | + matches = pattern.findall(text_body) |
| 250 | + for match in matches: |
| 251 | + # Handle tuple results from regex groups |
| 252 | + if isinstance(match, tuple): |
| 253 | + match = match[0] |
| 254 | + |
| 255 | + # Obfuscate the key |
| 256 | + key_str = match.decode("utf-8", errors="replace") |
| 257 | + if len(key_str) > 20: |
| 258 | + key_hidden = key_str[:10] + "***" + key_str[-7:] |
| 259 | + else: |
| 260 | + key_hidden = key_str[:4] + "***" |
| 261 | + |
| 262 | + bad_files.append( |
| 263 | + BadFile(filename, key_hidden, pattern_name), |
| 264 | + ) |
| 265 | + |
| 266 | + return bad_files |
| 267 | + |
| 268 | + |
| 269 | +def main(argv: Sequence[str] | None = None) -> int: |
| 270 | + parser = argparse.ArgumentParser() |
| 271 | + parser.add_argument("filenames", nargs="+", help="Filenames to run") |
| 272 | + args = parser.parse_args(argv) |
| 273 | + |
| 274 | + bad_filenames = check_file_for_azure_keys(args.filenames) |
| 275 | + if bad_filenames: |
| 276 | + for bad_file in bad_filenames: |
| 277 | + print( |
| 278 | + f"Azure credential ({bad_file.pattern_name}) found in " |
| 279 | + f"{bad_file.filename}: {bad_file.key}", |
| 280 | + ) |
| 281 | + return 1 |
| 282 | + else: |
| 283 | + return 0 |
| 284 | + |
| 285 | + |
| 286 | +if __name__ == "__main__": |
| 287 | + raise SystemExit(main()) |
0 commit comments