@@ -38,13 +38,41 @@ class SecurityScanner:
3838 # Sensitive word patterns (for detecting hardcoded secrets)
3939 # Updated to handle escaped quotes in string values
4040 SENSITIVE_PATTERNS = [
41+ # Password patterns
4142 (r'(?i)(password|passwd|pwd)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'password' ),
42- (r'(?i)(api_key|apikey|api_secret)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'API key' ),
43- (r'(?i)(secret|secret_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'secret key' ),
44- (r'(?i)(token|auth_token|access_token)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'token' ),
45- (r'(?i)(private_key|privatekey)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'private key' ),
46- (r'(?i)(aws_access_key|aws_secret)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'AWS credential' ),
47- (r'(?i)(database_url|db_password)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'database credential' ),
43+ (r'(?i)(password|passwd|pwd)\s*=\s*[\'"][^\'"]{8,}[\'"]' , 'password' ), # Longer passwords
44+
45+ # API keys and secrets
46+ (r'(?i)(api_key|apikey|api_secret|api-key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'API key' ),
47+ (r'(?i)api[_-]?key[_-]?\w*\s*=\s*[\'"][a-zA-Z0-9_\-]{16,}[\'"]' , 'API key' ), # API_KEY_XXX pattern
48+ (r'(?i)(secret|secret_key|secretkey)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'secret key' ),
49+ (r'(?i)secret[_-]?key[_-]?\w*\s*=\s*[\'"][a-zA-Z0-9_\-]{16,}[\'"]' , 'secret key' ),
50+
51+ # Token patterns
52+ (r'(?i)(token|auth_token|access_token|bearer|jwt)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'token' ),
53+ (r'(?i)(token|auth_token|access_token)\s*=\s*[\'"][a-zA-Z0-9_\-\.]{20,}[\'"]' , 'token' ),
54+ (r'(?i)jwt[_-]?secret\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'JWT secret' ),
55+
56+ # Private keys
57+ (r'(?i)(private_key|privatekey|private-key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'private key' ),
58+ (r'(?i)(private_key|privatekey)\s*=\s*[\'"][\'"]-----BEGIN' , 'private key' ), # PEM format
59+
60+ # Cloud provider credentials
61+ (r'(?i)(aws_access_key|aws_secret|aws_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'AWS credential' ),
62+ (r'(?i)(gcp_key|gcp_secret|google_api_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'GCP credential' ),
63+ (r'(?i)(azure_key|azure_secret|azure_connection_string)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'Azure credential' ),
64+
65+ # Database credentials
66+ (r'(?i)(database_url|db_password|db_user|db_host)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'database credential' ),
67+ (r'(?i)(mongodb_uri|mongo_url|postgres_url|mysql_url)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'database URL' ),
68+
69+ # OAuth and authentication
70+ (r'(?i)(oauth_token|oauth_secret|client_secret)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'OAuth credential' ),
71+ (r'(?i)(client_id|client_secret)\s*=\s*[\'"][a-zA-Z0-9_\-]{16,}[\'"]' , 'OAuth credential' ),
72+
73+ # Encryption keys
74+ (r'(?i)(encryption_key|encrypt_key|cipher_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]' , 'encryption key' ),
75+ (r'(?i)(salt|iv)\s*=\s*[\'"][a-zA-Z0-9_\-]{8,}[\'"]' , 'encryption salt/IV' ),
4876 ]
4977
5078 # SQL injection risk patterns
@@ -286,14 +314,19 @@ def _get_func_full_name(self, node: ast.AST) -> str:
286314
287315 def _check_command_injection (self , tree : ast .AST ):
288316 """Check for command injection risks"""
289- os_functions = {'system' , 'popen' , 'spawn' , 'call' , 'run' }
290- subprocess_functions = {'call' , 'run' , 'Popen' , 'check_output' }
317+ # Extended list of dangerous os functions
318+ os_functions = {'system' , 'popen' , 'spawn' , 'spawnl' , 'spawnle' , 'spawnlp' ,
319+ 'spawnlpe' , 'spawnv' , 'spawnve' , 'spawnvp' , 'spawnvpe' ,
320+ 'call' , 'run' , 'startfile' }
321+ subprocess_functions = {'call' , 'run' , 'Popen' , 'check_output' , 'check_call' , 'getoutput' , 'getstatusoutput' }
322+ # Legacy/deprecated modules (Python 2 compatibility layer in Python 3)
323+ legacy_modules = {'commands' , 'popen2' , 'popen3' , 'popen4' }
291324
292325 for node in ast .walk (tree ):
293326 if isinstance (node , ast .Call ):
294327 func_full_name = self ._get_func_full_name (node .func )
295328
296- # os.system, os.popen
329+ # os.system, os.popen, os.spawn*, etc.
297330 if func_full_name .startswith ('os.' ):
298331 # Ensure node.func is ast.Attribute type before accessing attr
299332 if isinstance (node .func , ast .Attribute ) and node .func .attr in os_functions :
@@ -336,6 +369,43 @@ def _check_command_injection(self, tree: ast.AST):
336369 lineno = node .lineno ,
337370 suggestion = "Use shell=False and pass argument list"
338371 ))
372+
373+ # Legacy modules like commands.getoutput
374+ elif func_full_name .startswith ('commands.' ):
375+ self .issues .append (CodeIssue (
376+ id = self ._generate_issue_id ("legacy_command" ),
377+ type = "security" ,
378+ severity = SeverityLevel .ERROR ,
379+ message = f"Legacy module 'commands' is insecure and removed in Python 3" ,
380+ lineno = node .lineno ,
381+ suggestion = "Use subprocess.run() with shell=False"
382+ ))
383+
384+ # popen2, popen3, popen4 modules
385+ elif any (func_full_name .startswith (f'{ mod } .' ) for mod in legacy_modules ):
386+ self .issues .append (CodeIssue (
387+ id = self ._generate_issue_id ("popen_legacy" ),
388+ type = "security" ,
389+ severity = SeverityLevel .ERROR ,
390+ message = f"Legacy module '{ func_full_name .split ('.' )[0 ]} ' is deprecated and insecure" ,
391+ lineno = node .lineno ,
392+ suggestion = "Use subprocess.Popen() instead"
393+ ))
394+
395+ # eval() and exec() with potentially user-controlled input
396+ elif isinstance (node .func , ast .Name ) and node .func .id in ('eval' , 'exec' ):
397+ if node .args :
398+ arg = node .args [0 ]
399+ # Check if it's not a constant (i.e., potentially dynamic)
400+ if not isinstance (arg , ast .Constant ):
401+ self .issues .append (CodeIssue (
402+ id = self ._generate_issue_id ("code_injection" ),
403+ type = "security" ,
404+ severity = SeverityLevel .ERROR ,
405+ message = f"Using { node .func .id } () with dynamic input is a code injection risk" ,
406+ lineno = node .lineno ,
407+ suggestion = "Avoid eval/exec with user input; use ast.literal_eval() for safe evaluation"
408+ ))
339409
340410 def _check_path_traversal (self , tree : ast .AST ):
341411 """Check for path traversal risks"""
0 commit comments