-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforensics_validator.py
More file actions
324 lines (271 loc) · 10.5 KB
/
forensics_validator.py
File metadata and controls
324 lines (271 loc) · 10.5 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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
"""
Master key and credential validation.
Provides:
- Master key validation with magic bytes
- FIPS mode detection
- API token structure validation
- Key file integrity checks
"""
import struct
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
DECRYPTION_MAGIC = b'::::MAGIC::::'
FIPS_MAGIC = b'FIPS::::'
@dataclass
class ValidationResult:
"""Result of validation operation."""
is_valid: bool
error_message: Optional[str] = None
details: Optional[dict] = None
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"is_valid": self.is_valid,
"error": self.error_message,
"details": self.details or {},
}
class Validator:
"""Validation engine for Jenkins master keys and credentials."""
def __init__(self):
"""Initialize validator."""
pass
def validate_master_key(self, key_data: bytes) -> ValidationResult:
"""
Validate a master.key file.
Args:
key_data: Raw bytes from master.key file
Returns:
Validation result
"""
if not key_data:
return ValidationResult(
is_valid=False,
error_message="Master key is empty"
)
# Clean trailing newlines
clean_key = key_data.rstrip(b'\n\r')
# Master key should be hex-encoded AES key (32 or 64 hex chars)
if len(clean_key) not in [32, 64]:
return ValidationResult(
is_valid=False,
error_message=f"Invalid master key length: {len(clean_key)} (expected 32 or 64)",
details={"length": len(clean_key)}
)
# Check if it's valid hex
try:
bytes.fromhex(clean_key.decode('ascii'))
except (ValueError, UnicodeDecodeError):
return ValidationResult(
is_valid=False,
error_message="Master key contains non-hex characters"
)
return ValidationResult(
is_valid=True,
details={
"length": len(clean_key),
"type": "AES-128" if len(clean_key) == 32 else "AES-256"
}
)
def validate_hudson_secret(self, secret_data: bytes) -> ValidationResult:
"""
Validate a hudson.util.Secret file.
Args:
secret_data: Raw bytes from hudson.util.Secret file
Returns:
Validation result
"""
if not secret_data:
return ValidationResult(
is_valid=False,
error_message="Hudson secret is empty"
)
# Clean trailing newlines
clean_secret = secret_data.rstrip(b'\n\r')
# Hudson secret should be 32 hex characters (16 bytes)
if len(clean_secret) != 32:
return ValidationResult(
is_valid=False,
error_message=f"Invalid hudson secret length: {len(clean_secret)} (expected 32)",
details={"length": len(clean_secret)}
)
# Check if it's valid hex
try:
bytes.fromhex(clean_secret.decode('ascii'))
except (ValueError, UnicodeDecodeError):
return ValidationResult(
is_valid=False,
error_message="Hudson secret contains non-hex characters"
)
return ValidationResult(is_valid=True, details={"length": len(clean_secret)})
def detect_fips_mode(self, decrypted_data: bytes) -> bool:
"""
Detect if Jenkins is running in FIPS mode based on decrypted data.
Args:
decrypted_data: Decrypted credential data
Returns:
True if FIPS mode detected
"""
return decrypted_data.startswith(FIPS_MAGIC)
def validate_decrypted_magic(self, decrypted_data: bytes) -> ValidationResult:
"""
Validate that decrypted data contains the magic bytes.
Args:
decrypted_data: Decrypted credential data
Returns:
Validation result
"""
if not decrypted_data:
return ValidationResult(
is_valid=False,
error_message="Decrypted data is empty"
)
# Check for FIPS magic first
if decrypted_data.startswith(FIPS_MAGIC):
return ValidationResult(
is_valid=True,
details={"format": "FIPS", "magic": "FIPS::::"}
)
# Check for standard magic
if decrypted_data.startswith(DECRYPTION_MAGIC):
return ValidationResult(
is_valid=True,
details={"format": "Standard", "magic": "::::MAGIC::::"}
)
return ValidationResult(
is_valid=False,
error_message="Invalid magic bytes (incorrect master.key or corrupted data)",
details={"preview": decrypted_data[:20].hex()}
)
def validate_api_token_structure(self, token: str) -> ValidationResult:
"""
Validate API token structure (offline validation only).
This performs basic structural validation without making network calls.
Args:
token: API token string
Returns:
Validation result
"""
if not token:
return ValidationResult(
is_valid=False,
error_message="Token is empty"
)
# GitHub tokens
if token.startswith(('ghp_', 'gho_', 'ghu_', 'ghs_', 'ghr_')):
if len(token) < 40:
return ValidationResult(
is_valid=False,
error_message="GitHub token too short",
details={"type": "GitHub", "length": len(token)}
)
return ValidationResult(
is_valid=True,
details={"type": "GitHub", "prefix": token[:4]}
)
# GitLab tokens
if token.startswith('glpat-'):
if len(token) < 26:
return ValidationResult(
is_valid=False,
error_message="GitLab token too short",
details={"type": "GitLab", "length": len(token)}
)
return ValidationResult(
is_valid=True,
details={"type": "GitLab"}
)
# AWS access keys
if token.startswith('AKIA'):
if len(token) != 20:
return ValidationResult(
is_valid=False,
error_message=f"AWS access key should be 20 characters, got {len(token)}",
details={"type": "AWS Access Key", "length": len(token)}
)
return ValidationResult(
is_valid=True,
details={"type": "AWS Access Key"}
)
# JWT tokens (basic structure check)
parts = token.split('.')
if len(parts) == 3:
return ValidationResult(
is_valid=True,
details={"type": "JWT", "parts": 3}
)
# Generic token validation (minimum length and character set)
if len(token) < 8:
return ValidationResult(
is_valid=False,
error_message="Token too short (minimum 8 characters)",
details={"length": len(token)}
)
# Check if it's alphanumeric with common token characters
valid_chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_+=/')
if not all(c in valid_chars for c in token):
return ValidationResult(
is_valid=False,
error_message="Token contains invalid characters",
details={"type": "Unknown"}
)
return ValidationResult(
is_valid=True,
details={"type": "Generic", "length": len(token)}
)
def validate_key_files(
self,
master_key_path: Path,
hudson_secret_path: Path
) -> Tuple[ValidationResult, ValidationResult]:
"""
Validate both master.key and hudson.util.Secret files.
Args:
master_key_path: Path to master.key
hudson_secret_path: Path to hudson.util.Secret
Returns:
Tuple of (master_key_result, hudson_secret_result)
"""
# Validate master.key exists and is readable
if not master_key_path.exists():
master_result = ValidationResult(
is_valid=False,
error_message=f"Master key file not found: {master_key_path}"
)
else:
try:
master_data = master_key_path.read_bytes()
master_result = self.validate_master_key(master_data)
except Exception as e:
master_result = ValidationResult(
is_valid=False,
error_message=f"Error reading master.key: {e}"
)
# Validate hudson.util.Secret exists and is readable
if not hudson_secret_path.exists():
hudson_result = ValidationResult(
is_valid=False,
error_message=f"Hudson secret file not found: {hudson_secret_path}"
)
else:
try:
hudson_data = hudson_secret_path.read_bytes()
hudson_result = self.validate_hudson_secret(hudson_data)
except Exception as e:
hudson_result = ValidationResult(
is_valid=False,
error_message=f"Error reading hudson.util.Secret: {e}"
)
return master_result, hudson_result
def quick_validate_keypair(self, master_key: bytes, hudson_secret: bytes) -> bool:
"""
Quick validation of a key pair.
Args:
master_key: Master key bytes
hudson_secret: Hudson secret bytes
Returns:
True if both keys appear valid
"""
master_result = self.validate_master_key(master_key)
hudson_result = self.validate_hudson_secret(hudson_secret)
return master_result.is_valid and hudson_result.is_valid