feat(approvals): integrate approval-hold detection and auto-remediation into fleet alert, loop diagnostics, and muse API
This commit is contained in:
+83
-35
@@ -20,7 +20,7 @@ Usage:
|
||||
Exit codes: 0 ok, 1 error, 2 APPROVAL_NEEDED (human decision),
|
||||
3 DOM_NOT_READY (browser not in expected chat state - safe to retry).
|
||||
"""
|
||||
import json, urllib.request, websocket, time, sys, argparse, importlib.util
|
||||
import json, urllib.request, websocket, time, sys, argparse, importlib.util, re
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
@@ -44,8 +44,8 @@ def _load_accounts():
|
||||
spec.loader.exec_module(mod)
|
||||
accounts = {}
|
||||
for node, rec in mod.load().items():
|
||||
accounts[node] = (node, "http://127.0.0.1:%d/json/list" %
|
||||
rec["cdp_port"])
|
||||
peer_ip = rec.get("peer_ip", "127.0.0.1")
|
||||
accounts[node] = (node, "http://%s:%d/json/list" % (peer_ip, rec["cdp_port"]))
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -59,8 +59,33 @@ TRUSTED_IPS = {
|
||||
}
|
||||
|
||||
def get_page(node, cdp_url):
|
||||
with urllib.request.urlopen(cdp_url, timeout=5) as r:
|
||||
ts = json.load(r)
|
||||
urls = [cdp_url]
|
||||
m = re.search(r":(\d+)/", cdp_url)
|
||||
if m:
|
||||
port = m.group(1)
|
||||
if "127.0.0.1" in cdp_url:
|
||||
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
|
||||
spec = importlib.util.spec_from_file_location("netvm_registry", path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
pip = mod.peer_ip_for(node)
|
||||
if pip:
|
||||
urls.append(f"http://{pip}:{port}/json/list")
|
||||
else:
|
||||
urls.append(f"http://127.0.0.1:{port}/json/list")
|
||||
ts = None
|
||||
last_err = None
|
||||
for u in urls:
|
||||
try:
|
||||
with urllib.request.urlopen(u, timeout=5) as r:
|
||||
ts = json.load(r)
|
||||
break
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
continue
|
||||
if not ts:
|
||||
print(f"ERROR: No page found ({last_err})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
pages = [t for t in ts if t.get('type') == 'page']
|
||||
if not pages:
|
||||
print("ERROR: No page found", file=sys.stderr)
|
||||
@@ -93,28 +118,45 @@ def check_approvals(ws):
|
||||
"""
|
||||
result = ev(ws, """(() => {
|
||||
const dialogs = [];
|
||||
// Look for permission prompts (common patterns)
|
||||
const body = document.body.innerText;
|
||||
// Check for "Allow ... to share" pattern
|
||||
if (body.includes('Allow') && body.includes('to share')) {
|
||||
// Find the dialog
|
||||
const els = [...document.querySelectorAll('*')].filter(el => {
|
||||
const t = el.innerText || '';
|
||||
return t.includes('Allow') && t.includes('to share') && t.length < 500;
|
||||
});
|
||||
for (const el of els.slice(0,3)) {
|
||||
dialogs.push(el.innerText.slice(0,200));
|
||||
// 1. Check confirmed structural selectors
|
||||
const headers = [...document.querySelectorAll('[data-testid="approval-panel-header"], [data-testid*="approval"]')];
|
||||
for (const h of headers) {
|
||||
let card = h;
|
||||
for (let i = 0; i < 6 && card && card.parentElement && card.parentElement !== document.body; i++) {
|
||||
if (card.querySelector('button[data-hatch-approval-primary-action="true"]') ||
|
||||
[...card.querySelectorAll('button')].some(b => {
|
||||
const t = (b.innerText||'').toLowerCase();
|
||||
return t.includes('allow once') || t.includes('deny');
|
||||
})) {
|
||||
dialogs.push(card.innerText.slice(0, 500));
|
||||
break;
|
||||
}
|
||||
card = card.parentElement;
|
||||
}
|
||||
}
|
||||
// Check for other permission patterns
|
||||
const perm_btns = [...document.querySelectorAll('button')].filter(b => {
|
||||
const t = (b.innerText||'').toLowerCase();
|
||||
return t.includes('allow') || t.includes('deny') || t.includes('block');
|
||||
});
|
||||
if (perm_btns.length >= 2 && dialogs.length === 0) {
|
||||
// Might be a permission dialog
|
||||
const parent = perm_btns[0].closest('div');
|
||||
if (parent) dialogs.push(parent.innerText.slice(0,200));
|
||||
// 2. Check for "Allow ... to share" pattern if not found
|
||||
if (dialogs.length === 0) {
|
||||
const body = document.body ? document.body.innerText : '';
|
||||
if (body.includes('Allow') && body.includes('to share')) {
|
||||
const els = [...document.querySelectorAll('*')].filter(el => {
|
||||
const t = el.innerText || '';
|
||||
return t.includes('Allow') && t.includes('to share') && t.length < 500;
|
||||
});
|
||||
for (const el of els.slice(0,3)) {
|
||||
dialogs.push(el.innerText.slice(0,200));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. Check for other permission patterns
|
||||
if (dialogs.length === 0) {
|
||||
const perm_btns = [...document.querySelectorAll('button')].filter(b => {
|
||||
const t = (b.innerText||'').toLowerCase();
|
||||
return t.includes('allow') || t.includes('deny') || t.includes('block');
|
||||
});
|
||||
if (perm_btns.length >= 2) {
|
||||
const parent = perm_btns[0].closest('div');
|
||||
if (parent) dialogs.push(parent.innerText.slice(0,200));
|
||||
}
|
||||
}
|
||||
return JSON.stringify(dialogs);
|
||||
})()""")
|
||||
@@ -128,18 +170,24 @@ def check_approvals(ws):
|
||||
# Extract IP if present
|
||||
import re
|
||||
ips = re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', d)
|
||||
# Only block for IP-based permission dialogs. Dialogs without IPs
|
||||
# are likely false positives (chat content, UI text) - don't block.
|
||||
if not ips:
|
||||
continue
|
||||
is_trusted = any(ip in TRUSTED_IPS for ip in ips)
|
||||
# Check trust: if IP present, must be in TRUSTED_IPS; if no IP, untrusted approval dialog
|
||||
if ips:
|
||||
is_trusted = any(ip in TRUSTED_IPS for ip in ips)
|
||||
else:
|
||||
# Check if this is a known false positive or real dialog
|
||||
if "allow once" in d.lower() or "approval-panel" in d.lower() or "wants to" in d.lower():
|
||||
is_trusted = False
|
||||
else:
|
||||
continue
|
||||
|
||||
if is_trusted:
|
||||
# Auto-approve: click "Allow once" or "Allow"
|
||||
# Auto-approve: click primary action or "Allow once" / "Allow"
|
||||
clicked = ev(ws, """(async()=>{
|
||||
const b = [...document.querySelectorAll('button')].find(x=>{
|
||||
const t = (x.innerText||'').toLowerCase();
|
||||
return t.includes('allow once') || t === 'allow';
|
||||
});
|
||||
const b = document.querySelector('button[data-hatch-approval-primary-action="true"]') ||
|
||||
[...document.querySelectorAll('button')].find(x=>{
|
||||
const t = (x.innerText||'').toLowerCase().trim();
|
||||
return t === 'allow once' || t.includes('allow once') || t === 'allow';
|
||||
});
|
||||
if (b) { b.click(); return 'clicked:'+b.innerText.slice(0,20); }
|
||||
return 'NOTFOUND';
|
||||
})()""", True)
|
||||
|
||||
Reference in New Issue
Block a user