secubox-deb/scripts/generate-secubox-yaml.py
CyberMind-FR bd7dda0c6f feat(secubox): complete meta-script generator Tasks 14-17
Task 14: Arch Profiles
- Add profiles/arch/arm64.yaml and profiles/arch/amd64.yaml
- Add ResolveWithArch() to merger for base → arch → tier chain
- Add tests for arch profile resolution

Task 15: Package secubox.yaml for All Packages
- Add scripts/generate-secubox-yaml.py generator script
- Generate 131 debian/secubox.yaml files with:
  - Category detection (security, network, system, etc.)
  - Tier assignment (all, lite, standard, pro)
  - API socket and UI path detection

Task 16: APT Repository Setup
- Add apt/conf/ reprepro configuration (multi-arch arm64/amd64)
- Add apt/hooks/lintian-check pre-publish validation
- Add scripts/apt-publish.sh and scripts/apt-sync.sh
- Add apt/README.md documentation

Task 17: CI Integration
- Add .github/workflows/build-secubox-cli.yml
- Build for linux-amd64 and linux-arm64
- Version injection via ldflags
- GitHub releases on tag push

Code Quality Fixes (Tasks 10-13):
- Add atomic writes for OTA boot control files (0600 perms)
- Fix NVME device extraction (nvme0n1p2 → nvme0n1)
- Add scanner.Err() checks in hardware detection
- Fix URL injection validation in fetch command
- Add proper cleanup on checksum failures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 05:32:29 +02:00

280 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
SecuBox-Deb :: generate-secubox-yaml.py
Generate debian/secubox.yaml files from existing debian/control files.
Usage:
python3 scripts/generate-secubox-yaml.py
python3 scripts/generate-secubox-yaml.py packages/secubox-crowdsec
CyberMind - Gerald Kerma
"""
import sys
from typing import Optional
import re
from pathlib import Path
# Category mapping based on package name patterns
CATEGORY_MAP = {
# Security
r'crowdsec|waf|auth|firewall|nac|hardening|cve|guardian': 'security',
# Network
r'wireguard|vpn|netmodes|network|dns|vortex|ddns|haproxy': 'network',
# Monitoring
r'netdata|metrics|grafana|prometheus|health|doctor': 'monitoring',
# System
r'core|system|hub|portal|admin|backup|config|daemon': 'system',
# Media
r'jellyfin|media|stream|plex|dlna': 'media',
# AI
r'ollama|ai-|llm|copilot|insights': 'ai',
# Publishing
r'metablog|publish|blog|cms|gitea|wiki': 'publishing',
# Dashboard
r'dashboard|c3box|soc|eye-remote': 'dashboard',
# VPN
r'tailscale|zerotier|nebula|mesh': 'vpn',
# Email
r'email|mail|smtp|imap': 'email',
# IoT
r'iot|mqtt|zigbee|zwave|home': 'iot',
# Communication
r'matrix|signal|chat|voice': 'communication',
}
# Tier mapping: packages exclusive to certain tiers
TIER_EXCLUSIVE = {
'pro': [
'secubox-dpi',
'secubox-ollama',
'secubox-jellyfin',
'secubox-matrix',
'secubox-nextcloud',
'secubox-gitea',
'secubox-ai-gateway',
'secubox-ai-insights',
],
'standard': [
'secubox-qos',
'secubox-cdn',
'secubox-waf',
'secubox-haproxy',
],
}
# Packages required for all tiers
REQUIRED_ALL = [
'secubox-core',
'secubox-hub',
'secubox-portal',
'secubox-system',
]
def detect_category(pkg_name: str) -> str:
"""Detect category from package name."""
name_lower = pkg_name.lower().replace('secubox-', '')
for pattern, category in CATEGORY_MAP.items():
if re.search(pattern, name_lower):
return category
return 'misc'
def detect_tier(pkg_name: str) -> str:
"""Detect minimum tier for package."""
if pkg_name in REQUIRED_ALL:
return 'all'
for tier, packages in TIER_EXCLUSIVE.items():
if pkg_name in packages:
return tier
return 'lite' # Available on all tiers by default
def parse_control(control_path: Path) -> dict:
"""Parse debian/control file and extract metadata."""
content = control_path.read_text()
result = {
'source': '',
'package': '',
'section': 'misc',
'architecture': 'all',
'depends': [],
'recommends': [],
'description': '',
'description_long': '',
}
# Parse Source stanza
source_match = re.search(r'^Source:\s*(.+)$', content, re.MULTILINE)
if source_match:
result['source'] = source_match.group(1).strip()
# Parse Package stanza
pkg_match = re.search(r'^Package:\s*(.+)$', content, re.MULTILINE)
if pkg_match:
result['package'] = pkg_match.group(1).strip()
# Parse Section
section_match = re.search(r'^Section:\s*(.+)$', content, re.MULTILINE)
if section_match:
result['section'] = section_match.group(1).strip()
# Parse Architecture
arch_match = re.search(r'^Architecture:\s*(.+)$', content, re.MULTILINE)
if arch_match:
result['architecture'] = arch_match.group(1).strip()
# Parse Depends (multi-line capable)
depends_match = re.search(r'^Depends:\s*(.+?)(?=^[A-Z]|\Z)', content, re.MULTILINE | re.DOTALL)
if depends_match:
deps_raw = depends_match.group(1)
# Clean up and split
deps_clean = re.sub(r'\s+', ' ', deps_raw).strip()
deps_list = [d.strip() for d in deps_clean.split(',') if d.strip()]
# Extract just package names (remove version constraints, alternatives)
deps_names = []
for dep in deps_list:
# Skip ${misc:Depends} etc
if dep.startswith('$'):
continue
# Handle alternatives (a | b)
if '|' in dep:
parts = dep.split('|')
dep = parts[0].strip()
# Remove version constraints
dep = re.sub(r'\s*\([^)]+\)', '', dep).strip()
if dep and not dep.startswith('$'):
deps_names.append(dep)
result['depends'] = deps_names
# Parse Recommends
recommends_match = re.search(r'^Recommends:\s*(.+)$', content, re.MULTILINE)
if recommends_match:
recs_raw = recommends_match.group(1)
recs_clean = re.sub(r'\s+', ' ', recs_raw).strip()
recs_list = [r.strip() for r in recs_clean.split(',') if r.strip()]
result['recommends'] = recs_list
# Parse Description
desc_match = re.search(r'^Description:\s*(.+?)(?=^[A-Z]|\Z)', content, re.MULTILINE | re.DOTALL)
if desc_match:
desc_lines = desc_match.group(1).strip().split('\n')
result['description'] = desc_lines[0].strip()
if len(desc_lines) > 1:
# Long description is indented with space or dot
long_desc = []
for line in desc_lines[1:]:
line = line.strip()
if line.startswith('.'):
long_desc.append('')
elif line:
long_desc.append(line)
result['description_long'] = '\n'.join(long_desc)
return result
def generate_secubox_yaml(pkg_dir: Path) -> Optional[str]:
"""Generate secubox.yaml content for a package."""
control_path = pkg_dir / 'debian' / 'control'
if not control_path.exists():
return None
meta = parse_control(control_path)
pkg_name = meta['package'] or pkg_dir.name
category = detect_category(pkg_name)
tier = detect_tier(pkg_name)
# Filter secubox-* dependencies
secubox_deps = [d for d in meta['depends'] if d.startswith('secubox-')]
lines = [
f"# debian/secubox.yaml",
f"# Auto-generated from debian/control",
f"",
f"name: {pkg_name}",
f"category: {category}",
f"tier: {tier}",
]
if meta['description']:
lines.append(f"description: \"{meta['description']}\"")
if secubox_deps:
lines.append(f"")
lines.append(f"depends:")
for dep in secubox_deps:
lines.append(f" - {dep}")
# Check for API socket
api_path = pkg_dir / 'api' / 'main.py'
if api_path.exists():
lines.append(f"")
lines.append(f"api:")
lines.append(f" socket: /run/secubox/{pkg_name.replace('secubox-', '')}.sock")
lines.append(f" health: /api/v1/{pkg_name.replace('secubox-', '')}/health")
# Check for www/ directory
www_path = pkg_dir / 'www'
if www_path.exists():
lines.append(f"")
lines.append(f"ui:")
lines.append(f" path: /srv/secubox/www/{pkg_name.replace('secubox-', '')}")
lines.append("")
return '\n'.join(lines)
def process_package(pkg_dir: Path) -> bool:
"""Process a single package directory."""
yaml_content = generate_secubox_yaml(pkg_dir)
if yaml_content is None:
print(f" SKIP: {pkg_dir.name} (no debian/control)")
return False
yaml_path = pkg_dir / 'debian' / 'secubox.yaml'
# Don't overwrite if exists with custom content
if yaml_path.exists():
existing = yaml_path.read_text()
if '# Custom' in existing or '# Manual' in existing:
print(f" SKIP: {pkg_dir.name} (custom secubox.yaml)")
return False
yaml_path.write_text(yaml_content)
print(f" OK: {pkg_dir.name}")
return True
def main():
script_dir = Path(__file__).parent
repo_root = script_dir.parent
packages_dir = repo_root / 'packages'
if len(sys.argv) > 1:
# Process specific package(s)
for pkg_path in sys.argv[1:]:
pkg_dir = Path(pkg_path)
if not pkg_dir.is_absolute():
pkg_dir = repo_root / pkg_path
if pkg_dir.exists():
process_package(pkg_dir)
else:
print(f" ERROR: {pkg_path} not found")
else:
# Process all packages
print(f"Generating secubox.yaml for packages in {packages_dir}...")
count = 0
for pkg_dir in sorted(packages_dir.iterdir()):
if pkg_dir.is_dir() and (pkg_dir / 'debian').exists():
if process_package(pkg_dir):
count += 1
print(f"\nGenerated {count} secubox.yaml files")
if __name__ == '__main__':
main()