2424# Add parent directory to path so we can import shared modules
2525sys .path .insert (0 , os .path .join (os .path .dirname (__file__ ), ".." ))
2626from shared .alert_dispatcher import dispatch_alert_async
27+ from shared .user_rules import get_custom_rules , USER_RULES_PATH
2728
2829logger = logging .getLogger ("clawedr.log_tailer" )
2930
@@ -47,14 +48,36 @@ def _load_policy_rule_index() -> dict:
4748 """Load compiled_policy.json to allow cross-referencing sandbox violations."""
4849 try :
4950 with open (POLICY_PATH ) as f :
50- return json .load (f )
51+ policy = json .load (f )
52+
53+ # Merge in custom user rules to the runtime mapping payload
54+ custom_rules = get_custom_rules ()
55+ for rule in custom_rules :
56+ rid = rule .get ("id" , "" )
57+ rtype = rule .get ("type" , "" )
58+ val = rule .get ("value" , "" )
59+ plat = rule .get ("platform" , "both" )
60+ if plat not in ("both" , "macos" ) or not rid :
61+ continue
62+
63+ if rtype == "executable" :
64+ policy .setdefault ("blocked_executables" , {})[rid ] = val
65+ elif rtype == "domain" :
66+ policy .setdefault ("blocked_domains" , {})[rid ] = val
67+ elif rtype == "path" :
68+ policy .setdefault ("blocked_paths" , {}).setdefault ("macos" , {})[rid ] = val
69+ elif rtype == "argument" :
70+ policy .setdefault ("deny_rules" , {}).setdefault ("macos" , {})[rid ] = val
71+
72+ return policy
5173 except (FileNotFoundError , json .JSONDecodeError ) as exc :
5274 logger .warning ("Could not load policy for rule index: %s" , exc )
5375 return {}
5476
5577
5678def tail_sandbox_log ():
5779 """Poll macOS sandbox violation events from the Unified Log."""
80+ last_mtime = 0.0
5881 rule_index = _load_policy_rule_index ()
5982 seen_events = set ()
6083 logger .info ("Starting log show polling for sandbox reporting..." )
@@ -64,6 +87,12 @@ def tail_sandbox_log():
6487
6588 while True :
6689 try :
90+ # Refresh rule index if policy or user rules changed
91+ changed , last_mtime = check_for_policy_update (last_mtime )
92+ if changed :
93+ logger .info ("Policy or user rules updated, reloading rule index..." )
94+ rule_index = _load_policy_rule_index ()
95+
6796 # Check the last 15 seconds to overlap and avoid missing events
6897 start_time_str = (datetime .now () - timedelta (seconds = 15 )).strftime ("%Y-%m-%d %H:%M:%S" )
6998 cmd = [
@@ -135,12 +164,24 @@ def tail_sandbox_log():
135164
136165
137166def check_for_policy_update (last_mtime : float ) -> tuple [bool , float ]:
138- """Check if the policy file has been updated since last_mtime."""
139- try :
140- mtime = os .path .getmtime (POLICY_PATH )
141- return (mtime != last_mtime , mtime )
142- except FileNotFoundError :
167+ """Check if the system policy or user rules have been updated since last_mtime."""
168+ mtimes = []
169+
170+ for path in (POLICY_PATH , USER_RULES_PATH ):
171+ try :
172+ mtimes .append (os .path .getmtime (path ))
173+ except FileNotFoundError :
174+ pass
175+
176+ if not mtimes :
143177 return (False , last_mtime )
178+
179+ current_max_mtime = max (mtimes )
180+
181+ if current_max_mtime != last_mtime :
182+ return (True , current_max_mtime )
183+
184+ return (False , last_mtime )
144185
145186
146187def notify_user (message : str ) -> None :
0 commit comments