-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathcve_2025_30066_scanner.py
More file actions
193 lines (156 loc) · 6.62 KB
/
cve_2025_30066_scanner.py
File metadata and controls
193 lines (156 loc) · 6.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import os
import base64
import re
import zipfile
import tempfile
def is_base64(encoded_string):
"""
Check if a string is valid base64 encoded.
Args:
encoded_string: The string to check
Returns:
bool: True if the string is valid base64, False otherwise
"""
try:
# Add padding if needed (base64 strings must be multiples of 4)
missing_padding = len(encoded_string) % 4
if missing_padding:
encoded_string += '=' * (4 - missing_padding)
# Try to decode and re-encode to verify it's valid base64
decoded = base64.b64decode(encoded_string, validate=False)
re_encoded = base64.b64encode(decoded).decode().rstrip('=')
return re_encoded == encoded_string.rstrip('=')
except Exception:
return False
def decode_base64(encoded_string):
"""
Decode a base64 string with proper padding.
Args:
encoded_string: The base64 string to decode
Returns:
str: The decoded string
"""
# Add padding if needed
if len(encoded_string) % 4:
encoded_string += '=' * (4 - len(encoded_string) % 4)
return base64.b64decode(encoded_string).decode(errors='ignore')
def find_secret_base64(file_path):
"""
Search a file for double-encoded base64 strings containing secrets.
Args:
file_path: Path to the file to scan
Returns:
list: List of tuples containing (original, decoded_secret)
"""
secret_matches = []
with open(file_path, 'r', errors='ignore') as file:
for line in file:
# Find potential base64 strings (20+ characters of base64 alphabet)
potential_base64 = re.findall(r'[A-Za-z0-9+/=]{20,}', line)
for encoded in potential_base64:
if is_base64(encoded):
# First decode
intermediate = decode_base64(encoded)
# Check if first decode is also base64
if is_base64(intermediate):
# Second decode to get the secret
decoded_secret = decode_base64(intermediate)
# Check if it contains a secret
if 'isSecret' in decoded_secret:
secret_matches.append((encoded, decoded_secret))
return secret_matches
def extract_zip(zip_path, extract_path):
"""
Extract a zip file to a temporary directory.
Args:
zip_path: Path to the zip file
extract_path: Path where to extract the zip file
Returns:
bool: True if extraction was successful, False otherwise
"""
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_path)
return True
except Exception as e:
print(f"Error extracting {zip_path}: {e}")
return False
def scan_for_secrets(directory):
"""
Recursively scan a directory for files containing secret base64 strings.
Handles both regular files and zip files.
Args:
directory: Directory path to scan
Returns:
tuple: (results dictionary, total files scanned, files with secrets)
"""
results = {}
total_files = 0
files_with_secrets = 0
# Create a temporary directory for zip extraction
with tempfile.TemporaryDirectory() as temp_dir:
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
total_files += 1
# Handle zip files
if file.lower().endswith('.zip'):
print(f"\nProcessing zip file: {file}")
zip_extract_path = os.path.join(temp_dir, os.path.splitext(file)[0])
os.makedirs(zip_extract_path, exist_ok=True)
if extract_zip(file_path, zip_extract_path):
# Scan the extracted contents
for extracted_root, _, extracted_files in os.walk(zip_extract_path):
for extracted_file in extracted_files:
extracted_path = os.path.join(extracted_root, extracted_file)
total_files += 1
matches = find_secret_base64(extracted_path)
if matches:
files_with_secrets += 1
# Store results with original zip file path
results[f"{file_path}/{os.path.relpath(extracted_path, zip_extract_path)}"] = matches
else:
# Handle regular files
matches = find_secret_base64(file_path)
if matches:
files_with_secrets += 1
results[file_path] = matches
return results, total_files, files_with_secrets
def print_results(results, total_files, files_with_secrets):
"""
Print the results in a readable format.
Args:
results: Dictionary of results from scan_for_secrets
total_files: Total number of files scanned
files_with_secrets: Number of files containing secrets
"""
print("\nScan Summary:")
print("-" * 50)
print(f"Total files scanned: {total_files}")
print(f"Files containing secrets: {files_with_secrets}")
print("-" * 50)
if not results:
print("\nNo secret base64 strings found in any of the scanned files.")
return
print("\nFound secret base64 strings in the following files:")
print("-" * 50)
for file_path, matches in results.items():
print(f"\nFile: {file_path}")
print("-" * 30)
for i, (encoded, decoded_secret) in enumerate(matches, 1):
print(f"\nMatch #{i}:")
print(f"Original: {encoded}")
print(f"Decoded Secret: {decoded_secret}")
def main():
print("CVE-2025-30066 - GitHub Actions Supply Chain Vulnerability")
print("Scan workflow logs for secrets potentially exposed via compromised tj-actions/changed-files")
print("=" * 20)
directory = input("\nEnter directory to scan: ").strip()
if not os.path.exists(directory):
print(f"Error: Directory '{directory}' does not exist.")
return
print("\nScanning...")
results, total_files, files_with_secrets = scan_for_secrets(directory)
print_results(results, total_files, files_with_secrets)
if __name__ == "__main__":
main()