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': '''''',
+ 'status_dot': '''
+ {label}
+
''',
+ 'service_link': '''''',
+ 'progress_bar': ''''''
+}
+
+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