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:
parent
9884965e2b
commit
bfd2ed7c1f
@ -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**')
|
||||
@ -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')
|
||||
@ -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)
|
||||
@ -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}')
|
||||
@ -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!')
|
||||
@ -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()
|
||||
@ -0,0 +1 @@
|
||||
streamlit>=1.30.0
|
||||
Loading…
Reference in New Issue
Block a user