v2.0 — Re-integrate autonomy framework from Linux non-public build
Add multi-model autonomous threat response system (SLM/SAM/LAM): - ModelRouter: concurrent model tiers with fallback chains - RulesEngine: condition-action automation with 11 condition/action types - AutonomyDaemon: background threat monitoring and rule dispatch - Web UI: 4-tab dashboard (Dashboard, Rules, Activity Log, Models) - Config: [slm], [sam], [lam], [autonomy] settings sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1789a07c2b
commit
6d4bef8d24
3
.gitignore
vendored
3
.gitignore
vendored
@ -61,6 +61,9 @@ build/
|
|||||||
build_temp/
|
build_temp/
|
||||||
*.spec.bak
|
*.spec.bak
|
||||||
|
|
||||||
|
# Local utility scripts
|
||||||
|
kill_autarch.bat
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
@ -64,6 +64,7 @@ hidden_imports = [
|
|||||||
'core.upnp', 'core.wireshark', 'core.wireguard',
|
'core.upnp', 'core.wireshark', 'core.wireguard',
|
||||||
'core.mcp_server', 'core.discovery',
|
'core.mcp_server', 'core.discovery',
|
||||||
'core.osint_db', 'core.nvd',
|
'core.osint_db', 'core.nvd',
|
||||||
|
'core.model_router', 'core.rules', 'core.autonomy',
|
||||||
|
|
||||||
# Web routes (Flask blueprints)
|
# Web routes (Flask blueprints)
|
||||||
'web.app', 'web.auth',
|
'web.app', 'web.auth',
|
||||||
@ -90,6 +91,7 @@ hidden_imports = [
|
|||||||
'web.routes.targets',
|
'web.routes.targets',
|
||||||
'web.routes.encmodules',
|
'web.routes.encmodules',
|
||||||
'web.routes.llm_trainer',
|
'web.routes.llm_trainer',
|
||||||
|
'web.routes.autonomy',
|
||||||
|
|
||||||
# Standard library (sometimes missed on Windows)
|
# Standard library (sometimes missed on Windows)
|
||||||
'email.mime.text', 'email.mime.multipart',
|
'email.mime.text', 'email.mime.multipart',
|
||||||
|
|||||||
@ -117,3 +117,35 @@ host = 0.0.0.0
|
|||||||
port = 17322
|
port = 17322
|
||||||
auto_start = false
|
auto_start = false
|
||||||
|
|
||||||
|
[slm]
|
||||||
|
enabled = true
|
||||||
|
backend = local
|
||||||
|
model_path =
|
||||||
|
n_ctx = 512
|
||||||
|
n_gpu_layers = -1
|
||||||
|
n_threads = 2
|
||||||
|
|
||||||
|
[sam]
|
||||||
|
enabled = true
|
||||||
|
backend = local
|
||||||
|
model_path =
|
||||||
|
n_ctx = 2048
|
||||||
|
n_gpu_layers = -1
|
||||||
|
n_threads = 4
|
||||||
|
|
||||||
|
[lam]
|
||||||
|
enabled = true
|
||||||
|
backend = local
|
||||||
|
model_path =
|
||||||
|
n_ctx = 4096
|
||||||
|
n_gpu_layers = -1
|
||||||
|
n_threads = 4
|
||||||
|
|
||||||
|
[autonomy]
|
||||||
|
enabled = false
|
||||||
|
monitor_interval = 3
|
||||||
|
rule_eval_interval = 5
|
||||||
|
max_concurrent_agents = 3
|
||||||
|
threat_threshold_auto_respond = 40
|
||||||
|
log_max_entries = 1000
|
||||||
|
|
||||||
|
|||||||
665
core/autonomy.py
Normal file
665
core/autonomy.py
Normal file
@ -0,0 +1,665 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Autonomy Daemon
|
||||||
|
Background loop that monitors threats, evaluates rules, and dispatches
|
||||||
|
AI-driven responses across all categories (defense, offense, counter,
|
||||||
|
analyze, OSINT, simulate).
|
||||||
|
|
||||||
|
The daemon ties together:
|
||||||
|
- ThreatMonitor (threat data gathering)
|
||||||
|
- RulesEngine (condition-action evaluation)
|
||||||
|
- ModelRouter (SLM/SAM/LAM model tiers)
|
||||||
|
- Agent (autonomous task execution)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional, Deque
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
from .rules import RulesEngine, Rule
|
||||||
|
from .model_router import get_model_router, ModelTier
|
||||||
|
|
||||||
|
_logger = logging.getLogger('autarch.autonomy')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ActivityEntry:
|
||||||
|
"""Single entry in the autonomy activity log."""
|
||||||
|
id: str
|
||||||
|
timestamp: str
|
||||||
|
rule_id: Optional[str] = None
|
||||||
|
rule_name: Optional[str] = None
|
||||||
|
tier: Optional[str] = None
|
||||||
|
action_type: str = ''
|
||||||
|
action_detail: str = ''
|
||||||
|
result: str = ''
|
||||||
|
success: bool = True
|
||||||
|
duration_ms: Optional[int] = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
class AutonomyDaemon:
|
||||||
|
"""Background daemon for autonomous threat response.
|
||||||
|
|
||||||
|
Lifecycle: start() -> pause()/resume() -> stop()
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG_PATH = Path(__file__).parent.parent / 'data' / 'autonomy_log.json'
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
|
self.config = config or get_config()
|
||||||
|
self.rules_engine = RulesEngine()
|
||||||
|
self._router = None # Lazy — get_model_router() on start
|
||||||
|
|
||||||
|
# State
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._running = False
|
||||||
|
self._paused = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
# Agent tracking
|
||||||
|
self._active_agents: Dict[str, threading.Thread] = {}
|
||||||
|
self._agent_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Activity log (ring buffer)
|
||||||
|
settings = self.config.get_autonomy_settings()
|
||||||
|
max_entries = settings.get('log_max_entries', 1000)
|
||||||
|
self._activity: Deque[ActivityEntry] = deque(maxlen=max_entries)
|
||||||
|
self._activity_lock = threading.Lock()
|
||||||
|
|
||||||
|
# SSE subscribers
|
||||||
|
self._subscribers: List = []
|
||||||
|
self._sub_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Load persisted log
|
||||||
|
self._load_log()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> dict:
|
||||||
|
"""Current daemon status."""
|
||||||
|
settings = self.config.get_autonomy_settings()
|
||||||
|
with self._agent_lock:
|
||||||
|
active = len(self._active_agents)
|
||||||
|
return {
|
||||||
|
'running': self._running,
|
||||||
|
'paused': self._paused,
|
||||||
|
'enabled': settings['enabled'],
|
||||||
|
'monitor_interval': settings['monitor_interval'],
|
||||||
|
'rule_eval_interval': settings['rule_eval_interval'],
|
||||||
|
'active_agents': active,
|
||||||
|
'max_agents': settings['max_concurrent_agents'],
|
||||||
|
'rules_count': len(self.rules_engine.get_all_rules()),
|
||||||
|
'activity_count': len(self._activity),
|
||||||
|
}
|
||||||
|
|
||||||
|
def start(self) -> bool:
|
||||||
|
"""Start the autonomy daemon background thread."""
|
||||||
|
if self._running:
|
||||||
|
_logger.warning('[Autonomy] Already running')
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._router = get_model_router()
|
||||||
|
self._running = True
|
||||||
|
self._paused = False
|
||||||
|
self._stop_event.clear()
|
||||||
|
|
||||||
|
self._thread = threading.Thread(
|
||||||
|
target=self._run_loop,
|
||||||
|
name='AutonomyDaemon',
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._thread.start()
|
||||||
|
self._log_activity('system', 'Autonomy daemon started')
|
||||||
|
_logger.info('[Autonomy] Daemon started')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the daemon and wait for thread exit."""
|
||||||
|
if not self._running:
|
||||||
|
return
|
||||||
|
self._running = False
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
self._thread.join(timeout=10)
|
||||||
|
self._log_activity('system', 'Autonomy daemon stopped')
|
||||||
|
_logger.info('[Autonomy] Daemon stopped')
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
"""Pause rule evaluation (monitoring continues)."""
|
||||||
|
self._paused = True
|
||||||
|
self._log_activity('system', 'Autonomy paused')
|
||||||
|
_logger.info('[Autonomy] Paused')
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
"""Resume rule evaluation."""
|
||||||
|
self._paused = False
|
||||||
|
self._log_activity('system', 'Autonomy resumed')
|
||||||
|
_logger.info('[Autonomy] Resumed')
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Main loop
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _run_loop(self):
|
||||||
|
"""Background loop: gather context, evaluate rules, dispatch."""
|
||||||
|
settings = self.config.get_autonomy_settings()
|
||||||
|
monitor_interval = settings['monitor_interval']
|
||||||
|
rule_eval_interval = settings['rule_eval_interval']
|
||||||
|
last_rule_eval = 0
|
||||||
|
|
||||||
|
while self._running and not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
# Gather threat context every cycle
|
||||||
|
context = self._gather_context()
|
||||||
|
|
||||||
|
# Evaluate rules at a slower cadence
|
||||||
|
now = time.time()
|
||||||
|
if not self._paused and (now - last_rule_eval) >= rule_eval_interval:
|
||||||
|
last_rule_eval = now
|
||||||
|
self._evaluate_and_dispatch(context)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f'[Autonomy] Loop error: {e}')
|
||||||
|
self._log_activity('error', f'Loop error: {e}', success=False)
|
||||||
|
|
||||||
|
# Sleep in short increments so stop is responsive
|
||||||
|
self._stop_event.wait(timeout=monitor_interval)
|
||||||
|
|
||||||
|
def _gather_context(self) -> Dict[str, Any]:
|
||||||
|
"""Gather current threat context from ThreatMonitor."""
|
||||||
|
try:
|
||||||
|
from modules.defender_monitor import get_threat_monitor
|
||||||
|
tm = get_threat_monitor()
|
||||||
|
except ImportError:
|
||||||
|
_logger.warning('[Autonomy] ThreatMonitor not available')
|
||||||
|
return {'timestamp': datetime.now().isoformat()}
|
||||||
|
|
||||||
|
context: Dict[str, Any] = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['connections'] = tm.get_connections()
|
||||||
|
context['connection_count'] = len(context['connections'])
|
||||||
|
except Exception:
|
||||||
|
context['connections'] = []
|
||||||
|
context['connection_count'] = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['bandwidth'] = {}
|
||||||
|
bw = tm.get_bandwidth()
|
||||||
|
if bw:
|
||||||
|
total_rx = sum(iface.get('rx_delta', 0) for iface in bw)
|
||||||
|
total_tx = sum(iface.get('tx_delta', 0) for iface in bw)
|
||||||
|
context['bandwidth'] = {
|
||||||
|
'rx_mbps': (total_rx * 8) / 1_000_000,
|
||||||
|
'tx_mbps': (total_tx * 8) / 1_000_000,
|
||||||
|
'interfaces': bw,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
context['bandwidth'] = {'rx_mbps': 0, 'tx_mbps': 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['arp_alerts'] = tm.check_arp_spoofing()
|
||||||
|
except Exception:
|
||||||
|
context['arp_alerts'] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['new_ports'] = tm.check_new_listening_ports()
|
||||||
|
except Exception:
|
||||||
|
context['new_ports'] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['threat_score'] = tm.calculate_threat_score()
|
||||||
|
except Exception:
|
||||||
|
context['threat_score'] = {'score': 0, 'level': 'LOW', 'details': []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['ddos'] = tm.detect_ddos()
|
||||||
|
except Exception:
|
||||||
|
context['ddos'] = {'under_attack': False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
context['scan_indicators'] = tm.check_port_scan_indicators()
|
||||||
|
if isinstance(context['scan_indicators'], list):
|
||||||
|
context['scan_indicators'] = len(context['scan_indicators'])
|
||||||
|
except Exception:
|
||||||
|
context['scan_indicators'] = 0
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Rule evaluation and dispatch
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _evaluate_and_dispatch(self, context: Dict[str, Any]):
|
||||||
|
"""Evaluate rules and dispatch matching actions."""
|
||||||
|
matches = self.rules_engine.evaluate(context)
|
||||||
|
|
||||||
|
for rule, resolved_actions in matches:
|
||||||
|
for action in resolved_actions:
|
||||||
|
action_type = action.get('type', '')
|
||||||
|
_logger.info(f'[Autonomy] Rule "{rule.name}" triggered -> {action_type}')
|
||||||
|
|
||||||
|
if self._is_agent_action(action_type):
|
||||||
|
self._dispatch_agent(rule, action, context)
|
||||||
|
else:
|
||||||
|
self._dispatch_direct(rule, action, context)
|
||||||
|
|
||||||
|
def _is_agent_action(self, action_type: str) -> bool:
|
||||||
|
"""Check if an action requires an AI agent."""
|
||||||
|
return action_type in ('run_module', 'counter_scan', 'escalate_to_lam')
|
||||||
|
|
||||||
|
def _dispatch_direct(self, rule: Rule, action: dict, context: dict):
|
||||||
|
"""Execute a simple action directly (no LLM needed)."""
|
||||||
|
action_type = action.get('type', '')
|
||||||
|
start = time.time()
|
||||||
|
success = True
|
||||||
|
result = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
if action_type == 'block_ip':
|
||||||
|
result = self._action_block_ip(action.get('ip', ''))
|
||||||
|
|
||||||
|
elif action_type == 'unblock_ip':
|
||||||
|
result = self._action_unblock_ip(action.get('ip', ''))
|
||||||
|
|
||||||
|
elif action_type == 'rate_limit_ip':
|
||||||
|
result = self._action_rate_limit(
|
||||||
|
action.get('ip', ''),
|
||||||
|
action.get('rate', '10/s'),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action_type == 'block_port':
|
||||||
|
result = self._action_block_port(
|
||||||
|
action.get('port', ''),
|
||||||
|
action.get('direction', 'inbound'),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action_type == 'kill_process':
|
||||||
|
result = self._action_kill_process(action.get('pid', ''))
|
||||||
|
|
||||||
|
elif action_type in ('alert', 'log_event'):
|
||||||
|
result = action.get('message', 'No message')
|
||||||
|
|
||||||
|
elif action_type == 'run_shell':
|
||||||
|
result = self._action_run_shell(action.get('command', ''))
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = f'Unknown action type: {action_type}'
|
||||||
|
success = False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result = f'Error: {e}'
|
||||||
|
success = False
|
||||||
|
|
||||||
|
duration = int((time.time() - start) * 1000)
|
||||||
|
detail = action.get('ip', '') or action.get('port', '') or action.get('message', '')[:80]
|
||||||
|
self._log_activity(
|
||||||
|
action_type, detail,
|
||||||
|
rule_id=rule.id, rule_name=rule.name,
|
||||||
|
result=result, success=success, duration_ms=duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _dispatch_agent(self, rule: Rule, action: dict, context: dict):
|
||||||
|
"""Spawn an AI agent to handle a complex action."""
|
||||||
|
settings = self.config.get_autonomy_settings()
|
||||||
|
max_agents = settings['max_concurrent_agents']
|
||||||
|
|
||||||
|
# Clean finished agents
|
||||||
|
with self._agent_lock:
|
||||||
|
self._active_agents = {
|
||||||
|
k: v for k, v in self._active_agents.items()
|
||||||
|
if v.is_alive()
|
||||||
|
}
|
||||||
|
if len(self._active_agents) >= max_agents:
|
||||||
|
_logger.warning('[Autonomy] Max agents reached, skipping')
|
||||||
|
self._log_activity(
|
||||||
|
action.get('type', 'agent'), 'Skipped: max agents reached',
|
||||||
|
rule_id=rule.id, rule_name=rule.name,
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
agent_id = str(uuid.uuid4())[:8]
|
||||||
|
action_type = action.get('type', '')
|
||||||
|
|
||||||
|
# Determine tier
|
||||||
|
if action_type == 'escalate_to_lam':
|
||||||
|
tier = ModelTier.LAM
|
||||||
|
else:
|
||||||
|
tier = ModelTier.SAM
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
target=self._run_agent,
|
||||||
|
args=(agent_id, tier, rule, action, context),
|
||||||
|
name=f'Agent-{agent_id}',
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self._agent_lock:
|
||||||
|
self._active_agents[agent_id] = t
|
||||||
|
|
||||||
|
t.start()
|
||||||
|
self._log_activity(
|
||||||
|
action_type, f'Agent {agent_id} spawned ({tier.value})',
|
||||||
|
rule_id=rule.id, rule_name=rule.name, tier=tier.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_agent(self, agent_id: str, tier: ModelTier, rule: Rule,
|
||||||
|
action: dict, context: dict):
|
||||||
|
"""Execute an agent task in a background thread."""
|
||||||
|
from .agent import Agent
|
||||||
|
from .tools import get_tool_registry
|
||||||
|
|
||||||
|
action_type = action.get('type', '')
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
# Build task prompt
|
||||||
|
if action_type == 'run_module':
|
||||||
|
module = action.get('module', '')
|
||||||
|
args = action.get('args', '')
|
||||||
|
task = f'Run the AUTARCH module "{module}" with arguments: {args}'
|
||||||
|
|
||||||
|
elif action_type == 'counter_scan':
|
||||||
|
target = action.get('target', '')
|
||||||
|
task = f'Perform a counter-scan against {target}. Gather reconnaissance and identify vulnerabilities.'
|
||||||
|
|
||||||
|
elif action_type == 'escalate_to_lam':
|
||||||
|
task = action.get('task', 'Analyze the current threat landscape and recommend actions.')
|
||||||
|
|
||||||
|
else:
|
||||||
|
task = f'Execute action: {action_type} with params: {json.dumps(action)}'
|
||||||
|
|
||||||
|
# Get LLM instance for the tier
|
||||||
|
router = self._router or get_model_router()
|
||||||
|
llm_inst = router.get_instance(tier)
|
||||||
|
|
||||||
|
if llm_inst is None or not llm_inst.is_loaded:
|
||||||
|
# Try fallback
|
||||||
|
for fallback in (ModelTier.SAM, ModelTier.LAM):
|
||||||
|
llm_inst = router.get_instance(fallback)
|
||||||
|
if llm_inst and llm_inst.is_loaded:
|
||||||
|
tier = fallback
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._log_activity(
|
||||||
|
action_type, f'Agent {agent_id}: no model loaded',
|
||||||
|
rule_id=rule.id, rule_name=rule.name,
|
||||||
|
tier=tier.value, success=False,
|
||||||
|
result='No model available for agent execution',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
llm=llm_inst,
|
||||||
|
tools=get_tool_registry(),
|
||||||
|
max_steps=15,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
result = agent.run(task)
|
||||||
|
duration = int((time.time() - start) * 1000)
|
||||||
|
|
||||||
|
self._log_activity(
|
||||||
|
action_type,
|
||||||
|
f'Agent {agent_id}: {result.summary[:100]}',
|
||||||
|
rule_id=rule.id, rule_name=rule.name,
|
||||||
|
tier=tier.value, success=result.success,
|
||||||
|
result=result.summary, duration_ms=duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration = int((time.time() - start) * 1000)
|
||||||
|
_logger.error(f'[Autonomy] Agent {agent_id} failed: {e}')
|
||||||
|
self._log_activity(
|
||||||
|
action_type, f'Agent {agent_id} failed: {e}',
|
||||||
|
rule_id=rule.id, rule_name=rule.name,
|
||||||
|
tier=tier.value, success=False,
|
||||||
|
result=str(e), duration_ms=duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
with self._agent_lock:
|
||||||
|
self._active_agents.pop(agent_id, None)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Direct action implementations
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _action_block_ip(self, ip: str) -> str:
|
||||||
|
if not ip:
|
||||||
|
return 'No IP specified'
|
||||||
|
try:
|
||||||
|
from modules.defender_monitor import get_threat_monitor
|
||||||
|
tm = get_threat_monitor()
|
||||||
|
tm.auto_block_ip(ip)
|
||||||
|
return f'Blocked {ip}'
|
||||||
|
except Exception as e:
|
||||||
|
return f'Block failed: {e}'
|
||||||
|
|
||||||
|
def _action_unblock_ip(self, ip: str) -> str:
|
||||||
|
if not ip:
|
||||||
|
return 'No IP specified'
|
||||||
|
try:
|
||||||
|
import subprocess, platform
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
cmd = f'netsh advfirewall firewall delete rule name="AUTARCH Block {ip}"'
|
||||||
|
else:
|
||||||
|
cmd = f'iptables -D INPUT -s {ip} -j DROP 2>/dev/null; iptables -D OUTPUT -d {ip} -j DROP 2>/dev/null'
|
||||||
|
subprocess.run(cmd, shell=True, capture_output=True, timeout=10)
|
||||||
|
return f'Unblocked {ip}'
|
||||||
|
except Exception as e:
|
||||||
|
return f'Unblock failed: {e}'
|
||||||
|
|
||||||
|
def _action_rate_limit(self, ip: str, rate: str) -> str:
|
||||||
|
if not ip:
|
||||||
|
return 'No IP specified'
|
||||||
|
try:
|
||||||
|
from modules.defender_monitor import get_threat_monitor
|
||||||
|
tm = get_threat_monitor()
|
||||||
|
tm.apply_rate_limit(ip)
|
||||||
|
return f'Rate limited {ip} at {rate}'
|
||||||
|
except Exception as e:
|
||||||
|
return f'Rate limit failed: {e}'
|
||||||
|
|
||||||
|
def _action_block_port(self, port: str, direction: str) -> str:
|
||||||
|
if not port:
|
||||||
|
return 'No port specified'
|
||||||
|
try:
|
||||||
|
import subprocess, platform
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
d = 'in' if direction == 'inbound' else 'out'
|
||||||
|
cmd = f'netsh advfirewall firewall add rule name="AUTARCH Block Port {port}" dir={d} action=block protocol=TCP localport={port}'
|
||||||
|
else:
|
||||||
|
chain = 'INPUT' if direction == 'inbound' else 'OUTPUT'
|
||||||
|
cmd = f'iptables -A {chain} -p tcp --dport {port} -j DROP'
|
||||||
|
subprocess.run(cmd, shell=True, capture_output=True, timeout=10)
|
||||||
|
return f'Blocked port {port} ({direction})'
|
||||||
|
except Exception as e:
|
||||||
|
return f'Block port failed: {e}'
|
||||||
|
|
||||||
|
def _action_kill_process(self, pid: str) -> str:
|
||||||
|
if not pid:
|
||||||
|
return 'No PID specified'
|
||||||
|
try:
|
||||||
|
import subprocess, platform
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
cmd = f'taskkill /F /PID {pid}'
|
||||||
|
else:
|
||||||
|
cmd = f'kill -9 {pid}'
|
||||||
|
subprocess.run(cmd, shell=True, capture_output=True, timeout=10)
|
||||||
|
return f'Killed process {pid}'
|
||||||
|
except Exception as e:
|
||||||
|
return f'Kill failed: {e}'
|
||||||
|
|
||||||
|
def _action_run_shell(self, command: str) -> str:
|
||||||
|
if not command:
|
||||||
|
return 'No command specified'
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(
|
||||||
|
command, shell=True, capture_output=True,
|
||||||
|
text=True, timeout=30,
|
||||||
|
)
|
||||||
|
output = result.stdout[:500]
|
||||||
|
if result.returncode != 0:
|
||||||
|
output += f'\n[exit {result.returncode}]'
|
||||||
|
return output.strip() or '[no output]'
|
||||||
|
except Exception as e:
|
||||||
|
return f'Shell failed: {e}'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Activity log
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _log_activity(self, action_type: str, detail: str, *,
|
||||||
|
rule_id: str = None, rule_name: str = None,
|
||||||
|
tier: str = None, result: str = '',
|
||||||
|
success: bool = True, duration_ms: int = None):
|
||||||
|
"""Add an entry to the activity log and notify SSE subscribers."""
|
||||||
|
entry = ActivityEntry(
|
||||||
|
id=str(uuid.uuid4())[:8],
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
rule_id=rule_id,
|
||||||
|
rule_name=rule_name,
|
||||||
|
tier=tier,
|
||||||
|
action_type=action_type,
|
||||||
|
action_detail=detail,
|
||||||
|
result=result,
|
||||||
|
success=success,
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self._activity_lock:
|
||||||
|
self._activity.append(entry)
|
||||||
|
|
||||||
|
# Notify SSE subscribers
|
||||||
|
self._notify_subscribers(entry)
|
||||||
|
|
||||||
|
# Persist periodically (every 10 entries)
|
||||||
|
if len(self._activity) % 10 == 0:
|
||||||
|
self._save_log()
|
||||||
|
|
||||||
|
def get_activity(self, limit: int = 50, offset: int = 0) -> List[dict]:
|
||||||
|
"""Get recent activity entries."""
|
||||||
|
with self._activity_lock:
|
||||||
|
entries = list(self._activity)
|
||||||
|
entries.reverse() # Newest first
|
||||||
|
return [e.to_dict() for e in entries[offset:offset + limit]]
|
||||||
|
|
||||||
|
def get_activity_count(self) -> int:
|
||||||
|
return len(self._activity)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# SSE streaming
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def subscribe(self):
|
||||||
|
"""Create an SSE subscriber queue."""
|
||||||
|
import queue
|
||||||
|
q = queue.Queue(maxsize=100)
|
||||||
|
with self._sub_lock:
|
||||||
|
self._subscribers.append(q)
|
||||||
|
return q
|
||||||
|
|
||||||
|
def unsubscribe(self, q):
|
||||||
|
"""Remove an SSE subscriber."""
|
||||||
|
with self._sub_lock:
|
||||||
|
try:
|
||||||
|
self._subscribers.remove(q)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _notify_subscribers(self, entry: ActivityEntry):
|
||||||
|
"""Push an activity entry to all SSE subscribers."""
|
||||||
|
data = json.dumps(entry.to_dict())
|
||||||
|
with self._sub_lock:
|
||||||
|
dead = []
|
||||||
|
for q in self._subscribers:
|
||||||
|
try:
|
||||||
|
q.put_nowait(data)
|
||||||
|
except Exception:
|
||||||
|
dead.append(q)
|
||||||
|
for q in dead:
|
||||||
|
try:
|
||||||
|
self._subscribers.remove(q)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Persistence
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _save_log(self):
|
||||||
|
"""Persist activity log to JSON file."""
|
||||||
|
try:
|
||||||
|
self.LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with self._activity_lock:
|
||||||
|
entries = [e.to_dict() for e in self._activity]
|
||||||
|
self.LOG_PATH.write_text(
|
||||||
|
json.dumps({'entries': entries[-200:]}, indent=2),
|
||||||
|
encoding='utf-8',
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f'[Autonomy] Failed to save log: {e}')
|
||||||
|
|
||||||
|
def _load_log(self):
|
||||||
|
"""Load persisted activity log."""
|
||||||
|
if not self.LOG_PATH.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = json.loads(self.LOG_PATH.read_text(encoding='utf-8'))
|
||||||
|
for entry_dict in data.get('entries', []):
|
||||||
|
entry = ActivityEntry(
|
||||||
|
id=entry_dict.get('id', str(uuid.uuid4())[:8]),
|
||||||
|
timestamp=entry_dict.get('timestamp', ''),
|
||||||
|
rule_id=entry_dict.get('rule_id'),
|
||||||
|
rule_name=entry_dict.get('rule_name'),
|
||||||
|
tier=entry_dict.get('tier'),
|
||||||
|
action_type=entry_dict.get('action_type', ''),
|
||||||
|
action_detail=entry_dict.get('action_detail', ''),
|
||||||
|
result=entry_dict.get('result', ''),
|
||||||
|
success=entry_dict.get('success', True),
|
||||||
|
duration_ms=entry_dict.get('duration_ms'),
|
||||||
|
)
|
||||||
|
self._activity.append(entry)
|
||||||
|
_logger.info(f'[Autonomy] Loaded {len(self._activity)} log entries')
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f'[Autonomy] Failed to load log: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Singleton
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
_daemon_instance: Optional[AutonomyDaemon] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_autonomy_daemon() -> AutonomyDaemon:
|
||||||
|
"""Get the global AutonomyDaemon instance."""
|
||||||
|
global _daemon_instance
|
||||||
|
if _daemon_instance is None:
|
||||||
|
_daemon_instance = AutonomyDaemon()
|
||||||
|
return _daemon_instance
|
||||||
|
|
||||||
|
|
||||||
|
def reset_autonomy_daemon():
|
||||||
|
"""Stop and reset the global daemon."""
|
||||||
|
global _daemon_instance
|
||||||
|
if _daemon_instance is not None:
|
||||||
|
_daemon_instance.stop()
|
||||||
|
_daemon_instance = None
|
||||||
@ -87,7 +87,39 @@ class Config:
|
|||||||
'host': '0.0.0.0',
|
'host': '0.0.0.0',
|
||||||
'port': '17322',
|
'port': '17322',
|
||||||
'auto_start': 'false',
|
'auto_start': 'false',
|
||||||
}
|
},
|
||||||
|
'slm': {
|
||||||
|
'enabled': 'true',
|
||||||
|
'backend': 'local',
|
||||||
|
'model_path': '',
|
||||||
|
'n_ctx': '512',
|
||||||
|
'n_gpu_layers': '-1',
|
||||||
|
'n_threads': '2',
|
||||||
|
},
|
||||||
|
'sam': {
|
||||||
|
'enabled': 'true',
|
||||||
|
'backend': 'local',
|
||||||
|
'model_path': '',
|
||||||
|
'n_ctx': '2048',
|
||||||
|
'n_gpu_layers': '-1',
|
||||||
|
'n_threads': '4',
|
||||||
|
},
|
||||||
|
'lam': {
|
||||||
|
'enabled': 'true',
|
||||||
|
'backend': 'local',
|
||||||
|
'model_path': '',
|
||||||
|
'n_ctx': '4096',
|
||||||
|
'n_gpu_layers': '-1',
|
||||||
|
'n_threads': '4',
|
||||||
|
},
|
||||||
|
'autonomy': {
|
||||||
|
'enabled': 'false',
|
||||||
|
'monitor_interval': '3',
|
||||||
|
'rule_eval_interval': '5',
|
||||||
|
'max_concurrent_agents': '3',
|
||||||
|
'threat_threshold_auto_respond': '40',
|
||||||
|
'log_max_entries': '1000',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config_path: str = None):
|
def __init__(self, config_path: str = None):
|
||||||
@ -332,6 +364,40 @@ class Config:
|
|||||||
'auto_start': self.get_bool('revshell', 'auto_start', False),
|
'auto_start': self.get_bool('revshell', 'auto_start', False),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_tier_settings(self, tier: str) -> dict:
|
||||||
|
"""Get settings for a model tier (slm, sam, lam)."""
|
||||||
|
return {
|
||||||
|
'enabled': self.get_bool(tier, 'enabled', True),
|
||||||
|
'backend': self.get(tier, 'backend', 'local'),
|
||||||
|
'model_path': self.get(tier, 'model_path', ''),
|
||||||
|
'n_ctx': self.get_int(tier, 'n_ctx', 2048),
|
||||||
|
'n_gpu_layers': self.get_int(tier, 'n_gpu_layers', -1),
|
||||||
|
'n_threads': self.get_int(tier, 'n_threads', 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_slm_settings(self) -> dict:
|
||||||
|
"""Get Small Language Model tier settings."""
|
||||||
|
return self.get_tier_settings('slm')
|
||||||
|
|
||||||
|
def get_sam_settings(self) -> dict:
|
||||||
|
"""Get Small Action Model tier settings."""
|
||||||
|
return self.get_tier_settings('sam')
|
||||||
|
|
||||||
|
def get_lam_settings(self) -> dict:
|
||||||
|
"""Get Large Action Model tier settings."""
|
||||||
|
return self.get_tier_settings('lam')
|
||||||
|
|
||||||
|
def get_autonomy_settings(self) -> dict:
|
||||||
|
"""Get autonomy daemon settings."""
|
||||||
|
return {
|
||||||
|
'enabled': self.get_bool('autonomy', 'enabled', False),
|
||||||
|
'monitor_interval': self.get_int('autonomy', 'monitor_interval', 3),
|
||||||
|
'rule_eval_interval': self.get_int('autonomy', 'rule_eval_interval', 5),
|
||||||
|
'max_concurrent_agents': self.get_int('autonomy', 'max_concurrent_agents', 3),
|
||||||
|
'threat_threshold_auto_respond': self.get_int('autonomy', 'threat_threshold_auto_respond', 40),
|
||||||
|
'log_max_entries': self.get_int('autonomy', 'log_max_entries', 1000),
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_templates_dir() -> Path:
|
def get_templates_dir() -> Path:
|
||||||
"""Get the path to the configuration templates directory."""
|
"""Get the path to the configuration templates directory."""
|
||||||
|
|||||||
305
core/model_router.py
Normal file
305
core/model_router.py
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Model Router
|
||||||
|
Manages concurrent SLM/LAM/SAM model instances for autonomous operation.
|
||||||
|
|
||||||
|
Model Tiers:
|
||||||
|
SLM (Small Language Model) — Fast classification, routing, yes/no decisions
|
||||||
|
SAM (Small Action Model) — Quick tool execution, simple automated responses
|
||||||
|
LAM (Large Action Model) — Complex multi-step agent tasks, strategic planning
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
|
||||||
|
_logger = logging.getLogger('autarch.model_router')
|
||||||
|
|
||||||
|
|
||||||
|
class ModelTier(Enum):
|
||||||
|
SLM = 'slm'
|
||||||
|
SAM = 'sam'
|
||||||
|
LAM = 'lam'
|
||||||
|
|
||||||
|
|
||||||
|
# Fallback chain: if a tier fails, try the next one
|
||||||
|
_FALLBACK = {
|
||||||
|
ModelTier.SLM: [ModelTier.SAM, ModelTier.LAM],
|
||||||
|
ModelTier.SAM: [ModelTier.LAM],
|
||||||
|
ModelTier.LAM: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _TierConfigProxy:
|
||||||
|
"""Proxies Config but overrides the backend section for a specific model tier.
|
||||||
|
|
||||||
|
When a tier says backend=local with model_path=X, this proxy makes the LLM
|
||||||
|
class (which reads [llama]) see the tier's model_path/n_ctx/etc instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_config, tier_name: str):
|
||||||
|
self._base = base_config
|
||||||
|
self._tier = tier_name
|
||||||
|
self._overrides: Dict[str, Dict[str, str]] = {}
|
||||||
|
self._build_overrides()
|
||||||
|
|
||||||
|
def _build_overrides(self):
|
||||||
|
backend = self._base.get(self._tier, 'backend', 'local')
|
||||||
|
model_path = self._base.get(self._tier, 'model_path', '')
|
||||||
|
n_ctx = self._base.get(self._tier, 'n_ctx', '2048')
|
||||||
|
n_gpu_layers = self._base.get(self._tier, 'n_gpu_layers', '-1')
|
||||||
|
n_threads = self._base.get(self._tier, 'n_threads', '4')
|
||||||
|
|
||||||
|
if backend == 'local':
|
||||||
|
self._overrides['llama'] = {
|
||||||
|
'model_path': model_path,
|
||||||
|
'n_ctx': n_ctx,
|
||||||
|
'n_gpu_layers': n_gpu_layers,
|
||||||
|
'n_threads': n_threads,
|
||||||
|
}
|
||||||
|
elif backend == 'transformers':
|
||||||
|
self._overrides['transformers'] = {
|
||||||
|
'model_path': model_path,
|
||||||
|
}
|
||||||
|
# claude and huggingface are API-based — no path override needed
|
||||||
|
|
||||||
|
def get(self, section: str, key: str, fallback=None):
|
||||||
|
overrides = self._overrides.get(section, {})
|
||||||
|
if key in overrides:
|
||||||
|
return overrides[key]
|
||||||
|
return self._base.get(section, key, fallback)
|
||||||
|
|
||||||
|
def get_int(self, section: str, key: str, fallback: int = 0) -> int:
|
||||||
|
overrides = self._overrides.get(section, {})
|
||||||
|
if key in overrides:
|
||||||
|
try:
|
||||||
|
return int(overrides[key])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return fallback
|
||||||
|
return self._base.get_int(section, key, fallback)
|
||||||
|
|
||||||
|
def get_float(self, section: str, key: str, fallback: float = 0.0) -> float:
|
||||||
|
overrides = self._overrides.get(section, {})
|
||||||
|
if key in overrides:
|
||||||
|
try:
|
||||||
|
return float(overrides[key])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return fallback
|
||||||
|
return self._base.get_float(section, key, fallback)
|
||||||
|
|
||||||
|
def get_bool(self, section: str, key: str, fallback: bool = False) -> bool:
|
||||||
|
overrides = self._overrides.get(section, {})
|
||||||
|
if key in overrides:
|
||||||
|
val = str(overrides[key]).lower()
|
||||||
|
return val in ('true', '1', 'yes', 'on')
|
||||||
|
return self._base.get_bool(section, key, fallback)
|
||||||
|
|
||||||
|
# Delegate all settings getters to base (they call self.get internally)
|
||||||
|
def get_llama_settings(self) -> dict:
|
||||||
|
from .config import Config
|
||||||
|
return Config.get_llama_settings(self)
|
||||||
|
|
||||||
|
def get_transformers_settings(self) -> dict:
|
||||||
|
from .config import Config
|
||||||
|
return Config.get_transformers_settings(self)
|
||||||
|
|
||||||
|
def get_claude_settings(self) -> dict:
|
||||||
|
return self._base.get_claude_settings()
|
||||||
|
|
||||||
|
def get_huggingface_settings(self) -> dict:
|
||||||
|
return self._base.get_huggingface_settings()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelRouter:
|
||||||
|
"""Manages up to 3 concurrent LLM instances (SLM, SAM, LAM).
|
||||||
|
|
||||||
|
Each tier can use a different backend (local GGUF, transformers, Claude API,
|
||||||
|
HuggingFace). The router handles loading, unloading, fallback, and thread-safe
|
||||||
|
access.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
|
self.config = config or get_config()
|
||||||
|
self._instances: Dict[ModelTier, Any] = {}
|
||||||
|
self._locks: Dict[ModelTier, threading.Lock] = {
|
||||||
|
tier: threading.Lock() for tier in ModelTier
|
||||||
|
}
|
||||||
|
self._load_lock = threading.Lock()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> Dict[str, dict]:
|
||||||
|
"""Return load status of all tiers."""
|
||||||
|
result = {}
|
||||||
|
for tier in ModelTier:
|
||||||
|
inst = self._instances.get(tier)
|
||||||
|
settings = self.config.get_tier_settings(tier.value)
|
||||||
|
result[tier.value] = {
|
||||||
|
'loaded': inst is not None and inst.is_loaded,
|
||||||
|
'model_name': inst.model_name if inst and inst.is_loaded else None,
|
||||||
|
'backend': settings['backend'],
|
||||||
|
'enabled': settings['enabled'],
|
||||||
|
'model_path': settings['model_path'],
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def load_tier(self, tier: ModelTier, verbose: bool = False) -> bool:
|
||||||
|
"""Load a single tier's model. Thread-safe."""
|
||||||
|
settings = self.config.get_tier_settings(tier.value)
|
||||||
|
|
||||||
|
if not settings['enabled']:
|
||||||
|
_logger.info(f"[Router] Tier {tier.value} is disabled, skipping")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not settings['model_path'] and settings['backend'] == 'local':
|
||||||
|
_logger.warning(f"[Router] No model_path configured for {tier.value}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with self._load_lock:
|
||||||
|
# Unload existing if any
|
||||||
|
if tier in self._instances:
|
||||||
|
self.unload_tier(tier)
|
||||||
|
|
||||||
|
try:
|
||||||
|
inst = self._create_instance(tier, verbose)
|
||||||
|
self._instances[tier] = inst
|
||||||
|
_logger.info(f"[Router] Loaded {tier.value}: {inst.model_name}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"[Router] Failed to load {tier.value}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unload_tier(self, tier: ModelTier):
|
||||||
|
"""Unload a tier's model and free resources."""
|
||||||
|
inst = self._instances.pop(tier, None)
|
||||||
|
if inst:
|
||||||
|
try:
|
||||||
|
inst.unload_model()
|
||||||
|
_logger.info(f"[Router] Unloaded {tier.value}")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"[Router] Error unloading {tier.value}: {e}")
|
||||||
|
|
||||||
|
def load_all(self, verbose: bool = False) -> Dict[str, bool]:
|
||||||
|
"""Load all enabled tiers. Returns {tier_name: success}."""
|
||||||
|
results = {}
|
||||||
|
for tier in ModelTier:
|
||||||
|
results[tier.value] = self.load_tier(tier, verbose)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def unload_all(self):
|
||||||
|
"""Unload all tiers."""
|
||||||
|
for tier in list(self._instances.keys()):
|
||||||
|
self.unload_tier(tier)
|
||||||
|
|
||||||
|
def get_instance(self, tier: ModelTier):
|
||||||
|
"""Get the LLM instance for a tier (may be None if not loaded)."""
|
||||||
|
return self._instances.get(tier)
|
||||||
|
|
||||||
|
def is_tier_loaded(self, tier: ModelTier) -> bool:
|
||||||
|
"""Check if a tier has a loaded model."""
|
||||||
|
inst = self._instances.get(tier)
|
||||||
|
return inst is not None and inst.is_loaded
|
||||||
|
|
||||||
|
def classify(self, text: str) -> Dict[str, Any]:
|
||||||
|
"""Use SLM to classify/triage an event or task.
|
||||||
|
|
||||||
|
Returns: {'tier': 'sam'|'lam', 'category': str, 'urgency': str, 'reasoning': str}
|
||||||
|
|
||||||
|
Falls back to SAM tier if SLM is not loaded.
|
||||||
|
"""
|
||||||
|
classify_prompt = f"""Classify this event/task for autonomous handling.
|
||||||
|
Respond with ONLY a JSON object, no other text:
|
||||||
|
{{"tier": "sam" or "lam", "category": "defense|offense|counter|analyze|osint|simulate", "urgency": "high|medium|low", "reasoning": "brief explanation"}}
|
||||||
|
|
||||||
|
Event: {text}"""
|
||||||
|
|
||||||
|
# Try SLM first, then fallback
|
||||||
|
for tier in [ModelTier.SLM, ModelTier.SAM, ModelTier.LAM]:
|
||||||
|
inst = self._instances.get(tier)
|
||||||
|
if inst and inst.is_loaded:
|
||||||
|
try:
|
||||||
|
with self._locks[tier]:
|
||||||
|
response = inst.generate(classify_prompt, max_tokens=200, temperature=0.1)
|
||||||
|
# Parse JSON from response
|
||||||
|
response = response.strip()
|
||||||
|
# Find JSON in response
|
||||||
|
start = response.find('{')
|
||||||
|
end = response.rfind('}')
|
||||||
|
if start >= 0 and end > start:
|
||||||
|
return json.loads(response[start:end + 1])
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"[Router] Classification failed on {tier.value}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Default if all tiers fail
|
||||||
|
return {'tier': 'sam', 'category': 'defense', 'urgency': 'medium',
|
||||||
|
'reasoning': 'Default classification (no model available)'}
|
||||||
|
|
||||||
|
def generate(self, tier: ModelTier, prompt: str, **kwargs) -> str:
|
||||||
|
"""Generate with a specific tier, falling back to higher tiers on failure.
|
||||||
|
|
||||||
|
Fallback chain: SLM -> SAM -> LAM, SAM -> LAM
|
||||||
|
"""
|
||||||
|
chain = [tier] + _FALLBACK.get(tier, [])
|
||||||
|
|
||||||
|
for t in chain:
|
||||||
|
inst = self._instances.get(t)
|
||||||
|
if inst and inst.is_loaded:
|
||||||
|
try:
|
||||||
|
with self._locks[t]:
|
||||||
|
return inst.generate(prompt, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"[Router] Generate failed on {t.value}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
from .llm import LLMError
|
||||||
|
raise LLMError(f"All tiers exhausted for generation (started at {tier.value})")
|
||||||
|
|
||||||
|
def _create_instance(self, tier: ModelTier, verbose: bool = False):
|
||||||
|
"""Create an LLM instance from tier config."""
|
||||||
|
from .llm import LLM, TransformersLLM, ClaudeLLM, HuggingFaceLLM
|
||||||
|
|
||||||
|
section = tier.value
|
||||||
|
backend = self.config.get(section, 'backend', 'local')
|
||||||
|
proxy = _TierConfigProxy(self.config, section)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
model_path = self.config.get(section, 'model_path', '')
|
||||||
|
_logger.info(f"[Router] Creating {tier.value} instance: backend={backend}, model={model_path}")
|
||||||
|
|
||||||
|
if backend == 'local':
|
||||||
|
inst = LLM(proxy)
|
||||||
|
elif backend == 'transformers':
|
||||||
|
inst = TransformersLLM(proxy)
|
||||||
|
elif backend == 'claude':
|
||||||
|
inst = ClaudeLLM(proxy)
|
||||||
|
elif backend == 'huggingface':
|
||||||
|
inst = HuggingFaceLLM(proxy)
|
||||||
|
else:
|
||||||
|
from .llm import LLMError
|
||||||
|
raise LLMError(f"Unknown backend '{backend}' for tier {tier.value}")
|
||||||
|
|
||||||
|
inst.load_model(verbose=verbose)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton
|
||||||
|
_router_instance = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_router() -> ModelRouter:
|
||||||
|
"""Get the global ModelRouter instance."""
|
||||||
|
global _router_instance
|
||||||
|
if _router_instance is None:
|
||||||
|
_router_instance = ModelRouter()
|
||||||
|
return _router_instance
|
||||||
|
|
||||||
|
|
||||||
|
def reset_model_router():
|
||||||
|
"""Reset the global ModelRouter (unloads all models)."""
|
||||||
|
global _router_instance
|
||||||
|
if _router_instance is not None:
|
||||||
|
_router_instance.unload_all()
|
||||||
|
_router_instance = None
|
||||||
333
core/rules.py
Normal file
333
core/rules.py
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
"""
|
||||||
|
AUTARCH Automation Rules Engine
|
||||||
|
Condition-action rules for autonomous threat response.
|
||||||
|
|
||||||
|
Rules are JSON-serializable and stored in data/automation_rules.json.
|
||||||
|
The engine evaluates conditions against a threat context dict and returns
|
||||||
|
matching rules with resolved action parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import ipaddress
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
|
||||||
|
_logger = logging.getLogger('autarch.rules')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Rule:
|
||||||
|
"""A single automation rule."""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
enabled: bool = True
|
||||||
|
priority: int = 50 # 0=highest, 100=lowest
|
||||||
|
conditions: List[Dict] = field(default_factory=list) # AND-combined
|
||||||
|
actions: List[Dict] = field(default_factory=list)
|
||||||
|
cooldown_seconds: int = 60
|
||||||
|
last_triggered: Optional[str] = None # ISO timestamp
|
||||||
|
created: Optional[str] = None
|
||||||
|
description: str = ''
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: dict) -> 'Rule':
|
||||||
|
return cls(
|
||||||
|
id=d.get('id', str(uuid.uuid4())[:8]),
|
||||||
|
name=d.get('name', 'Untitled'),
|
||||||
|
enabled=d.get('enabled', True),
|
||||||
|
priority=d.get('priority', 50),
|
||||||
|
conditions=d.get('conditions', []),
|
||||||
|
actions=d.get('actions', []),
|
||||||
|
cooldown_seconds=d.get('cooldown_seconds', 60),
|
||||||
|
last_triggered=d.get('last_triggered'),
|
||||||
|
created=d.get('created'),
|
||||||
|
description=d.get('description', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RulesEngine:
|
||||||
|
"""Evaluates automation rules against a threat context."""
|
||||||
|
|
||||||
|
RULES_PATH = Path(__file__).parent.parent / 'data' / 'automation_rules.json'
|
||||||
|
|
||||||
|
CONDITION_TYPES = {
|
||||||
|
'threat_score_above', 'threat_score_below', 'threat_level_is',
|
||||||
|
'port_scan_detected', 'ddos_detected', 'ddos_attack_type',
|
||||||
|
'connection_from_ip', 'connection_count_above',
|
||||||
|
'new_listening_port', 'bandwidth_rx_above_mbps',
|
||||||
|
'arp_spoof_detected', 'schedule', 'always',
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_TYPES = {
|
||||||
|
'block_ip', 'unblock_ip', 'rate_limit_ip', 'block_port',
|
||||||
|
'kill_process', 'alert', 'log_event', 'run_shell',
|
||||||
|
'run_module', 'counter_scan', 'escalate_to_lam',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._rules: List[Rule] = []
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"""Load rules from JSON file."""
|
||||||
|
if not self.RULES_PATH.exists():
|
||||||
|
self._rules = []
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = json.loads(self.RULES_PATH.read_text(encoding='utf-8'))
|
||||||
|
self._rules = [Rule.from_dict(r) for r in data.get('rules', [])]
|
||||||
|
_logger.info(f"[Rules] Loaded {len(self._rules)} rules")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"[Rules] Failed to load rules: {e}")
|
||||||
|
self._rules = []
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Save rules to JSON file."""
|
||||||
|
self.RULES_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
data = {
|
||||||
|
'version': 1,
|
||||||
|
'rules': [r.to_dict() for r in self._rules],
|
||||||
|
}
|
||||||
|
self.RULES_PATH.write_text(json.dumps(data, indent=2), encoding='utf-8')
|
||||||
|
|
||||||
|
def add_rule(self, rule: Rule) -> Rule:
|
||||||
|
if not rule.created:
|
||||||
|
rule.created = datetime.now().isoformat()
|
||||||
|
self._rules.append(rule)
|
||||||
|
self._rules.sort(key=lambda r: r.priority)
|
||||||
|
self.save()
|
||||||
|
return rule
|
||||||
|
|
||||||
|
def update_rule(self, rule_id: str, updates: dict) -> Optional[Rule]:
|
||||||
|
for rule in self._rules:
|
||||||
|
if rule.id == rule_id:
|
||||||
|
for key, value in updates.items():
|
||||||
|
if hasattr(rule, key) and key != 'id':
|
||||||
|
setattr(rule, key, value)
|
||||||
|
self._rules.sort(key=lambda r: r.priority)
|
||||||
|
self.save()
|
||||||
|
return rule
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_rule(self, rule_id: str) -> bool:
|
||||||
|
before = len(self._rules)
|
||||||
|
self._rules = [r for r in self._rules if r.id != rule_id]
|
||||||
|
if len(self._rules) < before:
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_rule(self, rule_id: str) -> Optional[Rule]:
|
||||||
|
for rule in self._rules:
|
||||||
|
if rule.id == rule_id:
|
||||||
|
return rule
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_rules(self) -> List[Rule]:
|
||||||
|
return list(self._rules)
|
||||||
|
|
||||||
|
def evaluate(self, context: Dict[str, Any]) -> List[Tuple[Rule, List[Dict]]]:
|
||||||
|
"""Evaluate all enabled rules against a threat context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Dict with keys from ThreatMonitor / AutonomyDaemon:
|
||||||
|
- threat_score: {'score': int, 'level': str, 'details': [...]}
|
||||||
|
- connection_count: int
|
||||||
|
- connections: [...]
|
||||||
|
- ddos: {'under_attack': bool, 'attack_type': str, ...}
|
||||||
|
- new_ports: [{'port': int, 'process': str}, ...]
|
||||||
|
- arp_alerts: [...]
|
||||||
|
- bandwidth: {'rx_mbps': float, 'tx_mbps': float}
|
||||||
|
- scan_indicators: int
|
||||||
|
- timestamp: str
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (Rule, resolved_actions) for rules that match and aren't in cooldown.
|
||||||
|
"""
|
||||||
|
matches = []
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
for rule in self._rules:
|
||||||
|
if not rule.enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check cooldown
|
||||||
|
if rule.last_triggered:
|
||||||
|
try:
|
||||||
|
last = datetime.fromisoformat(rule.last_triggered)
|
||||||
|
if (now - last).total_seconds() < rule.cooldown_seconds:
|
||||||
|
continue
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Evaluate all conditions (AND logic)
|
||||||
|
if not rule.conditions:
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_match = all(
|
||||||
|
self._evaluate_condition(cond, context)
|
||||||
|
for cond in rule.conditions
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_match:
|
||||||
|
# Resolve action variables
|
||||||
|
resolved = [self._resolve_variables(a, context) for a in rule.actions]
|
||||||
|
matches.append((rule, resolved))
|
||||||
|
|
||||||
|
# Mark triggered
|
||||||
|
rule.last_triggered = now.isoformat()
|
||||||
|
|
||||||
|
# Save updated trigger times
|
||||||
|
if matches:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
def _evaluate_condition(self, condition: dict, context: dict) -> bool:
|
||||||
|
"""Evaluate a single condition against context."""
|
||||||
|
ctype = condition.get('type', '')
|
||||||
|
value = condition.get('value')
|
||||||
|
|
||||||
|
if ctype == 'threat_score_above':
|
||||||
|
return context.get('threat_score', {}).get('score', 0) > (value or 0)
|
||||||
|
|
||||||
|
elif ctype == 'threat_score_below':
|
||||||
|
return context.get('threat_score', {}).get('score', 0) < (value or 100)
|
||||||
|
|
||||||
|
elif ctype == 'threat_level_is':
|
||||||
|
return context.get('threat_score', {}).get('level', 'LOW') == (value or 'HIGH')
|
||||||
|
|
||||||
|
elif ctype == 'port_scan_detected':
|
||||||
|
return context.get('scan_indicators', 0) > 0
|
||||||
|
|
||||||
|
elif ctype == 'ddos_detected':
|
||||||
|
return context.get('ddos', {}).get('under_attack', False)
|
||||||
|
|
||||||
|
elif ctype == 'ddos_attack_type':
|
||||||
|
return context.get('ddos', {}).get('attack_type', '') == (value or '')
|
||||||
|
|
||||||
|
elif ctype == 'connection_from_ip':
|
||||||
|
return self._check_ip_match(value, context.get('connections', []))
|
||||||
|
|
||||||
|
elif ctype == 'connection_count_above':
|
||||||
|
return context.get('connection_count', 0) > (value or 0)
|
||||||
|
|
||||||
|
elif ctype == 'new_listening_port':
|
||||||
|
return len(context.get('new_ports', [])) > 0
|
||||||
|
|
||||||
|
elif ctype == 'bandwidth_rx_above_mbps':
|
||||||
|
return context.get('bandwidth', {}).get('rx_mbps', 0) > (value or 0)
|
||||||
|
|
||||||
|
elif ctype == 'arp_spoof_detected':
|
||||||
|
return len(context.get('arp_alerts', [])) > 0
|
||||||
|
|
||||||
|
elif ctype == 'schedule':
|
||||||
|
return self._check_cron(condition.get('cron', ''))
|
||||||
|
|
||||||
|
elif ctype == 'always':
|
||||||
|
return True
|
||||||
|
|
||||||
|
_logger.warning(f"[Rules] Unknown condition type: {ctype}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_ip_match(self, pattern: str, connections: list) -> bool:
|
||||||
|
"""Check if any connection's remote IP matches a pattern (IP or CIDR)."""
|
||||||
|
if not pattern:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
network = ipaddress.ip_network(pattern, strict=False)
|
||||||
|
for conn in connections:
|
||||||
|
remote = conn.get('remote_addr', '')
|
||||||
|
if remote and remote not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
|
||||||
|
try:
|
||||||
|
if ipaddress.ip_address(remote) in network:
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
# Not a valid IP/CIDR, try exact match
|
||||||
|
return any(conn.get('remote_addr') == pattern for conn in connections)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_cron(self, cron_expr: str) -> bool:
|
||||||
|
"""Minimal 5-field cron matcher: minute hour day month weekday.
|
||||||
|
|
||||||
|
Supports * and */N. Does not support ranges or lists.
|
||||||
|
"""
|
||||||
|
if not cron_expr:
|
||||||
|
return False
|
||||||
|
|
||||||
|
parts = cron_expr.strip().split()
|
||||||
|
if len(parts) != 5:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
current = [now.minute, now.hour, now.day, now.month, now.isoweekday() % 7]
|
||||||
|
|
||||||
|
for field_val, pattern in zip(current, parts):
|
||||||
|
if pattern == '*':
|
||||||
|
continue
|
||||||
|
if pattern.startswith('*/'):
|
||||||
|
try:
|
||||||
|
step = int(pattern[2:])
|
||||||
|
if step > 0 and field_val % step != 0:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if field_val != int(pattern):
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _resolve_variables(self, action: dict, context: dict) -> dict:
|
||||||
|
"""Replace $variable placeholders in action parameters with context values."""
|
||||||
|
resolved = {}
|
||||||
|
|
||||||
|
# Build variable map from context
|
||||||
|
variables = {
|
||||||
|
'$threat_score': str(context.get('threat_score', {}).get('score', 0)),
|
||||||
|
'$threat_level': context.get('threat_score', {}).get('level', 'LOW'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Source IP = top talker (most connections)
|
||||||
|
connections = context.get('connections', [])
|
||||||
|
if connections:
|
||||||
|
ip_counts = {}
|
||||||
|
for c in connections:
|
||||||
|
rip = c.get('remote_addr', '')
|
||||||
|
if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
|
||||||
|
ip_counts[rip] = ip_counts.get(rip, 0) + 1
|
||||||
|
if ip_counts:
|
||||||
|
variables['$source_ip'] = max(ip_counts, key=ip_counts.get)
|
||||||
|
|
||||||
|
# New port
|
||||||
|
new_ports = context.get('new_ports', [])
|
||||||
|
if new_ports:
|
||||||
|
variables['$new_port'] = str(new_ports[0].get('port', ''))
|
||||||
|
variables['$suspicious_pid'] = str(new_ports[0].get('pid', ''))
|
||||||
|
|
||||||
|
# DDoS attack type
|
||||||
|
ddos = context.get('ddos', {})
|
||||||
|
if ddos:
|
||||||
|
variables['$attack_type'] = ddos.get('attack_type', 'unknown')
|
||||||
|
|
||||||
|
# Resolve in all string values
|
||||||
|
for key, val in action.items():
|
||||||
|
if isinstance(val, str):
|
||||||
|
for var_name, var_val in variables.items():
|
||||||
|
val = val.replace(var_name, var_val)
|
||||||
|
resolved[key] = val
|
||||||
|
|
||||||
|
return resolved
|
||||||
@ -65,6 +65,7 @@ def create_app():
|
|||||||
from web.routes.targets import targets_bp
|
from web.routes.targets import targets_bp
|
||||||
from web.routes.encmodules import encmodules_bp
|
from web.routes.encmodules import encmodules_bp
|
||||||
from web.routes.llm_trainer import llm_trainer_bp
|
from web.routes.llm_trainer import llm_trainer_bp
|
||||||
|
from web.routes.autonomy import autonomy_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
@ -89,6 +90,7 @@ def create_app():
|
|||||||
app.register_blueprint(targets_bp)
|
app.register_blueprint(targets_bp)
|
||||||
app.register_blueprint(encmodules_bp)
|
app.register_blueprint(encmodules_bp)
|
||||||
app.register_blueprint(llm_trainer_bp)
|
app.register_blueprint(llm_trainer_bp)
|
||||||
|
app.register_blueprint(autonomy_bp)
|
||||||
|
|
||||||
# Start network discovery advertising (mDNS + Bluetooth)
|
# Start network discovery advertising (mDNS + Bluetooth)
|
||||||
try:
|
try:
|
||||||
|
|||||||
241
web/routes/autonomy.py
Normal file
241
web/routes/autonomy.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
"""Autonomy routes — daemon control, model management, rules CRUD, activity log."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
|
||||||
|
from web.auth import login_required
|
||||||
|
|
||||||
|
autonomy_bp = Blueprint('autonomy', __name__, url_prefix='/autonomy')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_daemon():
|
||||||
|
from core.autonomy import get_autonomy_daemon
|
||||||
|
return get_autonomy_daemon()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_router():
|
||||||
|
from core.model_router import get_model_router
|
||||||
|
return get_model_router()
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== PAGES ====================
|
||||||
|
|
||||||
|
@autonomy_bp.route('/')
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
return render_template('autonomy.html')
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== DAEMON CONTROL ====================
|
||||||
|
|
||||||
|
@autonomy_bp.route('/status')
|
||||||
|
@login_required
|
||||||
|
def status():
|
||||||
|
daemon = _get_daemon()
|
||||||
|
router = _get_router()
|
||||||
|
return jsonify({
|
||||||
|
'daemon': daemon.status,
|
||||||
|
'models': router.status,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/start', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def start():
|
||||||
|
daemon = _get_daemon()
|
||||||
|
ok = daemon.start()
|
||||||
|
return jsonify({'success': ok, 'status': daemon.status})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/stop', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def stop():
|
||||||
|
daemon = _get_daemon()
|
||||||
|
daemon.stop()
|
||||||
|
return jsonify({'success': True, 'status': daemon.status})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/pause', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def pause():
|
||||||
|
daemon = _get_daemon()
|
||||||
|
daemon.pause()
|
||||||
|
return jsonify({'success': True, 'status': daemon.status})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/resume', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def resume():
|
||||||
|
daemon = _get_daemon()
|
||||||
|
daemon.resume()
|
||||||
|
return jsonify({'success': True, 'status': daemon.status})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== MODELS ====================
|
||||||
|
|
||||||
|
@autonomy_bp.route('/models')
|
||||||
|
@login_required
|
||||||
|
def models():
|
||||||
|
return jsonify(_get_router().status)
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/models/load/<tier>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def models_load(tier):
|
||||||
|
from core.model_router import ModelTier
|
||||||
|
try:
|
||||||
|
mt = ModelTier(tier)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({'error': f'Invalid tier: {tier}'}), 400
|
||||||
|
ok = _get_router().load_tier(mt, verbose=True)
|
||||||
|
return jsonify({'success': ok, 'models': _get_router().status})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/models/unload/<tier>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def models_unload(tier):
|
||||||
|
from core.model_router import ModelTier
|
||||||
|
try:
|
||||||
|
mt = ModelTier(tier)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({'error': f'Invalid tier: {tier}'}), 400
|
||||||
|
_get_router().unload_tier(mt)
|
||||||
|
return jsonify({'success': True, 'models': _get_router().status})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== RULES ====================
|
||||||
|
|
||||||
|
@autonomy_bp.route('/rules')
|
||||||
|
@login_required
|
||||||
|
def rules_list():
|
||||||
|
daemon = _get_daemon()
|
||||||
|
rules = daemon.rules_engine.get_all_rules()
|
||||||
|
return jsonify({'rules': [r.to_dict() for r in rules]})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/rules', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def rules_create():
|
||||||
|
from core.rules import Rule
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
rule = Rule.from_dict(data)
|
||||||
|
daemon = _get_daemon()
|
||||||
|
daemon.rules_engine.add_rule(rule)
|
||||||
|
return jsonify({'success': True, 'rule': rule.to_dict()})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/rules/<rule_id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def rules_update(rule_id):
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
daemon = _get_daemon()
|
||||||
|
rule = daemon.rules_engine.update_rule(rule_id, data)
|
||||||
|
if rule:
|
||||||
|
return jsonify({'success': True, 'rule': rule.to_dict()})
|
||||||
|
return jsonify({'error': 'Rule not found'}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/rules/<rule_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def rules_delete(rule_id):
|
||||||
|
daemon = _get_daemon()
|
||||||
|
ok = daemon.rules_engine.delete_rule(rule_id)
|
||||||
|
return jsonify({'success': ok})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/templates')
|
||||||
|
@login_required
|
||||||
|
def rule_templates():
|
||||||
|
"""Pre-built rule templates for common scenarios."""
|
||||||
|
templates = [
|
||||||
|
{
|
||||||
|
'name': 'Auto-Block Port Scanners',
|
||||||
|
'description': 'Block IPs that trigger port scan detection',
|
||||||
|
'conditions': [{'type': 'port_scan_detected'}],
|
||||||
|
'actions': [
|
||||||
|
{'type': 'block_ip', 'ip': '$source_ip'},
|
||||||
|
{'type': 'alert', 'message': 'Blocked scanner: $source_ip'},
|
||||||
|
],
|
||||||
|
'priority': 10,
|
||||||
|
'cooldown_seconds': 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'DDoS Auto-Response',
|
||||||
|
'description': 'Rate-limit top talkers during DDoS attacks',
|
||||||
|
'conditions': [{'type': 'ddos_detected'}],
|
||||||
|
'actions': [
|
||||||
|
{'type': 'rate_limit_ip', 'ip': '$source_ip', 'rate': '10/s'},
|
||||||
|
{'type': 'alert', 'message': 'DDoS mitigated: $attack_type from $source_ip'},
|
||||||
|
],
|
||||||
|
'priority': 5,
|
||||||
|
'cooldown_seconds': 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'High Threat Alert',
|
||||||
|
'description': 'Send alert when threat score exceeds threshold',
|
||||||
|
'conditions': [{'type': 'threat_score_above', 'value': 60}],
|
||||||
|
'actions': [
|
||||||
|
{'type': 'alert', 'message': 'Threat score: $threat_score ($threat_level)'},
|
||||||
|
],
|
||||||
|
'priority': 20,
|
||||||
|
'cooldown_seconds': 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'New Port Investigation',
|
||||||
|
'description': 'Use SAM agent to investigate new listening ports',
|
||||||
|
'conditions': [{'type': 'new_listening_port'}],
|
||||||
|
'actions': [
|
||||||
|
{'type': 'escalate_to_lam', 'task': 'Investigate new listening port $new_port (PID $suspicious_pid). Determine if this is legitimate or suspicious.'},
|
||||||
|
],
|
||||||
|
'priority': 30,
|
||||||
|
'cooldown_seconds': 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Bandwidth Spike Alert',
|
||||||
|
'description': 'Alert on unusual inbound bandwidth',
|
||||||
|
'conditions': [{'type': 'bandwidth_rx_above_mbps', 'value': 100}],
|
||||||
|
'actions': [
|
||||||
|
{'type': 'alert', 'message': 'Bandwidth spike detected (>100 Mbps RX)'},
|
||||||
|
],
|
||||||
|
'priority': 25,
|
||||||
|
'cooldown_seconds': 60,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return jsonify({'templates': templates})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== ACTIVITY LOG ====================
|
||||||
|
|
||||||
|
@autonomy_bp.route('/activity')
|
||||||
|
@login_required
|
||||||
|
def activity():
|
||||||
|
limit = request.args.get('limit', 50, type=int)
|
||||||
|
offset = request.args.get('offset', 0, type=int)
|
||||||
|
daemon = _get_daemon()
|
||||||
|
entries = daemon.get_activity(limit=limit, offset=offset)
|
||||||
|
return jsonify({'entries': entries, 'total': daemon.get_activity_count()})
|
||||||
|
|
||||||
|
|
||||||
|
@autonomy_bp.route('/activity/stream')
|
||||||
|
@login_required
|
||||||
|
def activity_stream():
|
||||||
|
"""SSE stream of live activity entries."""
|
||||||
|
daemon = _get_daemon()
|
||||||
|
q = daemon.subscribe()
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = q.get(timeout=30)
|
||||||
|
yield f'data: {data}\n\n'
|
||||||
|
except Exception:
|
||||||
|
# Send keepalive
|
||||||
|
yield f'data: {{"type":"keepalive"}}\n\n'
|
||||||
|
finally:
|
||||||
|
daemon.unsubscribe(q)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
stream_with_context(generate()),
|
||||||
|
mimetype='text/event-stream',
|
||||||
|
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'},
|
||||||
|
)
|
||||||
742
web/templates/autonomy.html
Normal file
742
web/templates/autonomy.html
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Autonomy - AUTARCH{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 style="color:var(--accent)">Autonomy</h1>
|
||||||
|
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||||||
|
Multi-model autonomous threat response — SLM / SAM / LAM
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div style="display:flex;gap:0;border-bottom:2px solid var(--border);margin-bottom:1.5rem">
|
||||||
|
<button class="auto-tab active" onclick="autoTab('dashboard')" id="tab-dashboard">Dashboard</button>
|
||||||
|
<button class="auto-tab" onclick="autoTab('rules')" id="tab-rules">Rules</button>
|
||||||
|
<button class="auto-tab" onclick="autoTab('activity')" id="tab-activity">Activity Log</button>
|
||||||
|
<button class="auto-tab" onclick="autoTab('models')" id="tab-models">Models</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== DASHBOARD TAB ==================== -->
|
||||||
|
<div id="panel-dashboard" class="auto-panel">
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="section">
|
||||||
|
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||||
|
<div id="daemon-status-badge" style="padding:6px 16px;border-radius:20px;font-size:0.8rem;font-weight:600;background:var(--bg-input);border:1px solid var(--border)">
|
||||||
|
STOPPED
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="autoStart()" id="btn-start">Start</button>
|
||||||
|
<button class="btn btn-danger" onclick="autoStop()" id="btn-stop" disabled>Stop</button>
|
||||||
|
<button class="btn" onclick="autoPause()" id="btn-pause" disabled style="background:var(--bg-input);border:1px solid var(--border)">Pause</button>
|
||||||
|
<button class="btn" onclick="autoResume()" id="btn-resume" disabled style="background:var(--bg-input);border:1px solid var(--border)">Resume</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1.5rem">
|
||||||
|
<div class="tool-card" style="text-align:center">
|
||||||
|
<div style="font-size:2rem;font-weight:700;color:var(--accent)" id="stat-agents">0</div>
|
||||||
|
<div style="font-size:0.8rem;color:var(--text-secondary)">Active Agents</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-card" style="text-align:center">
|
||||||
|
<div style="font-size:2rem;font-weight:700;color:var(--success)" id="stat-rules">0</div>
|
||||||
|
<div style="font-size:0.8rem;color:var(--text-secondary)">Rules</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-card" style="text-align:center">
|
||||||
|
<div style="font-size:2rem;font-weight:700;color:var(--warning)" id="stat-activity">0</div>
|
||||||
|
<div style="font-size:0.8rem;color:var(--text-secondary)">Activity Entries</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Tier Cards -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Model Tiers</h2>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem">
|
||||||
|
<div class="tool-card" id="card-slm">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<h4>SLM <span style="font-size:0.7rem;color:var(--text-muted);font-weight:400">Small Language Model</span></h4>
|
||||||
|
<span class="tier-dot" id="dot-slm"></span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.5rem 0">Fast classification, routing, yes/no decisions</p>
|
||||||
|
<div style="font-size:0.75rem;color:var(--text-muted)" id="slm-model">No model configured</div>
|
||||||
|
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-small btn-primary" onclick="autoLoadTier('slm')">Load</button>
|
||||||
|
<button class="btn btn-small" onclick="autoUnloadTier('slm')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-card" id="card-sam">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<h4>SAM <span style="font-size:0.7rem;color:var(--text-muted);font-weight:400">Small Action Model</span></h4>
|
||||||
|
<span class="tier-dot" id="dot-sam"></span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.5rem 0">Quick tool execution, simple automated responses</p>
|
||||||
|
<div style="font-size:0.75rem;color:var(--text-muted)" id="sam-model">No model configured</div>
|
||||||
|
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-small btn-primary" onclick="autoLoadTier('sam')">Load</button>
|
||||||
|
<button class="btn btn-small" onclick="autoUnloadTier('sam')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-card" id="card-lam">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<h4>LAM <span style="font-size:0.7rem;color:var(--text-muted);font-weight:400">Large Action Model</span></h4>
|
||||||
|
<span class="tier-dot" id="dot-lam"></span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.5rem 0">Complex multi-step agent tasks, strategic planning</p>
|
||||||
|
<div style="font-size:0.75rem;color:var(--text-muted)" id="lam-model">No model configured</div>
|
||||||
|
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-small btn-primary" onclick="autoLoadTier('lam')">Load</button>
|
||||||
|
<button class="btn btn-small" onclick="autoUnloadTier('lam')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== RULES TAB ==================== -->
|
||||||
|
<div id="panel-rules" class="auto-panel" style="display:none">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
|
||||||
|
<h2>Automation Rules</h2>
|
||||||
|
<div style="display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-primary" onclick="autoShowRuleModal()">+ Add Rule</button>
|
||||||
|
<button class="btn" onclick="autoShowTemplates()" style="background:var(--bg-input);border:1px solid var(--border)">Templates</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="data-table" id="rules-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:30px"></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Conditions</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
<th>Cooldown</th>
|
||||||
|
<th style="width:100px">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="rules-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="rules-empty" style="text-align:center;padding:2rem;color:var(--text-muted);display:none">
|
||||||
|
No rules configured. Add a rule or use a template to get started.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== ACTIVITY TAB ==================== -->
|
||||||
|
<div id="panel-activity" class="auto-panel" style="display:none">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
||||||
|
<h2>Activity Log</h2>
|
||||||
|
<div style="display:flex;gap:0.5rem;align-items:center">
|
||||||
|
<span id="activity-live-dot" class="live-dot"></span>
|
||||||
|
<span style="font-size:0.8rem;color:var(--text-secondary)" id="activity-count">0 entries</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="activity-log" style="max-height:600px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-secondary)">
|
||||||
|
<table class="data-table" style="margin:0">
|
||||||
|
<thead><tr>
|
||||||
|
<th style="width:150px">Time</th>
|
||||||
|
<th style="width:80px">Status</th>
|
||||||
|
<th style="width:100px">Action</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
<th style="width:80px">Rule</th>
|
||||||
|
<th style="width:50px">Tier</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="activity-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== MODELS TAB ==================== -->
|
||||||
|
<div id="panel-models" class="auto-panel" style="display:none">
|
||||||
|
<h2 style="margin-bottom:1rem">Model Configuration</h2>
|
||||||
|
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1.5rem">
|
||||||
|
Configure model tiers in <code>autarch_settings.conf</code> under <code>[slm]</code>, <code>[sam]</code>, and <code>[lam]</code> sections.
|
||||||
|
Each tier supports backends: <code>local</code> (GGUF), <code>transformers</code>, <code>claude</code>, <code>huggingface</code>.
|
||||||
|
</p>
|
||||||
|
<div id="models-detail" style="display:grid;grid-template-columns:1fr;gap:1rem"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== RULE EDITOR MODAL ==================== -->
|
||||||
|
<div id="rule-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1000;display:none;align-items:center;justify-content:center">
|
||||||
|
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:1.5rem;width:90%;max-width:700px;max-height:85vh;overflow-y:auto">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
||||||
|
<h3 id="rule-modal-title">Add Rule</h3>
|
||||||
|
<button onclick="autoCloseRuleModal()" style="background:none;border:none;color:var(--text-secondary);font-size:1.2rem;cursor:pointer">✕</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="rule-edit-id">
|
||||||
|
<div style="display:grid;gap:1rem">
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Name</label>
|
||||||
|
<input type="text" id="rule-name" class="form-input" placeholder="Rule name">
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Priority (0=highest)</label>
|
||||||
|
<input type="number" id="rule-priority" class="form-input" value="50" min="0" max="100">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Cooldown (seconds)</label>
|
||||||
|
<input type="number" id="rule-cooldown" class="form-input" value="60" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Description</label>
|
||||||
|
<input type="text" id="rule-description" class="form-input" placeholder="Optional description">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conditions -->
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
||||||
|
<label style="font-size:0.8rem;color:var(--text-secondary)">Conditions (AND logic)</label>
|
||||||
|
<button class="btn btn-small" onclick="autoAddCondition()" style="background:var(--bg-input);border:1px solid var(--border);font-size:0.75rem">+ Condition</button>
|
||||||
|
</div>
|
||||||
|
<div id="conditions-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
||||||
|
<label style="font-size:0.8rem;color:var(--text-secondary)">Actions</label>
|
||||||
|
<button class="btn btn-small" onclick="autoAddAction()" style="background:var(--bg-input);border:1px solid var(--border);font-size:0.75rem">+ Action</button>
|
||||||
|
</div>
|
||||||
|
<div id="actions-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.5rem">
|
||||||
|
<button class="btn" onclick="autoCloseRuleModal()" style="background:var(--bg-input);border:1px solid var(--border)">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="autoSaveRule()">Save Rule</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== TEMPLATES MODAL ==================== -->
|
||||||
|
<div id="templates-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1000;align-items:center;justify-content:center">
|
||||||
|
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:1.5rem;width:90%;max-width:600px;max-height:80vh;overflow-y:auto">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
||||||
|
<h3>Rule Templates</h3>
|
||||||
|
<button onclick="autoCloseTemplates()" style="background:none;border:none;color:var(--text-secondary);font-size:1.2rem;cursor:pointer">✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="templates-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.auto-tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.auto-tab:hover { color: var(--text-primary); }
|
||||||
|
.auto-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||||
|
.tier-dot {
|
||||||
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background: var(--text-muted);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.tier-dot.loaded { background: var(--success); box-shadow: 0 0 6px var(--success); }
|
||||||
|
.live-dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
background: var(--text-muted);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.live-dot.active { background: var(--success); animation: pulse 2s infinite; }
|
||||||
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.form-input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
.cond-row, .action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
.cond-row select, .action-row select {
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.cond-row input, .action-row input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.row-remove {
|
||||||
|
background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem; padding: 0 4px;
|
||||||
|
}
|
||||||
|
.activity-success { color: var(--success); }
|
||||||
|
.activity-fail { color: var(--danger); }
|
||||||
|
.activity-system { color: var(--text-muted); font-style: italic; }
|
||||||
|
.template-card {
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.template-card:hover { border-color: var(--accent); }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ==================== TAB NAVIGATION ====================
|
||||||
|
function autoTab(tab) {
|
||||||
|
document.querySelectorAll('.auto-panel').forEach(p => p.style.display = 'none');
|
||||||
|
document.querySelectorAll('.auto-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.getElementById('panel-' + tab).style.display = 'block';
|
||||||
|
document.getElementById('tab-' + tab).classList.add('active');
|
||||||
|
if (tab === 'activity') autoLoadActivity();
|
||||||
|
if (tab === 'models') autoLoadModelsDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== STATUS REFRESH ====================
|
||||||
|
let _autoRefreshTimer = null;
|
||||||
|
|
||||||
|
function autoRefreshStatus() {
|
||||||
|
fetch('/autonomy/status').then(r => r.json()).then(data => {
|
||||||
|
const d = data.daemon;
|
||||||
|
const badge = document.getElementById('daemon-status-badge');
|
||||||
|
if (d.running && !d.paused) {
|
||||||
|
badge.textContent = 'RUNNING';
|
||||||
|
badge.style.background = 'rgba(34,197,94,0.15)';
|
||||||
|
badge.style.borderColor = 'var(--success)';
|
||||||
|
badge.style.color = 'var(--success)';
|
||||||
|
} else if (d.running && d.paused) {
|
||||||
|
badge.textContent = 'PAUSED';
|
||||||
|
badge.style.background = 'rgba(245,158,11,0.15)';
|
||||||
|
badge.style.borderColor = 'var(--warning)';
|
||||||
|
badge.style.color = 'var(--warning)';
|
||||||
|
} else {
|
||||||
|
badge.textContent = 'STOPPED';
|
||||||
|
badge.style.background = 'var(--bg-input)';
|
||||||
|
badge.style.borderColor = 'var(--border)';
|
||||||
|
badge.style.color = 'var(--text-muted)';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-start').disabled = d.running;
|
||||||
|
document.getElementById('btn-stop').disabled = !d.running;
|
||||||
|
document.getElementById('btn-pause').disabled = !d.running || d.paused;
|
||||||
|
document.getElementById('btn-resume').disabled = !d.running || !d.paused;
|
||||||
|
|
||||||
|
document.getElementById('stat-agents').textContent = d.active_agents;
|
||||||
|
document.getElementById('stat-rules').textContent = d.rules_count;
|
||||||
|
document.getElementById('stat-activity').textContent = d.activity_count;
|
||||||
|
|
||||||
|
// Update model dots
|
||||||
|
const models = data.models;
|
||||||
|
for (const tier of ['slm', 'sam', 'lam']) {
|
||||||
|
const dot = document.getElementById('dot-' + tier);
|
||||||
|
const label = document.getElementById(tier + '-model');
|
||||||
|
if (models[tier]) {
|
||||||
|
if (models[tier].loaded) {
|
||||||
|
dot.classList.add('loaded');
|
||||||
|
label.textContent = models[tier].model_name || models[tier].model_path || 'Loaded';
|
||||||
|
} else {
|
||||||
|
dot.classList.remove('loaded');
|
||||||
|
label.textContent = models[tier].model_path || 'No model configured';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoStartRefresh() {
|
||||||
|
autoRefreshStatus();
|
||||||
|
_autoRefreshTimer = setInterval(autoRefreshStatus, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DAEMON CONTROLS ====================
|
||||||
|
function autoStart() {
|
||||||
|
fetch('/autonomy/start', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
|
||||||
|
}
|
||||||
|
function autoStop() {
|
||||||
|
fetch('/autonomy/stop', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
|
||||||
|
}
|
||||||
|
function autoPause() {
|
||||||
|
fetch('/autonomy/pause', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
|
||||||
|
}
|
||||||
|
function autoResume() {
|
||||||
|
fetch('/autonomy/resume', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MODEL CONTROLS ====================
|
||||||
|
function autoLoadTier(tier) {
|
||||||
|
fetch('/autonomy/models/load/' + tier, {method:'POST'}).then(r => r.json()).then(data => {
|
||||||
|
autoRefreshStatus();
|
||||||
|
if (data.success === false) alert('Failed to load ' + tier.toUpperCase() + ' tier. Check model configuration.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function autoUnloadTier(tier) {
|
||||||
|
fetch('/autonomy/models/unload/' + tier, {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoLoadModelsDetail() {
|
||||||
|
fetch('/autonomy/models').then(r => r.json()).then(data => {
|
||||||
|
const container = document.getElementById('models-detail');
|
||||||
|
let html = '';
|
||||||
|
for (const [tier, info] of Object.entries(data)) {
|
||||||
|
const status = info.loaded ? '<span style="color:var(--success)">Loaded</span>' : '<span style="color:var(--text-muted)">Not loaded</span>';
|
||||||
|
html += `<div class="tool-card">
|
||||||
|
<h4>${tier.toUpperCase()}</h4>
|
||||||
|
<table class="data-table" style="max-width:100%;margin-top:0.5rem">
|
||||||
|
<tr><td>Status</td><td>${status}</td></tr>
|
||||||
|
<tr><td>Backend</td><td><code>${info.backend}</code></td></tr>
|
||||||
|
<tr><td>Model</td><td style="word-break:break-all">${info.model_name || info.model_path || '<em>Not configured</em>'}</td></tr>
|
||||||
|
<tr><td>Enabled</td><td>${info.enabled ? 'Yes' : 'No'}</td></tr>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-small btn-primary" onclick="autoLoadTier('${tier}')">Load</button>
|
||||||
|
<button class="btn btn-small" onclick="autoUnloadTier('${tier}')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RULES ====================
|
||||||
|
let _rules = [];
|
||||||
|
|
||||||
|
function autoLoadRules() {
|
||||||
|
fetch('/autonomy/rules').then(r => r.json()).then(data => {
|
||||||
|
_rules = data.rules || [];
|
||||||
|
renderRules();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRules() {
|
||||||
|
const tbody = document.getElementById('rules-tbody');
|
||||||
|
const empty = document.getElementById('rules-empty');
|
||||||
|
if (_rules.length === 0) {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
empty.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
empty.style.display = 'none';
|
||||||
|
tbody.innerHTML = _rules.map(r => {
|
||||||
|
const enabled = r.enabled ? '<span style="color:var(--success)">●</span>' : '<span style="color:var(--text-muted)">○</span>';
|
||||||
|
const conds = (r.conditions||[]).map(c => c.type).join(', ') || '-';
|
||||||
|
const acts = (r.actions||[]).map(a => a.type).join(', ') || '-';
|
||||||
|
return `<tr>
|
||||||
|
<td>${enabled}</td>
|
||||||
|
<td><strong>${esc(r.name)}</strong></td>
|
||||||
|
<td>${r.priority}</td>
|
||||||
|
<td style="font-size:0.8rem">${esc(conds)}</td>
|
||||||
|
<td style="font-size:0.8rem">${esc(acts)}</td>
|
||||||
|
<td>${r.cooldown_seconds}s</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-small" onclick="autoEditRule('${r.id}')" style="background:var(--bg-input);border:1px solid var(--border);font-size:0.7rem">Edit</button>
|
||||||
|
<button class="btn btn-small" onclick="autoDeleteRule('${r.id}')" style="background:var(--bg-input);border:1px solid var(--danger);color:var(--danger);font-size:0.7rem">Del</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoDeleteRule(id) {
|
||||||
|
if (!confirm('Delete this rule?')) return;
|
||||||
|
fetch('/autonomy/rules/' + id, {method:'DELETE'}).then(r => r.json()).then(() => {
|
||||||
|
autoLoadRules();
|
||||||
|
autoRefreshStatus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RULE EDITOR ====================
|
||||||
|
const CONDITION_TYPES = [
|
||||||
|
{value:'threat_score_above', label:'Threat Score Above', hasValue:true, valueType:'number'},
|
||||||
|
{value:'threat_score_below', label:'Threat Score Below', hasValue:true, valueType:'number'},
|
||||||
|
{value:'threat_level_is', label:'Threat Level Is', hasValue:true, valueType:'text'},
|
||||||
|
{value:'port_scan_detected', label:'Port Scan Detected', hasValue:false},
|
||||||
|
{value:'ddos_detected', label:'DDoS Detected', hasValue:false},
|
||||||
|
{value:'ddos_attack_type', label:'DDoS Attack Type', hasValue:true, valueType:'text'},
|
||||||
|
{value:'connection_from_ip', label:'Connection From IP', hasValue:true, valueType:'text'},
|
||||||
|
{value:'connection_count_above', label:'Connection Count Above', hasValue:true, valueType:'number'},
|
||||||
|
{value:'new_listening_port', label:'New Listening Port', hasValue:false},
|
||||||
|
{value:'bandwidth_rx_above_mbps', label:'Bandwidth RX Above (Mbps)', hasValue:true, valueType:'number'},
|
||||||
|
{value:'arp_spoof_detected', label:'ARP Spoof Detected', hasValue:false},
|
||||||
|
{value:'schedule', label:'Schedule (Cron)', hasValue:true, valueType:'text'},
|
||||||
|
{value:'always', label:'Always', hasValue:false},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACTION_TYPES = [
|
||||||
|
{value:'block_ip', label:'Block IP', fields:['ip']},
|
||||||
|
{value:'unblock_ip', label:'Unblock IP', fields:['ip']},
|
||||||
|
{value:'rate_limit_ip', label:'Rate Limit IP', fields:['ip','rate']},
|
||||||
|
{value:'block_port', label:'Block Port', fields:['port','direction']},
|
||||||
|
{value:'kill_process', label:'Kill Process', fields:['pid']},
|
||||||
|
{value:'alert', label:'Alert', fields:['message']},
|
||||||
|
{value:'log_event', label:'Log Event', fields:['message']},
|
||||||
|
{value:'run_shell', label:'Run Shell', fields:['command']},
|
||||||
|
{value:'run_module', label:'Run Module (SAM)', fields:['module','args']},
|
||||||
|
{value:'counter_scan', label:'Counter Scan (SAM)', fields:['target']},
|
||||||
|
{value:'escalate_to_lam', label:'Escalate to LAM', fields:['task']},
|
||||||
|
];
|
||||||
|
|
||||||
|
function autoShowRuleModal(editRule) {
|
||||||
|
const modal = document.getElementById('rule-modal');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
document.getElementById('rule-modal-title').textContent = editRule ? 'Edit Rule' : 'Add Rule';
|
||||||
|
document.getElementById('rule-edit-id').value = editRule ? editRule.id : '';
|
||||||
|
document.getElementById('rule-name').value = editRule ? editRule.name : '';
|
||||||
|
document.getElementById('rule-priority').value = editRule ? editRule.priority : 50;
|
||||||
|
document.getElementById('rule-cooldown').value = editRule ? editRule.cooldown_seconds : 60;
|
||||||
|
document.getElementById('rule-description').value = editRule ? (editRule.description||'') : '';
|
||||||
|
|
||||||
|
const condList = document.getElementById('conditions-list');
|
||||||
|
const actList = document.getElementById('actions-list');
|
||||||
|
condList.innerHTML = '';
|
||||||
|
actList.innerHTML = '';
|
||||||
|
|
||||||
|
if (editRule) {
|
||||||
|
(editRule.conditions||[]).forEach(c => autoAddCondition(c));
|
||||||
|
(editRule.actions||[]).forEach(a => autoAddAction(a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoCloseRuleModal() {
|
||||||
|
document.getElementById('rule-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoEditRule(id) {
|
||||||
|
const rule = _rules.find(r => r.id === id);
|
||||||
|
if (rule) autoShowRuleModal(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoAddCondition(existing) {
|
||||||
|
const list = document.getElementById('conditions-list');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'cond-row';
|
||||||
|
|
||||||
|
const sel = document.createElement('select');
|
||||||
|
sel.innerHTML = CONDITION_TYPES.map(c => `<option value="${c.value}">${c.label}</option>`).join('');
|
||||||
|
if (existing) sel.value = existing.type;
|
||||||
|
|
||||||
|
const inp = document.createElement('input');
|
||||||
|
inp.placeholder = 'Value';
|
||||||
|
inp.style.display = 'none';
|
||||||
|
if (existing && existing.value !== undefined) { inp.value = existing.value; inp.style.display = ''; }
|
||||||
|
if (existing && existing.cron) { inp.value = existing.cron; inp.style.display = ''; }
|
||||||
|
|
||||||
|
sel.onchange = () => {
|
||||||
|
const ct = CONDITION_TYPES.find(c => c.value === sel.value);
|
||||||
|
inp.style.display = ct && ct.hasValue ? '' : 'none';
|
||||||
|
inp.placeholder = sel.value === 'schedule' ? 'Cron: */5 * * * *' : 'Value';
|
||||||
|
};
|
||||||
|
sel.onchange();
|
||||||
|
|
||||||
|
const rm = document.createElement('button');
|
||||||
|
rm.className = 'row-remove';
|
||||||
|
rm.textContent = '\u2715';
|
||||||
|
rm.onclick = () => row.remove();
|
||||||
|
|
||||||
|
row.append(sel, inp, rm);
|
||||||
|
list.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoAddAction(existing) {
|
||||||
|
const list = document.getElementById('actions-list');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'action-row';
|
||||||
|
row.style.flexWrap = 'wrap';
|
||||||
|
|
||||||
|
const sel = document.createElement('select');
|
||||||
|
sel.innerHTML = ACTION_TYPES.map(a => `<option value="${a.value}">${a.label}</option>`).join('');
|
||||||
|
if (existing) sel.value = existing.type;
|
||||||
|
|
||||||
|
const fieldsDiv = document.createElement('div');
|
||||||
|
fieldsDiv.style.cssText = 'display:flex;gap:0.5rem;flex:1;min-width:200px';
|
||||||
|
|
||||||
|
function renderFields() {
|
||||||
|
fieldsDiv.innerHTML = '';
|
||||||
|
const at = ACTION_TYPES.find(a => a.value === sel.value);
|
||||||
|
if (at) {
|
||||||
|
at.fields.forEach(f => {
|
||||||
|
const inp = document.createElement('input');
|
||||||
|
inp.placeholder = f;
|
||||||
|
inp.dataset.field = f;
|
||||||
|
if (existing && existing[f]) inp.value = existing[f];
|
||||||
|
fieldsDiv.appendChild(inp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sel.onchange = renderFields;
|
||||||
|
renderFields();
|
||||||
|
|
||||||
|
const rm = document.createElement('button');
|
||||||
|
rm.className = 'row-remove';
|
||||||
|
rm.textContent = '\u2715';
|
||||||
|
rm.onclick = () => row.remove();
|
||||||
|
|
||||||
|
row.append(sel, fieldsDiv, rm);
|
||||||
|
list.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSaveRule() {
|
||||||
|
const id = document.getElementById('rule-edit-id').value;
|
||||||
|
const name = document.getElementById('rule-name').value.trim();
|
||||||
|
if (!name) { alert('Rule name is required'); return; }
|
||||||
|
|
||||||
|
const conditions = [];
|
||||||
|
document.querySelectorAll('#conditions-list .cond-row').forEach(row => {
|
||||||
|
const type = row.querySelector('select').value;
|
||||||
|
const inp = row.querySelector('input');
|
||||||
|
const cond = {type};
|
||||||
|
if (inp.style.display !== 'none' && inp.value) {
|
||||||
|
if (type === 'schedule') cond.cron = inp.value;
|
||||||
|
else {
|
||||||
|
const num = Number(inp.value);
|
||||||
|
cond.value = isNaN(num) ? inp.value : num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conditions.push(cond);
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
document.querySelectorAll('#actions-list .action-row').forEach(row => {
|
||||||
|
const type = row.querySelector('select').value;
|
||||||
|
const action = {type};
|
||||||
|
row.querySelectorAll('input[data-field]').forEach(inp => {
|
||||||
|
if (inp.value) action[inp.dataset.field] = inp.value;
|
||||||
|
});
|
||||||
|
actions.push(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
name,
|
||||||
|
priority: parseInt(document.getElementById('rule-priority').value) || 50,
|
||||||
|
cooldown_seconds: parseInt(document.getElementById('rule-cooldown').value) || 60,
|
||||||
|
description: document.getElementById('rule-description').value.trim(),
|
||||||
|
conditions,
|
||||||
|
actions,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = id ? '/autonomy/rules/' + id : '/autonomy/rules';
|
||||||
|
const method = id ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
fetch(url, {method, headers:{'Content-Type':'application/json'}, body:JSON.stringify(rule)})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
autoCloseRuleModal();
|
||||||
|
autoLoadRules();
|
||||||
|
autoRefreshStatus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TEMPLATES ====================
|
||||||
|
function autoShowTemplates() {
|
||||||
|
const modal = document.getElementById('templates-modal');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
fetch('/autonomy/templates').then(r => r.json()).then(data => {
|
||||||
|
const list = document.getElementById('templates-list');
|
||||||
|
list.innerHTML = (data.templates||[]).map(t => `
|
||||||
|
<div class="template-card" onclick='autoApplyTemplate(${JSON.stringify(t).replace(/'/g,"'")})'>
|
||||||
|
<strong>${esc(t.name)}</strong>
|
||||||
|
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.25rem 0">${esc(t.description)}</p>
|
||||||
|
<span style="font-size:0.7rem;color:var(--text-muted)">Priority: ${t.priority} | Cooldown: ${t.cooldown_seconds}s</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoCloseTemplates() {
|
||||||
|
document.getElementById('templates-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoApplyTemplate(template) {
|
||||||
|
autoCloseTemplates();
|
||||||
|
fetch('/autonomy/rules', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(template),
|
||||||
|
}).then(r => r.json()).then(() => {
|
||||||
|
autoLoadRules();
|
||||||
|
autoRefreshStatus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ACTIVITY LOG ====================
|
||||||
|
let _activitySSE = null;
|
||||||
|
|
||||||
|
function autoLoadActivity() {
|
||||||
|
fetch('/autonomy/activity?limit=100').then(r => r.json()).then(data => {
|
||||||
|
renderActivity(data.entries || []);
|
||||||
|
document.getElementById('activity-count').textContent = (data.total||0) + ' entries';
|
||||||
|
});
|
||||||
|
// Start SSE
|
||||||
|
if (!_activitySSE) {
|
||||||
|
_activitySSE = new EventSource('/autonomy/activity/stream');
|
||||||
|
_activitySSE.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(e.data);
|
||||||
|
if (entry.type === 'keepalive') return;
|
||||||
|
prependActivity(entry);
|
||||||
|
document.getElementById('activity-live-dot').classList.add('active');
|
||||||
|
} catch(ex) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderActivity(entries) {
|
||||||
|
const tbody = document.getElementById('activity-tbody');
|
||||||
|
tbody.innerHTML = entries.map(e => activityRow(e)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependActivity(entry) {
|
||||||
|
const tbody = document.getElementById('activity-tbody');
|
||||||
|
tbody.insertAdjacentHTML('afterbegin', activityRow(entry));
|
||||||
|
// Limit rows
|
||||||
|
while (tbody.children.length > 200) tbody.removeChild(tbody.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activityRow(e) {
|
||||||
|
const time = e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '';
|
||||||
|
const cls = e.action_type === 'system' ? 'activity-system' : (e.success ? 'activity-success' : 'activity-fail');
|
||||||
|
const icon = e.success ? '✓' : '✗';
|
||||||
|
return `<tr class="${cls}">
|
||||||
|
<td style="font-size:0.75rem;font-family:monospace">${time}</td>
|
||||||
|
<td>${e.action_type === 'system' ? '-' : icon}</td>
|
||||||
|
<td style="font-size:0.8rem">${esc(e.action_type||'')}</td>
|
||||||
|
<td style="font-size:0.8rem">${esc(e.action_detail||'')}</td>
|
||||||
|
<td style="font-size:0.75rem">${esc(e.rule_name||'')}</td>
|
||||||
|
<td style="font-size:0.75rem">${esc(e.tier||'')}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UTILS ====================
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== INIT ====================
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
autoStartRefresh();
|
||||||
|
autoLoadRules();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -35,6 +35,7 @@
|
|||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<div class="nav-section-title">Categories</div>
|
<div class="nav-section-title">Categories</div>
|
||||||
<ul class="nav-links">
|
<ul class="nav-links">
|
||||||
|
<li><a href="{{ url_for('autonomy.index') }}" class="{% if request.blueprint == 'autonomy' %}active{% endif %}" style="color:var(--accent)">Autonomy</a></li>
|
||||||
<li><a href="{{ url_for('defense.index') }}" class="{% if request.blueprint == 'defense' and request.endpoint not in ('defense.linux_index', 'defense.windows_index', 'defense.monitor_index') %}active{% endif %}">Defense</a></li>
|
<li><a href="{{ url_for('defense.index') }}" class="{% if request.blueprint == 'defense' and request.endpoint not in ('defense.linux_index', 'defense.windows_index', 'defense.monitor_index') %}active{% endif %}">Defense</a></li>
|
||||||
<li><a href="{{ url_for('defense.linux_index') }}" class="{% if request.endpoint == 'defense.linux_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Linux</a></li>
|
<li><a href="{{ url_for('defense.linux_index') }}" class="{% if request.endpoint == 'defense.linux_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Linux</a></li>
|
||||||
<li><a href="{{ url_for('defense.windows_index') }}" class="{% if request.endpoint == 'defense.windows_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Windows</a></li>
|
<li><a href="{{ url_for('defense.windows_index') }}" class="{% if request.endpoint == 'defense.windows_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Windows</a></li>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user