Skip to content

Commit 6bfd474

Browse files
committed
[Fix] monitor: restore v1.6.6 ignore_inotify regex + ignore_paths ERE; issue #484
[Fix] _monitor_load_ignore_inotify_union: emit u:/d: prefixed tuples [Fix] monitor_cycle exclude-regex builder: per-entry semantic dispatch via _monitor_to_ere_entry — user entries raw ERE by default, literal: prefix opts into escape; defaults always escape [Fix] _monitor_filter_events: drop ignore-file arg; ignore_paths filtering moves to sibling grep -E -vf stage (ERE, matches scan-mode) [New] _monitor_to_ere_entry(): semantic dispatch helper [Change] CHANGELOG, CHANGELOG.RELEASE: v2.0.1 [Fix] + [New] entries [Change] tests/30-monitor-utils.bats: +10 tests (7 _monitor_to_ere_entry, 3 union: prefix); rewrite ERE pipe test; drop stale ignore-file arg from 5 sibling _monitor_filter_events tests [Change] tests/47-ignore-inotify-defaults.bats: update 4 existing union tests to expect u:/d: prefixed output (contract change per spec §11b edge case 3)
1 parent 4d78c6a commit 6bfd474

5 files changed

Lines changed: 203 additions & 50 deletions

File tree

CHANGELOG

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ v2.0.1 | Mar 25 2026:
8989

9090
-- Changes --
9191

92+
[New] _monitor_to_ere_entry(): semantic dispatch helper for ignore_inotify
93+
entry preparation (defaults-always-escape, user-raw-or-literal:)
9294
[Change] tests: prune two tautological assertions — the
9395
"uninstall.sh delegates service removal to pkg_service_uninstall"
9496
grep-for-token check in 01-install-cli.bats (string presence, not
@@ -232,6 +234,13 @@ v2.0.1 | Mar 25 2026:
232234

233235
-- Bug Fixes --
234236

237+
[Fix] monitor: restore v1.6.6 ERE semantics for ignore_inotify entries;
238+
introduce literal: per-entry prefix for opt-in escape of paths
239+
containing regex metachars; issue #484
240+
[Fix] monitor: ignore_paths now uses grep -E -vf (ERE) to match scan-mode
241+
semantics; fixes silent regression from awk index() substring
242+
filter introduced by monitor-mode redesign; issue #484
243+
235244
[Fix] monitor: ownership filters (scan_ignore_root/user/group) unconditionally
236245
excluded root-owned files in monitor mode; root-owned malware drops silently
237246
produced 0 quarantine hits despite detection; gated behind new

CHANGELOG.RELEASE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ v2.0.1 | Mar 25 2026:
8989

9090
-- Changes --
9191

92+
[New] _monitor_to_ere_entry(): semantic dispatch helper for ignore_inotify
93+
entry preparation (defaults-always-escape, user-raw-or-literal:)
9294
[Change] tests: prune two tautological assertions — the
9395
"uninstall.sh delegates service removal to pkg_service_uninstall"
9496
grep-for-token check in 01-install-cli.bats (string presence, not
@@ -232,6 +234,13 @@ v2.0.1 | Mar 25 2026:
232234

233235
-- Bug Fixes --
234236

237+
[Fix] monitor: restore v1.6.6 ERE semantics for ignore_inotify entries;
238+
introduce literal: per-entry prefix for opt-in escape of paths
239+
containing regex metachars; issue #484
240+
[Fix] monitor: ignore_paths now uses grep -E -vf (ERE) to match scan-mode
241+
semantics; fixes silent regression from awk index() substring
242+
filter introduced by monitor-mode redesign; issue #484
243+
235244
[Fix] monitor: ownership filters (scan_ignore_root/user/group) unconditionally
236245
excluded root-owned files in monitor mode; root-owned malware drops silently
237246
produced 0 quarantine hits despite detection; gated behind new

files/internals/lmd_monitor.sh

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -68,33 +68,39 @@ _monitor_escape_ere() {
6868
echo "$_str"
6969
}
7070

71+
_monitor_to_ere_entry() {
72+
# Semantic dispatch for ignore_inotify entry preparation.
73+
# $1=entry $2=source (user|defaults)
74+
# source=defaults → always escape (curated substrings; safe default)
75+
# source=user, literal: → strip prefix, escape remainder (opt-in literal mode)
76+
# source=user, no prefix → return entry unchanged (raw ERE; v1.6.6 semantics)
77+
local _entry="$1" _source="$2"
78+
case "$_source" in
79+
defaults) _monitor_escape_ere "$_entry" ;;
80+
user)
81+
case "$_entry" in
82+
literal:*)
83+
local _lit="${_entry#literal:}"
84+
[ -z "$_lit" ] && return 0 # empty after prefix — skip
85+
_monitor_escape_ere "$_lit"
86+
;;
87+
*) printf '%s\n' "$_entry" ;;
88+
esac
89+
;;
90+
*) return 1 ;; # unknown source — fail loud
91+
esac
92+
}
93+
7194
_monitor_filter_events() {
72-
# Filter inotify event lines from stdin. Extracts file paths from
73-
# CREATE/MODIFY/MOVED_TO events, deduplicates, and applies
74-
# substring-based ignore_paths filtering (matching current grep -vf
75-
# semantics). Outputs one path per line.
76-
local _ignore_file="$1"
77-
awk -v ignore_file="$_ignore_file" '
78-
BEGIN {
79-
if (ignore_file != "" && ignore_file != "/dev/null") {
80-
while ((getline line < ignore_file) > 0) {
81-
if (line != "") ign[line] = 1
82-
}
83-
close(ignore_file)
84-
}
85-
}
95+
# Extract + dedupe CREATE/MODIFY/MOVED_TO event paths from inotifywait
96+
# stdin. Ignore-path filtering is applied downstream via grep -E -vf
97+
# (see call site in monitor_cycle; matches scan-mode ERE semantics).
98+
awk '
8699
/ CREATE| MODIFY| MOVED_TO/ {
87-
# Match the exact inotifywait trailing metadata block:
88-
# [Space][Event(s)][Space][Day][Space][Month][Space][Time]$
89100
if (match($0, / (CREATE|MODIFY|MOVED_TO)[^ ]* [0-9][0-9]* [A-Za-z][A-Za-z]* [0-9:][0-9:]*$/)) {
90-
# Extract everything before the metadata block as the file path
91101
path = substr($0, 1, RSTART - 1)
92102
if (path == "" || seen[path]++) next
93-
skip = 0
94-
for (p in ign) {
95-
if (index(path, p) > 0) { skip = 1; break }
96-
}
97-
if (!skip) print path
103+
print path
98104
}
99105
}
100106
'
@@ -124,17 +130,22 @@ _monitor_append_extra_paths() {
124130
}
125131

126132
_monitor_load_ignore_inotify_union() {
127-
# Union user ignore_inotify + ignore_inotify.defaults. Skip blanks and
128-
# '#' comments, dedupe, emit one entry per line for downstream ERE
129-
# escaping. See issue #480 for the two-file defaults model.
130-
local _user="$1" _defaults="$2" _f _line
133+
# Union user ignore_inotify + ignore_inotify.defaults. Emit prefixed
134+
# tuples ("u:<entry>" / "d:<entry>") for downstream semantic dispatch.
135+
# Skip blanks and '#' comments; dedupe the combined prefixed stream.
136+
# See issue #480 (two-file defaults) and #484 (source-tag semantics).
137+
local _user="$1" _defaults="$2" _f _prefix _line
131138
for _f in "$_user" "$_defaults"; do
132139
[ -f "$_f" ] && [ -s "$_f" ] || continue
140+
case "$_f" in
141+
"$_user") _prefix="u:" ;;
142+
"$_defaults") _prefix="d:" ;;
143+
esac
133144
while IFS= read -r _line || [ -n "$_line" ]; do
134145
case "$_line" in
135146
''|\#*) continue ;;
136147
esac
137-
echo "$_line"
148+
printf '%s%s\n' "$_prefix" "$_line"
138149
done < "$_f"
139150
done | awk '!seen[$0]++'
140151
}
@@ -337,11 +348,17 @@ _monitor_cycle_tick() {
337348
fi
338349
_last_log_size="$_cur_size"
339350

340-
# Tier 1: event filtering
351+
# Tier 1: event filtering (extraction + dedup, then ERE ignore-path filter)
341352
local _event_list
342353
_event_list=$(mktemp "$tmpdir/.mon_events.XXXXXX")
343-
tlog_read "$inotify_log" "inotify" "$tmpdir" "bytes" | \
344-
_monitor_filter_events "$ignore_paths" > "$_event_list"
354+
if [ -s "$ignore_paths" ]; then
355+
tlog_read "$inotify_log" "inotify" "$tmpdir" "bytes" | \
356+
_monitor_filter_events | \
357+
grep -E -vf "$ignore_paths" > "$_event_list"
358+
else
359+
tlog_read "$inotify_log" "inotify" "$tmpdir" "bytes" | \
360+
_monitor_filter_events > "$_event_list"
361+
fi
345362

346363
local _event_count
347364
_event_count=$($wc -l < "$_event_list" 2>/dev/null) || _event_count=0
@@ -618,15 +635,23 @@ monitor_init() {
618635
_monitor_append_extra_paths "${monitor_paths_extra:-}" "$_inotify_fpaths"
619636

620637
# Build inotifywait --exclude regex — union ignore_inotify + defaults (issue #480)
638+
# Per-entry semantic dispatch via _monitor_to_ere_entry (issue #484)
621639
_inotify_exclude=()
622640
local _igregexp=""
623-
while IFS= read -r igfile; do
624-
local _escaped
625-
_escaped=$(_monitor_escape_ere "$igfile")
641+
local _src _ent _prepared
642+
while IFS= read -r _line; do
643+
_src="${_line:0:1}"
644+
_ent="${_line:2}"
645+
case "$_src" in
646+
u) _prepared=$(_monitor_to_ere_entry "$_ent" "user") ;;
647+
d) _prepared=$(_monitor_to_ere_entry "$_ent" "defaults") ;;
648+
*) continue ;;
649+
esac
650+
[ -z "$_prepared" ] && continue # literal: empty skip, etc.
626651
if [ -n "$_igregexp" ]; then
627-
_igregexp="$_igregexp|$_escaped"
652+
_igregexp="$_igregexp|$_prepared"
628653
else
629-
_igregexp="($_escaped"
654+
_igregexp="($_prepared"
630655
fi
631656
done < <(_monitor_load_ignore_inotify_union "$ignore_inotify" "${ignore_inotify_defaults:-}")
632657
if [ -n "$_igregexp" ]; then

tests/30-monitor-utils.bats

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,110 @@ _source_lmd_stack() {
140140
[ "$output" = "" ]
141141
}
142142

143+
# --- _monitor_to_ere_entry ---
144+
145+
# bats test_tags=monitor,unit
146+
@test "monitor: _monitor_to_ere_entry user entry without prefix returns raw ERE" {
147+
_source_lmd_stack
148+
run _monitor_to_ere_entry '^/tmp/.*scantem.*' 'user'
149+
[ "$status" -eq 0 ]
150+
[ "$output" = '^/tmp/.*scantem.*' ]
151+
}
152+
153+
# bats test_tags=monitor,unit
154+
@test "monitor: _monitor_to_ere_entry strips literal: prefix and escapes" {
155+
_source_lmd_stack
156+
run _monitor_to_ere_entry 'literal:/tmp/app.cache' 'user'
157+
[ "$status" -eq 0 ]
158+
[ "$output" = '/tmp/app\.cache' ]
159+
}
160+
161+
# bats test_tags=monitor,unit
162+
@test "monitor: _monitor_to_ere_entry defaults always escape regardless of content" {
163+
_source_lmd_stack
164+
run _monitor_to_ere_entry '/var/tmp/.mysql.sock' 'defaults'
165+
[ "$status" -eq 0 ]
166+
# Leading dots and the literal . in .sock both escaped.
167+
[[ "$output" == *'\.mysql\.sock'* ]]
168+
}
169+
170+
# bats test_tags=monitor,unit
171+
@test "monitor: _monitor_to_ere_entry user literal: with metachar-free path is idempotent after escape" {
172+
_source_lmd_stack
173+
run _monitor_to_ere_entry 'literal:/var/tmp/plain' 'user'
174+
[ "$status" -eq 0 ]
175+
[ "$output" = '/var/tmp/plain' ]
176+
}
177+
178+
# bats test_tags=monitor,unit
179+
@test "monitor: _monitor_to_ere_entry empty literal: after prefix skipped" {
180+
_source_lmd_stack
181+
run _monitor_to_ere_entry 'literal:' 'user'
182+
[ "$status" -eq 0 ]
183+
[ -z "$output" ]
184+
}
185+
186+
# bats test_tags=monitor,unit
187+
@test "monitor: _monitor_to_ere_entry uppercase LITERAL: not a special prefix (raw passthrough)" {
188+
_source_lmd_stack
189+
run _monitor_to_ere_entry 'LITERAL:/path' 'user'
190+
[ "$status" -eq 0 ]
191+
[ "$output" = 'LITERAL:/path' ]
192+
}
193+
194+
# bats test_tags=monitor,unit
195+
@test "monitor: _monitor_to_ere_entry defaults ignores literal: prefix (auto-escape always)" {
196+
_source_lmd_stack
197+
# Defensive: if a defaults entry were to start with "literal:",
198+
# defaults semantics still dominate (full escape, no prefix strip).
199+
run _monitor_to_ere_entry 'literal:/x' 'defaults'
200+
[ "$status" -eq 0 ]
201+
[[ "$output" == *'literal:/x'* ]] # prefix not stripped; entry fully escaped
202+
}
203+
204+
# --- _monitor_load_ignore_inotify_union — source-prefix output ---
205+
206+
# bats test_tags=monitor,unit
207+
@test "union: emits u: prefix for user entries" {
208+
_source_lmd_stack
209+
local tmpdir
210+
tmpdir=$(mktemp -d)
211+
printf '%s\n' '/user/one' > "$tmpdir/user"
212+
printf '' > "$tmpdir/defaults"
213+
run _monitor_load_ignore_inotify_union "$tmpdir/user" "$tmpdir/defaults"
214+
[ "$status" -eq 0 ]
215+
[ "$output" = 'u:/user/one' ]
216+
rm -rf "$tmpdir"
217+
}
218+
219+
# bats test_tags=monitor,unit
220+
@test "union: emits d: prefix for defaults entries" {
221+
_source_lmd_stack
222+
local tmpdir
223+
tmpdir=$(mktemp -d)
224+
printf '' > "$tmpdir/user"
225+
printf '%s\n' 'sql-temptable-' > "$tmpdir/defaults"
226+
run _monitor_load_ignore_inotify_union "$tmpdir/user" "$tmpdir/defaults"
227+
[ "$status" -eq 0 ]
228+
[ "$output" = 'd:sql-temptable-' ]
229+
rm -rf "$tmpdir"
230+
}
231+
232+
# bats test_tags=monitor,unit
233+
@test "union: preserves user and defaults as distinct tuples even when entry matches" {
234+
_source_lmd_stack
235+
local tmpdir
236+
tmpdir=$(mktemp -d)
237+
printf '%s\n' '/var/tmp/clamav-' > "$tmpdir/user"
238+
printf '%s\n' '/var/tmp/clamav-' > "$tmpdir/defaults"
239+
run _monitor_load_ignore_inotify_union "$tmpdir/user" "$tmpdir/defaults"
240+
[ "$status" -eq 0 ]
241+
# Both tuples survive — harmless redundancy in the OR-regex.
242+
echo "$output" | grep -qxF 'u:/var/tmp/clamav-'
243+
echo "$output" | grep -qxF 'd:/var/tmp/clamav-'
244+
rm -rf "$tmpdir"
245+
}
246+
143247
# --- _monitor_filter_events ---
144248

145249
# bats test_tags=monitor,unit
@@ -149,7 +253,7 @@ _source_lmd_stack() {
149253
tmpdir=$(mktemp -d)
150254
echo "/home/user/public_html/shell.php CREATE 18 Mar 10:30:01" > "$tmpdir/events"
151255
printf '' > "$tmpdir/ignore"
152-
run _monitor_filter_events "$tmpdir/ignore" < "$tmpdir/events"
256+
run _monitor_filter_events < "$tmpdir/events"
153257
[ "$status" -eq 0 ]
154258
[ "$output" = "/home/user/public_html/shell.php" ]
155259
rm -rf "$tmpdir"
@@ -166,15 +270,15 @@ _source_lmd_stack() {
166270
/home/user/file.php CREATE 18 Mar 10:30:03
167271
EVENTS
168272
printf '' > "$tmpdir/ignore"
169-
run _monitor_filter_events "$tmpdir/ignore" < "$tmpdir/events"
273+
run _monitor_filter_events < "$tmpdir/events"
170274
[ "$status" -eq 0 ]
171275
[ "$(echo "$output" | wc -l)" -eq 1 ]
172276
[ "$output" = "/home/user/file.php" ]
173277
rm -rf "$tmpdir"
174278
}
175279

176280
# bats test_tags=monitor,unit
177-
@test "monitor: _monitor_filter_events applies ignore_paths substring match" {
281+
@test "monitor: _monitor_filter_events + ignore_paths ERE filter pipe" {
178282
_source_lmd_stack
179283
local tmpdir
180284
tmpdir=$(mktemp -d)
@@ -183,8 +287,13 @@ EVENTS
183287
/home/user/.cache/bad.tmp CREATE 18 Mar 10:30:02
184288
/home/user/public_html/ok.js MODIFY 18 Mar 10:30:03
185289
EVENTS
186-
echo "/home/user/.cache" > "$tmpdir/ignore"
187-
run _monitor_filter_events "$tmpdir/ignore" < "$tmpdir/events"
290+
# New test exercises the two-stage pipe: extraction (no ignore arg)
291+
# then ERE filter via grep -E -vf. Fixture uses escaped dot to assert
292+
# ERE semantics of the new filter stage.
293+
# export -f required so bash -c subshell can call the function.
294+
printf '/home/user/\\.cache\n' > "$tmpdir/ignore"
295+
export -f _monitor_filter_events
296+
run bash -c '_monitor_filter_events < "$1" | grep -E -vf "$2"' _ "$tmpdir/events" "$tmpdir/ignore"
188297
[ "$status" -eq 0 ]
189298
[ "$(echo "$output" | wc -l)" -eq 2 ]
190299
echo "$output" | grep -q "good.php"
@@ -204,7 +313,7 @@ EVENTS
204313
/home/user/new.php CREATE 18 Mar 10:30:03
205314
EVENTS
206315
printf '' > "$tmpdir/ignore"
207-
run _monitor_filter_events "$tmpdir/ignore" < "$tmpdir/events"
316+
run _monitor_filter_events < "$tmpdir/events"
208317
[ "$status" -eq 0 ]
209318
[ "$output" = "/home/user/new.php" ]
210319
rm -rf "$tmpdir"
@@ -217,7 +326,7 @@ EVENTS
217326
tmpdir=$(mktemp -d)
218327
echo "/home/user/uploaded.php MOVED_TO 18 Mar 10:30:01" > "$tmpdir/events"
219328
printf '' > "$tmpdir/ignore"
220-
run _monitor_filter_events "$tmpdir/ignore" < "$tmpdir/events"
329+
run _monitor_filter_events < "$tmpdir/events"
221330
[ "$status" -eq 0 ]
222331
[ "$output" = "/home/user/uploaded.php" ]
223332
rm -rf "$tmpdir"
@@ -230,7 +339,7 @@ EVENTS
230339
tmpdir=$(mktemp -d)
231340
printf '' > "$tmpdir/events"
232341
printf '' > "$tmpdir/ignore"
233-
run _monitor_filter_events "$tmpdir/ignore" < "$tmpdir/events"
342+
run _monitor_filter_events < "$tmpdir/events"
234343
[ "$status" -eq 0 ]
235344
[ -z "$output" ]
236345
rm -rf "$tmpdir"

0 commit comments

Comments
 (0)