feat(fabricator): Add Widget Fabricator Streamlit app with live data

SecuBox Fabricator - Universal Constructor for SecuBox Components:
- Main dashboard with live stats from UCI and JSON cache
- Collectors page: manage stats scripts, view/run collectors, JSON cache
- Apps page: Streamlit instance management with test/restart/deploy
- Blogs page: MetaBlogizer site management from UCI config
- Services page: HAProxy vhosts/backends, Peek/Poke/Emancipate
- Widgets page: dashboard widget designer with live stats preview

All pages now use actual live data from UCI configs and /tmp/secubox/*.json

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-07 11:05:09 +01:00
parent 9884965e2b
commit bfd2ed7c1f
7 changed files with 1262 additions and 0 deletions

View File

@ -0,0 +1,193 @@
"""
SecuBox Fabricator - Universal Constructor for SecuBox Components
"""
import streamlit as st
import subprocess
import json
import os
import requests
st.set_page_config(
page_title="SecuBox Fabricator",
page_icon="🔧",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS
st.markdown('''
<style>
.stApp { background-color: #0a0a0f; }
.main-header { font-size: 2.5rem; background: linear-gradient(90deg, #00ffff, #ff00ff);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
text-align: center; margin-bottom: 1rem; }
.stat-box { background: #111; border-radius: 8px; padding: 1rem; text-align: center; border: 1px solid #333; }
.stat-value { font-size: 2rem; color: #0ff; font-weight: bold; }
.stat-label { color: #666; font-size: 0.8rem; text-transform: uppercase; }
</style>
''', unsafe_allow_html=True)
def run_cmd(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
return result.stdout.strip()
except Exception as e:
return str(e)
def load_json(path):
try:
with open(path, 'r') as f:
return json.load(f)
except:
return {}
def count_uci(config, type_name):
out = run_cmd(f'uci show {config} 2>/dev/null | grep -c "={type_name}"')
try:
return int(out)
except:
return 0
# Header
st.markdown('<div class="main-header">SecuBox Fabricator</div>', unsafe_allow_html=True)
st.markdown('<p style="text-align:center; color:#666">Universal Constructor for SecuBox Components</p>', unsafe_allow_html=True)
# Load live stats
health = load_json('/tmp/secubox/health-status.json')
crowdsec = load_json('/tmp/secubox/crowdsec-stats.json')
mitmproxy = load_json('/tmp/secubox/mitmproxy-stats.json')
# Count actual resources
streamlit_instances = count_uci('streamlit', 'instance')
streamlit_apps = int(run_cmd('ls -1d /srv/streamlit/apps/*/ 2>/dev/null | wc -l') or '0')
blog_sites = count_uci('metablogizer', 'site')
haproxy_vhosts = count_uci('haproxy', 'vhost')
collectors = int(run_cmd('ls /tmp/secubox/*.json 2>/dev/null | wc -l') or '0')
# Stats row 1
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown(f'''
<div class="stat-box">
<div class="stat-value" style="color:#ff00ff">{streamlit_instances}</div>
<div class="stat-label">Streamlit Instances</div>
</div>
''', unsafe_allow_html=True)
with col2:
st.markdown(f'''
<div class="stat-box">
<div class="stat-value" style="color:#00ff88">{blog_sites}</div>
<div class="stat-label">Blog Sites</div>
</div>
''', unsafe_allow_html=True)
with col3:
st.markdown(f'''
<div class="stat-box">
<div class="stat-value" style="color:#ffaa00">{haproxy_vhosts}</div>
<div class="stat-label">HAProxy Vhosts</div>
</div>
''', unsafe_allow_html=True)
with col4:
score = health.get('score', 0)
color = '#0f0' if score >= 80 else '#fa0' if score >= 50 else '#f00'
st.markdown(f'''
<div class="stat-box">
<div class="stat-value" style="color:{color}">{score}</div>
<div class="stat-label">Health Score</div>
</div>
''', unsafe_allow_html=True)
# Stats row 2
col1, col2, col3, col4 = st.columns(4)
with col1:
bans = crowdsec.get('bans', 0)
st.markdown(f'''
<div class="stat-box">
<div class="stat-value" style="color:#f00">{bans}</div>
<div class="stat-label">CrowdSec Bans</div>
</div>
''', unsafe_allow_html=True)
with col2:
threats = mitmproxy.get('threats_24h', 0)
st.markdown(f'''
<div class="stat-box">
<div class="stat-value" style="color:#fa0">{threats}</div>
<div class="stat-label">WAF Threats (24h)</div>
</div>
''', unsafe_allow_html=True)
with col3:
st.markdown(f'''
<div class="stat-box">
<div class="stat-value">{streamlit_apps}</div>
<div class="stat-label">Installed Apps</div>
</div>
''', unsafe_allow_html=True)
with col4:
st.markdown(f'''
<div class="stat-box">
<div class="stat-value">{collectors}</div>
<div class="stat-label">Stats Collectors</div>
</div>
''', unsafe_allow_html=True)
st.markdown('---')
# Quick Actions
st.subheader('⚡ Quick Actions')
col1, col2, col3, col4 = st.columns(4)
with col1:
if st.button('📊 Refresh Stats', use_container_width=True):
run_cmd('/usr/sbin/secubox-stats-collector.sh all')
run_cmd('/usr/sbin/secubox-heartbeat-status > /tmp/secubox/heartbeat.json')
st.success('Stats refreshed!')
st.rerun()
with col2:
if st.button('🔄 Reload HAProxy', use_container_width=True):
run_cmd('haproxyctl reload')
st.success('HAProxy reloaded!')
with col3:
if st.button('📡 Sync DNS', use_container_width=True):
run_cmd('dnsctl sync')
st.success('DNS synced!')
with col4:
if st.button('🔒 CrowdSec Reload', use_container_width=True):
run_cmd('/etc/init.d/crowdsec reload')
st.success('CrowdSec reloaded!')
st.markdown('---')
# Service Status
st.subheader('🔌 Service Status')
services = health.get('services', {})
col1, col2, col3, col4, col5 = st.columns(5)
def svc_status(name, status):
icon = '🟢' if status else '🔴'
return f"{icon} {name}"
with col1:
st.markdown(svc_status('DNS', services.get('dns', 0)))
with col2:
st.markdown(svc_status('BIND', services.get('bind', 0)))
with col3:
st.markdown(svc_status('CrowdSec', services.get('crowdsec', 0)))
with col4:
st.markdown(svc_status('HAProxy', services.get('haproxy', 0)))
with col5:
st.markdown(svc_status('mitmproxy', services.get('mitmproxy', 0)))
st.markdown('---')
st.info('👈 Use the sidebar to access: **Collectors** | **Apps** | **Blogs** | **Services** | **Widgets**')

View File

@ -0,0 +1,188 @@
"""
Collectors - Stats Collection Scripts Manager
"""
import streamlit as st
import subprocess
import json
import os
st.set_page_config(page_title="Collectors - Fabricator", page_icon="📊", layout="wide")
st.title('📊 Stats Collectors')
st.markdown('Manage stats collection scripts and JSON cache files')
def run_cmd(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
return result.stdout.strip(), result.returncode
except Exception as e:
return str(e), 1
def load_json(path):
try:
with open(path, 'r') as f:
return json.load(f)
except:
return None
def get_collectors():
"""List stats collector scripts"""
collectors = []
scripts = [
'/usr/sbin/secubox-stats-collector.sh',
'/usr/sbin/secubox-heartbeat-status',
'/usr/sbin/crowdsec-stats-gen.sh',
'/usr/sbin/mitmproxy-stats-gen.sh',
'/usr/sbin/firewall-stats-gen.sh',
'/usr/sbin/metablogizer-json.sh'
]
for script in scripts:
exists = os.path.exists(script)
name = os.path.basename(script)
collectors.append({
'path': script,
'name': name,
'exists': exists
})
return collectors
def get_json_cache_files():
"""List JSON cache files in /tmp/secubox/"""
cache = []
out, _ = run_cmd('ls -la /tmp/secubox/*.json 2>/dev/null')
for line in out.split('\n'):
if '.json' in line and not line.startswith('l'): # skip symlinks
parts = line.split()
if len(parts) >= 9:
size = parts[4]
time = ' '.join(parts[5:8])
path = parts[8]
name = os.path.basename(path)
cache.append({
'path': path,
'name': name,
'size': size,
'time': time
})
return cache
# Installed Collectors
st.subheader('🔧 Installed Collector Scripts')
collectors = get_collectors()
for col in collectors:
col1, col2, col3 = st.columns([3, 1, 1])
with col1:
status = '🟢' if col['exists'] else '🔴'
st.markdown(f"**{status} {col['name']}**")
st.caption(col['path'])
with col2:
if col['exists']:
if st.button('▶️ Run', key=f"run_{col['name']}", use_container_width=True):
with st.spinner('Running...'):
if 'heartbeat' in col['name']:
out, rc = run_cmd(f"{col['path']} > /tmp/secubox/heartbeat.json 2>&1")
else:
out, rc = run_cmd(f"{col['path']} all 2>&1")
if rc == 0:
st.success('Done!')
else:
st.error(f'Error: {out[:100]}')
with col3:
if col['exists']:
if st.button('📝 View', key=f"view_{col['name']}", use_container_width=True):
content, _ = run_cmd(f"head -50 {col['path']}")
st.code(content, language='bash')
st.markdown('---')
# JSON Cache Files
st.subheader('📦 JSON Cache Files (/tmp/secubox/)')
cache_files = get_json_cache_files()
if cache_files:
for cf in cache_files:
col1, col2, col3, col4 = st.columns([3, 1, 1, 1])
with col1:
st.markdown(f"**{cf['name']}**")
st.caption(f"{cf['size']} bytes | {cf['time']}")
with col2:
if st.button('👁️ View', key=f"view_cache_{cf['name']}", use_container_width=True):
data = load_json(cf['path'])
if data:
st.json(data)
else:
st.error('Failed to parse JSON')
with col3:
if st.button('🔄 Refresh', key=f"refresh_{cf['name']}", use_container_width=True):
# Determine which collector generates this file
if 'crowdsec' in cf['name']:
run_cmd('/usr/sbin/secubox-stats-collector.sh crowdsec')
elif 'mitmproxy' in cf['name']:
run_cmd('/usr/sbin/secubox-stats-collector.sh mitmproxy')
elif 'firewall' in cf['name']:
run_cmd('/usr/sbin/secubox-stats-collector.sh firewall')
elif 'health' in cf['name']:
run_cmd('/usr/sbin/secubox-stats-collector.sh health')
elif 'heartbeat' in cf['name']:
run_cmd('/usr/sbin/secubox-heartbeat-status > /tmp/secubox/heartbeat.json')
st.success('Refreshed!')
st.rerun()
with col4:
data = load_json(cf['path'])
if data:
st.caption(f"{len(data)} keys" if isinstance(data, dict) else f"{len(data)} items")
else:
st.info('No cache files found')
if st.button('🔄 Generate All Stats'):
run_cmd('/usr/sbin/secubox-stats-collector.sh all')
st.rerun()
st.markdown('---')
# Cron Jobs
st.subheader('⏰ Cron Schedule')
cron_out, _ = run_cmd('crontab -l 2>/dev/null | grep -E "(secubox|stats|heartbeat)"')
if cron_out:
st.code(cron_out)
else:
st.info('No stats-related cron jobs found')
if st.button(' Add Default Cron Schedule'):
run_cmd('(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/sbin/secubox-stats-collector.sh all >/dev/null 2>&1") | crontab -')
run_cmd('(crontab -l 2>/dev/null; echo "* * * * * /usr/sbin/secubox-heartbeat-status > /tmp/secubox/heartbeat.json 2>/dev/null") | crontab -')
st.success('Cron jobs added!')
st.rerun()
st.markdown('---')
# Quick Actions
st.subheader('⚡ Quick Actions')
col1, col2, col3 = st.columns(3)
with col1:
if st.button('🔄 Collect All Stats', type='primary', use_container_width=True):
with st.spinner('Collecting...'):
run_cmd('/usr/sbin/secubox-stats-collector.sh all')
run_cmd('/usr/sbin/secubox-heartbeat-status > /tmp/secubox/heartbeat.json')
st.success('All stats collected!')
st.rerun()
with col2:
if st.button('🧹 Clear Cache', use_container_width=True):
run_cmd('rm -f /tmp/secubox/*.json')
st.success('Cache cleared!')
st.rerun()
with col3:
if st.button('📊 View Summary', use_container_width=True):
out, _ = run_cmd('ls -lh /tmp/secubox/*.json 2>/dev/null | awk \'{print $9 ": " $5}\'')
st.code(out or 'No cache files')

View File

@ -0,0 +1,181 @@
"""
Apps - Streamlit Application Deployer
"""
import streamlit as st
import subprocess
import json
import requests
st.set_page_config(page_title="Apps - Fabricator", page_icon="🚀", layout="wide")
st.title('🚀 Streamlit Apps')
st.markdown('Deploy and manage Streamlit applications')
def run_cmd(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
return result.stdout.strip(), result.returncode
except Exception as e:
return str(e), 1
def test_url(url):
"""Test if URL is accessible"""
try:
r = requests.get(url, timeout=5, verify=False)
return r.status_code, r.elapsed.total_seconds()
except Exception as e:
return 0, str(e)
# List existing instances with test buttons
st.subheader('📱 Running Instances')
instances_raw, _ = run_cmd('uci show streamlit 2>/dev/null | grep "=instance"')
instances = []
for line in instances_raw.split('\n'):
if '=instance' in line:
name = line.split('.')[1].split('=')[0]
port, _ = run_cmd(f'uci get streamlit.{name}.port 2>/dev/null')
app, _ = run_cmd(f'uci get streamlit.{name}.app 2>/dev/null')
enabled, _ = run_cmd(f'uci get streamlit.{name}.enabled 2>/dev/null')
instances.append({'name': name, 'port': port, 'app': app, 'enabled': enabled == '1'})
if instances:
for inst in instances:
col1, col2, col3, col4, col5 = st.columns([2, 1, 1, 1, 1])
with col1:
status = '🟢' if inst['enabled'] else '🔴'
st.markdown(f"**{status} {inst['name']}** ({inst['app']})")
with col2:
st.caption(f"Port: {inst['port']}")
with col3:
if st.button('🧪 Test', key=f"test_{inst['name']}", use_container_width=True):
url = f"http://127.0.0.1:{inst['port']}/_stcore/health"
code, time_or_err = test_url(url)
if code == 200:
st.success(f'OK ({time_or_err:.2f}s)')
else:
st.error(f'Failed: {time_or_err}')
with col4:
if st.button('🔄 Restart', key=f"restart_{inst['name']}", use_container_width=True):
run_cmd(f"streamlitctl instance stop {inst['name']}")
run_cmd(f"streamlitctl instance start {inst['name']}")
st.success('Restarted!')
st.rerun()
with col5:
if st.button('🌐 Open', key=f"open_{inst['name']}", use_container_width=True):
st.markdown(f"[Open in new tab](http://192.168.255.1:{inst['port']})")
else:
st.info('No instances configured')
st.markdown('---')
# Bulk actions
st.subheader('⚡ Bulk Actions')
col1, col2, col3 = st.columns(3)
with col1:
if st.button('🧪 Test All Instances', use_container_width=True):
results = []
for inst in instances:
url = f"http://127.0.0.1:{inst['port']}/_stcore/health"
code, time_or_err = test_url(url)
status = '' if code == 200 else ''
results.append(f"{status} {inst['name']} ({inst['port']})")
st.code('\n'.join(results))
with col2:
if st.button('🔄 Restart All', use_container_width=True):
run_cmd('/etc/init.d/streamlit restart')
st.success('All instances restarted!')
with col3:
if st.button('📊 Regenerate Stats', use_container_width=True):
run_cmd('/usr/sbin/secubox-stats-collector.sh all')
run_cmd('/usr/sbin/metablogizer-json.sh')
st.success('Stats regenerated!')
st.markdown('---')
# Deploy new app
st.subheader(' Deploy New App')
col1, col2 = st.columns(2)
with col1:
app_name = st.text_input('App Name', placeholder='my-app')
app_port = st.number_input('Port', min_value=8500, max_value=8599, value=8520, step=1)
with col2:
deploy_method = st.radio('Deploy Method', ['Upload ZIP', 'From Template', 'Git Clone'])
if deploy_method == 'Upload ZIP':
uploaded_file = st.file_uploader('Upload ZIP archive', type=['zip'])
if uploaded_file and app_name:
if st.button('🚀 Deploy from ZIP', type='primary'):
with open(f'/tmp/{app_name}.zip', 'wb') as f:
f.write(uploaded_file.getvalue())
out, rc = run_cmd(f'streamlitctl app deploy {app_name} /tmp/{app_name}.zip 2>&1')
if rc == 0:
run_cmd(f'streamlitctl instance add {app_name} {app_port}')
run_cmd(f'streamlitctl instance start {app_name}')
st.success(f'Deployed {app_name} on port {app_port}!')
st.rerun()
else:
st.error(f'Deploy failed: {out}')
elif deploy_method == 'From Template':
templates = {
'basic': 'Basic Streamlit app with sidebar',
'dashboard': 'Dashboard with charts and metrics',
'form': 'Form-based data entry app',
}
template = st.selectbox('Template', list(templates.keys()), format_func=lambda x: templates[x])
if app_name and st.button('🚀 Create from Template', type='primary'):
run_cmd(f'streamlitctl app create {app_name}')
run_cmd(f'streamlitctl instance add {app_name} {app_port}')
run_cmd(f'streamlitctl instance start {app_name}')
st.success(f'Created {app_name} on port {app_port}!')
st.rerun()
elif deploy_method == 'Git Clone':
git_repo = st.text_input('Git Repository URL', placeholder='https://github.com/user/repo.git')
if app_name and git_repo and st.button('🚀 Clone and Deploy', type='primary'):
out, rc = run_cmd(f'streamlitctl gitea clone {app_name} {git_repo} 2>&1')
if rc == 0:
run_cmd(f'streamlitctl instance add {app_name} {app_port}')
run_cmd(f'streamlitctl instance start {app_name}')
st.success(f'Cloned and deployed {app_name}!')
st.rerun()
else:
st.error(f'Clone failed: {out}')
st.markdown('---')
# Emancipate
st.subheader('🌐 Emancipate (Expose to Internet)')
col1, col2 = st.columns(2)
with col1:
emancipate_instance = st.selectbox('Instance to Emancipate',
[i['name'] for i in instances] if instances else ['No instances'])
with col2:
emancipate_domain = st.text_input('Domain (optional)', placeholder='app.gk2.secubox.in')
if emancipate_instance and emancipate_instance != 'No instances':
if st.button('🚀 Emancipate!', type='primary', use_container_width=True):
cmd = f'streamlitctl emancipate {emancipate_instance}'
if emancipate_domain:
cmd += f' {emancipate_domain}'
out, rc = run_cmd(cmd + ' 2>&1')
if 'COMPLETE' in out or rc == 0:
st.success(f'Emancipated {emancipate_instance}!')
st.code(out[-500:] if len(out) > 500 else out)
else:
st.error(f'Emancipation failed')
st.code(out)

View File

@ -0,0 +1,185 @@
"""
Blogs - MetaBlogizer Site Manager
"""
import streamlit as st
import subprocess
import json
import requests
st.set_page_config(page_title="Blogs - Fabricator", page_icon="📰", layout="wide")
st.title('📰 Blog Sites (MetaBlogizer)')
st.markdown('Create and manage static blog sites with Hugo')
def run_cmd(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
return result.stdout.strip(), result.returncode
except Exception as e:
return str(e), 1
def test_url(url):
"""Test if URL is accessible"""
try:
r = requests.get(url, timeout=5, verify=False)
return r.status_code, r.elapsed.total_seconds()
except Exception as e:
return 0, str(e)
def get_sites_from_uci():
"""Get metablogizer sites from UCI config"""
sites = []
raw, _ = run_cmd('uci show metablogizer 2>/dev/null | grep "=site"')
for line in raw.split('\n'):
if '=site' in line:
section = line.split('.')[1].split('=')[0]
name, _ = run_cmd(f'uci get metablogizer.{section}.name 2>/dev/null')
domain, _ = run_cmd(f'uci get metablogizer.{section}.domain 2>/dev/null')
port, _ = run_cmd(f'uci get metablogizer.{section}.port 2>/dev/null')
enabled, _ = run_cmd(f'uci get metablogizer.{section}.enabled 2>/dev/null')
emancipated, _ = run_cmd(f'uci get metablogizer.{section}.emancipated 2>/dev/null')
ssl, _ = run_cmd(f'uci get metablogizer.{section}.ssl 2>/dev/null')
sites.append({
'section': section,
'name': name or section.replace('site_', ''),
'domain': domain,
'port': port or '8900',
'enabled': enabled == '1',
'emancipated': emancipated == '1',
'ssl': ssl == '1'
})
return sites
# List existing sites
st.subheader('📚 Existing Blog Sites')
sites = get_sites_from_uci()
if sites:
for site in sites:
col1, col2, col3, col4, col5 = st.columns([3, 2, 1, 1, 1])
with col1:
status = '🟢' if site['enabled'] else '🔴'
exposed = '🌐' if site['emancipated'] else '🔒'
ssl_icon = '🔐' if site['ssl'] else ''
st.markdown(f"**{status} {site['name']}** {exposed}{ssl_icon}")
st.caption(f"[{site['domain']}](https://{site['domain']})")
with col2:
st.caption(f"Port: {site['port']}")
with col3:
if st.button('🧪 Test', key=f"test_{site['name']}", use_container_width=True):
url = f"https://{site['domain']}"
code, time_or_err = test_url(url)
if 200 <= code < 400:
st.success(f'{code} ({time_or_err:.2f}s)')
else:
st.error(f'{code}: {time_or_err}')
with col4:
if st.button('🔄 Rebuild', key=f"rebuild_{site['name']}", use_container_width=True):
out, rc = run_cmd(f"metablogizerctl build {site['name']} 2>&1")
if rc == 0:
st.success('Built!')
else:
st.error(f'Failed: {out[:100]}')
with col5:
if not site['emancipated']:
if st.button('🚀 Expose', key=f"emancipate_{site['name']}", use_container_width=True):
out, rc = run_cmd(f"metablogizerctl emancipate {site['name']} 2>&1")
if rc == 0:
st.success('Emancipated!')
st.rerun()
else:
st.error(f'Failed')
else:
if st.button('🌐 Open', key=f"open_{site['name']}", use_container_width=True):
st.markdown(f"[Open](https://{site['domain']})")
else:
st.info('No blog sites configured. Create one below!')
st.markdown('---')
# Bulk actions
st.subheader('⚡ Bulk Actions')
col1, col2, col3 = st.columns(3)
with col1:
if st.button('🧪 Test All Sites', use_container_width=True):
results = []
for site in sites:
url = f"https://{site['domain']}"
code, time_or_err = test_url(url)
status = '' if 200 <= code < 400 else ''
results.append(f"{status} {site['domain']} ({code})")
st.code('\n'.join(results))
with col2:
if st.button('🔄 Rebuild All', use_container_width=True):
for site in sites:
run_cmd(f"metablogizerctl build {site['name']} 2>&1")
st.success('All sites rebuilt!')
with col3:
if st.button('📊 Refresh Stats', use_container_width=True):
run_cmd('/usr/sbin/metablogizer-json.sh 2>/dev/null')
st.success('Stats refreshed!')
st.rerun()
st.markdown('---')
# Create new site
st.subheader(' Create New Blog Site')
col1, col2 = st.columns(2)
with col1:
site_name = st.text_input('Site Name', placeholder='my-blog')
site_domain = st.text_input('Domain', placeholder='blog.example.com')
with col2:
site_theme = st.selectbox('Theme', ['ananke', 'papermod', 'terminal', 'blowfish', 'stack'])
auto_emancipate = st.checkbox('Auto-emancipate (DNS + SSL + HAProxy)', value=True)
if site_name and site_domain:
if st.button('🚀 Create Blog Site', type='primary'):
# Create site
out, rc = run_cmd(f'metablogizerctl create {site_name} {site_domain} 2>&1')
if rc == 0:
st.success(f'Created blog site: {site_name}')
if auto_emancipate:
out2, rc2 = run_cmd(f'metablogizerctl emancipate {site_name} 2>&1')
if rc2 == 0:
st.success(f'Emancipated: https://{site_domain}')
else:
st.warning(f'Emancipation issue: {out2[:200]}')
st.rerun()
else:
st.error(f'Failed: {out}')
st.markdown('---')
# Upload ZIP for existing site
st.subheader('📦 Upload Content ZIP')
upload_site = st.selectbox('Target Site', [s['name'] for s in sites] if sites else ['No sites'])
uploaded_file = st.file_uploader('Upload Hugo content ZIP', type=['zip'])
if uploaded_file and upload_site and upload_site != 'No sites':
if st.button('📤 Upload & Deploy'):
with open(f'/tmp/{upload_site}.zip', 'wb') as f:
f.write(uploaded_file.getvalue())
# Find site path
site_path = f"/srv/metablogizer/sites/{upload_site}"
out, rc = run_cmd(f'unzip -o /tmp/{upload_site}.zip -d {site_path}/ 2>&1')
if rc == 0:
# Rebuild
run_cmd(f'metablogizerctl build {upload_site}')
st.success('Content deployed and rebuilt!')
st.rerun()
else:
st.error(f'Extract failed: {out}')

View File

@ -0,0 +1,294 @@
"""
Services - Exposure and Emancipation Manager
"""
import streamlit as st
import subprocess
import json
import requests
st.set_page_config(page_title="Services - Fabricator", page_icon="🌐", layout="wide")
st.title('🌐 Service Exposure')
st.markdown('Peek, Poke, and Emancipate services')
def run_cmd(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
return result.stdout.strip(), result.returncode
except Exception as e:
return str(e), 1
def test_url(url):
"""Test if URL is accessible"""
try:
r = requests.get(url, timeout=5, verify=False)
return r.status_code, r.elapsed.total_seconds()
except Exception as e:
return 0, str(e)
def get_vhosts_from_uci():
"""Get HAProxy vhosts from UCI"""
vhosts = []
raw, _ = run_cmd('uci show haproxy 2>/dev/null | grep "=vhost"')
for line in raw.split('\n'):
if '=vhost' in line:
section = line.split('.')[1].split('=')[0]
domain, _ = run_cmd(f'uci get haproxy.{section}.domain 2>/dev/null')
backend, _ = run_cmd(f'uci get haproxy.{section}.backend 2>/dev/null')
ssl, _ = run_cmd(f'uci get haproxy.{section}.ssl 2>/dev/null')
enabled, _ = run_cmd(f'uci get haproxy.{section}.enabled 2>/dev/null')
original_backend, _ = run_cmd(f'uci get haproxy.{section}.original_backend 2>/dev/null')
# Check if mitmproxy inspection is active
mitmproxy_active = backend == 'mitmproxy_inspector'
actual_backend = original_backend if mitmproxy_active else backend
vhosts.append({
'section': section,
'domain': domain,
'backend': actual_backend,
'ssl': ssl == '1',
'enabled': enabled != '0',
'mitmproxy': mitmproxy_active
})
return vhosts
def get_backends_from_uci():
"""Get HAProxy backends from UCI"""
backends = []
raw, _ = run_cmd('uci show haproxy 2>/dev/null | grep "=backend"')
for line in raw.split('\n'):
if '=backend' in line:
section = line.split('.')[1].split('=')[0]
name, _ = run_cmd(f'uci get haproxy.{section}.name 2>/dev/null')
server, _ = run_cmd(f'uci get haproxy.{section}.server 2>/dev/null')
mode, _ = run_cmd(f'uci get haproxy.{section}.mode 2>/dev/null')
# Parse server line for port
port = ''
if server:
parts = server.split(':')
if len(parts) >= 2:
port = parts[-1].split()[0]
backends.append({
'section': section,
'name': name or section,
'server': server,
'port': port,
'mode': mode or 'http'
})
return backends
# Current Exposures (Vhosts)
st.subheader('📡 Current Exposures (HAProxy Vhosts)')
vhosts = get_vhosts_from_uci()
if vhosts:
for vhost in vhosts:
if not vhost['domain']:
continue
col1, col2, col3, col4 = st.columns([3, 2, 1, 1])
with col1:
ssl_icon = '🔐' if vhost['ssl'] else '🔓'
mitmproxy_icon = '🔍' if vhost['mitmproxy'] else ''
st.markdown(f"**{ssl_icon} {vhost['domain']}** {mitmproxy_icon}")
st.caption(f"Backend: {vhost['backend']}")
with col2:
status = '🟢 Active' if vhost['enabled'] else '🔴 Disabled'
st.markdown(status)
with col3:
if st.button('🧪 Test', key=f"test_vhost_{vhost['section']}", use_container_width=True):
proto = 'https' if vhost['ssl'] else 'http'
url = f"{proto}://{vhost['domain']}"
code, time_or_err = test_url(url)
if 200 <= code < 400:
st.success(f'{code}')
elif code >= 400:
st.warning(f'{code}')
else:
st.error(f'Fail')
with col4:
if st.button('🌐 Open', key=f"open_vhost_{vhost['section']}", use_container_width=True):
proto = 'https' if vhost['ssl'] else 'http'
st.markdown(f"[Open]({proto}://{vhost['domain']})")
else:
st.info('No vhosts configured')
st.markdown('---')
# Backends
st.subheader('🔌 HAProxy Backends')
backends = get_backends_from_uci()
if backends:
for backend in backends:
col1, col2, col3 = st.columns([2, 3, 1])
with col1:
st.markdown(f"**{backend['name']}**")
with col2:
st.caption(f"{backend['server']}")
with col3:
st.caption(f"Port: {backend['port']}")
else:
st.info('No backends configured')
st.markdown('---')
# Scan for local services (Peek)
st.subheader('🔍 Peek - Scan Local Services')
if st.button('🔍 Scan Listening Services', type='primary'):
with st.spinner('Scanning...'):
services = []
ports_out, _ = run_cmd('netstat -tlnp 2>/dev/null | grep LISTEN')
for line in ports_out.split('\n'):
if not line or 'LISTEN' not in line:
continue
parts = line.split()
if len(parts) >= 7:
addr_port = parts[3]
proc = parts[6] if len(parts) > 6 else '-'
if ':' in addr_port:
port = addr_port.split(':')[-1]
addr = addr_port.rsplit(':', 1)[0] or '0.0.0.0'
proc_name = proc.split('/')[1] if '/' in proc else proc
services.append({
'port': port,
'address': addr,
'process': proc_name
})
if services:
st.markdown("**Discovered Services:**")
for svc in sorted(services, key=lambda x: int(x['port']) if x['port'].isdigit() else 0):
col1, col2, col3, col4 = st.columns([1, 2, 2, 1])
with col1:
st.markdown(f"**:{svc['port']}**")
with col2:
exposable = '' if svc['address'] in ['0.0.0.0', '::'] else '⚠️ local'
st.write(f"{svc['address']} {exposable}")
with col3:
st.write(svc['process'])
with col4:
if svc['address'] in ['0.0.0.0', '::']:
if st.button('📤', key=f"expose_{svc['port']}", use_container_width=True):
st.session_state['expose_port'] = svc['port']
else:
st.info('No services found')
st.markdown('---')
# Emancipate service
st.subheader('🚀 Emancipate - Expose New Service')
col1, col2 = st.columns(2)
with col1:
default_port = int(st.session_state.get('expose_port', 8080))
service_port = st.number_input('Service Port', min_value=1, max_value=65535, value=default_port)
domain = st.text_input('Domain', placeholder='app.gk2.secubox.in')
with col2:
channels = st.multiselect('Exposure Channels',
['DNS/SSL (HAProxy)', 'Tor Hidden Service', 'Mesh (Vortex)'],
default=['DNS/SSL (HAProxy)'])
if service_port and domain:
backend_name = domain.replace('.', '_').replace('-', '_')
if st.button('🚀 EMANCIPATE', type='primary', use_container_width=True):
with st.spinner('Emancipating...'):
steps = []
# 1. Create DNS record
if 'DNS/SSL (HAProxy)' in channels:
# Extract subdomain
if '.secubox.in' in domain:
subdomain = domain.replace('.secubox.in', '')
elif '.maegia.tv' in domain:
subdomain = domain.replace('.maegia.tv', '')
else:
subdomain = domain.split('.')[0]
out, rc = run_cmd(f'dnsctl add A {subdomain} 82.67.100.75 2>&1')
steps.append(('DNS Record', rc == 0 or 'already' in out.lower(), out))
# 2. Create HAProxy backend + vhost
run_cmd(f'uci set haproxy.{backend_name}=backend')
run_cmd(f'uci set haproxy.{backend_name}.name="{backend_name}"')
run_cmd(f'uci set haproxy.{backend_name}.mode="http"')
run_cmd(f'uci set haproxy.{backend_name}.server="srv 127.0.0.1:{service_port} check"')
run_cmd(f'uci set haproxy.{backend_name}.enabled="1"')
run_cmd(f'uci set haproxy.{backend_name}_vhost=vhost')
run_cmd(f'uci set haproxy.{backend_name}_vhost.domain="{domain}"')
run_cmd(f'uci set haproxy.{backend_name}_vhost.backend="{backend_name}"')
run_cmd(f'uci set haproxy.{backend_name}_vhost.ssl="1"')
run_cmd(f'uci set haproxy.{backend_name}_vhost.ssl_redirect="1"')
run_cmd(f'uci set haproxy.{backend_name}_vhost.enabled="1"')
run_cmd('uci commit haproxy')
out, rc = run_cmd('haproxyctl generate && haproxyctl reload 2>&1')
steps.append(('HAProxy Vhost', rc == 0, out[-200:] if len(out) > 200 else out))
# 3. SSL Certificate
out, rc = run_cmd(f'haproxyctl cert add {domain} 2>&1')
steps.append(('SSL Certificate', 'success' in out.lower() or 'already' in out.lower(), out[-200:] if len(out) > 200 else out))
if 'Tor Hidden Service' in channels:
out, rc = run_cmd(f'secubox-exposure tor add {domain} {service_port} 2>&1')
steps.append(('Tor Hidden Service', rc == 0, out))
if 'Mesh (Vortex)' in channels:
out, rc = run_cmd(f'vortexctl mesh publish {domain} 2>&1')
steps.append(('Mesh Publish', rc == 0, out))
# Show results
for step_name, success, output in steps:
if success:
st.success(f'{step_name}')
else:
st.error(f'{step_name}: {output}')
st.markdown(f'**Service exposed at:** [https://{domain}](https://{domain})')
st.rerun()
st.markdown('---')
# Quick actions
st.subheader('⚡ Quick Actions')
col1, col2, col3 = st.columns(3)
with col1:
if st.button('🧪 Test All Vhosts', use_container_width=True):
results = []
for vhost in vhosts:
if vhost['domain']:
proto = 'https' if vhost['ssl'] else 'http'
url = f"{proto}://{vhost['domain']}"
code, _ = test_url(url)
status = '' if 200 <= code < 400 else '⚠️' if code >= 400 else ''
results.append(f"{status} {vhost['domain']} ({code})")
st.code('\n'.join(results))
with col2:
if st.button('🔄 Reload HAProxy', use_container_width=True):
run_cmd('haproxyctl reload')
st.success('HAProxy reloaded!')
with col3:
if st.button('📊 Regenerate Config', use_container_width=True):
run_cmd('haproxyctl generate')
run_cmd('haproxyctl reload')
st.success('Config regenerated!')

View File

@ -0,0 +1,220 @@
"""
Widgets - Dashboard Component Designer
"""
import streamlit as st
import subprocess
import json
st.set_page_config(page_title="Widgets - Fabricator", page_icon="🧩", layout="wide")
st.title('🧩 Widget Designer')
st.markdown('Create dashboard widgets and view live stats')
def run_cmd(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
return result.stdout.strip()
except Exception as e:
return str(e)
def load_json(path):
try:
with open(path, 'r') as f:
return json.load(f)
except Exception as e:
return None
# Available data sources
DATA_SOURCES = {
'Health Status': '/tmp/secubox/health-status.json',
'CrowdSec Stats': '/tmp/secubox/crowdsec-stats.json',
'CrowdSec Overview': '/tmp/secubox/crowdsec-overview.json',
'CrowdSec Detail': '/tmp/secubox/crowdsec-detail.json',
'mitmproxy WAF': '/tmp/secubox/mitmproxy-stats.json',
'Firewall Stats': '/tmp/secubox/firewall-stats.json',
'Heartbeat': '/tmp/secubox/heartbeat.json',
'Threats Detail': '/tmp/secubox/threats-detail.json',
'System Capacity': '/tmp/secubox/capacity.json'
}
# Live Stats Dashboard
st.subheader('📊 Live Stats Cache')
col1, col2 = st.columns([1, 3])
with col1:
selected_source = st.radio('Data Source', list(DATA_SOURCES.keys()))
with col2:
if selected_source:
path = DATA_SOURCES[selected_source]
data = load_json(path)
if data:
st.json(data)
# Quick stat extraction
st.markdown("**Quick Stats:**")
if selected_source == 'Health Status':
score = data.get('score', 0)
level = data.get('level', 'unknown')
color = '#0f0' if level == 'healthy' else '#fa0' if level == 'degraded' else '#f00'
st.markdown(f"<span style='font-size:2rem; color:{color}'>{score}</span> ({level})", unsafe_allow_html=True)
elif selected_source == 'CrowdSec Stats':
alerts = data.get('alerts', 0)
bans = data.get('bans', 0)
st.markdown(f"**Alerts:** {alerts} | **Bans:** {bans}")
elif selected_source == 'mitmproxy WAF':
threats = data.get('threats_24h', 0)
blocked = data.get('blocked_ips', 0)
st.markdown(f"**Threats (24h):** {threats} | **Blocked IPs:** {blocked}")
elif selected_source == 'Firewall Stats':
dropped = data.get('wan_dropped', 0)
conns = data.get('active_connections', 0)
st.markdown(f"**WAN Dropped:** {dropped} | **Active Connections:** {conns}")
elif selected_source == 'Heartbeat':
services = data.get('services', {})
online = sum(1 for v in services.values() if v)
total = len(services)
st.markdown(f"**Services:** {online}/{total} online")
else:
st.warning(f'No data at {path}')
if st.button('🔄 Generate Stats'):
run_cmd('/usr/sbin/secubox-stats-collector.sh all')
st.rerun()
st.markdown('---')
# Widget Builder
st.subheader('🔧 Widget Builder')
# Widget templates
WIDGET_TEMPLATES = {
'stat_card': '''<div class="stat-box">
<div class="stat-value" style="color:{color}">{value}</div>
<div class="stat-label">{label}</div>
</div>''',
'status_dot': '''<div class="health-indicator">
<span class="health-dot {status_class}"></span>{label}
</div>''',
'service_link': '''<div class="service">
<a href="{url}" target="_blank">{name}</a>
<div class="service-meta">{desc}</div>
</div>''',
'progress_bar': '''<div class="progress-container">
<div class="progress-label">{label}</div>
<div class="progress-bar">
<div class="progress-fill" style="width:{percent}%; background:{color}"></div>
</div>
<div class="progress-value">{value}</div>
</div>'''
}
col1, col2 = st.columns([1, 2])
with col1:
widget_type = st.selectbox('Widget Type', list(WIDGET_TEMPLATES.keys()))
if widget_type == 'stat_card':
value = st.text_input('Value', value='100')
label = st.text_input('Label', value='Health Score')
color = st.color_picker('Color', value='#00ffff')
elif widget_type == 'status_dot':
label = st.text_input('Label', value='CrowdSec')
status = st.selectbox('Status', ['green', 'yellow', 'red'])
elif widget_type == 'service_link':
name = st.text_input('Name', value='Console')
url = st.text_input('URL', value='https://console.gk2.secubox.in')
desc = st.text_input('Description', value='LuCI Admin')
elif widget_type == 'progress_bar':
label = st.text_input('Label', value='CPU Usage')
percent = st.slider('Percent', 0, 100, 65)
value = st.text_input('Value Text', value='65%')
color = st.color_picker('Bar Color', value='#00ff88')
with col2:
st.subheader('Preview')
# Generate widget HTML
if widget_type == 'stat_card':
widget_html = WIDGET_TEMPLATES['stat_card'].format(value=value, label=label, color=color)
elif widget_type == 'status_dot':
status_class = f'health-{status}'
widget_html = WIDGET_TEMPLATES['status_dot'].format(label=label, status_class=status_class)
elif widget_type == 'service_link':
widget_html = WIDGET_TEMPLATES['service_link'].format(name=name, url=url, desc=desc)
elif widget_type == 'progress_bar':
widget_html = WIDGET_TEMPLATES['progress_bar'].format(label=label, percent=percent, value=value, color=color)
else:
widget_html = '<div>Widget preview</div>'
# CSS for preview
css = '''
<style>
.stat-box { background: #111; border-radius: 8px; padding: 1rem; text-align: center; border: 1px solid #333; }
.stat-value { font-size: 2rem; font-weight: bold; }
.stat-label { color: #666; font-size: 0.8rem; text-transform: uppercase; }
.health-indicator { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; }
.health-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.health-green { background: #0f0; box-shadow: 0 0 8px #0f0; }
.health-yellow { background: #fa0; box-shadow: 0 0 8px #fa0; }
.health-red { background: #f00; box-shadow: 0 0 8px #f00; }
.service { background: rgba(255,255,255,0.05); border: 1px solid #333; border-radius: 6px; padding: 0.75rem; }
.service a { color: #0ff; text-decoration: none; font-weight: bold; }
.service-meta { font-size: 0.7rem; color: #666; margin-top: 0.25rem; }
.progress-container { padding: 0.5rem; }
.progress-label { font-size: 0.8rem; color: #aaa; margin-bottom: 0.25rem; }
.progress-bar { background: #222; border-radius: 4px; height: 8px; overflow: hidden; }
.progress-fill { height: 100%; transition: width 0.3s; }
.progress-value { font-size: 0.7rem; color: #666; text-align: right; margin-top: 0.25rem; }
</style>
'''
st.markdown(css + widget_html, unsafe_allow_html=True)
st.subheader('Generated Code')
st.code(widget_html, language='html')
st.markdown('---')
# All Stats Summary
st.subheader('📈 All Stats Summary')
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("**Health**")
health = load_json('/tmp/secubox/health-status.json')
if health:
score = health.get('score', 0)
color = '#0f0' if score >= 80 else '#fa0' if score >= 50 else '#f00'
st.markdown(f"<div style='font-size:2rem; color:{color}'>{score}</div>", unsafe_allow_html=True)
st.caption(health.get('level', 'unknown'))
with col2:
st.markdown("**CrowdSec**")
cs = load_json('/tmp/secubox/crowdsec-stats.json')
if cs:
st.metric("Alerts", cs.get('alerts', 0))
st.metric("Bans", cs.get('bans', 0))
with col3:
st.markdown("**WAF (mitmproxy)**")
waf = load_json('/tmp/secubox/mitmproxy-stats.json')
if waf:
st.metric("Threats 24h", waf.get('threats_24h', 0))
st.metric("Blocked IPs", waf.get('blocked_ips', 0))
st.markdown('---')
# Refresh all stats
if st.button('🔄 Refresh All Stats', use_container_width=True):
with st.spinner('Collecting stats...'):
run_cmd('/usr/sbin/secubox-stats-collector.sh all')
run_cmd('/usr/sbin/secubox-heartbeat-status > /tmp/secubox/heartbeat.json')
st.success('All stats refreshed!')
st.rerun()