feat(secubox): add profile inheritance merger

- Implement Merger with Resolve() for inheritance chain
- Merge packages, kernel, services, sysctl, features
- Add tier-lite, tier-standard, tier-pro profiles
- Handle excluded packages removal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-05-10 20:35:04 +02:00
parent c9da45fa21
commit 2bbecfdc5f
5 changed files with 256 additions and 0 deletions

View File

@ -0,0 +1,143 @@
// cmd/secubox/internal/profile/merger.go
package profile
import (
"fmt"
"path/filepath"
)
// Merger resolves profile inheritance
type Merger struct {
profilesDir string
cache map[string]*Profile
}
// NewMerger creates a new profile merger
func NewMerger(profilesDir string) *Merger {
return &Merger{
profilesDir: profilesDir,
cache: make(map[string]*Profile),
}
}
// Resolve loads a profile and merges all inherited profiles
func (m *Merger) Resolve(name string) (*Profile, error) {
// Check cache
if p, ok := m.cache[name]; ok {
return p, nil
}
// Load profile
path := filepath.Join(m.profilesDir, name+".yaml")
p, err := Load(path)
if err != nil {
return nil, fmt.Errorf("load profile %s: %w", name, err)
}
// If no inheritance, return as-is
if p.Inherits == "" {
m.cache[name] = p
return p, nil
}
// Resolve parent first
parent, err := m.Resolve(p.Inherits)
if err != nil {
return nil, fmt.Errorf("resolve parent %s: %w", p.Inherits, err)
}
// Merge parent into child
merged := m.merge(parent, p)
m.cache[name] = merged
return merged, nil
}
// merge combines parent and child profiles (child overrides parent)
func (m *Merger) merge(parent, child *Profile) *Profile {
result := &Profile{
Name: child.Name,
Description: child.Description,
}
// Merge packages (combine required, child excluded wins)
result.Packages.Required = append([]string{}, parent.Packages.Required...)
result.Packages.Required = append(result.Packages.Required, child.Packages.Required...)
result.Packages.Required = unique(result.Packages.Required)
result.Packages.Excluded = child.Packages.Excluded
// Remove excluded packages from required
result.Packages.Required = subtract(result.Packages.Required, result.Packages.Excluded)
// Kernel: child overrides or inherits
result.Kernel = parent.Kernel
if child.Kernel.Version != "" {
result.Kernel.Version = child.Kernel.Version
}
if len(child.Kernel.Modules.Enable) > 0 {
result.Kernel.Modules.Enable = append(result.Kernel.Modules.Enable, child.Kernel.Modules.Enable...)
}
if len(child.Kernel.Modules.Blacklist) > 0 {
result.Kernel.Modules.Blacklist = child.Kernel.Modules.Blacklist
}
// Services: merge
result.Services.Enable = append(parent.Services.Enable, child.Services.Enable...)
result.Services.Enable = unique(result.Services.Enable)
result.Services.Disable = child.Services.Disable
// Sysctl: merge maps
result.Sysctl = make(map[string]interface{})
for k, v := range parent.Sysctl {
result.Sysctl[k] = v
}
for k, v := range child.Sysctl {
result.Sysctl[k] = v
}
// Features: child overrides
result.Features = parent.Features
if child.Features.DPI != nil {
result.Features.DPI = child.Features.DPI
}
if child.Features.Swap != "" {
result.Features.Swap = child.Features.Swap
}
// LXC: explicit check since false is valid
result.Features.LXC = child.Features.LXC || parent.Features.LXC
// Constraints: child overrides
result.Constraints = child.Constraints
if result.Constraints.MinRAM == "" {
result.Constraints.MinRAM = parent.Constraints.MinRAM
}
return result
}
// unique removes duplicates from a slice
func unique(s []string) []string {
seen := make(map[string]bool)
result := []string{}
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
// subtract removes items in b from a
func subtract(a, b []string) []string {
exclude := make(map[string]bool)
for _, v := range b {
exclude[v] = true
}
result := []string{}
for _, v := range a {
if !exclude[v] {
result = append(result, v)
}
}
return result
}

View File

@ -34,3 +34,48 @@ packages:
t.Errorf("Packages.Required = %d, want 2", len(p.Packages.Required))
}
}
func TestMergeProfiles(t *testing.T) {
dir := t.TempDir()
// Create base profile
base := `
name: base
packages:
required:
- secubox-core
kernel:
version: "6.6"
`
basePath := filepath.Join(dir, "base.yaml")
os.WriteFile(basePath, []byte(base), 0644)
// Create child profile
child := `
name: child
inherits: base
packages:
required:
- secubox-crowdsec
excluded:
- secubox-dpi
`
childPath := filepath.Join(dir, "child.yaml")
os.WriteFile(childPath, []byte(child), 0644)
merger := NewMerger(dir)
result, err := merger.Resolve("child")
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
// Should have both base and child packages
if len(result.Packages.Required) != 2 {
t.Errorf("Packages.Required = %v, want 2 items", result.Packages.Required)
}
// Should inherit kernel version
if result.Kernel.Version != "6.6" {
t.Errorf("Kernel.Version = %q, want %q", result.Kernel.Version, "6.6")
}
}

27
profiles/tier-lite.yaml Normal file
View File

@ -0,0 +1,27 @@
# profiles/tier-lite.yaml
name: tier-lite
inherits: base
description: For 1-2GB RAM devices (ESPRESSObin)
constraints:
min_ram: 1G
max_ram: 2G
packages:
required:
- secubox-crowdsec
- secubox-wireguard
- secubox-netmodes
- secubox-nac
excluded:
- secubox-dpi
- secubox-ollama
- secubox-jellyfin
- secubox-matrix
- secubox-nextcloud
- secubox-gitea
features:
dpi: false
lxc: false
swap: 512M

16
profiles/tier-pro.yaml Normal file
View File

@ -0,0 +1,16 @@
# profiles/tier-pro.yaml
name: tier-pro
inherits: base
description: For 8GB+ RAM devices (MOCHAbin)
constraints:
min_ram: 8G
packages:
required:
- secubox-full
features:
dpi: inline
lxc: true
swap: "0"

View File

@ -0,0 +1,25 @@
# profiles/tier-standard.yaml
name: tier-standard
inherits: base
description: For 4GB RAM devices
constraints:
min_ram: 4G
max_ram: 8G
packages:
required:
- secubox-crowdsec
- secubox-wireguard
- secubox-netmodes
- secubox-nac
- secubox-dpi
- secubox-qos
- secubox-waf
- secubox-haproxy
- secubox-vhost
features:
dpi: mirror
lxc: true
swap: "0"