AUTARCH v1.9 — remote monitoring, SSH manager, daemon, vault, cleanup
- Add Remote Monitoring Station with PIAP device profile system - Add SSH/SSHD manager with fail2ban integration - Add privileged daemon architecture for safe root operations - Add encrypted vault, HAL memory, HAL auto-analyst - Add network security suite, module creator, codex training - Add start.sh launcher script and GTK3 desktop launcher - Remove Output/ build artifacts, installer files, loose docs - Update .gitignore for runtime data and build artifacts - Update README for v1.9 with new launch method, screenshots, and features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
37
web/app.py
37
web/app.py
@@ -15,8 +15,35 @@ else:
|
||||
FRAMEWORK_DIR = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(FRAMEWORK_DIR))
|
||||
|
||||
# ── Package resolution (same logic as autarch.py) ────────────────────────────
|
||||
# If not running from the project venv, add user site-packages so pip deps are visible.
|
||||
_venv_dir = FRAMEWORK_DIR / 'venv'
|
||||
_running_in_venv = hasattr(sys, 'prefix') and sys.prefix != sys.base_prefix
|
||||
|
||||
if not _running_in_venv:
|
||||
try:
|
||||
import pwd as _pwd
|
||||
_owner_uid = FRAMEWORK_DIR.stat().st_uid
|
||||
_user_site = (
|
||||
Path(_pwd.getpwuid(_owner_uid).pw_dir) / '.local' / 'lib'
|
||||
/ f'python{sys.version_info.major}.{sys.version_info.minor}' / 'site-packages'
|
||||
)
|
||||
if _user_site.is_dir() and str(_user_site) not in sys.path:
|
||||
sys.path.insert(1, str(_user_site))
|
||||
for _mod_name in ('typing_extensions',):
|
||||
if _mod_name in sys.modules:
|
||||
_mod = sys.modules[_mod_name]
|
||||
if hasattr(_mod, '__file__') and '/usr/lib/' in str(getattr(_mod, '__file__', '')):
|
||||
del sys.modules[_mod_name]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def create_app():
|
||||
# Install the subprocess.run hook so ALL sudo calls auto-route through the daemon
|
||||
from core.daemon import install_subprocess_hook
|
||||
install_subprocess_hook()
|
||||
|
||||
# In frozen builds, templates/static are inside _MEIPASS, not next to __file__
|
||||
bundle_web = FRAMEWORK_DIR / 'web'
|
||||
app = Flask(
|
||||
@@ -98,11 +125,14 @@ def create_app():
|
||||
from web.routes.email_sec import email_sec_bp
|
||||
from web.routes.mitm_proxy import mitm_proxy_bp
|
||||
from web.routes.pineapple import pineapple_bp
|
||||
from web.routes.incident_resp import incident_resp_bp
|
||||
from web.routes.sms_forge import sms_forge_bp
|
||||
from web.routes.starlink_hack import starlink_hack_bp
|
||||
from web.routes.rcs_tools import rcs_tools_bp
|
||||
from web.routes.port_scanner import port_scanner_bp
|
||||
from web.routes.network import network_bp
|
||||
from web.routes.module_creator import module_creator_bp
|
||||
from web.routes.ssh_manager import ssh_manager_bp
|
||||
from web.routes.remote_monitor import remote_monitor_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
@@ -160,11 +190,14 @@ def create_app():
|
||||
app.register_blueprint(email_sec_bp)
|
||||
app.register_blueprint(mitm_proxy_bp)
|
||||
app.register_blueprint(pineapple_bp)
|
||||
app.register_blueprint(incident_resp_bp)
|
||||
app.register_blueprint(sms_forge_bp)
|
||||
app.register_blueprint(starlink_hack_bp)
|
||||
app.register_blueprint(rcs_tools_bp)
|
||||
app.register_blueprint(port_scanner_bp)
|
||||
app.register_blueprint(network_bp)
|
||||
app.register_blueprint(module_creator_bp)
|
||||
app.register_blueprint(remote_monitor_bp)
|
||||
app.register_blueprint(ssh_manager_bp)
|
||||
|
||||
# Start network discovery advertising (mDNS + Bluetooth)
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Chat and Agent API routes — Hal chat with Agent system for module creation."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -53,6 +54,14 @@ def chat():
|
||||
if not message:
|
||||
return jsonify({'error': 'No message provided'})
|
||||
|
||||
# Store in HAL's encrypted memory
|
||||
try:
|
||||
from core.hal_memory import get_hal_memory
|
||||
mem = get_hal_memory()
|
||||
mem.add('user', message, metadata={'mode': mode})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mode == 'agent':
|
||||
return _handle_agent_chat(message)
|
||||
else:
|
||||
@@ -77,8 +86,16 @@ def _handle_direct_chat(message):
|
||||
system_prompt = _get_system_prompt()
|
||||
try:
|
||||
token_gen = llm.chat(message, system_prompt=system_prompt, stream=True)
|
||||
full_response = []
|
||||
for token in token_gen:
|
||||
full_response.append(token)
|
||||
yield f"data: {json.dumps({'token': token})}\n\n"
|
||||
# Store HAL's response in memory
|
||||
try:
|
||||
from core.hal_memory import get_hal_memory
|
||||
get_hal_memory().add('hal', ''.join(full_response))
|
||||
except Exception:
|
||||
pass
|
||||
except LLMError as e:
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
|
||||
|
||||
@@ -113,11 +130,27 @@ def _handle_agent_chat(message):
|
||||
tools = get_tool_registry()
|
||||
agent = Agent(llm=llm, tools=tools, max_steps=20, verbose=False)
|
||||
|
||||
# Inject system prompt into agent
|
||||
system_prompt = _get_system_prompt()
|
||||
agent.SYSTEM_PROMPT = system_prompt + "\n\n{tools_description}"
|
||||
# Inject system prompt — keep the THOUGHT/ACTION/PARAMS format from Agent,
|
||||
# prepend with our behavioral rules
|
||||
hal_prompt = _get_system_prompt()
|
||||
agent.SYSTEM_PROMPT = hal_prompt + """
|
||||
|
||||
FORMAT — you MUST use this exact format:
|
||||
|
||||
THOUGHT: your reasoning
|
||||
ACTION: tool_name
|
||||
PARAMS: {{"param": "value"}}
|
||||
|
||||
When done: ACTION: task_complete PARAMS: {{"summary": "what was done"}}
|
||||
When you need input: ACTION: ask_user PARAMS: {{"question": "your question"}}
|
||||
|
||||
{tools_description}
|
||||
"""
|
||||
|
||||
def on_step(step):
|
||||
# Check stop signal
|
||||
if stop_event.is_set():
|
||||
return
|
||||
if step.thought:
|
||||
steps.append({'type': 'thought', 'content': step.thought})
|
||||
if step.tool_name and step.tool_name not in ('task_complete', 'ask_user'):
|
||||
@@ -135,6 +168,22 @@ def _handle_agent_chat(message):
|
||||
else:
|
||||
steps.append({'type': 'error', 'content': result.error or result.summary})
|
||||
|
||||
# Store agent conversation in HAL memory
|
||||
try:
|
||||
from core.hal_memory import get_hal_memory
|
||||
mem = get_hal_memory()
|
||||
for step in result.steps:
|
||||
if step.thought:
|
||||
mem.add('hal_thought', step.thought)
|
||||
if step.tool_name:
|
||||
mem.add('hal_action', f'{step.tool_name}({json.dumps(step.tool_args or {})})')
|
||||
if step.tool_result:
|
||||
mem.add('hal_result', step.tool_result[:2000])
|
||||
mem.add('hal', result.summary if result.success else (result.error or result.summary))
|
||||
mem.save()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
steps.append({'type': 'error', 'content': str(e)})
|
||||
finally:
|
||||
@@ -181,6 +230,143 @@ def chat_reset():
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@chat_bp.route('/hal/analyze', methods=['POST'])
|
||||
@login_required
|
||||
def hal_analyze():
|
||||
"""Send tool output to HAL for AI analysis.
|
||||
|
||||
Expects JSON: {tool_name, output, context?, category?}
|
||||
Returns JSON: {available, analysis, risk_level, has_fixes, tool_name}
|
||||
"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
tool_name = data.get('tool_name', 'unknown')
|
||||
output = data.get('output', '')
|
||||
context = data.get('context', '')
|
||||
category = data.get('category', 'default')
|
||||
|
||||
if not output:
|
||||
return jsonify({'available': False, 'analysis': 'No output provided', 'tool_name': tool_name})
|
||||
|
||||
from core.hal_analyst import analyze_output
|
||||
result = analyze_output(tool_name, output, context=context, category=category)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@chat_bp.route('/hal/fix', methods=['POST'])
|
||||
@login_required
|
||||
def hal_fix():
|
||||
"""Execute a fix command suggested by HAL.
|
||||
|
||||
Expects JSON: {command: str}
|
||||
Returns JSON: {ok, output, exit_code}
|
||||
"""
|
||||
from core.daemon import root_exec
|
||||
import shlex
|
||||
import subprocess as _subprocess
|
||||
data = request.get_json(silent=True) or {}
|
||||
command = data.get('command', '').strip()
|
||||
|
||||
if not command:
|
||||
return jsonify({'ok': False, 'error': 'No command provided'})
|
||||
|
||||
# Safety: block obviously dangerous commands
|
||||
dangerous = ['rm -rf /', 'mkfs', 'dd if=', ':(){', 'format c:']
|
||||
for d in dangerous:
|
||||
if d in command.lower():
|
||||
return jsonify({'ok': False, 'error': f'Blocked dangerous command: {d}'})
|
||||
|
||||
# Clean the command: strip sudo, shell redirections
|
||||
import re
|
||||
command = re.sub(r'\s*2>/dev/null\s*', ' ', command)
|
||||
command = re.sub(r'\s*>/dev/null\s*', ' ', command)
|
||||
command = re.sub(r'\s*2>&1\s*', ' ', command)
|
||||
command = command.strip()
|
||||
|
||||
if command.startswith('sudo '):
|
||||
command = command[5:].strip()
|
||||
|
||||
# Commands that should run as the normal user, not root
|
||||
USER_COMMANDS = {'adb', 'fastboot'}
|
||||
|
||||
def _is_user_cmd(cmd_str):
|
||||
"""Check if a command should run as normal user."""
|
||||
base = cmd_str.split()[0] if cmd_str.split() else ''
|
||||
return os.path.basename(base) in USER_COMMANDS
|
||||
|
||||
def _run_user(cmd_parts, timeout=60):
|
||||
"""Run a command as the normal user via subprocess."""
|
||||
try:
|
||||
result = _subprocess.run(
|
||||
cmd_parts, capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
return {
|
||||
'ok': result.returncode == 0,
|
||||
'stdout': result.stdout,
|
||||
'stderr': result.stderr,
|
||||
'code': result.returncode,
|
||||
}
|
||||
except _subprocess.TimeoutExpired:
|
||||
return {'ok': False, 'stdout': '', 'stderr': f'Timeout after {timeout}s', 'code': -2}
|
||||
except FileNotFoundError:
|
||||
return {'ok': False, 'stdout': '', 'stderr': f'Command not found: {cmd_parts[0]}', 'code': -3}
|
||||
except Exception as e:
|
||||
return {'ok': False, 'stdout': '', 'stderr': str(e), 'code': -4}
|
||||
|
||||
def _exec(cmd_parts, timeout=60):
|
||||
"""Route to user or root execution based on command."""
|
||||
if cmd_parts and os.path.basename(cmd_parts[0]) in USER_COMMANDS:
|
||||
return _run_user(cmd_parts, timeout=timeout)
|
||||
return root_exec(cmd_parts, timeout=timeout)
|
||||
|
||||
# Handle pipes (cmd1 | cmd2) — run as shell command through bash
|
||||
if '|' in command:
|
||||
if _is_user_cmd(command):
|
||||
r = _run_user(['bash', '-c', command], timeout=60)
|
||||
else:
|
||||
r = root_exec(['bash', '-c', command], timeout=60)
|
||||
return jsonify({'ok': r['ok'], 'output': r['stdout'] + r['stderr'], 'exit_code': r['code']})
|
||||
|
||||
# Handle chained commands (&&) by running them sequentially
|
||||
if '&&' in command:
|
||||
parts = [c.strip() for c in command.split('&&') if c.strip()]
|
||||
all_output = ''
|
||||
for part in parts:
|
||||
if part.startswith('sudo '):
|
||||
part = part[5:].strip()
|
||||
part = re.sub(r'\s*2>/dev/null\s*', ' ', part).strip()
|
||||
part = re.sub(r'\s*>/dev/null\s*', ' ', part).strip()
|
||||
try:
|
||||
cmd_parts = shlex.split(part)
|
||||
except ValueError:
|
||||
cmd_parts = part.split()
|
||||
r = _exec(cmd_parts, timeout=60)
|
||||
all_output += r['stdout'] + r['stderr']
|
||||
if not r['ok']:
|
||||
return jsonify({'ok': False, 'output': all_output, 'exit_code': r['code']})
|
||||
return jsonify({'ok': True, 'output': all_output, 'exit_code': 0})
|
||||
|
||||
# Single command
|
||||
try:
|
||||
cmd_parts = shlex.split(command)
|
||||
except ValueError:
|
||||
cmd_parts = command.split()
|
||||
|
||||
r = _exec(cmd_parts, timeout=60)
|
||||
return jsonify({
|
||||
'ok': r['ok'],
|
||||
'output': r['stdout'] + r['stderr'],
|
||||
'exit_code': r['code'],
|
||||
})
|
||||
|
||||
|
||||
@chat_bp.route('/hal/available')
|
||||
@login_required
|
||||
def hal_available():
|
||||
"""Quick check if HAL analysis is available (LLM loaded)."""
|
||||
from core.hal_analyst import is_llm_available
|
||||
return jsonify({'available': is_llm_available()})
|
||||
|
||||
|
||||
@chat_bp.route('/chat/status')
|
||||
@login_required
|
||||
def chat_status():
|
||||
|
||||
@@ -115,12 +115,19 @@ def attack_continuous_stop():
|
||||
return jsonify(_get_deauth().stop_continuous())
|
||||
|
||||
|
||||
@deauth_bp.route('/attack/status')
|
||||
@deauth_bp.route('/attack/status', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def attack_status():
|
||||
return jsonify(_get_deauth().get_attack_status())
|
||||
|
||||
|
||||
@deauth_bp.route('/status', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def status():
|
||||
"""Alias for attack status — templates may call /status directly."""
|
||||
return jsonify(_get_deauth().get_attack_status())
|
||||
|
||||
|
||||
@deauth_bp.route('/history')
|
||||
@login_required
|
||||
def history():
|
||||
|
||||
@@ -7,6 +7,7 @@ import socket
|
||||
import json
|
||||
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
|
||||
from web.auth import login_required
|
||||
from core.daemon import root_exec
|
||||
|
||||
defense_bp = Blueprint('defense', __name__, url_prefix='/defense')
|
||||
|
||||
@@ -40,6 +41,9 @@ def index():
|
||||
except Exception:
|
||||
sys_info['ip'] = '127.0.0.1'
|
||||
|
||||
# Return JSON if requested (for OS detection by sub-pages)
|
||||
if request.headers.get('Accept', '') == 'application/json':
|
||||
return jsonify(sys_info)
|
||||
return render_template('defense.html', modules=modules, sys_info=sys_info)
|
||||
|
||||
|
||||
@@ -108,7 +112,8 @@ def linux_check(check_name):
|
||||
@login_required
|
||||
def linux_firewall_rules():
|
||||
"""Get current iptables rules."""
|
||||
success, output = _run_cmd("sudo iptables -L -n --line-numbers 2>/dev/null")
|
||||
r = root_exec(['iptables', '-L', '-n', '--line-numbers'])
|
||||
success, output = r['ok'], r['stdout']
|
||||
if success:
|
||||
return jsonify({'rules': output})
|
||||
return jsonify({'rules': 'Could not read iptables rules (need sudo privileges)'})
|
||||
@@ -123,7 +128,8 @@ def linux_firewall_block():
|
||||
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
|
||||
return jsonify({'error': 'Invalid IP address', 'success': False})
|
||||
|
||||
success, _ = _run_cmd(f"sudo iptables -A INPUT -s {ip} -j DROP")
|
||||
r = root_exec(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'])
|
||||
success, _ = r['ok'], r['stdout']
|
||||
if success:
|
||||
return jsonify({'message': f'Blocked {ip}', 'success': True})
|
||||
return jsonify({'error': f'Failed to block {ip} (need sudo)', 'success': False})
|
||||
@@ -138,7 +144,8 @@ def linux_firewall_unblock():
|
||||
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
|
||||
return jsonify({'error': 'Invalid IP address', 'success': False})
|
||||
|
||||
success, _ = _run_cmd(f"sudo iptables -D INPUT -s {ip} -j DROP")
|
||||
r = root_exec(['iptables', '-D', 'INPUT', '-s', ip, '-j', 'DROP'])
|
||||
success, _ = r['ok'], r['stdout']
|
||||
if success:
|
||||
return jsonify({'message': f'Unblocked {ip}', 'success': True})
|
||||
return jsonify({'error': f'Failed to unblock {ip}', 'success': False})
|
||||
|
||||
@@ -27,6 +27,24 @@ def status():
|
||||
|
||||
# ── ADB Endpoints ──────────────────────────────────────────────────
|
||||
|
||||
@hardware_bp.route('/adb/kill-server', methods=['POST'])
|
||||
@login_required
|
||||
def adb_kill_server():
|
||||
"""Kill the ADB server."""
|
||||
from core.daemon import root_exec
|
||||
r = root_exec(['adb', 'kill-server'], timeout=10)
|
||||
return jsonify({'ok': r['ok'], 'output': r['stdout'] + r['stderr']})
|
||||
|
||||
|
||||
@hardware_bp.route('/adb/start-server', methods=['POST'])
|
||||
@login_required
|
||||
def adb_start_server():
|
||||
"""Start the ADB server."""
|
||||
from core.daemon import root_exec
|
||||
r = root_exec(['adb', 'start-server'], timeout=10)
|
||||
return jsonify({'ok': r['ok'], 'output': r['stdout'] + r['stderr']})
|
||||
|
||||
|
||||
@hardware_bp.route('/adb/devices')
|
||||
@login_required
|
||||
def adb_devices():
|
||||
|
||||
262
web/routes/module_creator.py
Normal file
262
web/routes/module_creator.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""Module Creator route - create, edit, validate, and manage AUTARCH modules"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from web.auth import login_required
|
||||
|
||||
module_creator_bp = Blueprint('module_creator', __name__, url_prefix='/module-creator')
|
||||
|
||||
MODULES_DIR = Path(__file__).parent.parent.parent / 'modules'
|
||||
|
||||
CATEGORIES = ['defense', 'offense', 'counter', 'analyze', 'osint', 'simulate', 'core', 'hardware']
|
||||
|
||||
CATEGORY_DESCRIPTIONS = {
|
||||
'defense': 'Defensive security module for monitoring, hardening, and threat detection',
|
||||
'offense': 'Offensive security module for penetration testing and exploitation',
|
||||
'counter': 'Counter-intelligence module for anti-surveillance and evasion',
|
||||
'analyze': 'Analysis module for forensics, traffic inspection, and data processing',
|
||||
'osint': 'Open-source intelligence gathering and reconnaissance module',
|
||||
'simulate': 'Simulation module for attack modeling and scenario testing',
|
||||
'core': 'Core infrastructure module for platform internals and utilities',
|
||||
'hardware': 'Hardware interface module for RF, BLE, RFID, SDR, and embedded devices',
|
||||
}
|
||||
|
||||
|
||||
def _module_skeleton(name, category, description, author):
|
||||
"""Generate skeleton code for a new module."""
|
||||
return f'''"""
|
||||
{description}
|
||||
"""
|
||||
|
||||
DESCRIPTION = "{description}"
|
||||
AUTHOR = "{author}"
|
||||
VERSION = "1.0"
|
||||
CATEGORY = "{category}"
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from core.banner import Colors, clear_screen, display_banner
|
||||
|
||||
|
||||
def run():
|
||||
"""Main entry point."""
|
||||
clear_screen()
|
||||
display_banner()
|
||||
print(f"{{Colors.BOLD}}{name}{{Colors.RESET}}")
|
||||
print(f"{{Colors.DIM}}{{"─" * 50}}{{Colors.RESET}}\\n")
|
||||
|
||||
# TODO: Implement module logic here
|
||||
print(f"{{Colors.GREEN}}[+] Module loaded successfully{{Colors.RESET}}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
'''
|
||||
|
||||
|
||||
def _parse_module_metadata(filepath):
|
||||
"""Extract metadata from a module file."""
|
||||
meta = {
|
||||
'name': filepath.stem,
|
||||
'category': 'unknown',
|
||||
'description': '',
|
||||
'version': '',
|
||||
'author': '',
|
||||
'file_size': filepath.stat().st_size,
|
||||
'last_modified': datetime.fromtimestamp(filepath.stat().st_mtime).strftime('%Y-%m-%d %H:%M'),
|
||||
}
|
||||
try:
|
||||
source = filepath.read_text(errors='replace')
|
||||
tree = ast.parse(source)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and isinstance(node.value, ast.Constant):
|
||||
if target.id == 'DESCRIPTION':
|
||||
meta['description'] = str(node.value.value)
|
||||
elif target.id == 'CATEGORY':
|
||||
meta['category'] = str(node.value.value)
|
||||
elif target.id == 'VERSION':
|
||||
meta['version'] = str(node.value.value)
|
||||
elif target.id == 'AUTHOR':
|
||||
meta['author'] = str(node.value.value)
|
||||
except Exception:
|
||||
pass
|
||||
return meta
|
||||
|
||||
|
||||
@module_creator_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('module_creator.html')
|
||||
|
||||
|
||||
@module_creator_bp.route('/templates')
|
||||
@login_required
|
||||
def templates():
|
||||
"""Return skeleton templates for each category."""
|
||||
result = []
|
||||
for cat in CATEGORIES:
|
||||
result.append({
|
||||
'name': f'new_{cat}_module',
|
||||
'category': cat,
|
||||
'description': CATEGORY_DESCRIPTIONS.get(cat, ''),
|
||||
'code': _module_skeleton(f'new_{cat}_module', cat, CATEGORY_DESCRIPTIONS.get(cat, ''), 'darkHal'),
|
||||
})
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@module_creator_bp.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create():
|
||||
"""Create a new module file."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'Invalid JSON payload'}), 400
|
||||
|
||||
name = data.get('name', '').strip()
|
||||
category = data.get('category', '').strip()
|
||||
description = data.get('description', '').strip()
|
||||
author = data.get('author', 'darkHal').strip()
|
||||
code = data.get('code', '').strip()
|
||||
|
||||
# Validate name
|
||||
if not name:
|
||||
return jsonify({'success': False, 'error': 'Module name is required'}), 400
|
||||
if not re.match(r'^[A-Za-z0-9_]+$', name):
|
||||
return jsonify({'success': False, 'error': 'Module name must be alphanumeric and underscores only'}), 400
|
||||
|
||||
# Check category
|
||||
if category not in CATEGORIES:
|
||||
return jsonify({'success': False, 'error': f'Invalid category. Must be one of: {", ".join(CATEGORIES)}'}), 400
|
||||
|
||||
# Check existence
|
||||
target = MODULES_DIR / f'{name}.py'
|
||||
if target.exists():
|
||||
return jsonify({'success': False, 'error': f'Module "{name}" already exists'}), 409
|
||||
|
||||
# Use provided code or generate skeleton
|
||||
if not code:
|
||||
code = _module_skeleton(name, category, description, author)
|
||||
|
||||
try:
|
||||
target.write_text(code)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Failed to write module: {e}'}), 500
|
||||
|
||||
return jsonify({'success': True, 'message': f'Module "{name}" created successfully', 'path': str(target)})
|
||||
|
||||
|
||||
@module_creator_bp.route('/validate', methods=['POST'])
|
||||
@login_required
|
||||
def validate():
|
||||
"""Validate Python syntax and required attributes."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data or 'code' not in data:
|
||||
return jsonify({'valid': False, 'errors': ['No code provided']}), 400
|
||||
|
||||
code = data['code']
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Syntax check
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError as e:
|
||||
return jsonify({
|
||||
'valid': False,
|
||||
'errors': [f'Syntax error at line {e.lineno}: {e.msg}'],
|
||||
'warnings': [],
|
||||
})
|
||||
|
||||
# Check required attributes
|
||||
found_attrs = set()
|
||||
found_run = False
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id in ('DESCRIPTION', 'CATEGORY'):
|
||||
found_attrs.add(target.id)
|
||||
if isinstance(node, ast.FunctionDef) and node.name == 'run':
|
||||
found_run = True
|
||||
|
||||
if 'DESCRIPTION' not in found_attrs:
|
||||
errors.append('Missing required attribute: DESCRIPTION')
|
||||
if 'CATEGORY' not in found_attrs:
|
||||
errors.append('Missing required attribute: CATEGORY')
|
||||
if not found_run:
|
||||
errors.append('Missing required function: run()')
|
||||
|
||||
valid = len(errors) == 0
|
||||
if valid:
|
||||
warnings.append('All checks passed')
|
||||
|
||||
return jsonify({'valid': valid, 'errors': errors, 'warnings': warnings})
|
||||
|
||||
|
||||
@module_creator_bp.route('/list')
|
||||
@login_required
|
||||
def list_modules():
|
||||
"""Return JSON list of all existing modules."""
|
||||
modules = []
|
||||
if MODULES_DIR.exists():
|
||||
for f in sorted(MODULES_DIR.glob('*.py')):
|
||||
if f.name.startswith('__'):
|
||||
continue
|
||||
modules.append(_parse_module_metadata(f))
|
||||
return jsonify(modules)
|
||||
|
||||
|
||||
@module_creator_bp.route('/preview', methods=['POST'])
|
||||
@login_required
|
||||
def preview():
|
||||
"""Load and return source code of an existing module."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data or 'name' not in data:
|
||||
return jsonify({'success': False, 'error': 'Module name is required'}), 400
|
||||
|
||||
name = data['name'].strip()
|
||||
target = MODULES_DIR / f'{name}.py'
|
||||
if not target.exists():
|
||||
return jsonify({'success': False, 'error': f'Module "{name}" not found'}), 404
|
||||
|
||||
try:
|
||||
code = target.read_text(errors='replace')
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
meta = _parse_module_metadata(target)
|
||||
return jsonify({'success': True, 'code': code, 'metadata': meta})
|
||||
|
||||
|
||||
@module_creator_bp.route('/save', methods=['POST'])
|
||||
@login_required
|
||||
def save():
|
||||
"""Save edits to an existing module file."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'Invalid JSON payload'}), 400
|
||||
|
||||
name = data.get('name', '').strip()
|
||||
code = data.get('code', '')
|
||||
|
||||
if not name:
|
||||
return jsonify({'success': False, 'error': 'Module name is required'}), 400
|
||||
if not re.match(r'^[A-Za-z0-9_]+$', name):
|
||||
return jsonify({'success': False, 'error': 'Invalid module name'}), 400
|
||||
|
||||
target = MODULES_DIR / f'{name}.py'
|
||||
if not target.exists():
|
||||
return jsonify({'success': False, 'error': f'Module "{name}" does not exist'}), 404
|
||||
|
||||
try:
|
||||
target.write_text(code)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Failed to save: {e}'}), 500
|
||||
|
||||
return jsonify({'success': True, 'message': f'Module "{name}" saved successfully'})
|
||||
1517
web/routes/network.py
Normal file
1517
web/routes/network.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@ def stop_ap():
|
||||
return jsonify(_get_ap().stop_rogue_ap())
|
||||
|
||||
|
||||
@pineapple_bp.route('/status')
|
||||
@pineapple_bp.route('/status', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def status():
|
||||
return jsonify(_get_ap().get_status())
|
||||
|
||||
274
web/routes/remote_monitor.py
Normal file
274
web/routes/remote_monitor.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""Remote Monitoring Station — load .piap device profiles and control remote radios."""
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
remote_monitor_bp = Blueprint('remote_monitor', __name__, url_prefix='/remote-monitor')
|
||||
|
||||
PIAP_DIR = Path(__file__).parent.parent.parent / 'data' / 'piap'
|
||||
CAPTURE_DIR = Path(__file__).parent.parent.parent / 'data' / 'captures'
|
||||
|
||||
_ssh_sessions = {}
|
||||
_capture_threads = {}
|
||||
|
||||
|
||||
def _parse_piap(filepath):
|
||||
"""Parse a .piap file into a dict structure."""
|
||||
cfg = configparser.ConfigParser(interpolation=None)
|
||||
cfg.read(filepath)
|
||||
|
||||
device = dict(cfg['device']) if 'device' in cfg else {}
|
||||
connection = dict(cfg['connection']) if 'connection' in cfg else {}
|
||||
|
||||
radios = []
|
||||
i = 0
|
||||
while f'radio_{i}' in cfg:
|
||||
radio = dict(cfg[f'radio_{i}'])
|
||||
radio['index'] = i
|
||||
if 'channels' in radio:
|
||||
radio['channel_list'] = [c.strip() for c in radio['channels'].split(',')]
|
||||
if 'modes' in radio:
|
||||
radio['mode_list'] = [m.strip() for m in radio['modes'].split(',')]
|
||||
radios.append(radio)
|
||||
i += 1
|
||||
|
||||
features = dict(cfg['features']) if 'features' in cfg else {}
|
||||
info_cmds = dict(cfg['info']) if 'info' in cfg else {}
|
||||
|
||||
return {
|
||||
'device': device,
|
||||
'connection': connection,
|
||||
'radios': radios,
|
||||
'features': features,
|
||||
'info': info_cmds,
|
||||
'filename': os.path.basename(filepath),
|
||||
}
|
||||
|
||||
|
||||
def _ssh_cmd(conn, cmd, timeout=15):
|
||||
"""Run a command on the remote device over SSH."""
|
||||
host = conn.get('host', '')
|
||||
port = conn.get('port', '22')
|
||||
user = conn.get('user', 'root')
|
||||
auth = conn.get('auth', 'key')
|
||||
key_path = conn.get('key_path', '')
|
||||
password = conn.get('password', '')
|
||||
ssh_timeout = conn.get('timeout', '10')
|
||||
|
||||
ssh_args = ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'ConnectTimeout=' + ssh_timeout,
|
||||
'-p', port]
|
||||
|
||||
if auth == 'key' and key_path:
|
||||
ssh_args += ['-i', key_path]
|
||||
|
||||
ssh_args.append(f'{user}@{host}')
|
||||
ssh_args.append(cmd)
|
||||
|
||||
try:
|
||||
r = subprocess.run(ssh_args, capture_output=True, text=True, timeout=timeout)
|
||||
return {'ok': r.returncode == 0, 'stdout': r.stdout.strip(), 'stderr': r.stderr.strip(), 'code': r.returncode}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {'ok': False, 'stdout': '', 'stderr': 'timeout', 'code': -1}
|
||||
except Exception as e:
|
||||
return {'ok': False, 'stdout': '', 'stderr': str(e), 'code': -1}
|
||||
|
||||
|
||||
def _expand_cmd(cmd_template, radio=None, channel=None, bssid=None, count=None, timestamp=None):
|
||||
"""Replace {variables} in a command template."""
|
||||
if not cmd_template:
|
||||
return ''
|
||||
cmd = cmd_template
|
||||
if radio:
|
||||
cmd = cmd.replace('{phy}', radio.get('phy', ''))
|
||||
cmd = cmd.replace('{interface}', radio.get('interface', ''))
|
||||
cmd = cmd.replace('{mon}', radio.get('monitor_interface', ''))
|
||||
cmd = cmd.replace('{channels}', radio.get('channels', ''))
|
||||
if channel:
|
||||
cmd = cmd.replace('{channel}', str(channel))
|
||||
elif radio:
|
||||
cmd = cmd.replace('{channel}', radio.get('default_channel', '1'))
|
||||
if bssid:
|
||||
cmd = cmd.replace('{bssid}', bssid)
|
||||
if count:
|
||||
cmd = cmd.replace('{count}', str(count))
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
cmd = cmd.replace('{timestamp}', timestamp)
|
||||
return cmd
|
||||
|
||||
|
||||
# ── Routes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@remote_monitor_bp.route('/')
|
||||
def index():
|
||||
"""Main page — loads available .piap files for dropdown."""
|
||||
piap_files = []
|
||||
for f in sorted(PIAP_DIR.glob('*.piap')):
|
||||
if f.name == 'template.piap':
|
||||
continue
|
||||
try:
|
||||
p = _parse_piap(f)
|
||||
piap_files.append({'filename': f.name, 'name': p['device'].get('name', f.stem)})
|
||||
except Exception as e:
|
||||
logger.warning("Failed to parse %s: %s", f, e)
|
||||
return render_template('remote_monitor.html', piap_files=piap_files)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/load', methods=['POST'])
|
||||
def load_piap():
|
||||
"""Load a .piap file and return its full config."""
|
||||
filename = request.json.get('filename', '')
|
||||
filepath = PIAP_DIR / filename
|
||||
if not filepath.exists() or not filepath.suffix == '.piap':
|
||||
return jsonify({'ok': False, 'error': 'File not found'}), 404
|
||||
try:
|
||||
data = _parse_piap(filepath)
|
||||
return jsonify({'ok': True, 'data': data})
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/connect', methods=['POST'])
|
||||
def connect():
|
||||
"""Test SSH connection to the remote device."""
|
||||
conn = request.json.get('connection', {})
|
||||
result = _ssh_cmd(conn, 'echo ok')
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/info', methods=['POST'])
|
||||
def device_info():
|
||||
"""Get device info (uptime, memory, kernel, etc)."""
|
||||
conn = request.json.get('connection', {})
|
||||
info_cmds = request.json.get('info', {})
|
||||
results = {}
|
||||
for key, cmd in info_cmds.items():
|
||||
if key.startswith('cmd_'):
|
||||
label = key[4:]
|
||||
r = _ssh_cmd(conn, cmd)
|
||||
results[label] = r.get('stdout', r.get('stderr', ''))
|
||||
return jsonify({'ok': True, 'info': results})
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/radio/status', methods=['POST'])
|
||||
def radio_status():
|
||||
"""Get status of a specific radio."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
cmd = _expand_cmd(radio.get('cmd_status', 'iw dev'), radio)
|
||||
result = _ssh_cmd(conn, cmd)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/radio/monitor-on', methods=['POST'])
|
||||
def monitor_on():
|
||||
"""Enable monitor mode on a radio."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
channel = request.json.get('channel', radio.get('default_channel', '1'))
|
||||
cmd = _expand_cmd(radio.get('cmd_monitor_on', ''), radio, channel=channel)
|
||||
result = _ssh_cmd(conn, cmd)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/radio/monitor-off', methods=['POST'])
|
||||
def monitor_off():
|
||||
"""Disable monitor mode on a radio."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
cmd = _expand_cmd(radio.get('cmd_monitor_off', ''), radio)
|
||||
result = _ssh_cmd(conn, cmd)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/radio/set-channel', methods=['POST'])
|
||||
def set_channel():
|
||||
"""Set channel on a monitor interface."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
channel = request.json.get('channel', '1')
|
||||
cmd = _expand_cmd(radio.get('cmd_set_channel', ''), radio, channel=channel)
|
||||
result = _ssh_cmd(conn, cmd)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/capture/start', methods=['POST'])
|
||||
def capture_start():
|
||||
"""Start packet capture on remote device."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
features = request.json.get('features', {})
|
||||
cmd = _expand_cmd(features.get('cmd_capture_start', ''), radio)
|
||||
result = _ssh_cmd(conn, cmd)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/capture/stop', methods=['POST'])
|
||||
def capture_stop():
|
||||
"""Stop packet capture on remote device."""
|
||||
conn = request.json.get('connection', {})
|
||||
features = request.json.get('features', {})
|
||||
cmd = features.get('cmd_capture_stop', 'killall tcpdump 2>/dev/null')
|
||||
result = _ssh_cmd(conn, cmd)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/scan', methods=['POST'])
|
||||
def wifi_scan():
|
||||
"""Run passive WiFi scan."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
features = request.json.get('features', {})
|
||||
cmd = _expand_cmd(features.get('cmd_wifi_scan', ''), radio)
|
||||
result = _ssh_cmd(conn, cmd, timeout=30)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/deauth', methods=['POST'])
|
||||
def deauth():
|
||||
"""Send deauth frames."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio', {})
|
||||
features = request.json.get('features', {})
|
||||
bssid = request.json.get('bssid', '')
|
||||
count = request.json.get('count', '10')
|
||||
cmd = _expand_cmd(features.get('cmd_deauth', ''), radio, bssid=bssid, count=count)
|
||||
result = _ssh_cmd(conn, cmd, timeout=30)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/exec', methods=['POST'])
|
||||
def exec_cmd():
|
||||
"""Execute an arbitrary command on the remote device."""
|
||||
conn = request.json.get('connection', {})
|
||||
radio = request.json.get('radio')
|
||||
cmd = request.json.get('cmd', '')
|
||||
if radio:
|
||||
cmd = _expand_cmd(cmd, radio)
|
||||
result = _ssh_cmd(conn, cmd, timeout=30)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@remote_monitor_bp.route('/api/piap/list')
|
||||
def list_piaps():
|
||||
"""List available .piap files."""
|
||||
piap_files = []
|
||||
for f in sorted(PIAP_DIR.glob('*.piap')):
|
||||
if f.name == 'template.piap':
|
||||
continue
|
||||
try:
|
||||
p = _parse_piap(f)
|
||||
piap_files.append({'filename': f.name, 'name': p['device'].get('name', f.stem)})
|
||||
except:
|
||||
pass
|
||||
return jsonify({'ok': True, 'files': piap_files})
|
||||
@@ -209,14 +209,14 @@ def update_llm():
|
||||
config.set('claude', 'model', request.form.get('model', 'claude-sonnet-4-20250514'))
|
||||
api_key = request.form.get('api_key', '')
|
||||
if api_key:
|
||||
config.set('claude', 'api_key', api_key)
|
||||
config._set_secret('claude_api_key', api_key, 'claude', 'api_key')
|
||||
config.set('claude', 'max_tokens', request.form.get('max_tokens', '4096'))
|
||||
config.set('claude', 'temperature', request.form.get('temperature', '0.7'))
|
||||
elif backend == 'huggingface':
|
||||
config.set('huggingface', 'model', request.form.get('model', 'mistralai/Mistral-7B-Instruct-v0.3'))
|
||||
api_key = request.form.get('api_key', '')
|
||||
if api_key:
|
||||
config.set('huggingface', 'api_key', api_key)
|
||||
config._set_secret('huggingface_api_key', api_key, 'huggingface', 'api_key')
|
||||
config.set('huggingface', 'endpoint', request.form.get('endpoint', ''))
|
||||
config.set('huggingface', 'provider', request.form.get('provider', 'auto'))
|
||||
config.set('huggingface', 'max_tokens', request.form.get('max_tokens', '1024'))
|
||||
@@ -231,7 +231,7 @@ def update_llm():
|
||||
config.set('openai', 'model', request.form.get('model', 'gpt-4o'))
|
||||
api_key = request.form.get('api_key', '')
|
||||
if api_key:
|
||||
config.set('openai', 'api_key', api_key)
|
||||
config._set_secret('openai_api_key', api_key, 'openai', 'api_key')
|
||||
config.set('openai', 'base_url', request.form.get('base_url', 'https://api.openai.com/v1'))
|
||||
config.set('openai', 'max_tokens', request.form.get('max_tokens', '4096'))
|
||||
config.set('openai', 'temperature', request.form.get('temperature', '0.7'))
|
||||
@@ -273,6 +273,8 @@ def llm_settings():
|
||||
claude=config.get_claude_settings(),
|
||||
openai=config.get_openai_settings(),
|
||||
huggingface=config.get_huggingface_settings(),
|
||||
agents=config.get_agents_settings(),
|
||||
mcp_port=config.get('web', 'mcp_port', fallback='8081'),
|
||||
default_models_dir=default_models_dir,
|
||||
)
|
||||
|
||||
@@ -298,6 +300,61 @@ def llm_load():
|
||||
return jsonify({'ok': False, 'error': str(exc)})
|
||||
|
||||
|
||||
@settings_bp.route('/llm/claude-models', methods=['POST'])
|
||||
@login_required
|
||||
def llm_claude_models():
|
||||
"""Fetch available Claude models from the Anthropic API."""
|
||||
_log = logging.getLogger('autarch.settings')
|
||||
try:
|
||||
import anthropic
|
||||
except ImportError:
|
||||
return jsonify({'ok': False, 'error': 'anthropic package not installed'})
|
||||
|
||||
config = current_app.autarch_config
|
||||
api_key = config.get('claude', 'api_key', fallback='') or os.environ.get('ANTHROPIC_API_KEY', '')
|
||||
if not api_key:
|
||||
return jsonify({'ok': False, 'error': 'No Claude API key configured'})
|
||||
|
||||
try:
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
resp = client.models.list(limit=100)
|
||||
models = []
|
||||
for m in resp.data:
|
||||
models.append({
|
||||
'id': m.id,
|
||||
'name': getattr(m, 'display_name', m.id),
|
||||
'created': getattr(m, 'created_at', None),
|
||||
})
|
||||
# Sort: newest first, then alphabetical
|
||||
models.sort(key=lambda x: x['id'])
|
||||
return jsonify({'ok': True, 'models': models})
|
||||
except Exception as exc:
|
||||
_log.error(f"[Claude Models] API error: {exc}", exc_info=True)
|
||||
return jsonify({'ok': False, 'error': str(exc)})
|
||||
|
||||
|
||||
@settings_bp.route('/agents/save', methods=['POST'])
|
||||
@login_required
|
||||
def agents_save():
|
||||
"""Save agent configuration settings."""
|
||||
_log = logging.getLogger('autarch.settings')
|
||||
config = current_app.autarch_config
|
||||
data = request.get_json(silent=True) or {}
|
||||
|
||||
for key in ('backend', 'local_max_steps', 'local_verbose',
|
||||
'claude_enabled', 'claude_model', 'claude_max_tokens', 'claude_max_steps',
|
||||
'openai_enabled', 'openai_model', 'openai_base_url', 'openai_max_tokens', 'openai_max_steps'):
|
||||
if key in data:
|
||||
val = data[key]
|
||||
if isinstance(val, bool):
|
||||
val = 'true' if val else 'false'
|
||||
config.set('agents', key, str(val))
|
||||
|
||||
config.save()
|
||||
_log.info(f"[Agents] Settings saved — backend: {data.get('backend', '?')}")
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@settings_bp.route('/llm/scan-models', methods=['POST'])
|
||||
@login_required
|
||||
def llm_scan_models():
|
||||
@@ -382,6 +439,45 @@ def llm_hf_verify():
|
||||
|
||||
# ── MCP Server API ───────────────────────────────────────────
|
||||
|
||||
@settings_bp.route('/mcp/save', methods=['POST'])
|
||||
@login_required
|
||||
def mcp_save():
|
||||
"""Save MCP server configuration."""
|
||||
_log = logging.getLogger('autarch.settings')
|
||||
config = current_app.autarch_config
|
||||
data = request.get_json(silent=True) or {}
|
||||
|
||||
mcp_keys = ('enabled', 'auto_start', 'transport', 'host', 'port', 'log_level',
|
||||
'instructions', 'auth_enabled', 'auth_token', 'rate_limit',
|
||||
'mask_errors', 'request_timeout', 'max_message_size', 'cors_origins',
|
||||
'ssl_enabled', 'ssl_cert', 'ssl_key', 'disabled_tools',
|
||||
'nmap_timeout', 'tcpdump_timeout', 'whois_timeout', 'dns_timeout',
|
||||
'geoip_timeout', 'geoip_endpoint')
|
||||
|
||||
for key in mcp_keys:
|
||||
if key in data:
|
||||
val = data[key]
|
||||
if isinstance(val, bool):
|
||||
val = 'true' if val else 'false'
|
||||
config.set('mcp', key, str(val))
|
||||
|
||||
config.save()
|
||||
_log.info(f"[MCP] Settings saved")
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@settings_bp.route('/mcp/generate-token', methods=['POST'])
|
||||
@login_required
|
||||
def mcp_generate_token():
|
||||
"""Generate a new random auth token for MCP."""
|
||||
import secrets
|
||||
token = secrets.token_urlsafe(32)
|
||||
config = current_app.autarch_config
|
||||
config.set('mcp', 'auth_token', token)
|
||||
config.save()
|
||||
return jsonify({'ok': True, 'token': token})
|
||||
|
||||
|
||||
@settings_bp.route('/mcp/status', methods=['POST'])
|
||||
@login_required
|
||||
def mcp_status():
|
||||
@@ -400,8 +496,9 @@ def mcp_start():
|
||||
try:
|
||||
from core.mcp_server import start_sse_server
|
||||
config = current_app.autarch_config
|
||||
port = int(config.get('web', 'mcp_port', fallback='8081'))
|
||||
result = start_sse_server(port=port)
|
||||
port = config.get_int('mcp', 'port', 8081)
|
||||
host = config.get('mcp', 'host', '0.0.0.0')
|
||||
result = start_sse_server(host=host, port=port)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e)})
|
||||
@@ -535,6 +632,18 @@ def debug_test():
|
||||
return jsonify({'ok': True, 'sent': 5})
|
||||
|
||||
|
||||
# ==================== MCP SERVER ====================
|
||||
|
||||
@settings_bp.route('/mcp')
|
||||
@login_required
|
||||
def mcp_settings():
|
||||
"""MCP Server configuration and management page."""
|
||||
config = current_app.autarch_config
|
||||
return render_template('mcp_settings.html',
|
||||
mcp=config.get_mcp_settings(),
|
||||
)
|
||||
|
||||
|
||||
# ==================== DEPENDENCIES ====================
|
||||
|
||||
@settings_bp.route('/deps')
|
||||
@@ -544,6 +653,72 @@ def deps_index():
|
||||
return render_template('system_deps.html')
|
||||
|
||||
|
||||
@settings_bp.route('/deps/system-check', methods=['POST'])
|
||||
@login_required
|
||||
def deps_system_check():
|
||||
"""Check non-Python system tools availability."""
|
||||
import shutil
|
||||
tools_to_check = {
|
||||
'nmap': {'cmd': 'nmap', 'version_flag': '--version'},
|
||||
'tshark': {'cmd': 'tshark', 'version_flag': '--version'},
|
||||
'tcpdump': {'cmd': 'tcpdump', 'version_flag': '--version'},
|
||||
'msfconsole': {'cmd': 'msfconsole', 'version_flag': '--version'},
|
||||
'wg': {'cmd': 'wg', 'version_flag': '--version'},
|
||||
'node': {'cmd': 'node', 'version_flag': '--version'},
|
||||
'go': {'cmd': 'go', 'version_flag': 'version'},
|
||||
'adb': {'cmd': 'adb', 'version_flag': 'version'},
|
||||
'upnpc': {'cmd': 'upnpc', 'version_flag': '--help'},
|
||||
'whois': {'cmd': 'whois', 'version_flag': '--version'},
|
||||
'aircrack': {'cmd': 'aircrack-ng', 'version_flag': '--version'},
|
||||
'mdk4': {'cmd': 'mdk4', 'version_flag': '--help'},
|
||||
'sslstrip': {'cmd': 'sslstrip', 'version_flag': '-h'},
|
||||
'iw': {'cmd': 'iw', 'version_flag': '--version'},
|
||||
'nmcli': {'cmd': 'nmcli', 'version_flag': '--version'},
|
||||
'hostapd': {'cmd': 'hostapd', 'version_flag': '-v'},
|
||||
'dnsmasq': {'cmd': 'dnsmasq', 'version_flag': '--version'},
|
||||
'nft': {'cmd': 'nft', 'version_flag': '--version'},
|
||||
'torch': {'python_check': True},
|
||||
}
|
||||
results = {}
|
||||
for name, info in tools_to_check.items():
|
||||
if info.get('python_check'):
|
||||
try:
|
||||
import importlib
|
||||
mod = importlib.import_module('torch')
|
||||
ver = getattr(mod, '__version__', 'unknown')
|
||||
cuda = 'CUDA' if getattr(mod, 'cuda', None) and mod.cuda.is_available() else 'CPU'
|
||||
results[name] = {'found': True, 'version': f'{ver} ({cuda})'}
|
||||
except ImportError:
|
||||
results[name] = {'found': False}
|
||||
continue
|
||||
|
||||
cmd = info['cmd']
|
||||
path = shutil.which(cmd)
|
||||
# Also check bundled tools
|
||||
if not path:
|
||||
from core.paths import find_tool
|
||||
found = find_tool(cmd)
|
||||
if found:
|
||||
path = str(found)
|
||||
if path:
|
||||
version = ''
|
||||
try:
|
||||
r = subprocess.run([path, info['version_flag']],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
output = (r.stdout + r.stderr).strip()
|
||||
# Extract first line with a version-like pattern
|
||||
for line in output.split('\n')[:3]:
|
||||
if any(c.isdigit() for c in line):
|
||||
version = line.strip()[:80]
|
||||
break
|
||||
except Exception:
|
||||
version = 'found'
|
||||
results[name] = {'found': True, 'version': version or 'found'}
|
||||
else:
|
||||
results[name] = {'found': False}
|
||||
return jsonify({'ok': True, 'tools': results})
|
||||
|
||||
|
||||
@settings_bp.route('/deps/check', methods=['POST'])
|
||||
@login_required
|
||||
def deps_check():
|
||||
|
||||
730
web/routes/ssh_manager.py
Normal file
730
web/routes/ssh_manager.py
Normal file
@@ -0,0 +1,730 @@
|
||||
"""SSH / SSHD Configuration Manager routes."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from web.auth import login_required
|
||||
from core.daemon import root_exec
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ssh_manager_bp = Blueprint('ssh_manager', __name__, url_prefix='/ssh')
|
||||
|
||||
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _parse_sshd_config(text: str) -> dict:
|
||||
"""Parse sshd_config text into a dict of {directive: value} pairs.
|
||||
|
||||
Handles comments, blank lines, and Match blocks (flattened).
|
||||
For repeated directives only the first occurrence is kept (sshd semantics).
|
||||
"""
|
||||
result = {}
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
parts = stripped.split(None, 1)
|
||||
if len(parts) == 2:
|
||||
key, value = parts
|
||||
elif len(parts) == 1:
|
||||
key, value = parts[0], ''
|
||||
else:
|
||||
continue
|
||||
# sshd uses first-match semantics; keep only the first occurrence
|
||||
if key not in result:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
# ─── Routes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@ssh_manager_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
"""Render the SSH manager page."""
|
||||
return render_template('ssh_manager.html')
|
||||
|
||||
|
||||
# ── Status ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@ssh_manager_bp.route('/status', methods=['GET'])
|
||||
@login_required
|
||||
def status():
|
||||
"""Return JSON with SSH service status."""
|
||||
# Check active state — try sshd first, then ssh
|
||||
active_result = root_exec('systemctl is-active sshd', timeout=10)
|
||||
if active_result.get('code', 1) != 0:
|
||||
active_result = root_exec('systemctl is-active ssh', timeout=10)
|
||||
active = active_result.get('stdout', '').strip()
|
||||
|
||||
# Enabled state
|
||||
enabled_result = root_exec('systemctl is-enabled sshd', timeout=10)
|
||||
if enabled_result.get('code', 1) != 0:
|
||||
enabled_result = root_exec('systemctl is-enabled ssh', timeout=10)
|
||||
enabled = enabled_result.get('stdout', '').strip()
|
||||
|
||||
# Config exists
|
||||
config_exists = os.path.isfile('/etc/ssh/sshd_config')
|
||||
|
||||
# Version — sshd -V prints to stderr on OpenSSH
|
||||
ver_result = root_exec('sshd -V', timeout=10)
|
||||
version = (ver_result.get('stderr', '') + ver_result.get('stdout', '')).strip()
|
||||
# Often the first meaningful line is all we need
|
||||
if version:
|
||||
version = version.splitlines()[0]
|
||||
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'active': active,
|
||||
'enabled': enabled,
|
||||
'config_exists': config_exists,
|
||||
'version': version,
|
||||
})
|
||||
|
||||
|
||||
# ── Security Scan ────────────────────────────────────────────────────────────
|
||||
|
||||
@ssh_manager_bp.route('/scan', methods=['POST'])
|
||||
@login_required
|
||||
def scan():
|
||||
"""Security scan of sshd_config."""
|
||||
result = root_exec('cat /etc/ssh/sshd_config', timeout=10)
|
||||
if not result.get('ok'):
|
||||
return jsonify({'ok': False, 'error': 'Failed to read sshd_config: ' + result.get('stderr', '')}), 500
|
||||
|
||||
cfg = _parse_sshd_config(result['stdout'])
|
||||
checks = []
|
||||
|
||||
def _add(name, severity, current, recommended, description, status='fail'):
|
||||
checks.append({
|
||||
'name': name,
|
||||
'status': status,
|
||||
'severity': severity,
|
||||
'current_value': current,
|
||||
'recommended': recommended,
|
||||
'description': description,
|
||||
})
|
||||
|
||||
# PermitRootLogin
|
||||
val = cfg.get('PermitRootLogin', 'prohibit-password')
|
||||
if val.lower() == 'yes':
|
||||
_add('PermitRootLogin', 'CRITICAL', val, 'no', 'Root login with password is enabled — extremely dangerous.')
|
||||
else:
|
||||
_add('PermitRootLogin', 'CRITICAL', val, 'no', 'Root login is restricted.', 'pass')
|
||||
|
||||
# PasswordAuthentication
|
||||
val = cfg.get('PasswordAuthentication', 'yes')
|
||||
if val.lower() == 'yes':
|
||||
_add('PasswordAuthentication', 'WARNING', val, 'no', 'Password authentication is enabled — prefer SSH keys.')
|
||||
else:
|
||||
_add('PasswordAuthentication', 'WARNING', val, 'no', 'Password authentication is disabled.', 'pass')
|
||||
|
||||
# PermitEmptyPasswords
|
||||
val = cfg.get('PermitEmptyPasswords', 'no')
|
||||
if val.lower() == 'yes':
|
||||
_add('PermitEmptyPasswords', 'CRITICAL', val, 'no', 'Empty passwords are permitted — critical risk.')
|
||||
else:
|
||||
_add('PermitEmptyPasswords', 'CRITICAL', val, 'no', 'Empty passwords are not permitted.', 'pass')
|
||||
|
||||
# X11Forwarding
|
||||
val = cfg.get('X11Forwarding', 'no')
|
||||
if val.lower() == 'yes':
|
||||
_add('X11Forwarding', 'LOW', val, 'no', 'X11 forwarding is enabled — consider disabling if not needed.')
|
||||
else:
|
||||
_add('X11Forwarding', 'LOW', val, 'no', 'X11 forwarding is disabled.', 'pass')
|
||||
|
||||
# Port
|
||||
val = cfg.get('Port', '22')
|
||||
if val == '22':
|
||||
_add('Port', 'INFO', val, 'non-default', 'SSH is running on the default port — consider changing to reduce automated attacks.', 'info')
|
||||
else:
|
||||
_add('Port', 'INFO', val, 'non-default', 'SSH is running on a non-default port.', 'pass')
|
||||
|
||||
# Protocol
|
||||
val = cfg.get('Protocol', '')
|
||||
if val == '1':
|
||||
_add('Protocol', 'CRITICAL', val, '2', 'SSHv1 is enabled — it has known vulnerabilities.')
|
||||
elif val:
|
||||
_add('Protocol', 'CRITICAL', val, '2', 'Protocol version is set.', 'pass')
|
||||
|
||||
# MaxAuthTries
|
||||
val = cfg.get('MaxAuthTries', '6')
|
||||
try:
|
||||
if int(val) > 6:
|
||||
_add('MaxAuthTries', 'WARNING', val, '3-6', 'MaxAuthTries is high — allows excessive brute-force attempts.')
|
||||
else:
|
||||
_add('MaxAuthTries', 'WARNING', val, '3-6', 'MaxAuthTries is within acceptable range.', 'pass')
|
||||
except ValueError:
|
||||
_add('MaxAuthTries', 'WARNING', val, '3-6', 'Could not parse MaxAuthTries value.')
|
||||
|
||||
# LoginGraceTime
|
||||
val = cfg.get('LoginGraceTime', '120')
|
||||
try:
|
||||
numeric = int(val.rstrip('smSM'))
|
||||
if numeric > 120:
|
||||
_add('LoginGraceTime', 'WARNING', val, '60-120', 'LoginGraceTime is too long — connections can linger.')
|
||||
else:
|
||||
_add('LoginGraceTime', 'WARNING', val, '60-120', 'LoginGraceTime is acceptable.', 'pass')
|
||||
except ValueError:
|
||||
_add('LoginGraceTime', 'WARNING', val, '60-120', 'Could not parse LoginGraceTime value.')
|
||||
|
||||
# UsePAM
|
||||
val = cfg.get('UsePAM', 'yes')
|
||||
if val.lower() == 'no':
|
||||
_add('UsePAM', 'WARNING', val, 'yes', 'PAM is disabled — may break system authentication features.')
|
||||
else:
|
||||
_add('UsePAM', 'WARNING', val, 'yes', 'PAM is enabled.', 'pass')
|
||||
|
||||
# AllowTcpForwarding
|
||||
val = cfg.get('AllowTcpForwarding', 'yes')
|
||||
if val.lower() == 'yes':
|
||||
_add('AllowTcpForwarding', 'LOW', val, 'no', 'TCP forwarding is enabled — consider disabling if not required.')
|
||||
else:
|
||||
_add('AllowTcpForwarding', 'LOW', val, 'no', 'TCP forwarding is disabled.', 'pass')
|
||||
|
||||
# ClientAliveInterval
|
||||
val = cfg.get('ClientAliveInterval', '0')
|
||||
try:
|
||||
if int(val) == 0:
|
||||
_add('ClientAliveInterval', 'WARNING', val, '300', 'No client alive interval — idle sessions will never timeout.')
|
||||
else:
|
||||
_add('ClientAliveInterval', 'WARNING', val, '300', 'Client alive interval is set.', 'pass')
|
||||
except ValueError:
|
||||
_add('ClientAliveInterval', 'WARNING', val, '300', 'Could not parse ClientAliveInterval value.')
|
||||
|
||||
return jsonify({'ok': True, 'checks': checks})
|
||||
|
||||
|
||||
# ── Config Read / Save ───────────────────────────────────────────────────────
|
||||
|
||||
@ssh_manager_bp.route('/config', methods=['GET'])
|
||||
@login_required
|
||||
def config_read():
|
||||
"""Read sshd_config file contents."""
|
||||
result = root_exec('cat /etc/ssh/sshd_config', timeout=10)
|
||||
if not result.get('ok'):
|
||||
return jsonify({'ok': False, 'error': result.get('stderr', 'Failed to read config')}), 500
|
||||
return jsonify({'ok': True, 'config': result['stdout']})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/config/save', methods=['POST'])
|
||||
@login_required
|
||||
def config_save():
|
||||
"""Save sshd_config with backup and syntax validation."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data or 'config' not in data:
|
||||
return jsonify({'ok': False, 'error': 'Missing config field'}), 400
|
||||
|
||||
config_text = data['config']
|
||||
timestamp = int(time.time())
|
||||
backup_path = f'/etc/ssh/sshd_config.bak.{timestamp}'
|
||||
|
||||
# 1. Create backup
|
||||
bak = root_exec(f'cp /etc/ssh/sshd_config {backup_path}', timeout=10)
|
||||
if not bak.get('ok'):
|
||||
return jsonify({'ok': False, 'error': 'Failed to create backup: ' + bak.get('stderr', '')}), 500
|
||||
|
||||
# 2. Write new config via a temp file
|
||||
try:
|
||||
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.sshd_config', delete=False)
|
||||
tmp.write(config_text)
|
||||
tmp.close()
|
||||
|
||||
cp_result = root_exec(f'cp {tmp.name} /etc/ssh/sshd_config', timeout=10)
|
||||
os.unlink(tmp.name)
|
||||
|
||||
if not cp_result.get('ok'):
|
||||
# Restore backup
|
||||
root_exec(f'cp {backup_path} /etc/ssh/sshd_config', timeout=10)
|
||||
return jsonify({'ok': False, 'error': 'Failed to write config: ' + cp_result.get('stderr', '')}), 500
|
||||
except Exception as exc:
|
||||
root_exec(f'cp {backup_path} /etc/ssh/sshd_config', timeout=10)
|
||||
return jsonify({'ok': False, 'error': f'Write error: {exc}'}), 500
|
||||
|
||||
# 3. Validate syntax
|
||||
validate = root_exec('sshd -t', timeout=10)
|
||||
if validate.get('code', 1) != 0:
|
||||
# Restore backup
|
||||
root_exec(f'cp {backup_path} /etc/ssh/sshd_config', timeout=10)
|
||||
err = (validate.get('stderr', '') + validate.get('stdout', '')).strip()
|
||||
return jsonify({'ok': False, 'error': 'Syntax validation failed — backup restored.', 'validation': err}), 400
|
||||
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'validation': 'Configuration is valid.',
|
||||
'backup': backup_path,
|
||||
})
|
||||
|
||||
|
||||
# ── Config Generate ──────────────────────────────────────────────────────────
|
||||
|
||||
# All supported directives grouped logically
|
||||
_CONFIG_GROUPS = {
|
||||
'Connection': [
|
||||
'Port', 'AddressFamily', 'ListenAddress', 'Protocol',
|
||||
],
|
||||
'Authentication': [
|
||||
'PermitRootLogin', 'PubkeyAuthentication', 'PasswordAuthentication',
|
||||
'PermitEmptyPasswords', 'ChallengeResponseAuthentication',
|
||||
'KbdInteractiveAuthentication', 'UsePAM', 'AuthenticationMethods',
|
||||
'MaxAuthTries', 'LoginGraceTime',
|
||||
],
|
||||
'Keys': [
|
||||
'HostKey', 'AuthorizedKeysFile', 'AuthorizedPrincipalsFile',
|
||||
],
|
||||
'Session': [
|
||||
'MaxSessions', 'ClientAliveInterval', 'ClientAliveCountMax', 'TCPKeepAlive',
|
||||
],
|
||||
'Access Control': [
|
||||
'AllowUsers', 'AllowGroups', 'DenyUsers', 'DenyGroups',
|
||||
],
|
||||
'Forwarding': [
|
||||
'AllowTcpForwarding', 'X11Forwarding', 'X11DisplayOffset',
|
||||
'GatewayPorts', 'PermitTunnel',
|
||||
],
|
||||
'Logging': [
|
||||
'SyslogFacility', 'LogLevel',
|
||||
],
|
||||
'Security': [
|
||||
'StrictModes', 'HostbasedAuthentication', 'IgnoreRhosts',
|
||||
'IgnoreUserKnownHosts', 'RekeyLimit', 'Ciphers', 'MACs', 'KexAlgorithms',
|
||||
],
|
||||
'Other': [
|
||||
'Subsystem', 'Banner', 'PrintMotd', 'PrintLastLog',
|
||||
'AcceptEnv', 'UseDNS', 'PermitUserEnvironment', 'Compression',
|
||||
],
|
||||
}
|
||||
|
||||
# Flatten for quick lookup
|
||||
_ALL_DIRECTIVES = set()
|
||||
for _directives in _CONFIG_GROUPS.values():
|
||||
_ALL_DIRECTIVES.update(_directives)
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/config/generate', methods=['POST'])
|
||||
@login_required
|
||||
def config_generate():
|
||||
"""Generate a hardened sshd_config from submitted fields.
|
||||
|
||||
Returns the text without saving so the user can review it first.
|
||||
"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
|
||||
lines = [
|
||||
'# sshd_config — generated by AUTARCH SSH Manager',
|
||||
f'# Generated: {time.strftime("%Y-%m-%d %H:%M:%S %Z")}',
|
||||
'#',
|
||||
'# Review carefully before applying.',
|
||||
'',
|
||||
]
|
||||
|
||||
for group_name, directives in _CONFIG_GROUPS.items():
|
||||
group_lines = []
|
||||
for directive in directives:
|
||||
value = data.get(directive)
|
||||
if value is not None and str(value).strip() != '':
|
||||
group_lines.append(f'{directive} {value}')
|
||||
if group_lines:
|
||||
lines.append(f'# ── {group_name} {"─" * (60 - len(group_name))}')
|
||||
lines.extend(group_lines)
|
||||
lines.append('')
|
||||
|
||||
config_text = '\n'.join(lines) + '\n'
|
||||
return jsonify({'ok': True, 'config': config_text})
|
||||
|
||||
|
||||
# ── Service Control ──────────────────────────────────────────────────────────
|
||||
|
||||
_ALLOWED_ACTIONS = {'start', 'stop', 'restart', 'enable', 'disable'}
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/service/<action>', methods=['POST'])
|
||||
@login_required
|
||||
def service_action(action):
|
||||
"""Start / stop / restart / enable / disable the SSH service."""
|
||||
if action not in _ALLOWED_ACTIONS:
|
||||
return jsonify({'ok': False, 'error': f'Invalid action: {action}'}), 400
|
||||
|
||||
# Try sshd first, fall back to ssh
|
||||
result = root_exec(f'systemctl {action} sshd', timeout=20)
|
||||
if result.get('code', 1) != 0:
|
||||
result = root_exec(f'systemctl {action} ssh', timeout=20)
|
||||
|
||||
output = (result.get('stdout', '') + '\n' + result.get('stderr', '')).strip()
|
||||
return jsonify({
|
||||
'ok': result.get('code', 1) == 0,
|
||||
'output': output,
|
||||
})
|
||||
|
||||
|
||||
# ── Key Generation ───────────────────────────────────────────────────────────
|
||||
|
||||
@ssh_manager_bp.route('/keys/generate', methods=['POST'])
|
||||
@login_required
|
||||
def keys_generate():
|
||||
"""Generate an SSH key pair (does not require root)."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
key_type = data.get('type', 'ed25519')
|
||||
bits = int(data.get('bits', 4096))
|
||||
comment = data.get('comment', '')
|
||||
passphrase = data.get('passphrase', '')
|
||||
|
||||
if key_type not in ('ed25519', 'rsa'):
|
||||
return jsonify({'ok': False, 'error': 'Unsupported key type (use ed25519 or rsa)'}), 400
|
||||
|
||||
try:
|
||||
tmp_dir = tempfile.mkdtemp(prefix='autarch_sshkey_')
|
||||
key_path = os.path.join(tmp_dir, 'id_key')
|
||||
|
||||
cmd = ['ssh-keygen', '-t', key_type, '-f', key_path, '-N', passphrase]
|
||||
if key_type == 'rsa':
|
||||
cmd += ['-b', str(bits)]
|
||||
if comment:
|
||||
cmd += ['-C', comment]
|
||||
|
||||
import subprocess
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
if proc.returncode != 0:
|
||||
return jsonify({'ok': False, 'error': proc.stderr.strip()}), 500
|
||||
|
||||
with open(key_path, 'r') as f:
|
||||
private_key = f.read()
|
||||
with open(key_path + '.pub', 'r') as f:
|
||||
public_key = f.read().strip()
|
||||
|
||||
# Get fingerprint
|
||||
fp_proc = subprocess.run(
|
||||
['ssh-keygen', '-lf', key_path + '.pub'],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
fingerprint = fp_proc.stdout.strip()
|
||||
|
||||
# Clean up temp files
|
||||
os.unlink(key_path)
|
||||
os.unlink(key_path + '.pub')
|
||||
os.rmdir(tmp_dir)
|
||||
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'public_key': public_key,
|
||||
'private_key': private_key,
|
||||
'fingerprint': fingerprint,
|
||||
})
|
||||
except Exception as exc:
|
||||
log.exception('SSH key generation failed')
|
||||
return jsonify({'ok': False, 'error': str(exc)}), 500
|
||||
|
||||
|
||||
# ── Host Keys ────────────────────────────────────────────────────────────────
|
||||
|
||||
@ssh_manager_bp.route('/keys/host', methods=['GET'])
|
||||
@login_required
|
||||
def keys_host():
|
||||
"""List host public keys and their fingerprints."""
|
||||
import subprocess
|
||||
|
||||
result = root_exec('ls /etc/ssh/ssh_host_*_key.pub', timeout=10)
|
||||
if not result.get('ok'):
|
||||
return jsonify({'ok': False, 'error': 'No host keys found or permission denied.'}), 500
|
||||
|
||||
pub_files = [f.strip() for f in result['stdout'].splitlines() if f.strip()]
|
||||
keys = []
|
||||
for pub_file in pub_files:
|
||||
# Read the key
|
||||
cat_result = root_exec(f'cat {pub_file}', timeout=10)
|
||||
if not cat_result.get('ok'):
|
||||
continue
|
||||
key_text = cat_result['stdout'].strip()
|
||||
key_type = key_text.split()[0] if key_text else 'unknown'
|
||||
|
||||
# Fingerprint
|
||||
fp_result = root_exec(f'ssh-keygen -lf {pub_file}', timeout=10)
|
||||
fingerprint = fp_result.get('stdout', '').strip() if fp_result.get('ok') else ''
|
||||
|
||||
keys.append({
|
||||
'type': key_type,
|
||||
'fingerprint': fingerprint,
|
||||
'file': pub_file,
|
||||
})
|
||||
|
||||
return jsonify({'ok': True, 'keys': keys})
|
||||
|
||||
|
||||
# ── Authorized Keys ─────────────────────────────────────────────────────────
|
||||
|
||||
def _authorized_keys_path() -> str:
|
||||
return os.path.expanduser('~/.ssh/authorized_keys')
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/keys/authorized', methods=['GET'])
|
||||
@login_required
|
||||
def keys_authorized():
|
||||
"""Read ~/.ssh/authorized_keys."""
|
||||
ak_path = _authorized_keys_path()
|
||||
keys = []
|
||||
try:
|
||||
if os.path.isfile(ak_path):
|
||||
with open(ak_path, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
parts = line.split(None, 2)
|
||||
comment = parts[2] if len(parts) >= 3 else ''
|
||||
keys.append({'key': line, 'comment': comment})
|
||||
except Exception as exc:
|
||||
return jsonify({'ok': False, 'error': str(exc)}), 500
|
||||
|
||||
return jsonify({'ok': True, 'keys': keys})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/keys/authorized/add', methods=['POST'])
|
||||
@login_required
|
||||
def keys_authorized_add():
|
||||
"""Append a public key to authorized_keys."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
key = data.get('key', '').strip()
|
||||
if not key:
|
||||
return jsonify({'ok': False, 'error': 'No key provided'}), 400
|
||||
|
||||
ak_path = _authorized_keys_path()
|
||||
try:
|
||||
ssh_dir = os.path.dirname(ak_path)
|
||||
os.makedirs(ssh_dir, mode=0o700, exist_ok=True)
|
||||
with open(ak_path, 'a') as f:
|
||||
f.write(key + '\n')
|
||||
os.chmod(ak_path, 0o600)
|
||||
except Exception as exc:
|
||||
return jsonify({'ok': False, 'error': str(exc)}), 500
|
||||
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/keys/authorized/remove', methods=['POST'])
|
||||
@login_required
|
||||
def keys_authorized_remove():
|
||||
"""Remove a key by index from authorized_keys."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
index = data.get('index')
|
||||
if index is None:
|
||||
return jsonify({'ok': False, 'error': 'No index provided'}), 400
|
||||
|
||||
try:
|
||||
index = int(index)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'ok': False, 'error': 'Index must be an integer'}), 400
|
||||
|
||||
ak_path = _authorized_keys_path()
|
||||
try:
|
||||
if not os.path.isfile(ak_path):
|
||||
return jsonify({'ok': False, 'error': 'authorized_keys file does not exist'}), 404
|
||||
|
||||
with open(ak_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Build list of non-empty, non-comment key lines with original indices
|
||||
key_lines = []
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped and not stripped.startswith('#'):
|
||||
key_lines.append(i)
|
||||
|
||||
if index < 0 or index >= len(key_lines):
|
||||
return jsonify({'ok': False, 'error': f'Index {index} out of range (0-{len(key_lines) - 1})'}), 400
|
||||
|
||||
# Remove the line at the original file index
|
||||
del lines[key_lines[index]]
|
||||
|
||||
with open(ak_path, 'w') as f:
|
||||
f.writelines(lines)
|
||||
os.chmod(ak_path, 0o600)
|
||||
except Exception as exc:
|
||||
return jsonify({'ok': False, 'error': str(exc)}), 500
|
||||
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# FAIL2BAN
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/status')
|
||||
@login_required
|
||||
def f2b_status():
|
||||
r = root_exec(['fail2ban-client', 'status'])
|
||||
if not r['ok']:
|
||||
return jsonify({'ok': False, 'error': r['stderr'] or 'fail2ban not running', 'active': False})
|
||||
jails = []
|
||||
total_banned = 0
|
||||
for line in r['stdout'].split('\n'):
|
||||
if 'Jail list:' in line:
|
||||
jails = [j.strip() for j in line.split(':')[1].strip().split(',') if j.strip()]
|
||||
jail_details = []
|
||||
for jail in jails:
|
||||
jr = root_exec(['fail2ban-client', 'status', jail])
|
||||
banned = 0
|
||||
banned_ips = []
|
||||
if jr['ok']:
|
||||
for line in jr['stdout'].split('\n'):
|
||||
if 'Currently banned:' in line:
|
||||
try: banned = int(line.split(':')[1].strip())
|
||||
except: pass
|
||||
elif 'Banned IP list:' in line:
|
||||
banned_ips = [ip.strip() for ip in line.split(':',1)[1].strip().split() if ip.strip()]
|
||||
total_banned += banned
|
||||
jail_details.append({'name': jail, 'banned': banned, 'banned_ips': banned_ips})
|
||||
sr = root_exec(['systemctl', 'is-active', 'fail2ban'])
|
||||
return jsonify({'ok': True, 'active': sr['stdout'].strip() == 'active',
|
||||
'jail_count': len(jails), 'total_banned': total_banned, 'jails': jail_details})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/service/<action>', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_service(action):
|
||||
if action not in ('start', 'stop', 'restart', 'enable', 'disable'):
|
||||
return jsonify({'ok': False, 'error': 'Invalid action'})
|
||||
r = root_exec(['systemctl', action, 'fail2ban'])
|
||||
return jsonify({'ok': r['ok'], 'output': r['stdout'] + r['stderr']})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/banned')
|
||||
@login_required
|
||||
def f2b_banned():
|
||||
r = root_exec(['fail2ban-client', 'status'])
|
||||
if not r['ok']:
|
||||
return jsonify({'ok': False, 'error': 'fail2ban not running'})
|
||||
all_banned = []
|
||||
jails = []
|
||||
for line in r['stdout'].split('\n'):
|
||||
if 'Jail list:' in line:
|
||||
jails = [j.strip() for j in line.split(':')[1].strip().split(',') if j.strip()]
|
||||
for jail in jails:
|
||||
jr = root_exec(['fail2ban-client', 'status', jail])
|
||||
if jr['ok']:
|
||||
for line in jr['stdout'].split('\n'):
|
||||
if 'Banned IP list:' in line:
|
||||
for ip in line.split(':', 1)[1].strip().split():
|
||||
if ip.strip():
|
||||
all_banned.append({'ip': ip.strip(), 'jail': jail})
|
||||
return jsonify({'ok': True, 'banned': all_banned, 'total': len(all_banned)})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/ban', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_ban():
|
||||
data = request.get_json(silent=True) or {}
|
||||
ip = data.get('ip', '').strip()
|
||||
jail = data.get('jail', 'sshd').strip()
|
||||
if not ip: return jsonify({'ok': False, 'error': 'IP required'})
|
||||
r = root_exec(['fail2ban-client', 'set', jail, 'banip', ip])
|
||||
return jsonify({'ok': r['ok'], 'output': r['stdout'] + r['stderr']})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/unban', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_unban():
|
||||
data = request.get_json(silent=True) or {}
|
||||
ip = data.get('ip', '').strip()
|
||||
jail = data.get('jail', '').strip()
|
||||
if not ip: return jsonify({'ok': False, 'error': 'IP required'})
|
||||
r = root_exec(['fail2ban-client', 'set', jail, 'unbanip', ip]) if jail else root_exec(['fail2ban-client', 'unban', ip])
|
||||
return jsonify({'ok': r['ok'], 'output': r['stdout'] + r['stderr']})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/search', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_search():
|
||||
data = request.get_json(silent=True) or {}
|
||||
ip = data.get('ip', '').strip()
|
||||
if not ip: return jsonify({'ok': False, 'error': 'IP required'})
|
||||
results = []
|
||||
r = root_exec(['fail2ban-client', 'status'])
|
||||
jails = []
|
||||
if r['ok']:
|
||||
for line in r['stdout'].split('\n'):
|
||||
if 'Jail list:' in line:
|
||||
jails = [j.strip() for j in line.split(':')[1].strip().split(',') if j.strip()]
|
||||
for jail in jails:
|
||||
jr = root_exec(['fail2ban-client', 'status', jail])
|
||||
if jr['ok'] and ip in jr['stdout']:
|
||||
results.append({'jail': jail, 'status': 'banned'})
|
||||
lr = root_exec(['grep', ip, '/var/log/fail2ban.log'])
|
||||
log_entries = [l.strip() for l in lr['stdout'].strip().split('\n')[-20:] if l.strip()] if lr['ok'] else []
|
||||
return jsonify({'ok': True, 'ip': ip, 'active_bans': results, 'log_entries': log_entries})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/jail/create', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_jail_create():
|
||||
data = request.get_json(silent=True) or {}
|
||||
name = data.get('name', '').strip()
|
||||
if not name or not re.match(r'^[a-zA-Z0-9_-]+$', name):
|
||||
return jsonify({'ok': False, 'error': 'Invalid jail name'})
|
||||
config = f"[{name}]\nenabled = {'true' if data.get('enabled', True) else 'false'}\nfilter = {data.get('filter', name)}\nlogpath = {data.get('logpath', '')}\nmaxretry = {data.get('maxretry', '5')}\nfindtime = {data.get('findtime', '10m')}\nbantime = {data.get('bantime', '1h')}\naction = {data.get('action', '%(action_mwl)s')}\n"
|
||||
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.local', delete=False)
|
||||
tmp.write(config); tmp.close()
|
||||
r = root_exec(['cp', tmp.name, f'/etc/fail2ban/jail.d/{name}.local'])
|
||||
os.unlink(tmp.name)
|
||||
if not r['ok']: return jsonify({'ok': False, 'error': r['stderr']})
|
||||
root_exec(['fail2ban-client', 'reload'])
|
||||
return jsonify({'ok': True, 'config': config})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/scan-apps', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_scan_apps():
|
||||
checks = [
|
||||
('sshd', 'openssh-server', '/var/log/auth.log', 'sshd'),
|
||||
('apache2', 'apache2', '/var/log/apache2/error.log', 'apache-auth'),
|
||||
('nginx', 'nginx', '/var/log/nginx/error.log', 'nginx-http-auth'),
|
||||
('postfix', 'postfix', '/var/log/mail.log', 'postfix'),
|
||||
('dovecot', 'dovecot-core', '/var/log/mail.log', 'dovecot'),
|
||||
('mysql', 'mysql-server', '/var/log/mysql/error.log', 'mysqld-auth'),
|
||||
('postgresql', 'postgresql', '/var/log/postgresql/*.log', 'postgresql'),
|
||||
('vsftpd', 'vsftpd', '/var/log/vsftpd.log', 'vsftpd'),
|
||||
('exim4', 'exim4', '/var/log/exim4/mainlog', 'exim'),
|
||||
('recidive', None, '/var/log/fail2ban.log', 'recidive'),
|
||||
]
|
||||
existing = set()
|
||||
r = root_exec(['fail2ban-client', 'status'])
|
||||
if r['ok']:
|
||||
for line in r['stdout'].split('\n'):
|
||||
if 'Jail list:' in line:
|
||||
existing = set(j.strip() for j in line.split(':')[1].strip().split(',') if j.strip())
|
||||
apps = []
|
||||
for service, pkg, logpath, filt in checks:
|
||||
installed = True if not pkg else (root_exec(['dpkg', '-l', pkg])['ok'] and 'ii' in root_exec(['dpkg', '-l', pkg])['stdout'])
|
||||
lr = root_exec(['ls', logpath.split('*')[0] if '*' in logpath else logpath])
|
||||
apps.append({'service': service, 'package': pkg, 'installed': installed,
|
||||
'log_path': logpath, 'log_exists': lr['ok'], 'filter': filt,
|
||||
'has_jail': filt in existing or service in existing})
|
||||
return jsonify({'ok': True, 'apps': apps})
|
||||
|
||||
|
||||
@ssh_manager_bp.route('/fail2ban/auto-config', methods=['POST'])
|
||||
@login_required
|
||||
def f2b_auto_config():
|
||||
data = request.get_json(silent=True) or {}
|
||||
apply_now = data.get('apply', False)
|
||||
checks = [
|
||||
('sshd', '/var/log/auth.log', 'sshd', '5', '10m', '1h'),
|
||||
('apache2', '/var/log/apache2/error.log', 'apache-auth', '5', '10m', '1h'),
|
||||
('nginx', '/var/log/nginx/error.log', 'nginx-http-auth', '5', '10m', '1h'),
|
||||
('postfix', '/var/log/mail.log', 'postfix', '5', '10m', '1h'),
|
||||
('recidive', '/var/log/fail2ban.log', 'recidive', '3', '1d', '1w'),
|
||||
]
|
||||
generated = []
|
||||
for svc, logpath, filt, maxr, findt, bant in checks:
|
||||
if not root_exec(['ls', logpath])['ok']: continue
|
||||
generated.append({'service': svc, 'config': f"[{svc}]\nenabled = true\nfilter = {filt}\nlogpath = {logpath}\nmaxretry = {maxr}\nfindtime = {findt}\nbantime = {bant}\n"})
|
||||
if apply_now and generated:
|
||||
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.local', delete=False)
|
||||
tmp.write('\n'.join(g['config'] for g in generated)); tmp.close()
|
||||
root_exec(['cp', tmp.name, '/etc/fail2ban/jail.d/autarch-auto.local'])
|
||||
os.unlink(tmp.name)
|
||||
root_exec(['fail2ban-client', 'reload'])
|
||||
return jsonify({'ok': True, 'generated': generated, 'applied': apply_now, 'count': len(generated)})
|
||||
@@ -41,17 +41,41 @@ def _new_target(data: dict) -> dict:
|
||||
host = data.get('host', '').strip()
|
||||
now = _now()
|
||||
return {
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': data.get('name', '').strip() or host,
|
||||
'host': host,
|
||||
'type': data.get('type', 'ip'),
|
||||
'status': data.get('status', 'active'),
|
||||
'os': data.get('os', 'Unknown'),
|
||||
'tags': [t.strip() for t in data.get('tags', '').split(',') if t.strip()],
|
||||
'ports': data.get('ports', '').strip(),
|
||||
'notes': data.get('notes', '').strip(),
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': data.get('name', '').strip() or host,
|
||||
'host': host,
|
||||
'type': data.get('type', 'ip'),
|
||||
'status': data.get('status', 'active'),
|
||||
'os': data.get('os', 'Unknown'),
|
||||
'tags': [t.strip() for t in data.get('tags', '').split(',') if t.strip()],
|
||||
'ports': data.get('ports', '').strip(),
|
||||
'notes': data.get('notes', '').strip(),
|
||||
# Investigation profile fields
|
||||
'ipv4': data.get('ipv4', '').strip(),
|
||||
'ipv6': data.get('ipv6', '').strip(),
|
||||
'domain': data.get('domain', '').strip(),
|
||||
'dns_records': data.get('dns_records', '').strip(),
|
||||
'email': data.get('email', '').strip(),
|
||||
'usernames': data.get('usernames', '').strip(),
|
||||
'geo_country': data.get('geo_country', '').strip(),
|
||||
'geo_city': data.get('geo_city', '').strip(),
|
||||
'geo_isp': data.get('geo_isp', '').strip(),
|
||||
'geo_asn': data.get('geo_asn', '').strip(),
|
||||
'geo_coords': data.get('geo_coords', '').strip(),
|
||||
'traceroute': data.get('traceroute', '').strip(),
|
||||
'whois': data.get('whois', '').strip(),
|
||||
'rdns': data.get('rdns', '').strip(),
|
||||
'mac_address': data.get('mac_address', '').strip(),
|
||||
'hostname': data.get('hostname', '').strip(),
|
||||
'services': data.get('services', '').strip(),
|
||||
'vulns': data.get('vulns', '').strip(),
|
||||
'threat_level': data.get('threat_level', 'unknown'),
|
||||
'source': data.get('source', '').strip(),
|
||||
'first_seen': data.get('first_seen', now),
|
||||
'last_seen': data.get('last_seen', now),
|
||||
'custom_fields': data.get('custom_fields', []),
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
}
|
||||
|
||||
|
||||
@@ -165,3 +189,145 @@ def import_targets():
|
||||
added += 1
|
||||
_save(existing)
|
||||
return jsonify({'ok': True, 'added': added, 'total': len(existing)})
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# INVESTIGATION REPORTS (IR)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _ir_file() -> Path:
|
||||
p = Path(__file__).parent.parent.parent / 'data' / 'reports' / 'investigation_reports.json'
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
def _load_irs() -> list:
|
||||
f = _ir_file()
|
||||
if f.exists():
|
||||
try:
|
||||
return json.loads(f.read_text())
|
||||
except Exception:
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
def _save_irs(irs: list):
|
||||
_ir_file().write_text(json.dumps(irs, indent=2))
|
||||
|
||||
|
||||
def _generate_ir_id(ip: str = '') -> str:
|
||||
"""Generate IR identifier — hex from IP if available, otherwise random 9-char hex."""
|
||||
if ip and ip.strip():
|
||||
# Convert IP octets to hex
|
||||
parts = ip.strip().split('.')
|
||||
if len(parts) == 4:
|
||||
try:
|
||||
return 'IR-' + ''.join(f'{int(p):02X}' for p in parts)
|
||||
except ValueError:
|
||||
pass
|
||||
# Random 9-char hex
|
||||
return 'IR-' + uuid.uuid4().hex[:9].upper()
|
||||
|
||||
|
||||
@targets_bp.route('/ir')
|
||||
@login_required
|
||||
def ir_list():
|
||||
"""List all investigation reports."""
|
||||
return jsonify({'ok': True, 'reports': _load_irs()})
|
||||
|
||||
|
||||
@targets_bp.route('/ir/create', methods=['POST'])
|
||||
@login_required
|
||||
def ir_create():
|
||||
"""Create a new investigation report."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
ip = data.get('ip', data.get('host', ''))
|
||||
ir_id = _generate_ir_id(ip)
|
||||
|
||||
# Detect if created by HAL
|
||||
is_hal = 'HAL' in data.get('source', '') or 'hal' in data.get('source', '').lower()
|
||||
|
||||
report = {
|
||||
'id': ir_id,
|
||||
'title': data.get('title', f'Investigation {ir_id}'),
|
||||
'ip': ip,
|
||||
'status': data.get('status', 'open'),
|
||||
'threat_level': data.get('threat_level', 'unknown'),
|
||||
'source': data.get('source', ''),
|
||||
'created_by_hal': is_hal,
|
||||
'scan_type': data.get('scan_type', ''),
|
||||
'scan_output': data.get('scan_output', ''),
|
||||
'analysis': data.get('analysis', ''),
|
||||
'risk_level': data.get('risk_level', ''),
|
||||
'fix_attempted': data.get('fix_attempted', False),
|
||||
'fix_results': data.get('fix_results', ''),
|
||||
'recommendations': data.get('recommendations', ''),
|
||||
'geo': data.get('geo', {}),
|
||||
'custom_fields': data.get('custom_fields', []),
|
||||
'notes': data.get('notes', ''),
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
}
|
||||
|
||||
irs = _load_irs()
|
||||
irs.insert(0, report)
|
||||
_save_irs(irs)
|
||||
return jsonify({'ok': True, 'ir': report})
|
||||
|
||||
|
||||
@targets_bp.route('/ir/<ir_id>', methods=['GET'])
|
||||
@login_required
|
||||
def ir_get(ir_id):
|
||||
"""Get a single IR."""
|
||||
irs = _load_irs()
|
||||
for ir in irs:
|
||||
if ir['id'] == ir_id:
|
||||
return jsonify({'ok': True, 'ir': ir})
|
||||
return jsonify({'ok': False, 'error': 'IR not found'})
|
||||
|
||||
|
||||
@targets_bp.route('/ir/<ir_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
def ir_update(ir_id):
|
||||
"""Update an existing IR."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
irs = _load_irs()
|
||||
for ir in irs:
|
||||
if ir['id'] == ir_id:
|
||||
for key in data:
|
||||
if key != 'id':
|
||||
ir[key] = data[key]
|
||||
ir['updated_at'] = datetime.now(timezone.utc).isoformat()
|
||||
_save_irs(irs)
|
||||
return jsonify({'ok': True, 'ir': ir})
|
||||
return jsonify({'ok': False, 'error': 'IR not found'})
|
||||
|
||||
|
||||
@targets_bp.route('/ir/<ir_id>/load-to-hal', methods=['POST'])
|
||||
@login_required
|
||||
def ir_load_to_hal(ir_id):
|
||||
"""Load an IR's details into HAL's memory so the agent can continue working on it."""
|
||||
irs = _load_irs()
|
||||
for ir in irs:
|
||||
if ir['id'] == ir_id:
|
||||
try:
|
||||
from core.hal_memory import get_hal_memory
|
||||
mem = get_hal_memory()
|
||||
mem.add('context', json.dumps(ir), metadata={'type': 'ir_loaded', 'ir_id': ir_id})
|
||||
mem.save()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({'ok': True, 'ir': ir})
|
||||
return jsonify({'ok': False, 'error': 'IR not found'})
|
||||
|
||||
|
||||
@targets_bp.route('/ir/<ir_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def ir_delete(ir_id):
|
||||
"""Delete an IR."""
|
||||
irs = _load_irs()
|
||||
irs = [ir for ir in irs if ir['id'] != ir_id]
|
||||
_save_irs(irs)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
@@ -657,47 +657,63 @@ textarea.form-control { font-family: monospace; resize: vertical; }
|
||||
}
|
||||
.hal-toggle-btn:hover { background: var(--accent-hover, #2563eb); }
|
||||
|
||||
@keyframes halPulse {
|
||||
0%, 100% { box-shadow: 0 0 4px rgba(0,255,65,0.3); }
|
||||
50% { box-shadow: 0 0 16px rgba(0,255,65,0.7), 0 0 32px rgba(0,255,65,0.3); }
|
||||
}
|
||||
|
||||
/* ── HAL Chat Panel ──────────────────────────────────────────── */
|
||||
|
||||
.hal-panel {
|
||||
position: fixed;
|
||||
bottom: 64px;
|
||||
right: 20px;
|
||||
z-index: 1100;
|
||||
width: 360px;
|
||||
height: 480px;
|
||||
background: var(--bg-card, #1a1a2e);
|
||||
border: 1px solid var(--border, #2a2a3e);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
width: 420px;
|
||||
height: 520px;
|
||||
min-width: 300px;
|
||||
min-height: 240px;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Header bar ── */
|
||||
|
||||
.hal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-nav, #12122a);
|
||||
border-bottom: 1px solid var(--border, #2a2a3e);
|
||||
background: #12122a;
|
||||
border-bottom: 1px solid #2a2a3e;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
color: var(--accent, #6366f1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary, #888);
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
.hal-close:hover { color: var(--text, #e0e0e0); background: var(--bg-hover, #2a2a3e); }
|
||||
.hal-close:hover { color: #e0e0e0; background: #2a2a3e; }
|
||||
|
||||
/* ── Mode toggle switch ── */
|
||||
|
||||
/* Hal mode toggle switch */
|
||||
.hal-mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -706,21 +722,22 @@ textarea.form-control { font-family: monospace; resize: vertical; }
|
||||
user-select: none;
|
||||
}
|
||||
.hal-mode-switch input { display: none; }
|
||||
|
||||
.hal-mode-slider {
|
||||
width: 28px;
|
||||
height: 14px;
|
||||
background: var(--bg-input, #2a2d3e);
|
||||
background: #2a2d3e;
|
||||
border-radius: 7px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
border: 1px solid var(--border, #333650);
|
||||
border: 1px solid #333650;
|
||||
}
|
||||
.hal-mode-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--text-secondary, #888);
|
||||
background: #888;
|
||||
border-radius: 50%;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
@@ -734,61 +751,164 @@ textarea.form-control { font-family: monospace; resize: vertical; }
|
||||
transform: translateX(14px);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.hal-mode-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
color: #888;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
/* ── Message area — flexbox column chat feed ── */
|
||||
|
||||
.hal-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px 10px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── Base message bubble ── */
|
||||
|
||||
.hal-msg {
|
||||
padding: 7px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 92%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 85%;
|
||||
width: fit-content;
|
||||
align-self: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── User bubble — right-aligned, blue ── */
|
||||
|
||||
.hal-msg-user {
|
||||
background: var(--accent);
|
||||
background: #1a56db;
|
||||
color: #fff;
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
/* ── HAL text response — left-aligned, dark ── */
|
||||
|
||||
.hal-msg-bot {
|
||||
background: var(--bg-hover, #2a2a3e);
|
||||
color: var(--text, #e0e0e0);
|
||||
background: #1e1e2e;
|
||||
color: #ddd;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
/* ── Command HAL is running — code-style accent ── */
|
||||
|
||||
.hal-msg-action {
|
||||
background: #0a1628;
|
||||
color: #7dd3fc;
|
||||
align-self: flex-start;
|
||||
max-width: 90%;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.82rem;
|
||||
border-left: 3px solid #38bdf8;
|
||||
border-radius: 6px;
|
||||
border-bottom-left-radius: 3px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* ── Command output — scrollable code block ── */
|
||||
|
||||
.hal-msg-result {
|
||||
background: #0f0f0f;
|
||||
color: #999;
|
||||
align-self: stretch;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.78rem;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ── HAL thinking — subtle italic ── */
|
||||
|
||||
.hal-msg-thought {
|
||||
background: transparent;
|
||||
color: #666;
|
||||
align-self: flex-start;
|
||||
max-width: 85%;
|
||||
font-style: italic;
|
||||
font-size: 0.8rem;
|
||||
padding: 4px 14px;
|
||||
}
|
||||
|
||||
/* ── Status line — centered, no bubble ── */
|
||||
|
||||
.hal-msg-status {
|
||||
background: transparent;
|
||||
color: #555;
|
||||
font-size: 0.78rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
/* ── Error — red accent ── */
|
||||
|
||||
.hal-msg-error {
|
||||
background: #1a0505;
|
||||
color: #f87171;
|
||||
align-self: flex-start;
|
||||
max-width: 90%;
|
||||
border-left: 3px solid #dc2626;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* ── Footer input bar ── */
|
||||
|
||||
.hal-footer {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--border, #2a2a3e);
|
||||
background: var(--bg-nav, #12122a);
|
||||
border-top: 1px solid #2a2a3e;
|
||||
background: #12122a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hal-footer input {
|
||||
flex: 1;
|
||||
font-size: 0.82rem;
|
||||
padding: 5px 9px;
|
||||
background: var(--bg-input, #0d0d1a);
|
||||
border: 1px solid var(--border, #2a2a3e);
|
||||
padding: 6px 10px;
|
||||
background: #0d0d1a;
|
||||
border: 1px solid #2a2a3e;
|
||||
border-radius: 6px;
|
||||
color: var(--text, #e0e0e0);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.hal-footer input:focus { outline: none; border-color: var(--accent, #6366f1); }
|
||||
|
||||
.hal-footer button {
|
||||
font-size: 0.78rem;
|
||||
padding: 5px 10px;
|
||||
background: #1e1e2e;
|
||||
color: #ccc;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.hal-footer button:hover {
|
||||
background: #2a2a3e;
|
||||
color: #fff;
|
||||
}
|
||||
.hal-footer input:focus { outline: none; border-color: var(--accent); }
|
||||
|
||||
/* ── Debug Console Window ─────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ function startOsintSearch() {
|
||||
if (osintResults.length > 0) {
|
||||
document.getElementById('result-actions').style.display = 'flex';
|
||||
}
|
||||
halAnalyze('OSINT', JSON.stringify(data, null, 2), 'osint results', 'default');
|
||||
} else if (data.type === 'error' || data.error) {
|
||||
progressText.textContent = 'Error: ' + (data.message || data.error);
|
||||
source.close();
|
||||
@@ -564,6 +565,7 @@ function runCounterScan() {
|
||||
// Summary
|
||||
var sumEl = document.getElementById('scan-summary');
|
||||
if (sumEl && data.summary) sumEl.textContent = data.summary;
|
||||
halAnalyze('Counter: Threat Scan', JSON.stringify(data, null, 2), 'threat detection', 'counter');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -576,6 +578,7 @@ function runCounterCheck(name) {
|
||||
return '[' + t.severity.toUpperCase() + '] ' + t.category + ': ' + t.message;
|
||||
});
|
||||
if (resultEl) resultEl.textContent = lines.join('\n') || data.message || 'No threats found';
|
||||
halAnalyze('Counter: ' + name, JSON.stringify(data, null, 2), 'threat detection', 'counter');
|
||||
}).catch(function() { if (resultEl) resultEl.textContent = 'Request failed'; });
|
||||
}
|
||||
|
||||
@@ -598,6 +601,7 @@ function loadLogins() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Counter: Login Analysis', JSON.stringify(data, null, 2), 'threat detection', 'counter');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -624,6 +628,7 @@ function analyzeFile() {
|
||||
lines.push(' SHA256: ' + (data.hashes.sha256 || ''));
|
||||
}
|
||||
renderOutput('file-output', lines.join('\n'));
|
||||
halAnalyze('Analyze: File Analysis', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -642,6 +647,7 @@ function extractStrings() {
|
||||
if (data.emails && data.emails.length) { lines.push('Emails (' + data.emails.length + '):'); data.emails.forEach(function(e){lines.push(' ' + e);}); lines.push(''); }
|
||||
if (data.paths && data.paths.length) { lines.push('Paths (' + data.paths.length + '):'); data.paths.forEach(function(p){lines.push(' ' + p);}); }
|
||||
renderOutput('strings-output', lines.join('\n') || 'No interesting strings found');
|
||||
halAnalyze('Analyze: String Extraction', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -653,6 +659,7 @@ function hashLookup() {
|
||||
var lines = ['Hash Type: ' + (data.hash_type || 'Unknown'), ''];
|
||||
(data.links || []).forEach(function(l) { lines.push(l.name + ': ' + l.url); });
|
||||
renderOutput('hash-output', lines.join('\n'));
|
||||
halAnalyze('Analyze: Hash Lookup', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -673,6 +680,7 @@ function analyzeLog() {
|
||||
lines.push('Errors: ' + (data.error_count || 0));
|
||||
if (data.time_range) { lines.push('Time Range: ' + data.time_range.first + ' - ' + data.time_range.last); }
|
||||
renderOutput('log-analyze-output', lines.join('\n'));
|
||||
halAnalyze('Analyze: Log Analysis', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -687,6 +695,7 @@ function hexDump() {
|
||||
setLoading(btn, false);
|
||||
if (data.error) { renderOutput('hex-output', 'Error: ' + data.error); return; }
|
||||
renderOutput('hex-output', data.hex || 'No data');
|
||||
halAnalyze('Analyze: Hex Dump', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -708,6 +717,7 @@ function compareFiles() {
|
||||
lines.push('SHA256: ' + (data.sha256_match ? 'MATCH' : 'DIFFERENT'));
|
||||
if (data.diff) { lines.push('\nDiff:\n' + data.diff); }
|
||||
renderOutput('compare-output', lines.join('\n'));
|
||||
halAnalyze('Analyze: File Compare', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -947,6 +957,7 @@ function wsStartCapture() {
|
||||
document.getElementById('ws-capture-status').innerHTML = '<span class="status-dot inactive"></span>Idle';
|
||||
document.getElementById('ws-analysis-section').style.display = 'block';
|
||||
wsShowPacketsTable();
|
||||
halAnalyze('Wireshark', JSON.stringify(d, null, 2), 'packet capture', 'network');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -993,6 +1004,7 @@ function wsAnalyzePcap() {
|
||||
|
||||
document.getElementById('ws-analysis-section').style.display = 'block';
|
||||
wsRenderPackets(data.packets || []);
|
||||
halAnalyze('Wireshark', JSON.stringify(data, null, 2), 'packet capture', 'network');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -1045,6 +1057,7 @@ function wsLoadProtocols() {
|
||||
+ '</div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Wireshark', JSON.stringify(data, null, 2), 'packet capture', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1064,6 +1077,7 @@ function wsLoadConversations() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Wireshark', JSON.stringify(data, null, 2), 'packet capture', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1083,6 +1097,7 @@ function wsLoadDNS() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Wireshark', JSON.stringify(data, null, 2), 'packet capture', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1104,6 +1119,7 @@ function wsLoadHTTP() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Wireshark', JSON.stringify(data, null, 2), 'packet capture', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1125,6 +1141,7 @@ function wsLoadCredentials() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Wireshark', JSON.stringify(data, null, 2), 'packet capture', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1448,6 +1465,28 @@ function hwArchonStop() {
|
||||
|
||||
// ── ADB (mode-aware) ──
|
||||
|
||||
function hwAdbKillServer() {
|
||||
var status = document.getElementById('hw-adb-server-status');
|
||||
if (status) status.textContent = 'Killing...';
|
||||
fetch('/hardware/adb/kill-server', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (status) status.textContent = d.ok ? 'ADB server killed' : 'Error: ' + (d.output || '');
|
||||
setTimeout(function() { if (status) status.textContent = ''; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
function hwAdbStartServer() {
|
||||
var status = document.getElementById('hw-adb-server-status');
|
||||
if (status) status.textContent = 'Starting...';
|
||||
fetch('/hardware/adb/start-server', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (status) status.textContent = d.ok ? 'ADB server started' : 'Error: ' + (d.output || '');
|
||||
setTimeout(function() { if (status) status.textContent = ''; hwRefreshAdbDevices(); }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function hwRefreshAdbDevices() {
|
||||
if (hwConnectionMode === 'direct') return; // Direct mode uses connect buttons
|
||||
var container = document.getElementById('hw-adb-devices');
|
||||
@@ -2128,6 +2167,9 @@ function halToggle() {
|
||||
}
|
||||
}
|
||||
|
||||
var _halReader = null; // Track active stream reader for stop button
|
||||
var _halAbortController = null;
|
||||
|
||||
function halSend() {
|
||||
var inp = document.getElementById('hal-input');
|
||||
if (!inp) return;
|
||||
@@ -2138,17 +2180,26 @@ function halSend() {
|
||||
halAppend('user', msg);
|
||||
var container = document.getElementById('hal-messages');
|
||||
|
||||
// Show stop button, hide send
|
||||
var sendBtn = document.getElementById('hal-send-btn');
|
||||
var stopBtn = document.getElementById('hal-stop-btn');
|
||||
if (sendBtn) sendBtn.style.display = 'none';
|
||||
if (stopBtn) stopBtn.style.display = '';
|
||||
|
||||
_halAbortController = new AbortController();
|
||||
|
||||
fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({message: msg, mode: halAgentMode ? 'agent' : 'chat'})
|
||||
body: JSON.stringify({message: msg, mode: halAgentMode ? 'agent' : 'chat'}),
|
||||
signal: _halAbortController.signal
|
||||
}).then(function(res) {
|
||||
var reader = res.body.getReader();
|
||||
_halReader = res.body.getReader();
|
||||
var dec = new TextDecoder();
|
||||
var buf = '';
|
||||
function pump() {
|
||||
reader.read().then(function(chunk) {
|
||||
if (chunk.done) { inp.disabled = false; inp.focus(); return; }
|
||||
_halReader.read().then(function(chunk) {
|
||||
if (chunk.done) { _halFinished(inp); return; }
|
||||
buf += dec.decode(chunk.value, {stream: true});
|
||||
var parts = buf.split('\n\n');
|
||||
buf = parts.pop();
|
||||
@@ -2170,7 +2221,6 @@ function halSend() {
|
||||
} else if (d.type === 'error') {
|
||||
halAppendStyled('error', d.content);
|
||||
} else if (d.token) {
|
||||
// Legacy streaming token mode
|
||||
var last = container.lastElementChild;
|
||||
if (!last || !last.classList.contains('hal-msg-bot')) {
|
||||
last = halAppend('bot', '');
|
||||
@@ -2178,40 +2228,51 @@ function halSend() {
|
||||
last.textContent += d.token;
|
||||
halScroll();
|
||||
} else if (d.done) {
|
||||
inp.disabled = false;
|
||||
inp.focus();
|
||||
_halFinished(inp);
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
pump();
|
||||
});
|
||||
}).catch(function() { _halFinished(inp); });
|
||||
}
|
||||
pump();
|
||||
}).catch(function(e) {
|
||||
halAppendStyled('error', e.message);
|
||||
inp.disabled = false;
|
||||
if (e.name !== 'AbortError') halAppendStyled('error', e.message);
|
||||
_halFinished(inp);
|
||||
});
|
||||
}
|
||||
|
||||
function _halFinished(inp) {
|
||||
if (inp) { inp.disabled = false; inp.focus(); }
|
||||
_halReader = null;
|
||||
_halAbortController = null;
|
||||
var sendBtn = document.getElementById('hal-send-btn');
|
||||
var stopBtn = document.getElementById('hal-stop-btn');
|
||||
if (sendBtn) sendBtn.style.display = '';
|
||||
if (stopBtn) stopBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
function halStop() {
|
||||
// Abort the fetch stream
|
||||
if (_halAbortController) {
|
||||
_halAbortController.abort();
|
||||
}
|
||||
// Cancel the reader
|
||||
if (_halReader) {
|
||||
try { _halReader.cancel(); } catch(e) {}
|
||||
}
|
||||
halAppendStyled('status', '-- Stopped by user --');
|
||||
_halFinished(document.getElementById('hal-input'));
|
||||
}
|
||||
|
||||
function halAppendStyled(type, text) {
|
||||
var msgs = document.getElementById('hal-messages');
|
||||
if (!msgs) return;
|
||||
var div = document.createElement('div');
|
||||
div.className = 'hal-msg hal-msg-' + type;
|
||||
if (type === 'thought') {
|
||||
div.style.cssText = 'font-style:italic;color:var(--text-muted,#888);font-size:0.8rem';
|
||||
div.textContent = text;
|
||||
} else if (type === 'action') {
|
||||
div.style.cssText = 'font-family:monospace;color:var(--accent,#0af);font-size:0.78rem;background:rgba(0,170,255,0.08);padding:4px 8px;border-radius:4px';
|
||||
if (type === 'action') {
|
||||
div.textContent = '> ' + text;
|
||||
} else if (type === 'result') {
|
||||
div.style.cssText = 'font-family:monospace;color:var(--text-secondary,#aaa);font-size:0.75rem;max-height:100px;overflow-y:auto;white-space:pre-wrap;background:rgba(255,255,255,0.03);padding:4px 8px;border-radius:4px';
|
||||
div.textContent = text;
|
||||
} else if (type === 'status') {
|
||||
div.style.cssText = 'color:var(--text-muted,#666);font-size:0.78rem;font-style:italic';
|
||||
div.textContent = text;
|
||||
} else if (type === 'error') {
|
||||
div.style.cssText = 'color:var(--danger,#f55);font-size:0.82rem';
|
||||
div.textContent = 'Error: ' + text;
|
||||
} else {
|
||||
div.textContent = text;
|
||||
@@ -2242,6 +2303,410 @@ function halClear() {
|
||||
fetch('/api/chat/reset', {method: 'POST'}).catch(function() {});
|
||||
}
|
||||
|
||||
// ── HAL Auto-Analyst ──────────────────────────────────────────────────────────
|
||||
// Call halAnalyze() after any tool displays results. HAL will analyze the output
|
||||
// via the loaded LLM and open the chat panel with findings.
|
||||
//
|
||||
// Usage: halAnalyze('Log Analyzer', logText, 'syslog', 'defense');
|
||||
// halAnalyze('ARP Table', arpOutput, '', 'network');
|
||||
|
||||
var _halAnalyzing = false;
|
||||
var _halFeedbackEnabled = true; // Auto-analysis on tool output
|
||||
|
||||
function halToggleFeedback() {
|
||||
_halFeedbackEnabled = !_halFeedbackEnabled;
|
||||
var btn = document.getElementById('hal-feedback-btn');
|
||||
if (btn) {
|
||||
btn.style.color = _halFeedbackEnabled ? 'var(--accent)' : 'var(--text-muted)';
|
||||
btn.style.borderColor = _halFeedbackEnabled ? 'var(--accent)' : 'var(--text-muted)';
|
||||
btn.title = _halFeedbackEnabled ? 'Auto-analysis ON — click to disable' : 'Auto-analysis OFF — click to enable';
|
||||
}
|
||||
}
|
||||
|
||||
function halAnalyze(toolName, output, context, category) {
|
||||
if (!_halFeedbackEnabled) return; // Feedback disabled by user
|
||||
if (!toolName || !output) return;
|
||||
if (_halAnalyzing) return; // Prevent concurrent analysis
|
||||
|
||||
// Auto-close previous IR when a new scan starts
|
||||
_halAutoCloseActiveIR();
|
||||
_halAnalyzing = true;
|
||||
|
||||
// Truncate very long output for the API call
|
||||
var maxLen = 12000;
|
||||
var sendOutput = output.length > maxLen ? output.substring(0, maxLen) + '\n...[truncated]' : output;
|
||||
|
||||
// Show notification on HAL button with rotating status messages
|
||||
var btn = document.getElementById('hal-toggle-btn');
|
||||
if (btn) {
|
||||
btn.style.animation = 'halPulse 1s ease-in-out infinite';
|
||||
var _halMsgs = [
|
||||
'Analyzing...', 'Processing data...', 'Scanning output...',
|
||||
'Evaluating risks...', 'Checking signatures...', 'Cross-referencing...',
|
||||
'Building assessment...', 'Identifying threats...', 'Mapping vectors...',
|
||||
'Correlating events...', 'Parsing results...', 'Running diagnostics...'
|
||||
];
|
||||
var _halMsgIdx = 0;
|
||||
btn.textContent = _halMsgs[0];
|
||||
btn.style.minWidth = '120px';
|
||||
window._halStatusInterval = setInterval(function() {
|
||||
_halMsgIdx = (_halMsgIdx + 1) % _halMsgs.length;
|
||||
if (btn) btn.textContent = _halMsgs[_halMsgIdx];
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
fetch('/api/hal/analyze', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
tool_name: toolName,
|
||||
output: sendOutput,
|
||||
context: context || '',
|
||||
category: category || 'default'
|
||||
})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
_halAnalyzing = false;
|
||||
if (btn) { btn.style.animation = ''; btn.textContent = 'HAL'; btn.style.minWidth = ''; } if (window._halStatusInterval) { clearInterval(window._halStatusInterval); window._halStatusInterval = null; }
|
||||
|
||||
if (!d.available) {
|
||||
// No LLM loaded — skip silently
|
||||
return;
|
||||
}
|
||||
|
||||
// Open HAL panel and show analysis
|
||||
var panel = document.getElementById('hal-panel');
|
||||
if (panel) panel.style.display = 'flex';
|
||||
|
||||
// Risk level badge
|
||||
var riskColors = {
|
||||
clean: 'var(--success,#34c759)',
|
||||
low: '#8ec07c',
|
||||
medium: '#f59e0b',
|
||||
high: '#ff6b35',
|
||||
critical: 'var(--danger,#ff3b30)',
|
||||
unknown: 'var(--text-muted)'
|
||||
};
|
||||
var riskColor = riskColors[d.risk_level] || riskColors.unknown;
|
||||
|
||||
halAppendStyled('status', '─── HAL Analysis: ' + toolName + ' ───');
|
||||
|
||||
// Risk badge
|
||||
var msgs = document.getElementById('hal-messages');
|
||||
if (msgs) {
|
||||
var badge = document.createElement('div');
|
||||
badge.style.cssText = 'display:inline-block;padding:2px 10px;border-radius:3px;font-size:0.75rem;font-weight:700;margin:4px 0;border:1px solid ' + riskColor + ';color:' + riskColor;
|
||||
badge.textContent = 'Risk: ' + (d.risk_level || 'unknown').toUpperCase();
|
||||
msgs.appendChild(badge);
|
||||
}
|
||||
|
||||
// Analysis text — render markdown-style
|
||||
var analysis = d.analysis || 'No analysis available.';
|
||||
var div = document.createElement('div');
|
||||
div.className = 'hal-msg hal-msg-bot';
|
||||
div.style.cssText = 'white-space:pre-wrap;font-size:0.82rem;line-height:1.55';
|
||||
div.textContent = analysis;
|
||||
if (msgs) msgs.appendChild(div);
|
||||
|
||||
// Store analysis for fix/IR extraction
|
||||
window._halLastAnalysis = analysis;
|
||||
window._halLastToolName = toolName;
|
||||
window._halLastRiskLevel = d.risk_level || 'unknown';
|
||||
|
||||
// Action buttons
|
||||
var btnDiv = document.createElement('div');
|
||||
btnDiv.style.cssText = 'margin:8px 0;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);font-size:0.78rem;display:flex;gap:6px;flex-wrap:wrap;align-items:center';
|
||||
|
||||
if (d.has_fixes) {
|
||||
btnDiv.innerHTML += '<button onclick="halAutoFix()" class="btn btn-sm" style="border-color:#f59e0b;color:#f59e0b">Let HAL Fix It</button>';
|
||||
btnDiv.innerHTML += '<button onclick="halFixAndIR()" class="btn btn-sm" style="border-color:var(--accent);color:var(--accent)">Let HAL Fix It + IR</button>';
|
||||
}
|
||||
btnDiv.innerHTML += '<button onclick="halCreateIR()" class="btn btn-sm" style="border-color:var(--text-secondary);color:var(--text-secondary)">Report Only</button>';
|
||||
if (msgs) msgs.appendChild(btnDiv);
|
||||
|
||||
halScroll();
|
||||
})
|
||||
.catch(function(e) {
|
||||
_halAnalyzing = false;
|
||||
if (btn) { btn.style.animation = ''; btn.textContent = 'HAL'; btn.style.minWidth = ''; } if (window._halStatusInterval) { clearInterval(window._halStatusInterval); window._halStatusInterval = null; }
|
||||
});
|
||||
}
|
||||
|
||||
function halCreateIR() {
|
||||
var analysis = window._halLastAnalysis || '';
|
||||
var toolName = window._halLastToolName || 'Unknown';
|
||||
var riskLevel = window._halLastRiskLevel || 'unknown';
|
||||
halAppendStyled('status', 'Creating Investigation Report...');
|
||||
// Truncate analysis to prevent oversized requests
|
||||
var trimmedAnalysis = analysis.length > 50000 ? analysis.substring(0, 50000) + '\n...[truncated]' : analysis;
|
||||
|
||||
fetch('/targets/ir/create', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
title: 'IR: ' + toolName,
|
||||
source: 'HAL Auto-Analyst',
|
||||
scan_type: toolName,
|
||||
analysis: trimmedAnalysis,
|
||||
risk_level: riskLevel,
|
||||
fix_attempted: false,
|
||||
})
|
||||
}).then(function(r) {
|
||||
if (!r.ok) { halAppendStyled('error', 'IR creation failed: HTTP ' + r.status); return Promise.reject(); }
|
||||
var ct = r.headers.get('content-type') || '';
|
||||
if (ct.indexOf('json') === -1) { halAppendStyled('error', 'IR endpoint returned non-JSON (check login session)'); return Promise.reject(); }
|
||||
return r.json();
|
||||
}).then(function(d) {
|
||||
if (!d) return;
|
||||
if (d.ok) {
|
||||
window._halActiveIR = d.ir.id;
|
||||
halAppendStyled('status', 'IR Created: ' + d.ir.id + ' — ' + d.ir.title);
|
||||
var link = document.createElement('div');
|
||||
link.style.cssText = 'font-size:0.78rem;margin:4px 0;padding:8px 10px;background:rgba(0,255,65,0.06);border:1px solid var(--accent);border-radius:4px';
|
||||
link.innerHTML = 'Report ID: <strong style="color:var(--accent)">' + escapeHtml(d.ir.id) + '</strong> — <a href="/targets" style="color:var(--accent)">View in Investigations</a>'
|
||||
+ '<div style="margin-top:6px;display:flex;gap:6px">'
|
||||
+ '<button class="btn btn-sm" style="border-color:var(--accent);color:var(--accent)" onclick="halUpdateIR()">Update IR</button>'
|
||||
+ '<button class="btn btn-sm" style="border-color:var(--text-muted);color:var(--text-muted)" onclick="halCloseIR()">Close IR</button>'
|
||||
+ '</div>';
|
||||
var msgs = document.getElementById('hal-messages');
|
||||
if (msgs) msgs.appendChild(link);
|
||||
halScroll();
|
||||
} else {
|
||||
halAppendStyled('error', 'Failed to create IR: ' + (d.error || 'Unknown'));
|
||||
}
|
||||
}).catch(function(e) { halAppendStyled('error', 'IR creation failed: ' + e.message); });
|
||||
}
|
||||
|
||||
function halFixAndIR() {
|
||||
// Run the fix first, then create an IR with the fix results
|
||||
halAppendStyled('status', 'Running fixes then creating IR...');
|
||||
var analysis = window._halLastAnalysis || '';
|
||||
var toolName = window._halLastToolName || 'Unknown';
|
||||
var riskLevel = window._halLastRiskLevel || 'unknown';
|
||||
|
||||
// Extract and run commands
|
||||
var commands = [];
|
||||
var lines = analysis.split('\n');
|
||||
var inBlock = false;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (line.startsWith('```')) { inBlock = !inBlock; continue; }
|
||||
if (inBlock && line && !line.startsWith('#')) { commands.push(line); }
|
||||
else if (!inBlock && (line.startsWith('apt') || line.startsWith('systemctl ') || line.startsWith('ufw ') || line.startsWith('iptables ') || line.startsWith('chmod ') || line.startsWith('chown ') || line.startsWith('sed ') || line.startsWith('adb ') || line.startsWith('fastboot ') || line.startsWith('pm ') || line.startsWith('am ') || line.startsWith('dumpsys ') || line.startsWith('settings put '))) { commands.push(line); }
|
||||
}
|
||||
commands = commands.map(function(c) { return c.replace(/^sudo\s+/, ''); });
|
||||
var seen = {};
|
||||
commands = commands.filter(function(c) { if (seen[c]) return false; seen[c] = true; return true; });
|
||||
|
||||
var fixResults = [];
|
||||
var idx = 0;
|
||||
function runNext() {
|
||||
if (idx >= commands.length) {
|
||||
// All done — create the IR
|
||||
var trimAnalysis = analysis.length > 50000 ? analysis.substring(0, 50000) + '\n...[truncated]' : analysis;
|
||||
fetch('/targets/ir/create', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
title: 'IR: ' + toolName + ' (auto-fixed)',
|
||||
source: 'HAL Auto-Analyst + Auto-Fix',
|
||||
scan_type: toolName,
|
||||
analysis: trimAnalysis,
|
||||
risk_level: riskLevel,
|
||||
fix_attempted: true,
|
||||
fix_results: fixResults.map(function(r) { return r.cmd + ': ' + (r.ok ? 'OK' : 'FAILED') + ' ' + r.output; }).join('\n'),
|
||||
})
|
||||
}).then(function(r) {
|
||||
if (!r.ok) { halAppendStyled('error', 'IR creation failed: HTTP ' + r.status); return null; }
|
||||
var ct = r.headers.get('content-type') || '';
|
||||
if (ct.indexOf('json') === -1) { halAppendStyled('error', 'IR endpoint returned non-JSON'); return null; }
|
||||
return r.json();
|
||||
}).then(function(d) {
|
||||
if (!d) return;
|
||||
if (d.ok) {
|
||||
window._halActiveIR = d.ir.id;
|
||||
halAppendStyled('status', 'IR Created: ' + d.ir.id);
|
||||
var link = document.createElement('div');
|
||||
link.style.cssText = 'font-size:0.78rem;margin:4px 0;padding:8px 10px;background:rgba(0,255,65,0.06);border:1px solid var(--accent);border-radius:4px';
|
||||
link.innerHTML = 'Report: <strong style="color:var(--accent)">' + escapeHtml(d.ir.id) + '</strong> — Fix attempted: ' + commands.length + ' commands — <a href="/targets" style="color:var(--accent)">View</a>'
|
||||
+ '<div style="margin-top:6px;display:flex;gap:6px">'
|
||||
+ '<button class="btn btn-sm" style="border-color:var(--accent);color:var(--accent)" onclick="halUpdateIR()">Update IR</button>'
|
||||
+ '<button class="btn btn-sm" style="border-color:var(--text-muted);color:var(--text-muted)" onclick="halCloseIR()">Close IR</button>'
|
||||
+ '</div>';
|
||||
var msgs = document.getElementById('hal-messages');
|
||||
if (msgs) msgs.appendChild(link);
|
||||
halScroll();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
var cmd = commands[idx++];
|
||||
halAppendStyled('action', cmd);
|
||||
fetch('/api/hal/fix', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({command: cmd})
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
fixResults.push({cmd: cmd, ok: d.ok, output: (d.output || d.error || '').trim()});
|
||||
if (d.ok) { halAppendStyled('result', d.output || '(success)'); }
|
||||
else { halAppendStyled('error', d.error || d.output || 'Failed'); }
|
||||
halScroll();
|
||||
runNext();
|
||||
}).catch(function(e) {
|
||||
fixResults.push({cmd: cmd, ok: false, output: e.message});
|
||||
halAppendStyled('error', e.message);
|
||||
runNext();
|
||||
});
|
||||
}
|
||||
if (commands.length > 0) {
|
||||
halAppendStyled('status', 'Executing ' + commands.length + ' fix command(s) + creating IR...');
|
||||
runNext();
|
||||
} else {
|
||||
halAppendStyled('status', 'No fix commands found — creating IR with analysis only...');
|
||||
halCreateIR();
|
||||
}
|
||||
}
|
||||
|
||||
// Active IR tracking
|
||||
window._halActiveIR = null;
|
||||
|
||||
function halUpdateIR() {
|
||||
var irId = window._halActiveIR;
|
||||
if (!irId) { halAppendStyled('error', 'No active IR to update.'); return; }
|
||||
var analysis = window._halLastAnalysis || '';
|
||||
halAppendStyled('status', 'Updating IR ' + irId + '...');
|
||||
fetch('/targets/ir/' + irId + '/update', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
analysis: analysis,
|
||||
risk_level: window._halLastRiskLevel || 'unknown',
|
||||
updated_by: 'HAL',
|
||||
})
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (d.ok) halAppendStyled('status', 'IR ' + irId + ' updated.');
|
||||
else halAppendStyled('error', 'Update failed: ' + (d.error || ''));
|
||||
}).catch(function(e) { halAppendStyled('error', e.message); });
|
||||
}
|
||||
|
||||
function halCloseIR() {
|
||||
var irId = window._halActiveIR;
|
||||
if (!irId) { halAppendStyled('error', 'No active IR.'); return; }
|
||||
halAppendStyled('status', 'Closing IR ' + irId + '...');
|
||||
fetch('/targets/ir/' + irId + '/update', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ status: 'closed' })
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (d.ok) {
|
||||
halAppendStyled('status', 'IR ' + irId + ' closed.');
|
||||
window._halActiveIR = null;
|
||||
} else {
|
||||
halAppendStyled('error', 'Close failed: ' + (d.error || ''));
|
||||
}
|
||||
}).catch(function(e) { halAppendStyled('error', e.message); });
|
||||
}
|
||||
|
||||
function _halAutoCloseActiveIR() {
|
||||
// Auto-close previous IR when a new scan starts
|
||||
if (window._halActiveIR) {
|
||||
fetch('/targets/ir/' + window._halActiveIR + '/update', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ status: 'closed', notes: 'Auto-closed: new scan detected' })
|
||||
}).catch(function() {});
|
||||
window._halActiveIR = null;
|
||||
}
|
||||
}
|
||||
|
||||
function halAutoFix() {
|
||||
var analysis = window._halLastAnalysis || '';
|
||||
if (!analysis) { halAppendStyled('error', 'No analysis to extract fixes from.'); return; }
|
||||
|
||||
// Extract commands from code blocks and inline commands
|
||||
var commands = [];
|
||||
var lines = analysis.split('\n');
|
||||
var inBlock = false;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (line.startsWith('```')) { inBlock = !inBlock; continue; }
|
||||
if (inBlock && line && !line.startsWith('#')) { commands.push(line); }
|
||||
else if (!inBlock && (line.startsWith('sudo ') || line.startsWith('apt') || line.startsWith('systemctl ') || line.startsWith('ufw ') || line.startsWith('iptables ') || line.startsWith('chmod ') || line.startsWith('chown ') || line.startsWith('adb ') || line.startsWith('fastboot ') || line.startsWith('pm ') || line.startsWith('am ') || line.startsWith('dumpsys ') || line.startsWith('settings put '))) { commands.push(line); }
|
||||
}
|
||||
|
||||
// Clean commands: strip sudo, split && chains, remove shell redirections
|
||||
var expanded = [];
|
||||
commands.forEach(function(cmd) {
|
||||
// Split && chains into individual commands
|
||||
cmd.split('&&').forEach(function(part) {
|
||||
part = part.trim()
|
||||
.replace(/^sudo\s+/, '')
|
||||
.replace(/\s*2>\/dev\/null\s*/g, ' ')
|
||||
.replace(/\s*>\/dev\/null\s*/g, ' ')
|
||||
.replace(/\s*2>&1\s*/g, ' ')
|
||||
.trim();
|
||||
if (part) expanded.push(part);
|
||||
});
|
||||
});
|
||||
commands = expanded;
|
||||
|
||||
// Deduplicate
|
||||
var seen = {};
|
||||
commands = commands.filter(function(cmd) {
|
||||
if (seen[cmd]) return false;
|
||||
seen[cmd] = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (commands.length === 0) {
|
||||
halAppendStyled('status', 'No executable commands found in the analysis.');
|
||||
return;
|
||||
}
|
||||
|
||||
halAppendStyled('status', 'Executing ' + commands.length + ' fix command(s) via daemon...');
|
||||
|
||||
// Execute commands sequentially
|
||||
var idx = 0;
|
||||
function runNext() {
|
||||
if (idx >= commands.length) {
|
||||
halAppendStyled('status', '─── Fix complete ───');
|
||||
return;
|
||||
}
|
||||
var cmd = commands[idx++];
|
||||
halAppendStyled('action', cmd);
|
||||
|
||||
fetch('/api/hal/fix', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({command: cmd})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok) {
|
||||
halAppendStyled('result', d.output || '(success)');
|
||||
} else {
|
||||
halAppendStyled('error', d.error || d.output || 'Command failed');
|
||||
}
|
||||
halScroll();
|
||||
runNext();
|
||||
})
|
||||
.catch(function(e) {
|
||||
halAppendStyled('error', 'Failed: ' + e.message);
|
||||
runNext();
|
||||
});
|
||||
}
|
||||
runNext();
|
||||
}
|
||||
|
||||
// Check if HAL is available on page load (show/hide notification capability)
|
||||
var _halAvailable = false;
|
||||
(function() {
|
||||
fetch('/api/hal/available').then(function(r) { return r.json(); }).then(function(d) {
|
||||
_halAvailable = d.available || false;
|
||||
}).catch(function() {});
|
||||
})();
|
||||
|
||||
// ── Debug Console ─────────────────────────────────────────────────────────────
|
||||
|
||||
var _dbgEs = null;
|
||||
|
||||
@@ -690,6 +690,9 @@ async function api(endpoint, body, panel) {
|
||||
});
|
||||
const data = await res.json();
|
||||
outJson(panel, data);
|
||||
if (typeof halAnalyze === 'function') {
|
||||
halAnalyze('Android Exploit: ' + endpoint, JSON.stringify(data, null, 2), 'android exploitation', 'android');
|
||||
}
|
||||
return data;
|
||||
} catch(e) {
|
||||
out(panel, 'Error: ' + escHtml(e.message));
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
<span style="font-weight:600;font-size:0.85rem;color:var(--text-secondary)">ADB Mode:</span>
|
||||
<button id="ap-mode-server" class="btn btn-sm active" onclick="apSetMode('server')">Server (Local ADB)</button>
|
||||
<button id="ap-mode-direct" class="btn btn-sm" onclick="apSetMode('direct')">Direct (WebUSB)</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="apAdbKillServer()" title="Kill ADB server process">Kill ADB</button>
|
||||
<button class="btn btn-sm" onclick="apAdbStartServer()" title="Restart ADB server">Start ADB</button>
|
||||
<span id="ap-adb-status" style="font-size:0.78rem;color:var(--text-muted)"></span>
|
||||
<span id="ap-direct-bar" style="display:none;align-items:center;gap:0.5rem">
|
||||
<span id="ap-device-label" style="font-size:0.85rem;color:var(--text-secondary)">Not connected</span>
|
||||
<button class="btn btn-sm" onclick="apDirectConnect()">Connect</button>
|
||||
@@ -387,6 +390,24 @@ let apLastScan = null;
|
||||
let apMode = 'server';
|
||||
|
||||
/* ── Mode Switching ── */
|
||||
function apAdbKillServer() {
|
||||
var s = document.getElementById('ap-adb-status');
|
||||
if (s) s.textContent = 'Killing...';
|
||||
fetch('/hardware/adb/kill-server', {method: 'POST'}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (s) s.textContent = d.ok ? 'Killed' : 'Error';
|
||||
setTimeout(function() { if (s) s.textContent = ''; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
function apAdbStartServer() {
|
||||
var s = document.getElementById('ap-adb-status');
|
||||
if (s) s.textContent = 'Starting...';
|
||||
fetch('/hardware/adb/start-server', {method: 'POST'}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (s) s.textContent = d.ok ? 'Started' : 'Error';
|
||||
setTimeout(function() { if (s) s.textContent = ''; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
function apSetMode(mode) {
|
||||
apMode = mode;
|
||||
localStorage.setItem('ap_connection_mode', mode);
|
||||
@@ -542,6 +563,7 @@ function apScan(type) {
|
||||
apSetStatus('Done');
|
||||
apLastScan = data;
|
||||
box.innerHTML = apRenderScan(type, data);
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
}).catch(e => {
|
||||
apSetStatus('Error');
|
||||
box.innerHTML = '<span style="color:#e74c3c">Error: ' + e.message + '</span>';
|
||||
@@ -733,6 +755,7 @@ function apExportReport() {
|
||||
} else {
|
||||
box.innerHTML = '<span style="color:#e74c3c">Error: ' + (data.error || 'Unknown') + '</span>';
|
||||
}
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -753,6 +776,7 @@ function apFindDangerous() {
|
||||
});
|
||||
} else h += '<p style="color:#27ae60">No apps with dangerous combos found.</p>';
|
||||
box.innerHTML = h;
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -779,6 +803,7 @@ function apAnalyzePerms() {
|
||||
(p.denied||[]).forEach(pm => h += '<li><code>' + pm + '</code></li>');
|
||||
h += '</ul>';
|
||||
box.innerHTML = h;
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -805,6 +830,7 @@ function apPermHeatmap() {
|
||||
h += '</tbody></table></div>';
|
||||
if (matrix.length > 50) h += '<p><small>Showing 50 of ' + matrix.length + ' apps</small></p>';
|
||||
box.innerHTML = h;
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -826,6 +852,7 @@ function apFix(action) {
|
||||
} else {
|
||||
box.innerHTML = '<span style="color:#e74c3c">Error: ' + (data.error || JSON.stringify(data)) + '</span>';
|
||||
}
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -839,6 +866,7 @@ function apFixProxy() {
|
||||
h += '<div>' + (r.ok ? '<span style="color:#27ae60">[OK]</span>' : '<span style="color:#e74c3c">[FAIL]</span>') + ' ' + r.setting + '</div>';
|
||||
});
|
||||
box.innerHTML = h;
|
||||
halAnalyze('Shield', JSON.stringify(data, null, 2), 'android protection', 'android');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ function autoRefreshStatus() {
|
||||
}
|
||||
}
|
||||
}
|
||||
halAnalyze('Autonomy', JSON.stringify(data, null, 2), 'threat monitoring', 'defense');
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -414,6 +415,7 @@ function autoLoadModelsDetail() {
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Autonomy', JSON.stringify(data, null, 2), 'threat monitoring', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -424,6 +426,7 @@ function autoLoadRules() {
|
||||
fetch('/autonomy/rules').then(r => r.json()).then(data => {
|
||||
_rules = data.rules || [];
|
||||
renderRules();
|
||||
halAnalyze('Autonomy', JSON.stringify(data, null, 2), 'threat monitoring', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,6 +687,7 @@ 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';
|
||||
halAnalyze('Autonomy', JSON.stringify(data, null, 2), 'threat monitoring', 'defense');
|
||||
});
|
||||
// Start SSE
|
||||
if (!_activitySSE) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ url_for('dashboard.index') }}" class="{% if request.endpoint == 'dashboard.index' %}active{% endif %}">Dashboard</a></li>
|
||||
<li><a href="{{ url_for('port_scanner.index') }}" class="{% if request.blueprint == 'port_scanner' %}active{% endif %}">🔍 Port Scanner</a></li>
|
||||
<li><a href="{{ url_for('targets.index') }}" class="{% if request.blueprint == 'targets' %}active{% endif %}">Targets</a></li>
|
||||
<li><a href="{{ url_for('targets.index') }}" class="{% if request.blueprint == 'targets' %}active{% endif %}">Investigations</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
@@ -45,7 +45,6 @@
|
||||
<li><a href="{{ url_for('log_correlator.index') }}" class="{% if request.blueprint == 'log_correlator' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Log Correlator</a></li>
|
||||
<li><a href="{{ url_for('container_sec.index') }}" class="{% if request.blueprint == 'container_sec' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Container Sec</a></li>
|
||||
<li><a href="{{ url_for('email_sec.index') }}" class="{% if request.blueprint == 'email_sec' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Email Sec</a></li>
|
||||
<li><a href="{{ url_for('incident_resp.index') }}" class="{% if request.blueprint == 'incident_resp' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Incident Response</a></li>
|
||||
<li><a href="{{ url_for('offense.index') }}" class="{% if request.blueprint == 'offense' %}active{% endif %}">Offense</a></li>
|
||||
<li><a href="{{ url_for('loadtest.index') }}" class="{% if request.blueprint == 'loadtest' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Load Test</a></li>
|
||||
<li><a href="{{ url_for('phishmail.index') }}" class="{% if request.blueprint == 'phishmail' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Gone Fishing</a></li>
|
||||
@@ -53,15 +52,11 @@
|
||||
<li><a href="{{ url_for('hack_hijack.index') }}" class="{% if request.blueprint == 'hack_hijack' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Hack Hijack</a></li>
|
||||
<li><a href="{{ url_for('webapp_scanner.index') }}" class="{% if request.blueprint == 'webapp_scanner' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Web Scanner</a></li>
|
||||
<li><a href="{{ url_for('c2_framework.index') }}" class="{% if request.blueprint == 'c2_framework' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ C2 Framework</a></li>
|
||||
<li><a href="{{ url_for('wifi_audit.index') }}" class="{% if request.blueprint == 'wifi_audit' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ WiFi Audit</a></li>
|
||||
<li><a href="{{ url_for('deauth.index') }}" class="{% if request.blueprint == 'deauth' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Deauth Attack</a></li>
|
||||
<li><a href="{{ url_for('api_fuzzer.index') }}" class="{% if request.blueprint == 'api_fuzzer' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ API Fuzzer</a></li>
|
||||
<li><a href="{{ url_for('cloud_scan.index') }}" class="{% if request.blueprint == 'cloud_scan' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Cloud Scan</a></li>
|
||||
<li><a href="{{ url_for('vuln_scanner.index') }}" class="{% if request.blueprint == 'vuln_scanner' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Vuln Scanner</a></li>
|
||||
<li><a href="{{ url_for('exploit_dev.index') }}" class="{% if request.blueprint == 'exploit_dev' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Exploit Dev</a></li>
|
||||
<li><a href="{{ url_for('ad_audit.index') }}" class="{% if request.blueprint == 'ad_audit' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ AD Audit</a></li>
|
||||
<li><a href="{{ url_for('mitm_proxy.index') }}" class="{% if request.blueprint == 'mitm_proxy' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ MITM Proxy</a></li>
|
||||
<li><a href="{{ url_for('pineapple.index') }}" class="{% if request.blueprint == 'pineapple' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Pineapple</a></li>
|
||||
<li><a href="{{ url_for('counter.index') }}" class="{% if request.blueprint == 'counter' %}active{% endif %}">Counter</a></li>
|
||||
<li><a href="{{ url_for('steganography.index') }}" class="{% if request.blueprint == 'steganography' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Steganography</a></li>
|
||||
<li><a href="{{ url_for('anti_forensics.index') }}" class="{% if request.blueprint == 'anti_forensics' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Anti-Forensics</a></li>
|
||||
@@ -69,7 +64,6 @@
|
||||
<li><a href="{{ url_for('analyze.hash_detection') }}" class="{% if request.endpoint == 'analyze.hash_detection' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Hash Toolkit</a></li>
|
||||
<li><a href="{{ url_for('llm_trainer.index') }}" class="{% if request.blueprint == 'llm_trainer' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ LLM Trainer</a></li>
|
||||
<li><a href="{{ url_for('password_toolkit.index') }}" class="{% if request.blueprint == 'password_toolkit' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Password Toolkit</a></li>
|
||||
<li><a href="{{ url_for('net_mapper.index') }}" class="{% if request.blueprint == 'net_mapper' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Net Mapper</a></li>
|
||||
<li><a href="{{ url_for('report_engine.index') }}" class="{% if request.blueprint == 'report_engine' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Reports</a></li>
|
||||
<li><a href="{{ url_for('ble_scanner.index') }}" class="{% if request.blueprint == 'ble_scanner' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ BLE Scanner</a></li>
|
||||
<li><a href="{{ url_for('forensics.index') }}" class="{% if request.blueprint == 'forensics' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Forensics</a></li>
|
||||
@@ -82,11 +76,24 @@
|
||||
<li><a href="{{ url_for('simulate.legendary_creator') }}" class="{% if request.endpoint == 'simulate.legendary_creator' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Legendary Creator</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Network</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ url_for('network.index') }}" class="{% if request.blueprint == 'network' %}active{% endif %}">🛡 Network Security</a></li>
|
||||
<li><a href="{{ url_for('wireshark.index') }}" class="{% if request.blueprint == 'wireshark' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Wireshark</a></li>
|
||||
<li><a href="{{ url_for('net_mapper.index') }}" class="{% if request.blueprint == 'net_mapper' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Net Mapper</a></li>
|
||||
<li><a href="{{ url_for('wifi_audit.index') }}" class="{% if request.blueprint == 'wifi_audit' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ WiFi Audit</a></li>
|
||||
<li><a href="{{ url_for('deauth.index') }}" class="{% if request.blueprint == 'deauth' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem;color:var(--danger,#f55)">└ Deauth</a></li>
|
||||
<li><a href="{{ url_for('pineapple.index') }}" class="{% if request.blueprint == 'pineapple' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem;color:var(--danger,#f55)">└ Evil Twin / Pineapple</a></li>
|
||||
<li><a href="{{ url_for('mitm_proxy.index') }}" class="{% if request.blueprint == 'mitm_proxy' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem;color:var(--danger,#f55)">└ MITM Proxy</a></li>
|
||||
<li><a href="{{ url_for('remote_monitor.index') }}" class="{% if request.blueprint == 'remote_monitor' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Remote Monitor</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Tools</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ url_for('module_creator.index') }}" class="{% if request.blueprint == 'module_creator' %}active{% endif %}" style="color:var(--accent)">➕ Create Module</a></li>
|
||||
<li><a href="{{ url_for('encmodules.index') }}" class="{% if request.blueprint == 'encmodules' %}active{% endif %}" style="color:var(--danger,#f55)">🔒 Enc Modules</a></li>
|
||||
<li><a href="{{ url_for('wireshark.index') }}" class="{% if request.blueprint == 'wireshark' %}active{% endif %}">Wireshark</a></li>
|
||||
<li><a href="{{ url_for('hardware.index') }}" class="{% if request.blueprint == 'hardware' %}active{% endif %}">Hardware</a></li>
|
||||
<li><a href="{{ url_for('android_exploit.index') }}" class="{% if request.blueprint == 'android_exploit' %}active{% endif %}">Android Exploit</a></li>
|
||||
<li><a href="{{ url_for('sms_forge.index') }}" class="{% if request.blueprint == 'sms_forge' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ SMS Forge</a></li>
|
||||
@@ -102,6 +109,7 @@
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">System</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ url_for('ssh_manager.index') }}" class="{% if request.blueprint == 'ssh_manager' %}active{% endif %}">SSH / SSHD</a></li>
|
||||
<li><a href="{{ url_for('upnp.index') }}" class="{% if request.blueprint == 'upnp' %}active{% endif %}">UPnP</a></li>
|
||||
<li><a href="{{ url_for('wireguard.index') }}" class="{% if request.blueprint == 'wireguard' %}active{% endif %}">WireGuard</a></li>
|
||||
<li><a href="{{ url_for('msf.index') }}" class="{% if request.blueprint == 'msf' %}active{% endif %}">MSF Console</a></li>
|
||||
@@ -109,6 +117,7 @@
|
||||
<li><a href="{{ url_for('dns_service.nameserver') }}" class="{% if request.endpoint == 'dns_service.nameserver' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Nameserver</a></li>
|
||||
<li><a href="{{ url_for('settings.index') }}" class="{% if request.blueprint == 'settings' and request.endpoint not in ('settings.llm_settings', 'settings.deps_index') %}active{% endif %}">Settings</a></li>
|
||||
<li><a href="{{ url_for('settings.llm_settings') }}" class="{% if request.endpoint == 'settings.llm_settings' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ LLM Config</a></li>
|
||||
<li><a href="{{ url_for('settings.mcp_settings') }}" class="{% if request.endpoint == 'settings.mcp_settings' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ MCP Server</a></li>
|
||||
<li><a href="{{ url_for('settings.deps_index') }}" class="{% if request.endpoint == 'settings.deps_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Dependencies</a></li>
|
||||
<li><a href="{{ url_for('dashboard.manual') }}" class="{% if request.endpoint == 'dashboard.manual' %}active{% endif %}">User Manual</a></li>
|
||||
<li><a href="{{ url_for('dashboard.manual_windows') }}" class="{% if request.endpoint == 'dashboard.manual_windows' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">└ Windows Guide</a></li>
|
||||
@@ -175,12 +184,16 @@
|
||||
<span class="hal-mode-slider"></span>
|
||||
<span class="hal-mode-label" id="hal-mode-label">Chat</span>
|
||||
</label>
|
||||
<button onclick="halToggleFeedback()" id="hal-feedback-btn" class="hal-feedback-btn"
|
||||
title="Toggle auto-analysis feedback on tool results"
|
||||
style="font-size:0.6rem;padding:1px 5px;border-radius:3px;border:1px solid var(--accent);color:var(--accent);background:transparent;cursor:pointer">FB</button>
|
||||
<button onclick="halToggle()" class="hal-close" title="Close">✕</button>
|
||||
</div>
|
||||
<div id="hal-messages" class="hal-messages"></div>
|
||||
<div class="hal-footer">
|
||||
<input id="hal-input" type="text" placeholder="Ask Hal..." onkeypress="if(event.key==='Enter')halSend()">
|
||||
<button onclick="halSend()" class="btn btn-sm btn-primary">Send</button>
|
||||
<button onclick="halSend()" class="btn btn-sm btn-primary" id="hal-send-btn">Send</button>
|
||||
<button onclick="halStop()" class="btn btn-sm" id="hal-stop-btn" title="Stop generation" style="display:none;background:var(--danger,#ff3b30);color:#fff">Stop</button>
|
||||
<button onclick="halClear()" class="btn btn-sm" title="Clear history">↺</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,6 +202,7 @@ function bleScan() {
|
||||
document.getElementById('ble-scan-status').textContent = 'Found ' + bleDevices.length + ' device(s)';
|
||||
bleRenderDevices();
|
||||
bleUpdateDeviceSelector();
|
||||
halAnalyze('BLE Scanner', JSON.stringify(data, null, 2), 'bluetooth', 'network');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -263,6 +264,7 @@ function bleVulnScan() {
|
||||
});
|
||||
bleRenderDevices();
|
||||
}
|
||||
halAnalyze('BLE Scanner', JSON.stringify(data, null, 2), 'bluetooth', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,6 +334,7 @@ function bleConnect() {
|
||||
document.getElementById('ble-connect-status').textContent = 'Connected to ' + addr;
|
||||
document.getElementById('btn-ble-disconnect').style.display = '';
|
||||
bleRenderServices(data.services || []);
|
||||
halAnalyze('BLE Scanner', JSON.stringify(data, null, 2), 'bluetooth', 'network');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
|
||||
@@ -323,6 +323,7 @@ function csDockerAudit() {
|
||||
+ escapeHtml(f.detail || '') + '</td></tr>';
|
||||
});
|
||||
document.getElementById('cs-docker-audit-results').innerHTML = html || '<tr><td colspan="4">No findings.</td></tr>';
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -342,6 +343,7 @@ function csListContainers() {
|
||||
+ '</td></tr>';
|
||||
});
|
||||
document.getElementById('cs-containers-list').innerHTML = html || '<tr><td colspan="5" class="empty-state">No containers found.</td></tr>';
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -369,6 +371,7 @@ function csAuditContainer(id, name) {
|
||||
});
|
||||
document.getElementById('cs-container-audit-results').innerHTML = html || '<tr><td colspan="3">No findings.</td></tr>';
|
||||
document.getElementById('cs-container-audit-section').scrollIntoView({behavior: 'smooth'});
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,6 +404,7 @@ function csEscapeCheck(id, name) {
|
||||
}
|
||||
document.getElementById('cs-escape-results').innerHTML = html;
|
||||
document.getElementById('cs-escape-section').scrollIntoView({behavior: 'smooth'});
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,6 +452,7 @@ function csK8sPods() {
|
||||
+ '</tr>';
|
||||
});
|
||||
document.getElementById('cs-k8s-pods-list').innerHTML = html || '<tr><td colspan="6" class="empty-state">No pods found in this namespace.</td></tr>';
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -476,6 +481,7 @@ function csK8sPodAudit(podName) {
|
||||
});
|
||||
document.getElementById('cs-k8s-pod-audit-results').innerHTML = html || '<tr><td colspan="3">No findings.</td></tr>';
|
||||
sec.scrollIntoView({behavior: 'smooth'});
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -501,6 +507,7 @@ function csK8sRBAC() {
|
||||
});
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -526,6 +533,7 @@ function csK8sSecrets() {
|
||||
});
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -552,6 +560,7 @@ function csK8sNetPolicies() {
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -596,6 +605,7 @@ function csScanImage() {
|
||||
+ '</tr>';
|
||||
});
|
||||
document.getElementById('cs-scan-results').innerHTML = html;
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -616,6 +626,7 @@ function csListImages() {
|
||||
+ '</tr>';
|
||||
});
|
||||
document.getElementById('cs-images-list').innerHTML = html || '<tr><td colspan="5" class="empty-state">No local images found.</td></tr>';
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -656,6 +667,7 @@ function csLintDockerfile() {
|
||||
+ '</tr>';
|
||||
});
|
||||
document.getElementById('cs-lint-results').innerHTML = html;
|
||||
halAnalyze('Container Security', JSON.stringify(data, null, 2), 'docker/k8s', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ function linuxRunAudit() {
|
||||
+ (c.passed ? 'PASS' : 'FAIL') + '</span></td><td>' + escapeHtml(c.details || '') + '</td></tr>';
|
||||
});
|
||||
document.getElementById('audit-results').innerHTML = html || '<tr><td colspan="3">No results</td></tr>';
|
||||
halAnalyze('Defense: Security Audit', JSON.stringify(data, null, 2), 'linux security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -110,12 +111,14 @@ function linuxRunCheck(name) {
|
||||
return (c.passed ? '[PASS] ' : '[FAIL] ') + c.name + (c.details ? ' — ' + c.details : '');
|
||||
});
|
||||
if (el) el.textContent = lines.join('\n') || 'No results';
|
||||
halAnalyze('Defense: ' + name + ' (single check only, do NOT run a full audit)', JSON.stringify(data, null, 2), 'single check: ' + name, 'defense');
|
||||
}).catch(function() { if (el) el.textContent = 'Request failed'; });
|
||||
}
|
||||
|
||||
function linuxLoadFwRules() {
|
||||
fetchJSON('/defense/linux/firewall/rules').then(function(data) {
|
||||
renderOutput('fw-rules', data.rules || 'Could not load rules');
|
||||
halAnalyze('Defense: Firewall Rules', JSON.stringify(data, null, 2), 'linux security audit', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,6 +128,7 @@ function linuxBlockIP() {
|
||||
postJSON('/defense/linux/firewall/block', {ip: ip}).then(function(data) {
|
||||
renderOutput('fw-result', data.message || data.error);
|
||||
if (data.success) { document.getElementById('block-ip').value = ''; linuxLoadFwRules(); }
|
||||
halAnalyze('Defense: Block IP', JSON.stringify(data, null, 2), 'linux security audit', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,6 +138,7 @@ function linuxUnblockIP() {
|
||||
postJSON('/defense/linux/firewall/unblock', {ip: ip}).then(function(data) {
|
||||
renderOutput('fw-result', data.message || data.error);
|
||||
if (data.success) linuxLoadFwRules();
|
||||
halAnalyze('Defense: Unblock IP', JSON.stringify(data, null, 2), 'linux security audit', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,6 +162,7 @@ function linuxAnalyzeLogs() {
|
||||
});
|
||||
}
|
||||
renderOutput('log-output', lines.join('\n') || 'No findings.');
|
||||
halAnalyze('Defense: Log Analysis', JSON.stringify(data, null, 2), 'linux security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -776,6 +776,7 @@ function loadConnections() {
|
||||
+ '</td></tr>';
|
||||
});
|
||||
document.getElementById('conn-table').innerHTML = html || '<tr><td colspan="6">No connections found</td></tr>';
|
||||
halAnalyze('Defense Monitor: Connections', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -801,6 +802,7 @@ function niBandwidth() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
document.getElementById('ni-bandwidth').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Bandwidth', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -823,6 +825,7 @@ function niArpCheck() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
document.getElementById('ni-arp').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: ARP Check', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -845,6 +848,7 @@ function niNewPorts() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
document.getElementById('ni-ports').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: New Ports', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -868,6 +872,7 @@ function niGeoip() {
|
||||
+ '<tr><td>ASN</td><td>' + (data.asn || '—') + '</td></tr>'
|
||||
+ '</tbody></table>';
|
||||
document.getElementById('ni-geoip-result').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: GeoIP Lookup', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -896,6 +901,7 @@ function niConnsGeo() {
|
||||
+ '<td>' + escapeHtml(c.process || '') + '</td></tr>';
|
||||
});
|
||||
document.getElementById('ni-geo-tbody').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Connections GeoIP', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -911,6 +917,7 @@ function niConnRate() {
|
||||
+ '<tr><td>Peak</td><td>' + (data.peak_rate || 0) + '</td></tr>'
|
||||
+ '</tbody></table>';
|
||||
document.getElementById('ni-conn-rate').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Connection Rate', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -952,6 +959,7 @@ function loadThreatReport() {
|
||||
data.blocklist.forEach(function(ip) { lines.push(' ' + ip); });
|
||||
}
|
||||
renderOutput('threat-output', lines.join('\n'));
|
||||
halAnalyze('Defense Monitor: Threat Report', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -1025,6 +1033,11 @@ function capDone(stats) {
|
||||
document.getElementById('btn-cap-start').style.display = '';
|
||||
document.getElementById('btn-cap-stop').style.display = 'none';
|
||||
document.getElementById('cap-status').textContent = 'Capture complete' + (stats.packet_count ? ' — ' + stats.packet_count + ' packets' : '');
|
||||
// Send captured packet output to HAL for analysis
|
||||
var packetPanel = document.getElementById('cap-live-packets');
|
||||
if (packetPanel && packetPanel.textContent.trim()) {
|
||||
halAnalyze('Threat Monitor: Packet Capture', packetPanel.textContent, 'packet capture analysis', 'network');
|
||||
}
|
||||
}
|
||||
|
||||
function capProtocols() {
|
||||
@@ -1043,6 +1056,7 @@ function capProtocols() {
|
||||
html += '<p class="empty-state">No packet data. Run a capture first.</p>';
|
||||
}
|
||||
document.getElementById('cap-analysis').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Protocol Distribution', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1061,6 +1075,7 @@ function capConversations() {
|
||||
html += '<p class="empty-state">No conversation data. Run a capture first.</p>';
|
||||
}
|
||||
document.getElementById('cap-analysis').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Top Conversations', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1087,6 +1102,7 @@ function ddosDetect() {
|
||||
html += '<pre class="output-panel" style="margin-top:8px;font-size:0.78rem">' + data.indicators.map(escapeHtml).join('\n') + '</pre>';
|
||||
}
|
||||
document.getElementById('ddos-status').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: DDoS Detection', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -1116,6 +1132,7 @@ function ddosTopTalkers() {
|
||||
+ '</td></tr>';
|
||||
});
|
||||
document.getElementById('ddos-talkers-tbody').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Top Talkers', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -1213,6 +1230,7 @@ function ddosHistory() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
document.getElementById('ddos-history').innerHTML = html;
|
||||
halAnalyze('Defense Monitor: Mitigation History', JSON.stringify(data, null, 2), 'security audit', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,31 @@
|
||||
<a href="{{ url_for('defense.index') }}" class="btn btn-sm" style="margin-left:auto">← Defense</a>
|
||||
</div>
|
||||
|
||||
<!-- OS Mismatch Warning -->
|
||||
<div id="os-mismatch-banner" style="display:none;padding:0.75rem 1rem;border:2px solid var(--danger,#ff3b30);border-radius:var(--radius);background:rgba(255,59,48,0.08);margin-bottom:1rem">
|
||||
<strong style="color:var(--danger,#ff3b30)">Wrong platform detected!</strong>
|
||||
<span style="font-size:0.85rem;color:var(--text-secondary)">
|
||||
This system is running <strong id="os-detected">Linux</strong>, not Windows.
|
||||
These scans will not work correctly.
|
||||
<a href="{{ url_for('defense.linux_index') }}" style="color:var(--accent);font-weight:bold">Switch to Linux Defense</a>
|
||||
</span>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
fetch('/defense/', {headers: {'Accept': 'application/json'}})
|
||||
.then(function(r) { return r.json(); })
|
||||
.catch(function() { return null; })
|
||||
.then(function(d) {
|
||||
if (d && d.platform && d.platform !== 'Windows') {
|
||||
var banner = document.getElementById('os-mismatch-banner');
|
||||
var det = document.getElementById('os-detected');
|
||||
if (banner) banner.style.display = '';
|
||||
if (det) det.textContent = d.platform + ' (' + (d.os_version || '') + ')';
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<div class="tab-bar">
|
||||
<button class="tab active" data-tab-group="windef" data-tab="audit" onclick="showTab('windef','audit')">Security Audit</button>
|
||||
@@ -159,6 +184,7 @@ function winRunAudit() {
|
||||
+ (c.passed ? 'PASS' : 'FAIL') + '</span></td><td>' + escapeHtml(c.details || '') + '</td></tr>';
|
||||
});
|
||||
document.getElementById('win-audit-results').innerHTML = html || '<tr><td colspan="3">No results</td></tr>';
|
||||
halAnalyze('Windows Defense', JSON.stringify(data, null, 2), 'windows security', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -171,6 +197,7 @@ function winRunCheck(name) {
|
||||
return (c.passed ? '[PASS] ' : '[FAIL] ') + c.name + (c.details ? ' — ' + c.details : '');
|
||||
});
|
||||
if (el) el.textContent = lines.join('\n') || 'No results';
|
||||
halAnalyze('Windows Defense', JSON.stringify(data, null, 2), 'windows security', 'defense');
|
||||
}).catch(function() { if (el) el.textContent = 'Request failed'; });
|
||||
}
|
||||
|
||||
@@ -218,6 +245,7 @@ function winAnalyzeLogs() {
|
||||
});
|
||||
}
|
||||
renderOutput('win-log-output', lines.join('\n') || 'No findings.');
|
||||
halAnalyze('Windows Defense', JSON.stringify(data, null, 2), 'windows security', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); renderOutput('win-log-output', 'Request failed'); });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -735,6 +735,7 @@ function checkDNS() {
|
||||
el.innerHTML = 'Status: <span style="color:var(--text-muted);font-weight:600">STOPPED</span>';
|
||||
document.getElementById('dns-metrics').textContent = '';
|
||||
}
|
||||
halAnalyze('DNS Service', JSON.stringify(d, null, 2), 'dns query', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -780,6 +781,7 @@ function loadZones() {
|
||||
s.innerHTML = opts;
|
||||
if (_currentZone) s.value = _currentZone;
|
||||
});
|
||||
halAnalyze('DNS Service', JSON.stringify(d, null, 2), 'dns query', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -834,6 +836,7 @@ function loadRecords() {
|
||||
fetch(`/dns/zones/${zone}/records`).then(r => r.json()).then(d => {
|
||||
_allRecords = (d.ok && d.records) ? d.records : [];
|
||||
filterRecords();
|
||||
halAnalyze('DNS Service', JSON.stringify(d, null, 2), 'dns query', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -945,6 +948,7 @@ function exportZone() {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
document.getElementById('exp-output').style.display = '';
|
||||
document.getElementById('exp-text').value = d.zone_file || d.data || JSON.stringify(d, null, 2);
|
||||
halAnalyze('DNS Service', JSON.stringify(d, null, 2), 'dns query', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1140,6 +1144,7 @@ function loadConfig() {
|
||||
// Hosts
|
||||
document.getElementById('cfg-hosts-file').value = c.hosts_file || '';
|
||||
document.getElementById('cfg-hosts-autoload').checked = c.hosts_auto_load === true;
|
||||
halAnalyze('DNS Service', JSON.stringify(d, null, 2), 'dns query', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1208,6 +1213,7 @@ function ezScanNetwork() {
|
||||
ezRenderHosts();
|
||||
|
||||
document.getElementById('ez-scan-status').innerHTML = `<span style="color:#4ade80">Found ${_ezHosts.length} hosts on ${d.subnet || 'network'}</span>`;
|
||||
halAnalyze('DNS Service', JSON.stringify(d, null, 2), 'dns query', 'network');
|
||||
}).catch(e => {
|
||||
document.getElementById('ez-scan-status').innerHTML = `<span style="color:var(--danger)">Scan failed: ${e.message}</span>`;
|
||||
});
|
||||
|
||||
@@ -470,6 +470,7 @@ function esDomainAnalyze(){
|
||||
}
|
||||
renderFindings('es-mx-findings',d.mx.findings);
|
||||
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){setLoading(btn,false);alert('Error: '+e);});
|
||||
}
|
||||
|
||||
@@ -499,6 +500,7 @@ function esBlCheck(){
|
||||
rows+='<tr><td>'+esc(r.blacklist)+'</td><td>'+st+'</td><td style="font-size:0.82rem">'+esc(r.details)+'</td></tr>';
|
||||
});
|
||||
document.getElementById('es-bl-rows').innerHTML=rows;
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){setLoading(btn,false);alert('Error: '+e);});
|
||||
}
|
||||
|
||||
@@ -579,6 +581,7 @@ function esAnalyzeHeaders(){
|
||||
} else {
|
||||
findSect.style.display='none';
|
||||
}
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){setLoading(btn,false);alert('Error: '+e);});
|
||||
}
|
||||
|
||||
@@ -628,6 +631,7 @@ function esDetectPhishing(){
|
||||
});
|
||||
}
|
||||
document.getElementById('es-phish-urls').innerHTML=uHtml;
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){setLoading(btn,false);alert('Error: '+e);});
|
||||
}
|
||||
|
||||
@@ -651,6 +655,7 @@ function esAbuseReport(){
|
||||
if(d.error){alert(d.error);return;}
|
||||
document.getElementById('es-abuse-output').style.display='';
|
||||
document.getElementById('es-abuse-text').textContent=d.report_text;
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){setLoading(btn,false);alert('Error: '+e);});
|
||||
}
|
||||
function esAbuseCopy(){
|
||||
@@ -708,6 +713,7 @@ function esMbSearch(){
|
||||
rows='<tr><td colspan="5" class="empty-state">No messages found.</td></tr>';
|
||||
}
|
||||
document.getElementById('es-mb-rows').innerHTML=rows;
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){setLoading(btn,false);document.getElementById('es-mb-status').innerHTML='<span style="color:var(--danger)">'+esc(String(e))+'</span>';});
|
||||
}
|
||||
|
||||
@@ -740,6 +746,7 @@ function esMbView(msgId){
|
||||
attRows='<tr><td colspan="3" class="empty-state">No attachments</td></tr>';
|
||||
}
|
||||
document.getElementById('es-viewer-att-rows').innerHTML=attRows;
|
||||
halAnalyze('Email Security', JSON.stringify(d, null, 2), 'email analysis', 'defense');
|
||||
}).catch(function(e){document.getElementById('es-viewer-headers-text').textContent='Error: '+e;});
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ function forHashFile() {
|
||||
document.getElementById('for-hash-sha1').textContent = data.sha1 || '—';
|
||||
document.getElementById('for-hash-sha256').textContent = data.sha256 || '—';
|
||||
forLogCustody('hash', path, 'Computed hashes', data.sha256 || '');
|
||||
halAnalyze('Digital Forensics', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -283,6 +284,7 @@ function forVerifyHash() {
|
||||
el.style.color = 'var(--danger)';
|
||||
}
|
||||
forLogCustody('verify', path, data.match ? 'Hash verified' : 'Hash mismatch', expected);
|
||||
halAnalyze('Digital Forensics', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -310,6 +312,7 @@ function forCreateImage() {
|
||||
if (data.duration) lines.push('Duration: ' + data.duration + 's');
|
||||
log.textContent = lines.join('\n') || 'Done.';
|
||||
forLogCustody('image', output, 'Disk image created from ' + source, data.hash || '');
|
||||
halAnalyze('Digital Forensics', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -341,6 +344,7 @@ function forCarve() {
|
||||
document.getElementById('for-carve-status').textContent = 'Carved ' + forCarvedFiles.length + ' file(s)';
|
||||
forRenderCarved();
|
||||
forLogCustody('carve', source, 'Carved ' + forCarvedFiles.length + ' files (' + types.join(',') + ')', '');
|
||||
halAnalyze('Digital Forensics', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -401,6 +405,7 @@ function forBuildTimeline() {
|
||||
document.getElementById('for-timeline-status').textContent = 'Timeline built — ' + forTimelineEvents.length + ' event(s)';
|
||||
forRenderTimeline();
|
||||
forLogCustody('timeline', path, 'Built timeline with ' + forTimelineEvents.length + ' events', '');
|
||||
halAnalyze('Digital Forensics', JSON.stringify(data, null, 2), 'forensics', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,9 @@
|
||||
<h2>ADB Devices</h2>
|
||||
<div class="tool-actions" id="hw-adb-refresh-bar">
|
||||
<button class="btn btn-primary btn-small" onclick="hwRefreshAdbDevices()">Refresh</button>
|
||||
<button class="btn btn-danger btn-small" onclick="hwAdbKillServer()">Kill ADB Server</button>
|
||||
<button class="btn btn-small" onclick="hwAdbStartServer()">Start ADB Server</button>
|
||||
<span id="hw-adb-server-status" style="font-size:0.8rem;color:var(--text-muted)"></span>
|
||||
</div>
|
||||
<div id="hw-adb-devices"></div>
|
||||
</div>
|
||||
|
||||
@@ -401,6 +401,7 @@ function irCreate() {
|
||||
document.getElementById('ir-name').value = '';
|
||||
document.getElementById('ir-desc').value = '';
|
||||
irLoadList();
|
||||
halAnalyze('Incident Response', JSON.stringify(data, null, 2), 'incident response', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -432,6 +433,7 @@ function irLoadList() {
|
||||
body.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--text-muted)">No incidents found</td></tr>';
|
||||
}
|
||||
irPopulateSelectors();
|
||||
halAnalyze('Incident Response', JSON.stringify(data, null, 2), 'incident response', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -451,6 +453,7 @@ function irSelect(id) {
|
||||
document.getElementById('ir-detail-desc').textContent = inc.description || '';
|
||||
document.getElementById('ir-update-status').value = inc.status === 'closed' ? 'resolved' : inc.status;
|
||||
irLoadPlaybook(id, inc);
|
||||
halAnalyze('Incident Response', JSON.stringify(inc, null, 2), 'incident response', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -636,6 +639,7 @@ function swSweep() {
|
||||
if (!swLastMatches.length) {
|
||||
body.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-muted)">No matches found - system appears clean</td></tr>';
|
||||
}
|
||||
halAnalyze('Incident Response', JSON.stringify(data, null, 2), 'incident response', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -789,6 +793,7 @@ function tlGenReport() {
|
||||
|
||||
document.getElementById('tl-report-content').innerHTML = html;
|
||||
sec.scrollIntoView({behavior: 'smooth'});
|
||||
halAnalyze('Incident Response', JSON.stringify(rpt, null, 2), 'incident response', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ function createCapLink() {
|
||||
document.getElementById('cap-res-article').value = base + d.article_path;
|
||||
document.getElementById('cap-result').style.display = '';
|
||||
loadCapLinks();
|
||||
halAnalyze('IP Capture', JSON.stringify(d, null, 2), 'ip tracking', 'network');
|
||||
} else {
|
||||
alert(d.error);
|
||||
}
|
||||
@@ -167,6 +168,7 @@ function loadCapLinks() {
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
halAnalyze('IP Capture', JSON.stringify(d, null, 2), 'ip tracking', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,6 +220,7 @@ function loadCaptures() {
|
||||
<td style="padding:5px;font-size:0.75rem">${c.accept_language || ''}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
halAnalyze('IP Capture', JSON.stringify(d, null, 2), 'ip tracking', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<p style="font-size:0.8rem;color:var(--text-secondary);margin-top:0.5rem">
|
||||
Configured backend: <strong style="color:var(--accent)">{{ llm_backend }}</strong>
|
||||
— select a tab, fill in settings, and click <em>Save & Activate</em>, then <em>Load Model</em> to initialise.
|
||||
— select a tab, fill in settings, and click <em>Save & Activate</em>, then <em>Initialize LLM</em> to initialise.
|
||||
</p>
|
||||
|
||||
<!-- Load / Status bar -->
|
||||
@@ -32,10 +32,10 @@
|
||||
<div id="llm-status-dot" style="width:10px;height:10px;border-radius:50%;
|
||||
background:var(--text-muted);flex-shrink:0" title="Not loaded"></div>
|
||||
<span id="llm-status-text" style="font-size:0.83rem;color:var(--text-secondary);flex:1">
|
||||
Not loaded — click <strong>Load Model</strong> to initialise the current backend.
|
||||
Not loaded — click <strong>Initialize LLM</strong> to initialise the current backend.
|
||||
</span>
|
||||
<button id="btn-llm-load" class="btn btn-primary btn-sm" onclick="loadLLM()">
|
||||
Load Model
|
||||
Initialize LLM
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="debugOpen()"
|
||||
title="Open debug console to see detailed load output">
|
||||
@@ -367,62 +367,71 @@
|
||||
<div class="section">
|
||||
<h2>Claude API</h2>
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary)">
|
||||
Requires an <a href="https://console.anthropic.com" target="_blank" rel="noopener">Anthropic account</a>.
|
||||
Get your API key from the console.
|
||||
Requires an <a href="https://console.anthropic.com" target="_blank" rel="noopener">Anthropic API</a> key.
|
||||
AUTARCH calls Claude directly for chat, agent, and analysis tasks.
|
||||
</p>
|
||||
<form method="POST" action="{{ url_for('settings.update_llm') }}" class="settings-form">
|
||||
<input type="hidden" name="backend" value="claude">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="claude-key">API Key</label>
|
||||
<input type="password" id="claude-key" name="api_key" value="{{ claude.api_key }}" placeholder="sk-ant-api03-...">
|
||||
<small>Stored in autarch_settings.conf — keep it safe.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-key">API Key</label>
|
||||
<input type="password" id="claude-key" value="{{ claude.api_key }}" placeholder="sk-ant-api03-...">
|
||||
<small>Stored in autarch_settings.conf — keep it safe.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="claude-model">Model</label>
|
||||
<select id="claude-model" name="model">
|
||||
<optgroup label="Claude 4.6">
|
||||
<option value="claude-opus-4-6" {% if claude.model == 'claude-opus-4-6' %}selected{% endif %}>claude-opus-4-6 (most capable)</option>
|
||||
<option value="claude-sonnet-4-6" {% if claude.model == 'claude-sonnet-4-6' %}selected{% endif %}>claude-sonnet-4-6 (balanced)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Claude 4.5">
|
||||
<option value="claude-opus-4-5" {% if claude.model == 'claude-opus-4-5' %}selected{% endif %}>claude-opus-4-5</option>
|
||||
<option value="claude-sonnet-4-5" {% if claude.model == 'claude-sonnet-4-5' %}selected{% endif %}>claude-sonnet-4-5</option>
|
||||
<option value="claude-haiku-4-5-20251001" {% if claude.model == 'claude-haiku-4-5-20251001' %}selected{% endif %}>claude-haiku-4-5 (fastest)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Claude 3.5 / 3">
|
||||
<option value="claude-3-5-sonnet-20241022" {% if claude.model == 'claude-3-5-sonnet-20241022' %}selected{% endif %}>claude-3-5-sonnet-20241022</option>
|
||||
<option value="claude-3-5-haiku-20241022" {% if claude.model == 'claude-3-5-haiku-20241022' %}selected{% endif %}>claude-3-5-haiku-20241022</option>
|
||||
<option value="claude-3-opus-20240229" {% if claude.model == 'claude-3-opus-20240229' %}selected{% endif %}>claude-3-opus-20240229</option>
|
||||
</optgroup>
|
||||
<div class="form-group">
|
||||
<label for="claude-model">Model</label>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center">
|
||||
<select id="claude-model" style="flex:1">
|
||||
<option value="{{ claude.model }}" selected>{{ claude.model }}</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" onclick="claudeFetchModels()" id="btn-claude-refresh" title="Fetch available models from API">Refresh</button>
|
||||
</div>
|
||||
<small id="claude-model-hint">Click <strong>Refresh</strong> to fetch available models from the API.</small>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem 1rem">
|
||||
<div class="form-group">
|
||||
<label for="claude-max-tok">Max Tokens</label>
|
||||
<input type="number" id="claude-max-tok" name="max_tokens" value="{{ claude.max_tokens }}" min="1" max="200000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-temp">Temperature</label>
|
||||
<input type="number" id="claude-temp" name="temperature" value="{{ claude.temperature }}" step="0.05" min="0" max="1">
|
||||
<small>0–1. Claude default is 1.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-top-p">Top-P</label>
|
||||
<input type="number" id="claude-top-p" name="top_p" value="{{ claude.get('top_p', 1.0) }}" step="0.05" min="0" max="1">
|
||||
<small>Use with lower temp.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-top-k">Top-K</label>
|
||||
<input type="number" id="claude-top-k" name="top_k" value="{{ claude.get('top_k', 0) }}" min="0">
|
||||
<small>0 = disabled.</small>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem 1rem">
|
||||
<div class="form-group">
|
||||
<label for="claude-max-tok">Max Tokens</label>
|
||||
<input type="number" id="claude-max-tok" value="{{ claude.max_tokens }}" min="1" max="200000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-temp">Temperature</label>
|
||||
<input type="number" id="claude-temp" value="{{ claude.temperature }}" step="0.05" min="0" max="1">
|
||||
<small>0–1. Claude default is 1.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-top-p">Top-P</label>
|
||||
<input type="number" id="claude-top-p" value="{{ claude.get('top_p', 1.0) }}" step="0.05" min="0" max="1">
|
||||
<small>Use with lower temp.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claude-top-k">Top-K</label>
|
||||
<input type="number" id="claude-top-k" value="{{ claude.get('top_k', 0) }}" min="0">
|
||||
<small>0 = disabled.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save & Activate Claude</button>
|
||||
</form>
|
||||
<!-- Action buttons + inline status -->
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin-top:0.5rem">
|
||||
<button class="btn btn-primary" onclick="claudeActivate()" id="btn-claude-activate">
|
||||
Activate Claude
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="claudeSave()" id="btn-claude-save">
|
||||
Save Settings
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="claudeReload()" id="btn-claude-reload" title="Re-test API key and reconnect">
|
||||
Reload
|
||||
</button>
|
||||
<div id="claude-status-dot" style="width:10px;height:10px;border-radius:50%;
|
||||
background:{% if llm_backend == 'claude' %}var(--success, #34c759){% else %}var(--text-muted){% endif %};flex-shrink:0"></div>
|
||||
<span id="claude-status-text" style="font-size:0.83rem;color:var(--text-secondary)">
|
||||
{% if llm_backend == 'claude' %}Active — {{ claude.model }}{% else %}Not active{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<p style="font-size:0.72rem;color:var(--text-muted);margin-top:0.35rem">
|
||||
<strong>Save</strong> stores settings + API key to encrypted vault.
|
||||
<strong>Reload</strong> re-tests the connection without changing settings.
|
||||
<strong>Activate</strong> saves + loads Claude as the active backend.
|
||||
</p>
|
||||
</div>
|
||||
</div><!-- end tab-claude -->
|
||||
|
||||
@@ -605,9 +614,159 @@
|
||||
</div>
|
||||
</div><!-- end tab-huggingface -->
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<!-- AGENTS SECTION -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<div class="section" style="margin-top:2rem">
|
||||
<h2>Agent Configuration</h2>
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
||||
Configure the AI agent backends used by Agent Hal and Autonomy.
|
||||
Agents use the THOUGHT/ACTION/PARAMS loop to accomplish tasks with tools.
|
||||
</p>
|
||||
|
||||
<!-- Agent sub-tabs -->
|
||||
<div class="tab-bar" id="agent-tab-bar">
|
||||
<button class="tab active" onclick="agentTab('local')">Local Agent</button>
|
||||
<button class="tab" onclick="agentTab('claude')">Claude Agent</button>
|
||||
<button class="tab" onclick="agentTab('openai')">OpenAI Agent</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Local Agent Sub-Tab ──────────────────────────────────────── -->
|
||||
<div id="agent-tab-local" class="agent-tab-panel">
|
||||
<div style="padding:1rem 0">
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Uses the currently loaded LLM backend (configured above) for agent operations.
|
||||
Best for offline or privacy-sensitive work.
|
||||
</p>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
|
||||
<div class="form-group">
|
||||
<label for="agent-local-steps">Max Steps</label>
|
||||
<input type="number" id="agent-local-steps" value="{{ agents.local_max_steps }}" min="1" max="100">
|
||||
<small>Maximum tool-use steps per task.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agent-local-verbose">
|
||||
<input type="checkbox" id="agent-local-verbose" {% if agents.local_verbose %}checked{% endif %}
|
||||
style="margin-right:0.4rem">
|
||||
Verbose Output
|
||||
</label>
|
||||
<small>Show step-by-step agent reasoning.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="agentSave('local')">Save Local Agent Settings</button>
|
||||
<span id="agent-local-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Claude Agent Sub-Tab ─────────────────────────────────────── -->
|
||||
<div id="agent-tab-claude" class="agent-tab-panel hidden">
|
||||
<div style="padding:1rem 0">
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Uses the Anthropic Claude API with native tool use for agent operations.
|
||||
Requires a Claude API key (configured in the Claude tab above).
|
||||
Supports extended thinking and structured tool calls.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="agent-claude-model">Agent Model</label>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center">
|
||||
<select id="agent-claude-model" style="flex:1">
|
||||
<option value="{{ agents.claude_model }}" selected>{{ agents.claude_model }}</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" onclick="agentClaudeFetchModels()" id="btn-agent-claude-refresh">Refresh</button>
|
||||
</div>
|
||||
<small id="agent-claude-model-hint">Click Refresh to fetch models from the API. Uses the API key from the Claude tab.</small>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
|
||||
<div class="form-group">
|
||||
<label for="agent-claude-tokens">Max Tokens</label>
|
||||
<input type="number" id="agent-claude-tokens" value="{{ agents.claude_max_tokens }}" min="1" max="200000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agent-claude-steps">Max Steps</label>
|
||||
<input type="number" id="agent-claude-steps" value="{{ agents.claude_max_steps }}" min="1" max="200">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agent-claude-enabled">
|
||||
<input type="checkbox" id="agent-claude-enabled" {% if agents.claude_enabled %}checked{% endif %}
|
||||
style="margin-right:0.4rem">
|
||||
Enable Claude Agent
|
||||
</label>
|
||||
<small>Allow agent tasks to use Claude.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="agentSave('claude')">Save Claude Agent Settings</button>
|
||||
<span id="agent-claude-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── OpenAI Agent Sub-Tab ─────────────────────────────────────── -->
|
||||
<div id="agent-tab-openai" class="agent-tab-panel hidden">
|
||||
<div style="padding:1rem 0">
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Uses the OpenAI API (or any compatible endpoint: Ollama, vLLM, LiteLLM) with function calling for agent operations.
|
||||
Requires an API key configured in the OpenAI tab above.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="agent-openai-model">Agent Model</label>
|
||||
<input type="text" id="agent-openai-model" value="{{ agents.openai_model }}" placeholder="gpt-4o"
|
||||
list="agent-openai-model-list">
|
||||
<datalist id="agent-openai-model-list">
|
||||
<option value="gpt-4o">
|
||||
<option value="gpt-4o-mini">
|
||||
<option value="gpt-4.1">
|
||||
<option value="gpt-4.1-mini">
|
||||
<option value="gpt-4.1-nano">
|
||||
<option value="o3">
|
||||
<option value="o4-mini">
|
||||
</datalist>
|
||||
<small>Model ID. For local servers (Ollama, etc.) use the name you pulled.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agent-openai-base-url">Base URL</label>
|
||||
<input type="text" id="agent-openai-base-url" value="{{ agents.openai_base_url }}" placeholder="https://api.openai.com/v1">
|
||||
<small>Change for Ollama (<code>http://localhost:11434/v1</code>), vLLM, LiteLLM, etc.</small>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
|
||||
<div class="form-group">
|
||||
<label for="agent-openai-tokens">Max Tokens</label>
|
||||
<input type="number" id="agent-openai-tokens" value="{{ agents.openai_max_tokens }}" min="1" max="200000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agent-openai-steps">Max Steps</label>
|
||||
<input type="number" id="agent-openai-steps" value="{{ agents.openai_max_steps }}" min="1" max="200">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agent-openai-enabled">
|
||||
<input type="checkbox" id="agent-openai-enabled" {% if agents.openai_enabled %}checked{% endif %}
|
||||
style="margin-right:0.4rem">
|
||||
Enable OpenAI Agent
|
||||
</label>
|
||||
<small>Allow agent tasks to use OpenAI.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="agentSave('openai')">Save OpenAI Agent Settings</button>
|
||||
<span id="agent-openai-status" style="font-size:0.82rem;color:var(--text-secondary)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active agent backend indicator -->
|
||||
<div style="margin-top:0.75rem;padding:0.5rem 0.75rem;border-radius:var(--radius);border:1px solid var(--border);
|
||||
background:var(--bg-card);font-size:0.82rem;color:var(--text-secondary)">
|
||||
Active agent backend: <strong id="agent-active-backend" style="color:var(--accent)">{{ agents.backend }}</strong>
|
||||
— The agent will use this backend when processing tasks.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hidden { display: none !important; }
|
||||
.form-group small { display:block; font-size:0.72rem; color:var(--text-muted); margin-top:0.2rem; }
|
||||
.agent-tab-panel.hidden { display: none !important; }
|
||||
|
||||
/* ── GPU Preset Cards ───────────────────────────────────────────────────── */
|
||||
.gpu-presets {
|
||||
@@ -667,7 +826,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// ── Load Model ────────────────────────────────────────────────────────────────
|
||||
// ── Initialize LLM ────────────────────────────────────────────────────────────────
|
||||
function loadLLM() {
|
||||
var btn = document.getElementById('btn-llm-load');
|
||||
var dot = document.getElementById('llm-status-dot');
|
||||
@@ -683,7 +842,7 @@ function loadLLM() {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Load Model';
|
||||
btn.textContent = 'Initialize LLM';
|
||||
if (d.ok) {
|
||||
dot.style.background = 'var(--success, #34c759)';
|
||||
dot.title = 'Loaded';
|
||||
@@ -700,7 +859,7 @@ function loadLLM() {
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Load Model';
|
||||
btn.textContent = 'Initialize LLM';
|
||||
dot.style.background = 'var(--danger, #ff3b30)';
|
||||
dot.title = 'Error';
|
||||
text.textContent = 'Request failed: ' + e.message;
|
||||
@@ -866,5 +1025,277 @@ function hfVerifyToken() {
|
||||
})
|
||||
.catch(function(e) { btn.textContent = 'Verify Token'; btn.disabled = false; alert('Request failed: ' + e.message); });
|
||||
}
|
||||
|
||||
// ── Claude: Activate (save + load in one click) ──────────────────────────────
|
||||
function claudeSave() {
|
||||
var btn = document.getElementById('btn-claude-save');
|
||||
var text = document.getElementById('claude-status-text');
|
||||
btn.disabled = true; btn.textContent = 'Saving…';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('backend', 'claude');
|
||||
fd.append('api_key', document.getElementById('claude-key').value);
|
||||
fd.append('model', document.getElementById('claude-model').value);
|
||||
fd.append('max_tokens', document.getElementById('claude-max-tok').value);
|
||||
fd.append('temperature', document.getElementById('claude-temp').value);
|
||||
|
||||
fetch('/settings/llm', {method: 'POST', body: fd, redirect: 'manual'})
|
||||
.then(function() {
|
||||
btn.disabled = false; btn.textContent = 'Save Settings';
|
||||
text.innerHTML = '<span style="color:var(--success,#34c759)">✓ Settings saved to encrypted vault</span>';
|
||||
setTimeout(function() { text.textContent = ''; }, 3000);
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false; btn.textContent = 'Save Settings';
|
||||
text.textContent = 'Save failed: ' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
function claudeReload() {
|
||||
var btn = document.getElementById('btn-claude-reload');
|
||||
var dot = document.getElementById('claude-status-dot');
|
||||
var text = document.getElementById('claude-status-text');
|
||||
btn.disabled = true; btn.textContent = 'Reloading…';
|
||||
dot.style.background = '#f59e0b';
|
||||
text.innerHTML = '<em>Re-testing Claude connection…</em>';
|
||||
|
||||
fetch('/settings/llm/load', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Reload';
|
||||
if (d.ok) {
|
||||
dot.style.background = 'var(--success, #34c759)';
|
||||
text.innerHTML = '✓ <strong style="color:var(--success,#34c759)">Connected</strong> — ' + escapeHtml(d.model_name);
|
||||
} else {
|
||||
dot.style.background = 'var(--danger, #ff3b30)';
|
||||
text.innerHTML = '✕ ' + escapeHtml(d.error || 'Connection failed');
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false; btn.textContent = 'Reload';
|
||||
dot.style.background = 'var(--danger, #ff3b30)';
|
||||
text.textContent = 'Failed: ' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
function claudeActivate() {
|
||||
var btn = document.getElementById('btn-claude-activate');
|
||||
var dot = document.getElementById('claude-status-dot');
|
||||
var text = document.getElementById('claude-status-text');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
dot.style.background = '#f59e0b';
|
||||
text.textContent = 'Saving settings…';
|
||||
|
||||
// Build form data from the Claude tab fields
|
||||
var fd = new FormData();
|
||||
fd.append('backend', 'claude');
|
||||
fd.append('api_key', document.getElementById('claude-key').value);
|
||||
fd.append('model', document.getElementById('claude-model').value);
|
||||
fd.append('max_tokens', document.getElementById('claude-max-tok').value);
|
||||
fd.append('temperature', document.getElementById('claude-temp').value);
|
||||
|
||||
// Step 1: Save settings via the existing update_llm route (no redirect — use fetch)
|
||||
fetch('/settings/llm', {method: 'POST', body: fd, redirect: 'manual'})
|
||||
.then(function() {
|
||||
// Step 2: Now load the backend
|
||||
btn.textContent = 'Loading…';
|
||||
text.innerHTML = '<em>Initialising Claude — please wait…</em>';
|
||||
return fetch('/settings/llm/load', {method: 'POST'});
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Activate Claude';
|
||||
if (d.ok) {
|
||||
dot.style.background = 'var(--success, #34c759)';
|
||||
text.innerHTML = '✓ <strong style="color:var(--success,#34c759)">Claude active</strong> — '
|
||||
+ escapeHtml(d.model_name);
|
||||
// Also update the top-level status bar
|
||||
var topDot = document.getElementById('llm-status-dot');
|
||||
var topText = document.getElementById('llm-status-text');
|
||||
if (topDot) { topDot.style.background = 'var(--success, #34c759)'; topDot.title = 'Loaded'; }
|
||||
if (topText) { topText.innerHTML = '✓ <strong style="color:var(--success,#34c759)">claude</strong> ready — ' + escapeHtml(d.model_name); }
|
||||
} else {
|
||||
dot.style.background = 'var(--danger, #ff3b30)';
|
||||
text.innerHTML = '✕ <strong style="color:var(--danger,#ff3b30)">Load failed:</strong> '
|
||||
+ escapeHtml(d.error || 'Unknown error')
|
||||
+ ' — <em>check Debug Log for details</em>';
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Activate Claude';
|
||||
dot.style.background = 'var(--danger, #ff3b30)';
|
||||
text.textContent = 'Request failed: ' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Agent tab switching ───────────────────────────────────────────────────────
|
||||
function agentTab(name) {
|
||||
// Hide all agent panels
|
||||
var panels = document.querySelectorAll('.agent-tab-panel');
|
||||
for (var i = 0; i < panels.length; i++) panels[i].classList.add('hidden');
|
||||
// Deactivate all agent tabs
|
||||
var tabs = document.querySelectorAll('#agent-tab-bar .tab');
|
||||
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
|
||||
// Show selected
|
||||
var panel = document.getElementById('agent-tab-' + name);
|
||||
if (panel) panel.classList.remove('hidden');
|
||||
// Activate tab button (find by text match)
|
||||
var labels = {local: 'Local Agent', claude: 'Claude Agent', openai: 'OpenAI Agent'};
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
if (tabs[i].textContent.trim() === labels[name]) tabs[i].classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Agent save ───────────────────────────────────────────────────────────────
|
||||
function agentSave(tab) {
|
||||
var status = document.getElementById('agent-' + tab + '-status');
|
||||
var data = {backend: tab};
|
||||
|
||||
if (tab === 'local') {
|
||||
data.local_max_steps = parseInt(document.getElementById('agent-local-steps').value) || 20;
|
||||
data.local_verbose = document.getElementById('agent-local-verbose').checked;
|
||||
} else if (tab === 'claude') {
|
||||
data.claude_enabled = document.getElementById('agent-claude-enabled').checked;
|
||||
data.claude_model = document.getElementById('agent-claude-model').value;
|
||||
data.claude_max_tokens = parseInt(document.getElementById('agent-claude-tokens').value) || 16384;
|
||||
data.claude_max_steps = parseInt(document.getElementById('agent-claude-steps').value) || 30;
|
||||
} else if (tab === 'openai') {
|
||||
data.openai_enabled = document.getElementById('agent-openai-enabled').checked;
|
||||
data.openai_model = document.getElementById('agent-openai-model').value;
|
||||
data.openai_base_url = document.getElementById('agent-openai-base-url').value;
|
||||
data.openai_max_tokens = parseInt(document.getElementById('agent-openai-tokens').value) || 16384;
|
||||
data.openai_max_steps = parseInt(document.getElementById('agent-openai-steps').value) || 30;
|
||||
}
|
||||
|
||||
status.textContent = 'Saving…';
|
||||
fetch('/settings/agents/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok) {
|
||||
status.innerHTML = '<span style="color:var(--success,#34c759)">✓ Saved</span>';
|
||||
document.getElementById('agent-active-backend').textContent = tab;
|
||||
} else {
|
||||
status.innerHTML = '<span style="color:var(--danger,#ff3b30)">✕ ' + escapeHtml(d.error || 'Error') + '</span>';
|
||||
}
|
||||
setTimeout(function() { status.textContent = ''; }, 4000);
|
||||
})
|
||||
.catch(function(e) {
|
||||
status.textContent = 'Failed: ' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Agent Claude: Fetch models (reuses the same API endpoint) ────────────────
|
||||
function agentClaudeFetchModels() {
|
||||
var btn = document.getElementById('btn-agent-claude-refresh');
|
||||
var sel = document.getElementById('agent-claude-model');
|
||||
var hint = document.getElementById('agent-claude-model-hint');
|
||||
var curVal = sel.value;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Fetching…';
|
||||
hint.textContent = 'Querying Anthropic API…';
|
||||
|
||||
fetch('/settings/llm/claude-models', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Refresh';
|
||||
if (!d.ok) {
|
||||
hint.textContent = 'Error: ' + (d.error || 'Unknown');
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = '';
|
||||
var models = d.models || [];
|
||||
if (models.length === 0) {
|
||||
hint.textContent = 'No models returned.';
|
||||
var opt = document.createElement('option');
|
||||
opt.value = curVal; opt.textContent = curVal; opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
return;
|
||||
}
|
||||
var foundCurrent = false;
|
||||
for (var i = 0; i < models.length; i++) {
|
||||
var m = models[i];
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = m.name || m.id;
|
||||
if (m.id === curVal) { opt.selected = true; foundCurrent = true; }
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
if (!foundCurrent && curVal) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = curVal; opt.textContent = curVal + ' (current)'; opt.selected = true;
|
||||
sel.insertBefore(opt, sel.firstChild);
|
||||
}
|
||||
hint.textContent = models.length + ' models available.';
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Refresh';
|
||||
hint.textContent = 'Failed: ' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Claude: Fetch models from API ────────────────────────────────────────────
|
||||
function claudeFetchModels() {
|
||||
var btn = document.getElementById('btn-claude-refresh');
|
||||
var sel = document.getElementById('claude-model');
|
||||
var hint = document.getElementById('claude-model-hint');
|
||||
var curVal = sel.value;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Fetching…';
|
||||
hint.textContent = 'Querying Anthropic API…';
|
||||
|
||||
fetch('/settings/llm/claude-models', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Refresh';
|
||||
if (!d.ok) {
|
||||
hint.textContent = 'Error: ' + (d.error || 'Unknown error');
|
||||
return;
|
||||
}
|
||||
// Rebuild the dropdown with live models
|
||||
sel.innerHTML = '';
|
||||
var models = d.models || [];
|
||||
if (models.length === 0) {
|
||||
hint.textContent = 'No models returned — check API key permissions.';
|
||||
var opt = document.createElement('option');
|
||||
opt.value = curVal; opt.textContent = curVal; opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
return;
|
||||
}
|
||||
var foundCurrent = false;
|
||||
for (var i = 0; i < models.length; i++) {
|
||||
var m = models[i];
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = m.name || m.id;
|
||||
if (m.id === curVal) { opt.selected = true; foundCurrent = true; }
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
// Keep current value selected even if not in list
|
||||
if (!foundCurrent && curVal) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = curVal; opt.textContent = curVal + ' (current)'; opt.selected = true;
|
||||
sel.insertBefore(opt, sel.firstChild);
|
||||
}
|
||||
hint.textContent = models.length + ' models available.';
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Refresh';
|
||||
hint.textContent = 'Request failed: ' + e.message;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -241,6 +241,7 @@ function logIngestFile() {
|
||||
setLoading(btn, false);
|
||||
renderOutput('log-ingest-file-output', data.message || data.error || 'Done');
|
||||
if (data.success) logLoadSources();
|
||||
halAnalyze('Log Correlator', JSON.stringify(data, null, 2), 'log correlation', 'log_analysis');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -253,6 +254,7 @@ function logIngestPaste() {
|
||||
setLoading(btn, false);
|
||||
renderOutput('log-ingest-paste-output', data.message || data.error || 'Done');
|
||||
if (data.success) logLoadSources();
|
||||
halAnalyze('Log Correlator', JSON.stringify(data, null, 2), 'log correlation', 'log_analysis');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -294,6 +296,7 @@ function logSearch() {
|
||||
+ '<td style="font-family:monospace;font-size:0.8rem">' + esc(r.entry) + '</td></tr>';
|
||||
});
|
||||
tb.innerHTML = html;
|
||||
halAnalyze('Log Correlator', JSON.stringify(data, null, 2), 'log correlation', 'log_analysis');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -338,6 +341,7 @@ function logFilterAlerts(severity, btnEl) {
|
||||
+ '<td style="font-family:monospace;font-size:0.8rem;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(a.entry || '') + '</td></tr>';
|
||||
});
|
||||
tb.innerHTML = html;
|
||||
halAnalyze('Log Correlator', JSON.stringify(data, null, 2), 'log correlation', 'log_analysis');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -400,6 +404,7 @@ function logAddRule() {
|
||||
document.getElementById('log-rule-name').value = '';
|
||||
document.getElementById('log-rule-pattern').value = '';
|
||||
}
|
||||
halAnalyze('Log Correlator', JSON.stringify(data, null, 2), 'log correlation', 'log_analysis');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -459,6 +464,7 @@ function logLoadStats() {
|
||||
barsEl.innerHTML = barsHtml;
|
||||
labelsEl.innerHTML = labelsHtml;
|
||||
}
|
||||
halAnalyze('Log Correlator', JSON.stringify(data, null, 2), 'log correlation', 'log_analysis');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ function sandboxSubmit() {
|
||||
setLoading(btn, false);
|
||||
renderOutput('sandbox-submit-output', data.message || data.error || 'Submitted');
|
||||
if (data.success) { sandboxLoadSamples(); sandboxRefreshSelect(); }
|
||||
halAnalyze('Malware Sandbox', JSON.stringify(data, null, 2), 'malware analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); renderOutput('sandbox-submit-output', 'Upload failed'); });
|
||||
} else if (pathInput) {
|
||||
setLoading(btn, true);
|
||||
@@ -191,6 +192,7 @@ function sandboxSubmit() {
|
||||
setLoading(btn, false);
|
||||
renderOutput('sandbox-submit-output', data.message || data.error || 'Submitted');
|
||||
if (data.success) { sandboxLoadSamples(); sandboxRefreshSelect(); }
|
||||
halAnalyze('Malware Sandbox', JSON.stringify(data, null, 2), 'malware analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
} else {
|
||||
renderOutput('sandbox-submit-output', 'Select a file or enter a path.');
|
||||
@@ -266,6 +268,7 @@ function sandboxStatic() {
|
||||
|
||||
var strings = (data.strings || []).join('\n');
|
||||
renderOutput('sandbox-strings', strings || 'No interesting strings found.');
|
||||
halAnalyze('Malware Sandbox', JSON.stringify(data, null, 2), 'malware analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -328,6 +331,7 @@ function sandboxRenderDynamic(data) {
|
||||
renderOutput('sandbox-syscalls', (data.syscalls || []).join('\n') || 'No syscalls captured.');
|
||||
renderOutput('sandbox-files', (data.files_accessed || []).join('\n') || 'No file access recorded.');
|
||||
renderOutput('sandbox-network', (data.network_calls || []).join('\n') || 'No network activity recorded.');
|
||||
halAnalyze('Malware Sandbox', JSON.stringify(data, null, 2), 'malware analysis', 'analyze');
|
||||
}
|
||||
|
||||
/* ── Reports ── */
|
||||
@@ -395,6 +399,7 @@ function sandboxViewReport(reportId) {
|
||||
|
||||
html += '</div>';
|
||||
document.getElementById('sandbox-report-content').innerHTML = html;
|
||||
halAnalyze('Malware Sandbox', JSON.stringify(data, null, 2), 'malware analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
427
web/templates/mcp_settings.html
Normal file
427
web/templates/mcp_settings.html
Normal file
@@ -0,0 +1,427 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}MCP Server - AUTARCH{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||
<h1>MCP Server</h1>
|
||||
<a href="{{ url_for('settings.index') }}" class="btn btn-sm" style="margin-left:auto">← Back to Settings</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Model Context Protocol</h2>
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
The <a href="https://modelcontextprotocol.io/docs/getting-started/intro" target="_blank" rel="noopener">Model Context Protocol (MCP)</a>
|
||||
lets AI assistants like Claude Desktop, Claude Code, and other MCP-compatible clients use AUTARCH's security tools directly.
|
||||
When connected, Claude can run nmap scans, look up IPs, capture packets, manage devices, and more — all through natural conversation.
|
||||
</p>
|
||||
|
||||
<!-- ── Section 1: Server Control ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Server Control</h3>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="mcpStart()" id="btn-mcp-start">Start MCP Server</button>
|
||||
<button class="btn btn-sm" onclick="mcpStop()" id="btn-mcp-stop">Stop</button>
|
||||
<div id="mcp-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-muted);flex-shrink:0"></div>
|
||||
<span id="mcp-status-text" style="font-size:0.82rem;color:var(--text-secondary)">Checking...</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap;font-size:0.82rem;margin-bottom:0.5rem">
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer">
|
||||
<input type="checkbox" id="mcp-auto-start" {{ 'checked' if mcp.auto_start == 'true' }}>
|
||||
Auto-start on launch
|
||||
</label>
|
||||
<span style="color:var(--text-muted)">Transport: <strong id="mcp-transport-display">{{ mcp.transport }}</strong></span>
|
||||
</div>
|
||||
<p style="font-size:0.75rem;color:var(--text-muted);margin:0">
|
||||
SSE endpoint: <code>http://{{ request.host.split(':')[0] }}:{{ mcp.port }}/sse</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 2: Transport & Network ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Transport & Network Settings</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.75rem;font-size:0.82rem">
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Transport</label>
|
||||
<select id="mcp-transport" style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem">
|
||||
<option value="sse" {{ 'selected' if mcp.transport == 'sse' }}>SSE (HTTP)</option>
|
||||
<option value="stdio" {{ 'selected' if mcp.transport == 'stdio' }}>stdio (CLI only)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Host</label>
|
||||
<input type="text" id="mcp-host" value="{{ mcp.host }}" placeholder="0.0.0.0"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Port</label>
|
||||
<input type="number" id="mcp-port" value="{{ mcp.port }}" placeholder="8081"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">CORS Origins</label>
|
||||
<input type="text" id="mcp-cors-origins" value="{{ mcp.cors_origins }}" placeholder="*"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 3: Security ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Security</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:0.75rem;font-size:0.82rem">
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer">
|
||||
<input type="checkbox" id="mcp-auth-enabled" {{ 'checked' if mcp.auth_enabled == 'true' }}>
|
||||
Enable authentication
|
||||
</label>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Auth Token</label>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap">
|
||||
<input type="password" id="mcp-auth-token" value="{{ mcp.auth_token }}" readonly
|
||||
style="flex:1;min-width:200px;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;font-family:monospace">
|
||||
<button class="btn btn-sm" onclick="mcpToggleToken()" id="btn-toggle-token" style="font-size:0.75rem">Show</button>
|
||||
<button class="btn btn-sm" onclick="mcpGenerateToken()" style="font-size:0.75rem">Generate New Token</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem">
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Rate Limit</label>
|
||||
<input type="text" id="mcp-rate-limit" value="{{ mcp.rate_limit }}" placeholder="100/hour"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer">
|
||||
<input type="checkbox" id="mcp-mask-errors" {{ 'checked' if mcp.mask_errors == 'true' }}>
|
||||
Mask error details in responses
|
||||
</label>
|
||||
<div style="border-top:1px solid var(--border);padding-top:0.75rem;margin-top:0.25rem">
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin-bottom:0.5rem">
|
||||
<input type="checkbox" id="mcp-ssl-enabled" {{ 'checked' if mcp.ssl_enabled == 'true' }}>
|
||||
Enable SSL / TLS
|
||||
</label>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0.75rem">
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">SSL Certificate Path</label>
|
||||
<input type="text" id="mcp-ssl-cert" value="{{ mcp.ssl_cert }}" placeholder="/path/to/cert.pem"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">SSL Key Path</label>
|
||||
<input type="text" id="mcp-ssl-key" value="{{ mcp.ssl_key }}" placeholder="/path/to/key.pem"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 4: Tool Management ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Tool Management</h3>
|
||||
<p style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.75rem">
|
||||
Enable or disable individual MCP tools. Disabled tools will not be exposed to connected clients.
|
||||
</p>
|
||||
<div id="mcp-tools-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0.5rem;font-size:0.82rem">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 5: Tool Timeouts ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Tool Timeouts</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem;font-size:0.82rem">
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">nmap timeout (s)</label>
|
||||
<input type="number" id="mcp-nmap-timeout" value="{{ mcp.nmap_timeout }}" placeholder="120"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">tcpdump timeout (s)</label>
|
||||
<input type="number" id="mcp-tcpdump-timeout" value="{{ mcp.tcpdump_timeout }}" placeholder="30"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">whois timeout (s)</label>
|
||||
<input type="number" id="mcp-whois-timeout" value="{{ mcp.whois_timeout }}" placeholder="15"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">DNS timeout (s)</label>
|
||||
<input type="number" id="mcp-dns-timeout" value="{{ mcp.dns_timeout }}" placeholder="10"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">GeoIP timeout (s)</label>
|
||||
<input type="number" id="mcp-geoip-timeout" value="{{ mcp.geoip_timeout }}" placeholder="10"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">GeoIP Endpoint URL</label>
|
||||
<input type="text" id="mcp-geoip-endpoint" value="{{ mcp.geoip_endpoint }}" placeholder="http://ip-api.com/json/"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 6: Advanced ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Advanced</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:0.75rem;font-size:0.82rem">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem">
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Log Level</label>
|
||||
<select id="mcp-log-level" style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem">
|
||||
<option value="DEBUG" {{ 'selected' if mcp.log_level == 'DEBUG' }}>DEBUG</option>
|
||||
<option value="INFO" {{ 'selected' if mcp.log_level == 'INFO' }}>INFO</option>
|
||||
<option value="WARNING" {{ 'selected' if mcp.log_level == 'WARNING' }}>WARNING</option>
|
||||
<option value="ERROR" {{ 'selected' if mcp.log_level == 'ERROR' }}>ERROR</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Request Timeout (s)</label>
|
||||
<input type="number" id="mcp-request-timeout" value="{{ mcp.request_timeout }}" placeholder="30"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Max Message Size (bytes)</label>
|
||||
<input type="number" id="mcp-max-message-size" value="{{ mcp.max_message_size }}" placeholder="1048576"
|
||||
style="width:100%;padding:0.35rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;box-sizing:border-box">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;color:var(--text-secondary);margin-bottom:0.25rem">Server Instructions</label>
|
||||
<textarea id="mcp-instructions" rows="4" placeholder="Instructions sent to MCP clients describing this server's capabilities..."
|
||||
style="width:100%;padding:0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-main);color:inherit;font-size:0.82rem;font-family:inherit;resize:vertical;box-sizing:border-box">{{ mcp.instructions }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 7: Integration ── -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1.25rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Integration</h3>
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.5rem">
|
||||
<strong>Claude Desktop / Claude Code config:</strong> Add this to your
|
||||
<code>claude_desktop_config.json</code> or <code>.claude/settings.json</code>:
|
||||
</p>
|
||||
<div style="position:relative;margin-bottom:1rem">
|
||||
<pre id="mcp-config-block" style="background:var(--bg-main);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:0.75rem;font-size:0.78rem;overflow-x:auto;margin:0">Loading...</pre>
|
||||
<button class="btn btn-sm" onclick="mcpCopyConfig()" id="btn-mcp-copy"
|
||||
style="position:absolute;top:0.4rem;right:0.4rem;font-size:0.7rem">Copy</button>
|
||||
</div>
|
||||
<p style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.5rem"><strong>CLI Commands:</strong></p>
|
||||
<pre style="background:var(--bg-main);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:0.75rem;font-size:0.78rem;overflow-x:auto;margin:0"><span style="color:var(--text-muted)"># stdio mode (Claude Desktop / Claude Code)</span>
|
||||
python autarch.py --mcp stdio
|
||||
|
||||
<span style="color:var(--text-muted)"># SSE mode (remote / web clients)</span>
|
||||
python autarch.py --mcp sse --mcp-port {{ mcp.port }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- ── Save Button ── -->
|
||||
<div style="position:sticky;bottom:0;padding:0.75rem 0;background:var(--bg-main);border-top:1px solid var(--border);display:flex;align-items:center;gap:1rem;z-index:10">
|
||||
<button class="btn btn-primary" onclick="mcpSave()" id="btn-mcp-save" style="font-size:0.9rem;padding:0.5rem 1.5rem">
|
||||
Save MCP Settings
|
||||
</button>
|
||||
<span id="mcp-save-status" style="font-size:0.82rem;color:var(--text-muted)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Tool definitions ─────────────────────────────────────────────────────────
|
||||
var MCP_TOOLS = [
|
||||
{name: 'nmap_scan', desc: 'Run an nmap network scan'},
|
||||
{name: 'geoip_lookup', desc: 'GeoIP information for an IP'},
|
||||
{name: 'dns_lookup', desc: 'DNS record queries'},
|
||||
{name: 'whois_lookup', desc: 'WHOIS lookup for domain/IP'},
|
||||
{name: 'packet_capture', desc: 'Capture network packets'},
|
||||
{name: 'wireguard_status', desc: 'WireGuard VPN status'},
|
||||
{name: 'upnp_status', desc: 'UPnP port mapping status'},
|
||||
{name: 'system_info', desc: 'System information'},
|
||||
{name: 'llm_chat', desc: 'Chat with configured LLM'},
|
||||
{name: 'android_devices', desc: 'List Android devices via ADB'},
|
||||
{name: 'config_get', desc: 'Read AUTARCH configuration'}
|
||||
];
|
||||
|
||||
var disabledToolsRaw = {{ mcp.disabled_tools | tojson }};
|
||||
|
||||
function escapeHtml(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function buildToolGrid() {
|
||||
var disabled = (disabledToolsRaw || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean);
|
||||
var grid = document.getElementById('mcp-tools-grid');
|
||||
grid.innerHTML = '';
|
||||
for (var i = 0; i < MCP_TOOLS.length; i++) {
|
||||
var t = MCP_TOOLS[i];
|
||||
var isEnabled = disabled.indexOf(t.name) === -1;
|
||||
var el = document.createElement('label');
|
||||
el.style.cssText = 'display:flex;align-items:flex-start;gap:0.5rem;padding:0.5rem 0.6rem;border-radius:4px;border:1px solid var(--border);background:var(--bg-main);cursor:pointer';
|
||||
el.innerHTML = '<input type="checkbox" class="mcp-tool-cb" data-tool="' + t.name + '" ' + (isEnabled ? 'checked' : '') + ' style="margin-top:0.15rem">'
|
||||
+ '<div><strong style="color:var(--accent)">' + escapeHtml(t.name) + '</strong>'
|
||||
+ '<div style="font-size:0.72rem;color:var(--text-muted);margin-top:0.1rem">' + escapeHtml(t.desc) + '</div></div>';
|
||||
grid.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Server Controls ──────────────────────────────────────────────────────────
|
||||
function mcpStart() {
|
||||
var btn = document.getElementById('btn-mcp-start');
|
||||
btn.disabled = true; btn.textContent = 'Starting...';
|
||||
fetch('/settings/mcp/start', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Start MCP Server';
|
||||
mcpRefreshStatus();
|
||||
})
|
||||
.catch(function(e) { btn.disabled = false; btn.textContent = 'Start MCP Server'; alert(e.message); });
|
||||
}
|
||||
|
||||
function mcpStop() {
|
||||
var btn = document.getElementById('btn-mcp-stop');
|
||||
btn.disabled = true; btn.textContent = 'Stopping...';
|
||||
fetch('/settings/mcp/stop', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Stop';
|
||||
mcpRefreshStatus();
|
||||
})
|
||||
.catch(function(e) { btn.disabled = false; btn.textContent = 'Stop'; });
|
||||
}
|
||||
|
||||
function mcpRefreshStatus() {
|
||||
fetch('/settings/mcp/status', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var dot = document.getElementById('mcp-status-dot');
|
||||
var text = document.getElementById('mcp-status-text');
|
||||
if (d.ok && d.status && d.status.running) {
|
||||
dot.style.background = 'var(--success, #34c759)';
|
||||
text.innerHTML = '✓ Running (PID ' + d.status.pid + ')';
|
||||
} else {
|
||||
dot.style.background = 'var(--text-muted)';
|
||||
text.textContent = 'Stopped';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('mcp-status-dot').style.background = 'var(--text-muted)';
|
||||
document.getElementById('mcp-status-text').textContent = 'Error checking status';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Save ─────────────────────────────────────────────────────────────────────
|
||||
function mcpSave() {
|
||||
var btn = document.getElementById('btn-mcp-save');
|
||||
var status = document.getElementById('mcp-save-status');
|
||||
btn.disabled = true; btn.textContent = 'Saving...';
|
||||
status.textContent = '';
|
||||
|
||||
// Collect disabled tools
|
||||
var disabledTools = [];
|
||||
var cbs = document.querySelectorAll('.mcp-tool-cb');
|
||||
for (var i = 0; i < cbs.length; i++) {
|
||||
if (!cbs[i].checked) disabledTools.push(cbs[i].getAttribute('data-tool'));
|
||||
}
|
||||
|
||||
var payload = {
|
||||
enabled: true,
|
||||
auto_start: document.getElementById('mcp-auto-start').checked,
|
||||
transport: document.getElementById('mcp-transport').value,
|
||||
host: document.getElementById('mcp-host').value,
|
||||
port: document.getElementById('mcp-port').value,
|
||||
cors_origins: document.getElementById('mcp-cors-origins').value,
|
||||
auth_enabled: document.getElementById('mcp-auth-enabled').checked,
|
||||
auth_token: document.getElementById('mcp-auth-token').value,
|
||||
rate_limit: document.getElementById('mcp-rate-limit').value,
|
||||
mask_errors: document.getElementById('mcp-mask-errors').checked,
|
||||
ssl_enabled: document.getElementById('mcp-ssl-enabled').checked,
|
||||
ssl_cert: document.getElementById('mcp-ssl-cert').value,
|
||||
ssl_key: document.getElementById('mcp-ssl-key').value,
|
||||
log_level: document.getElementById('mcp-log-level').value,
|
||||
instructions: document.getElementById('mcp-instructions').value,
|
||||
request_timeout: document.getElementById('mcp-request-timeout').value,
|
||||
max_message_size: document.getElementById('mcp-max-message-size').value,
|
||||
disabled_tools: disabledTools.join(','),
|
||||
nmap_timeout: document.getElementById('mcp-nmap-timeout').value,
|
||||
tcpdump_timeout: document.getElementById('mcp-tcpdump-timeout').value,
|
||||
whois_timeout: document.getElementById('mcp-whois-timeout').value,
|
||||
dns_timeout: document.getElementById('mcp-dns-timeout').value,
|
||||
geoip_timeout: document.getElementById('mcp-geoip-timeout').value,
|
||||
geoip_endpoint: document.getElementById('mcp-geoip-endpoint').value
|
||||
};
|
||||
|
||||
fetch('/settings/mcp/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Save MCP Settings';
|
||||
if (d.ok) {
|
||||
status.style.color = 'var(--success, #34c759)';
|
||||
status.textContent = 'Settings saved successfully.';
|
||||
} else {
|
||||
status.style.color = 'var(--danger, #ff3b30)';
|
||||
status.textContent = 'Error: ' + (d.error || 'Unknown error');
|
||||
}
|
||||
setTimeout(function() { status.textContent = ''; }, 4000);
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false; btn.textContent = 'Save MCP Settings';
|
||||
status.style.color = 'var(--danger, #ff3b30)';
|
||||
status.textContent = 'Error: ' + e.message;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Token helpers ────────────────────────────────────────────────────────────
|
||||
function mcpGenerateToken() {
|
||||
fetch('/settings/mcp/generate-token', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok && d.token) {
|
||||
var input = document.getElementById('mcp-auth-token');
|
||||
input.value = d.token;
|
||||
input.type = 'text';
|
||||
document.getElementById('btn-toggle-token').textContent = 'Hide';
|
||||
}
|
||||
})
|
||||
.catch(function(e) { alert('Failed to generate token: ' + e.message); });
|
||||
}
|
||||
|
||||
function mcpToggleToken() {
|
||||
var input = document.getElementById('mcp-auth-token');
|
||||
var btn = document.getElementById('btn-toggle-token');
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
btn.textContent = 'Hide';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
btn.textContent = 'Show';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config copy ──────────────────────────────────────────────────────────────
|
||||
function mcpCopyConfig() {
|
||||
var block = document.getElementById('mcp-config-block');
|
||||
navigator.clipboard.writeText(block.textContent).then(function() {
|
||||
var btn = document.getElementById('btn-mcp-copy');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Page load ────────────────────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
buildToolGrid();
|
||||
mcpRefreshStatus();
|
||||
// Fetch config snippet
|
||||
fetch('/settings/mcp/config', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok) document.getElementById('mcp-config-block').textContent = d.config;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
398
web/templates/module_creator.html
Normal file
398
web/templates/module_creator.html
Normal file
@@ -0,0 +1,398 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Module Creator - AUTARCH{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Module Creator</h1>
|
||||
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||||
Create, edit, validate, and manage AUTARCH modules
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#module-code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.82rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
resize: vertical;
|
||||
tab-size: 4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
#module-code:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.mc-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
.mc-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
.mc-editor, .mc-list {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
}
|
||||
.mc-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.6rem 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.mc-form-grid .form-group { margin-bottom: 0; }
|
||||
.mc-form-grid label {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.mc-form-grid input, .mc-form-grid select {
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.mc-btn-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mc-btn-row .btn { font-size: 0.82rem; padding: 0.4rem 0.9rem; }
|
||||
.mc-status {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.8rem;
|
||||
display: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.mc-status.success { display: block; background: rgba(34,197,94,0.12); color: #4ade80; border: 1px solid rgba(34,197,94,0.25); }
|
||||
.mc-status.error { display: block; background: rgba(239,68,68,0.12); color: #f87171; border: 1px solid rgba(239,68,68,0.25); }
|
||||
.mc-status.info { display: block; background: rgba(99,102,241,0.12); color: var(--accent-hover); border: 1px solid rgba(99,102,241,0.25); }
|
||||
.mc-list h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.mc-modules-scroll {
|
||||
max-height: 620px;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
.mc-cat-group { margin-bottom: 0.75rem; }
|
||||
.mc-cat-group h3 {
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.3rem;
|
||||
padding-bottom: 0.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.mc-mod-item {
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.mc-mod-item:hover { background: var(--bg-input); }
|
||||
.mc-mod-item .mod-name { font-weight: 600; color: var(--text-primary); }
|
||||
.mc-mod-item .mod-desc {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mc-mod-item .mod-meta {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mc-layout">
|
||||
<!-- Left column: editor -->
|
||||
<div class="mc-editor">
|
||||
<div class="mc-form-grid">
|
||||
<div class="form-group">
|
||||
<label for="mod-name">Module Name</label>
|
||||
<input type="text" id="mod-name" placeholder="my_module" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mod-category">Category</label>
|
||||
<select id="mod-category">
|
||||
<option value="defense">defense</option>
|
||||
<option value="offense">offense</option>
|
||||
<option value="counter">counter</option>
|
||||
<option value="analyze">analyze</option>
|
||||
<option value="osint">osint</option>
|
||||
<option value="simulate">simulate</option>
|
||||
<option value="core">core</option>
|
||||
<option value="hardware">hardware</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mod-desc">Description</label>
|
||||
<input type="text" id="mod-desc" placeholder="Short description of the module">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mod-author">Author</label>
|
||||
<input type="text" id="mod-author" value="darkHal">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:0.5rem">
|
||||
<button class="btn btn-sm" onclick="loadTemplate()" style="font-size:0.78rem">Load Template</button>
|
||||
</div>
|
||||
|
||||
<textarea id="module-code" rows="25" placeholder="# Module code will appear here... # Click 'Load Template' or select a module from the list." spellcheck="false"></textarea>
|
||||
|
||||
<div class="mc-btn-row">
|
||||
<button class="btn" onclick="validateCode()">Validate</button>
|
||||
<button class="btn" onclick="createModule()" style="background:var(--accent);color:#fff">Create Module</button>
|
||||
<button class="btn" onclick="saveModule()">Save Changes</button>
|
||||
</div>
|
||||
|
||||
<div id="mc-status" class="mc-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right column: module list -->
|
||||
<div class="mc-list">
|
||||
<h2>
|
||||
Modules
|
||||
<button class="btn btn-sm" onclick="refreshModuleList()" style="font-size:0.72rem;padding:0.25rem 0.6rem">Refresh</button>
|
||||
</h2>
|
||||
<div id="module-list" class="mc-modules-scroll">
|
||||
<p style="color:var(--text-muted);font-size:0.8rem">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/module-creator';
|
||||
const statusEl = document.getElementById('mc-status');
|
||||
const codeEl = document.getElementById('module-code');
|
||||
let currentEditName = null;
|
||||
|
||||
function showStatus(msg, type) {
|
||||
statusEl.className = 'mc-status ' + type;
|
||||
statusEl.textContent = msg;
|
||||
}
|
||||
|
||||
function clearStatus() {
|
||||
statusEl.className = 'mc-status';
|
||||
statusEl.textContent = '';
|
||||
}
|
||||
|
||||
/* Tab key support in textarea */
|
||||
codeEl.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 4;
|
||||
}
|
||||
});
|
||||
|
||||
/* Load template skeleton for selected category */
|
||||
function loadTemplate() {
|
||||
const cat = document.getElementById('mod-category').value;
|
||||
fetch(API + '/templates')
|
||||
.then(r => r.json())
|
||||
.then(templates => {
|
||||
const t = templates.find(x => x.category === cat);
|
||||
if (t) {
|
||||
codeEl.value = t.code;
|
||||
if (!document.getElementById('mod-desc').value) {
|
||||
document.getElementById('mod-desc').value = t.description;
|
||||
}
|
||||
currentEditName = null;
|
||||
showStatus('Template loaded for category: ' + cat, 'info');
|
||||
}
|
||||
})
|
||||
.catch(err => showStatus('Failed to load templates: ' + err, 'error'));
|
||||
}
|
||||
|
||||
/* Validate code syntax */
|
||||
function validateCode() {
|
||||
const code = codeEl.value.trim();
|
||||
if (!code) { showStatus('No code to validate', 'error'); return; }
|
||||
fetch(API + '/validate', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({code: code})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
showStatus('Validation passed. ' + (data.warnings || []).join('; '), 'success');
|
||||
} else {
|
||||
showStatus('Validation failed:\n' + (data.errors || []).join('\n'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => showStatus('Validation request failed: ' + err, 'error'));
|
||||
}
|
||||
|
||||
/* Create new module */
|
||||
function createModule() {
|
||||
const name = document.getElementById('mod-name').value.trim();
|
||||
const category = document.getElementById('mod-category').value;
|
||||
const description = document.getElementById('mod-desc').value.trim();
|
||||
const author = document.getElementById('mod-author').value.trim() || 'darkHal';
|
||||
const code = codeEl.value;
|
||||
|
||||
if (!name) { showStatus('Module name is required', 'error'); return; }
|
||||
if (!code.trim()) { showStatus('Module code is empty', 'error'); return; }
|
||||
|
||||
fetch(API + '/create', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, category, description, author, code})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showStatus(data.message, 'success');
|
||||
currentEditName = name;
|
||||
refreshModuleList();
|
||||
} else {
|
||||
showStatus(data.error || 'Creation failed', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => showStatus('Request failed: ' + err, 'error'));
|
||||
}
|
||||
|
||||
/* Save changes to existing module */
|
||||
function saveModule() {
|
||||
const name = currentEditName || document.getElementById('mod-name').value.trim();
|
||||
const code = codeEl.value;
|
||||
|
||||
if (!name) { showStatus('No module selected to save. Enter a name or click a module from the list.', 'error'); return; }
|
||||
if (!code.trim()) { showStatus('Module code is empty', 'error'); return; }
|
||||
|
||||
fetch(API + '/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, code})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showStatus(data.message, 'success');
|
||||
refreshModuleList();
|
||||
} else {
|
||||
showStatus(data.error || 'Save failed', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => showStatus('Request failed: ' + err, 'error'));
|
||||
}
|
||||
|
||||
/* Load a module into the editor */
|
||||
function loadModule(name) {
|
||||
fetch(API + '/preview', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
codeEl.value = data.code;
|
||||
currentEditName = name;
|
||||
const m = data.metadata || {};
|
||||
document.getElementById('mod-name').value = name;
|
||||
if (m.category) document.getElementById('mod-category').value = m.category;
|
||||
if (m.description) document.getElementById('mod-desc').value = m.description;
|
||||
if (m.author) document.getElementById('mod-author').value = m.author;
|
||||
showStatus('Loaded module: ' + name, 'info');
|
||||
} else {
|
||||
showStatus(data.error || 'Failed to load module', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => showStatus('Failed to load module: ' + err, 'error'));
|
||||
}
|
||||
|
||||
/* Refresh the module list */
|
||||
function refreshModuleList() {
|
||||
const container = document.getElementById('module-list');
|
||||
container.innerHTML = '<p style="color:var(--text-muted);font-size:0.8rem">Loading...</p>';
|
||||
|
||||
fetch(API + '/list')
|
||||
.then(r => r.json())
|
||||
.then(modules => {
|
||||
if (!modules.length) {
|
||||
container.innerHTML = '<p style="color:var(--text-muted);font-size:0.8rem">No modules found.</p>';
|
||||
return;
|
||||
}
|
||||
/* Group by category */
|
||||
const groups = {};
|
||||
modules.forEach(m => {
|
||||
const cat = m.category || 'unknown';
|
||||
if (!groups[cat]) groups[cat] = [];
|
||||
groups[cat].push(m);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
const catOrder = ['defense','offense','counter','analyze','osint','simulate','core','hardware','unknown'];
|
||||
catOrder.forEach(cat => {
|
||||
if (!groups[cat]) return;
|
||||
html += '<div class="mc-cat-group">';
|
||||
html += '<h3>' + cat + ' (' + groups[cat].length + ')</h3>';
|
||||
groups[cat].forEach(m => {
|
||||
const desc = m.description ? m.description.substring(0, 60) : '';
|
||||
const ver = m.version ? 'v' + m.version : '';
|
||||
html += '<div class="mc-mod-item" onclick="loadModule(\'' + m.name.replace(/'/g, "\\'") + '\')">';
|
||||
html += '<div class="mod-name">' + m.name + '</div>';
|
||||
if (desc) html += '<div class="mod-desc">' + desc + '</div>';
|
||||
html += '<div class="mod-meta">' + [ver, m.last_modified].filter(Boolean).join(' · ') + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
/* Any categories not in catOrder */
|
||||
Object.keys(groups).forEach(cat => {
|
||||
if (catOrder.includes(cat)) return;
|
||||
html += '<div class="mc-cat-group">';
|
||||
html += '<h3>' + cat + ' (' + groups[cat].length + ')</h3>';
|
||||
groups[cat].forEach(m => {
|
||||
const desc = m.description ? m.description.substring(0, 60) : '';
|
||||
const ver = m.version ? 'v' + m.version : '';
|
||||
html += '<div class="mc-mod-item" onclick="loadModule(\'' + m.name.replace(/'/g, "\\'") + '\')">';
|
||||
html += '<div class="mod-name">' + m.name + '</div>';
|
||||
if (desc) html += '<div class="mod-desc">' + desc + '</div>';
|
||||
html += '<div class="mod-meta">' + [ver, m.last_modified].filter(Boolean).join(' · ') + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
})
|
||||
.catch(err => {
|
||||
container.innerHTML = '<p style="color:var(--danger);font-size:0.8rem">Failed to load modules</p>';
|
||||
});
|
||||
}
|
||||
|
||||
/* Initial load */
|
||||
refreshModuleList();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -112,6 +112,7 @@ function startDiscover(){
|
||||
document.getElementById('disc-status').innerHTML='';
|
||||
currentHosts=s.hosts||[];
|
||||
renderHosts(currentHosts);
|
||||
halAnalyze('Net Mapper', JSON.stringify(s, null, 2), 'network topology', 'network');
|
||||
});
|
||||
},2000);
|
||||
}).catch(e=>showDiscError(e.message));
|
||||
@@ -160,6 +161,7 @@ function showTopology(){
|
||||
if(!d.ok) return;
|
||||
renderTopology(d);
|
||||
switchTab('map');
|
||||
halAnalyze('Net Mapper', JSON.stringify(d, null, 2), 'network topology', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,6 +233,7 @@ function loadScans(){
|
||||
s1.innerHTML+=`<option value="${esc(s.file)}">${esc(s.name)} (${s.host_count})</option>`;
|
||||
s2.innerHTML+=`<option value="${esc(s.file)}">${esc(s.name)} (${s.host_count})</option>`;
|
||||
});
|
||||
halAnalyze('Net Mapper', JSON.stringify(d, null, 2), 'network topology', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -240,6 +243,7 @@ function loadSavedScan(file){
|
||||
currentHosts=d.scan.hosts||[];
|
||||
renderHosts(currentHosts);
|
||||
switchTab('discover');
|
||||
halAnalyze('Net Mapper', JSON.stringify(d, null, 2), 'network topology', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -256,6 +260,7 @@ function diffScans(){
|
||||
html+=`<div style="color:var(--danger)"><strong>- Removed (${d.removed_hosts.length}):</strong> ${d.removed_hosts.join(', ')||'none'}</div>`;
|
||||
html+=`<div style="color:var(--text-muted)">Unchanged: ${d.unchanged_hosts.length}</div></div>`;
|
||||
document.getElementById('diff-results').innerHTML=html;
|
||||
halAnalyze('Net Mapper', JSON.stringify(d, null, 2), 'network topology', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
895
web/templates/network.html
Normal file
895
web/templates/network.html
Normal file
@@ -0,0 +1,895 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Network Security - AUTARCH{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Network Security</h1>
|
||||
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||||
Connection analysis, intrusion detection, rogue device scanning, and real-time monitoring.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<div class="tab-bar" id="network-tab-bar">
|
||||
<button class="tab active" onclick="networkTab('connections')">Connections</button>
|
||||
<button class="tab" onclick="networkTab('ids')">Intrusion Detection</button>
|
||||
<button class="tab" onclick="networkTab('rogue')">Rogue Devices</button>
|
||||
<button class="tab" onclick="networkTab('monitor')">Monitor</button>
|
||||
<button class="tab" onclick="networkTab('wifi')">WiFi Scanner</button>
|
||||
<button class="tab" onclick="networkTab('attacks')">Attack Detection</button>
|
||||
<button class="tab" onclick="networkTab('arpspoof')">ARP Spoof</button>
|
||||
<button class="tab" onclick="networkTab('ssid')">SSID Map</button>
|
||||
</div>
|
||||
|
||||
<!-- ==================== CONNECTIONS TAB ==================== -->
|
||||
<div class="network-tab-panel" id="network-tab-connections">
|
||||
|
||||
<div class="section">
|
||||
<h2>Active Connections</h2>
|
||||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1rem">
|
||||
<button class="btn btn-primary" onclick="scanConnections()">Scan Connections</button>
|
||||
<button class="btn" onclick="scanArpTable()">ARP Table</button>
|
||||
<button class="btn" onclick="scanInterfaces()">Interfaces</button>
|
||||
</div>
|
||||
<div id="conn-status" style="margin-bottom:0.5rem;font-size:0.85rem;color:var(--text-secondary)"></div>
|
||||
<div id="conn-results" style="overflow-x:auto"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ==================== INTRUSION DETECTION TAB ==================== -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-ids">
|
||||
|
||||
<div class="section">
|
||||
<h2>Intrusion Detection System</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem">
|
||||
<button class="btn btn-primary" onclick="runIdsScan()">Run IDS Scan</button>
|
||||
<span id="ids-overall" style="font-size:0.85rem"></span>
|
||||
</div>
|
||||
<div id="ids-results"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ==================== ROGUE DEVICES TAB ==================== -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-rogue">
|
||||
|
||||
<div class="section">
|
||||
<h2>Rogue Device Detection</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem">
|
||||
<button class="btn btn-primary" onclick="scanRogueDevices()">Scan for Rogues</button>
|
||||
<span id="rogue-summary" style="font-size:0.85rem;color:var(--text-secondary)"></span>
|
||||
</div>
|
||||
|
||||
<div id="rogue-new" style="margin-bottom:1rem"></div>
|
||||
|
||||
<h3 style="margin-top:1rem">Known Devices</h3>
|
||||
<div id="rogue-known" style="overflow-x:auto"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ==================== MONITOR TAB ==================== -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-monitor">
|
||||
|
||||
<div class="section">
|
||||
<h2>Real-Time Connection Monitor</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem">
|
||||
<button class="btn btn-primary" id="monitor-start-btn" onclick="startMonitor()">Start Monitor</button>
|
||||
<button class="btn btn-danger" id="monitor-stop-btn" onclick="stopMonitor()" disabled>Stop Monitor</button>
|
||||
<span id="monitor-status" style="font-size:0.85rem;color:var(--text-secondary)">Stopped</span>
|
||||
</div>
|
||||
<div id="monitor-feed" style="overflow-x:auto;max-height:500px;overflow-y:auto">
|
||||
<table class="data-table" style="font-size:0.8rem">
|
||||
<thead><tr><th>Time</th><th>Protocol</th><th>Local</th><th>Remote</th><th>Process</th></tr></thead>
|
||||
<tbody id="monitor-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- WiFi Scanner Tab -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-wifi">
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">WiFi Network Scanner</h3>
|
||||
<p style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.75rem">
|
||||
Scan for nearby WiFi networks. Shows SSIDs, BSSIDs, channels, signal strength, and security.
|
||||
Requires a wireless interface.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:0.75rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="wifiScan()">Scan WiFi Networks</button>
|
||||
</div>
|
||||
<div id="wifi-scan-status" style="font-size:0.82rem;color:var(--text-muted);margin-bottom:0.5rem"></div>
|
||||
<div id="wifi-scan-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attack Detection Tab -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-attacks">
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">WiFi Attack Detection</h3>
|
||||
<p style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.75rem">
|
||||
Scan for active attacks against your network: deauth floods, evil twin APs,
|
||||
WiFi Pineapple rogue APs, MITM/ARP poisoning, and SSL stripping.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.75rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="detectAttacks()" id="btn-detect-attacks">Run Attack Detection</button>
|
||||
<span id="attack-detect-status" style="font-size:0.82rem;color:var(--text-muted)"></span>
|
||||
</div>
|
||||
<div id="attack-results"></div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Pentesting Tools</h3>
|
||||
<p style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.75rem">
|
||||
Launch offensive WiFi tools for authorized penetration testing.
|
||||
</p>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.5rem">
|
||||
<a href="/deauth" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">Deauth Attack</a>
|
||||
<a href="/pineapple" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">Pineapple / Evil Twin</a>
|
||||
<a href="/mitm-proxy" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">MITM Proxy</a>
|
||||
<a href="/wifi-audit" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">WiFi Audit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ARP Spoof Detection & Remediation Tab -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-arpspoof">
|
||||
|
||||
<!-- Detection -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">ARP Spoof Detection</h3>
|
||||
<p style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.75rem">
|
||||
Scans your ARP table for poisoning indicators: IPs with multiple MACs, gateway MAC changes,
|
||||
and suspicious broadcast entries. Compares against your saved baseline.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.75rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="arpSpoofScan()" id="btn-arp-scan">Scan for ARP Spoofing</button>
|
||||
<button class="btn btn-sm" onclick="arpSaveBaseline()" id="btn-arp-baseline">Save Current as Baseline</button>
|
||||
<span id="arp-scan-status" style="font-size:0.82rem;color:var(--text-muted)"></span>
|
||||
</div>
|
||||
<div id="arp-scan-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- ARP Table -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Current ARP Table</h3>
|
||||
<div id="arp-table-display"></div>
|
||||
</div>
|
||||
|
||||
<!-- Remediation Tools -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">Remediation Tools</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0.75rem;font-size:0.82rem">
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.65rem;background:var(--bg-main)">
|
||||
<strong>Flush & Set Static ARP</strong>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin:0.3rem 0">
|
||||
Remove the poisoned entry and lock in the correct MAC for an IP.
|
||||
</div>
|
||||
<div style="display:flex;gap:0.4rem;margin-top:0.4rem">
|
||||
<input type="text" id="arp-fix-ip" placeholder="IP (e.g. 192.168.1.1)" style="flex:1;padding:0.3rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-card);color:inherit;font-size:0.78rem">
|
||||
<input type="text" id="arp-fix-mac" placeholder="MAC (e.g. aa:bb:cc:dd:ee:ff)" style="flex:1;padding:0.3rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-card);color:inherit;font-size:0.78rem">
|
||||
</div>
|
||||
<button class="btn btn-sm" onclick="arpFixStatic()" style="margin-top:0.4rem">Flush & Set Static</button>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.65rem;background:var(--bg-main)">
|
||||
<strong>Enable Kernel ARP Protection</strong>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin:0.3rem 0">
|
||||
Set <code>arp_announce=2</code>, <code>arp_ignore=1</code>, and <code>rp_filter=1</code>
|
||||
to make the kernel reject suspicious ARP replies.
|
||||
</div>
|
||||
<button class="btn btn-sm" onclick="arpEnableProtection()" style="margin-top:0.4rem">Enable Protection</button>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.65rem;background:var(--bg-main)">
|
||||
<strong>Flush Specific Entry</strong>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin:0.3rem 0">
|
||||
Remove a single IP from the ARP cache so it re-learns the correct MAC.
|
||||
</div>
|
||||
<div style="display:flex;gap:0.4rem;margin-top:0.4rem">
|
||||
<input type="text" id="arp-flush-ip" placeholder="IP to flush" style="flex:1;padding:0.3rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-card);color:inherit;font-size:0.78rem">
|
||||
<button class="btn btn-sm" onclick="arpFlushEntry()">Flush</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="arp-fix-results" style="margin-top:0.75rem"></div>
|
||||
</div>
|
||||
|
||||
<!-- How to Fix Guide -->
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">How ARP Spoofing Works & How to Fix It</h3>
|
||||
<div style="font-size:0.8rem;color:var(--text-secondary);line-height:1.65">
|
||||
<p><strong>What is ARP Spoofing?</strong><br>
|
||||
An attacker sends fake ARP (Address Resolution Protocol) replies to associate their MAC address
|
||||
with the IP of another device (usually the gateway). This causes your traffic to route through
|
||||
the attacker's machine instead of directly to the router — enabling eavesdropping, credential
|
||||
theft, and session hijacking.</p>
|
||||
|
||||
<p style="margin-top:0.6rem"><strong>Signs you're being spoofed:</strong></p>
|
||||
<ul style="padding-left:1.2rem;margin:0.3rem 0">
|
||||
<li>An IP address (especially the gateway) shows multiple MAC addresses</li>
|
||||
<li>Your gateway's MAC address changed from what it was before</li>
|
||||
<li>Internet is slow or connections drop intermittently</li>
|
||||
<li>HTTPS certificate warnings appearing on sites that worked before</li>
|
||||
</ul>
|
||||
|
||||
<p style="margin-top:0.6rem"><strong>Immediate fix (Linux):</strong></p>
|
||||
<pre style="background:var(--bg-main);border:1px solid var(--border);border-radius:var(--radius);padding:0.5rem;font-size:0.75rem;overflow-x:auto;margin:0.3rem 0"><span style="color:var(--text-muted)"># 1. Find your gateway IP and its REAL MAC (check your router's label)</span>
|
||||
ip route show default
|
||||
<span style="color:var(--text-muted)"># 2. Flush the poisoned entry</span>
|
||||
sudo ip neigh flush 192.168.1.1
|
||||
<span style="color:var(--text-muted)"># 3. Set a static ARP entry (replace with your router's real MAC)</span>
|
||||
sudo arp -s 192.168.1.1 aa:bb:cc:dd:ee:ff
|
||||
<span style="color:var(--text-muted)"># 4. Enable kernel-level ARP protection</span>
|
||||
sudo sysctl -w net.ipv4.conf.all.arp_announce=2
|
||||
sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
|
||||
sudo sysctl -w net.ipv4.conf.all.rp_filter=1</pre>
|
||||
|
||||
<p style="margin-top:0.6rem"><strong>Permanent fix:</strong></p>
|
||||
<ul style="padding-left:1.2rem;margin:0.3rem 0">
|
||||
<li>Add the sysctl settings to <code>/etc/sysctl.conf</code> so they persist across reboots</li>
|
||||
<li>Use <strong>Dynamic ARP Inspection (DAI)</strong> on managed switches</li>
|
||||
<li>Use <strong>802.1X port authentication</strong> on your network</li>
|
||||
<li>Use a VPN — encrypted traffic can't be read even if intercepted</li>
|
||||
<li>Install <strong>arpwatch</strong>: <code>sudo apt install arpwatch</code> — monitors ARP changes 24/7</li>
|
||||
</ul>
|
||||
|
||||
<p style="margin-top:0.6rem"><strong>Find the attacker:</strong></p>
|
||||
<ul style="padding-left:1.2rem;margin:0.3rem 0">
|
||||
<li>Note the attacker's MAC address from the scan results</li>
|
||||
<li>Use the <strong>Intruder Trace</strong> section below with the attacker's IP</li>
|
||||
<li>Check your router's DHCP lease table to identify the device</li>
|
||||
<li>Run <code>nmap -sn 192.168.1.0/24</code> to find all devices and their MACs</li>
|
||||
<li>Block the attacker: <code>sudo iptables -A INPUT -m mac --mac-source XX:XX:XX:XX:XX:XX -j DROP</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSID Map Tab -->
|
||||
<div class="network-tab-panel hidden" id="network-tab-ssid">
|
||||
<div style="border:1px solid var(--border);background:var(--bg-card);border-radius:var(--radius);padding:0.85rem 1rem;margin-bottom:1rem">
|
||||
<h3 style="font-size:0.95rem;margin-bottom:0.5rem">SSID Scanner & Mapper</h3>
|
||||
<p style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.75rem">
|
||||
Map all WiFi networks in range. Groups access points by SSID, showing all BSSIDs,
|
||||
channels, signal strength, and security for each network. Useful for identifying
|
||||
multi-AP deployments and spotting rogues.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.75rem">
|
||||
<button class="btn btn-primary btn-sm" onclick="ssidMap()" id="btn-ssid-map">Build SSID Map</button>
|
||||
<span id="ssid-map-status" style="font-size:0.82rem;color:var(--text-muted)"></span>
|
||||
</div>
|
||||
<div id="ssid-map-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== INTRUDER TRACE (always visible) ==================== -->
|
||||
<div class="section" style="margin-top:2rem;border-top:1px solid var(--border);padding-top:1.5rem">
|
||||
<h2>Intruder Trace</h2>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Trace an IP address: reverse DNS, GeoIP, whois, open ports, associated processes, connection history.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
|
||||
<input type="text" id="trace-ip" class="form-control" placeholder="Enter IP address" style="max-width:250px">
|
||||
<button class="btn btn-primary" onclick="traceIntruder()">Trace</button>
|
||||
</div>
|
||||
<div id="trace-status" style="margin-top:0.5rem;font-size:0.85rem;color:var(--text-secondary)"></div>
|
||||
<div id="trace-results" style="margin-top:1rem"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hidden { display: none !important; }
|
||||
.ids-group {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ids-group h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.severity-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.severity-critical { background: rgba(239,68,68,0.2); color: #ef4444; }
|
||||
.severity-warning { background: rgba(234,179,8,0.2); color: #eab308; }
|
||||
.severity-clean { background: rgba(34,197,94,0.2); color: #22c55e; }
|
||||
.rogue-new-device {
|
||||
background: rgba(239,68,68,0.1);
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.trace-section {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.trace-section h4 { margin: 0 0 0.5rem 0; color: var(--accent); }
|
||||
.trace-section pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* ── Tab switching ── */
|
||||
function networkTab(name) {
|
||||
var panels = document.querySelectorAll('.network-tab-panel');
|
||||
for (var i = 0; i < panels.length; i++) panels[i].classList.add('hidden');
|
||||
var tabs = document.querySelectorAll('#network-tab-bar .tab');
|
||||
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
|
||||
document.getElementById('network-tab-' + name).classList.remove('hidden');
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
/* ── Helpers ── */
|
||||
function postJSON(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: body ? JSON.stringify(body) : '{}'
|
||||
}).then(function(r) { return r.json(); });
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
/* ── Connections ── */
|
||||
function scanConnections() {
|
||||
var el = document.getElementById('conn-results');
|
||||
var st = document.getElementById('conn-status');
|
||||
st.textContent = 'Scanning connections...';
|
||||
el.innerHTML = '';
|
||||
postJSON('/network/connections').then(function(d) {
|
||||
if (!d.ok) { st.textContent = 'Error: ' + (d.error || 'unknown'); return; }
|
||||
st.textContent = d.count + ' connection(s) found';
|
||||
var html = '<table class="data-table" style="font-size:0.8rem"><thead><tr>' +
|
||||
'<th>Protocol</th><th>Local</th><th>Remote</th><th>State</th><th>Process</th></tr></thead><tbody>';
|
||||
for (var i = 0; i < d.connections.length; i++) {
|
||||
var c = d.connections[i];
|
||||
html += '<tr><td>' + escHtml(c.protocol) + '</td><td>' + escHtml(c.local) +
|
||||
'</td><td>' + escHtml(c.remote) + '</td><td>' + escHtml(c.state) +
|
||||
'</td><td>' + escHtml(c.process) + '</td></tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
halAnalyze('Network: Connection Scan', JSON.stringify(d, null, 2), 'network scan', 'network');
|
||||
}).catch(function(e) { st.textContent = 'Error: ' + e; });
|
||||
}
|
||||
|
||||
function scanArpTable() {
|
||||
var el = document.getElementById('conn-results');
|
||||
var st = document.getElementById('conn-status');
|
||||
st.textContent = 'Fetching ARP table...';
|
||||
el.innerHTML = '';
|
||||
postJSON('/network/arp-table').then(function(d) {
|
||||
if (!d.ok) { st.textContent = 'Error: ' + (d.error || 'unknown'); return; }
|
||||
st.textContent = d.count + ' ARP entries';
|
||||
var html = '<table class="data-table" style="font-size:0.8rem"><thead><tr>' +
|
||||
'<th>IP</th><th>MAC</th><th>Interface</th><th>State</th></tr></thead><tbody>';
|
||||
for (var i = 0; i < d.entries.length; i++) {
|
||||
var e = d.entries[i];
|
||||
html += '<tr><td>' + escHtml(e.ip) + '</td><td>' + escHtml(e.mac) +
|
||||
'</td><td>' + escHtml(e.dev) + '</td><td>' + escHtml(e.state) + '</td></tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
}).catch(function(e) { st.textContent = 'Error: ' + e; });
|
||||
}
|
||||
|
||||
function scanInterfaces() {
|
||||
var el = document.getElementById('conn-results');
|
||||
var st = document.getElementById('conn-status');
|
||||
st.textContent = 'Listing interfaces...';
|
||||
el.innerHTML = '';
|
||||
postJSON('/network/interfaces').then(function(d) {
|
||||
if (!d.ok) { st.textContent = 'Error: ' + (d.error || 'unknown'); return; }
|
||||
st.textContent = d.interfaces.length + ' interface(s)';
|
||||
var html = '<table class="data-table" style="font-size:0.8rem"><thead><tr>' +
|
||||
'<th>Name</th><th>State</th><th>MAC</th><th>MTU</th><th>Addresses</th></tr></thead><tbody>';
|
||||
for (var i = 0; i < d.interfaces.length; i++) {
|
||||
var ifc = d.interfaces[i];
|
||||
var addrs = '';
|
||||
if (ifc.addresses) {
|
||||
for (var j = 0; j < ifc.addresses.length; j++) {
|
||||
if (j > 0) addrs += '<br>';
|
||||
addrs += escHtml(ifc.addresses[j].address || '');
|
||||
}
|
||||
}
|
||||
html += '<tr><td><strong>' + escHtml(ifc.name) + '</strong></td><td>' + escHtml(ifc.state) +
|
||||
'</td><td>' + escHtml(ifc.mac) + '</td><td>' + escHtml(ifc.mtu) +
|
||||
'</td><td>' + addrs + '</td></tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
}).catch(function(e) { st.textContent = 'Error: ' + e; });
|
||||
}
|
||||
|
||||
/* ── IDS ── */
|
||||
function severityBadge(sev) {
|
||||
return '<span class="severity-badge severity-' + sev + '">' + sev + '</span>';
|
||||
}
|
||||
|
||||
function runIdsScan() {
|
||||
var el = document.getElementById('ids-results');
|
||||
var ov = document.getElementById('ids-overall');
|
||||
ov.innerHTML = 'Scanning...';
|
||||
el.innerHTML = '';
|
||||
postJSON('/network/ids/scan').then(function(d) {
|
||||
if (!d.ok) { ov.textContent = 'Scan failed'; return; }
|
||||
var r = d.results;
|
||||
ov.innerHTML = 'Overall: ' + severityBadge(r.overall);
|
||||
|
||||
var groups = [
|
||||
{key: 'arp_spoof', title: 'ARP Spoof Detection'},
|
||||
{key: 'promiscuous', title: 'Promiscuous Mode'},
|
||||
{key: 'dhcp', title: 'Unauthorized DHCP'},
|
||||
{key: 'suspicious_conns', title: 'Suspicious Connections'},
|
||||
{key: 'raw_sockets', title: 'Raw Socket Processes'}
|
||||
];
|
||||
|
||||
var html = '';
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var g = groups[i];
|
||||
var data = r[g.key];
|
||||
html += '<div class="ids-group">';
|
||||
html += '<h4>' + escHtml(g.title) + ' ' + severityBadge(data.severity) + '</h4>';
|
||||
html += '<p style="font-size:0.85rem;color:var(--text-secondary);margin:0 0 0.5rem 0">' + escHtml(data.details) + '</p>';
|
||||
if (data.alerts && data.alerts.length > 0) {
|
||||
html += '<ul style="margin:0;padding-left:1.2rem;font-size:0.8rem">';
|
||||
for (var j = 0; j < data.alerts.length; j++) {
|
||||
var sev = data.severity === 'critical' ? 'color:#ef4444' : 'color:#eab308';
|
||||
html += '<li style="' + sev + ';margin-bottom:0.25rem">' + escHtml(data.alerts[j].message) + '</li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
halAnalyze('Network: IDS Scan', JSON.stringify(d, null, 2), 'network scan', 'network');
|
||||
}).catch(function(e) { ov.textContent = 'Error: ' + e; });
|
||||
}
|
||||
|
||||
/* ── Rogue Devices ── */
|
||||
function scanRogueDevices() {
|
||||
var newEl = document.getElementById('rogue-new');
|
||||
var knownEl = document.getElementById('rogue-known');
|
||||
var sumEl = document.getElementById('rogue-summary');
|
||||
sumEl.textContent = 'Scanning...';
|
||||
newEl.innerHTML = '';
|
||||
knownEl.innerHTML = '';
|
||||
postJSON('/network/rogue-detect').then(function(d) {
|
||||
if (!d.ok) { sumEl.textContent = 'Error: ' + (d.error || 'unknown'); return; }
|
||||
var s = d.summary;
|
||||
sumEl.textContent = s.total + ' devices found, ' + s.known + ' known, ' + s.new + ' new, ' + s.spoofed + ' spoofed';
|
||||
|
||||
// New/unauthorized devices
|
||||
if (d.new_devices.length > 0) {
|
||||
var html = '<h3 style="color:#ef4444;margin-bottom:0.5rem">New / Unknown Devices</h3>';
|
||||
for (var i = 0; i < d.new_devices.length; i++) {
|
||||
var dev = d.new_devices[i];
|
||||
html += '<div class="rogue-new-device">';
|
||||
html += '<div><strong>' + escHtml(dev.ip) + '</strong> — ' + escHtml(dev.mac) + '</div>';
|
||||
html += '<div style="display:flex;gap:0.5rem">';
|
||||
html += '<button class="btn btn-sm" onclick="trustDevice(\'' + escHtml(dev.ip) + '\',\'' + escHtml(dev.mac) + '\')">Trust</button>';
|
||||
html += '<button class="btn btn-danger btn-sm" onclick="blockDevice(\'' + escHtml(dev.ip) + '\')">Block</button>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
newEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// Spoofed alerts
|
||||
if (d.spoofed.length > 0) {
|
||||
var shtml = '<h3 style="color:#ef4444;margin:1rem 0 0.5rem 0">Spoofed MAC Alerts</h3>';
|
||||
for (var i = 0; i < d.spoofed.length; i++) {
|
||||
shtml += '<div class="rogue-new-device">' + escHtml(d.spoofed[i].message) + '</div>';
|
||||
}
|
||||
newEl.innerHTML += shtml;
|
||||
}
|
||||
|
||||
// Known devices table
|
||||
var known = d.known_devices;
|
||||
var keys = Object.keys(known);
|
||||
if (keys.length > 0) {
|
||||
var khtml = '<table class="data-table" style="font-size:0.8rem"><thead><tr>' +
|
||||
'<th>IP</th><th>MAC</th><th>Trusted</th><th>First Seen</th><th>Last Seen</th></tr></thead><tbody>';
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var kd = known[k];
|
||||
khtml += '<tr><td>' + escHtml(k) + '</td><td>' + escHtml(kd.mac) +
|
||||
'</td><td>' + (kd.trusted ? 'Yes' : 'No') +
|
||||
'</td><td>' + escHtml(kd.first_seen || '') +
|
||||
'</td><td>' + escHtml(kd.last_seen || '') + '</td></tr>';
|
||||
}
|
||||
khtml += '</tbody></table>';
|
||||
knownEl.innerHTML = khtml;
|
||||
} else {
|
||||
knownEl.innerHTML = '<p style="color:var(--text-secondary);font-size:0.85rem">No known devices yet. Scan and trust devices to build your baseline.</p>';
|
||||
}
|
||||
halAnalyze('Network: Rogue Device Scan', JSON.stringify(d, null, 2), 'network scan', 'network');
|
||||
}).catch(function(e) { sumEl.textContent = 'Error: ' + e; });
|
||||
}
|
||||
|
||||
function trustDevice(ip, mac) {
|
||||
postJSON('/network/rogue-detect/trust', {ip: ip, mac: mac}).then(function(d) {
|
||||
if (d.ok) scanRogueDevices();
|
||||
else alert('Error: ' + (d.error || 'unknown'));
|
||||
});
|
||||
}
|
||||
|
||||
function blockDevice(ip) {
|
||||
if (!confirm('Block IP ' + ip + '?')) return;
|
||||
postJSON('/network/block-ip', {ip: ip, action: 'block'}).then(function(d) {
|
||||
alert(d.message || d.error || 'Done');
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Monitor ── */
|
||||
var monitorSource = null;
|
||||
|
||||
function startMonitor() {
|
||||
document.getElementById('monitor-body').innerHTML = '';
|
||||
postJSON('/network/monitor/start').then(function(d) {
|
||||
if (!d.ok) { alert(d.error || 'Failed'); return; }
|
||||
document.getElementById('monitor-start-btn').disabled = true;
|
||||
document.getElementById('monitor-stop-btn').disabled = false;
|
||||
document.getElementById('monitor-status').textContent = 'Running...';
|
||||
document.getElementById('monitor-status').style.color = '#22c55e';
|
||||
|
||||
monitorSource = new EventSource('/network/monitor/feed');
|
||||
monitorSource.onmessage = function(ev) {
|
||||
try {
|
||||
var data = JSON.parse(ev.data);
|
||||
if (data.done) { stopMonitor(); return; }
|
||||
var body = document.getElementById('monitor-body');
|
||||
var row = document.createElement('tr');
|
||||
var ts = data.timestamp ? data.timestamp.split('T')[1].split('.')[0] : '';
|
||||
row.innerHTML = '<td>' + escHtml(ts) + '</td><td>' + escHtml(data.protocol) +
|
||||
'</td><td>' + escHtml(data.local) + '</td><td>' + escHtml(data.remote) +
|
||||
'</td><td>' + escHtml(data.process) + '</td>';
|
||||
body.insertBefore(row, body.firstChild);
|
||||
// Cap displayed rows
|
||||
while (body.children.length > 200) body.removeChild(body.lastChild);
|
||||
} catch(e) {}
|
||||
};
|
||||
monitorSource.onerror = function() {
|
||||
document.getElementById('monitor-status').textContent = 'Connection lost';
|
||||
document.getElementById('monitor-status').style.color = '#ef4444';
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function stopMonitor() {
|
||||
postJSON('/network/monitor/stop').then(function() {
|
||||
document.getElementById('monitor-start-btn').disabled = false;
|
||||
document.getElementById('monitor-stop-btn').disabled = true;
|
||||
document.getElementById('monitor-status').textContent = 'Stopped';
|
||||
document.getElementById('monitor-status').style.color = 'var(--text-secondary)';
|
||||
});
|
||||
if (monitorSource) { monitorSource.close(); monitorSource = null; }
|
||||
}
|
||||
|
||||
/* ── Intruder Trace ── */
|
||||
function traceIntruder() {
|
||||
var ip = document.getElementById('trace-ip').value.trim();
|
||||
if (!ip) { alert('Enter an IP address'); return; }
|
||||
var st = document.getElementById('trace-status');
|
||||
var el = document.getElementById('trace-results');
|
||||
st.textContent = 'Tracing ' + ip + '... this may take up to 30 seconds.';
|
||||
el.innerHTML = '';
|
||||
postJSON('/network/intruder-trace', {ip: ip}).then(function(d) {
|
||||
if (!d.ok) { st.textContent = 'Error: ' + (d.error || 'unknown'); return; }
|
||||
st.textContent = 'Trace complete for ' + ip;
|
||||
var t = d.trace;
|
||||
var sections = [
|
||||
{title: 'Reverse DNS', data: t.reverse_dns},
|
||||
{title: 'GeoIP', data: t.geoip},
|
||||
{title: 'Whois', data: t.whois},
|
||||
{title: 'Open Ports', data: t.open_ports},
|
||||
{title: 'Associated Processes', data: t.processes},
|
||||
{title: 'Connection History', data: t.connection_history}
|
||||
];
|
||||
var html = '';
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
html += '<div class="trace-section"><h4>' + escHtml(sections[i].title) + '</h4>';
|
||||
html += '<pre>' + escHtml(sections[i].data) + '</pre></div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}).catch(function(e) { st.textContent = 'Error: ' + e; });
|
||||
}
|
||||
|
||||
// ── WiFi Scanner ─────────────────────────────────────────────────────────────
|
||||
function wifiScan() {
|
||||
var status = document.getElementById('wifi-scan-status');
|
||||
var results = document.getElementById('wifi-scan-results');
|
||||
status.textContent = 'Scanning WiFi networks…';
|
||||
results.innerHTML = '';
|
||||
|
||||
fetch('/network/wifi/scan', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (!d.ok) { status.textContent = 'Error: ' + (d.error || 'Unknown'); return; }
|
||||
status.textContent = d.count + ' network(s) found';
|
||||
if (!d.networks || !d.networks.length) { results.innerHTML = '<p style="color:var(--text-muted)">No networks found.</p>'; return; }
|
||||
|
||||
var html = '<table class="data-table" style="font-size:0.82rem"><thead><tr>'
|
||||
+ '<th>SSID</th><th>BSSID</th><th>Channel</th><th>Signal</th><th>Security</th><th>Mode</th>'
|
||||
+ '</tr></thead><tbody>';
|
||||
for (var i = 0; i < d.networks.length; i++) {
|
||||
var n = d.networks[i];
|
||||
var sig = parseInt(n.signal) || 0;
|
||||
var sigColor = sig > 70 ? 'var(--success,#34c759)' : sig > 40 ? '#f59e0b' : 'var(--danger,#ff3b30)';
|
||||
html += '<tr><td><strong>' + escHtml(n.ssid) + '</strong></td>'
|
||||
+ '<td style="font-family:monospace;font-size:0.75rem">' + escHtml(n.bssid) + '</td>'
|
||||
+ '<td>' + escHtml(n.channel) + '</td>'
|
||||
+ '<td style="color:' + sigColor + '">' + escHtml(n.signal) + '%</td>'
|
||||
+ '<td>' + escHtml(n.security) + '</td>'
|
||||
+ '<td>' + escHtml(n.mode || '') + '</td></tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
results.innerHTML = html;
|
||||
halAnalyze('Network: WiFi Scan', JSON.stringify(d, null, 2), 'network scan', 'network');
|
||||
})
|
||||
.catch(function(e) { status.textContent = 'Request failed: ' + e.message; });
|
||||
}
|
||||
|
||||
// ── Attack Detection ─────────────────────────────────────────────────────────
|
||||
function detectAttacks() {
|
||||
var btn = document.getElementById('btn-detect-attacks');
|
||||
var status = document.getElementById('attack-detect-status');
|
||||
var results = document.getElementById('attack-results');
|
||||
btn.disabled = true; btn.textContent = 'Scanning…';
|
||||
status.textContent = 'Running 5 attack detection checks…';
|
||||
results.innerHTML = '';
|
||||
|
||||
fetch('/network/wifi/detect-attacks', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Run Attack Detection';
|
||||
if (!d.ok) { status.textContent = 'Error: ' + (d.error || 'Unknown'); return; }
|
||||
|
||||
var sev = d.severity || {};
|
||||
status.innerHTML = '<span style="color:var(--danger,#ff3b30)">' + (sev.critical || 0) + ' critical</span>'
|
||||
+ ' · <span style="color:#f59e0b">' + (sev.warning || 0) + ' warning</span>'
|
||||
+ ' · <span style="color:var(--success,#34c759)">' + (sev.clean || 0) + ' clean</span>';
|
||||
|
||||
var html = '';
|
||||
var findings = d.findings || [];
|
||||
for (var i = 0; i < findings.length; i++) {
|
||||
var f = findings[i];
|
||||
var color = f.severity === 'critical' ? 'var(--danger,#ff3b30)' : f.severity === 'warning' ? '#f59e0b' : 'var(--success,#34c759)';
|
||||
var icon = f.severity === 'critical' ? '⚠' : f.severity === 'warning' ? '⚠' : '✓';
|
||||
html += '<div style="border:1px solid ' + color + ';border-radius:var(--radius);padding:0.65rem 0.85rem;margin-bottom:0.5rem;background:var(--bg-card)">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center">'
|
||||
+ '<strong style="color:' + color + '">' + icon + ' ' + escHtml(f.check) + '</strong>'
|
||||
+ '<span style="font-size:0.72rem;padding:2px 8px;border-radius:3px;border:1px solid ' + color + ';color:' + color + '">' + f.severity + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:0.82rem;color:var(--text-secondary);margin-top:0.3rem">' + escHtml(f.description) + '</div>';
|
||||
if (f.details && f.details.length) {
|
||||
html += '<ul style="font-size:0.75rem;color:var(--text-muted);margin:0.4rem 0 0 1rem;padding:0">';
|
||||
for (var j = 0; j < f.details.length; j++) {
|
||||
html += '<li>' + escHtml(f.details[j]) + '</li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
results.innerHTML = html;
|
||||
halAnalyze('Network: Attack Detection', JSON.stringify(d, null, 2), 'network scan', 'network');
|
||||
})
|
||||
.catch(function(e) { btn.disabled = false; btn.textContent = 'Run Attack Detection'; status.textContent = 'Failed: ' + e.message; });
|
||||
}
|
||||
|
||||
// ── SSID Map ─────────────────────────────────────────────────────────────────
|
||||
function ssidMap() {
|
||||
var btn = document.getElementById('btn-ssid-map');
|
||||
var status = document.getElementById('ssid-map-status');
|
||||
var results = document.getElementById('ssid-map-results');
|
||||
btn.disabled = true; btn.textContent = 'Mapping…';
|
||||
status.textContent = 'Building SSID map…';
|
||||
results.innerHTML = '';
|
||||
|
||||
fetch('/network/wifi/ssid-map', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Build SSID Map';
|
||||
if (!d.ok) { status.textContent = 'Error: ' + (d.error || 'Unknown'); return; }
|
||||
status.textContent = d.total_ssids + ' SSIDs, ' + d.total_aps + ' access points';
|
||||
|
||||
var html = '';
|
||||
var ssids = d.ssids || [];
|
||||
for (var i = 0; i < ssids.length; i++) {
|
||||
var s = ssids[i];
|
||||
var aps = s.aps || [];
|
||||
html += '<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.65rem 0.85rem;margin-bottom:0.5rem;background:var(--bg-card)">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem">'
|
||||
+ '<strong style="color:var(--accent)">' + escHtml(s.ssid) + '</strong>'
|
||||
+ '<span style="font-size:0.72rem;color:var(--text-muted)">' + aps.length + ' AP(s) · ' + escHtml(s.security || '') + '</span>'
|
||||
+ '</div>';
|
||||
|
||||
if (aps.length > 0) {
|
||||
html += '<table style="width:100%;font-size:0.75rem;border-collapse:collapse">'
|
||||
+ '<tr style="color:var(--text-muted)"><td>BSSID</td><td>Ch</td><td>Signal</td><td>Security</td></tr>';
|
||||
for (var j = 0; j < aps.length; j++) {
|
||||
var a = aps[j];
|
||||
var sig = parseInt(a.signal) || 0;
|
||||
var sigColor = sig > 70 ? 'var(--success,#34c759)' : sig > 40 ? '#f59e0b' : 'var(--danger,#ff3b30)';
|
||||
html += '<tr><td style="font-family:monospace">' + escHtml(a.bssid) + '</td>'
|
||||
+ '<td>' + escHtml(a.channel) + '</td>'
|
||||
+ '<td style="color:' + sigColor + '">' + sig + '%</td>'
|
||||
+ '<td>' + escHtml(a.security || '') + '</td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
results.innerHTML = html || '<p style="color:var(--text-muted)">No SSIDs found.</p>';
|
||||
halAnalyze('Network: SSID Map', JSON.stringify(d, null, 2), 'network scan', 'network');
|
||||
})
|
||||
.catch(function(e) { btn.disabled = false; btn.textContent = 'Build SSID Map'; status.textContent = 'Failed: ' + e.message; });
|
||||
}
|
||||
|
||||
// ── ARP Spoof Detection & Remediation ────────────────────────────────────────
|
||||
function arpSpoofScan() {
|
||||
var btn = document.getElementById('btn-arp-scan');
|
||||
var status = document.getElementById('arp-scan-status');
|
||||
var results = document.getElementById('arp-scan-results');
|
||||
var tableDisplay = document.getElementById('arp-table-display');
|
||||
btn.disabled = true; btn.textContent = 'Scanning...';
|
||||
status.textContent = 'Checking ARP table and gateway...';
|
||||
results.innerHTML = ''; tableDisplay.innerHTML = '';
|
||||
|
||||
fetch('/network/arp-spoof/scan', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Scan for ARP Spoofing';
|
||||
if (!d.ok) { status.textContent = 'Error: ' + (d.error || 'Unknown'); return; }
|
||||
|
||||
// Gateway info
|
||||
var gw = d.gateway || {};
|
||||
var gwHtml = '<div style="font-size:0.82rem;color:var(--text-secondary);margin-bottom:0.5rem">'
|
||||
+ 'Gateway: <strong>' + escHtml(gw.ip || '?') + '</strong>'
|
||||
+ ' · MAC: <code>' + escHtml(gw.mac || '?') + '</code>'
|
||||
+ ' · Interface: ' + escHtml(gw.interface || '?')
|
||||
+ ' · Baseline: ' + (d.has_baseline ? '<span style="color:var(--success,#34c759)">saved</span>' : '<span style="color:#f59e0b">not set</span>')
|
||||
+ '</div>';
|
||||
|
||||
// Severity summary
|
||||
var sevColors = {critical: 'var(--danger,#ff3b30)', warning: '#f59e0b', clean: 'var(--success,#34c759)'};
|
||||
status.innerHTML = '<span style="color:' + (sevColors[d.severity] || 'var(--text-muted)') + ';font-weight:700">'
|
||||
+ d.severity.toUpperCase() + '</span> — ' + (d.findings || []).length + ' finding(s)';
|
||||
|
||||
// Findings
|
||||
var html = gwHtml;
|
||||
var findings = d.findings || [];
|
||||
if (findings.length === 0) {
|
||||
html += '<div style="padding:0.5rem 0.75rem;border:1px solid var(--success,#34c759);border-radius:var(--radius);color:var(--success,#34c759);font-size:0.82rem">'
|
||||
+ '✓ No ARP spoofing detected. Your ARP table looks clean.</div>';
|
||||
}
|
||||
for (var i = 0; i < findings.length; i++) {
|
||||
var f = findings[i];
|
||||
var fc = f.severity === 'critical' ? 'var(--danger,#ff3b30)' : '#f59e0b';
|
||||
html += '<div style="border:1px solid ' + fc + ';border-radius:var(--radius);padding:0.6rem 0.8rem;margin-bottom:0.5rem;background:var(--bg-card)">'
|
||||
+ '<strong style="color:' + fc + '">⚠ ' + escHtml(f.message) + '</strong>'
|
||||
+ '<div style="font-size:0.78rem;color:var(--text-secondary);margin-top:0.25rem">' + escHtml(f.detail || '') + '</div>';
|
||||
if (f.fix) {
|
||||
html += '<div style="font-size:0.75rem;margin-top:0.3rem"><strong>Fix:</strong> <code>' + escHtml(f.fix) + '</code></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
results.innerHTML = html;
|
||||
|
||||
// ARP table
|
||||
var entries = d.arp_table || [];
|
||||
if (entries.length) {
|
||||
var thtml = '<table class="data-table" style="font-size:0.8rem"><thead><tr><th>IP</th><th>MAC</th><th>State</th><th>Action</th></tr></thead><tbody>';
|
||||
for (var j = 0; j < entries.length; j++) {
|
||||
var e = entries[j];
|
||||
thtml += '<tr><td>' + escHtml(e.ip) + '</td><td style="font-family:monospace">' + escHtml(e.mac) + '</td>'
|
||||
+ '<td>' + escHtml(e.state) + '</td>'
|
||||
+ '<td><button class="btn btn-sm" style="font-size:0.65rem;padding:1px 6px" '
|
||||
+ 'onclick="arpFlushOne(\'' + escHtml(e.ip) + '\')">Flush</button></td></tr>';
|
||||
}
|
||||
thtml += '</tbody></table>';
|
||||
tableDisplay.innerHTML = thtml;
|
||||
}
|
||||
|
||||
halAnalyze('Network: ARP Spoof Scan', JSON.stringify(d, null, 2), 'ARP spoofing detection', 'network');
|
||||
})
|
||||
.catch(function(e) { btn.disabled = false; btn.textContent = 'Scan for ARP Spoofing'; status.textContent = 'Failed: ' + e.message; });
|
||||
}
|
||||
|
||||
function arpSaveBaseline() {
|
||||
var btn = document.getElementById('btn-arp-baseline');
|
||||
btn.disabled = true; btn.textContent = 'Saving...';
|
||||
fetch('/network/arp-spoof/save-baseline', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
btn.disabled = false; btn.textContent = 'Save Current as Baseline';
|
||||
if (d.ok) {
|
||||
alert('Baseline saved! Gateway MAC: ' + (d.gateway_mac || 'unknown') + ', ' + d.entries + ' entries stored.');
|
||||
} else {
|
||||
alert('Error saving baseline');
|
||||
}
|
||||
})
|
||||
.catch(function(e) { btn.disabled = false; btn.textContent = 'Save Current as Baseline'; });
|
||||
}
|
||||
|
||||
function arpFixStatic() {
|
||||
var ip = document.getElementById('arp-fix-ip').value.trim();
|
||||
var mac = document.getElementById('arp-fix-mac').value.trim();
|
||||
if (!ip || !mac) { alert('Enter both IP and MAC'); return; }
|
||||
_arpFix('flush_and_static', {ip: ip, mac: mac});
|
||||
}
|
||||
|
||||
function arpEnableProtection() {
|
||||
_arpFix('enable_arp_protection', {});
|
||||
}
|
||||
|
||||
function arpFlushEntry() {
|
||||
var ip = document.getElementById('arp-flush-ip').value.trim();
|
||||
if (!ip) { alert('Enter an IP'); return; }
|
||||
_arpFix('flush_entry', {ip: ip});
|
||||
}
|
||||
|
||||
function arpFlushOne(ip) {
|
||||
_arpFix('flush_entry', {ip: ip});
|
||||
}
|
||||
|
||||
function _arpFix(action, extra) {
|
||||
var el = document.getElementById('arp-fix-results');
|
||||
el.innerHTML = '<span style="color:var(--text-muted)">Applying fix...</span>';
|
||||
var payload = Object.assign({action: action}, extra);
|
||||
fetch('/network/arp-spoof/fix', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (!d.ok) { el.innerHTML = '<span style="color:var(--danger,#ff3b30)">Error: ' + escHtml(d.error || 'Unknown') + '</span>'; return; }
|
||||
var html = '';
|
||||
var results = d.results || [];
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var r = results[i];
|
||||
var color = r.ok ? 'var(--success,#34c759)' : 'var(--danger,#ff3b30)';
|
||||
html += '<div style="font-size:0.78rem;margin-bottom:0.3rem">'
|
||||
+ '<span style="color:' + color + '">' + (r.ok ? '✓' : '✕') + '</span> '
|
||||
+ '<code>' + escHtml(r.cmd) + '</code>';
|
||||
if (r.output) html += ' <span style="color:var(--text-muted)">' + escHtml(r.output.trim()) + '</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
el.innerHTML = html || '<span style="color:var(--success,#34c759)">Done</span>';
|
||||
})
|
||||
.catch(function(e) { el.innerHTML = '<span style="color:var(--danger,#ff3b30)">Failed: ' + e.message + '</span>'; });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -226,6 +226,7 @@ function identifyHash(){
|
||||
${r.types.length?r.types.map(t=>`<span class="conf-${t.confidence}">${t.confidence.toUpperCase()}</span> ${t.name} `).join(' | '):'<span style="color:var(--text-muted)">Unknown</span>'}
|
||||
</div>`).join('');
|
||||
}
|
||||
halAnalyze('Password Toolkit', JSON.stringify(d, null, 2), 'password analysis', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,6 +238,7 @@ function hashString(){
|
||||
document.getElementById('hash-result').innerHTML=d.ok?
|
||||
`<strong>${d.algorithm}:</strong> ${d.hash} <span class="copy-btn" onclick="navigator.clipboard.writeText('${d.hash}')">[copy]</span>`
|
||||
:`Error: ${d.error}`;
|
||||
halAnalyze('Password Toolkit', JSON.stringify(d, null, 2), 'password analysis', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,6 +270,7 @@ function startCrack(){
|
||||
} else {
|
||||
div.innerHTML=`<div style="color:var(--text-muted)">${esc(d.message||d.error||'No result')}</div>`;
|
||||
}
|
||||
halAnalyze('Password Toolkit', JSON.stringify(d, null, 2), 'password analysis', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -298,6 +301,7 @@ function generatePw(){
|
||||
<span style="font-size:0.75rem;color:var(--text-muted)">${p.entropy} bits — ${p.strength}</span>
|
||||
<div class="strength-bar str-${p.strength}" style="width:100px"><div class="strength-fill"></div></div>
|
||||
</div>`).join('');
|
||||
halAnalyze('Password Toolkit', JSON.stringify(d, null, 2), 'password analysis', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ function startScan() {
|
||||
document.getElementById('ps-cancel-btn').style.display = '';
|
||||
appendLine('info', `► Scan started | job=${d.job_id} | ${d.port_count} ports queued`);
|
||||
openStream(d.job_id);
|
||||
halAnalyze('Port Scanner', JSON.stringify(d, null, 2), 'port scan', 'network');
|
||||
}).catch(e => appendLine('error', '✗ ' + e.message));
|
||||
}
|
||||
|
||||
|
||||
722
web/templates/remote_monitor.html
Normal file
722
web/templates/remote_monitor.html
Normal file
@@ -0,0 +1,722 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Remote Monitor Station - AUTARCH{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Remote Monitoring Station</h1>
|
||||
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||||
Connect and control remote WiFi monitoring devices via .piap profiles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Device Selector -->
|
||||
<div class="section" style="margin-bottom:1rem">
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
|
||||
<select id="piap-select" onchange="rmLoadPiap()" style="min-width:200px;padding:0.4rem">
|
||||
<option value="">— Select Device —</option>
|
||||
{% for p in piap_files %}
|
||||
<option value="{{ p.filename }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
<option value="__new__">+ Create New</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" id="rm-connect-btn" onclick="rmConnect()" disabled>Connect</button>
|
||||
<button class="btn" id="rm-disconnect-btn" onclick="rmDisconnect()" disabled style="display:none">Disconnect</button>
|
||||
<span id="rm-conn-status" style="font-size:0.85rem;color:var(--text-secondary)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Panel (hidden until loaded) -->
|
||||
<div id="rm-device-panel" style="display:none">
|
||||
|
||||
<!-- Device Info Bar -->
|
||||
<div class="section" style="margin-bottom:1rem;padding:0.5rem 1rem;display:flex;gap:2rem;flex-wrap:wrap;font-size:0.85rem" id="rm-device-info-bar">
|
||||
<span><strong>Model:</strong> <span id="rm-model"></span></span>
|
||||
<span><strong>OS:</strong> <span id="rm-os"></span></span>
|
||||
<span><strong>Chipset:</strong> <span id="rm-chipset"></span></span>
|
||||
<span><strong>Host:</strong> <span id="rm-host"></span></span>
|
||||
</div>
|
||||
|
||||
<!-- Sub-tab bar (built dynamically from .piap) -->
|
||||
<div class="tab-bar" id="rm-tab-bar">
|
||||
<button class="tab active" onclick="rmTab('radios')">Radios</button>
|
||||
<!-- Additional tabs built from features -->
|
||||
</div>
|
||||
|
||||
<!-- ==================== RADIOS TAB ==================== -->
|
||||
<div class="rm-tab-panel" id="rm-tab-radios">
|
||||
<div id="rm-radios-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== CAPTURE TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-capture">
|
||||
<div class="section">
|
||||
<h2>Packet Capture</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-cap-radio" style="padding:0.4rem"></select>
|
||||
<button class="btn btn-primary" onclick="rmCaptureStart()">Start Capture</button>
|
||||
<button class="btn btn-danger" onclick="rmCaptureStop()">Stop Capture</button>
|
||||
</div>
|
||||
<div id="rm-capture-status" style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:0.5rem"></div>
|
||||
<pre id="rm-capture-output" style="max-height:400px;overflow:auto;background:var(--bg-secondary);padding:1rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== SCAN TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-scan">
|
||||
<div class="section">
|
||||
<h2>WiFi Scan</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-scan-radio" style="padding:0.4rem"></select>
|
||||
<button class="btn btn-primary" onclick="rmWifiScan()">Scan</button>
|
||||
</div>
|
||||
<div id="rm-scan-status" style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:0.5rem"></div>
|
||||
<pre id="rm-scan-output" style="max-height:400px;overflow:auto;background:var(--bg-secondary);padding:1rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== DEAUTH TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-deauth">
|
||||
<div class="section">
|
||||
<h2>Deauthentication</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-deauth-radio" style="padding:0.4rem"></select>
|
||||
<label>BSSID:</label>
|
||||
<input type="text" id="rm-deauth-bssid" placeholder="AA:BB:CC:DD:EE:FF" style="padding:0.4rem;width:180px">
|
||||
<label>Count:</label>
|
||||
<input type="number" id="rm-deauth-count" value="10" min="1" max="1000" style="padding:0.4rem;width:60px">
|
||||
<button class="btn btn-danger" onclick="rmDeauth()">Send Deauth</button>
|
||||
</div>
|
||||
<pre id="rm-deauth-output" style="max-height:300px;overflow:auto;background:var(--bg-secondary);padding:1rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== CHANNEL HOP TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-chanhop">
|
||||
<div class="section">
|
||||
<h2>Channel Hopper</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-hop-radio" style="padding:0.4rem"></select>
|
||||
<label>Dwell (ms):</label>
|
||||
<input type="number" id="rm-hop-dwell" value="500" min="100" max="5000" step="100" style="padding:0.4rem;width:80px">
|
||||
<button class="btn btn-primary" onclick="rmHopStart()">Start Hopping</button>
|
||||
<button class="btn btn-danger" onclick="rmHopStop()">Stop</button>
|
||||
</div>
|
||||
<div style="margin-bottom:0.5rem">
|
||||
<label>Channels (comma separated, leave blank for all):</label>
|
||||
<input type="text" id="rm-hop-channels" placeholder="1,6,11 or blank for all" style="width:100%;padding:0.4rem;margin-top:0.25rem">
|
||||
</div>
|
||||
<pre id="rm-hop-output" style="max-height:200px;overflow:auto;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== AIRODUMP TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-airodump">
|
||||
<div class="section">
|
||||
<h2>Airodump-ng</h2>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-airo-radio" style="padding:0.4rem"></select>
|
||||
<label>Channel:</label>
|
||||
<input type="text" id="rm-airo-channel" placeholder="all or specific" value="" style="padding:0.4rem;width:80px">
|
||||
<label>Band:</label>
|
||||
<select id="rm-airo-band" style="padding:0.4rem">
|
||||
<option value="">All</option>
|
||||
<option value="--band a">5GHz</option>
|
||||
<option value="--band bg">2.4GHz</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" onclick="rmAirodumpStart()">Start</button>
|
||||
<button class="btn btn-danger" onclick="rmAirodumpStop()">Stop</button>
|
||||
<button class="btn" onclick="rmAirodumpRefresh()">Refresh</button>
|
||||
</div>
|
||||
<div style="margin-bottom:0.5rem;font-size:0.85rem;color:var(--text-secondary)" id="rm-airo-status"></div>
|
||||
<h3>Access Points</h3>
|
||||
<div id="rm-airo-aps" style="overflow-x:auto;margin-bottom:1rem"></div>
|
||||
<h3>Clients</h3>
|
||||
<div id="rm-airo-clients" style="overflow-x:auto"></div>
|
||||
<pre id="rm-airo-raw" style="max-height:300px;overflow:auto;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem;display:none"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== HANDSHAKE TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-handshake">
|
||||
<div class="section">
|
||||
<h2>Handshake Capture</h2>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Capture WPA/WPA2 handshakes for offline cracking. Optionally deauth clients to force reconnection.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-hs-radio" style="padding:0.4rem"></select>
|
||||
<label>Target BSSID:</label>
|
||||
<input type="text" id="rm-hs-bssid" placeholder="AA:BB:CC:DD:EE:FF" style="padding:0.4rem;width:180px">
|
||||
<label>Channel:</label>
|
||||
<input type="text" id="rm-hs-channel" placeholder="6" style="padding:0.4rem;width:50px">
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<button class="btn btn-primary" onclick="rmHsCapture()">Start Capture</button>
|
||||
<button class="btn btn-danger" onclick="rmHsDeauth()">Deauth + Capture</button>
|
||||
<button class="btn" onclick="rmHsStop()">Stop</button>
|
||||
<button class="btn" onclick="rmHsPull()">Pull Capture File</button>
|
||||
<label><input type="checkbox" id="rm-hs-auto-deauth" checked> Auto-deauth clients</label>
|
||||
</div>
|
||||
<div id="rm-hs-status" style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:0.5rem"></div>
|
||||
<pre id="rm-hs-output" style="max-height:300px;overflow:auto;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== RECON TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-recon">
|
||||
<div class="section">
|
||||
<h2>Wireless Recon</h2>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Passive reconnaissance — discover APs, clients, probe requests, and hidden networks.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-recon-radio" style="padding:0.4rem"></select>
|
||||
<label>Duration (sec):</label>
|
||||
<input type="number" id="rm-recon-duration" value="30" min="5" max="300" style="padding:0.4rem;width:60px">
|
||||
<button class="btn btn-primary" onclick="rmReconStart()">Start Recon</button>
|
||||
<button class="btn btn-danger" onclick="rmReconStop()">Stop</button>
|
||||
</div>
|
||||
<div id="rm-recon-status" style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:0.5rem"></div>
|
||||
<h3>Discovered Networks</h3>
|
||||
<div id="rm-recon-networks" style="overflow-x:auto;margin-bottom:1rem"></div>
|
||||
<h3>Probe Requests</h3>
|
||||
<div id="rm-recon-probes" style="overflow-x:auto;margin-bottom:1rem"></div>
|
||||
<h3>Raw Output</h3>
|
||||
<pre id="rm-recon-raw" style="max-height:300px;overflow:auto;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== INJECTION TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-injection">
|
||||
<div class="section">
|
||||
<h2>Frame Injection</h2>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">
|
||||
Test frame injection capability and send custom frames.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<label>Radio:</label>
|
||||
<select id="rm-inj-radio" style="padding:0.4rem"></select>
|
||||
<button class="btn btn-primary" onclick="rmInjTest()">Test Injection</button>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem">
|
||||
<label>Custom aireplay-ng command:</label>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:0.25rem">
|
||||
<input type="text" id="rm-inj-cmd" placeholder="--fakeauth 0 -a AA:BB:CC:DD:EE:FF" style="flex:1;padding:0.4rem">
|
||||
<button class="btn" onclick="rmInjCustom()">Run</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="rm-inj-output" style="max-height:300px;overflow:auto;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== TERMINAL TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-terminal">
|
||||
<div class="section">
|
||||
<h2>Remote Terminal</h2>
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<input type="text" id="rm-cmd-input" placeholder="Command..." style="flex:1;padding:0.4rem" onkeypress="if(event.key==='Enter')rmExec()">
|
||||
<button class="btn btn-primary" onclick="rmExec()">Run</button>
|
||||
</div>
|
||||
<pre id="rm-terminal-output" style="max-height:500px;overflow:auto;background:var(--bg-secondary);padding:1rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== INFO TAB ==================== -->
|
||||
<div class="rm-tab-panel hidden" id="rm-tab-info">
|
||||
<div class="section">
|
||||
<h2>Device Info</h2>
|
||||
<button class="btn" onclick="rmGetInfo()" style="margin-bottom:1rem">Refresh</button>
|
||||
<div id="rm-info-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Create New Dialog -->
|
||||
<div id="rm-new-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:999;display:none;align-items:center;justify-content:center">
|
||||
<div style="background:var(--bg-primary);padding:2rem;border-radius:8px;max-width:500px;width:90%">
|
||||
<h2 style="margin-top:0">Create New Device Profile</h2>
|
||||
<p>To add a new device:</p>
|
||||
<ol>
|
||||
<li>Navigate to <code>data/piap/</code></li>
|
||||
<li>Copy <code>template.piap</code> to a new file</li>
|
||||
<li>Edit the file with your device's information</li>
|
||||
<li>Fill in all fields marked <code>CHANGEME</code></li>
|
||||
<li>Save and refresh this page</li>
|
||||
</ol>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary)">
|
||||
Template location: <code>/data/piap/template.piap</code>
|
||||
</p>
|
||||
<button class="btn" onclick="document.getElementById('rm-new-dialog').style.display='none'">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let rmPiap = null;
|
||||
let rmConnected = false;
|
||||
|
||||
function rmTab(name) {
|
||||
document.querySelectorAll('.rm-tab-panel').forEach(p => p.classList.add('hidden'));
|
||||
document.querySelectorAll('#rm-tab-bar .tab').forEach(t => t.classList.remove('active'));
|
||||
const panel = document.getElementById('rm-tab-' + name);
|
||||
if (panel) panel.classList.remove('hidden');
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
function rmLoadPiap() {
|
||||
const sel = document.getElementById('piap-select');
|
||||
const filename = sel.value;
|
||||
|
||||
if (filename === '__new__') {
|
||||
document.getElementById('rm-new-dialog').style.display = 'flex';
|
||||
sel.value = '';
|
||||
return;
|
||||
}
|
||||
if (!filename) {
|
||||
document.getElementById('rm-device-panel').style.display = 'none';
|
||||
document.getElementById('rm-connect-btn').disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/remote-monitor/api/load', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({filename: filename})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.ok) { alert(data.error); return; }
|
||||
rmPiap = data.data;
|
||||
rmBuildUI();
|
||||
document.getElementById('rm-connect-btn').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function rmBuildUI() {
|
||||
const d = rmPiap.device;
|
||||
document.getElementById('rm-model').textContent = d.model || '';
|
||||
document.getElementById('rm-os').textContent = d.os || '';
|
||||
document.getElementById('rm-chipset').textContent = d.wifi_chipset || d.chipset || '';
|
||||
document.getElementById('rm-host').textContent = rmPiap.connection.host || '';
|
||||
|
||||
// Build tab bar
|
||||
const tabBar = document.getElementById('rm-tab-bar');
|
||||
tabBar.innerHTML = '<button class="tab active" onclick="rmTab(\'radios\')">Radios</button>';
|
||||
const features = rmPiap.features;
|
||||
if (features.capture === 'true') tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'capture\')">Capture</button>';
|
||||
if (features.wifi_scan === 'true') tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'scan\')">WiFi Scan</button>';
|
||||
tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'recon\')">Recon</button>';
|
||||
if (features.aircrack === 'true') tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'airodump\')">Airodump</button>';
|
||||
tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'handshake\')">Handshake</button>';
|
||||
if (features.deauth === 'true') tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'deauth\')">Deauth</button>';
|
||||
if (features.channel_hop === 'true') tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'chanhop\')">Channel Hop</button>';
|
||||
tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'injection\')">Injection</button>';
|
||||
tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'terminal\')">Terminal</button>';
|
||||
tabBar.innerHTML += '<button class="tab" onclick="rmTab(\'info\')">Info</button>';
|
||||
|
||||
// Build radio panels
|
||||
const container = document.getElementById('rm-radios-container');
|
||||
container.innerHTML = '';
|
||||
rmPiap.radios.forEach((radio, idx) => {
|
||||
const channels = (radio.channel_list || []).map(c =>
|
||||
`<option value="${c}" ${c === radio.default_channel ? 'selected' : ''}>${c}</option>`
|
||||
).join('');
|
||||
const modes = (radio.mode_list || []).map(m =>
|
||||
`<span style="background:var(--bg-tertiary);padding:2px 8px;border-radius:3px;font-size:0.8rem">${m}</span>`
|
||||
).join(' ');
|
||||
|
||||
container.innerHTML += `
|
||||
<div class="section" style="margin-bottom:1rem">
|
||||
<h2>${radio.name || 'Radio ' + idx}</h2>
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:0.5rem">
|
||||
<span><strong>PHY:</strong> ${radio.phy}</span>
|
||||
<span><strong>Band:</strong> ${radio.band || ''}</span>
|
||||
<span><strong>Modes:</strong> ${modes}</span>
|
||||
<span><strong>Injection:</strong> ${radio.injection === 'true' ? 'Yes' : 'No'}</span>
|
||||
<span><strong>Radiotap:</strong> ${radio.radiotap === 'true' ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.5rem">
|
||||
<label>Channel:</label>
|
||||
<select id="rm-radio-${idx}-ch" style="padding:0.4rem">${channels}</select>
|
||||
<button class="btn btn-primary" onclick="rmMonitorOn(${idx})">Monitor ON</button>
|
||||
<button class="btn btn-danger" onclick="rmMonitorOff(${idx})">Monitor OFF</button>
|
||||
<button class="btn" onclick="rmSetChannel(${idx})">Set Channel</button>
|
||||
<button class="btn" onclick="rmRadioStatus(${idx})">Status</button>
|
||||
</div>
|
||||
<pre id="rm-radio-${idx}-output" style="max-height:200px;overflow:auto;background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem"></pre>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Populate radio selects in feature tabs
|
||||
const radioOpts = rmPiap.radios.map((r, i) =>
|
||||
`<option value="${i}">${r.name || 'Radio ' + i}</option>`
|
||||
).join('');
|
||||
['rm-cap-radio', 'rm-scan-radio', 'rm-deauth-radio', 'rm-hop-radio', 'rm-airo-radio', 'rm-hs-radio', 'rm-recon-radio', 'rm-inj-radio'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = radioOpts;
|
||||
});
|
||||
|
||||
document.getElementById('rm-device-panel').style.display = 'block';
|
||||
}
|
||||
|
||||
function _rmPost(url, extra) {
|
||||
const body = {connection: rmPiap.connection, features: rmPiap.features, ...extra};
|
||||
return fetch('/remote-monitor/api/' + url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
function rmConnect() {
|
||||
document.getElementById('rm-conn-status').textContent = 'Connecting...';
|
||||
_rmPost('connect', {}).then(data => {
|
||||
if (data.ok) {
|
||||
rmConnected = true;
|
||||
document.getElementById('rm-conn-status').innerHTML = '<span style="color:#4caf50">Connected</span>';
|
||||
document.getElementById('rm-connect-btn').style.display = 'none';
|
||||
document.getElementById('rm-disconnect-btn').style.display = '';
|
||||
document.getElementById('rm-disconnect-btn').disabled = false;
|
||||
} else {
|
||||
document.getElementById('rm-conn-status').innerHTML = '<span style="color:#f44336">Failed: ' + (data.stderr || 'unknown') + '</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function rmDisconnect() {
|
||||
rmConnected = false;
|
||||
document.getElementById('rm-conn-status').textContent = 'Disconnected';
|
||||
document.getElementById('rm-connect-btn').style.display = '';
|
||||
document.getElementById('rm-connect-btn').disabled = false;
|
||||
document.getElementById('rm-disconnect-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function rmMonitorOn(idx) {
|
||||
const channel = document.getElementById('rm-radio-' + idx + '-ch').value;
|
||||
const out = document.getElementById('rm-radio-' + idx + '-output');
|
||||
out.textContent = 'Enabling monitor mode...';
|
||||
_rmPost('radio/monitor-on', {radio: rmPiap.radios[idx], channel: channel}).then(data => {
|
||||
out.textContent = data.ok ? 'Monitor mode enabled on channel ' + channel : 'Error: ' + (data.stderr || data.stdout);
|
||||
});
|
||||
}
|
||||
|
||||
function rmMonitorOff(idx) {
|
||||
const out = document.getElementById('rm-radio-' + idx + '-output');
|
||||
out.textContent = 'Disabling monitor mode...';
|
||||
_rmPost('radio/monitor-off', {radio: rmPiap.radios[idx]}).then(data => {
|
||||
out.textContent = data.ok ? 'Monitor mode disabled' : 'Error: ' + (data.stderr || data.stdout);
|
||||
});
|
||||
}
|
||||
|
||||
function rmSetChannel(idx) {
|
||||
const channel = document.getElementById('rm-radio-' + idx + '-ch').value;
|
||||
const out = document.getElementById('rm-radio-' + idx + '-output');
|
||||
_rmPost('radio/set-channel', {radio: rmPiap.radios[idx], channel: channel}).then(data => {
|
||||
out.textContent = data.ok ? 'Channel set to ' + channel : 'Error: ' + (data.stderr || data.stdout);
|
||||
});
|
||||
}
|
||||
|
||||
function rmRadioStatus(idx) {
|
||||
const out = document.getElementById('rm-radio-' + idx + '-output');
|
||||
out.textContent = 'Getting status...';
|
||||
_rmPost('radio/status', {radio: rmPiap.radios[idx]}).then(data => {
|
||||
out.textContent = data.stdout || data.stderr || 'No output';
|
||||
});
|
||||
}
|
||||
|
||||
function rmCaptureStart() {
|
||||
const idx = document.getElementById('rm-cap-radio').value;
|
||||
document.getElementById('rm-capture-status').textContent = 'Starting capture...';
|
||||
_rmPost('capture/start', {radio: rmPiap.radios[idx]}).then(data => {
|
||||
document.getElementById('rm-capture-status').textContent = data.ok ? 'Capture running' : 'Error: ' + (data.stderr || '');
|
||||
});
|
||||
}
|
||||
|
||||
function rmCaptureStop() {
|
||||
document.getElementById('rm-capture-status').textContent = 'Stopping...';
|
||||
_rmPost('capture/stop', {}).then(data => {
|
||||
document.getElementById('rm-capture-status').textContent = 'Capture stopped';
|
||||
});
|
||||
}
|
||||
|
||||
function rmWifiScan() {
|
||||
const idx = document.getElementById('rm-scan-radio').value;
|
||||
const out = document.getElementById('rm-scan-output');
|
||||
document.getElementById('rm-scan-status').textContent = 'Scanning...';
|
||||
out.textContent = '';
|
||||
_rmPost('scan', {radio: rmPiap.radios[idx]}).then(data => {
|
||||
document.getElementById('rm-scan-status').textContent = data.ok ? 'Scan complete' : 'Error';
|
||||
out.textContent = data.stdout || data.stderr || 'No results';
|
||||
});
|
||||
}
|
||||
|
||||
function rmDeauth() {
|
||||
const idx = document.getElementById('rm-deauth-radio').value;
|
||||
const bssid = document.getElementById('rm-deauth-bssid').value;
|
||||
const count = document.getElementById('rm-deauth-count').value;
|
||||
if (!bssid) { alert('Enter a BSSID'); return; }
|
||||
_rmPost('deauth', {radio: rmPiap.radios[idx], bssid: bssid, count: count}).then(data => {
|
||||
document.getElementById('rm-deauth-output').textContent = data.stdout || data.stderr || 'Sent';
|
||||
});
|
||||
}
|
||||
|
||||
function rmExec() {
|
||||
const cmd = document.getElementById('rm-cmd-input').value;
|
||||
if (!cmd) return;
|
||||
const out = document.getElementById('rm-terminal-output');
|
||||
out.textContent += '$ ' + cmd + '\n';
|
||||
_rmPost('exec', {cmd: cmd}).then(data => {
|
||||
out.textContent += (data.stdout || data.stderr || '') + '\n';
|
||||
out.scrollTop = out.scrollHeight;
|
||||
});
|
||||
document.getElementById('rm-cmd-input').value = '';
|
||||
}
|
||||
|
||||
function rmGetInfo() {
|
||||
const out = document.getElementById('rm-info-output');
|
||||
out.innerHTML = '<em>Loading...</em>';
|
||||
_rmPost('info', {info: rmPiap.info}).then(data => {
|
||||
if (!data.ok) { out.textContent = 'Error'; return; }
|
||||
let html = '';
|
||||
for (const [key, val] of Object.entries(data.info)) {
|
||||
html += `<div class="section" style="margin-bottom:0.5rem"><h3>${key}</h3><pre style="background:var(--bg-secondary);padding:0.5rem;border-radius:4px;font-size:0.8rem">${val}</pre></div>`;
|
||||
}
|
||||
out.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Channel Hopper ──────────────────────────────────────────────────────
|
||||
let rmHopRunning = false;
|
||||
function rmHopStart() {
|
||||
const idx = document.getElementById('rm-hop-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const dwell = document.getElementById('rm-hop-dwell').value;
|
||||
const customCh = document.getElementById('rm-hop-channels').value.trim();
|
||||
const channels = customCh || radio.channels;
|
||||
const out = document.getElementById('rm-hop-output');
|
||||
out.textContent = 'Starting channel hopper...';
|
||||
|
||||
const cmd = `sh -c 'while true; do for ch in ${channels.replace(/,/g, ' ')}; do iw dev ${radio.monitor_interface} set channel $ch 2>/dev/null && echo "Channel: $ch"; sleep ${dwell / 1000}; done; done' &`;
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
rmHopRunning = true;
|
||||
out.textContent = 'Hopping across channels: ' + channels;
|
||||
});
|
||||
}
|
||||
|
||||
function rmHopStop() {
|
||||
_rmPost('exec', {cmd: "pkill -f 'while true; do for ch'"}).then(data => {
|
||||
rmHopRunning = false;
|
||||
document.getElementById('rm-hop-output').textContent = 'Stopped';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Airodump ────────────────────────────────────────────────────────────
|
||||
function rmAirodumpStart() {
|
||||
const idx = document.getElementById('rm-airo-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const channel = document.getElementById('rm-airo-channel').value.trim();
|
||||
const band = document.getElementById('rm-airo-band').value;
|
||||
document.getElementById('rm-airo-status').textContent = 'Starting airodump-ng...';
|
||||
|
||||
let cmd = `airodump-ng ${radio.monitor_interface} --write /tmp/airo --output-format csv -w /tmp/airo --write-interval 2`;
|
||||
if (channel) cmd += ` -c ${channel}`;
|
||||
if (band) cmd += ` ${band}`;
|
||||
cmd += ' &';
|
||||
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
document.getElementById('rm-airo-status').textContent = 'Airodump running. Click Refresh to see results.';
|
||||
});
|
||||
}
|
||||
|
||||
function rmAirodumpStop() {
|
||||
_rmPost('exec', {cmd: 'killall airodump-ng 2>/dev/null'}).then(data => {
|
||||
document.getElementById('rm-airo-status').textContent = 'Stopped';
|
||||
});
|
||||
}
|
||||
|
||||
function rmAirodumpRefresh() {
|
||||
_rmPost('exec', {cmd: 'cat /tmp/airo-01.csv 2>/dev/null || echo "No data yet"'}).then(data => {
|
||||
const raw = data.stdout || '';
|
||||
document.getElementById('rm-airo-raw').textContent = raw;
|
||||
document.getElementById('rm-airo-raw').style.display = 'block';
|
||||
rmParseAirodump(raw);
|
||||
});
|
||||
}
|
||||
|
||||
function rmParseAirodump(csv) {
|
||||
const lines = csv.split('\n');
|
||||
let aps = [], clients = [], section = 'ap';
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.includes('Station MAC')) { section = 'client'; return; }
|
||||
if (line.trim() === '' || line.startsWith('BSSID') || line.startsWith('\r')) return;
|
||||
const cols = line.split(',').map(c => c.trim());
|
||||
if (section === 'ap' && cols.length >= 14) {
|
||||
aps.push({bssid: cols[0], power: cols[8], channel: cols[3], enc: cols[5], essid: cols[13]});
|
||||
} else if (section === 'client' && cols.length >= 6) {
|
||||
clients.push({mac: cols[0], power: cols[3], bssid: cols[5], probes: cols[6] || ''});
|
||||
}
|
||||
});
|
||||
|
||||
let apHtml = '<table style="width:100%;font-size:0.8rem"><tr><th>BSSID</th><th>Ch</th><th>Pwr</th><th>Enc</th><th>ESSID</th></tr>';
|
||||
aps.forEach(a => { apHtml += `<tr><td>${a.bssid}</td><td>${a.channel}</td><td>${a.power}</td><td>${a.enc}</td><td>${a.essid}</td></tr>`; });
|
||||
apHtml += '</table>';
|
||||
document.getElementById('rm-airo-aps').innerHTML = aps.length ? apHtml : '<em>No APs found</em>';
|
||||
|
||||
let clHtml = '<table style="width:100%;font-size:0.8rem"><tr><th>MAC</th><th>Pwr</th><th>BSSID</th><th>Probes</th></tr>';
|
||||
clients.forEach(c => { clHtml += `<tr><td>${c.mac}</td><td>${c.power}</td><td>${c.bssid}</td><td>${c.probes}</td></tr>`; });
|
||||
clHtml += '</table>';
|
||||
document.getElementById('rm-airo-clients').innerHTML = clients.length ? clHtml : '<em>No clients found</em>';
|
||||
}
|
||||
|
||||
// ── Handshake Capture ───────────────────────────────────────────────────
|
||||
function rmHsCapture() {
|
||||
const idx = document.getElementById('rm-hs-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const bssid = document.getElementById('rm-hs-bssid').value;
|
||||
const channel = document.getElementById('rm-hs-channel').value;
|
||||
if (!bssid) { alert('Enter target BSSID'); return; }
|
||||
|
||||
const out = document.getElementById('rm-hs-output');
|
||||
document.getElementById('rm-hs-status').textContent = 'Starting handshake capture...';
|
||||
|
||||
let cmd = `iw dev ${radio.monitor_interface} set channel ${channel} 2>/dev/null; airodump-ng ${radio.monitor_interface} --bssid ${bssid} -c ${channel} --write /tmp/handshake --output-format pcap -w /tmp/handshake &`;
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
document.getElementById('rm-hs-status').textContent = 'Capturing on channel ' + channel + ' for ' + bssid;
|
||||
out.textContent = 'Waiting for handshake...\n';
|
||||
|
||||
if (document.getElementById('rm-hs-auto-deauth').checked) {
|
||||
setTimeout(() => rmHsDeauth(), 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function rmHsDeauth() {
|
||||
const idx = document.getElementById('rm-hs-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const bssid = document.getElementById('rm-hs-bssid').value;
|
||||
if (!bssid) { alert('Enter target BSSID'); return; }
|
||||
|
||||
document.getElementById('rm-hs-status').textContent = 'Sending deauth to force reconnection...';
|
||||
const cmd = `aireplay-ng --deauth 5 -a ${bssid} ${radio.monitor_interface}`;
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
document.getElementById('rm-hs-output').textContent += (data.stdout || data.stderr || '') + '\n';
|
||||
});
|
||||
}
|
||||
|
||||
function rmHsStop() {
|
||||
_rmPost('exec', {cmd: 'killall airodump-ng 2>/dev/null; killall aireplay-ng 2>/dev/null'}).then(data => {
|
||||
document.getElementById('rm-hs-status').textContent = 'Stopped';
|
||||
});
|
||||
}
|
||||
|
||||
function rmHsPull() {
|
||||
document.getElementById('rm-hs-status').textContent = 'Checking for capture files...';
|
||||
_rmPost('exec', {cmd: 'ls -la /tmp/handshake* 2>/dev/null'}).then(data => {
|
||||
document.getElementById('rm-hs-output').textContent = data.stdout || 'No capture files found';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Wireless Recon ──────────────────────────────────────────────────────
|
||||
function rmReconStart() {
|
||||
const idx = document.getElementById('rm-recon-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const duration = document.getElementById('rm-recon-duration').value;
|
||||
document.getElementById('rm-recon-status').textContent = 'Running passive recon for ' + duration + 's...';
|
||||
|
||||
const cmd = `timeout ${duration} tcpdump -i ${radio.monitor_interface} -e -c 500 2>&1`;
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
const raw = data.stdout || '';
|
||||
document.getElementById('rm-recon-raw').textContent = raw;
|
||||
document.getElementById('rm-recon-status').textContent = 'Recon complete';
|
||||
rmParseRecon(raw);
|
||||
});
|
||||
}
|
||||
|
||||
function rmReconStop() {
|
||||
_rmPost('exec', {cmd: 'killall tcpdump 2>/dev/null'}).then(data => {
|
||||
document.getElementById('rm-recon-status').textContent = 'Stopped';
|
||||
});
|
||||
}
|
||||
|
||||
function rmParseRecon(raw) {
|
||||
const lines = raw.split('\n');
|
||||
const networks = {};
|
||||
const probes = {};
|
||||
|
||||
lines.forEach(line => {
|
||||
// Extract beacons
|
||||
const beaconMatch = line.match(/Beacon \(([^)]*)\)/);
|
||||
const bssidMatch = line.match(/BSSID:([0-9a-f:]+)/i);
|
||||
const sigMatch = line.match(/(-?\d+)dBm/);
|
||||
if (beaconMatch && bssidMatch) {
|
||||
const ssid = beaconMatch[1] || '(hidden)';
|
||||
const bssid = bssidMatch[1];
|
||||
networks[bssid] = {ssid: ssid, bssid: bssid, signal: sigMatch ? sigMatch[1] : '', count: (networks[bssid]?.count || 0) + 1};
|
||||
}
|
||||
|
||||
// Extract probe requests
|
||||
const probeMatch = line.match(/Probe Request \(([^)]*)\)/);
|
||||
const saMatch = line.match(/SA:([0-9a-f:]+)/i);
|
||||
if (probeMatch && saMatch) {
|
||||
const ssid = probeMatch[1] || '(broadcast)';
|
||||
const sa = saMatch[1];
|
||||
const key = sa + ':' + ssid;
|
||||
probes[key] = {mac: sa, ssid: ssid, count: (probes[key]?.count || 0) + 1};
|
||||
}
|
||||
});
|
||||
|
||||
let netHtml = '<table style="width:100%;font-size:0.8rem"><tr><th>SSID</th><th>BSSID</th><th>Signal</th><th>Beacons</th></tr>';
|
||||
Object.values(networks).sort((a, b) => b.count - a.count).forEach(n => {
|
||||
netHtml += `<tr><td>${n.ssid}</td><td>${n.bssid}</td><td>${n.signal}dBm</td><td>${n.count}</td></tr>`;
|
||||
});
|
||||
netHtml += '</table>';
|
||||
document.getElementById('rm-recon-networks').innerHTML = Object.keys(networks).length ? netHtml : '<em>No networks found</em>';
|
||||
|
||||
let probeHtml = '<table style="width:100%;font-size:0.8rem"><tr><th>Client MAC</th><th>Probing For</th><th>Count</th></tr>';
|
||||
Object.values(probes).sort((a, b) => b.count - a.count).forEach(p => {
|
||||
probeHtml += `<tr><td>${p.mac}</td><td>${p.ssid}</td><td>${p.count}</td></tr>`;
|
||||
});
|
||||
probeHtml += '</table>';
|
||||
document.getElementById('rm-recon-probes').innerHTML = Object.keys(probes).length ? probeHtml : '<em>No probe requests found</em>';
|
||||
}
|
||||
|
||||
// ── Frame Injection ─────────────────────────────────────────────────────
|
||||
function rmInjTest() {
|
||||
const idx = document.getElementById('rm-inj-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const out = document.getElementById('rm-inj-output');
|
||||
out.textContent = 'Testing injection capability...';
|
||||
const cmd = `aireplay-ng --test ${radio.monitor_interface} 2>&1`;
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
out.textContent = data.stdout || data.stderr || 'No output';
|
||||
});
|
||||
}
|
||||
|
||||
function rmInjCustom() {
|
||||
const idx = document.getElementById('rm-inj-radio').value;
|
||||
const radio = rmPiap.radios[idx];
|
||||
const args = document.getElementById('rm-inj-cmd').value;
|
||||
const out = document.getElementById('rm-inj-output');
|
||||
const cmd = `aireplay-ng ${args} ${radio.monitor_interface} 2>&1`;
|
||||
_rmPost('exec', {cmd: cmd, radio: radio}).then(data => {
|
||||
out.textContent += (data.stdout || data.stderr || '') + '\n';
|
||||
out.scrollTop = out.scrollHeight;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.rm-tab-panel { margin-top: 1rem; }
|
||||
.rm-tab-panel table { border-collapse: collapse; }
|
||||
.rm-tab-panel th, .rm-tab-panel td { padding: 4px 8px; border-bottom: 1px solid var(--border-color, #333); text-align: left; }
|
||||
.rm-tab-panel th { color: var(--text-secondary); font-weight: 600; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -123,6 +123,7 @@ function loadReports(){
|
||||
<button class="btn btn-sm" style="color:var(--danger)" onclick="event.stopPropagation();deleteReport('${r.id}')">Delete</button>
|
||||
</div>
|
||||
</div></div>`).join('');
|
||||
halAnalyze('Report Engine', JSON.stringify(d, null, 2), 'pentest report', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,6 +155,7 @@ function openReport(id){
|
||||
document.getElementById('ed-status').value=r.status||'draft';
|
||||
renderFindings(r.findings||[]);
|
||||
switchTab('editor');
|
||||
halAnalyze('Report Engine', JSON.stringify(d, null, 2), 'pentest report', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,6 +238,7 @@ function loadTemplates(){
|
||||
<p style="font-size:0.8rem;margin:0.3rem 0;color:var(--text-secondary)">${esc(t.description)}</p>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted)">${(t.references||[]).join(', ')}</div>
|
||||
</div>`).join('');
|
||||
halAnalyze('Report Engine', JSON.stringify(d, null, 2), 'pentest report', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -435,6 +435,7 @@ function reAnalyze() {
|
||||
_reStrings = r.strings_preview || [];
|
||||
document.getElementById('a-strings-count').textContent = '(' + (r.strings_count||0).toLocaleString() + ' total, showing first ' + _reStrings.length + ')';
|
||||
reFilterStrings();
|
||||
halAnalyze('Reverse Engineering', JSON.stringify(r, null, 2), 'binary analysis', 'analyze');
|
||||
}).catch(function(e) {
|
||||
setLoading(btn, false);
|
||||
alert('Analysis failed: ' + e);
|
||||
@@ -492,6 +493,7 @@ function reCompare() {
|
||||
lines.push('MD5 file1: ' + r.file1.hashes.md5);
|
||||
lines.push('MD5 file2: ' + r.file2.hashes.md5);
|
||||
out.textContent = lines.join('\n');
|
||||
halAnalyze('Reverse Engineering', JSON.stringify(r, null, 2), 'binary analysis', 'analyze');
|
||||
}).catch(function(e) {
|
||||
setLoading(btn, false);
|
||||
out.style.display = '';
|
||||
@@ -548,6 +550,7 @@ function reDisassemble() {
|
||||
row += '</tr>';
|
||||
tbody.innerHTML += row;
|
||||
});
|
||||
halAnalyze('Reverse Engineering', JSON.stringify(r, null, 2), 'binary analysis', 'analyze');
|
||||
}).catch(function(e) {
|
||||
setLoading(btn, false);
|
||||
alert('Disassembly failed: ' + e);
|
||||
@@ -642,6 +645,7 @@ function reYaraScan() {
|
||||
html += '</div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
halAnalyze('Reverse Engineering', JSON.stringify(r, null, 2), 'binary analysis', 'analyze');
|
||||
}).catch(function(e) {
|
||||
setLoading(btn, false);
|
||||
alert('YARA scan failed: ' + e);
|
||||
@@ -680,6 +684,7 @@ function reHexDump() {
|
||||
});
|
||||
pre.innerHTML = html;
|
||||
document.getElementById('hex-info').textContent = 'Offset: 0x' + _hexCurrentOffset.toString(16) + ' | Showing: ' + r.length + ' bytes | File: ' + r.file_size.toLocaleString() + ' bytes total';
|
||||
halAnalyze('Reverse Engineering', JSON.stringify(r, null, 2), 'binary analysis', 'analyze');
|
||||
}).catch(function(e) {
|
||||
setLoading(btn, false);
|
||||
alert('Hex dump failed: ' + e);
|
||||
@@ -720,6 +725,7 @@ function reHexSearch() {
|
||||
html += '</div>';
|
||||
});
|
||||
list.innerHTML = html;
|
||||
halAnalyze('Reverse Engineering', JSON.stringify(r, null, 2), 'binary analysis', 'analyze');
|
||||
}).catch(function(e) {
|
||||
setLoading(btn, false);
|
||||
alert('Hex search failed: ' + e);
|
||||
|
||||
@@ -162,6 +162,7 @@ function rfidLFSearch() {
|
||||
if (data.error) { renderOutput('rfid-scan-output', 'Error: ' + data.error); return; }
|
||||
renderOutput('rfid-scan-output', data.output || 'No card detected.');
|
||||
if (data.card) rfidUpdateLastCard(data.card);
|
||||
halAnalyze('RFID/NFC', JSON.stringify(data, null, 2), 'rfid', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -173,6 +174,7 @@ function rfidHFSearch() {
|
||||
if (data.error) { renderOutput('rfid-scan-output', 'Error: ' + data.error); return; }
|
||||
renderOutput('rfid-scan-output', data.output || 'No card detected.');
|
||||
if (data.card) rfidUpdateLastCard(data.card);
|
||||
halAnalyze('RFID/NFC', JSON.stringify(data, null, 2), 'rfid', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -184,6 +186,7 @@ function rfidNFCScan() {
|
||||
if (data.error) { renderOutput('rfid-scan-output', 'Error: ' + data.error); return; }
|
||||
renderOutput('rfid-scan-output', data.output || 'No NFC tag detected.');
|
||||
if (data.card) rfidUpdateLastCard(data.card);
|
||||
halAnalyze('RFID/NFC', JSON.stringify(data, null, 2), 'rfid', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -203,6 +206,7 @@ function rfidEMClone() {
|
||||
postJSON('/rfid/clone/em410x', {card_id: id}).then(function(data) {
|
||||
setLoading(btn, false);
|
||||
renderOutput('rfid-em-output', data.message || data.error || 'Done');
|
||||
halAnalyze('RFID/NFC', JSON.stringify(data, null, 2), 'rfid', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -212,6 +216,7 @@ function rfidMFDump() {
|
||||
postJSON('/rfid/dump/mifare', {}).then(function(data) {
|
||||
setLoading(btn, false);
|
||||
renderOutput('rfid-mf-dump-output', data.output || data.error || 'No output');
|
||||
halAnalyze('RFID/NFC', JSON.stringify(data, null, 2), 'rfid', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -223,6 +228,7 @@ function rfidMFClone() {
|
||||
postJSON('/rfid/clone/mifare', {dump_path: path}).then(function(data) {
|
||||
setLoading(btn, false);
|
||||
renderOutput('rfid-mf-clone-output', data.message || data.error || 'Done');
|
||||
halAnalyze('RFID/NFC', JSON.stringify(data, null, 2), 'rfid', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
|
||||
@@ -452,6 +452,7 @@ function sdrDetectDevices() {
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -492,6 +493,7 @@ function sdrScanSpectrum() {
|
||||
document.getElementById('sdr-scan-info').textContent = startMhz + ' - ' + endMhz + ' MHz | ' + sdrSpectrumData.length + ' pts | ' + dev;
|
||||
sdrDrawSpectrum(sdrSpectrumData, startMhz, endMhz);
|
||||
document.getElementById('sdr-spectrum-section').style.display = '';
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() {
|
||||
setLoading(btn, false);
|
||||
document.getElementById('sdr-scan-status').textContent = 'Scan request failed.';
|
||||
@@ -667,6 +669,7 @@ function sdrStartCapture() {
|
||||
setTimeout(function() {
|
||||
sdrCaptureFinished();
|
||||
}, (body.duration + 2) * 1000);
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -739,6 +742,7 @@ function sdrLoadRecordings() {
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -785,6 +789,7 @@ function sdrReplay() {
|
||||
} else {
|
||||
document.getElementById('sdr-replay-output').textContent = data.message || 'Replay complete.';
|
||||
}
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() {
|
||||
setLoading(btn, false);
|
||||
document.getElementById('sdr-replay-output').textContent = 'Replay request failed.';
|
||||
@@ -812,6 +817,7 @@ function sdrDemodulate() {
|
||||
+ '\nSamples: ' + (data.samples || 0)
|
||||
+ '\nMode: ' + (data.mode || mode.toUpperCase());
|
||||
}
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() {
|
||||
setLoading(btn, false);
|
||||
document.getElementById('sdr-demod-output').textContent = 'Demod request failed.';
|
||||
@@ -849,6 +855,7 @@ function sdrAnalyze() {
|
||||
+ '<tr><td>Device</td><td>' + escapeHtml(data.device || 'Unknown') + '</td></tr>'
|
||||
+ '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() {
|
||||
setLoading(btn, false);
|
||||
document.getElementById('sdr-analysis-results').innerHTML = '<div class="empty-state">Analysis request failed.</div>';
|
||||
@@ -873,6 +880,7 @@ function sdrAdsbStart() {
|
||||
// Start polling
|
||||
sdrAdsbInterval = setInterval(sdrAdsbRefresh, 3000);
|
||||
sdrAdsbRefresh();
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -918,6 +926,7 @@ function sdrAdsbRefresh() {
|
||||
+ '</tr>';
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -970,6 +979,7 @@ function sdrGpsDetect() {
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() {
|
||||
setLoading(btn, false);
|
||||
document.getElementById('sdr-gps-results').innerHTML = '<div class="empty-state">GPS detection request failed.</div>';
|
||||
@@ -1010,6 +1020,7 @@ function sdrDroneStart() {
|
||||
sdrDroneCheckStatus();
|
||||
}, (dur + 5) * 1000);
|
||||
}
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -1103,6 +1114,7 @@ function sdrDroneRefresh() {
|
||||
table.classList.add('drone-alert-flash');
|
||||
setTimeout(function() { table.classList.remove('drone-alert-flash'); }, 1500);
|
||||
}
|
||||
halAnalyze('SDR/RF Tools', JSON.stringify(data, null, 2), 'rf analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
1045
web/templates/ssh_manager.html
Normal file
1045
web/templates/ssh_manager.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -250,6 +250,7 @@ function stegoHide() {
|
||||
if (data.bytes_hidden) lines.push('Bytes embedded: ' + data.bytes_hidden);
|
||||
if (data.capacity_used) lines.push('Capacity used: ' + data.capacity_used + '%');
|
||||
renderOutput('hide-output-result', lines.join('\n'));
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -268,6 +269,7 @@ function stegoCapacity() {
|
||||
lines.push('Max characters: ~' + (data.max_chars || '--'));
|
||||
if (data.dimensions) lines.push('Dimensions: ' + data.dimensions);
|
||||
renderOutput('hide-output-result', lines.join('\n'));
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -279,6 +281,7 @@ function wsEncode() {
|
||||
postJSON('/stego/whitespace/encode', {cover: cover, hidden: hidden}).then(function(data) {
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
document.getElementById('ws-result').value = data.encoded || '';
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,6 +292,7 @@ function wsDecode() {
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
document.getElementById('ws-hidden').value = data.decoded || '';
|
||||
document.getElementById('ws-result').value = 'Decoded: ' + (data.decoded || '(empty)');
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -323,6 +327,7 @@ function stegoExtract() {
|
||||
} else {
|
||||
renderOutput('extract-result', data.text || data.message || 'No hidden data found.');
|
||||
}
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -396,6 +401,7 @@ function stegoDetect() {
|
||||
} else {
|
||||
iContainer.innerHTML = '<div class="empty-state">No specific indicators found.</div>';
|
||||
}
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -425,6 +431,7 @@ function stegoBatchScan() {
|
||||
lines.push(flag + ' ' + r.filename + ' (confidence: ' + (r.confidence || 0) + '%)');
|
||||
});
|
||||
renderOutput('batch-output', lines.join('\n'));
|
||||
halAnalyze('Steganography', JSON.stringify(data, null, 2), 'stego analysis', 'analyze');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -42,9 +42,261 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System (non-Python) Dependencies -->
|
||||
<div class="section">
|
||||
<h2>System Dependencies</h2>
|
||||
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||||
These tools must be installed separately — AUTARCH cannot install them for you.
|
||||
</p>
|
||||
<div id="sys-deps-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0.75rem;font-size:0.82rem">
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>Nmap</strong>
|
||||
<span id="dep-nmap" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Network scanner used by OSINT, port scanning, and IDS modules.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install nmap</code><br>
|
||||
<strong>Windows:</strong> <a href="https://nmap.org/download.html" target="_blank" rel="noopener">nmap.org/download</a><br>
|
||||
<strong>macOS:</strong> <code>brew install nmap</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>Wireshark / tshark</strong>
|
||||
<span id="dep-tshark" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Packet capture and protocol analysis.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install tshark wireshark</code><br>
|
||||
<strong>Windows:</strong> <a href="https://www.wireshark.org/download.html" target="_blank" rel="noopener">wireshark.org/download</a> (includes Npcap)<br>
|
||||
<strong>macOS:</strong> <code>brew install wireshark</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>tcpdump</strong>
|
||||
<span id="dep-tcpdump" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Low-level packet capture used by MCP tools and network module.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install tcpdump</code><br>
|
||||
<strong>Windows:</strong> Included with Npcap/Wireshark<br>
|
||||
<strong>macOS:</strong> Pre-installed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>Metasploit Framework</strong>
|
||||
<span id="dep-msfconsole" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Penetration testing framework for offense modules.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <a href="https://docs.metasploit.com/docs/using-metasploit/getting-started/nightly-installers.html" target="_blank" rel="noopener">Metasploit nightly installer</a><br>
|
||||
<strong>Windows:</strong> <a href="https://www.metasploit.com/download" target="_blank" rel="noopener">metasploit.com/download</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>WireGuard</strong>
|
||||
<span id="dep-wg" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">VPN tunnel management.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install wireguard wireguard-tools</code><br>
|
||||
<strong>Windows:</strong> <a href="https://www.wireguard.com/install/" target="_blank" rel="noopener">wireguard.com/install</a><br>
|
||||
<strong>macOS:</strong> <code>brew install wireguard-tools</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>Node.js / npm</strong>
|
||||
<span id="dep-node" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Required for building WebUSB hardware bundles (ADB, Fastboot, ESP32).</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>All platforms:</strong> <a href="https://nodejs.org/en/download" target="_blank" rel="noopener">nodejs.org/download</a><br>
|
||||
<strong>Linux:</strong> <code>sudo apt install nodejs npm</code><br>
|
||||
<strong>macOS:</strong> <code>brew install node</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>Go</strong>
|
||||
<span id="dep-go" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Required for the DNS server and Setec Manager services.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>All platforms:</strong> <a href="https://go.dev/dl/" target="_blank" rel="noopener">go.dev/dl</a><br>
|
||||
<strong>Linux:</strong> <code>sudo apt install golang-go</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>ADB / Fastboot</strong>
|
||||
<span id="dep-adb" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Android device management. Bundled in android/ for Linux ARM64.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>All platforms:</strong> <a href="https://developer.android.com/tools/releases/platform-tools" target="_blank" rel="noopener">Android Platform Tools</a><br>
|
||||
<strong>Linux:</strong> <code>sudo apt install android-tools-adb</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>miniupnpc</strong>
|
||||
<span id="dep-upnpc" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">UPnP port forwarding client.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install miniupnpc</code><br>
|
||||
<strong>macOS:</strong> <code>brew install miniupnpc</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>whois</strong>
|
||||
<span id="dep-whois" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Domain/IP registration lookups for OSINT and MCP.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install whois</code><br>
|
||||
<strong>macOS:</strong> Pre-installed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>aircrack-ng</strong>
|
||||
<span id="dep-aircrack" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">WiFi auditing, deauth attacks, handshake capture.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install aircrack-ng</code><br>
|
||||
<strong>Website:</strong> <a href="https://www.aircrack-ng.org/" target="_blank" rel="noopener">aircrack-ng.org</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>mdk3 / mdk4</strong>
|
||||
<span id="dep-mdk4" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Advanced WiFi deauthentication and beacon flooding. Used by the Deauth Attack module.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install mdk4</code><br>
|
||||
<strong>Note:</strong> mdk3 is legacy; mdk4 is the maintained fork.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>sslstrip</strong>
|
||||
<span id="dep-sslstrip" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">HTTPS downgrade tool for MITM testing. Used by the Pineapple module.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>pip install sslstrip</code> or <code>sudo apt install sslstrip</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>Monitor Mode WiFi Adapter</strong>
|
||||
<span id="dep-monitor" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">—</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Required for WiFi Audit, Deauth, and Pineapple. The built-in Pi WiFi does NOT support monitor mode. You need a USB adapter.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Recommended:</strong> Alfa AWUS036ACH (dual-band, widely supported)<br>
|
||||
<strong>Budget:</strong> Alfa AWUS036NHA (2.4GHz only, ~$20)<br>
|
||||
<strong>Check yours:</strong> <code>iw phy | grep -A5 "Supported interface modes"</code> — look for "monitor"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>PyTorch (GPU)</strong>
|
||||
<span id="dep-torch" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Required for local Transformers models and LoRA training. Install the correct version for your GPU.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>All platforms:</strong> <a href="https://pytorch.org/get-started/locally/" target="_blank" rel="noopener">pytorch.org/get-started</a><br>
|
||||
<strong>CPU only:</strong> <code>pip install torch --index-url https://download.pytorch.org/whl/cpu</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>iw / wireless-tools</strong>
|
||||
<span id="dep-iw" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">WiFi interface management for scanning, monitor mode, and channel control.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install iw wireless-tools</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>NetworkManager (nmcli)</strong>
|
||||
<span id="dep-nmcli" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">WiFi scanning and connection management. Used by Network Security WiFi Scanner.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install network-manager</code> (usually pre-installed)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>hostapd</strong>
|
||||
<span id="dep-hostapd" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Access point daemon for Pineapple/Evil Twin and rogue AP pentesting.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install hostapd</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>dnsmasq</strong>
|
||||
<span id="dep-dnsmasq" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">DHCP/DNS server for captive portals and rogue AP pentesting.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install dnsmasq</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;background:var(--bg-card)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
|
||||
<strong>nftables / iptables</strong>
|
||||
<span id="dep-nft" style="font-size:0.72rem;padding:2px 6px;border-radius:3px;background:var(--bg-main)">checking…</span>
|
||||
</div>
|
||||
<div style="color:var(--text-muted);font-size:0.75rem;margin-bottom:0.4rem">Firewall and packet filtering. Used by Network Security IP blocking and NAT.</div>
|
||||
<div style="font-size:0.72rem">
|
||||
<strong>Linux:</strong> <code>sudo apt install nftables</code> (usually pre-installed)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button class="btn btn-sm" onclick="checkSystemDeps()" style="margin-top:0.75rem" id="btn-sys-check">Check System Tools</button>
|
||||
</div>
|
||||
|
||||
<!-- Quick Install -->
|
||||
<div class="section">
|
||||
<h2>Install Packages</h2>
|
||||
<h2>Python Packages</h2>
|
||||
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||||
Install individual packages or preset groups.
|
||||
</p>
|
||||
@@ -165,6 +417,37 @@ function depsInstallOne() {
|
||||
document.getElementById('install-pkg').value = '';
|
||||
}
|
||||
|
||||
function checkSystemDeps() {
|
||||
var btn = document.getElementById('btn-sys-check');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Checking…'; }
|
||||
fetch('/settings/deps/system-check', {method: 'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Check System Tools'; }
|
||||
if (!d.ok) return;
|
||||
var tools = d.tools || {};
|
||||
for (var name in tools) {
|
||||
var el = document.getElementById('dep-' + name);
|
||||
if (!el) continue;
|
||||
var info = tools[name];
|
||||
if (info.found) {
|
||||
el.textContent = info.version || 'installed';
|
||||
el.style.color = 'var(--success, #34c759)';
|
||||
el.style.borderColor = 'var(--success, #34c759)';
|
||||
el.style.border = '1px solid var(--success, #34c759)';
|
||||
} else {
|
||||
el.textContent = 'not found';
|
||||
el.style.color = 'var(--danger, #ff3b30)';
|
||||
el.style.border = '1px solid var(--danger, #ff3b30)';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() { if (btn) { btn.disabled = false; btn.textContent = 'Check System Tools'; } });
|
||||
}
|
||||
|
||||
// Auto-check system deps on page load
|
||||
document.addEventListener('DOMContentLoaded', function() { checkSystemDeps(); });
|
||||
|
||||
function depsInstallGroup(group) {
|
||||
var pkgs = _depsGroupPkgs[group];
|
||||
if (!pkgs || !pkgs.length) return;
|
||||
|
||||
@@ -1,89 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Targets - AUTARCH{% endblock %}
|
||||
{% block title %}Investigations - AUTARCH{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||
<div>
|
||||
<h1>Targets</h1>
|
||||
<h1>Investigations</h1>
|
||||
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||||
Manage scope — IPs, CIDRs, domains, and URLs for your engagement.
|
||||
Investigation profiles for IPs, domains, threat actors, and incidents. Build detailed profiles with GeoIP, traceroute, DNS, WHOIS, and custom fields.
|
||||
</p>
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;gap:0.5rem;flex-wrap:wrap">
|
||||
<button class="btn btn-sm btn-primary" onclick="toggleAddForm()">+ Add Target</button>
|
||||
<button class="btn btn-sm" onclick="exportTargets()">Export JSON</button>
|
||||
<label class="btn btn-sm" style="cursor:pointer" title="Import targets from JSON file">
|
||||
Import JSON
|
||||
<input type="file" accept=".json" style="display:none" onchange="importTargets(this)">
|
||||
</label>
|
||||
<button class="btn btn-sm btn-primary" onclick="toggleAddForm()">+ New Investigation</button>
|
||||
<button class="btn btn-sm" onclick="exportTargets()">Export</button>
|
||||
<label class="btn btn-sm" style="cursor:pointer">Import<input type="file" accept=".json" style="display:none" onchange="importTargets(this)"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Target Form -->
|
||||
<!-- Add Investigation Form -->
|
||||
<div id="add-form" class="section" style="display:none">
|
||||
<h2>Add Target</h2>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem 1rem">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-host">Host / IP / CIDR <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" id="add-host" placeholder="192.168.1.1 or example.com">
|
||||
<h2>New Investigation Profile</h2>
|
||||
|
||||
<!-- Basic Info -->
|
||||
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.75rem">
|
||||
<legend style="font-size:0.85rem;font-weight:600;color:var(--accent);padding:0 0.5rem">Basic Info</legend>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem">
|
||||
<div class="form-group" style="margin:0"><label>Host / IP <span style="color:var(--danger)">*</span></label><input type="text" id="add-host" placeholder="192.168.1.1 or example.com"></div>
|
||||
<div class="form-group" style="margin:0"><label>Name / Label</label><input type="text" id="add-name" placeholder="Investigation name"></div>
|
||||
<div class="form-group" style="margin:0"><label>Type</label><select id="add-type"><option value="ip">IP</option><option value="cidr">CIDR</option><option value="domain">Domain</option><option value="url">URL</option><option value="email">Email</option><option value="actor">Threat Actor</option><option value="incident">Incident</option></select></div>
|
||||
<div class="form-group" style="margin:0"><label>Status</label><select id="add-status"><option value="active">Active</option><option value="pending">Pending</option><option value="completed">Completed</option><option value="escalated">Escalated</option><option value="closed">Closed</option></select></div>
|
||||
<div class="form-group" style="margin:0"><label>Threat Level</label><select id="add-threat"><option value="unknown">Unknown</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option></select></div>
|
||||
<div class="form-group" style="margin:0"><label>OS</label><select id="add-os"><option>Unknown</option><option>Linux</option><option>Windows</option><option>macOS</option><option>Android</option><option>iOS</option><option>Network Device</option></select></div>
|
||||
<div class="form-group" style="margin:0"><label>Source</label><input type="text" id="add-source" placeholder="fail2ban, IDS, manual"></div>
|
||||
<div class="form-group" style="margin:0"><label>Tags</label><input type="text" id="add-tags" placeholder="brute-force, ssh, external"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-name">Name / Label</label>
|
||||
<input type="text" id="add-name" placeholder="Corp Web Server">
|
||||
</fieldset>
|
||||
|
||||
<!-- Network Info -->
|
||||
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.75rem">
|
||||
<legend style="font-size:0.85rem;font-weight:600;color:var(--accent);padding:0 0.5rem">Network</legend>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem">
|
||||
<div class="form-group" style="margin:0"><label>IPv4</label><input type="text" id="add-ipv4" placeholder="192.168.1.1"></div>
|
||||
<div class="form-group" style="margin:0"><label>IPv6</label><input type="text" id="add-ipv6" placeholder="::1"></div>
|
||||
<div class="form-group" style="margin:0"><label>Domain</label><input type="text" id="add-domain" placeholder="example.com"></div>
|
||||
<div class="form-group" style="margin:0"><label>Reverse DNS</label><input type="text" id="add-rdns" placeholder="host.isp.net"></div>
|
||||
<div class="form-group" style="margin:0"><label>MAC Address</label><input type="text" id="add-mac" placeholder="aa:bb:cc:dd:ee:ff"></div>
|
||||
<div class="form-group" style="margin:0"><label>Hostname</label><input type="text" id="add-hostname" placeholder="server01"></div>
|
||||
<div class="form-group" style="margin:0"><label>Ports / Services</label><input type="text" id="add-ports" placeholder="22/ssh, 80/http, 443/https"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-type">Type</label>
|
||||
<select id="add-type">
|
||||
<option value="ip">IP Address</option>
|
||||
<option value="cidr">CIDR / Range</option>
|
||||
<option value="domain">Domain</option>
|
||||
<option value="url">URL</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<!-- GeoIP -->
|
||||
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.75rem">
|
||||
<legend style="font-size:0.85rem;font-weight:600;color:var(--accent);padding:0 0.5rem">GeoIP</legend>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem">
|
||||
<div class="form-group" style="margin:0"><label>Country</label><input type="text" id="add-geo-country" placeholder="US"></div>
|
||||
<div class="form-group" style="margin:0"><label>City</label><input type="text" id="add-geo-city" placeholder="New York"></div>
|
||||
<div class="form-group" style="margin:0"><label>ISP</label><input type="text" id="add-geo-isp" placeholder="Comcast"></div>
|
||||
<div class="form-group" style="margin:0"><label>ASN</label><input type="text" id="add-geo-asn" placeholder="AS7922"></div>
|
||||
<div class="form-group" style="margin:0"><label>Coordinates</label><input type="text" id="add-geo-coords" placeholder="40.7128,-74.0060"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-os">OS</label>
|
||||
<select id="add-os">
|
||||
<option>Unknown</option>
|
||||
<option>Linux</option>
|
||||
<option>Windows</option>
|
||||
<option>macOS</option>
|
||||
<option>Android</option>
|
||||
<option>iOS</option>
|
||||
<option>Network Device</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<!-- Intel -->
|
||||
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.75rem">
|
||||
<legend style="font-size:0.85rem;font-weight:600;color:var(--accent);padding:0 0.5rem">Intelligence</legend>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:0.75rem">
|
||||
<div class="form-group" style="margin:0"><label>DNS Records</label><textarea id="add-dns" rows="2" placeholder="A: 1.2.3.4 MX: mail.example.com" style="font-family:monospace;font-size:0.78rem"></textarea></div>
|
||||
<div class="form-group" style="margin:0"><label>Email(s)</label><input type="text" id="add-email" placeholder="admin@example.com, abuse@isp.net"></div>
|
||||
<div class="form-group" style="margin:0"><label>Username(s)</label><input type="text" id="add-usernames" placeholder="root, admin, attacker123"></div>
|
||||
<div class="form-group" style="margin:0"><label>Vulnerabilities</label><textarea id="add-vulns" rows="2" placeholder="CVE-2024-1234, open SSH" style="font-family:monospace;font-size:0.78rem"></textarea></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-status">Status</label>
|
||||
<select id="add-status">
|
||||
<option value="active">Active</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="out-of-scope">Out of Scope</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-ports">Known Ports</label>
|
||||
<input type="text" id="add-ports" placeholder="22,80,443,8080">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem 1rem;margin-top:0.75rem">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-tags">Tags (comma-separated)</label>
|
||||
<input type="text" id="add-tags" placeholder="web, internal, critical">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="add-notes">Notes</label>
|
||||
<input type="text" id="add-notes" placeholder="Brief notes about this target">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem">
|
||||
<button class="btn btn-primary" onclick="addTarget()">Add Target</button>
|
||||
<div class="form-group" style="margin:0.5rem 0 0"><label>Traceroute</label><textarea id="add-traceroute" rows="3" placeholder="Paste traceroute output" style="font-family:monospace;font-size:0.78rem;width:100%"></textarea></div>
|
||||
<div class="form-group" style="margin:0.5rem 0 0"><label>WHOIS</label><textarea id="add-whois" rows="3" placeholder="Paste WHOIS output" style="font-family:monospace;font-size:0.78rem;width:100%"></textarea></div>
|
||||
<div class="form-group" style="margin:0.5rem 0 0"><label>Notes</label><textarea id="add-notes" rows="3" placeholder="Investigation notes, observations, timeline..." style="font-size:0.82rem;width:100%"></textarea></div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<fieldset style="border:1px solid var(--border);border-radius:var(--radius);padding:0.75rem;margin-bottom:0.75rem">
|
||||
<legend style="font-size:0.85rem;font-weight:600;color:var(--accent);padding:0 0.5rem">Custom Fields</legend>
|
||||
<div id="custom-fields-container"></div>
|
||||
<button class="btn btn-sm" onclick="addCustomField()" style="margin-top:0.5rem">+ Add Field</button>
|
||||
</fieldset>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-top:0.75rem">
|
||||
<button class="btn btn-primary" onclick="addTarget()">Create Investigation</button>
|
||||
<button class="btn btn-sm" onclick="toggleAddForm()">Cancel</button>
|
||||
<span id="add-status-msg" style="font-size:0.82rem;color:var(--text-secondary);align-self:center"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Investigation Reports -->
|
||||
<div class="section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
|
||||
<h2 style="margin:0">Investigation Reports</h2>
|
||||
<button class="btn btn-sm" onclick="loadIRs()">Refresh</button>
|
||||
</div>
|
||||
<div id="ir-list" style="margin-bottom:0.5rem"><p style="color:var(--text-muted)">Loading reports...</p></div>
|
||||
</div>
|
||||
|
||||
<!-- Filter / Search -->
|
||||
<div class="section" style="padding:0.6rem 1rem">
|
||||
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap">
|
||||
@@ -272,24 +286,74 @@ function toggleAddForm() {
|
||||
}
|
||||
|
||||
// ── Add target ────────────────────────────────────────────────────────────────
|
||||
var _customFieldCount = 0;
|
||||
function addCustomField() {
|
||||
_customFieldCount++;
|
||||
var container = document.getElementById('custom-fields-container');
|
||||
var row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;gap:0.5rem;align-items:flex-start;margin-bottom:0.5rem;flex-wrap:wrap';
|
||||
row.innerHTML = '<input type="text" class="cf-name" placeholder="Field name" style="width:150px;padding:0.3rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-input);color:inherit;font-size:0.8rem">'
|
||||
+ '<select class="cf-type" style="width:130px;padding:0.3rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-input);color:inherit;font-size:0.8rem">'
|
||||
+ '<option value="text">Text</option><option value="ipv4">IPv4</option><option value="ipv6">IPv6</option>'
|
||||
+ '<option value="domain">Domain</option><option value="dns">DNS Record</option>'
|
||||
+ '<option value="email">Email</option><option value="username">Username</option>'
|
||||
+ '<option value="url">URL</option><option value="mac">MAC Address</option>'
|
||||
+ '<option value="hash">Hash</option><option value="misc">Misc (4000 char)</option></select>'
|
||||
+ '<input type="text" class="cf-value" placeholder="Value" style="flex:1;min-width:200px;padding:0.3rem 0.5rem;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-input);color:inherit;font-size:0.8rem">'
|
||||
+ '<button class="btn btn-sm" onclick="this.parentElement.remove()" style="padding:2px 8px;color:var(--danger)">X</button>';
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function _getVal(id) { var e = document.getElementById(id); return e ? (e.value||'').trim() : ''; }
|
||||
|
||||
function addTarget() {
|
||||
var host = document.getElementById('add-host').value.trim();
|
||||
var host = _getVal('add-host');
|
||||
if (!host) { document.getElementById('add-host').focus(); return; }
|
||||
var msg = document.getElementById('add-status-msg');
|
||||
msg.textContent = 'Saving…';
|
||||
|
||||
// Collect custom fields
|
||||
var customFields = [];
|
||||
document.querySelectorAll('#custom-fields-container > div').forEach(function(row) {
|
||||
var name = row.querySelector('.cf-name').value.trim();
|
||||
var type = row.querySelector('.cf-type').value;
|
||||
var value = row.querySelector('.cf-value').value.trim();
|
||||
if (name && value) customFields.push({name: name, type: type, value: value});
|
||||
});
|
||||
|
||||
fetch('/targets/add', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
host: host,
|
||||
name: document.getElementById('add-name').value,
|
||||
type: document.getElementById('add-type').value,
|
||||
os: document.getElementById('add-os').value,
|
||||
status: document.getElementById('add-status').value,
|
||||
ports: document.getElementById('add-ports').value,
|
||||
tags: document.getElementById('add-tags').value,
|
||||
notes: document.getElementById('add-notes').value,
|
||||
host: host,
|
||||
name: _getVal('add-name'),
|
||||
type: _getVal('add-type'),
|
||||
os: _getVal('add-os'),
|
||||
status: _getVal('add-status'),
|
||||
threat_level: _getVal('add-threat'),
|
||||
source: _getVal('add-source'),
|
||||
ports: _getVal('add-ports'),
|
||||
tags: _getVal('add-tags'),
|
||||
notes: _getVal('add-notes'),
|
||||
ipv4: _getVal('add-ipv4'),
|
||||
ipv6: _getVal('add-ipv6'),
|
||||
domain: _getVal('add-domain'),
|
||||
rdns: _getVal('add-rdns'),
|
||||
mac_address: _getVal('add-mac'),
|
||||
hostname: _getVal('add-hostname'),
|
||||
services: _getVal('add-ports'),
|
||||
geo_country: _getVal('add-geo-country'),
|
||||
geo_city: _getVal('add-geo-city'),
|
||||
geo_isp: _getVal('add-geo-isp'),
|
||||
geo_asn: _getVal('add-geo-asn'),
|
||||
geo_coords: _getVal('add-geo-coords'),
|
||||
dns_records: _getVal('add-dns'),
|
||||
email: _getVal('add-email'),
|
||||
usernames: _getVal('add-usernames'),
|
||||
vulns: _getVal('add-vulns'),
|
||||
traceroute: _getVal('add-traceroute'),
|
||||
whois: _getVal('add-whois'),
|
||||
custom_fields: customFields,
|
||||
})
|
||||
}).then(function(r){ return r.json(); })
|
||||
.then(function(d) {
|
||||
@@ -434,6 +498,90 @@ document.addEventListener('DOMContentLoaded', function(){
|
||||
document.getElementById('add-notes').addEventListener('keypress', function(e){
|
||||
if (e.key === 'Enter') addTarget();
|
||||
});
|
||||
loadIRs();
|
||||
});
|
||||
|
||||
function loadIRs() {
|
||||
fetch('/targets/ir').then(function(r){ return r.json(); }).then(function(d) {
|
||||
var el = document.getElementById('ir-list');
|
||||
if (!d.ok || !d.reports || !d.reports.length) {
|
||||
el.innerHTML = '<p style="color:var(--text-muted);font-size:0.85rem">No investigation reports yet. Use HAL\'s "Report Only" or "Let HAL Fix It + IR" buttons after a scan.</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<table class="data-table" style="font-size:0.82rem;width:100%"><thead><tr>'
|
||||
+ '<th>ID</th><th>Title</th><th>Risk</th><th>Status</th><th>Source</th><th>Created</th><th>Actions</th>'
|
||||
+ '</tr></thead><tbody>';
|
||||
d.reports.forEach(function(r) {
|
||||
var riskColors = {critical:'#ff3b30',high:'#ff6b35',medium:'#f59e0b',low:'#8ec07c',clean:'#34c759',unknown:'#888'};
|
||||
var rc = riskColors[r.risk_level] || '#888';
|
||||
var halBadge = r.created_by_hal ? ' <span style="background:var(--accent);color:#000;font-size:0.6rem;padding:1px 4px;border-radius:2px;font-weight:700">HAL</span>' : '';
|
||||
var statusColor = r.status === 'closed' ? 'var(--text-muted)' : r.status === 'open' ? 'var(--accent)' : '#f59e0b';
|
||||
var created = r.created_at ? new Date(r.created_at).toLocaleString() : '';
|
||||
html += '<tr>'
|
||||
+ '<td><strong style="font-family:monospace;color:var(--accent)">' + escapeHtml(r.id) + '</strong>' + halBadge + '</td>'
|
||||
+ '<td>' + escapeHtml(r.title || '') + '</td>'
|
||||
+ '<td><span style="color:' + rc + ';font-weight:600;text-transform:uppercase;font-size:0.75rem">' + escapeHtml(r.risk_level || 'unknown') + '</span></td>'
|
||||
+ '<td><span style="color:' + statusColor + '">' + escapeHtml(r.status || '') + '</span></td>'
|
||||
+ '<td style="font-size:0.75rem">' + escapeHtml(r.source || '') + '</td>'
|
||||
+ '<td style="font-size:0.75rem">' + escapeHtml(created) + '</td>'
|
||||
+ '<td style="white-space:nowrap">'
|
||||
+ '<button class="btn btn-sm" style="font-size:0.65rem;padding:1px 6px;margin-right:3px" onclick="viewIR(\'' + escapeHtml(r.id) + '\')">View</button>'
|
||||
+ '<button class="btn btn-sm" style="font-size:0.65rem;padding:1px 6px;margin-right:3px" onclick="loadIRToHal(\'' + escapeHtml(r.id) + '\')">Send to HAL</button>'
|
||||
+ '<button class="btn btn-sm" style="font-size:0.65rem;padding:1px 6px;color:var(--danger);border-color:var(--danger)" onclick="deleteIR(\'' + escapeHtml(r.id) + '\')">Delete</button>'
|
||||
+ '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
}).catch(function() {
|
||||
document.getElementById('ir-list').innerHTML = '<p style="color:var(--danger)">Failed to load reports.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function viewIR(irId) {
|
||||
fetch('/targets/ir/' + irId).then(function(r){ return r.json(); }).then(function(d) {
|
||||
if (!d.ok) { alert('IR not found'); return; }
|
||||
var ir = d.ir;
|
||||
var w = window.open('', '_blank', 'width=700,height=600');
|
||||
w.document.write('<html><head><title>' + ir.id + '</title>'
|
||||
+ '<style>body{background:#1a1a2e;color:#e0e0e0;font-family:system-ui;padding:20px;font-size:14px} '
|
||||
+ 'h1{color:#00ff41;font-size:1.2rem} h2{color:#5ac8fa;font-size:1rem;margin-top:1rem} '
|
||||
+ 'pre{background:#12122a;padding:10px;border-radius:6px;white-space:pre-wrap;font-size:0.82rem;border:1px solid #333} '
|
||||
+ '.badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:0.75rem;font-weight:700}</style></head><body>');
|
||||
w.document.write('<h1>' + ir.id + (ir.created_by_hal ? ' <span class="badge" style="background:#00ff41;color:#000">HAL</span>' : '') + '</h1>');
|
||||
w.document.write('<div><strong>Title:</strong> ' + (ir.title||'') + '</div>');
|
||||
w.document.write('<div><strong>Status:</strong> ' + (ir.status||'') + ' · <strong>Risk:</strong> ' + (ir.risk_level||'') + ' · <strong>Source:</strong> ' + (ir.source||'') + '</div>');
|
||||
w.document.write('<div><strong>Created:</strong> ' + (ir.created_at||'') + '</div>');
|
||||
if (ir.ip) w.document.write('<div><strong>IP:</strong> ' + ir.ip + '</div>');
|
||||
if (ir.analysis) { w.document.write('<h2>Analysis</h2><pre>' + ir.analysis.replace(/</g,'<') + '</pre>'); }
|
||||
if (ir.fix_attempted) {
|
||||
w.document.write('<h2>Fix Attempted</h2><pre>' + (ir.fix_results||'').replace(/</g,'<') + '</pre>');
|
||||
}
|
||||
if (ir.notes) { w.document.write('<h2>Notes</h2><pre>' + ir.notes.replace(/</g,'<') + '</pre>'); }
|
||||
w.document.write('</body></html>');
|
||||
w.document.close();
|
||||
});
|
||||
}
|
||||
|
||||
function loadIRToHal(irId) {
|
||||
fetch('/targets/ir/' + irId + '/load-to-hal', {method:'POST'}).then(function(r){return r.json()}).then(function(d) {
|
||||
if (d.ok) {
|
||||
// Open HAL panel and show the loaded IR
|
||||
var panel = document.getElementById('hal-panel');
|
||||
if (panel) panel.style.display = 'flex';
|
||||
halAppendStyled('status', 'Loaded IR ' + d.ir.id + ' into HAL memory');
|
||||
halAppendStyled('bot', 'IR ' + d.ir.id + ': ' + (d.ir.title||'') + '\nRisk: ' + (d.ir.risk_level||'unknown') + '\nStatus: ' + (d.ir.status||'') + '\n\nAnalysis loaded. Ask me to continue working on this investigation.');
|
||||
halScroll();
|
||||
} else {
|
||||
alert('Failed to load IR: ' + (d.error||''));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteIR(irId) {
|
||||
if (!confirm('Delete investigation report ' + irId + '?')) return;
|
||||
fetch('/targets/ir/' + irId + '/delete', {method:'POST'}).then(function(r){return r.json()}).then(function(d) {
|
||||
loadIRs();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -350,6 +350,7 @@ function iocAdd() {
|
||||
document.getElementById('ioc-tags').value = '';
|
||||
document.getElementById('ioc-desc').value = '';
|
||||
iocLoad();
|
||||
halAnalyze('Threat Intel', JSON.stringify(data, null, 2), 'threat intelligence', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -357,6 +358,7 @@ function iocLoad() {
|
||||
fetchJSON('/threat-intel/iocs').then(function(data) {
|
||||
_iocCache = data.iocs || [];
|
||||
iocRender(_iocCache);
|
||||
halAnalyze('Threat Intel', JSON.stringify(data, null, 2), 'threat intelligence', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -536,6 +538,7 @@ function repLookup() {
|
||||
lines.push('');
|
||||
}
|
||||
renderOutput('rep-output', lines.join('\n'));
|
||||
halAnalyze('Threat Intel', JSON.stringify(data, null, 2), 'threat intelligence', 'defense');
|
||||
}).catch(function() { setLoading(btn, false); });
|
||||
}
|
||||
|
||||
@@ -547,6 +550,7 @@ function blGenerate() {
|
||||
}).then(function(data) {
|
||||
if (data.error) { renderOutput('bl-output', 'Error: ' + data.error); return; }
|
||||
renderOutput('bl-output', data.blocklist || 'No IOCs match the selected criteria.');
|
||||
halAnalyze('Threat Intel', JSON.stringify(data, null, 2), 'threat intelligence', 'defense');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,7 @@ function wgServerStatus() {
|
||||
} else {
|
||||
wgResult('wg-dashboard-results', wgOk('Server status refreshed.'));
|
||||
}
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -327,6 +328,7 @@ function wgRefreshPeers() {
|
||||
document.getElementById('wg-online-count').textContent = online;
|
||||
wgPopulateIpDropdowns();
|
||||
wgRenderPeersTable();
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,6 +384,7 @@ function wgRefreshClients() {
|
||||
wgClients = data.clients || [];
|
||||
wgPopulateIpDropdowns();
|
||||
wgRenderClientsTable();
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,6 +435,7 @@ function wgCreateClient() {
|
||||
wgResult('wg-clients-results', wgOk('Created: ' + c.name + ' (' + c.assigned_ip + ')'));
|
||||
document.getElementById('wg-new-name').value = '';
|
||||
wgRefreshClients();
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
} else {
|
||||
wgResult('wg-clients-results', wgErr(data.error || 'Failed'));
|
||||
}
|
||||
@@ -466,6 +470,7 @@ function wgViewClient(id) {
|
||||
h += '</div>';
|
||||
h += '<div id="wg-config-box" style="display:none;margin-top:10px"></div>';
|
||||
document.getElementById('wg-detail-content').innerHTML = h;
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -532,6 +537,7 @@ function wgAdbAutoConnect() {
|
||||
});
|
||||
wgResult('wg-adb-results', h);
|
||||
wgAdbDevices();
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -549,6 +555,7 @@ function wgAdbDevices() {
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
el.innerHTML = h;
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -561,6 +568,7 @@ function wgUsbipStatus() {
|
||||
txt.textContent = data.modules_loaded ? 'Loaded' : 'Not loaded';
|
||||
document.getElementById('wg-usbip-imports').textContent = data.active_imports || '0';
|
||||
wgUsbipPorts();
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -596,6 +604,7 @@ function wgUsbipListRemote() {
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
el.innerHTML = h;
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -632,6 +641,7 @@ function wgUsbipPorts() {
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
el.innerHTML = h;
|
||||
halAnalyze('WireGuard', JSON.stringify(data, null, 2), 'vpn status', 'network');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user