-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcavlite
More file actions
executable file
·400 lines (332 loc) · 12.9 KB
/
cavlite
File metadata and controls
executable file
·400 lines (332 loc) · 12.9 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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
#!/bin/bash
VERSION="0.2.4"
# Setup
# --- CONFIGURATION START ---
# Default values
WEBHOOK_URL=""
SERVER_NAME=$(hostname -s)
# Load configuration
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# 1. Load system config (Production)
if [ -f "/etc/cavlite/cavlite.conf" ]; then
source "/etc/cavlite/cavlite.conf"
fi
# 2. Load local config (Development - Overrides system)
if [ -f "$SCRIPT_DIR/config/cavlite.conf" ]; then
source "$SCRIPT_DIR/config/cavlite.conf"
fi
# 3. Load test config (Test Runner - Overrides all)
if [ -f "$SCRIPT_DIR/test/test.cavlite.conf" ]; then
source "$SCRIPT_DIR/test/test.cavlite.conf"
fi
# Set defaults if not provided in config
: "${SCAN_PATH:=/}"
: "${QUARANTINE_DIR:=/var/quarantine}"
: "${CLAMD_TIMEOUT:=300}"
# Paths
: "${LOG_FILE:=/var/log/cavlite}"
LOG_DIR="$LOG_FILE"
LOG_FILE="$LOG_DIR/$(date +%A | tr '[:upper:]' '[:lower:]').log"
PID_FILE="/var/run/cavlite.pid"
STATE_FILE="/var/run/cavlite.logpath"
if [ -f "$SCRIPT_DIR/lib/filter" ]; then
CLEANER_SCRIPT="$SCRIPT_DIR/lib/filter"
else
CLEANER_SCRIPT="/usr/local/lib/cavlite/filter"
fi
CLAMD_CONF="/etc/clamav/clamd.conf"
# --- CONFIGURATION END ---
function check_root() {
if [ "$EUID" -ne 0 ]; then
echo "❌ Error: This operation requires root privileges. Please run as root."
exit 1
fi
}
function show_version() {
echo "cavlite version $VERSION"
}
function cleanup_clamd_config() {
if [ -f "$CLAMD_CONF" ]; then
# Check if our block exists
if grep -q "### CAVlite Start" "$CLAMD_CONF"; then
echo "🧹 Cleaning up cavlite configuration from $CLAMD_CONF..." | tee -a "$LOG_FILE"
# Remove the block from Start to End
sed -i '/### CAVlite Start/,/### CAVlite END/d' "$CLAMD_CONF"
# Restart daemon to apply cleanup - REMOVED to prevent double restarts/hangs
# The daemon is either restarted in update_clamd_config or stopped at end of scan.
# systemctl stop clamav-daemon.socket 2>/dev/null
# systemctl restart clamav-daemon
fi
fi
}
function update_clamd_config() {
cleanup_clamd_config # Ensure clean slate
if [ -n "${EXCLUDE_PATHS+x}" ]; then
echo "⚙️ Injecting exclusions into $CLAMD_CONF..." | tee -a "$LOG_FILE"
{
echo "### CAVlite Start"
for path in "${EXCLUDE_PATHS[@]}"; do
# Escape special characters for regex if necessary, but simple paths are usually safe
# ExcludePath expects a regex
echo "ExcludePath $path"
done
echo "### CAVlite END"
} >> "$CLAMD_CONF"
# Restart daemon to apply changes
echo "🔄 Restarting clamav-daemon to apply configuration..." | tee -a "$LOG_FILE"
systemctl stop clamav-daemon.socket 2>/dev/null
systemctl restart clamav-daemon
fi
}
function wait_for_clamd() {
echo "⏳ Waiting for ClamAV daemon to load signatures into memory (timeout: ${CLAMD_TIMEOUT}s)..." | tee -a "$LOG_FILE"
local sleep_interval=2
local max_retries=$((CLAMD_TIMEOUT / sleep_interval))
for ((i=1; i<=max_retries; i++)); do
# --ping 1 successfully asks the daemon if it's ready
if clamdscan --ping 1 > /dev/null 2>&1; then
echo "✅ ClamAV daemon is fully ready." | tee -a "$LOG_FILE"
return 0
fi
sleep $sleep_interval
done
echo "⚠️ Warning: ClamAV daemon did not respond to ping after ${CLAMD_TIMEOUT} seconds. Scan might fail." | tee -a "$LOG_FILE"
return 1
}
function send_notification() {
local content="$1"
local file_to_attach="$2"
if [ -z "$WEBHOOK_URL" ]; then
echo "⚠️ Warning: WEBHOOK_URL is not set. Skipping notification."
return
fi
read -r -d '' PAYLOAD <<EOF
{
"content": "$(echo -e "$content" | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g')"
}
EOF
if [[ -n "$file_to_attach" && -f "$file_to_attach" ]]; then
curl -s -X POST "$WEBHOOK_URL" -F "payload_json=$PAYLOAD" -F "file=@$file_to_attach"
else
curl -s -X POST "$WEBHOOK_URL" -H "Content-Type: application/json" -d "$PAYLOAD"
fi
}
function check_discord() {
check_root
echo "🔍 Checking Discord Webhook configuration..."
if [ -z "$WEBHOOK_URL" ]; then
echo "❌ Error: WEBHOOK_URL is not configured in /etc/cavlite/cavlite.conf"
exit 1
fi
echo "📨 Sending test notification..."
send_notification "✅ **Test Notification**: cavlite webhook is working correctly on $SERVER_NAME."
echo "✅ Test notification sent. Please check your Discord channel."
}
function start_scan() {
check_root
if [ -f "$PID_FILE" ]; then
echo "Scan is already in progress (PID: $(cat $PID_FILE)). Exiting."
exit 1
fi
# Create log directory
mkdir -p "$LOG_DIR"
# Log rotation is handled by day-based filenames (overwrites previous week)
echo $$ >"$PID_FILE"
echo "$LOG_FILE" > "$STATE_FILE"
trap 'cleanup_clamd_config; rm -f "$PID_FILE" "$STATE_FILE"; systemctl stop clamav-daemon &>/dev/null' EXIT
# Start logging
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "🔐 Security Audit Started - $TIMESTAMP on $SERVER_NAME" | tee "$LOG_FILE"
echo "📂 Scanning Path: $SCAN_PATH" | tee -a "$LOG_FILE"
if [ -n "${EXCLUDE_PATHS+x}" ]; then
echo "🚫 Excluded Paths (Configured in $CLAMD_CONF): ${EXCLUDE_PATHS[*]}" | tee -a "$LOG_FILE"
fi
# Initialize exit codes
CLAM_EXIT=0
LYNIS_EXIT=0
# --- CLAMAV SCAN ---
if [[ "${CLAMDSCAN}" == "True" ]]; then
echo "Running ClamAV scan on $SERVER_NAME..." >> "$LOG_FILE"
echo "🔍 Calculating files to scan..." | tee -a "$LOG_FILE"
# Stop ClamAV temporarily and clear cache to free up RAM for the file count
systemctl stop clamav-daemon 2>/dev/null
sync; echo 3 > /proc/sys/vm/drop_caches
if [ ${#EXCLUDE_PATHS[@]} -eq 0 ]; then
FILE_COUNT=$(find "$SCAN_PATH" -type f 2>/dev/null | wc -l)
else
COMBINED_REGEX=$(IFS="|"; echo "${EXCLUDE_PATHS[*]}")
FILE_COUNT=$(find "$SCAN_PATH" -type f 2>/dev/null | grep -vE "$COMBINED_REGEX" | wc -l)
fi
echo "📄 Found $FILE_COUNT files to scan." | tee -a "$LOG_FILE"
# Inject configuration
update_clamd_config
# Ensure daemon is running
if ! systemctl is-active --quiet clamav-daemon; then
echo "🚀 Starting clamav-daemon..." | tee -a "$LOG_FILE"
systemctl start clamav-daemon
fi
# Wait for socket
wait_for_clamd
mkdir -p "$QUARANTINE_DIR"
# Run clamdscan
clamdscan --multiscan --fdpass "$SCAN_PATH" --move="$QUARANTINE_DIR" --stdout 2>&1 | tee -a "$LOG_FILE" || true
CLAM_EXIT=${PIPESTATUS[0]}
else
echo "ℹ️ ClamAV scan skipped (Disabled in config)" | tee -a "$LOG_FILE"
fi
# --- LYNIS AUDIT ---
if [[ "${LYNIS}" == "True" ]]; then
echo -e "\nRunning Lynis audit..." >> "$LOG_FILE"
lynis audit system >> "$LOG_FILE"
LYNIS_EXIT=${PIPESTATUS[0]}
else
echo -e "\nℹ️ Lynis audit skipped (Disabled in config)" >> "$LOG_FILE"
fi
echo -e "\nCleaning up log file..." >> "$LOG_FILE"
CLEANED_LOG_FILE=$(python3 "$CLEANER_SCRIPT" "$LOG_FILE")
if [[ -n "$CLEANED_LOG_FILE" && -f "$CLEANED_LOG_FILE" ]]; then
CLEANED_CONTENT=$(cat "$CLEANED_LOG_FILE")
else
if [[ $CLAM_EXIT -eq 0 && $LYNIS_EXIT -eq 0 ]]; then
CLEANED_CONTENT="✅ Security audit completed successfully on $SERVER_NAME."
else
CLEANED_CONTENT="⚠️ Security audit completed with issues on $SERVER_NAME."
fi
fi
# Cleanup config immediately after scan
cleanup_clamd_config
# Send the main notification
send_notification "$CLEANED_CONTENT" "$LOG_FILE"
# --- MARKER: THIS PREVENTS DOUBLE NOTIFICATIONS ---
echo "### SCAN_FINISHED_NOTIFICATION_SENT ###" >> "$LOG_FILE"
echo "Stopping clamav-daemon..." >> "$LOG_FILE"
systemctl stop clamav-daemon
echo "✅ Scan completed. Logs available at $LOG_FILE"
}
function stop_scan() {
check_root
# Attempt to load the correctly dated log file from the running process state
if [ -f "$STATE_FILE" ]; then
LOG_FILE=$(cat "$STATE_FILE")
fi
# Ensure config is cleaned up even on stop
cleanup_clamd_config
# --- UPDATED LOGIC TO PREVENT DOUBLE NOTIFICATION ---
# 1. Check if the SUCCESS notification marker exists
# 2. OR Check if we are in the cleaning up phase
if [ -f "$LOG_FILE" ]; then
if grep -q "### SCAN_FINISHED_NOTIFICATION_SENT ###" "$LOG_FILE"; then
echo "Scan finished and notification was already sent. Stopping silently..."
SILENT_STOP=true
elif grep -q "Cleaning up log file..." "$LOG_FILE"; then
# If it's cleaning up but hasn't sent the notification yet, we might want to wait,
# but to be safe let's treat it as 'almost done' and kill silently to avoid spam.
echo "Scan is in final cleanup phase. Stopping silently..."
SILENT_STOP=true
else
SILENT_STOP=false
fi
else
SILENT_STOP=false
fi
if [ "$SILENT_STOP" = true ]; then
systemctl stop clamav-daemon.socket 2>/dev/null
systemctl stop clamav-daemon 2>/dev/null
pkill -9 clamdscan 2>/dev/null
pkill -9 clamd 2>/dev/null
pkill -9 lynis 2>/dev/null
if [ -f "$PID_FILE" ]; then
kill "$(cat "$PID_FILE")" 2>/dev/null
rm -f "$PID_FILE" "$STATE_FILE"
fi
exit 0
fi
# ----------------------------------------
echo "Stopping security scan..."
# 1. STOP SYSTEMD SERVICES FIRST (Prevent restart loop)
# Important: Stop the socket too, otherwise it can auto-activate the service
echo "Stopping clamav-daemon service and socket..."
systemctl stop clamav-daemon.socket
systemctl stop clamav-daemon
# 2. KILL CLAMSCAN / CLAMDSCAN
if pgrep "clamdscan" > /dev/null; then
echo "Found running clamdscan process. Killing it..."
pkill -9 clamdscan
fi
# 3. KILL CLAMD (Daemon may be stuck)
if pgrep "clamd" > /dev/null; then
echo "Found running clamd process. Killing it..."
pkill -9 clamd
fi
# 4. KILL LYNIS
if pgrep "lynis" > /dev/null; then
echo "Found running Lynis process. Killing it..."
pkill -9 lynis
fi
# 3. Stop the daemon
if systemctl is-active --quiet clamav-daemon; then
echo "Stopping clamav-daemon..."
systemctl stop clamav-daemon
fi
# 4. Kill the script PID
if [ -f "$PID_FILE" ]; then
SCAN_PID=$(cat "$PID_FILE")
if ps -p "$SCAN_PID" >/dev/null; then
echo "Terminating script manager (PID: $SCAN_PID)..."
kill "$SCAN_PID"
fi
rm -f "$PID_FILE" "$STATE_FILE"
fi
# 5. Send Interrupted Notification
echo "Waiting for logs to finalize..."
sleep 3
if [ -f "$LOG_FILE" ]; then
# Double check marker one last time in case it finished inside the 3 second sleep
if grep -q "### SCAN_FINISHED_NOTIFICATION_SENT ###" "$LOG_FILE"; then
echo "Scan finished during stop process. Skipping interruption alert."
exit 0
fi
echo "Processing partial log file..."
echo -e "\n\n--- SCAN INTERRUPTED MANUALLY ---" >> "$LOG_FILE"
CLEANED_LOG_FILE=$(python3 "$CLEANER_SCRIPT" "$LOG_FILE")
if [[ -n "$CLEANED_LOG_FILE" && -f "$CLEANED_LOG_FILE" ]]; then
CLEANED_CONTENT=$(cat "$CLEANED_LOG_FILE")
NOTIFICATION_MSG="🟡 **Scan Interrupted on $SERVER_NAME**\nThe security scan did not complete. Here is a summary of the partial results:\n\n$CLEANED_CONTENT"
send_notification "$NOTIFICATION_MSG" "$LOG_FILE"
else
send_notification "🟡 **Scan Interrupted on $SERVER_NAME**\nThe security scan was stopped, but no summary could be generated." "$LOG_FILE"
fi
else
send_notification "🟡 **Scan Interrupted on $SERVER_NAME**\nThe security scan was stopped, but no log file was found to process."
fi
echo "Scan stopped."
}
function show_help() {
echo "Usage: $0 {--start|--stop|--check-discord|--version|--help|-h}"
echo " --start Start the security scan (ClamAV and Lynis)."
echo " --stop Stop any running security scan."
echo " --check-discord Test Discord webhook configuration."
echo " --version, --v Show version information."
echo " --help, -h Display this help message."
}
case "$1" in
--start)
start_scan
;;
--stop)
stop_scan
;;
--check-discord)
check_discord
;;
--version|--v)
show_version
;;
--help|-h)
show_help
;;
*)
show_help
exit 1
;;
esac