From bfd2ed7c1f7c9ed93013457c764529e798b7c08d Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 7 Feb 2026 11:05:09 +0100 Subject: [PATCH] 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 --- .../srv/streamlit/apps/fabricator/app.py | 193 ++++++++++++ .../apps/fabricator/pages/01_๐Ÿ“Š_Collectors.py | 188 +++++++++++ .../apps/fabricator/pages/02_๐Ÿš€_Apps.py | 181 +++++++++++ .../apps/fabricator/pages/03_๐Ÿ“ฐ_Blogs.py | 185 +++++++++++ .../apps/fabricator/pages/04_๐ŸŒ_Services.py | 294 ++++++++++++++++++ .../apps/fabricator/pages/05_๐Ÿงฉ_Widgets.py | 220 +++++++++++++ .../apps/fabricator/requirements.txt | 1 + 7 files changed, 1262 insertions(+) create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/app.py create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/01_๐Ÿ“Š_Collectors.py create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/02_๐Ÿš€_Apps.py create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/03_๐Ÿ“ฐ_Blogs.py create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/04_๐ŸŒ_Services.py create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/05_๐Ÿงฉ_Widgets.py create mode 100644 package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/requirements.txt diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/app.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/app.py new file mode 100644 index 00000000..78be005b --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/app.py @@ -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(''' + +''', 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('
SecuBox Fabricator
', unsafe_allow_html=True) +st.markdown('

Universal Constructor for SecuBox Components

', 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''' +
+
{streamlit_instances}
+
Streamlit Instances
+
+ ''', unsafe_allow_html=True) + +with col2: + st.markdown(f''' +
+
{blog_sites}
+
Blog Sites
+
+ ''', unsafe_allow_html=True) + +with col3: + st.markdown(f''' +
+
{haproxy_vhosts}
+
HAProxy Vhosts
+
+ ''', 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''' +
+
{score}
+
Health Score
+
+ ''', unsafe_allow_html=True) + +# Stats row 2 +col1, col2, col3, col4 = st.columns(4) + +with col1: + bans = crowdsec.get('bans', 0) + st.markdown(f''' +
+
{bans}
+
CrowdSec Bans
+
+ ''', unsafe_allow_html=True) + +with col2: + threats = mitmproxy.get('threats_24h', 0) + st.markdown(f''' +
+
{threats}
+
WAF Threats (24h)
+
+ ''', unsafe_allow_html=True) + +with col3: + st.markdown(f''' +
+
{streamlit_apps}
+
Installed Apps
+
+ ''', unsafe_allow_html=True) + +with col4: + st.markdown(f''' +
+
{collectors}
+
Stats Collectors
+
+ ''', 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**') diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/01_๐Ÿ“Š_Collectors.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/01_๐Ÿ“Š_Collectors.py new file mode 100644 index 00000000..00632988 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/01_๐Ÿ“Š_Collectors.py @@ -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') diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/02_๐Ÿš€_Apps.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/02_๐Ÿš€_Apps.py new file mode 100644 index 00000000..4dd23f60 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/02_๐Ÿš€_Apps.py @@ -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) diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/03_๐Ÿ“ฐ_Blogs.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/03_๐Ÿ“ฐ_Blogs.py new file mode 100644 index 00000000..c80960b7 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/03_๐Ÿ“ฐ_Blogs.py @@ -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}') diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/04_๐ŸŒ_Services.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/04_๐ŸŒ_Services.py new file mode 100644 index 00000000..96a647f9 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/04_๐ŸŒ_Services.py @@ -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!') diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/05_๐Ÿงฉ_Widgets.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/05_๐Ÿงฉ_Widgets.py new file mode 100644 index 00000000..288403e9 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/pages/05_๐Ÿงฉ_Widgets.py @@ -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"{score} ({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': '''
+
{value}
+
{label}
+
''', + 'status_dot': '''
+ {label} +
''', + 'service_link': '''
+ {name} +
{desc}
+
''', + 'progress_bar': '''
+
{label}
+
+
+
+
{value}
+
''' +} + +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 = '
Widget preview
' + + # CSS for preview + css = ''' + + ''' + + 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"
{score}
", 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() diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/requirements.txt b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/requirements.txt new file mode 100644 index 00000000..db2182f6 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/fabricator/requirements.txt @@ -0,0 +1 @@ +streamlit>=1.30.0