Skip to content

Commit e318ed5

Browse files
committed
feat: hourly rule updates from GitHub registry, Settings tab, macOS restart banner
Made-with: Cursor
1 parent 0a243c6 commit e318ed5

File tree

6 files changed

+510
-1
lines changed

6 files changed

+510
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ Open [http://localhost:8477](http://localhost:8477) in your browser.
270270
- **Platform filtering** — Auto-detects OS and shows only relevant rules. Pill bar to switch between This Mac / All Platforms / macOS / Linux.
271271
- **Search** — Filter rules by Rule ID, value, or category.
272272
- **Sessions dropdown** — Shows active OpenClaw instances being monitored.
273+
- **Settings tab** — Toggle auto-check for rule updates (hourly). Manual "Check for updates now" button.
274+
- **Rule updates** — Agents query the GitHub registry hourly for updated threat signatures. On **Linux**, new rules are downloaded and enforced automatically (monitor hot-reloads). On **macOS**, a banner shows the number of changes and prompts to restart OpenClaw to enforce.
273275

274276
### API Endpoints
275277

@@ -285,6 +287,10 @@ Open [http://localhost:8477](http://localhost:8477) in your browser.
285287
| `PUT` | `/api/custom-rules/{id}` | Update a custom rule's value or platform |
286288
| `DELETE` | `/api/custom-rules/{id}` | Delete a custom rule |
287289
| `GET` | `/api/sessions` | Active OpenClaw sessions |
290+
| `GET` | `/api/settings` | Dashboard settings (auto-update toggle) |
291+
| `POST` | `/api/settings` | Update settings |
292+
| `GET` | `/api/updates` | Check for rule updates from registry |
293+
| `POST` | `/api/updates/apply` | Download and apply updates |
288294

289295
## User Rules
290296

deploy/dashboard/app.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,17 @@
3535
load_user_rules, save_user_rules, USER_RULES_PATH,
3636
add_custom_rule, update_custom_rule, delete_custom_rule,
3737
get_custom_rules, get_custom_rule_metadata, CUSTOM_RULE_TYPES,
38+
load_settings, save_settings,
3839
)
40+
from shared.rule_updater import check_for_updates, download_and_apply
3941

4042
logger = logging.getLogger("clawedr.dashboard")
4143

4244
app = FastAPI(title="ClawEDR Dashboard", version="1.0.0")
4345

46+
# Cached result from background update check (macOS banner)
47+
_pending_updates: dict | None = None
48+
4449
POLICY_PATH = os.environ.get(
4550
"CLAWEDR_POLICY_PATH", "/usr/local/share/clawedr/compiled_policy.json"
4651
)
@@ -517,6 +522,64 @@ async def get_status():
517522
return JSONResponse(status)
518523

519524

525+
# ---------------------------------------------------------------------------
526+
# Settings & Rule Updates
527+
# ---------------------------------------------------------------------------
528+
529+
@app.get("/api/settings")
530+
async def get_settings():
531+
"""Return dashboard settings (auto-update toggle, etc.)."""
532+
return JSONResponse(load_settings())
533+
534+
535+
@app.post("/api/settings")
536+
async def post_settings(request: Request):
537+
"""Update dashboard settings."""
538+
try:
539+
body = await request.json()
540+
settings = load_settings()
541+
if "auto_update_rules" in body:
542+
settings["auto_update_rules"] = bool(body["auto_update_rules"])
543+
save_settings(settings)
544+
return JSONResponse({"status": "ok", "settings": settings})
545+
except Exception as exc:
546+
return JSONResponse({"error": str(exc)}, status_code=400)
547+
548+
549+
@app.get("/api/updates")
550+
async def get_updates():
551+
"""Check for rule updates from the registry. Returns has_updates, change_count, etc."""
552+
global _pending_updates
553+
result = check_for_updates()
554+
if result.get("has_updates"):
555+
_pending_updates = result
556+
else:
557+
_pending_updates = None
558+
return JSONResponse(result)
559+
560+
561+
@app.get("/api/updates/cached")
562+
async def get_updates_cached():
563+
"""Return cached update result from background check (for lightweight polling)."""
564+
return JSONResponse(_pending_updates or {"has_updates": False})
565+
566+
567+
@app.post("/api/updates/apply")
568+
async def apply_updates():
569+
"""Download and apply rule updates. Linux: hot-reload. macOS: replace files, user must restart."""
570+
ok, msg = download_and_apply()
571+
if ok:
572+
global _pending_updates
573+
_pending_updates = None
574+
# Update last_check timestamp
575+
settings = load_settings()
576+
from datetime import datetime
577+
settings["last_update_check"] = datetime.utcnow().isoformat() + "Z"
578+
save_settings(settings)
579+
return JSONResponse({"status": "ok", "message": msg})
580+
return JSONResponse({"error": msg}, status_code=500)
581+
582+
520583
@app.get("/api/sessions")
521584
async def get_sessions():
522585
"""Return active OpenClaw sessions."""
@@ -574,8 +637,47 @@ async def dashboard():
574637
return HTMLResponse("<h1>ClawEDR Dashboard</h1><p>Template not found.</p>")
575638

576639

640+
def _run_update_check():
641+
"""Background task: hourly check for rule updates."""
642+
import platform
643+
import time
644+
global _pending_updates
645+
CHECK_INTERVAL = 3600 # 1 hour
646+
time.sleep(60) # Defer first check to avoid startup load
647+
while True:
648+
try:
649+
settings = load_settings()
650+
if not settings.get("auto_update_rules", True):
651+
continue
652+
result = check_for_updates()
653+
if result.get("error"):
654+
continue
655+
if not result.get("has_updates"):
656+
_pending_updates = None
657+
continue
658+
if platform.system() == "Linux":
659+
ok, msg = download_and_apply()
660+
if ok:
661+
logger.info("Auto-applied rule update: %s", msg)
662+
_pending_updates = None
663+
settings = load_settings()
664+
from datetime import datetime
665+
settings["last_update_check"] = datetime.utcnow().isoformat() + "Z"
666+
save_settings(settings)
667+
else:
668+
# macOS: cache for banner, user must restart
669+
_pending_updates = result
670+
logger.info("Rule updates available (%d changes). Restart OpenClaw to enforce.", result.get("change_count", 0))
671+
except Exception as e:
672+
logger.exception("Update check failed: %s", e)
673+
time.sleep(CHECK_INTERVAL)
674+
675+
577676
def main():
677+
import threading
578678
import uvicorn
679+
t = threading.Thread(target=_run_update_check, daemon=True)
680+
t.start()
579681
port = int(os.environ.get("CLAWEDR_DASHBOARD_PORT", "8477"))
580682
logger.info("Starting ClawEDR Dashboard on http://localhost:%d", port)
581683
uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")

deploy/dashboard/templates/index.html

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,35 @@
12081208
padding: 0;
12091209
display: flex;
12101210
}
1211+
1212+
/* Update banner (macOS: new rules available) */
1213+
.update-banner {
1214+
position: fixed;
1215+
top: 0;
1216+
left: 0;
1217+
right: 0;
1218+
background: linear-gradient(135deg, var(--warning) 0%, #f59e0b 100%);
1219+
color: #1a1a1a;
1220+
padding: 12px 24px;
1221+
z-index: 500;
1222+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1223+
}
1224+
.update-banner-content {
1225+
display: flex;
1226+
align-items: center;
1227+
justify-content: center;
1228+
gap: 16px;
1229+
flex-wrap: wrap;
1230+
}
1231+
.update-banner-content .btn-primary {
1232+
background: #1a1a1a;
1233+
color: white;
1234+
border-color: #1a1a1a;
1235+
}
1236+
.update-banner-content .btn-primary:hover {
1237+
background: #333;
1238+
color: white;
1239+
}
12111240
</style>
12121241
</head>
12131242

@@ -1251,6 +1280,15 @@ <h1>ClawEDR <span>Dashboard</span></h1>
12511280
</svg>
12521281
Policy Rules
12531282
</button>
1283+
<button class="tab sidebar-link" data-tab="settings"
1284+
style="text-align: left; display: flex; align-items: center; gap: 12px; padding: 10px 16px;">
1285+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
1286+
stroke-linecap="round" stroke-linejoin="round">
1287+
<circle cx="12" cy="12" r="3"></circle>
1288+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
1289+
</svg>
1290+
Settings
1291+
</button>
12541292
</nav>
12551293

12561294
<div class="sidebar-footer" style="margin-top: 24px; display: flex; flex-direction: column; gap: 12px;">
@@ -1419,6 +1457,40 @@ <h3 style="font-size: 15px; font-weight: 600;">Alert Details</h3>
14191457
</div>
14201458
</div>
14211459

1460+
<!-- Settings Tab -->
1461+
<div class="tab-panel" id="tab-settings">
1462+
<div class="card">
1463+
<div class="card-header">
1464+
<span class="card-title">Rule Updates</span>
1465+
</div>
1466+
<p style="color:var(--text-muted);font-size:12px;margin-bottom:16px;">
1467+
ClawEDR checks the GitHub registry every hour for updated threat signatures/rules.
1468+
When enabled, updates are applied automatically on Linux. On macOS, you'll see a banner
1469+
and must restart OpenClaw to enforce new rules.
1470+
</p>
1471+
<div class="form-group">
1472+
<label class="form-label" style="display:flex;align-items:center;gap:12px;cursor:pointer;">
1473+
<input type="checkbox" id="autoUpdateToggle" checked onchange="toggleAutoUpdate(this.checked)">
1474+
<span>Auto-check for rule updates (hourly)</span>
1475+
</label>
1476+
</div>
1477+
<div style="display:flex;gap:12px;margin-top:16px;">
1478+
<button class="btn btn-primary" onclick="checkForUpdates()" id="checkUpdatesBtn">
1479+
Check for updates now
1480+
</button>
1481+
<span id="lastCheckLabel" style="font-size:12px;color:var(--text-muted);align-self:center;"></span>
1482+
</div>
1483+
</div>
1484+
</div>
1485+
1486+
<!-- Update banner (macOS: when updates available, show count + restart message) -->
1487+
<div class="update-banner" id="updateBanner" style="display:none;">
1488+
<div class="update-banner-content">
1489+
<span id="updateBannerText"></span>
1490+
<button class="btn btn-primary btn-sm" onclick="applyUpdates()" id="applyUpdatesBtn">Download & apply</button>
1491+
</div>
1492+
</div>
1493+
14221494
<div class="footer">ClawEDR v2.0 — Kernel-level endpoint detection for AI agents</div>
14231495
</main>
14241496
</div>
@@ -2071,11 +2143,100 @@ <h2 id="modalTitle">Add Custom Rule</h2>
20712143
renderAlerts(activeDisplayedAlerts);
20722144
}
20732145

2146+
// ── Settings & Updates ──
2147+
async function loadSettings() {
2148+
try {
2149+
const s = await fetchJSON('/api/settings');
2150+
document.getElementById('autoUpdateToggle').checked = s.auto_update_rules !== false;
2151+
const last = s.last_update_check;
2152+
document.getElementById('lastCheckLabel').textContent = last
2153+
? 'Last check: ' + new Date(last).toLocaleString()
2154+
: '';
2155+
} catch (_) {}
2156+
}
2157+
async function toggleAutoUpdate(on) {
2158+
try {
2159+
await fetch('/api/settings', {
2160+
method: 'POST',
2161+
headers: { 'Content-Type': 'application/json' },
2162+
body: JSON.stringify({ auto_update_rules: on })
2163+
});
2164+
showToast(on ? 'Auto-update enabled' : 'Auto-update disabled', 'info');
2165+
} catch (e) { showToast('Failed to save', 'danger'); }
2166+
}
2167+
async function checkForUpdates() {
2168+
const btn = document.getElementById('checkUpdatesBtn');
2169+
btn.disabled = true;
2170+
btn.textContent = 'Checking...';
2171+
try {
2172+
const r = await fetchJSON('/api/updates');
2173+
if (r.error) {
2174+
showToast(r.error, 'danger');
2175+
} else if (r.has_updates) {
2176+
const n = r.change_count || 0;
2177+
showToast(`${n} rule change(s) available`, 'warning');
2178+
showUpdateBanner(r);
2179+
} else {
2180+
showToast('Rules are up to date', 'success');
2181+
hideUpdateBanner();
2182+
}
2183+
} catch (e) {
2184+
showToast('Check failed: ' + e.message, 'danger');
2185+
}
2186+
btn.disabled = false;
2187+
btn.textContent = 'Check for updates now';
2188+
loadSettings();
2189+
}
2190+
function showUpdateBanner(r) {
2191+
const n = r.change_count || 0;
2192+
const isMac = detectedOS === 'Darwin';
2193+
const el = document.getElementById('updateBanner');
2194+
const text = document.getElementById('updateBannerText');
2195+
text.textContent = isMac
2196+
? `${n} new rule change(s) available. Restart OpenClaw to enforce.`
2197+
: `${n} new rule change(s) available.`;
2198+
el.style.display = 'block';
2199+
}
2200+
function hideUpdateBanner() {
2201+
document.getElementById('updateBanner').style.display = 'none';
2202+
}
2203+
async function applyUpdates() {
2204+
const btn = document.getElementById('applyUpdatesBtn');
2205+
btn.disabled = true;
2206+
btn.textContent = 'Applying...';
2207+
try {
2208+
const res = await fetch('/api/updates/apply', { method: 'POST' });
2209+
const data = await res.json();
2210+
if (data.error) {
2211+
showToast(data.error, 'danger');
2212+
} else {
2213+
showToast(data.message || 'Updates applied', 'success');
2214+
hideUpdateBanner();
2215+
if (detectedOS === 'Linux') loadRules();
2216+
}
2217+
} catch (e) {
2218+
showToast('Apply failed: ' + e.message, 'danger');
2219+
}
2220+
btn.disabled = false;
2221+
btn.textContent = 'Download & apply';
2222+
loadSettings();
2223+
}
2224+
20742225
// ── Init ──
2075-
loadStatus().then(() => loadRules().then(() => loadUserRules()));
2226+
loadStatus().then(() => {
2227+
loadRules().then(() => loadUserRules());
2228+
loadSettings();
2229+
checkForUpdates().then(() => {}); // Initial check for banner
2230+
});
20762231
loadSessions(); loadAlerts(); loadCustomRules();
20772232
setInterval(loadAlerts, 30000);
20782233
setInterval(loadSessions, 15000);
2234+
setInterval(async () => {
2235+
try {
2236+
const r = await fetchJSON('/api/updates/cached');
2237+
if (r.has_updates) showUpdateBanner(r);
2238+
} catch (_) {}
2239+
}, 120000); // Poll for cached updates every 2 min (from background hourly check)
20792240
</script>
20802241
</body>
20812242

deploy/install.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ install_macos() {
198198
# Shared modules
199199
fetch "$CLAWEDR_BASE_URL/shared/user_rules.py" "$tmpdir/user_rules.py"
200200
fetch "$CLAWEDR_BASE_URL/shared/alert_dispatcher.py" "$tmpdir/alert_dispatcher.py"
201+
fetch "$CLAWEDR_BASE_URL/shared/rule_updater.py" "$tmpdir/rule_updater.py"
201202
# Dashboard
202203
fetch "$CLAWEDR_BASE_URL/dashboard/app.py" "$tmpdir/dashboard_app.py"
203204
fetch "$CLAWEDR_BASE_URL/dashboard/templates/index.html" "$tmpdir/dashboard_index.html"
@@ -217,6 +218,7 @@ install_macos() {
217218
cp "$tmpdir/compiled_policy.json" "$CLAWEDR_DIR/"
218219
cp "$tmpdir/user_rules.py" "$CLAWEDR_DIR/shared/"
219220
cp "$tmpdir/alert_dispatcher.py" "$CLAWEDR_DIR/shared/"
221+
cp "$tmpdir/rule_updater.py" "$CLAWEDR_DIR/shared/"
220222
cp "$tmpdir/dashboard_app.py" "$CLAWEDR_DIR/dashboard/app.py"
221223
cp "$tmpdir/dashboard_index.html" "$CLAWEDR_DIR/dashboard/templates/index.html"
222224
touch "$CLAWEDR_DIR/shared/__init__.py"
@@ -261,6 +263,7 @@ install_linux() {
261263
# Shared modules
262264
fetch "$CLAWEDR_BASE_URL/shared/user_rules.py" "$tmpdir/user_rules.py"
263265
fetch "$CLAWEDR_BASE_URL/shared/alert_dispatcher.py" "$tmpdir/alert_dispatcher.py"
266+
fetch "$CLAWEDR_BASE_URL/shared/rule_updater.py" "$tmpdir/rule_updater.py"
264267
# Dashboard
265268
fetch "$CLAWEDR_BASE_URL/dashboard/app.py" "$tmpdir/dashboard_app.py"
266269
fetch "$CLAWEDR_BASE_URL/dashboard/templates/index.html" "$tmpdir/dashboard_index.html"
@@ -279,6 +282,7 @@ install_linux() {
279282
cp "$tmpdir/monitor.py" "$CLAWEDR_DIR/"
280283
cp "$tmpdir/user_rules.py" "$CLAWEDR_DIR/shared/"
281284
cp "$tmpdir/alert_dispatcher.py" "$CLAWEDR_DIR/shared/"
285+
cp "$tmpdir/rule_updater.py" "$CLAWEDR_DIR/shared/"
282286
cp "$tmpdir/dashboard_app.py" "$CLAWEDR_DIR/dashboard/app.py"
283287
cp "$tmpdir/dashboard_index.html" "$CLAWEDR_DIR/dashboard/templates/index.html"
284288
touch "$CLAWEDR_DIR/shared/__init__.py"

0 commit comments

Comments
 (0)