-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsysdiag.py
More file actions
363 lines (330 loc) · 11.4 KB
/
Copy pathsysdiag.py
File metadata and controls
363 lines (330 loc) · 11.4 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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#!/usr/bin/env python3
import re
import sys
from dataclasses import dataclass
from typing import list
" Basic - Since Python 3.7, the @dataclass simplifies class definitions by using type hints to determine fields and then writing the underlying code for you
@dataclass
class Diagnostic:
key: str
title: str
patterns: list[str]
commands: list[str]
notes: list[str]
DIAGNOSTICS = [
Diagnostic(
key="git_deploy_state",
title="Inspect git deployment state directly on production",
patterns=[
r"\bgit\b",
r"\bbranch\b",
r"\bdeployment\b",
r"\bdeployed\b",
r"\bproduction\b",
r"\bcommit\b",
],
commands=[
"cd /var/www/YOUR_APP",
"git status",
"git branch --show-current",
"git rev-parse HEAD",
"git log -1 --oneline",
"git remote -v",
"git fetch --all --prune",
"git status -sb",
"git log --oneline --decorate --graph -10",
],
notes=[
"Do not run git pull blindly on production.",
"Compare current HEAD with expected deploy commit.",
"Check whether local files are modified.",
],
),
Diagnostic(
key="app_env",
title="Inspect APP_ENV from .env in /var/www",
patterns=[
r"\bAPP_ENV\b",
r"\.env",
r"\benvironment\b",
r"\bproduction\b",
r"\bstaging\b",
],
commands=[
"cd /var/www/YOUR_APP",
"grep '^APP_ENV=' .env",
"grep '^APP_DEBUG=' .env",
"php artisan env",
"php artisan about",
"php artisan config:show app.env",
],
notes=[
"If config is cached, .env changes may not reflect immediately.",
"Check bootstrap/cache/config.php if Laravel config is cached.",
],
),
Diagnostic(
key="malware_cron",
title="Detect malware or suspicious cron entries",
patterns=[
r"\bmalware\b",
r"\bsuspicious\b",
r"\bcron\b",
r"\bcrontab\b",
r"\bhacked\b",
r"\binfected\b",
],
commands=[
"crontab -l",
"sudo ls -lah /etc/cron.*",
"sudo cat /etc/crontab",
"sudo grep -R \"curl\\|wget\\|base64\\|bash\\|sh\" /etc/cron* /var/spool/cron/crontabs 2>/dev/null",
"find /var/www -type f -mtime -3",
"find /var/www -type f -name '*.php' -exec grep -Il \"eval\\|base64_decode\\|shell_exec\\|passthru\\|assert\" {} \\;",
],
notes=[
"Look for curl/wget piping into bash.",
"Look for recently modified PHP files.",
"Do not delete suspicious files immediately; copy them for evidence first.",
],
),
Diagnostic(
key="php_upload_limits",
title="Debug PHP upload limits and timeout settings",
patterns=[
r"\bupload\b",
r"\btimeout\b",
r"\bphp.ini\b",
r"\bpost_max_size\b",
r"\bupload_max_filesize\b",
r"\bmax_execution_time\b",
],
commands=[
"php -i | grep -E 'upload_max_filesize|post_max_size|max_execution_time|max_input_time|memory_limit'",
"php --ini",
"php -v",
"grep -R \"upload_max_filesize\\|post_max_size\\|max_execution_time\" /etc/php/* -n 2>/dev/null",
"sudo systemctl status php*-fpm",
"sudo nginx -T | grep -E 'client_max_body_size|fastcgi_read_timeout'",
],
notes=[
"Effective upload size is limited by the smallest relevant limit.",
"Check PHP-FPM config, web server config, and Laravel validation rules.",
"Restart PHP-FPM after changing php.ini.",
],
),
Diagnostic(
key="laravel_cache_redis",
title="Inspect Laravel cache/store driver health and Redis",
patterns=[
r"\bcache\b",
r"\bredis\b",
r"\bsession\b",
r"\bstore\b",
r"\bdriver\b",
],
commands=[
"cd /var/www/YOUR_APP",
"grep -E 'CACHE_DRIVER|CACHE_STORE|SESSION_DRIVER|QUEUE_CONNECTION|REDIS_HOST|REDIS_PORT' .env",
"php artisan tinker --execute=\"Cache::put('sysdiag_test', 'ok', 60); dump(Cache::get('sysdiag_test'));\"",
"redis-cli ping",
"redis-cli info server",
"redis-cli info memory",
"sudo systemctl status redis",
],
notes=[
"Expected Redis response is PONG.",
"Check if Laravel uses database/file/redis cache store.",
"If config is cached, run php artisan config:show or inspect cached config.",
],
),
Diagnostic(
key="disk_cleanup",
title="Safely clean disk space without downtime",
patterns=[
r"\bdisk\b",
r"\bspace\b",
r"\bstorage\b",
r"\bcleanup\b",
r"\bclean\b",
r"\bfull\b",
r"\bno space\b",
],
commands=[
"df -h",
"du -h --max-depth=1 /var/www 2>/dev/null | sort -h",
"du -h --max-depth=1 /var/log 2>/dev/null | sort -h",
"journalctl --disk-usage",
"sudo find /var/log -type f -name '*.gz' -size +50M -ls",
"sudo find /var/www -type f -name '*.log' -size +100M -ls",
"php artisan queue:prune-failed --hours=168",
],
notes=[
"Do not delete active log files blindly.",
"Prefer logrotate, journalctl vacuuming, old release cleanup, and Laravel failed-job pruning.",
"Check backups before removing large archives.",
],
),
Diagnostic(
key="env_compare",
title="Compare environment variables across servers",
patterns=[
r"\bcompare\b",
r"\benv\b",
r"\benvironment variables\b",
r"\bservers\b",
r"\bstaging\b",
r"\bproduction\b",
],
commands=[
"cd /var/www/YOUR_APP",
"grep -v '^#' .env | sort > /tmp/env.sorted",
"ssh other-server 'cd /var/www/YOUR_APP && grep -v \"^#\" .env | sort' > /tmp/env.other.sorted",
"diff -u /tmp/env.sorted /tmp/env.other.sorted",
],
notes=[
"Be careful not to paste secrets into logs or chat.",
"Compare keys first before comparing values.",
"Useful keys: APP_ENV, APP_DEBUG, DB_*, CACHE_*, QUEUE_*, REDIS_*, MAIL_*.",
],
),
Diagnostic(
key="laravel_queue",
title="Inspect Laravel queue workers and failed jobs",
patterns=[
r"\bqueue\b",
r"\bworker\b",
r"\bfailed job\b",
r"\bfailed_jobs\b",
r"\bjob\b",
],
commands=[
"cd /var/www/YOUR_APP",
"grep '^QUEUE_CONNECTION=' .env",
"php artisan queue:failed",
"php artisan queue:work --once -vvv",
"ps aux | grep 'queue:work' | grep -v grep",
"supervisorctl status",
"sudo systemctl status supervisor",
],
notes=[
"Run queue:work --once to test one job safely.",
"Check failed_jobs and worker process state.",
"If using Redis queues, verify Redis health too.",
],
),
Diagnostic(
key="horizon_supervisor",
title="Debug Laravel Horizon or Supervisor issues",
patterns=[
r"\bhorizon\b",
r"\bsupervisor\b",
r"\bsupervisorctl\b",
r"\bdaemon\b",
],
commands=[
"cd /var/www/YOUR_APP",
"php artisan horizon:status",
"php artisan horizon:supervisors",
"supervisorctl status",
"sudo systemctl status supervisor",
"sudo tail -n 100 /var/log/supervisor/supervisord.log",
"ls -lah /etc/supervisor/conf.d/",
],
notes=[
"Horizon requires Redis.",
"Supervisor may be running but the specific program may be stopped.",
"After deploys, use php artisan horizon:terminate for graceful restart.",
],
),
Diagnostic(
key="failed_cron",
title="Inspect failed cron jobs",
patterns=[
r"\bcron\b",
r"\bschedule\b",
r"\bscheduler\b",
r"\bfailed cron\b",
r"\bnot running\b",
],
commands=[
"crontab -l",
"sudo grep CRON /var/log/syslog | tail -n 100",
"sudo journalctl -u cron --since '1 hour ago'",
"cd /var/www/YOUR_APP && php artisan schedule:list",
"cd /var/www/YOUR_APP && php artisan schedule:run -vvv",
],
notes=[
"Laravel scheduler usually needs one cron entry running every minute.",
"Check cron user, working directory, PHP path, and permissions.",
],
),
Diagnostic(
key="safe_restart",
title="Restart Nginx, Apache, PHP-FPM, or MySQL safely",
patterns=[
r"\brestart\b",
r"\breload\b",
r"\bnginx\b",
r"\bapache\b",
r"\bphp-fpm\b",
r"\bmysql\b",
r"\bmariadb\b",
],
commands=[
"sudo nginx -t && sudo systemctl reload nginx",
"sudo apachectl configtest && sudo systemctl reload apache2",
"sudo systemctl status php*-fpm",
"sudo systemctl reload php*-fpm",
"sudo systemctl status mysql",
"sudo systemctl status mariadb",
],
notes=[
"Prefer reload over restart when supported.",
"Always test Nginx/Apache config before reloading.",
"Be careful restarting MySQL on production.",
],
),
]
def score_diagnostic(text: str, diagnostic: Diagnostic) -> int:
score = 0
for pattern in diagnostic.patterns:
if re.search(pattern, text, re.IGNORECASE):
score += 1
return score
def detect(text: str) -> list[tuple[int, Diagnostic]]:
results = []
for diagnostic in DIAGNOSTICS:
score = score_diagnostic(text, diagnostic)
if score > 0:
results.append((score, diagnostic))
return sorted(results, key=lambda item: item[0], reverse=True)
def print_diagnostic(score: int, diagnostic: Diagnostic) -> None:
print("=" * 72)
print(f"Detected: {diagnostic.title}")
print(f"Key: {diagnostic.key}")
print(f"Confidence score: {score}")
print()
print("Commands to inspect:")
for command in diagnostic.commands:
print(f" - {command}")
print()
print("What to look for:")
for note in diagnostic.notes:
print(f" - {note}")
print()
def main() -> None:
if len(sys.argv) < 2:
print("Usage:")
print(' python sysdiag.py "Laravel queue is not processing jobs"')
sys.exit(1)
text = " ".join(sys.argv[1:])
matches = detect(text)
if not matches:
print("No exact diagnostic matched.")
print("Try mentioning git, APP_ENV, cron, upload, Redis, disk, queue, Horizon, Supervisor, Nginx, Apache, PHP-FPM, or MySQL.")
sys.exit(0)
for score, diagnostic in matches[:3]:
print_diagnostic(score, diagnostic)
if __name__ == "__main__":
main()