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:
SsSnake
2026-03-24 06:59:06 -07:00
parent 1092689f45
commit da53899f66
382 changed files with 15277 additions and 493964 deletions

View File

@@ -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:

View File

@@ -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():

View File

@@ -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():

View File

@@ -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})

View File

@@ -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():

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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())

View 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})

View File

@@ -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
View 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)})

View File

@@ -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})

View File

@@ -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 ─────────────────────────────────────── */

View File

@@ -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;

View File

@@ -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));

View File

@@ -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');
});
}

View File

@@ -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) {

View File

@@ -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 %}">&#x1F50D; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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 %}">&#x1F6E1; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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)">&#x2514; 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)">&#x2514; 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)">&#x2514; 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">&#x2514; 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)">&#x2795; Create Module</a></li>
<li><a href="{{ url_for('encmodules.index') }}" class="{% if request.blueprint == 'encmodules' %}active{% endif %}" style="color:var(--danger,#f55)">&#x1F512; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2514; 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">&#x2715;</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">&#x21BA;</button>
</div>
</div>

View File

@@ -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); });
}

View File

@@ -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); });
}

View File

@@ -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>

View File

@@ -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');
});
}

View File

@@ -12,6 +12,31 @@
<a href="{{ url_for('defense.index') }}" class="btn btn-sm" style="margin-left:auto">&larr; 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>

View File

@@ -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>`;
});

View File

@@ -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;});
}

View File

@@ -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); });
}

View File

@@ -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>

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View File

@@ -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 &amp; Activate</em>, then <em>Load Model</em> to initialise.
— select a tab, fill in settings, and click <em>Save &amp; 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>01. 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>01. 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 &amp; 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)">&#x2713; 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 = '&#x2713; <strong style="color:var(--success,#34c759)">Connected</strong> — ' + escapeHtml(d.model_name);
} else {
dot.style.background = 'var(--danger, #ff3b30)';
text.innerHTML = '&#x2715; ' + 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 = '&#x2713; <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 = '&#x2713; <strong style="color:var(--success,#34c759)">claude</strong> ready — ' + escapeHtml(d.model_name); }
} else {
dot.style.background = 'var(--danger, #ff3b30)';
text.innerHTML = '&#x2715; <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)">&#x2713; Saved</span>';
document.getElementById('agent-active-backend').textContent = tab;
} else {
status.innerHTML = '<span style="color:var(--danger,#ff3b30)">&#x2715; ' + 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 %}

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View 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">&larr; 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 &mdash; 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 &amp; 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 = '&#x2713; 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 %}

View 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...&#10;# 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(' &middot; ') + '</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(' &middot; ') + '</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 %}

View File

@@ -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
View 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* ── 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> &mdash; ' + 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>'
+ ' &middot; <span style="color:#f59e0b">' + (sev.warning || 0) + ' warning</span>'
+ ' &middot; <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' ? '&#x26A0;' : f.severity === 'warning' ? '&#x26A0;' : '&#x2713;';
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) &middot; ' + 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>'
+ ' &middot; MAC: <code>' + escHtml(gw.mac || '?') + '</code>'
+ ' &middot; Interface: ' + escHtml(gw.interface || '?')
+ ' &middot; 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">'
+ '&#x2713; 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 + '">&#x26A0; ' + 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 ? '&#x2713;' : '&#x2715;') + '</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 %}

View File

@@ -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');
});
}

View File

@@ -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));
}

View 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 %}

View File

@@ -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');
});
}

View File

@@ -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);

View File

@@ -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); });
}

View File

@@ -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');
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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;

View File

@@ -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&#10;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||'') + ' &middot; <strong>Risk:</strong> ' + (ir.risk_level||'') + ' &middot; <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,'&lt;') + '</pre>'); }
if (ir.fix_attempted) {
w.document.write('<h2>Fix Attempted</h2><pre>' + (ir.fix_results||'').replace(/</g,'&lt;') + '</pre>');
}
if (ir.notes) { w.document.write('<h2>Notes</h2><pre>' + ir.notes.replace(/</g,'&lt;') + '</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 %}

View File

@@ -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');
});
}

View File

@@ -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');
});
}