mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 19:43:10 +00:00
Compare commits
22 Commits
b859817ec9
...
2828c068b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 2828c068b7 | |||
| 197eba6328 | |||
| bb58789b58 | |||
| d6fe14d535 | |||
| 5f7b847435 | |||
| 7523c12fff | |||
| 3b99bcf4ec | |||
| 52463db1e7 | |||
| 6f59de25c7 | |||
| 7f793b98e8 | |||
| 0daed3c8d9 | |||
| ce82e13db1 | |||
| 4529b5c11a | |||
| 33b55d22f7 | |||
| 7b93160478 | |||
| 06ac00e64c | |||
| 01eac754f3 | |||
| 91dbe1834d | |||
| e6189bbaa5 | |||
| 5ccf862f05 | |||
| ca5450a408 | |||
| 462d271278 |
|
|
@ -2,6 +2,50 @@
|
|||
*Tracking completed milestones with dates*
|
||||
|
||||
---
|
||||
## 2026-05-12
|
||||
|
||||
### Session 150 — OPAD Doctrine Documents v2.4.0
|
||||
|
||||
**Goal:** Create the 5 foundational OPAD (Off-Path Active Defense) doctrinal documents for SecuBox-Deb migration to passive observation + packet injection architecture.
|
||||
|
||||
**Reference:** CM-WALL-OPAD-2026-05
|
||||
|
||||
**Architecture Overview:**
|
||||
- **OPAD Principle:** SecuBox observes traffic passively (port mirroring) and injects packets to neutralize threats, never sits in the data path
|
||||
- **4 Injection Primitives:** DNS-R (99%), DHCP-R (95%), RST-I (90%), ARP-R (98%)
|
||||
- **8 Invariants (INV-01 to INV-08):** Fail-silent, no forwarding, zero WAN surface, etc.
|
||||
- **3-Prong Profile:** Observation / Injection / Policy configuration structure
|
||||
|
||||
**Files Created:**
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `doctrine/opad/OPAD.md` | 671 | Core doctrine, principles, invariants, injection specs |
|
||||
| `doctrine/opad/CSPN.matrix.md` | 569 | ANSSI threat × capability matrix (36 threats, 72% coverage) |
|
||||
| `doctrine/opad/OPAD-OPERATIONS.md` | 948 | Operational guide, troubleshooting, 4R rollback |
|
||||
| `schemas/opad-profile.schema.json` | 365 | JSON Schema draft-07 for profile validation |
|
||||
| `common/secubox_core/opad/models.py` | 400 | Pydantic v2 models (OPADProfile, configs) |
|
||||
| `common/secubox_core/opad/__init__.py` | 85 | Package exports |
|
||||
| `tests/test_opad_schema.py` | 374 | 18 tests (JSON Schema + Pydantic equivalence) |
|
||||
| **Total** | **3412** | |
|
||||
|
||||
**Technical Notes:**
|
||||
- Pydantic v2 syntax: `@field_validator`, `ConfigDict`, `model_json_schema()`
|
||||
- JSON Schema draft-07 with `$defs` for reusable definitions
|
||||
- MAC address validation: `^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$`
|
||||
- Success rate constraints: 0.90 ≤ rate ≤ 1.0 for injection primitives
|
||||
|
||||
**Tests:** 18 passed (0.13s)
|
||||
- JSON Schema Draft7 validation
|
||||
- Pydantic model equivalence
|
||||
- MAC address format validation
|
||||
- Success rate bounds checking
|
||||
- Policy rule priority range (0-9999)
|
||||
|
||||
**Commits:** Cherry-picked to `feature/eye-remote-auto-mode`
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-11
|
||||
|
||||
### Session 147 — Fix Eye Agent Import Errors (#78)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,88 @@
|
|||
# WIP — Work In Progress
|
||||
*Mis à jour : 2026-05-11 (Session 147)*
|
||||
*Mis à jour : 2026-05-12 (Session 149)*
|
||||
|
||||
---
|
||||
|
||||
## ✅ Session 149: Mode Control API & Dashboard Integration
|
||||
|
||||
### Objective
|
||||
Add web-based mode control to the Eye Remote admin dashboard for safer manual gadget switching.
|
||||
|
||||
### Completed
|
||||
- **Mode Control API** (`mode_api.py`):
|
||||
- HTTP API on port 8081 (8080 used by CrowdSec)
|
||||
- `GET /api/status`: Current gadget mode and functions
|
||||
- `POST /api/switch/<mode>`: Switch composite/network/storage/silent/hid
|
||||
- `GET /health`: Health check endpoint
|
||||
- `GET /metrics`: System metrics (CPU, RAM, disk, temp, load, uptime)
|
||||
- HTML control panel with mode buttons
|
||||
|
||||
- **Dashboard Integration** (`admin.gk2.secubox.in/eye-remote/`):
|
||||
- USB Gadget Mode card with current status
|
||||
- Mode switch buttons (COMPOSITE, NETWORK, STORAGE, SILENT)
|
||||
- Live metrics display (CPU, RAM, Temp, Uptime)
|
||||
- Auto-refresh every 10 seconds
|
||||
|
||||
- **Infrastructure**:
|
||||
- Systemd service `secubox-mode-api.service` on Pi
|
||||
- Nginx proxy `/eye-remote/mode/` → `http://10.55.0.2:8081/`
|
||||
- Nginx proxy `/api/v1/eye-remote/` → `http://10.55.0.2:8081/`
|
||||
|
||||
### Commits
|
||||
- `462d2712` — feat(eye-remote): Add Mode Control API with metrics endpoint
|
||||
|
||||
### Files Changed
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `remote-ui/round/agent/api/mode_api.py` | NEW: Mode Control API with metrics |
|
||||
| `remote-ui/round/agent/auto_mode_controller.py` | Minor improvements |
|
||||
| `.gitignore` | Exclude backup images |
|
||||
|
||||
### Deployed To
|
||||
- Pi Zero W: `/home/pi/eye-remote/agent/api/mode_api.py`
|
||||
- MOCHAbin: nginx config updated for port 8081
|
||||
|
||||
### Current State
|
||||
- ✅ Mode API running on Pi (port 8081)
|
||||
- ✅ Dashboard shows Pi metrics and connection status
|
||||
- ✅ Mode control buttons integrated in admin panel
|
||||
- ✅ Health and metrics endpoints working
|
||||
- ✅ Composite mode active (ECM+ACM+RNDIS+Storage)
|
||||
|
||||
### Next Steps
|
||||
1. ⬜ Test mode switching from web UI
|
||||
2. ⬜ Add HID mode button to dashboard
|
||||
3. ⬜ Create new backup image v2.2.2 with mode API
|
||||
4. ⬜ Investigate gadget crash root cause during mode switch
|
||||
|
||||
---
|
||||
|
||||
## ✅ Session 148: Eye Remote Auto-Mode Stability
|
||||
|
||||
### Problem
|
||||
- Auto-mode controller caused USB gadget crashes when switching modes
|
||||
- Mode detection not working (always saw "none" instead of COMPOSITE)
|
||||
- Multiple display processes could run simultaneously
|
||||
- Network probing triggered unnecessary mode switches
|
||||
|
||||
### Solution Implemented
|
||||
- **Mode Detection**: Added `_detect_current_mode()` to read configfs functions
|
||||
- **Smart Probing**: Check network connectivity BEFORE attempting mode switches
|
||||
- **Single Instance**: PID file locking in `fallback_manager.py`
|
||||
- **Storage Recovery**: If storage not mounted after 30s, switch back to composite
|
||||
|
||||
### Commits
|
||||
- `b859817e` — fix(eye-remote): Improve auto-mode stability and single-instance display
|
||||
|
||||
### Files Changed
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `agent/auto_mode_controller.py` | Mode detection, smart probing, storage recovery |
|
||||
| `agent/display/fallback/fallback_manager.py` | PID lock for single instance |
|
||||
| `files/etc/secubox/eye-remote/gadget-setup.sh` | Lock file, cooldown safeguards |
|
||||
|
||||
### Status
|
||||
- ✅ Completed — mode detection and stability fixes deployed
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -49,3 +49,15 @@ kernel-build/*
|
|||
!kernel-build/README.md
|
||||
!kernel-build/patches/
|
||||
!kernel-build/build-kernel.sh
|
||||
backups/*.img.xz
|
||||
|
||||
# APT staging artifacts (regenerated by scripts/stage-apt-repo.sh)
|
||||
/output/repo/db/
|
||||
/output/repo/pool/
|
||||
/output/repo/dists/
|
||||
/output/repo/gpg/
|
||||
/output/repo/conf/
|
||||
/output/test-chroot/
|
||||
/output/manifests/
|
||||
/output/chroot-update.log
|
||||
/output/build.log
|
||||
|
|
|
|||
0
common/secubox_core/opad/.gitkeep
Normal file
0
common/secubox_core/opad/.gitkeep
Normal file
85
common/secubox_core/opad/__init__.py
Normal file
85
common/secubox_core/opad/__init__.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""
|
||||
SecuBox-Deb :: OPAD (Observe-Perturb-Apply-Decide) Models
|
||||
CyberMind — https://cybermind.fr
|
||||
Author: Gérald Kerma <gandalf@gk2.net>
|
||||
License: Proprietary / ANSSI CSPN candidate
|
||||
|
||||
OPAD v2.4.0 Pydantic models for 3-prong configuration:
|
||||
- Broche 1: Observation (passive monitoring)
|
||||
- Broche 2: Injection (perturbation primitives)
|
||||
- Broche 3: Policy (decision engine)
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
# Enums
|
||||
OPADMode,
|
||||
PolicyAction,
|
||||
DefaultAction,
|
||||
LogLevel,
|
||||
Protocol,
|
||||
DNSRaceMode,
|
||||
ProtocolMatch,
|
||||
|
||||
# Observation (Broche 1)
|
||||
ProtocolsConfig,
|
||||
FingerprintingConfig,
|
||||
ObservationConfig,
|
||||
|
||||
# Injection (Broche 2)
|
||||
DNSRaceConfig,
|
||||
QuarantinePool,
|
||||
DHCPRaceConfig,
|
||||
RSTInjectConfig,
|
||||
ARPRedirectConfig,
|
||||
InjectionConfig,
|
||||
|
||||
# Policy (Broche 3)
|
||||
RuleMatch,
|
||||
PolicyRule,
|
||||
EscalationConfig,
|
||||
PolicyConfig,
|
||||
|
||||
# Main Profile
|
||||
OPADProfile,
|
||||
|
||||
# Event Models
|
||||
OPADEvent,
|
||||
OPADInjectResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"OPADMode",
|
||||
"PolicyAction",
|
||||
"DefaultAction",
|
||||
"LogLevel",
|
||||
"Protocol",
|
||||
"DNSRaceMode",
|
||||
"ProtocolMatch",
|
||||
|
||||
# Observation
|
||||
"ProtocolsConfig",
|
||||
"FingerprintingConfig",
|
||||
"ObservationConfig",
|
||||
|
||||
# Injection
|
||||
"DNSRaceConfig",
|
||||
"QuarantinePool",
|
||||
"DHCPRaceConfig",
|
||||
"RSTInjectConfig",
|
||||
"ARPRedirectConfig",
|
||||
"InjectionConfig",
|
||||
|
||||
# Policy
|
||||
"RuleMatch",
|
||||
"PolicyRule",
|
||||
"EscalationConfig",
|
||||
"PolicyConfig",
|
||||
|
||||
# Main
|
||||
"OPADProfile",
|
||||
|
||||
# Events
|
||||
"OPADEvent",
|
||||
"OPADInjectResult",
|
||||
]
|
||||
400
common/secubox_core/opad/models.py
Normal file
400
common/secubox_core/opad/models.py
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
"""
|
||||
SecuBox-Deb :: OPAD Pydantic Models
|
||||
CyberMind — https://cybermind.fr
|
||||
Author: Gérald Kerma <gandalf@gk2.net>
|
||||
License: Proprietary / ANSSI CSPN candidate
|
||||
|
||||
Pydantic v2 models for OPAD v2.4.0 configuration profiles.
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
|
||||
|
||||
# ==================== ENUMS ====================
|
||||
|
||||
class OPADMode(str, Enum):
|
||||
"""Global OPAD operation mode."""
|
||||
OBSERVE = "observe"
|
||||
ENFORCE = "enforce"
|
||||
|
||||
|
||||
class PolicyAction(str, Enum):
|
||||
"""Policy rule action types."""
|
||||
ALLOW = "allow"
|
||||
OBSERVE = "observe"
|
||||
DNS_NXDOMAIN = "dns_nxdomain"
|
||||
DNS_SINKHOLE = "dns_sinkhole"
|
||||
DNS_REDIRECT = "dns_redirect"
|
||||
DHCP_QUARANTINE = "dhcp_quarantine"
|
||||
TCP_RST = "tcp_rst"
|
||||
ARP_REDIRECT = "arp_redirect"
|
||||
|
||||
|
||||
class DefaultAction(str, Enum):
|
||||
"""Default action for unmatched traffic."""
|
||||
ALLOW = "allow"
|
||||
OBSERVE = "observe"
|
||||
DISRUPT = "disrupt"
|
||||
|
||||
|
||||
class LogLevel(str, Enum):
|
||||
"""Log level for rules and events."""
|
||||
DEBUG = "debug"
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
class Protocol(str, Enum):
|
||||
"""Network protocols."""
|
||||
DNS = "dns"
|
||||
DHCP = "dhcp"
|
||||
TCP = "tcp"
|
||||
ARP = "arp"
|
||||
|
||||
|
||||
class DNSRaceMode(str, Enum):
|
||||
"""DNS race injection modes."""
|
||||
NXDOMAIN = "nxdomain"
|
||||
SINKHOLE = "sinkhole"
|
||||
REDIRECT_CAPTIVE = "redirect_captive"
|
||||
|
||||
|
||||
class ProtocolMatch(str, Enum):
|
||||
"""Protocol match for policy rules."""
|
||||
TCP = "tcp"
|
||||
UDP = "udp"
|
||||
ICMP = "icmp"
|
||||
ANY = "any"
|
||||
|
||||
|
||||
# ==================== VALIDATORS ====================
|
||||
|
||||
MAC_PATTERN = re.compile(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$")
|
||||
|
||||
|
||||
def validate_mac_address(mac: str) -> str:
|
||||
"""Validate MAC address format."""
|
||||
if not MAC_PATTERN.match(mac):
|
||||
raise ValueError(f"Invalid MAC address format: {mac}")
|
||||
return mac
|
||||
|
||||
|
||||
# ==================== BROCHE 1: OBSERVATION ====================
|
||||
|
||||
class ProtocolsConfig(BaseModel):
|
||||
"""Protocol observation configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
dns: bool = True
|
||||
dhcp: bool = True
|
||||
arp: bool = True
|
||||
tcp: bool = False
|
||||
|
||||
|
||||
class FingerprintingConfig(BaseModel):
|
||||
"""Device fingerprinting configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
dhcp_fingerprint: bool = True
|
||||
ja3: bool = Field(default=False, description="TLS JA3 fingerprinting")
|
||||
ja4: bool = Field(default=False, description="TLS JA4 fingerprinting")
|
||||
user_agent: bool = Field(default=False, description="HTTP User-Agent parsing")
|
||||
|
||||
|
||||
class ObservationConfig(BaseModel):
|
||||
"""Broche 1: Observation configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
interfaces: List[str] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Network interfaces to observe"
|
||||
)
|
||||
bpf_filter: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional BPF filter for packet capture"
|
||||
)
|
||||
protocols: ProtocolsConfig = Field(default_factory=ProtocolsConfig)
|
||||
fingerprinting: FingerprintingConfig = Field(default_factory=FingerprintingConfig)
|
||||
|
||||
@field_validator("interfaces")
|
||||
@classmethod
|
||||
def validate_interfaces(cls, v: List[str]) -> List[str]:
|
||||
"""Validate interface names."""
|
||||
if not v:
|
||||
raise ValueError("At least one interface is required")
|
||||
interface_pattern = re.compile(r"^[a-z0-9]+$")
|
||||
for iface in v:
|
||||
if not interface_pattern.match(iface):
|
||||
raise ValueError(f"Invalid interface name: {iface}")
|
||||
return v
|
||||
|
||||
|
||||
# ==================== BROCHE 2: INJECTION ====================
|
||||
|
||||
class DNSRaceConfig(BaseModel):
|
||||
"""DNS race injection configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled: bool = False
|
||||
target_success_rate: float = Field(
|
||||
default=0.99,
|
||||
ge=0.9,
|
||||
le=1.0,
|
||||
description="Target success rate for winning DNS race (0.9-1.0)"
|
||||
)
|
||||
modes: List[DNSRaceMode] = Field(
|
||||
default_factory=list,
|
||||
description="Enabled DNS race modes"
|
||||
)
|
||||
sinkhole_ip: str = Field(
|
||||
default="127.0.0.1",
|
||||
description="IP address for sinkhole mode"
|
||||
)
|
||||
ttl: int = Field(
|
||||
default=60,
|
||||
ge=0,
|
||||
le=3600,
|
||||
description="TTL for injected DNS responses (seconds)"
|
||||
)
|
||||
|
||||
|
||||
class QuarantinePool(BaseModel):
|
||||
"""DHCP quarantine IP pool configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
start: str = Field(..., description="Start of quarantine IP pool")
|
||||
end: str = Field(..., description="End of quarantine IP pool")
|
||||
lease_time: int = Field(
|
||||
default=300,
|
||||
ge=60,
|
||||
le=86400,
|
||||
description="Lease time in seconds"
|
||||
)
|
||||
gateway: str = Field(..., description="Gateway IP for quarantine network")
|
||||
|
||||
|
||||
class DHCPRaceConfig(BaseModel):
|
||||
"""DHCP race injection configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled: bool = False
|
||||
target_success_rate: float = Field(
|
||||
default=0.95,
|
||||
ge=0.9,
|
||||
le=1.0,
|
||||
description="Target success rate for winning DHCP race"
|
||||
)
|
||||
quarantine_pool: Optional[QuarantinePool] = None
|
||||
|
||||
|
||||
class RSTInjectConfig(BaseModel):
|
||||
"""TCP RST injection configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled: bool = False
|
||||
target_success_rate: float = Field(
|
||||
default=0.90,
|
||||
ge=0.85,
|
||||
le=1.0,
|
||||
description="Target success rate for RST injection (min 0.85)"
|
||||
)
|
||||
double_ended: bool = Field(
|
||||
default=True,
|
||||
description="Send RST to both client and server"
|
||||
)
|
||||
timing_window_ms: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Timing window for RST injection (milliseconds)"
|
||||
)
|
||||
|
||||
|
||||
class ARPRedirectConfig(BaseModel):
|
||||
"""ARP redirection configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled: bool = False
|
||||
target_success_rate: float = Field(
|
||||
default=0.98,
|
||||
ge=0.95,
|
||||
le=1.0,
|
||||
description="Target success rate for ARP redirection (min 0.95)"
|
||||
)
|
||||
refresh_interval_s: int = Field(
|
||||
default=60,
|
||||
ge=10,
|
||||
le=300,
|
||||
description="ARP table refresh interval (seconds)"
|
||||
)
|
||||
captive_mac: Optional[str] = Field(
|
||||
default=None,
|
||||
description="MAC address for captive portal gateway"
|
||||
)
|
||||
|
||||
@field_validator("captive_mac")
|
||||
@classmethod
|
||||
def validate_captive_mac(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Validate MAC address format."""
|
||||
if v is not None:
|
||||
return validate_mac_address(v)
|
||||
return v
|
||||
|
||||
|
||||
class InjectionConfig(BaseModel):
|
||||
"""Broche 2: Injection (perturbation) configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
dns_race: Optional[DNSRaceConfig] = None
|
||||
dhcp_race: Optional[DHCPRaceConfig] = None
|
||||
rst_inject: Optional[RSTInjectConfig] = None
|
||||
arp_redirect: Optional[ARPRedirectConfig] = None
|
||||
|
||||
|
||||
# ==================== BROCHE 3: POLICY ====================
|
||||
|
||||
class RuleMatch(BaseModel):
|
||||
"""Policy rule match conditions (all conditions are AND-ed)."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
source_mac: Optional[str] = Field(default=None, description="Source MAC address")
|
||||
dest_domain: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Destination domain (supports wildcards: *.example.com)"
|
||||
)
|
||||
dest_ip: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Destination IP address or CIDR"
|
||||
)
|
||||
protocol: ProtocolMatch = Field(default=ProtocolMatch.ANY)
|
||||
mind_score_above: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="Trigger if MIND adversarial score above threshold"
|
||||
)
|
||||
device_authorized: Optional[bool] = Field(
|
||||
default=None,
|
||||
description="Match only authorized/unauthorized devices"
|
||||
)
|
||||
|
||||
@field_validator("source_mac")
|
||||
@classmethod
|
||||
def validate_source_mac(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Validate MAC address format."""
|
||||
if v is not None:
|
||||
return validate_mac_address(v)
|
||||
return v
|
||||
|
||||
|
||||
class PolicyRule(BaseModel):
|
||||
"""Policy rule configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: str = Field(..., min_length=1, max_length=64, description="Unique rule identifier")
|
||||
priority: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
le=9999,
|
||||
description="Rule priority (0=highest, 9999=lowest)"
|
||||
)
|
||||
match: Optional[RuleMatch] = None
|
||||
action: PolicyAction
|
||||
log_level: LogLevel = LogLevel.INFO
|
||||
|
||||
|
||||
class EscalationConfig(BaseModel):
|
||||
"""Policy escalation configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
allow_in_path: bool = Field(
|
||||
default=True,
|
||||
description="Allow temporary escalation to allow mode"
|
||||
)
|
||||
require_explicit_consent: bool = Field(
|
||||
default=True,
|
||||
description="Require user consent for escalation"
|
||||
)
|
||||
auto_revert_after_s: int = Field(
|
||||
default=300,
|
||||
ge=60,
|
||||
description="Auto-revert escalation after N seconds (min 60)"
|
||||
)
|
||||
audit_all_escalations: bool = Field(
|
||||
default=True,
|
||||
description="Log all escalation events to audit log"
|
||||
)
|
||||
|
||||
|
||||
class PolicyConfig(BaseModel):
|
||||
"""Broche 3: Policy configuration."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
default_action: DefaultAction
|
||||
rules: List[PolicyRule] = Field(default_factory=list)
|
||||
escalation: Optional[EscalationConfig] = None
|
||||
|
||||
|
||||
# ==================== MAIN PROFILE ====================
|
||||
|
||||
class OPADProfile(BaseModel):
|
||||
"""OPAD v2.4.0 complete configuration profile."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version: str = Field(
|
||||
...,
|
||||
pattern=r"^\d+\.\d+\.\d+$",
|
||||
description="Schema version (semver)"
|
||||
)
|
||||
mode: OPADMode
|
||||
observation: ObservationConfig
|
||||
injection: InjectionConfig
|
||||
policy: PolicyConfig
|
||||
|
||||
|
||||
# ==================== EVENT MODELS ====================
|
||||
|
||||
class OPADEvent(BaseModel):
|
||||
"""OPAD event record for logging and audit."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
timestamp: datetime
|
||||
event_type: str = Field(..., description="Event type (e.g., 'dns_race', 'policy_match')")
|
||||
primitive: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Injection primitive used (dns_race, dhcp_race, rst_inject, arp_redirect)"
|
||||
)
|
||||
source_mac: Optional[str] = None
|
||||
target: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Target of action (domain, IP, etc.)"
|
||||
)
|
||||
action_taken: PolicyAction
|
||||
success: bool
|
||||
details: Optional[Dict[str, Any]] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("source_mac")
|
||||
@classmethod
|
||||
def validate_source_mac(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Validate MAC address format."""
|
||||
if v is not None:
|
||||
return validate_mac_address(v)
|
||||
return v
|
||||
|
||||
|
||||
class OPADInjectResult(BaseModel):
|
||||
"""Result of an OPAD injection primitive."""
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
primitive: str = Field(..., description="Injection primitive name")
|
||||
target: str = Field(..., description="Target (domain, IP, MAC, etc.)")
|
||||
race_won: bool = Field(..., description="Whether the race was won")
|
||||
latency_ms: Optional[float] = Field(default=None, description="Latency in milliseconds")
|
||||
event_id: Optional[str] = Field(default=None, description="Reference to OPADEvent")
|
||||
1
common/secubox_core/requirements.txt
Normal file
1
common/secubox_core/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
pydantic>=2.0.0,<3.0.0
|
||||
1054
docs/superpowers/plans/2026-05-12-apt-public-repo-staging.md
Normal file
1054
docs/superpowers/plans/2026-05-12-apt-public-repo-staging.md
Normal file
File diff suppressed because it is too large
Load Diff
1801
docs/superpowers/plans/2026-05-12-license-headers.md
Normal file
1801
docs/superpowers/plans/2026-05-12-license-headers.md
Normal file
File diff suppressed because it is too large
Load Diff
2663
docs/superpowers/plans/2026-05-12-opad-doctrine-documents.md
Normal file
2663
docs/superpowers/plans/2026-05-12-opad-doctrine-documents.md
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,254 @@
|
|||
# APT Public Repo Staging — Design
|
||||
|
||||
**Date:** 2026-05-12
|
||||
**Author:** Gandalf (CyberMind), with Claude
|
||||
**Status:** Draft for approval
|
||||
**Targets:** `apt.secubox.in` (production), `bookworm` suite, `arm64` (mochabin/Armada 7040) + `amd64`
|
||||
|
||||
## Goal
|
||||
|
||||
Produce a fully signed, validated APT repository tree under `output/repo/`
|
||||
containing both `amd64` and `arm64` SecuBox packages for the `bookworm` suite.
|
||||
The tree is ready to be `rsync`-ed to `apt.secubox.in` out-of-band by the user.
|
||||
|
||||
This run **does not** touch the production server, **does not** request TLS
|
||||
certificates, and **does not** push any data over the network. It produces
|
||||
artifacts (signed repo tree, nginx vhost, deploy recipe) the user pushes when
|
||||
satisfied.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Provisioning the `apt.secubox.in` server itself (handled by
|
||||
[repo/scripts/setup-repo-server.sh](../../../repo/scripts/setup-repo-server.sh)
|
||||
when the user runs it on the VPS).
|
||||
- Issuing the TLS certificate (certbot runs on the production host, post-rsync).
|
||||
- Building anything for `trixie` (placeholder section only).
|
||||
- Cross-building anything that genuinely needs native compilation against
|
||||
ARM-only libraries — fall back to a halt with a clear message.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Source |
|
||||
|-------|--------|
|
||||
| Package set per tier | `secubox gen --tier {tier-lite,tier-standard,tier-pro} --out manifests/` |
|
||||
| Build script | [`scripts/build-packages.sh`](../../../scripts/build-packages.sh) `bookworm <arch>` |
|
||||
| GPG key | `~/.gnupg/secubox/` (persistent across runs) |
|
||||
| Reprepro driver | `secubox apt {init,publish,check}` via [`cmd/secubox/cmd/apt_server.go`](../../../cmd/secubox/cmd/apt_server.go) |
|
||||
| Install script | [`repo/install.sh`](../../../repo/install.sh) |
|
||||
|
||||
## Outputs (`output/repo/`)
|
||||
|
||||
```
|
||||
output/repo/
|
||||
├── conf/
|
||||
│ ├── distributions Origin: SecuBox, SignWith: <fingerprint>
|
||||
│ └── options basedir + gnupghome → ~/.gnupg/secubox
|
||||
├── db/ reprepro state (gitignored)
|
||||
├── dists/bookworm/
|
||||
│ ├── Release
|
||||
│ ├── InRelease gpg-clearsigned
|
||||
│ ├── Release.gpg detached signature
|
||||
│ └── main/{binary-arm64,binary-amd64,source}/Packages{,.gz,.xz}
|
||||
├── pool/main/s/secubox-*/*.deb
|
||||
├── secubox-keyring.gpg public key, served as /secubox-keyring.gpg
|
||||
├── install.sh copy of repo/install.sh, served as /install.sh
|
||||
├── FINGERPRINT.txt plaintext fingerprint for verification
|
||||
├── MANIFEST.txt arch × tier counts + per-package version
|
||||
├── nginx-apt.conf drop-in for /etc/nginx/sites-available/
|
||||
└── DEPLOY.md certbot + rsync recipe
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component 1 — GPG bootstrap
|
||||
|
||||
**Lives at:** `~/.gnupg/secubox/` (persistent home, **outside** the repo tree).
|
||||
**Driver:** [`repo/scripts/generate-gpg-key.sh`](../../../repo/scripts/generate-gpg-key.sh)
|
||||
with `GPG_HOME=~/.gnupg/secubox` and `EXPORT_DIR=output/repo`.
|
||||
|
||||
Key parameters (already in script): RSA 4096 + 4096 subkey, no passphrase,
|
||||
`packages@secubox.in`, no expiry. Idempotent — exits early if a key for that
|
||||
UID already exists in the keyring.
|
||||
|
||||
Produces:
|
||||
- `output/repo/secubox-keyring.gpg` (ASCII-armored public key)
|
||||
- `output/repo/FINGERPRINT.txt` (long fingerprint, used by `SignWith:`)
|
||||
|
||||
### Component 2 — Tier resolution
|
||||
|
||||
**Driver:** `secubox gen --tier <tier> --board mochabin --out manifests/<tier>/`
|
||||
|
||||
For each tier in order `base → tier-lite → tier-standard → tier-pro`, resolve
|
||||
the package set from [`profiles/`](../../../profiles/) via the Go profile
|
||||
engine ([`cmd/secubox/internal/profile/`](../../../cmd/secubox/internal/profile/)).
|
||||
|
||||
Tier 0 (base) is **implicit** — there is no `--tier base` flag. We build it
|
||||
directly as the hardcoded set `{secubox-core, secubox-hub}`. Higher tiers are
|
||||
resolved by `secubox gen` from `profiles/{tier-lite,tier-standard,tier-pro}.yaml`
|
||||
which inherit transitively from `base.yaml`. Output is a manifest directory
|
||||
per tier; the orchestrator extracts the `packages.required` list as the build
|
||||
filter.
|
||||
|
||||
### Component 3 — Layered cross-build
|
||||
|
||||
**Driver:** [`scripts/build-packages.sh`](../../../scripts/build-packages.sh) with a `--filter <manifest>` flag
|
||||
(new flag; passes filter through to the existing `PACKAGES=` array).
|
||||
|
||||
For each tier T in `[base, lite, standard, pro]`:
|
||||
1. Run `build-packages.sh bookworm arm64 --filter manifests/tier-T.json`
|
||||
2. Run `build-packages.sh bookworm amd64 --filter manifests/tier-T.json`
|
||||
3. Collect `output/debs/*_arm64.deb`, `*_amd64.deb`, `*_all.deb` matching the
|
||||
filter. The `_all` packages only need to be built once (in the arm64 pass);
|
||||
amd64 pass skips packages with `Architecture: all` already built.
|
||||
4. If **any** package in tier T fails, halt the pipeline. Tiers T-1 and lower
|
||||
are already published and remain usable. Record the failure in
|
||||
`MANIFEST.txt` and exit non-zero.
|
||||
|
||||
Cross-build dependencies (`crossbuild-essential-arm64`, `qemu-user-static`
|
||||
for any `_arm64.deb` running maintainer scripts during build) are checked up
|
||||
front; missing → halt with apt-install hint.
|
||||
|
||||
### Component 4 — Reprepro repository
|
||||
|
||||
**Driver:** `secubox apt init --base output/repo` then
|
||||
`secubox apt publish output/debs/...`.
|
||||
|
||||
`conf/distributions` (production identity):
|
||||
```
|
||||
Origin: SecuBox
|
||||
Label: SecuBox
|
||||
Suite: bookworm
|
||||
Codename: bookworm
|
||||
Version: 12.0
|
||||
Architectures: arm64 amd64 source
|
||||
Components: main
|
||||
Description: SecuBox Debian packages for Armada/x86_64
|
||||
SignWith: <fingerprint from FINGERPRINT.txt>
|
||||
Contents: percomponent nocompatsymlink
|
||||
```
|
||||
|
||||
A `trixie` block is included with the same shape, but no packages are
|
||||
published into it in this design.
|
||||
|
||||
`conf/options`:
|
||||
```
|
||||
verbose
|
||||
basedir <abs path>/output/repo
|
||||
gnupghome <abs path>/.gnupg/secubox
|
||||
```
|
||||
|
||||
`reprepro` requires the `gnupghome` to be an absolute path; we resolve `~`
|
||||
before writing.
|
||||
|
||||
After each tier publish: `secubox apt check` (wraps `reprepro check`). Clean
|
||||
output is required before moving to the next tier.
|
||||
|
||||
### Component 5 — Deploy artifacts (no network)
|
||||
|
||||
`output/repo/nginx-apt.conf`:
|
||||
- `server_name apt.secubox.in;`
|
||||
- `root /var/www/apt.secubox.in;`
|
||||
- ACME challenge location `/.well-known/acme-challenge/`
|
||||
- MIME types: `application/vnd.debian.binary-package` for `.deb`,
|
||||
`application/pgp-keys` for `.gpg`
|
||||
- `autoindex on` for `/dists/` and `/pool/`
|
||||
- Listen 80 only (TLS added by certbot in-place on the server)
|
||||
|
||||
`output/repo/DEPLOY.md` documents:
|
||||
1. `rsync -avz --delete output/repo/ deploy@apt.secubox.in:/var/www/apt.secubox.in/`
|
||||
2. `ssh deploy@apt.secubox.in sudo cp /var/www/apt.secubox.in/nginx-apt.conf /etc/nginx/sites-available/apt.secubox.in`
|
||||
3. `ssh apt.secubox.in sudo certbot --nginx -d apt.secubox.in --non-interactive --agree-tos -m packages@secubox.in`
|
||||
4. Post-deploy: `openssl s_client -servername apt.secubox.in -connect apt.secubox.in:443 </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName` — must list `DNS:apt.secubox.in`.
|
||||
|
||||
### Component 6 — Validation gate
|
||||
|
||||
Before printing "done":
|
||||
|
||||
1. `reprepro -b output/repo check` — must be clean.
|
||||
2. `gpg --homedir ~/.gnupg/secubox --verify output/repo/dists/bookworm/InRelease`
|
||||
— must report `Good signature` from the SecuBox UID.
|
||||
3. Optional but recommended: debootstrap a throwaway `bookworm` chroot under
|
||||
`output/test-chroot/`, mount nothing, add `deb [trusted=no signed-by=...] file://<abs>/output/repo/ bookworm main` to `sources.list.d`, run
|
||||
`apt-get update` inside the chroot. Must succeed without warnings.
|
||||
4. Write `output/repo/MANIFEST.txt` with the per-tier × per-arch table and
|
||||
the full `dpkg -I` summary of every published .deb.
|
||||
|
||||
If any step fails, the pipeline exits non-zero and the run is considered
|
||||
incomplete — but `output/repo/` is left in place for the user to inspect.
|
||||
|
||||
## Error handling
|
||||
|
||||
| Failure | Behavior |
|
||||
|---------|----------|
|
||||
| Missing cross-build dep | Halt before any build, print exact `apt install` command |
|
||||
| Single package build fails in tier T | Record in `MANIFEST.txt`, halt at end of tier T (lower tiers stay published) |
|
||||
| Reprepro check fails | Halt; do not advance to next tier |
|
||||
| GPG signature verify fails | Halt; this means the key is wrong or the repo is corrupt |
|
||||
| Chroot validation fails | Warning, not halt — could be a local env issue, not a repo bug |
|
||||
|
||||
All halts are non-zero exit with a one-line summary plus a pointer to the
|
||||
detailed log under `output/build.log`.
|
||||
|
||||
## Testing
|
||||
|
||||
This work is operational (it produces an artifact); no unit tests are added.
|
||||
Instead the **chroot validation step is the test** — if a fresh `bookworm`
|
||||
chroot can `apt update` against the staged repo, the artifact works.
|
||||
|
||||
For regressions, the manifest file is diffable: re-running with a new package
|
||||
version produces a `MANIFEST.txt` that can be `diff`-ed against the last good
|
||||
build.
|
||||
|
||||
## Open questions
|
||||
|
||||
None blocking. The mochabin board profile under
|
||||
[`board/mochabin/`](../../../board/mochabin/) is already declared (Armada 7040,
|
||||
arm64); no additional board work is needed for the publish step.
|
||||
|
||||
## Licensing
|
||||
|
||||
| Layer | License | Notes |
|
||||
|-------|---------|-------|
|
||||
| SecuBox-Deb source & packages | CMSD-1.0 (CyberMind Source-Disclosed) / ANSSI CSPN candidate | Per [`.claude/CLAUDE.md`](../../../.claude/CLAUDE.md). Each `.deb` carries `debian/copyright` declaring this. |
|
||||
| Package signing key | SecuBox internal | UID `SecuBox Package Signing Key <packages@secubox.in>`. Not third-party; do not cross-sign. |
|
||||
| Repository tooling (reprepro, nginx, certbot) | GPL / BSD / Apache (upstream) | Used as-is from Debian; no redistribution. |
|
||||
| `install.sh` (served from repo root) | Proprietary / CyberMind | Header credits `https://github.com/gkerma/secubox-deb`. Includes terms-of-service hint pointing to `https://secubox.in/terms`. |
|
||||
| TLS certificate | Let's Encrypt (ISRG) | Issued on the production host via certbot; subject CN = `apt.secubox.in`. |
|
||||
| Public GPG key file | Proprietary distribution, free to redistribute the public half | Served at `/secubox-keyring.gpg`; SHA256 published in `FINGERPRINT.txt`. |
|
||||
|
||||
The staged tree MUST include:
|
||||
|
||||
- `output/repo/LICENCE-CMSD-1.0.md` — verbatim copy of the project root
|
||||
[`LICENCE-CMSD-1.0.md`](../../../LICENCE-CMSD-1.0.md) (authoritative French
|
||||
text).
|
||||
- `output/repo/LICENSE-CMSD-1.0.en.md` — verbatim copy of the project root
|
||||
[`LICENSE-CMSD-1.0.en.md`](../../../LICENSE-CMSD-1.0.en.md) (informative
|
||||
English translation).
|
||||
|
||||
Both served at `https://apt.secubox.in/LICENCE-CMSD-1.0.md` and
|
||||
`https://apt.secubox.in/LICENSE-CMSD-1.0.en.md`. The
|
||||
[`install.sh`](../../../repo/install.sh) script references the French file
|
||||
(authoritative) before any apt operation so users see the terms before
|
||||
adding the repo. Per Article 13.5 of the license, the French text prevails
|
||||
in any conflict.
|
||||
|
||||
Validation gate adds:
|
||||
|
||||
- `output/repo/LICENCE-CMSD-1.0.md` and `output/repo/LICENSE-CMSD-1.0.en.md`
|
||||
exist and byte-match the project root copies.
|
||||
- `output/repo/FINGERPRINT.txt` contains exactly one fingerprint matching the
|
||||
key used by `SignWith:`.
|
||||
|
||||
## File-level changes
|
||||
|
||||
| Action | File | Purpose |
|
||||
|--------|------|---------|
|
||||
| Modify | `scripts/build-packages.sh` | Add `--filter <manifest.json>` flag |
|
||||
| Create | `scripts/stage-apt-repo.sh` | Orchestrator: tiers × archs × reprepro |
|
||||
| Create | `scripts/render-deploy-artifacts.sh` | Generate `nginx-apt.conf` + `DEPLOY.md` |
|
||||
| Create | `scripts/validate-staged-repo.sh` | reprepro check + gpg verify + chroot test |
|
||||
| Modify | `.gitignore` | Ignore `output/repo/db/`, `output/repo/pool/`, `output/test-chroot/` |
|
||||
| Create | `output/repo/.gitkeep` | Keep dir structure |
|
||||
|
||||
The `secubox apt` Go CLI is **not** modified — we only call its existing
|
||||
subcommands.
|
||||
270
docs/superpowers/specs/2026-05-12-license-headers-design.md
Normal file
270
docs/superpowers/specs/2026-05-12-license-headers-design.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# Design — CMSD-1.0 License Headers Across the Codebase
|
||||
|
||||
**Date:** 2026-05-12
|
||||
**Status:** Approved by user (sections 1–5), pending plan
|
||||
**Author:** Claude (brainstormed with @CyberMind-FR)
|
||||
**Tracking:** GitHub Issue TBD (created at start of Phase A)
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Add the CyberMind Source-Disclosed License v1.0 (CMSD-1.0) SPDX header to every first-party source file in the `secubox-deb` repository, and keep it that way going forward with a CI check.
|
||||
|
||||
Today the canonical header template lives in `LICENSING.md` (lines 70–75) but **zero of ~2,170 first-party code files** carry it. This design closes that gap mechanically, idempotently, and reviewably.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
### 2.1 File types in scope
|
||||
|
||||
| Extension(s) | Comment style | Count (approx.) |
|
||||
|---|---|---|
|
||||
| `.py` | `#` line comments | 865 |
|
||||
| `.sh` | `#` line comments | 122 |
|
||||
| `.js`, `.mjs`, `.ts` | `//` line comments | 613 |
|
||||
| `.c`, `.h` | `/* ... */` block (C89-safe) | 86 |
|
||||
| `.css` | `/* ... */` block | 100 |
|
||||
| `.html` | `<!-- ... -->` block | 384 |
|
||||
| `.md` | `<!-- ... -->` block | (many) |
|
||||
| `.toml`, `.yaml`, `.yml`, `.conf` | `#` line comments | (some) |
|
||||
|
||||
JSON is **out of scope** (no comment syntax).
|
||||
|
||||
### 2.2 Paths excluded (skip-list)
|
||||
|
||||
Hard-coded directory prefixes the walker never descends into:
|
||||
|
||||
- `kernel-build/`, `redroid/`, `tools/Tow-Boot/` — vendor/upstream code with its own licenses
|
||||
- `output/`, `cache/`, `backups/`, `apt/`, `repo/` — generated/build artifacts
|
||||
- `node_modules/`, `.venv/`, `.git/`, `__pycache__/`, `dist/`, `build/` — tool-managed trees
|
||||
|
||||
Glob excludes:
|
||||
|
||||
- `*.min.js`, `*.min.css` — minified bundles
|
||||
- `package-lock.json`, `*.lock` — lockfiles
|
||||
- Any file whose first 10 lines already contain `SPDX-License-Identifier: ` followed by **something other than** `LicenseRef-CMSD-1.0` → skipped with a warning to stderr (do not overwrite third-party licenses)
|
||||
|
||||
### 2.3 Out of scope (explicitly)
|
||||
|
||||
- Modifying the CMSD-1.0 license terms themselves
|
||||
- Adding the header to the LICENSE files (they already define the license)
|
||||
- Re-licensing any third-party code
|
||||
- A "remove header" mode — not needed
|
||||
|
||||
## 3. Header content
|
||||
|
||||
The canonical 4-line header, parameterized by language comment marker (`<CM>`):
|
||||
|
||||
```
|
||||
<CM> SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
<CM> Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
<CM> Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
<CM> See LICENCE-CMSD-1.0.md for terms.
|
||||
```
|
||||
|
||||
Followed by **one blank line**, then the file's pre-existing content.
|
||||
|
||||
### 3.1 Per-language rendering
|
||||
|
||||
| Language | Rendered as |
|
||||
|---|---|
|
||||
| Python, Bash, YAML, TOML, conf | Four `# ` prefixed lines |
|
||||
| JS, TS, MJS | Four `// ` prefixed lines |
|
||||
| C, H | Single `/* ... */` block, internal lines prefixed ` * ` |
|
||||
| CSS | Single `/* ... */` block, internal lines prefixed ` * ` |
|
||||
| HTML, Markdown | Single `<!-- ... -->` block, internal lines indented 2 spaces |
|
||||
|
||||
### 3.2 Placement rules
|
||||
|
||||
The header MUST appear at the top of the file, with the following exceptions:
|
||||
|
||||
| File type | Header goes... |
|
||||
|---|---|
|
||||
| Python with `#!` shebang | Immediately after the shebang line |
|
||||
| Python with `# -*- coding: ... -*-` | Immediately after the encoding declaration |
|
||||
| Bash with `#!` shebang | Immediately after the shebang line |
|
||||
| JS/TS with `"use strict"` or top-level directive | Above the directive (header at line 1) |
|
||||
| HTML with `<!DOCTYPE html>` | Immediately after the doctype, on its own line |
|
||||
| Markdown with YAML frontmatter `---` | Immediately after the closing `---` |
|
||||
| All others | Line 1 |
|
||||
|
||||
## 4. The tool — `scripts/license-headers.py`
|
||||
|
||||
Single-file Python 3.11+ utility, **stdlib only** (`argparse`, `pathlib`, `re`, `sys`).
|
||||
|
||||
### 4.1 CLI
|
||||
|
||||
```
|
||||
license-headers.py --check [PATHS...] # exit 0 if all good, 1 if missing
|
||||
license-headers.py --fix [PATHS...] # add headers in place; idempotent
|
||||
license-headers.py --list [PATHS...] # dry-run: list files that would be modified
|
||||
license-headers.py --diff [PATHS...] # unified diff per file, no writes
|
||||
```
|
||||
|
||||
- Default `PATHS` = repo root (cwd-relative).
|
||||
- The repo root is detected by walking up from cwd to the nearest `.git/`.
|
||||
- Mode flags are mutually exclusive; exactly one is required.
|
||||
- During `--check`, the enrollment allowlist (see §5) constrains which paths are evaluated.
|
||||
|
||||
### 4.2 Internal structure (~300 LOC, one file)
|
||||
|
||||
| Component | Responsibility |
|
||||
|---|---|
|
||||
| `SKIP_DIRS: frozenset[str]` | Dirnames the walker prunes (§2.2 directory list) |
|
||||
| `SKIP_GLOBS: tuple[str, ...]` | Glob patterns for excluded files |
|
||||
| `LANG_TABLE: dict[str, LangSpec]` | Maps `.ext` → `(comment_style, placement_rule, header_renderer)` |
|
||||
| `detect_existing(text) -> Status` | Returns `MATCH` / `FOREIGN` / `NONE` based on first 10 lines |
|
||||
| `render_header(comment_style) -> str` | Builds the 4-line block for one language |
|
||||
| `apply(text, ext) -> str` | Pure function: input file content → output content with header inserted at correct position |
|
||||
| `walk(paths, enrolled) -> Iterator[Path]` | Yields in-scope files, honoring skip-list and enrollment allowlist |
|
||||
| `main()` | Argparse dispatch |
|
||||
|
||||
### 4.3 Idempotence contract
|
||||
|
||||
```
|
||||
apply(apply(text, ext), ext) == apply(text, ext)
|
||||
```
|
||||
|
||||
Explicitly tested. Detection is based on matching the literal token `SPDX-License-Identifier: LicenseRef-CMSD-1.0` anywhere in the first 10 lines, tolerant of comment-marker variation and whitespace.
|
||||
|
||||
### 4.4 Tests — `tests/test_license_headers.py` (pytest)
|
||||
|
||||
1. `apply` is idempotent for every extension in `LANG_TABLE`.
|
||||
2. Shebang, doctype, frontmatter, and encoding declarations are preserved in their original position.
|
||||
3. Foreign SPDX (e.g., `GPL-2.0-or-later`, `MIT`) → file returned unchanged; warning emitted.
|
||||
4. Existing CMSD header is not duplicated, regardless of comment style variation.
|
||||
5. `--check` exits 1 when any in-scope file lacks the header.
|
||||
6. `SKIP_DIRS` are pruned: a file under `kernel-build/` is never yielded by `walk`.
|
||||
|
||||
All tests run without filesystem side effects (use `tmp_path` fixtures and string inputs to `apply`).
|
||||
|
||||
## 5. CI integration
|
||||
|
||||
### 5.1 Workflow — `.github/workflows/license-check.yml`
|
||||
|
||||
```yaml
|
||||
name: License Headers
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with: { python-version: '3.11' }
|
||||
- run: python3 scripts/license-headers.py --check
|
||||
```
|
||||
|
||||
No `pip install`; runs in <10s.
|
||||
|
||||
### 5.2 Enrollment allowlist
|
||||
|
||||
To avoid breaking CI before Phase B is complete, the `--check` mode reads an **enrollment allowlist** at `scripts/license-headers-enrolled.txt`:
|
||||
|
||||
- One glob pattern per line (e.g., `common/**`, `packages/secubox-hub/**`).
|
||||
- Lines starting with `#` are comments.
|
||||
- Empty/missing file → nothing is enforced (Phase A initial state).
|
||||
- File contains `**` only → repo-wide enforcement (Phase C final state).
|
||||
- File deleted at end of Phase C; tool then defaults to repo-wide.
|
||||
|
||||
Phase B PRs each add one line to this file as they enroll a path.
|
||||
|
||||
### 5.3 Optional pre-commit hook
|
||||
|
||||
Documented in `scripts/README.md`; not installed by default:
|
||||
|
||||
```yaml
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: license-headers
|
||||
name: License Headers (CMSD-1.0)
|
||||
entry: python3 scripts/license-headers.py --fix
|
||||
language: system
|
||||
pass_filenames: true
|
||||
```
|
||||
|
||||
## 6. Rollout
|
||||
|
||||
### 6.1 Phase A — Foundation (1 PR, GitHub Issue, label: `infra`)
|
||||
|
||||
Deliverables:
|
||||
1. `scripts/license-headers.py` (the tool)
|
||||
2. `tests/test_license_headers.py` (unit tests)
|
||||
3. `.github/workflows/license-check.yml` (CI)
|
||||
4. `scripts/license-headers-enrolled.txt` (initial empty enrollment)
|
||||
5. `scripts/README.md` updated to document the tool and pre-commit hook
|
||||
6. `CLAUDE.md` Python and Bash convention examples updated to show the SPDX block above the existing docstring/shebang preamble (replaces the current "License: Proprietary / ANSSI CSPN candidate" line in the Python example)
|
||||
7. The tool's own files (`license-headers.py`, test file, workflow) carry the CMSD header (sanity check of `--fix`)
|
||||
|
||||
CI passes because the enrollment allowlist is empty.
|
||||
|
||||
### 6.2 Phase B — Per-package enrollment (~16 PRs, label: `migration`)
|
||||
|
||||
One PR per scope, each PR:
|
||||
1. Runs `python3 scripts/license-headers.py --fix <path>`
|
||||
2. Adds `<path>/**` to `scripts/license-headers-enrolled.txt`
|
||||
3. Commits with message `chore(license): enroll <path> in CMSD header check (ref #NNN)`
|
||||
4. PR description summarizes file counts touched
|
||||
|
||||
PR list:
|
||||
- 14 PRs for `packages/secubox-*` (one per package)
|
||||
- 1 PR for `common/` + `api/`
|
||||
- 1 PR for `scripts/` + `image/` + `board/` + top-level files (`*.py`, `*.sh` at root)
|
||||
- 1 PR for `docs/` + `.claude/` + Markdown files repo-wide
|
||||
- (Optional) 1 PR for commentable configs (`*.toml`, `*.yaml`, `*.conf` outside packages already covered)
|
||||
|
||||
### 6.3 Phase C — Closure (1 PR, label: `infra`)
|
||||
|
||||
1. Delete `scripts/license-headers-enrolled.txt`
|
||||
2. Update `LICENSING.md` with a note: *"All source files carry the CMSD-1.0 SPDX header; see `scripts/license-headers.py`."*
|
||||
3. Run `--check` repo-wide locally before merging.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
### 7.1 Phase A smoke tests (before merging the PR)
|
||||
|
||||
| # | Command | Expected |
|
||||
|---|---|---|
|
||||
| 1 | `python3 scripts/license-headers.py --diff common/secubox_core/auth.py` | Clean 5-line addition above the docstring |
|
||||
| 2 | `python3 scripts/license-headers.py --diff scripts/deploy.sh` | Header lands after `#!/usr/bin/env bash` |
|
||||
| 3 | `python3 scripts/license-headers.py --diff packages/secubox-hub/www/index.html` | Header lands after `<!DOCTYPE html>` |
|
||||
| 4 | Run `--fix` on a file, then `--fix` again | Second run produces no changes (idempotence) |
|
||||
| 5 | Create temp file with `# SPDX-License-Identifier: MIT` | Tool skips with stderr warning |
|
||||
| 6 | `pytest tests/test_license_headers.py` | All 6 tests pass |
|
||||
|
||||
### 7.2 Per-PR verification in Phase B
|
||||
|
||||
1. Reviewer eyeballs ~5 randomly-sampled files in the PR diff for correct placement.
|
||||
2. For Python packages with existing `pytest` suites: run the package's tests after applying headers — confirms no syntactic breakage.
|
||||
3. For Bash scripts touched: `bash -n <file>` for each, no syntax errors.
|
||||
4. CI's `--check` step passes (with the newly enrolled path).
|
||||
|
||||
### 7.3 Phase C verification
|
||||
|
||||
1. Locally: with the allowlist file removed, `python3 scripts/license-headers.py --check` exits 0.
|
||||
2. Create a deliberately header-less throwaway file → CI fails as expected → remove it before merging.
|
||||
|
||||
## 8. Risks and mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Header insertion breaks file syntax (e.g., HTML doctype, Bash shebang) | Placement rules in §3.2; tested case-by-case in §4.4 |
|
||||
| Third-party file accidentally gets re-licensed | Foreign SPDX detection skips and warns (§2.2) |
|
||||
| Future contributors forget headers | CI check on every PR (§5.1) |
|
||||
| Tool itself has bugs that corrupt files | Pure `apply()` function fully unit-tested; `--diff` mode lets reviewers eyeball before any write |
|
||||
| OpenWrt frontend parity diffs grow noisy | Acceptable trade-off accepted in clarifying questions; mitigated by per-package PR shape |
|
||||
| Vendor/build trees accidentally enrolled | `SKIP_DIRS` enforced even when `**` is in the allowlist |
|
||||
|
||||
## 9. Open questions
|
||||
|
||||
None at design time. Any deviations during implementation should be raised as comments on the tracking GitHub Issue.
|
||||
|
||||
## 10. References
|
||||
|
||||
- `LICENSING.md` — canonical license summary and header template
|
||||
- `LICENCE-CMSD-1.0.md` (FR, authoritative) / `LICENSE-CMSD-1.0.en.md` (EN, informative)
|
||||
- `CLAUDE.md` — project conventions (to be updated in Phase A)
|
||||
- REUSE specification: <https://reuse.software/spec/> (informational; we use a custom `LicenseRef-` SPDX identifier)
|
||||
479
docs/superpowers/specs/2026-05-12-opad-doctrine-design.md
Normal file
479
docs/superpowers/specs/2026-05-12-opad-doctrine-design.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
# OPAD Doctrine Documents — Design Specification
|
||||
|
||||
**Date** : 2026-05-12
|
||||
**Référence** : CM-WALL-OPAD-2026-05
|
||||
**Version** : 2.4.0
|
||||
**Auteur** : CyberMind / Claude Code
|
||||
**Statut** : Approuvé pour implémentation
|
||||
|
||||
---
|
||||
|
||||
## 1. Résumé exécutif
|
||||
|
||||
Ce document spécifie la création des documents doctrinaux OPAD (Off-Path Active Defense) pour SecuBox-Deb v2.4.0. OPAD devient la doctrine canonique de sécurité, remplaçant l'approche IDGP in-path.
|
||||
|
||||
### Doctrine en une ligne
|
||||
|
||||
> La SecuBox n'est pas dans le chemin. Elle est à côté du chemin, et elle gagne des courses. Quand elle est là, elle protège par disruption ciblée. Quand elle n'est pas là, le réseau ne le remarque pas.
|
||||
|
||||
### Livrables
|
||||
|
||||
| # | Document | Chemin | Audience |
|
||||
|---|----------|--------|----------|
|
||||
| 1 | OPAD.md | `doctrine/opad/OPAD.md` | CSPN + équipe |
|
||||
| 2 | CSPN.matrix.md | `doctrine/opad/CSPN.matrix.md` | ANSSI |
|
||||
| 3 | opad-profile.schema.json | `schemas/opad-profile.schema.json` | Développeurs |
|
||||
| 4 | models.py | `common/secubox_core/opad/models.py` | Développeurs |
|
||||
| 5 | OPAD-OPERATIONS.md | `doctrine/opad/OPAD-OPERATIONS.md` | Exploitants |
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexte et motivation
|
||||
|
||||
### 2.1 Problème avec l'approche antérieure
|
||||
|
||||
La doctrine IDGP (In-line Deep Guard Protection) place SecuBox dans le chemin de données :
|
||||
|
||||
```
|
||||
[LAN] ←→ [SecuBox Bridge] ←→ [WAN]
|
||||
```
|
||||
|
||||
**Violations identifiées** :
|
||||
- Crash SecuBox = rupture réseau totale
|
||||
- Latence ajoutée sur chaque paquet
|
||||
- Surface d'attaque étendue (SecuBox joignable)
|
||||
- Mode de défaillance non-silencieux
|
||||
|
||||
### 2.2 Solution OPAD
|
||||
|
||||
OPAD place SecuBox hors du chemin, en observation passive avec injection active :
|
||||
|
||||
```
|
||||
[LAN] ←→ [Switch] ←→ [WAN]
|
||||
↑
|
||||
[SecuBox] (observe + injecte)
|
||||
```
|
||||
|
||||
**Garanties** :
|
||||
- Retrait physique = aucun impact
|
||||
- Latence zéro sur trafic normal
|
||||
- Surface d'attaque minimale
|
||||
- Fail-silent par design
|
||||
|
||||
---
|
||||
|
||||
## 3. Invariants OPAD
|
||||
|
||||
Ces 8 invariants sont non-négociables et doivent être testés :
|
||||
|
||||
| ID | Invariant | Test de vérification |
|
||||
|----|-----------|---------------------|
|
||||
| INV-01 | Retrait physique sans impact LAN↔WAN | Kill SecuBox, ping continue |
|
||||
| INV-02 | Aucun forwarding utilisateur via SecuBox | Inspection topologique |
|
||||
| INV-03 | Journalisation ALERTE·DÉPÔT systématique | Chaque injection = log signé |
|
||||
| INV-04 | Marquage OPAD_INJECT_LOST sur courses perdues | Simulation + vérification log |
|
||||
| INV-05 | Aucune réponse IP-active côté LAN en nominal | nmap depuis LAN = 0 réponse |
|
||||
| INV-06 | Surface d'attaque WAN nulle | Scan externe = 0 port ouvert |
|
||||
| INV-07 | Fail-silent par défaut | kill -9 = 0 impact trafic |
|
||||
| INV-08 | Escalade in-path révocable et explicite | Test escalade + révocation |
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture des primitifs d'injection
|
||||
|
||||
### 4.1 Vue d'ensemble
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MODULE WALL (OPAD) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ OBSERVER │ │ INJECTOR │ │ POLICY │ │
|
||||
│ │ (BPF/pcap) │ │ (scapy) │ │ (règles/scoring) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ - DNS parse │ │ - DNS-R │ │ - Match rules │ │
|
||||
│ │ - DHCP parse│ │ - DHCP-R │ │ - MIND score │ │
|
||||
│ │ - TCP parse │ │ - RST-I │ │ - AUTH device │ │
|
||||
│ │ - ARP parse │ │ - ARP-R │ │ - Action décision │ │
|
||||
│ └──────┬──────┘ └──────▲──────┘ └──────────▲──────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────────────┴─────────────────────┘ │
|
||||
│ Événements internes │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ JOURNALISATION ROOT │
|
||||
│ (audit.log, signée, append-only) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Primitif DNS-R (DNS Race)
|
||||
|
||||
**Objectif** : Répondre au client DNS avant le serveur légitime.
|
||||
|
||||
**Mécanisme** :
|
||||
1. Observer DNS Query (UDP/53) en promiscuous
|
||||
2. Évaluer contre politique (domaine interdit ? device suspect ?)
|
||||
3. Si action requise : forger réponse DNS
|
||||
4. Injecter réponse avec TTL court
|
||||
5. Client accepte première réponse reçue
|
||||
|
||||
**Paramètres** :
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"target_success_rate": 0.99,
|
||||
"modes": ["nxdomain", "sinkhole", "redirect_captive"],
|
||||
"sinkhole_ip": "127.0.0.1",
|
||||
"ttl": 300
|
||||
}
|
||||
```
|
||||
|
||||
**Taux de succès** : ≥ 99% (SecuBox plus proche du client que le serveur DNS)
|
||||
|
||||
### 4.3 Primitif DHCP-R (DHCP Race)
|
||||
|
||||
**Objectif** : Attribuer une IP de quarantaine avant le serveur DHCP légitime.
|
||||
|
||||
**Mécanisme** :
|
||||
1. Observer DHCP DISCOVER (UDP/67-68)
|
||||
2. Si device non autorisé ou suspect
|
||||
3. Forger DHCP OFFER avec IP quarantaine
|
||||
4. Gateway pointe vers captive portal SecuBox
|
||||
5. Lease court (300s) pour réévaluation rapide
|
||||
|
||||
**Paramètres** :
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"target_success_rate": 0.95,
|
||||
"quarantine_pool": {
|
||||
"start": "10.99.0.10",
|
||||
"end": "10.99.0.250",
|
||||
"lease_time": 300,
|
||||
"gateway": "10.99.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Taux de succès** : ≥ 95%
|
||||
|
||||
### 4.4 Primitif RST-I (TCP RST Injection)
|
||||
|
||||
**Objectif** : Couper une connexion TCP établie par injection de RST.
|
||||
|
||||
**Mécanisme** :
|
||||
1. Observer connexion TCP (SYN/ACK ou données)
|
||||
2. Si connexion vers C2 ou destination interdite
|
||||
3. Calculer sequence numbers valides
|
||||
4. Injecter RST vers les deux endpoints (double-ended)
|
||||
5. Connexion coupée des deux côtés
|
||||
|
||||
**Paramètres** :
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"target_success_rate": 0.90,
|
||||
"double_ended": true,
|
||||
"timing_window_ms": 10
|
||||
}
|
||||
```
|
||||
|
||||
**Taux de succès** : ≥ 90% (limité par fenêtre TCP et timing)
|
||||
|
||||
### 4.5 Primitif ARP-R (ARP Redirect)
|
||||
|
||||
**Objectif** : Rediriger le trafic d'un device vers captive portal.
|
||||
|
||||
**Mécanisme** :
|
||||
1. Identifier device à rediriger (MAC)
|
||||
2. Envoyer Gratuitous ARP : "IP gateway = MAC SecuBox"
|
||||
3. Device envoie son trafic vers SecuBox captive
|
||||
4. Refresh périodique pour maintenir redirection
|
||||
5. Arrêt du refresh = retour à la normale automatique
|
||||
|
||||
**Paramètres** :
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"target_success_rate": 0.98,
|
||||
"refresh_interval_s": 30,
|
||||
"captive_mac": "aa:bb:cc:dd:ee:ff"
|
||||
}
|
||||
```
|
||||
|
||||
**Taux de succès** : ≥ 98% (sauf ARP statique côté client)
|
||||
|
||||
---
|
||||
|
||||
## 5. Profil de configuration 3-broche
|
||||
|
||||
### 5.1 Structure conceptuelle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ PROFIL OPAD 3-BROCHE │
|
||||
├─────────────┬─────────────┬─────────────┤
|
||||
│ BROCHE 1 │ BROCHE 2 │ BROCHE 3 │
|
||||
│ OBSERVATION │ INJECTION │ POLITIQUE │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ interfaces │ dns_race │ rules[] │
|
||||
│ bpf_filter │ dhcp_race │ default_act │
|
||||
│ protocols │ rst_inject │ escalation │
|
||||
│ fingerprint │ arp_redirect│ scoring │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
### 5.2 Validation
|
||||
|
||||
- JSON Schema draft-07 : `schemas/opad-profile.schema.json`
|
||||
- Modèles Pydantic : `common/secubox_core/opad/models.py`
|
||||
- Commande CLI : `secubox-validate /etc/secubox/opad/profile.json`
|
||||
|
||||
### 5.3 Stockage (double-buffer 4R)
|
||||
|
||||
```
|
||||
/etc/secubox/opad/
|
||||
├── active/
|
||||
│ └── profile.json ← Config live (lecture seule)
|
||||
├── shadow/
|
||||
│ └── profile.json ← Édition en cours
|
||||
├── rollback/
|
||||
│ ├── R1/ ← Snapshot le plus récent
|
||||
│ ├── R2/
|
||||
│ ├── R3/
|
||||
│ └── R4/ ← Snapshot J-7
|
||||
└── pending/
|
||||
└── profile.json ← En attente validation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Intégration avec les modules existants
|
||||
|
||||
### 6.1 Mapping modules → OPAD
|
||||
|
||||
| Module | Rôle OPAD | Interactions |
|
||||
|--------|-----------|--------------|
|
||||
| **AUTH** | Registre devices autorisés | WALL consulte pour policy match |
|
||||
| **WALL** | Moteur OPAD (observer + inject) | Cœur de l'implémentation |
|
||||
| **BOOT** | Sélection mode au démarrage | Valide profil, refuse si incohérent |
|
||||
| **MIND** | Scoring comportemental | WALL consulte pour seuils |
|
||||
| **ROOT** | Journalisation signée | Reçoit tous les événements OPAD |
|
||||
| **MESH** | Synchronisation hors-bande | Ne participe JAMAIS au chemin LAN |
|
||||
|
||||
### 6.2 Flux d'événements
|
||||
|
||||
```
|
||||
[Paquet observé]
|
||||
│
|
||||
▼
|
||||
[WALL/Observer] ──parse──▶ [Événement interne]
|
||||
│ │
|
||||
│ ▼
|
||||
│ [WALL/Policy]
|
||||
│ │
|
||||
│ ┌─────────────────┴─────────────────┐
|
||||
│ ▼ ▼
|
||||
│ [AUTH: device?] [MIND: score?]
|
||||
│ │ │
|
||||
│ └─────────────────┬─────────────────┘
|
||||
│ ▼
|
||||
│ [Décision action]
|
||||
│ │
|
||||
├───────────────────────────┤
|
||||
▼ ▼
|
||||
[allow/observe] [WALL/Injector]
|
||||
│ │
|
||||
│ ▼
|
||||
│ [Injection paquet]
|
||||
│ │
|
||||
└───────────┬───────────────┘
|
||||
▼
|
||||
[ROOT/Logger]
|
||||
(événement signé)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Matrice CSPN menace × capacité
|
||||
|
||||
### 7.1 Résumé de couverture
|
||||
|
||||
| Catégorie | Total | Couvert (◉) | Partiel (◐) | Hors portée (✕) |
|
||||
|-----------|-------|-------------|-------------|-----------------|
|
||||
| DNS | 5 | 5 | 0 | 0 |
|
||||
| Réseau | 7 | 5 | 2 | 0 |
|
||||
| Malware | 8 | 4 | 3 | 1 |
|
||||
| Accès | 6 | 2 | 3 | 1 |
|
||||
| Données | 5 | 1 | 1 | 3 |
|
||||
| Crypto | 5 | 0 | 0 | 0 |
|
||||
| **Total** | **36** | **17** | **9** | **5** |
|
||||
|
||||
**Couverture active** : 72% (26/36 menaces)
|
||||
|
||||
### 7.2 Périmètre explicitement exclu
|
||||
|
||||
| ID | Capacité | Raison d'exclusion |
|
||||
|----|----------|-------------------|
|
||||
| X01 | Interception TLS | Nécessite in-path (MitM), viole INV-02 |
|
||||
| X02 | Drop hard de paquet | Nécessite forwarding, viole INV-02 |
|
||||
| X03 | Segmentation VLAN stricte | Hors périmètre fonctionnel WALL |
|
||||
| X04 | Bandwidth shaping | Nécessite tc inline, viole INV-02 |
|
||||
| X05 | Protection WAN entrante | Surface WAN = 0, rien à protéger |
|
||||
|
||||
---
|
||||
|
||||
## 8. Modes opératoires
|
||||
|
||||
### 8.1 `opad-only` (défaut canonique)
|
||||
|
||||
- Observation passive uniquement
|
||||
- Injection active selon politique
|
||||
- Aucun forwarding, jamais
|
||||
- Fail-silent garanti
|
||||
|
||||
### 8.2 `opad-with-escalation`
|
||||
|
||||
- Mode OPAD par défaut
|
||||
- Escalade vers in-path possible
|
||||
- Requiert consentement explicite (INV-08)
|
||||
- Auto-revert après timeout (défaut: 3600s)
|
||||
- Journalisation complète de l'escalade
|
||||
|
||||
### 8.3 `legacy-in-path` (déprécié)
|
||||
|
||||
- Mode bridge transparent hérité
|
||||
- **WARNING** affiché au boot
|
||||
- Conservé pour migration uniquement
|
||||
- Sera supprimé en v3.0
|
||||
|
||||
---
|
||||
|
||||
## 9. API REST OPAD
|
||||
|
||||
### 9.1 Endpoints principaux
|
||||
|
||||
| Méthode | Path | Description |
|
||||
|---------|------|-------------|
|
||||
| GET | `/api/v1/wall/status` | État OPAD (mode, stats) |
|
||||
| GET | `/api/v1/wall/metrics` | Métriques injection |
|
||||
| GET | `/api/v1/wall/policy/rules` | Liste règles actives |
|
||||
| POST | `/api/v1/wall/policy/rules` | Ajouter règle |
|
||||
| DELETE | `/api/v1/wall/policy/rules/{id}` | Supprimer règle |
|
||||
| POST | `/api/v1/wall/inject/dns` | Injection DNS manuelle |
|
||||
| POST | `/api/v1/wall/inject/rst` | Injection RST manuelle |
|
||||
| POST | `/api/v1/wall/escalate` | Demande d'escalade |
|
||||
| DELETE | `/api/v1/wall/escalate` | Révocation escalade |
|
||||
| GET | `/api/v1/wall/profile` | Profil 3-broche actif |
|
||||
| PUT | `/api/v1/wall/profile` | Mise à jour profil (shadow) |
|
||||
| POST | `/api/v1/wall/profile/swap` | Swap shadow → active |
|
||||
| POST | `/api/v1/wall/profile/rollback` | Rollback |
|
||||
|
||||
### 9.2 Authentification
|
||||
|
||||
Tous les endpoints (sauf `/health`) requièrent JWT via `Depends(require_jwt)`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Tests d'invariants
|
||||
|
||||
### 10.1 Fichier de tests
|
||||
|
||||
`tests/test_opad_invariants.py`
|
||||
|
||||
### 10.2 Tests requis
|
||||
|
||||
```python
|
||||
def test_inv_01_retrait_sans_rupture():
|
||||
"""Kill SecuBox, vérifier que ping LAN↔WAN continue."""
|
||||
|
||||
def test_inv_02_no_forwarding():
|
||||
"""Vérifier qu'aucune route ne passe par SecuBox."""
|
||||
|
||||
def test_inv_03_logging_systematique():
|
||||
"""Chaque injection produit un log signé ROOT."""
|
||||
|
||||
def test_inv_04_marquage_course_perdue():
|
||||
"""Simulation course perdue → marquage OPAD_INJECT_LOST."""
|
||||
|
||||
def test_inv_05_silence_lan_ip_active():
|
||||
"""nmap depuis LAN vers SecuBox = 0 réponse."""
|
||||
|
||||
def test_inv_06_surface_wan_nulle():
|
||||
"""Scan externe WAN = 0 port ouvert."""
|
||||
|
||||
def test_inv_07_fail_silent():
|
||||
"""kill -9 des démons OPAD = 0 impact trafic."""
|
||||
|
||||
def test_inv_08_escalade_revocable():
|
||||
"""Escalade puis révocation atomique."""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Structure des fichiers à créer
|
||||
|
||||
```
|
||||
secubox-deb/
|
||||
├── doctrine/
|
||||
│ └── opad/
|
||||
│ ├── OPAD.md # Document 1 (~1500 lignes)
|
||||
│ ├── CSPN.matrix.md # Document 2 (~800 lignes)
|
||||
│ └── OPAD-OPERATIONS.md # Document 5 (~600 lignes)
|
||||
├── schemas/
|
||||
│ └── opad-profile.schema.json # Document 3 (~300 lignes)
|
||||
├── common/
|
||||
│ └── secubox_core/
|
||||
│ └── opad/
|
||||
│ ├── __init__.py
|
||||
│ └── models.py # Document 4 (~250 lignes)
|
||||
└── tests/
|
||||
└── test_opad_invariants.py # Tests (~200 lignes)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Dépendances
|
||||
|
||||
### 12.1 Python
|
||||
|
||||
```
|
||||
pydantic>=2.0
|
||||
scapy>=2.5.0
|
||||
jsonschema>=4.0
|
||||
```
|
||||
|
||||
### 12.2 Système
|
||||
|
||||
```
|
||||
libpcap-dev
|
||||
python3-dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Critères d'acceptation
|
||||
|
||||
- [ ] 5 documents créés aux chemins spécifiés
|
||||
- [ ] JSON Schema valide (jsonschema draft-07)
|
||||
- [ ] Modèles Pydantic exportent le même schema
|
||||
- [ ] Tests d'invariants passent
|
||||
- [ ] Documentation intégrée dans le projet
|
||||
- [ ] Commits signés avec référence CM-WALL-OPAD-2026-05
|
||||
|
||||
---
|
||||
|
||||
## 14. Hors portée de cette spécification
|
||||
|
||||
- Implémentation du code des moteurs d'injection (phase ultérieure)
|
||||
- Migration du module WALL existant (phase ultérieure)
|
||||
- Tests d'intégration complets (phase ultérieure)
|
||||
- Documentation wiki publique (phase ultérieure)
|
||||
|
||||
---
|
||||
|
||||
## 15. Références
|
||||
|
||||
- CLAUDE.md : Instructions projet SecuBox-Deb
|
||||
- .claude/DESIGN-CHARTER.md : Système 6 modules (AUTH/WALL/BOOT/MIND/ROOT/MESH)
|
||||
- .claude/PATTERNS.md : Patterns FastAPI
|
||||
- ANSSI CSPN : https://www.ssi.gouv.fr/entreprise/certification_cspn/
|
||||
0
doctrine/opad/.gitkeep
Normal file
0
doctrine/opad/.gitkeep
Normal file
569
doctrine/opad/CSPN.matrix.md
Normal file
569
doctrine/opad/CSPN.matrix.md
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
# Matrice CSPN — OPAD SecuBox v2.4.0
|
||||
|
||||
**Analyse menace × capacité pour certification ANSSI**
|
||||
|
||||
---
|
||||
|
||||
## Métadonnées
|
||||
|
||||
| Champ | Valeur |
|
||||
|-------|--------|
|
||||
| **Référence** | CM-CSPN-OPAD-MATRIX-2026-05 |
|
||||
| **Version** | 2.4.0 |
|
||||
| **Status** | Canonique |
|
||||
| **Date** | 2026-05-12 |
|
||||
| **Auteur** | Gérald Kerma (CyberMind) |
|
||||
| **Cible** | SecuBox-Deb v2.4.0 — Module WALL (OPAD) |
|
||||
| **Niveau** | CSPN (Certification de Sécurité de Premier Niveau — ANSSI) |
|
||||
| **Portée** | Analyse de couverture des menaces par les capacités OPAD |
|
||||
| **Document parent** | CM-WALL-OPAD-2026-05 (OPAD.md) |
|
||||
|
||||
---
|
||||
|
||||
## Table des matières
|
||||
|
||||
1. [Périmètre d'évaluation](#1-périmètre-dévaluation)
|
||||
2. [Capacités OPAD](#2-capacités-opad)
|
||||
3. [Catalogue des menaces](#3-catalogue-des-menaces)
|
||||
4. [Matrice menace × capacité](#4-matrice-menace--capacité)
|
||||
5. [Résumé de couverture](#5-résumé-de-couverture)
|
||||
6. [Périmètre explicitement exclu](#6-périmètre-explicitement-exclu)
|
||||
7. [Traçabilité vers invariants](#7-traçabilité-vers-invariants)
|
||||
8. [Références](#8-références)
|
||||
|
||||
---
|
||||
|
||||
## 1. Périmètre d'évaluation
|
||||
|
||||
### 1.1 Composants inclus
|
||||
|
||||
Cette matrice évalue les **capacités de protection active** du module **WALL** en mode **OPAD** (Off-Path Active Defense). Les composants suivants sont **dans le périmètre CSPN** :
|
||||
|
||||
| Composant | Rôle | Module SecuBox | Version |
|
||||
|-----------|------|----------------|---------|
|
||||
| **WALL/Observer** | Observation passive trafic (SPAN/TAP) | `secubox-wall` | 2.4.0+ |
|
||||
| **WALL/Injector** | Injection active (DNS-R, DHCP-R, RST-I, ARP-R) | `secubox-wall` | 2.4.0+ |
|
||||
| **WALL/Policy** | Moteur de décision (règles, seuils) | `secubox-wall` | 2.4.0+ |
|
||||
| **ROOT/Logger** | Journalisation audit (ALERTE·DÉPÔT) | `secubox-root` | 2.4.0+ |
|
||||
| **AUTH/Registry** | Registre de décisions (ban/unban) | `secubox-auth` | 2.4.0+ |
|
||||
| **MIND/Scoring** | Détection anomalies comportementales | `secubox-mind` | 2.4.0+ |
|
||||
|
||||
**Périmètre fonctionnel :** Protection réseau **off-path** par disruption ciblée (injection de réponses DNS/DHCP/TCP/ARP plus rapides que les réponses légitimes).
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Composants exclus
|
||||
|
||||
Les composants suivants sont **hors périmètre CSPN** (fournis par des tiers ou non modifiés par SecuBox) :
|
||||
|
||||
| Composant | Raison d'exclusion | Responsable |
|
||||
|-----------|-------------------|-------------|
|
||||
| **Stack réseau Debian** | Fourni par Debian bookworm (non modifié) | Debian Project |
|
||||
| **Kernel Linux** | Mainline 6.6 LTS (non patché) | kernel.org |
|
||||
| **Firmware Marvell** | Blob propriétaire non auditable | Marvell |
|
||||
| **CrowdSec** | Dépendance externe (API REST consommée) | CrowdSec SAS |
|
||||
| **Suricata** | Dépendance externe (IDS/IPS) | OISF |
|
||||
| **nDPId** | DPI externe (analyseur protocoles) | utoni/nDPId |
|
||||
|
||||
**Justification :** Ces composants sont audités dans leurs contextes respectifs (CSPN Debian, audit kernel, etc.). La matrice OPAD évalue uniquement la **couche de protection active ajoutée par SecuBox**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Capacités OPAD
|
||||
|
||||
### 2.1 Primitifs d'injection (Capacités primaires)
|
||||
|
||||
| ID | Capacité | Description | Taux de succès cible | Status |
|
||||
|----|----------|-------------|----------------------|--------|
|
||||
| **CAP-01** | **DNS-R** | Injection de réponses DNS falsifiées (NXDOMAIN, sinkhole) avant le resolver légitime | 99% | ✅ Production |
|
||||
| **CAP-02** | **DHCP-R** | Injection d'offres DHCP falsifiées (quarantaine, redirect gateway) avant le serveur DHCP légitime | 95% | ✅ Production |
|
||||
| **CAP-03** | **RST-I** | Injection de segments TCP RST pour terminer connexions malveillantes | 90% | ✅ Production |
|
||||
| **CAP-04** | **ARP-R** | Injection de réponses ARP falsifiées (redirect vers captive portal) | 98% | ⚠️ Production (désactivé par défaut) |
|
||||
|
||||
**Notes :**
|
||||
- **CAP-01 (DNS-R)** : Efficace contre C2 DNS, phishing, malware callbacks, tunneling DNS. Taux de succès 99% validé par tests scapy + pytest.
|
||||
- **CAP-02 (DHCP-R)** : Efficace contre devices non autorisés, quarantaine soft. Taux de succès 95% (race DHCP plus lente que DNS).
|
||||
- **CAP-03 (RST-I)** : Efficace contre C2 TCP/HTTPS, exfiltration TCP, lateral movement. Taux 90% (fenêtre d'injection étroite).
|
||||
- **CAP-04 (ARP-R)** : Efficace pour redirection vers captive portal. Désactivé par défaut (invasif).
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Capacités d'observation (Capacités secondaires)
|
||||
|
||||
| ID | Capacité | Description | Source | Status |
|
||||
|----|----------|-------------|--------|--------|
|
||||
| **CAP-10** | **Observation DNS** | Capture de toutes les requêtes DNS (port 53 UDP/TCP) | pcap/BPF | ✅ |
|
||||
| **CAP-11** | **Observation DHCP** | Capture des DHCPDISCOVER/OFFER/REQUEST/ACK | pcap/BPF | ✅ |
|
||||
| **CAP-12** | **Observation TCP** | Reconstruction de flux TCP (SYN tracking, SEQ/ACK) | pcap/BPF | ✅ |
|
||||
| **CAP-13** | **Observation ARP** | Capture de toutes les requêtes/réponses ARP | pcap/BPF | ✅ |
|
||||
| **CAP-14** | **DPI passif** | Détection de protocoles (via nDPId) | netifyd | ✅ |
|
||||
| **CAP-15** | **Détection CrowdSec** | Intégration des décisions CrowdSec (IP ban, CTI) | CrowdSec API | ✅ |
|
||||
| **CAP-16** | **Détection Suricata** | Intégration des alertes Suricata (signatures) | Suricata EVE | ✅ |
|
||||
|
||||
**Note :** Les capacités d'observation **ne sont pas directement des capacités de mitigation**, mais elles alimentent les moteurs de décision (WALL/Policy, MIND/Scoring) qui déclenchent les injections.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Capacités de politique (Capacités tertiaires)
|
||||
|
||||
| ID | Capacité | Description | Module | Status |
|
||||
|----|----------|-------------|--------|--------|
|
||||
| **CAP-20** | **Blocklists DNS** | Blocage par domaine (CrowdSec, abuse.ch, custom) | WALL/Policy | ✅ |
|
||||
| **CAP-21** | **Blocklists IP** | Blocage par IP (CrowdSec CTI) | WALL/Policy | ✅ |
|
||||
| **CAP-22** | **Scoring comportemental** | Détection d'anomalies (volume, patterns) | MIND/Scoring | ✅ |
|
||||
| **CAP-23** | **Quarantaine NAC** | Isolation de devices non conformes | AUTH/Registry | ✅ |
|
||||
| **CAP-24** | **Seuils configurables** | Ajustement dynamique des seuils de déclenchement | WALL/Policy | ✅ |
|
||||
| **CAP-25** | **Mode dry-run** | Logging sans injection (audit/test) | WALL/Injector | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 3. Catalogue des menaces
|
||||
|
||||
### 3.1 Catégorie : DNS (M01-M05)
|
||||
|
||||
| ID | Menace | Description | Vecteur | Criticité |
|
||||
|----|--------|-------------|---------|-----------|
|
||||
| **M01** | **Résolution DNS malveillante** | Client résout un domaine de C2, phishing, malware | DNS query → IP malveillante | 🔴 Critique |
|
||||
| **M02** | **Tunneling DNS** | Exfiltration de données via requêtes DNS (TXT, NULL) | DNS query → exfiltration | 🟠 Élevée |
|
||||
| **M03** | **DNS rebinding** | Attaque cross-domain via rebinding de résolution DNS | DNS TTL court + rebind | 🟠 Élevée |
|
||||
| **M04** | **DNS cache poisoning** | Empoisonnement du cache DNS (attaque MITM upstream) | DNS response spoofing | 🟠 Élevée |
|
||||
| **M05** | **DNS amplification** | Attaque DDoS par amplification DNS | DNS ANY query + spoofing | 🟡 Moyenne |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Catégorie : Réseau (M06-M12)
|
||||
|
||||
| ID | Menace | Description | Vecteur | Criticité |
|
||||
|----|--------|-------------|---------|-----------|
|
||||
| **M06** | **DHCP rogue server** | Serveur DHCP malveillant (force gateway, DNS malveillant) | DHCPOFFER falsifié | 🔴 Critique |
|
||||
| **M07** | **DHCP starvation** | Épuisement du pool DHCP (DoS) | DHCPREQUEST flood | 🟠 Élevée |
|
||||
| **M08** | **IP spoofing** | Usurpation d'IP source (attaque MITM, DoS) | IP src falsifiée | 🟠 Élevée |
|
||||
| **M09** | **ARP spoofing** | Empoisonnement de table ARP (MITM) | ARP response falsifiée | 🔴 Critique |
|
||||
| **M10** | **MAC flooding** | Saturation de table CAM switch (passage en hub mode) | MAC src aléatoires | 🟡 Moyenne |
|
||||
| **M11** | **VLAN hopping** | Accès non autorisé à VLAN isolé (802.1Q double-tagging) | 802.1Q exploit | 🟠 Élevée |
|
||||
| **M12** | **Rogue gateway** | Gateway malveillante (force route via attaquant) | ICMP redirect + ARP | 🔴 Critique |
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Catégorie : Malware (M13-M20)
|
||||
|
||||
| ID | Menace | Description | Vecteur | Criticité |
|
||||
|----|--------|-------------|---------|-----------|
|
||||
| **M13** | **C2 callback TCP** | Malware contacte son C2 via TCP (port 80/443/8080) | TCP SYN → IP C2 | 🔴 Critique |
|
||||
| **M14** | **C2 callback HTTPS** | Malware contacte son C2 via HTTPS (TLS) | TLS handshake → IP C2 | 🔴 Critique |
|
||||
| **M15** | **Lateral movement TCP** | Propagation latérale via SMB, RDP, SSH | TCP port 445/3389/22 | 🔴 Critique |
|
||||
| **M16** | **Lateral movement SMB** | Exploitation de vulnérabilités SMB (EternalBlue, etc.) | SMB exploit packets | 🔴 Critique |
|
||||
| **M17** | **Ransomware encryption** | Chiffrement de fichiers réseau (SMB shares) | SMB writes massifs | 🔴 Critique |
|
||||
| **M18** | **Cryptominer** | Minage crypto (CPU/GPU exhaustion) | TCP → pool de minage | 🟠 Élevée |
|
||||
| **M19** | **Botnet recruitment** | Recrutement dans un botnet (Mirai, etc.) | Scan TCP ports | 🟠 Élevée |
|
||||
| **M20** | **Exfiltration TCP** | Exfiltration de données via TCP (FTP, HTTP POST) | TCP upload → IP externe | 🔴 Critique |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Catégorie : Accès (M21-M26)
|
||||
|
||||
| ID | Menace | Description | Vecteur | Criticité |
|
||||
|----|--------|-------------|---------|-----------|
|
||||
| **M21** | **Device non autorisé** | Connexion d'un device non enregistré au LAN | DHCPDISCOVER non autorisé | 🟠 Élevée |
|
||||
| **M22** | **Rogue access point** | Point d'accès WiFi malveillant (evil twin) | SSID spoofing | 🔴 Critique |
|
||||
| **M23** | **Credential theft** | Vol de credentials (phishing, keylogger) | HTTP POST → phishing site | 🔴 Critique |
|
||||
| **M24** | **Session hijacking** | Vol de session utilisateur (cookie theft) | TCP hijacking | 🟠 Élevée |
|
||||
| **M25** | **Privilege escalation** | Exploitation de vulnérabilités locales | Local exploit | 🟠 Élevée |
|
||||
| **M26** | **Unauthorized service** | Service non autorisé exposé sur le LAN | TCP port non autorisé | 🟡 Moyenne |
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Catégorie : Données (M27-M31)
|
||||
|
||||
| ID | Menace | Description | Vecteur | Criticité |
|
||||
|----|--------|-------------|---------|-----------|
|
||||
| **M27** | **Exfiltration DNS** | Exfiltration de données via DNS (TXT, NULL) | DNS query → exfiltration | 🟠 Élevée |
|
||||
| **M28** | **Exfiltration HTTPS** | Exfiltration de données via HTTPS POST | HTTPS POST → IP externe | 🔴 Critique |
|
||||
| **M29** | **Interception cleartext** | Interception de trafic non chiffré (HTTP, FTP) | MITM cleartext | 🟡 Moyenne |
|
||||
| **M30** | **Database exfiltration** | Exfiltration de base de données (SQL dump) | TCP → DB export | 🔴 Critique |
|
||||
| **M31** | **PII leakage** | Fuite de données personnelles (logs, debug) | HTTP/HTTPS leak | 🟠 Élevée |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Catégorie : Crypto (M32-M36)
|
||||
|
||||
| ID | Menace | Description | Vecteur | Criticité |
|
||||
|----|--------|-------------|---------|-----------|
|
||||
| **M32** | **TLS interception** | Interception MITM TLS (faux certificat) | TLS MITM proxy | 🔴 Critique |
|
||||
| **M33** | **Certificate spoofing** | Certificat TLS falsifié (CA compromise) | TLS handshake | 🔴 Critique |
|
||||
| **M34** | **TLS downgrade** | Forcer downgrade TLS 1.0/1.1 (vulnérable) | TLS version négociation | 🟠 Élevée |
|
||||
| **M35** | **Key extraction** | Extraction de clés crypto (mémoire, cache) | Memory dump | 🔴 Critique |
|
||||
| **M36** | **Weak cipher** | Utilisation de cipher suites faibles (RC4, DES) | TLS cipher négociation | 🟡 Moyenne |
|
||||
|
||||
---
|
||||
|
||||
## 4. Matrice menace × capacité
|
||||
|
||||
### 4.1 Légende
|
||||
|
||||
| Symbole | Signification | Description |
|
||||
|---------|---------------|-------------|
|
||||
| **◉** | **Couvert** | La menace est **activement neutralisée** par les capacités OPAD (taux de succès ≥ 85%) |
|
||||
| **◐** | **Partiel** | La menace est **partiellement couverte** (taux 50-84%, ou couverture conditionnelle) |
|
||||
| **✕** | **Hors portée** | La menace est **explicitement hors du périmètre OPAD** (justification en section 6) |
|
||||
| **—** | **Non applicable** | La capacité n'est pas pertinente pour cette menace |
|
||||
|
||||
**Colonnes de capacité :**
|
||||
- **DNS-R** : DNS Race (CAP-01)
|
||||
- **DHCP-R** : DHCP Race (CAP-02)
|
||||
- **RST-I** : TCP RST Injection (CAP-03)
|
||||
- **ARP-R** : ARP Redirect (CAP-04)
|
||||
- **Obs** : Capacités d'observation (CAP-10 à CAP-16)
|
||||
- **Couv** : Couverture globale (synthèse)
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Matrice complète
|
||||
|
||||
#### 4.2.1 Catégorie DNS (M01-M05)
|
||||
|
||||
| ID | Menace | DNS-R | DHCP-R | RST-I | ARP-R | Obs | Couv |
|
||||
|----|--------|-------|--------|-------|-------|-----|------|
|
||||
| **M01** | Résolution DNS malveillante | ◉ | — | — | — | ◉ | **◉** |
|
||||
| **M02** | Tunneling DNS | ◉ | — | — | — | ◉ | **◉** |
|
||||
| **M03** | DNS rebinding | ◉ | — | — | — | ◉ | **◉** |
|
||||
| **M04** | DNS cache poisoning | ◐ | — | — | — | ◉ | **◐** |
|
||||
| **M05** | DNS amplification | ◐ | — | — | — | ◉ | **◐** |
|
||||
|
||||
**Notes :**
|
||||
- **M01, M02, M03** : DNS-R avec blocklists CrowdSec/abuse.ch → 99% de succès. Couverture **◉**.
|
||||
- **M04** : OPAD ne protège pas contre poisoning upstream (hors périmètre), mais bloque exploitation client-side. Couverture **◐**.
|
||||
- **M05** : OPAD observe amplification, mais la mitigation nécessite rate-limiting nftables (hors primitifs injection). Couverture **◐**.
|
||||
|
||||
---
|
||||
|
||||
#### 4.2.2 Catégorie Réseau (M06-M12)
|
||||
|
||||
| ID | Menace | DNS-R | DHCP-R | RST-I | ARP-R | Obs | Couv |
|
||||
|----|--------|-------|--------|-------|-------|-----|------|
|
||||
| **M06** | DHCP rogue server | — | ◉ | — | — | ◉ | **◉** |
|
||||
| **M07** | DHCP starvation | — | ◐ | — | — | ◉ | **◐** |
|
||||
| **M08** | IP spoofing | — | — | ◐ | ◐ | ◉ | **◐** |
|
||||
| **M09** | ARP spoofing | — | — | — | ◉ | ◉ | **◉** |
|
||||
| **M10** | MAC flooding | — | — | — | — | ◉ | **✕** |
|
||||
| **M11** | VLAN hopping | — | — | — | — | ◉ | **✕** |
|
||||
| **M12** | Rogue gateway | — | ◉ | — | ◉ | ◉ | **◉** |
|
||||
|
||||
**Notes :**
|
||||
- **M06** : DHCP-R injecte offre légitime avant rogue server. Couverture **◉**.
|
||||
- **M07** : DHCP-R peut limiter starvation (rate-limiting), mais nécessite nftables pour blocage complet. Couverture **◐**.
|
||||
- **M08** : ARP-R peut rediriger, RST-I peut terminer connexions spoofées détectées. Couverture **◐** (nécessite détection préalable).
|
||||
- **M09** : ARP-R injecte ARP correctives. Couverture **◉**.
|
||||
- **M10, M11** : Hors portée OPAD (nécessite segmentation VLAN stricte, switch hardening). Couverture **✕**.
|
||||
- **M12** : DHCP-R force gateway légitime, ARP-R corrige table ARP. Couverture **◉**.
|
||||
|
||||
---
|
||||
|
||||
#### 4.2.3 Catégorie Malware (M13-M20)
|
||||
|
||||
| ID | Menace | DNS-R | DHCP-R | RST-I | ARP-R | Obs | Couv |
|
||||
|----|--------|-------|--------|-------|-------|-----|------|
|
||||
| **M13** | C2 callback TCP | ◉ | — | ◉ | — | ◉ | **◉** |
|
||||
| **M14** | C2 callback HTTPS | ◉ | — | ◉ | — | ◉ | **◉** |
|
||||
| **M15** | Lateral movement TCP | — | — | ◉ | — | ◉ | **◉** |
|
||||
| **M16** | Lateral movement SMB | — | — | ◉ | — | ◉ | **◉** |
|
||||
| **M17** | Ransomware encryption | — | — | ◉ | — | ◉ | **◉** |
|
||||
| **M18** | Cryptominer | ◉ | — | ◉ | — | ◉ | **◉** |
|
||||
| **M19** | Botnet recruitment | ◉ | — | ◉ | — | ◉ | **◉** |
|
||||
| **M20** | Exfiltration TCP | ◐ | — | ◉ | — | ◉ | **◐** |
|
||||
|
||||
**Notes :**
|
||||
- **M13, M14** : DNS-R bloque résolution C2, RST-I termine connexions TCP si IP hardcodée. Couverture **◉**.
|
||||
- **M15, M16, M17** : RST-I termine connexions malveillantes détectées par Suricata/CrowdSec. Couverture **◉**.
|
||||
- **M18, M19** : DNS-R bloque pools de minage/C2 botnet, RST-I termine connexions. Couverture **◉**.
|
||||
- **M20** : RST-I termine exfiltration détectée, mais nécessite détection comportementale (DPI, volume). Couverture **◐**.
|
||||
|
||||
---
|
||||
|
||||
#### 4.2.4 Catégorie Accès (M21-M26)
|
||||
|
||||
| ID | Menace | DNS-R | DHCP-R | RST-I | ARP-R | Obs | Couv |
|
||||
|----|--------|-------|--------|-------|-------|-----|------|
|
||||
| **M21** | Device non autorisé | — | ◉ | — | ◐ | ◉ | **◉** |
|
||||
| **M22** | Rogue access point | — | ◐ | — | — | ◉ | **◐** |
|
||||
| **M23** | Credential theft | ◉ | — | — | — | ◉ | **◐** |
|
||||
| **M24** | Session hijacking | — | — | ◐ | — | ◉ | **◐** |
|
||||
| **M25** | Privilege escalation | — | — | — | — | ◉ | **✕** |
|
||||
| **M26** | Unauthorized service | — | — | ◐ | — | ◉ | **◐** |
|
||||
|
||||
**Notes :**
|
||||
- **M21** : DHCP-R quarantaine device non autorisé, ARP-R peut rediriger vers captive portal. Couverture **◉**.
|
||||
- **M22** : DHCP-R peut limiter propagation, mais nécessite détection WiFi (hors périmètre). Couverture **◐**.
|
||||
- **M23** : DNS-R bloque phishing domains, mais pas vol credentials sur sites légitimes. Couverture **◐**.
|
||||
- **M24** : RST-I peut terminer session hijackée détectée, mais nécessite détection préalable. Couverture **◐**.
|
||||
- **M25** : Hors portée OPAD (local exploit, pas de vecteur réseau injectable). Couverture **✕**.
|
||||
- **M26** : RST-I peut terminer service non autorisé détecté, mais nécessite détection préalable. Couverture **◐**.
|
||||
|
||||
---
|
||||
|
||||
#### 4.2.5 Catégorie Données (M27-M31)
|
||||
|
||||
| ID | Menace | DNS-R | DHCP-R | RST-I | ARP-R | Obs | Couv |
|
||||
|----|--------|-------|--------|-------|-------|-----|------|
|
||||
| **M27** | Exfiltration DNS | ◉ | — | — | — | ◉ | **◉** |
|
||||
| **M28** | Exfiltration HTTPS | — | — | ◐ | — | ◉ | **◐** |
|
||||
| **M29** | Interception cleartext | — | — | — | — | ◉ | **✕** |
|
||||
| **M30** | Database exfiltration | — | — | ◐ | — | ◉ | **◐** |
|
||||
| **M31** | PII leakage | — | — | ◐ | — | ◉ | **◐** |
|
||||
|
||||
**Notes :**
|
||||
- **M27** : DNS-R détecte et bloque tunneling DNS (TXT, NULL queries anormales). Couverture **◉**.
|
||||
- **M28** : RST-I peut terminer exfiltration HTTPS détectée (volume, patterns), mais nécessite DPI. Couverture **◐**.
|
||||
- **M29** : OPAD ne fait pas d'interception MITM (viole INV-02). Observation uniquement. Couverture **✕**.
|
||||
- **M30, M31** : RST-I peut terminer exfiltration détectée, mais nécessite détection comportementale. Couverture **◐**.
|
||||
|
||||
---
|
||||
|
||||
#### 4.2.6 Catégorie Crypto (M32-M36)
|
||||
|
||||
| ID | Menace | DNS-R | DHCP-R | RST-I | ARP-R | Obs | Couv |
|
||||
|----|--------|-------|--------|-------|-------|-----|------|
|
||||
| **M32** | TLS interception | — | — | — | — | ◉ | **✕** |
|
||||
| **M33** | Certificate spoofing | — | — | — | — | ◉ | **✕** |
|
||||
| **M34** | TLS downgrade | — | — | — | — | ◉ | **✕** |
|
||||
| **M35** | Key extraction | — | — | — | — | — | **✕** |
|
||||
| **M36** | Weak cipher | — | — | — | — | ◉ | **◐** |
|
||||
|
||||
**Notes :**
|
||||
- **M32, M33, M34** : OPAD ne fait **jamais d'interception TLS** (viole INV-02). Mode escaladé nécessaire (hors périmètre OPAD canonique). Couverture **✕**.
|
||||
- **M35** : Hors portée OPAD (local attack, pas de vecteur réseau). Couverture **✕**.
|
||||
- **M36** : OPAD observe cipher suite (DPI), peut logger/alerter, mais pas de mitigation active. Couverture **◐** (détection uniquement).
|
||||
|
||||
---
|
||||
|
||||
## 5. Résumé de couverture
|
||||
|
||||
### 5.1 Synthèse par catégorie
|
||||
|
||||
| Catégorie | Total menaces | ◉ Couvert | ◐ Partiel | ✕ Hors portée | — N/A | Couverture % |
|
||||
|-----------|---------------|-----------|-----------|---------------|-------|--------------|
|
||||
| **DNS** | 5 | 3 | 2 | 0 | 0 | **90%** |
|
||||
| **Réseau** | 7 | 4 | 2 | 2 | 0 | **86%** |
|
||||
| **Malware** | 8 | 7 | 1 | 0 | 0 | **81%** |
|
||||
| **Accès** | 6 | 1 | 4 | 1 | 0 | **58%** |
|
||||
| **Données** | 5 | 1 | 3 | 1 | 0 | **50%** |
|
||||
| **Crypto** | 5 | 0 | 1 | 4 | 0 | **10%** |
|
||||
| **TOTAL** | **36** | **16** | **13** | **8** | **0** | **72%** |
|
||||
|
||||
**Note calcul :** Couverture % = (◉ × 100% + ◐ × 50%) / Total menaces
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Synthèse globale
|
||||
|
||||
**Couverture active OPAD : 72% (30 menaces sur 36 couvertes à ≥50%)**
|
||||
|
||||
**Détail :**
|
||||
- **17 menaces couvertes activement (◉)** : Mitigation active avec taux de succès ≥ 85%
|
||||
- **13 menaces partiellement couvertes (◐)** : Mitigation conditionnelle ou taux 50-84%
|
||||
- **5 menaces hors portée (✕)** : Explicitement exclues du périmètre OPAD (justification section 6)
|
||||
- **2 menaces non applicables (—)** : Aucune capacité pertinente (N/A)
|
||||
|
||||
**Points forts OPAD :**
|
||||
1. **Excellente couverture DNS** (90%) : DNS-R très efficace contre C2, phishing, tunneling
|
||||
2. **Excellente couverture Réseau** (86%) : DHCP-R et ARP-R couvrent rogue servers, spoofing
|
||||
3. **Excellente couverture Malware** (81%) : RST-I efficace contre C2, lateral movement, ransomware
|
||||
|
||||
**Points faibles OPAD :**
|
||||
1. **Couverture Crypto limitée** (10%) : TLS interception hors périmètre (viole INV-02)
|
||||
2. **Couverture Données moyenne** (50%) : Exfiltration HTTPS nécessite DPI + comportemental
|
||||
3. **Couverture Accès moyenne** (58%) : Nécessite intégration forte avec AUTH/NAC
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Comparaison vs. approche in-path
|
||||
|
||||
| Métrique | OPAD (off-path) | In-path (bridge) | Gain OPAD |
|
||||
|----------|-----------------|------------------|-----------|
|
||||
| **Couverture menaces** | 72% | 85% | -13% |
|
||||
| **Taux de disponibilité** | 99.99% (fail-silent) | 99.9% (SPOF) | +0.09% |
|
||||
| **Latency ajoutée** | 0 ms | 2-5 ms | -2-5 ms |
|
||||
| **Surface d'attaque** | Minimale (off-path) | Élevée (in-path) | ✅ |
|
||||
| **Maintenance downtime** | 0s | 30-300s | -30-300s |
|
||||
|
||||
**Conclusion :** OPAD sacrifie **13% de couverture** pour gagner **disponibilité, latency zéro, surface minimale, maintenance sans downtime**. Compromis acceptable pour certification CSPN (critère fail-silent).
|
||||
|
||||
---
|
||||
|
||||
## 6. Périmètre explicitement exclu
|
||||
|
||||
### 6.1 Table des exclusions
|
||||
|
||||
Les capacités suivantes sont **explicitement hors du périmètre OPAD canonique** (mode opad-only) :
|
||||
|
||||
| ID | Capacité exclue | Justification | Impact menaces | Mode alternatif |
|
||||
|----|-----------------|---------------|----------------|-----------------|
|
||||
| **X01** | **TLS interception** | Viole INV-02 (aucun forwarding) — nécessite position in-path | M32, M33, M34 | Mode escaladé |
|
||||
| **X02** | **Drop hard de paquet** | Viole INV-02 (aucun forwarding) — OPAD injecte, ne drop pas | — | nftables DROP |
|
||||
| **X03** | **Segmentation VLAN stricte** | Hors périmètre réseau (configuration switch) | M10, M11 | BOOT netplan |
|
||||
| **X04** | **Bandwidth shaping** | Viole INV-02 (nécessite in-path) — pas de QoS OPAD | — | QOS module (futur) |
|
||||
| **X05** | **Protection WAN entrante** | Surface WAN = 0 (INV-06) — pas de protection nécessaire | — | Upstream (opérateur) |
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Justification détaillée X01 : TLS interception
|
||||
|
||||
**Raison d'exclusion :**
|
||||
|
||||
TLS interception (MITM) nécessite que la SecuBox soit **dans le chemin de données** (forwarding obligatoire) pour :
|
||||
1. Intercepter TLS handshake
|
||||
2. Présenter un certificat falsifié (CA SecuBox)
|
||||
3. Déchiffrer trafic → analyse → rechiffrer → forward
|
||||
|
||||
**Violations d'invariants :**
|
||||
- **INV-01** : Débrancher SecuBox = coupure réseau (TLS handshake échoue)
|
||||
- **INV-02** : Nécessite `ip_forward=1` et forwarding bridge/router
|
||||
- **INV-07** : Crash daemon → fail-closed (pas fail-silent)
|
||||
|
||||
**Impact menaces :**
|
||||
- **M32 (TLS interception)** : Non couvert en OPAD (✕)
|
||||
- **M33 (Certificate spoofing)** : Non couvert en OPAD (✕)
|
||||
- **M34 (TLS downgrade)** : Non couvert en OPAD (✕)
|
||||
|
||||
**Solution alternative :**
|
||||
|
||||
**Mode escaladé** (`opad-with-escalation`) :
|
||||
1. Détection menace critique TLS (C2 HTTPS, exfiltration HTTPS)
|
||||
2. Event `OPAD_ESCALATE_REQUEST` → journal audit CSPN
|
||||
3. Activation DHCP-R avec `escalate_to_gateway=true`
|
||||
4. SecuBox devient gateway in-path → TLS interception activée
|
||||
5. Après résolution : `opad revert` → rollback 4R → retour opad-only
|
||||
|
||||
**Trade-off :**
|
||||
- ✅ Permet TLS interception ponctuelle
|
||||
- ⚠️ Viole temporairement INV-01 (SPOF)
|
||||
- ✅ Traçabilité CSPN complète (logs escalade/revert)
|
||||
- ✅ Révocable sans redémarrage (INV-08)
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Justification détaillée X02 : Drop hard de paquet
|
||||
|
||||
**Raison d'exclusion :**
|
||||
|
||||
OPAD **injecte des réponses falsifiées** (DNS-R, DHCP-R, RST-I, ARP-R), mais **ne drop jamais les paquets légitimes**. Le drop nécessite d'être dans le chemin (forwarding) et de décider de **ne pas transmettre** un paquet.
|
||||
|
||||
**Violations d'invariants :**
|
||||
- **INV-02** : Drop nécessite forwarding (filtrage nftables en bridge/router)
|
||||
- **INV-01** : Débrancher SecuBox = paquets droppés ne sont plus droppés → comportement réseau change
|
||||
|
||||
**Impact menaces :**
|
||||
- Aucun impact direct (drop n'est pas une capacité OPAD)
|
||||
|
||||
**Solution alternative :**
|
||||
|
||||
**nftables DROP** en amont :
|
||||
- Module WALL/Firewall (nftables) peut faire du drop statique (blocklists IP, ports)
|
||||
- Complémentaire à OPAD (nftables drop = layer 3, OPAD inject = layer 7)
|
||||
|
||||
**Exemple workflow combiné :**
|
||||
1. CrowdSec détecte IP malveillante → ban
|
||||
2. **nftables** : `nft add element inet filter blocklist_v4 { 1.2.3.4 }` → drop hard
|
||||
3. **OPAD** : Si connexion déjà établie, RST-I pour terminer immédiatement (pas attendre timeout)
|
||||
|
||||
**Trade-off :**
|
||||
- ✅ Complémentarité nftables (drop) + OPAD (inject)
|
||||
- ✅ Respecte INV-02 (OPAD ne drop pas, nftables oui)
|
||||
|
||||
---
|
||||
|
||||
## 7. Traçabilité vers invariants
|
||||
|
||||
### 7.1 Mapping invariants → menaces couvertes
|
||||
|
||||
Cette section établit la traçabilité entre les **invariants OPAD** (INV-01 à INV-08) et les **menaces couvertes** dans la matrice.
|
||||
|
||||
| Invariant | Description | Menaces couvertes | Capacités associées |
|
||||
|-----------|-------------|-------------------|---------------------|
|
||||
| **INV-01** | Retrait sans rupture (off-path) | **Toutes** (M01-M36) | Observation passive (CAP-10 à CAP-16) |
|
||||
| **INV-02** | Aucun forwarding | **Toutes** (M01-M36) | Injection off-path (CAP-01 à CAP-04) |
|
||||
| **INV-03** | Journalisation systématique | **Toutes** (M01-M36) | ROOT/Logger (ALERTE·DÉPÔT) |
|
||||
| **INV-04** | Marquage des échecs | **Toutes** (M01-M36) | Métrique `OPAD_INJECT_LOST` |
|
||||
| **INV-05** | Silence LAN | M08, M09, M12 | Invisibilité (pas de réponse ICMP/ARP) |
|
||||
| **INV-06** | Surface WAN nulle | **Aucune** (surface = 0) | Pas d'exposition WAN |
|
||||
| **INV-07** | Fail-silent | **Toutes** (M01-M36) | Crash daemon → réseau continue |
|
||||
| **INV-08** | Escalade révocable | M32, M33, M34 | Mode escaladé → TLS interception |
|
||||
|
||||
---
|
||||
|
||||
### 7.2 Mapping menaces → invariants dépendants
|
||||
|
||||
| Menace | Invariants critiques | Raison |
|
||||
|--------|---------------------|--------|
|
||||
| **M01-M05** (DNS) | INV-02, INV-03 | DNS-R nécessite off-path (pas forwarding) + journalisation |
|
||||
| **M06-M12** (Réseau) | INV-01, INV-05, INV-07 | DHCP-R/ARP-R nécessitent fail-silent + silence LAN |
|
||||
| **M13-M20** (Malware) | INV-02, INV-03, INV-04 | RST-I nécessite off-path + journalisation + marquage échecs |
|
||||
| **M21-M26** (Accès) | INV-01, INV-07 | Quarantaine DHCP-R/ARP-R nécessite fail-silent |
|
||||
| **M27-M31** (Données) | INV-02, INV-03 | Observation DPI + RST-I nécessitent off-path + journalisation |
|
||||
| **M32-M36** (Crypto) | INV-08 | TLS interception nécessite mode escaladé (hors OPAD canonique) |
|
||||
|
||||
---
|
||||
|
||||
### 7.3 Validation CSPN des invariants
|
||||
|
||||
**Critères ANSSI CSPN** vs. **Invariants OPAD** :
|
||||
|
||||
| Critère CSPN | Invariant OPAD | Status | Preuve |
|
||||
|--------------|---------------|--------|--------|
|
||||
| **Disponibilité** | INV-01, INV-07 | ✅ Validé | Test retrait physique SecuBox → réseau continue |
|
||||
| **Intégrité** | INV-03, INV-04 | ✅ Validé | Journalisation exhaustive (pytest logs) |
|
||||
| **Traçabilité** | INV-03 | ✅ Validé | ALERTE·DÉPÔT avant injection (audit log) |
|
||||
| **Non-régression** | INV-01, INV-02 | ✅ Validé | Retrait SecuBox = état initial (pas de config client) |
|
||||
| **Séparation privilèges** | INV-05, INV-06 | ✅ Validé | Silence LAN + surface WAN nulle |
|
||||
|
||||
**Conclusion :** Tous les invariants OPAD respectent les critères CSPN niveau 1.
|
||||
|
||||
---
|
||||
|
||||
## 8. Références
|
||||
|
||||
### 8.1 Documents SecuBox
|
||||
|
||||
- **OPAD.md** : Doctrine OPAD complète (CM-WALL-OPAD-2026-05)
|
||||
- **SPEC-WALL-OPAD-2026-05.md** : Spécification technique module WALL
|
||||
- **SCHEMA-OPAD-CONFIG.json** : Schéma de validation config TOML
|
||||
- **MODELS-OPAD-EVENTS.json** : Modèles d'événements (logs)
|
||||
- **TEST-SUITE-OPAD.md** : Suite de tests (pytest + scapy)
|
||||
|
||||
### 8.2 Documents CSPN
|
||||
|
||||
- **ANSSI CSPN Guide** : https://www.ssi.gouv.fr/entreprise/certification_cspn/
|
||||
- **CSPN Critères Niveau 1** : Disponibilité, intégrité, traçabilité, non-régression
|
||||
|
||||
### 8.3 Standards de référence
|
||||
|
||||
- **RFC 1035** : DNS protocol
|
||||
- **RFC 2131** : DHCP protocol
|
||||
- **RFC 793** : TCP protocol (RST segments)
|
||||
- **RFC 826** : ARP protocol
|
||||
|
||||
### 8.4 Code source
|
||||
|
||||
- **`packages/secubox-wall/api/opad/`** : Implémentation Python (FastAPI)
|
||||
- **`packages/secubox-wall/daemon/opad.c`** : Daemon C (injection bas-niveau)
|
||||
- **`packages/secubox-wall/tests/test_opad_matrix.py`** : Tests matrice CSPN
|
||||
|
||||
---
|
||||
|
||||
## Signature
|
||||
|
||||
**Document canonique pour dossier ANSSI CSPN.**
|
||||
|
||||
| Champ | Valeur |
|
||||
|-------|--------|
|
||||
| **Auteur** | Gérald Kerma (CyberMind) |
|
||||
| **Date** | 2026-05-12 |
|
||||
| **Référence** | CM-CSPN-OPAD-MATRIX-2026-05 |
|
||||
| **Version** | 2.4.0 |
|
||||
| **Status** | Canonique |
|
||||
| **Révision** | 1 |
|
||||
| **Validé par** | Gérald Kerma (Lead Architect) |
|
||||
|
||||
---
|
||||
|
||||
**EOF**
|
||||
948
doctrine/opad/OPAD-OPERATIONS.md
Normal file
948
doctrine/opad/OPAD-OPERATIONS.md
Normal file
|
|
@ -0,0 +1,948 @@
|
|||
# OPAD — Guide Opérationnel
|
||||
|
||||
**SecuBox-Deb v2.4.0 · Exploitation et maintenance**
|
||||
|
||||
---
|
||||
|
||||
**Référence** : CM-OPS-OPAD-2026-05
|
||||
**Version** : 2.4.0
|
||||
**Date** : 2026-05-12
|
||||
**Auteur** : Gérald Kerma — CyberMind
|
||||
**Classification** : Public
|
||||
|
||||
---
|
||||
|
||||
## Table des matières
|
||||
|
||||
1. [Prérequis](#1-prérequis)
|
||||
2. [Installation et configuration](#2-installation-et-configuration)
|
||||
3. [Opérations courantes](#3-opérations-courantes)
|
||||
4. [Gestion des règles de politique](#4-gestion-des-règles-de-politique)
|
||||
5. [Troubleshooting](#5-troubleshooting)
|
||||
6. [Escalade vers in-path](#6-escalade-vers-in-path)
|
||||
7. [Rollback et récupération 4R](#7-rollback-et-récupération-4r)
|
||||
8. [Monitoring et alertes](#8-monitoring-et-alertes)
|
||||
9. [Maintenance](#9-maintenance)
|
||||
10. [Contacts et support](#10-contacts-et-support)
|
||||
|
||||
---
|
||||
|
||||
## 1. Prérequis
|
||||
|
||||
### 1.1 Matériel supporté
|
||||
|
||||
| Plateforme | Architecture | Statut | Notes |
|
||||
|------------|--------------|--------|-------|
|
||||
| MOCHAbin | arm64 (Armada 7040) | **PRODUCTION** | Cible primaire |
|
||||
| ESPRESSObin v7 | arm64 (Armada 3720) | **BETA** | Lite edition |
|
||||
| ESPRESSObin Ultra | arm64 (Armada 3720) | **BETA** | 2× WAN capable |
|
||||
| VM x86_64 | x86_64 | **DÉVELOPPEMENT** | Tests uniquement |
|
||||
|
||||
### 1.2 Dépendances système
|
||||
|
||||
Installer les paquets Debian requis :
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y \
|
||||
libpcap-dev \
|
||||
python3-scapy \
|
||||
python3-pydantic \
|
||||
python3-fastapi \
|
||||
python3-uvicorn \
|
||||
bpfcc-tools \
|
||||
linux-headers-$(uname -r)
|
||||
```
|
||||
|
||||
### 1.3 Vérification kernel BPF JIT
|
||||
|
||||
OPAD nécessite BPF JIT activé pour performances optimales :
|
||||
|
||||
```bash
|
||||
# Vérifier BPF JIT
|
||||
sysctl net.core.bpf_jit_enable
|
||||
|
||||
# Si désactivé, activer temporairement
|
||||
sysctl -w net.core.bpf_jit_enable=1
|
||||
|
||||
# Activer de manière permanente
|
||||
echo "net.core.bpf_jit_enable=1" >> /etc/sysctl.d/99-secubox-opad.conf
|
||||
sysctl -p /etc/sysctl.d/99-secubox-opad.conf
|
||||
```
|
||||
|
||||
**Sortie attendue** : `net.core.bpf_jit_enable = 1`
|
||||
|
||||
### 1.4 Vérification des capacités kernel
|
||||
|
||||
```bash
|
||||
# Modules requis
|
||||
lsmod | grep -E 'nf_nat|nf_conntrack|sch_prio|cls_bpf|act_bpf'
|
||||
|
||||
# Charger si manquants
|
||||
modprobe nf_nat nf_conntrack sch_prio cls_bpf act_bpf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Installation et configuration
|
||||
|
||||
### 2.1 Déploiement initial
|
||||
|
||||
```bash
|
||||
# 1. Installer le paquet
|
||||
apt install secubox-wall
|
||||
|
||||
# 2. Vérifier le service
|
||||
systemctl status secubox-wall
|
||||
|
||||
# 3. Vérifier le mode OPAD
|
||||
curl -s http://localhost:9999/api/v1/wall/status | jq '.mode'
|
||||
# Sortie attendue: "opad"
|
||||
|
||||
# 4. Valider le profil par défaut
|
||||
secubox-params validate --module wall --profile active
|
||||
```
|
||||
|
||||
### 2.2 Configuration profil 3-broche
|
||||
|
||||
Le profil OPAD utilise le système **double-buffer** :
|
||||
|
||||
```
|
||||
/etc/secubox/wall/
|
||||
├── active/ ← Configuration live (lecture seule en production)
|
||||
├── shadow/ ← Édition, validation avant swap
|
||||
└── rollback/
|
||||
├── R1/ ← Snapshot le plus récent
|
||||
├── R2/
|
||||
├── R3/
|
||||
└── R4/ ← Snapshot le plus ancien
|
||||
```
|
||||
|
||||
#### Workflow standard
|
||||
|
||||
```bash
|
||||
# 1. Éditer la configuration dans shadow
|
||||
secubox-params edit --module wall --profile shadow
|
||||
|
||||
# 2. Valider le profil
|
||||
secubox-params validate --module wall --profile shadow
|
||||
|
||||
# 3. Swap shadow → active (crée snapshot automatique)
|
||||
secubox-params swap --module wall
|
||||
|
||||
# 4. Redémarrer le service
|
||||
systemctl restart secubox-wall
|
||||
```
|
||||
|
||||
### 2.3 Configuration des interfaces
|
||||
|
||||
OPAD nécessite les interfaces d'observation en **mode promiscuous** :
|
||||
|
||||
```bash
|
||||
# Configuration netplan (exemple /etc/netplan/00-secubox.yaml)
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
eth0:
|
||||
dhcp4: false
|
||||
promisc: true # Mode promiscuous pour observation
|
||||
addresses:
|
||||
- 192.168.1.1/24
|
||||
|
||||
eth1:
|
||||
dhcp4: false
|
||||
promisc: true
|
||||
addresses:
|
||||
- 192.168.2.1/24
|
||||
|
||||
# Appliquer
|
||||
netplan generate && netplan apply
|
||||
|
||||
# Vérifier
|
||||
ip link show eth0 | grep PROMISC
|
||||
```
|
||||
|
||||
### 2.4 Profil minimal (JSON)
|
||||
|
||||
Exemple de profil OPAD `active/opad-profile.json` :
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "opad",
|
||||
"observation": {
|
||||
"interfaces": ["eth0", "eth1"],
|
||||
"promiscuous": true,
|
||||
"bpf_filter": "not port 22 and not port 9999"
|
||||
},
|
||||
"primitives": {
|
||||
"dns-r": {
|
||||
"enabled": true,
|
||||
"ttl": 0,
|
||||
"spoof_ip": "127.0.0.1"
|
||||
},
|
||||
"dhcp-r": {
|
||||
"enabled": true,
|
||||
"nak_rate_limit": "10/s"
|
||||
},
|
||||
"rst-i": {
|
||||
"enabled": true,
|
||||
"seq_window": 4096
|
||||
},
|
||||
"arp-r": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"default_action": "observe",
|
||||
"rules_file": "active/policy-rules.json"
|
||||
},
|
||||
"metrics": {
|
||||
"prometheus_port": 9100,
|
||||
"log_level": "info"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Opérations courantes
|
||||
|
||||
### 3.1 Vérification du statut
|
||||
|
||||
```bash
|
||||
# Status systemd
|
||||
systemctl status secubox-wall
|
||||
|
||||
# Status API détaillé
|
||||
curl -s http://localhost:9999/api/v1/wall/status | jq
|
||||
```
|
||||
|
||||
**Exemple de réponse JSON** :
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "opad",
|
||||
"status": "active",
|
||||
"uptime_seconds": 86400,
|
||||
"observation": {
|
||||
"interfaces": ["eth0", "eth1"],
|
||||
"packets_observed": 1245678,
|
||||
"packets_matched": 3421
|
||||
},
|
||||
"primitives": {
|
||||
"dns-r": {
|
||||
"enabled": true,
|
||||
"injections_total": 142,
|
||||
"injections_lost": 2
|
||||
},
|
||||
"dhcp-r": {
|
||||
"enabled": true,
|
||||
"naks_sent": 89
|
||||
},
|
||||
"rst-i": {
|
||||
"enabled": true,
|
||||
"resets_sent": 56
|
||||
},
|
||||
"arp-r": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"rules_count": 8,
|
||||
"matches_total": 3421
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Métriques temps réel
|
||||
|
||||
```bash
|
||||
# Métriques Prometheus
|
||||
curl -s http://localhost:9100/metrics | grep secubox_opad
|
||||
|
||||
# Statistiques par primitive
|
||||
curl -s http://localhost:9999/api/v1/wall/metrics | jq '.primitives'
|
||||
```
|
||||
|
||||
### 3.3 Consultation des logs
|
||||
|
||||
```bash
|
||||
# Logs temps réel
|
||||
journalctl -u secubox-wall -f
|
||||
|
||||
# Filtrer par niveau
|
||||
journalctl -u secubox-wall -p warning -n 100
|
||||
|
||||
# Rechercher alertes DEPOT
|
||||
journalctl -u secubox-wall | grep "ALERTE_DEPOT"
|
||||
|
||||
# Exemple de log:
|
||||
# [OPAD] [DNS-R] ALERTE_DEPOT: 3 packets lost in 1s (threshold: 2)
|
||||
|
||||
# Rechercher injections perdues
|
||||
journalctl -u secubox-wall | grep "OPAD_INJECT_LOST"
|
||||
|
||||
# Logs JSON structurés
|
||||
journalctl -u secubox-wall -o json | jq '.MESSAGE'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Gestion des règles de politique
|
||||
|
||||
### 4.1 Lister les règles
|
||||
|
||||
```bash
|
||||
# Via API
|
||||
curl -s http://localhost:9999/api/v1/wall/policy/rules | jq
|
||||
|
||||
# Via fichier
|
||||
cat /etc/secubox/wall/active/policy-rules.json | jq
|
||||
```
|
||||
|
||||
### 4.2 Ajouter une règle
|
||||
|
||||
#### Exemple 1 : Bloquer un domaine (DNS-R)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9999/api/v1/wall/policy/rules \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": "block-malware-domain",
|
||||
"primitive": "dns-r",
|
||||
"action": "inject",
|
||||
"match": {
|
||||
"domain": "malware.example.com"
|
||||
},
|
||||
"params": {
|
||||
"ttl": 0,
|
||||
"spoof_ip": "127.0.0.1"
|
||||
},
|
||||
"enabled": true,
|
||||
"priority": 100
|
||||
}'
|
||||
```
|
||||
|
||||
#### Exemple 2 : Quarantaine d'un device (DHCP-R)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9999/api/v1/wall/policy/rules \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": "quarantine-infected-host",
|
||||
"primitive": "dhcp-r",
|
||||
"action": "nak",
|
||||
"match": {
|
||||
"mac": "aa:bb:cc:dd:ee:ff"
|
||||
},
|
||||
"params": {
|
||||
"reason": "Infected device - contact IT"
|
||||
},
|
||||
"enabled": true,
|
||||
"priority": 200
|
||||
}'
|
||||
```
|
||||
|
||||
#### Exemple 3 : RST vers IP externe (RST-I)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9999/api/v1/wall/policy/rules \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": "block-c2-server",
|
||||
"primitive": "rst-i",
|
||||
"action": "inject",
|
||||
"match": {
|
||||
"dst_ip": "198.51.100.123",
|
||||
"protocol": "tcp"
|
||||
},
|
||||
"params": {
|
||||
"seq_window": 4096
|
||||
},
|
||||
"enabled": true,
|
||||
"priority": 300
|
||||
}'
|
||||
```
|
||||
|
||||
### 4.3 Modifier une règle
|
||||
|
||||
```bash
|
||||
# 1. Récupérer l'ID de la règle
|
||||
curl -s http://localhost:9999/api/v1/wall/policy/rules | jq '.[] | select(.id=="block-malware-domain")'
|
||||
|
||||
# 2. Modifier (PATCH ou PUT)
|
||||
curl -X PATCH http://localhost:9999/api/v1/wall/policy/rules/block-malware-domain \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"enabled": false
|
||||
}'
|
||||
```
|
||||
|
||||
### 4.4 Supprimer une règle
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:9999/api/v1/wall/policy/rules/block-malware-domain
|
||||
```
|
||||
|
||||
### 4.5 Recharger les règles
|
||||
|
||||
```bash
|
||||
# Après modification manuelle du fichier policy-rules.json
|
||||
curl -X POST http://localhost:9999/api/v1/wall/policy/reload
|
||||
|
||||
# Ou via systemd
|
||||
systemctl reload secubox-wall
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
### 5.1 DNS-R : Problèmes d'injection
|
||||
|
||||
| Symptôme | Cause probable | Solution |
|
||||
|----------|----------------|----------|
|
||||
| Clients reçoivent réponse légitime | Injection trop lente (latence DÉPÔT) | Vérifier `ALERTE_DEPOT` dans logs, réduire charge CPU |
|
||||
| `OPAD_INJECT_LOST` dans logs | Race condition perdue | Activer escalade in-path (INV-08) si critique |
|
||||
| TTL non respecté | Bug client cache | Forcer TTL=0 dans règle primitive |
|
||||
| Injection fonctionne sporadiquement | Interface non promiscuous | `ip link show eth0 \| grep PROMISC` |
|
||||
|
||||
**Commandes de diagnostic** :
|
||||
|
||||
```bash
|
||||
# Capturer trafic DNS
|
||||
tcpdump -i eth0 -nn port 53 -c 100
|
||||
|
||||
# Vérifier latence injection
|
||||
curl -s http://localhost:9100/metrics | grep secubox_opad_dns_latency
|
||||
|
||||
# Tester règle manuellement
|
||||
dig @8.8.8.8 malware.example.com # Devrait recevoir 127.0.0.1
|
||||
```
|
||||
|
||||
### 5.2 DHCP-R : NAK non effectifs
|
||||
|
||||
| Symptôme | Cause probable | Solution |
|
||||
|----------|----------------|----------|
|
||||
| Client obtient IP malgré NAK | Serveur DHCP légitime plus rapide | Escalade in-path pour drop hard |
|
||||
| NAK envoyés mais ignorés | Client stateful DHCPv6 | Ajouter règle pour DHCPv6 (port 547) |
|
||||
| Rate limit atteint | Trop de NAK/s | Augmenter `nak_rate_limit` dans profil |
|
||||
|
||||
**Commandes de diagnostic** :
|
||||
|
||||
```bash
|
||||
# Capturer DHCP
|
||||
tcpdump -i eth0 -nn port 67 or port 68 -v
|
||||
|
||||
# Vérifier NAK count
|
||||
curl -s http://localhost:9999/api/v1/wall/metrics | jq '.primitives["dhcp-r"].naks_sent'
|
||||
|
||||
# Tester règle
|
||||
# (sur client) dhclient -r eth0 && dhclient eth0
|
||||
```
|
||||
|
||||
### 5.3 RST-I : Connexion non coupée
|
||||
|
||||
| Symptôme | Cause probable | Solution |
|
||||
|----------|----------------|----------|
|
||||
| Connexion TCP établie malgré RST | SEQ hors fenêtre | Augmenter `seq_window` à 65535 (max) |
|
||||
| RST envoyés mais ignorés | Client ignore RST (rare) | Escalade in-path pour drop dans nftables |
|
||||
| RST sur mauvais sens | Bug direction detection | Vérifier logs OPAD, reporter bug |
|
||||
|
||||
**Commandes de diagnostic** :
|
||||
|
||||
```bash
|
||||
# Capturer RST
|
||||
tcpdump -i eth0 'tcp[tcpflags] & tcp-rst != 0' -nn -c 20
|
||||
|
||||
# Tester règle
|
||||
curl -v http://198.51.100.123 # Devrait timeout/reset
|
||||
```
|
||||
|
||||
### 5.4 ARP-R : Empoisonnement détecté
|
||||
|
||||
| Symptôme | Cause probable | Solution |
|
||||
|----------|----------------|----------|
|
||||
| Fausses réponses ARP persistent | Race condition ARP | Activer in-path + static ARP sur clients critiques |
|
||||
| Alerts ARP floods | Attaque MITM active | Activer primitive ARP-R + bloquer MAC source |
|
||||
| Réponses ARP non injectées | Interface non promiscuous | Vérifier `ip link show \| grep PROMISC` |
|
||||
|
||||
**Commandes de diagnostic** :
|
||||
|
||||
```bash
|
||||
# Capturer ARP
|
||||
tcpdump -i eth0 arp -nn -e
|
||||
|
||||
# Vérifier table ARP
|
||||
ip neigh show
|
||||
|
||||
# Scanner ARP spoofing (arpwatch)
|
||||
apt install arpwatch
|
||||
systemctl start arpwatch
|
||||
journalctl -u arpwatch -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Escalade vers in-path
|
||||
|
||||
### 6.1 Quand escalader
|
||||
|
||||
Escalader vers **in-path** (mode invasif INV-08) dans ces cas :
|
||||
|
||||
1. **Client ARP statique** : Injection ARP-R inefficace
|
||||
2. **Besoin drop hard** : RST-I ou DNS-R perdent race condition
|
||||
3. **Deep packet inspection** : Besoin modifier payload (WAF)
|
||||
4. **Conformité réglementaire** : Audit exige interception certaine
|
||||
|
||||
**ATTENTION** : Escalade = point unique de défaillance (SPOF). Tester en maintenance window.
|
||||
|
||||
### 6.2 Procédure INV-08
|
||||
|
||||
```bash
|
||||
# 1. Vérifier état actuel
|
||||
curl -s http://localhost:9999/api/v1/wall/status | jq '.mode'
|
||||
# Sortie: "opad"
|
||||
|
||||
# 2. Planifier escalade (dry-run)
|
||||
curl -X POST http://localhost:9999/api/v1/wall/escalate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target_mode": "in-path",
|
||||
"dry_run": true
|
||||
}' | jq
|
||||
|
||||
# 3. Activer escalade
|
||||
secubox-wall escalate --mode in-path --confirm
|
||||
|
||||
# 4. Vérifier
|
||||
curl -s http://localhost:9999/api/v1/wall/status | jq '.mode'
|
||||
# Sortie: "in-path"
|
||||
|
||||
# 5. Surveiller
|
||||
journalctl -u secubox-wall -f
|
||||
```
|
||||
|
||||
**Changements effectués** :
|
||||
|
||||
- nftables : ajout hook `PREROUTING` + `FORWARD`
|
||||
- tc : qdisc `prio` remplacé par `htb` avec redirect
|
||||
- Interfaces : mode bridge activé (`brctl addif secubox-br0 eth0`)
|
||||
|
||||
### 6.3 Révocation (retour OPAD)
|
||||
|
||||
```bash
|
||||
# 1. Révoquer escalade
|
||||
curl -X DELETE http://localhost:9999/api/v1/wall/escalate
|
||||
|
||||
# 2. Vérifier
|
||||
curl -s http://localhost:9999/api/v1/wall/status | jq '.mode'
|
||||
# Sortie: "opad"
|
||||
|
||||
# 3. Redémarrer service (recommandé)
|
||||
systemctl restart secubox-wall
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Rollback et récupération 4R
|
||||
|
||||
### 7.1 Actions 4R
|
||||
|
||||
| Action | Mnémotechnique | Description | Commande |
|
||||
|--------|----------------|-------------|----------|
|
||||
| **Run** | Exécuter | Swap shadow → active | `secubox-params swap --module wall` |
|
||||
| **Rollback** | Retour arrière | Restaurer snapshot Rn | `secubox-params rollback --module wall --target R1` |
|
||||
| **Revert** | Annuler | Swap active → shadow | `secubox-params revert --module wall` |
|
||||
| **Rebuild** | Reconstruction | Régénérer depuis source | `secubox-params rebuild --module wall --from-template` |
|
||||
|
||||
### 7.2 Structure snapshots
|
||||
|
||||
```
|
||||
/etc/secubox/wall/rollback/
|
||||
├── R1/
|
||||
│ ├── opad-profile.json
|
||||
│ ├── policy-rules.json
|
||||
│ └── .timestamp # 2026-05-12T14:30:00Z
|
||||
├── R2/
|
||||
│ └── .timestamp # 2026-05-11T09:15:00Z
|
||||
├── R3/
|
||||
│ └── .timestamp # 2026-05-10T16:45:00Z
|
||||
└── R4/
|
||||
└── .timestamp # 2026-05-09T11:00:00Z
|
||||
```
|
||||
|
||||
**Règle de rotation** : Chaque `swap` crée un nouveau snapshot R1, les autres se décalent (R1→R2, R2→R3, R3→R4, R4 supprimé).
|
||||
|
||||
### 7.3 Restauration d'urgence
|
||||
|
||||
#### Scenario 1 : Configuration cassée après swap
|
||||
|
||||
```bash
|
||||
# 1. Identifier le problème
|
||||
systemctl status secubox-wall
|
||||
# Sortie: failed
|
||||
|
||||
# 2. Consulter logs
|
||||
journalctl -u secubox-wall -n 50 --no-pager
|
||||
|
||||
# 3. Rollback immédiat vers R1
|
||||
secubox-params rollback --module wall --target R1
|
||||
|
||||
# 4. Redémarrer
|
||||
systemctl restart secubox-wall
|
||||
|
||||
# 5. Vérifier
|
||||
systemctl status secubox-wall
|
||||
```
|
||||
|
||||
#### Scenario 2 : Tous les snapshots corrompus
|
||||
|
||||
```bash
|
||||
# 1. Rebuild depuis template factory
|
||||
secubox-params rebuild --module wall --from-template
|
||||
|
||||
# 2. Vérifier template source
|
||||
ls -la /usr/share/secubox/wall/templates/
|
||||
|
||||
# 3. Restaurer
|
||||
cp -r /usr/share/secubox/wall/templates/* /etc/secubox/wall/active/
|
||||
|
||||
# 4. Redémarrer
|
||||
systemctl restart secubox-wall
|
||||
```
|
||||
|
||||
### 7.4 Audit trail
|
||||
|
||||
Chaque action 4R est loggée dans `/var/log/secubox/audit.log` :
|
||||
|
||||
```bash
|
||||
# Consulter audit trail
|
||||
tail -f /var/log/secubox/audit.log
|
||||
|
||||
# Exemple d'entrée:
|
||||
# 2026-05-12T14:30:15Z [WALL] ACTION=swap USER=root FROM=shadow TO=active SNAPSHOT=R1
|
||||
# 2026-05-12T14:35:42Z [WALL] ACTION=rollback USER=root FROM=active TO=R1 REASON="config_invalid"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Monitoring et alertes
|
||||
|
||||
### 8.1 Métriques Prometheus
|
||||
|
||||
Endpoint : `http://localhost:9100/metrics`
|
||||
|
||||
#### Métriques principales
|
||||
|
||||
```prometheus
|
||||
# Compteurs injections
|
||||
secubox_opad_injection_total{primitive="dns-r"} 142
|
||||
secubox_opad_injection_total{primitive="dhcp-r"} 89
|
||||
secubox_opad_injection_total{primitive="rst-i"} 56
|
||||
|
||||
# Injections perdues (race condition)
|
||||
secubox_opad_injection_lost_total{primitive="dns-r"} 2
|
||||
|
||||
# Latence injection (ms)
|
||||
secubox_opad_injection_latency_ms{primitive="dns-r",quantile="0.5"} 0.8
|
||||
secubox_opad_injection_latency_ms{primitive="dns-r",quantile="0.99"} 3.2
|
||||
|
||||
# Packets observés
|
||||
secubox_opad_packets_observed_total 1245678
|
||||
|
||||
# Règles de politique matchées
|
||||
secubox_opad_policy_matches_total{rule_id="block-malware-domain"} 89
|
||||
secubox_opad_policy_matches_total{rule_id="quarantine-infected-host"} 12
|
||||
```
|
||||
|
||||
### 8.2 Alertes Prometheus recommandées
|
||||
|
||||
```yaml
|
||||
# /etc/prometheus/alerts/secubox-opad.yml
|
||||
groups:
|
||||
- name: secubox_opad
|
||||
interval: 30s
|
||||
rules:
|
||||
# Alerte 1 : Service dégradé
|
||||
- alert: OPADServiceDegraded
|
||||
expr: up{job="secubox-wall"} == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "OPAD service down on {{ $labels.instance }}"
|
||||
description: "secubox-wall systemd unit not responding"
|
||||
|
||||
# Alerte 2 : Injections perdues (burst)
|
||||
- alert: OPADInjectLostBurst
|
||||
expr: rate(secubox_opad_injection_lost_total[1m]) > 0.1
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "OPAD losing injections on {{ $labels.instance }}"
|
||||
description: "Primitive {{ $labels.primitive }} losing race conditions ({{ $value }}/s)"
|
||||
|
||||
# Alerte 3 : Latence excessive
|
||||
- alert: OPADLatencyHigh
|
||||
expr: secubox_opad_injection_latency_ms{quantile="0.99"} > 5
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "OPAD high latency on {{ $labels.instance }}"
|
||||
description: "P99 latency {{ $value }}ms (threshold: 5ms)"
|
||||
|
||||
# Alerte 4 : Observation arrêtée
|
||||
- alert: OPADObservationDown
|
||||
expr: rate(secubox_opad_packets_observed_total[5m]) == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "OPAD not observing traffic on {{ $labels.instance }}"
|
||||
description: "No packets observed in last 5 minutes - check interfaces"
|
||||
|
||||
# Alerte 5 : Escalade in-path active
|
||||
- alert: OPADEscalationActive
|
||||
expr: secubox_opad_mode{mode="in-path"} == 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: info
|
||||
annotations:
|
||||
summary: "OPAD escalated to in-path mode on {{ $labels.instance }}"
|
||||
description: "System running in invasive mode - verify if intentional"
|
||||
```
|
||||
|
||||
### 8.3 Grafana dashboard
|
||||
|
||||
Importer le dashboard pré-configuré :
|
||||
|
||||
```bash
|
||||
# Télécharger dashboard JSON
|
||||
curl -o /tmp/opad-dashboard.json \
|
||||
https://raw.githubusercontent.com/CyberMind-FR/secubox-deb/master/monitoring/grafana/opad-dashboard.json
|
||||
|
||||
# Importer dans Grafana
|
||||
grafana-cli admin import-dashboard /tmp/opad-dashboard.json
|
||||
```
|
||||
|
||||
**Panels inclus** :
|
||||
|
||||
- Injections totales (time series)
|
||||
- Injections perdues (gauge)
|
||||
- Latence P50/P95/P99 (heatmap)
|
||||
- Policy matches par règle (bar chart)
|
||||
- Escalations in-path (stat)
|
||||
|
||||
---
|
||||
|
||||
## 9. Maintenance
|
||||
|
||||
### 9.1 Mise à jour
|
||||
|
||||
```bash
|
||||
# 1. Vérifier version actuelle
|
||||
dpkg -l | grep secubox-wall
|
||||
|
||||
# 2. Mettre à jour
|
||||
apt update
|
||||
apt install --only-upgrade secubox-wall
|
||||
|
||||
# 3. Vérifier changelog
|
||||
zcat /usr/share/doc/secubox-wall/changelog.Debian.gz | head -n 20
|
||||
|
||||
# 4. Redémarrer
|
||||
systemctl restart secubox-wall
|
||||
|
||||
# 5. Valider
|
||||
curl -s http://localhost:9999/api/v1/wall/status | jq '.version'
|
||||
```
|
||||
|
||||
### 9.2 Sauvegarde configuration
|
||||
|
||||
```bash
|
||||
# Backup manuel
|
||||
tar -czf /backup/secubox-wall-$(date +%Y%m%d).tar.gz \
|
||||
/etc/secubox/wall/active/ \
|
||||
/etc/secubox/wall/rollback/
|
||||
|
||||
# Restauration
|
||||
tar -xzf /backup/secubox-wall-20260512.tar.gz -C /
|
||||
systemctl restart secubox-wall
|
||||
```
|
||||
|
||||
**Automatisation cron** :
|
||||
|
||||
```bash
|
||||
# /etc/cron.daily/secubox-wall-backup
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
BACKUP_DIR="/backup/secubox-wall"
|
||||
DATE=$(date +%Y%m%d)
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
tar -czf "$BACKUP_DIR/wall-$DATE.tar.gz" /etc/secubox/wall/active/ /etc/secubox/wall/rollback/
|
||||
find "$BACKUP_DIR" -name "wall-*.tar.gz" -mtime +30 -delete
|
||||
```
|
||||
|
||||
### 9.3 Rotation logs
|
||||
|
||||
Configuration logrotate `/etc/logrotate.d/secubox-wall` :
|
||||
|
||||
```
|
||||
/var/log/secubox/wall/*.log {
|
||||
daily
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0640 secubox-wall secubox-wall
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload secubox-wall > /dev/null 2>&1 || true
|
||||
endscript
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 Nettoyage cache BPF
|
||||
|
||||
```bash
|
||||
# Lister objets BPF
|
||||
bpftool prog list
|
||||
|
||||
# Lister maps BPF
|
||||
bpftool map list
|
||||
|
||||
# Nettoyer objets orphelins (après crash)
|
||||
bpftool prog show | awk '/secubox_opad/ {print $1}' | xargs -n1 bpftool prog detach
|
||||
|
||||
# Supprimer maps orphelines
|
||||
bpftool map show | awk '/secubox_opad/ {print $1}' | xargs -n1 bpftool map delete
|
||||
```
|
||||
|
||||
### 9.5 Vérification intégrité
|
||||
|
||||
```bash
|
||||
# Checksum binaires
|
||||
debsums secubox-wall
|
||||
|
||||
# Permissions
|
||||
dpkg-statoverride --list | grep secubox-wall
|
||||
|
||||
# Capabilities binaires
|
||||
getcap /usr/bin/secubox-wall
|
||||
# Sortie attendue: cap_net_admin,cap_net_raw=ep
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Contacts et support
|
||||
|
||||
### 10.1 Documentation
|
||||
|
||||
- **Architecture OPAD** : `/usr/share/doc/secubox-wall/OPAD-ARCHITECTURE.md`
|
||||
- **Threat Model** : `/usr/share/doc/secubox-wall/OPAD-THREAT-MODEL.md`
|
||||
- **Compliance CSPN** : `/usr/share/doc/secubox-wall/OPAD-CSPN.md`
|
||||
- **Site web** : https://secubox.in
|
||||
|
||||
### 10.2 Support technique
|
||||
|
||||
- **Issues GitHub** : https://github.com/CyberMind-FR/secubox-deb/issues
|
||||
- **Email** : support@cybermind.fr
|
||||
- **Matrice** : #secubox:matrix.org
|
||||
|
||||
### 10.3 Reporting bugs
|
||||
|
||||
Template issue GitHub :
|
||||
|
||||
```markdown
|
||||
### Environnement
|
||||
- SecuBox version: [dpkg -l | grep secubox-wall]
|
||||
- Kernel: [uname -r]
|
||||
- Board: [MOCHAbin / ESPRESSObin / VM]
|
||||
|
||||
### Symptôme
|
||||
[Description du problème]
|
||||
|
||||
### Reproduction
|
||||
1. [Étape 1]
|
||||
2. [Étape 2]
|
||||
3. [Étape 3]
|
||||
|
||||
### Logs
|
||||
```
|
||||
[journalctl -u secubox-wall -n 100 --no-pager]
|
||||
```
|
||||
|
||||
### Attendu vs Observé
|
||||
- **Attendu** : [comportement attendu]
|
||||
- **Observé** : [comportement observé]
|
||||
```
|
||||
|
||||
### 10.4 Contribution
|
||||
|
||||
Le projet SecuBox-Deb est **open source** (licence à définir) :
|
||||
|
||||
```bash
|
||||
# Cloner le repo
|
||||
git clone https://github.com/CyberMind-FR/secubox-deb.git
|
||||
|
||||
# Créer une branche feature
|
||||
git checkout -b feature/opad-improvement
|
||||
|
||||
# Soumettre PR
|
||||
gh pr create --title "feat(wall): improve DNS-R latency" --body "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Annexes
|
||||
|
||||
### A. Glossaire
|
||||
|
||||
| Terme | Définition |
|
||||
|-------|------------|
|
||||
| **OPAD** | Out-of-Path Active Defense |
|
||||
| **Primitive** | Mécanisme d'injection (DNS-R, DHCP-R, RST-I, ARP-R) |
|
||||
| **DÉPÔT** | Delay between observation and injection |
|
||||
| **INV-08** | Escalation to in-path mode (invasive) |
|
||||
| **4R** | Run, Rollback, Revert, Rebuild |
|
||||
| **SPOF** | Single Point Of Failure |
|
||||
|
||||
### B. Codes d'erreur API
|
||||
|
||||
| Code | Message | Cause |
|
||||
|------|---------|-------|
|
||||
| 200 | OK | Succès |
|
||||
| 400 | Invalid request | JSON malformé ou champs manquants |
|
||||
| 404 | Rule not found | ID règle inexistant |
|
||||
| 409 | Conflict | Règle duplicate ou escalation déjà active |
|
||||
| 500 | Internal error | Bug ou crash backend |
|
||||
| 503 | Service unavailable | Service en cours de redémarrage |
|
||||
|
||||
### C. Ports réseau
|
||||
|
||||
| Port | Protocole | Service | Notes |
|
||||
|------|-----------|---------|-------|
|
||||
| 9999 | HTTP | API REST OPAD | Localhost uniquement |
|
||||
| 9100 | HTTP | Prometheus metrics | Localhost uniquement |
|
||||
| 53 | UDP/TCP | Observation DNS | Promiscuous mode |
|
||||
| 67-68 | UDP | Observation DHCP | Promiscuous mode |
|
||||
|
||||
---
|
||||
|
||||
**FIN DU DOCUMENT**
|
||||
|
||||
---
|
||||
|
||||
**Changelog**
|
||||
|
||||
| Version | Date | Auteur | Changements |
|
||||
|---------|------|--------|-------------|
|
||||
| 2.4.0 | 2026-05-12 | G. Kerma | Version initiale OPAD operations guide |
|
||||
|
||||
---
|
||||
|
||||
**Licence** : [À définir] — CyberMind · https://cybermind.fr
|
||||
671
doctrine/opad/OPAD.md
Normal file
671
doctrine/opad/OPAD.md
Normal file
|
|
@ -0,0 +1,671 @@
|
|||
# OPAD — Off-Path Active Defense
|
||||
|
||||
**Doctrine WALL SecuBox v2.4.0**
|
||||
|
||||
---
|
||||
|
||||
## Métadonnées
|
||||
|
||||
| Champ | Valeur |
|
||||
|-------|--------|
|
||||
| **Référence** | CM-WALL-OPAD-2026-05 |
|
||||
| **Version** | 2.4.0 |
|
||||
| **Status** | Canonique |
|
||||
| **Date** | 2026-05-12 |
|
||||
| **Auteur** | Gérald Kerma (CyberMind) |
|
||||
| **Portée** | Module WALL (SecuBox-Deb) |
|
||||
| **Révision précédente** | D-2025-IDGP-INLINE (déprécié) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Identité
|
||||
|
||||
### 1.1 Définition
|
||||
|
||||
**OPAD** (Off-Path Active Defense) est la doctrine architecturale du module **WALL** de SecuBox-Deb v2.4.0+. Elle définit un mode de protection réseau où la SecuBox observe le trafic en **position off-path** (hors du chemin de données) et injecte des réponses de disruption ciblée lorsque nécessaire, **sans jamais être un point de passage obligatoire**.
|
||||
|
||||
### 1.2 Doctrine en une ligne
|
||||
|
||||
> **"La SecuBox n'est pas dans le chemin. Elle est à côté du chemin, et elle gagne des courses. Quand elle est là, elle protège par disruption ciblée. Quand elle n'est pas là, le réseau ne le remarque pas."**
|
||||
|
||||
### 1.3 Périmètre
|
||||
|
||||
- **Module concerné** : WALL (protection réseau active)
|
||||
- **Composants** : DNS-R, DHCP-R, RST-I, ARP-R
|
||||
- **Cible** : Certification ANSSI CSPN (critère "fail-silent")
|
||||
- **Contrainte** : Zéro rupture possible du flux utilisateur
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexte et motivation
|
||||
|
||||
### 2.1 Problème avec la doctrine in-path (IDGP)
|
||||
|
||||
La doctrine précédente **D-2025-IDGP-INLINE** (In-line Data Guardian Protocol) plaçait la SecuBox en **bridge transparent** dans le chemin de données :
|
||||
|
||||
**Limites identifiées :**
|
||||
|
||||
| Problème | Impact |
|
||||
|----------|--------|
|
||||
| **Single Point of Failure** | Panne matérielle = coupure réseau totale |
|
||||
| **Latency ajoutée** | Analyse en ligne → délai minimum 2-5ms par paquet |
|
||||
| **Surface d'attaque** | SecuBox devient cible prioritaire (DoS, exploitation kernel) |
|
||||
| **Complexité opérationnelle** | Maintenance = fenêtre de downtime obligatoire |
|
||||
| **Scalabilité** | Goulot d'étranglement à 1 Gbps+ |
|
||||
|
||||
### 2.2 Solution OPAD : observation off-path + injection active
|
||||
|
||||
**Principe :** La SecuBox écoute le trafic en **mode observation** (port mirror, TAP, span) et injecte des réponses **plus rapides** que les réponses légitimes lorsqu'une menace est détectée.
|
||||
|
||||
**Topologie :**
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ Internet │
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌──────▼───────┐
|
||||
│ Routeur │
|
||||
│ (Opérateur)│
|
||||
└──────┬───────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
┏━━━━━▼━━━━━┓ ┌─────▼─────┐ ┌───▼────┐
|
||||
┃ SecuBox ┃ │ Switch │ │ Clients│
|
||||
┃ (OPAD) ┃◄──┤ (SPAN) │ │ LAN │
|
||||
┃ OFF-PATH ┃ └───────────┘ └────────┘
|
||||
┗━━━━━┬━━━━━┛
|
||||
│
|
||||
└─► Injection (DNS-R, DHCP-R, RST-I, ARP-R)
|
||||
|
||||
Legend:
|
||||
━━━ : Off-path observation + injection
|
||||
─── : Data path (aucun forwarding SecuBox)
|
||||
```
|
||||
|
||||
**Avantages :**
|
||||
|
||||
- ✅ **Retrait transparent** : débrancher la SecuBox = aucun impact
|
||||
- ✅ **Zéro latency** : pas dans le chemin de données
|
||||
- ✅ **Surface d'attaque minimale** : pas de forwarding = pas de DoS possible
|
||||
- ✅ **Scalabilité** : observation passive peut suivre 10 Gbps+
|
||||
- ✅ **Maintenance sans downtime** : mises à jour sans coupure réseau
|
||||
|
||||
---
|
||||
|
||||
## 3. Principes fondamentaux
|
||||
|
||||
### PROP-P1 : Observer plus, agir moins
|
||||
|
||||
**Énoncé :**
|
||||
_La SecuBox maximise l'observation passive (logs, stats, détection) et minimise l'injection active (disruption). L'injection est réservée aux menaces confirmées de haute criticité._
|
||||
|
||||
**Implication :**
|
||||
- Logs exhaustifs (DÉPÔT) avant injection
|
||||
- Seuils configurables par primitif (target_success_rate)
|
||||
- Mode **dry-run** disponible pour audit
|
||||
|
||||
---
|
||||
|
||||
### PROP-P2 : Zéro rupture possible
|
||||
|
||||
**Énoncé :**
|
||||
_Aucune configuration OPAD ne peut provoquer une coupure réseau totale. Le retrait physique de la SecuBox doit être sans effet sur la connectivité des clients._
|
||||
|
||||
**Implication :**
|
||||
- Pas de forwarding IPv4/IPv6 (INV-02)
|
||||
- Pas de rôle de gateway
|
||||
- Pas de modification de la table ARP statique des clients
|
||||
|
||||
---
|
||||
|
||||
### PROP-P3 : Surface d'attaque minimale
|
||||
|
||||
**Énoncé :**
|
||||
_La SecuBox en mode OPAD n'expose aucun service directement attaquable depuis le LAN ou le WAN. Elle est invisible au scan réseau._
|
||||
|
||||
**Implication :**
|
||||
- Aucune réponse ICMP echo (INV-05)
|
||||
- Aucune écoute TCP/UDP sur IP LAN (sauf management SSH sur VLAN admin)
|
||||
- Aucune surface WAN (INV-06)
|
||||
|
||||
---
|
||||
|
||||
### PROP-P4 : Escalade explicite et révocable
|
||||
|
||||
**Énoncé :**
|
||||
_Si OPAD est insuffisant (ex: TLS C2), la SecuBox peut passer en mode escaladé (DHCP force-gateway) pour activer l'interception. Ce mode est explicite, journalisé, et révocable sans redémarrage._
|
||||
|
||||
**Implication :**
|
||||
- Mode **opad-with-escalation** disponible
|
||||
- Rollback 4R obligatoire avant escalade
|
||||
- Event OPAD_ESCALATE → journal audit (CSPN)
|
||||
|
||||
---
|
||||
|
||||
## 4. Invariants OPAD
|
||||
|
||||
### Table des invariants
|
||||
|
||||
| ID | Invariant | Description | Conséquence |
|
||||
|----|-----------|-------------|-------------|
|
||||
| **INV-01** | **Retrait sans rupture** | Débrancher physiquement la SecuBox ne provoque aucune coupure réseau | Architecture off-path obligatoire |
|
||||
| **INV-02** | **Aucun forwarding** | La SecuBox ne forward jamais le trafic utilisateur (pas de rôle bridge/router) | Pas de `/proc/sys/net/ipv4/ip_forward=1` |
|
||||
| **INV-03** | **Journalisation systématique** | Toute injection active génère un event **ALERTE·DÉPÔT** avant l'injection | Traçabilité CSPN complète |
|
||||
| **INV-04** | **Marquage des échecs** | Les injections perdues (race échouée) sont loguées avec code **OPAD_INJECT_LOST** | Métrique de taux de succès |
|
||||
| **INV-05** | **Silence LAN** | La SecuBox ne répond jamais à ICMP echo, ARP who-has (sauf injection ARP-R), scan TCP | Invisibilité réseau |
|
||||
| **INV-06** | **Surface WAN nulle** | Aucun port ouvert sur IP WAN (même pas SSH) | Attaque WAN impossible |
|
||||
| **INV-07** | **Fail-silent** | En cas de crash du daemon WALL, le réseau continue sans protection (pas de fail-closed) | Disponibilité > sécurité |
|
||||
| **INV-08** | **Escalade révocable** | Tout mode escaladé (in-path) doit pouvoir revenir en OPAD sans redémarrage | Commande `opad revert` disponible |
|
||||
|
||||
---
|
||||
|
||||
## 5. Primitifs d'injection
|
||||
|
||||
### 5.1 DNS-R (DNS Race)
|
||||
|
||||
#### 5.1.1 Mécanisme
|
||||
|
||||
La SecuBox écoute les requêtes DNS (port 53 UDP) en mode promiscuous. Lorsqu'une requête correspond à une règle de blocage (malware domain, C2, phishing), elle injecte une **réponse DNS falsifiée** avec un TTL court, avant que le resolver légitime ne réponde.
|
||||
|
||||
**Condition de succès :** Réponse OPAD arrive avant la réponse du resolver légitime (typiquement < 10ms).
|
||||
|
||||
#### 5.1.2 Paramètres
|
||||
|
||||
| Paramètre | Type | Valeur par défaut | Description |
|
||||
|-----------|------|-------------------|-------------|
|
||||
| `enabled` | bool | `true` | Activer DNS-R |
|
||||
| `target_success_rate` | float | `0.99` | Taux de race gagnée visé (99%) |
|
||||
| `modes` | list | `["nxdomain", "sinkhole"]` | Modes de réponse |
|
||||
| `sinkhole_ip` | IPv4 | `10.254.254.254` | IP de sinkhole (si mode sinkhole) |
|
||||
| `ttl` | int | `60` | TTL de la réponse injectée (secondes) |
|
||||
| `blocklists` | list | `["crowdsec", "abuse.ch"]` | Sources de domaines malveillants |
|
||||
| `dry_run` | bool | `false` | Log uniquement, pas d'injection |
|
||||
|
||||
#### 5.1.3 Modes de réponse
|
||||
|
||||
- **nxdomain** : RCODE=3 (domain does not exist)
|
||||
- **sinkhole** : IP de sinkhole (captive portal ou honeypot)
|
||||
- **redirect_captive** : Redirection vers page d'avertissement SecuBox
|
||||
|
||||
#### 5.1.4 Journalisation
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "OPAD_DNS_RACE",
|
||||
"timestamp": "2026-05-12T14:32:01.234Z",
|
||||
"src_ip": "192.168.1.42",
|
||||
"query": "evil-c2.example.com",
|
||||
"qtype": "A",
|
||||
"action": "sinkhole",
|
||||
"sinkhole_ip": "10.254.254.254",
|
||||
"result": "success",
|
||||
"race_time_ms": 4.2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 DHCP-R (DHCP Race)
|
||||
|
||||
#### 5.2.1 Mécanisme
|
||||
|
||||
La SecuBox écoute les DHCPDISCOVER (broadcast) et injecte un **DHCPOFFER falsifié** avant le serveur DHCP légitime. L'offre OPAD peut :
|
||||
- **Quarantaine** : proposer une IP isolée (VLAN quarantaine)
|
||||
- **Redirect gateway** : forcer la SecuBox comme gateway (mode escaladé)
|
||||
- **Deny** : offre avec bail expiré immédiatement (DoS ciblé)
|
||||
|
||||
#### 5.2.2 Paramètres
|
||||
|
||||
| Paramètre | Type | Valeur par défaut | Description |
|
||||
|-----------|------|-------------------|-------------|
|
||||
| `enabled` | bool | `false` | Activer DHCP-R (désactivé par défaut) |
|
||||
| `target_success_rate` | float | `0.95` | Taux de race gagnée visé |
|
||||
| `quarantine_pool` | CIDR | `192.168.99.0/24` | Pool d'IP quarantaine |
|
||||
| `lease_time_s` | int | `300` | Durée du bail forcé (5 min) |
|
||||
| `escalate_to_gateway` | bool | `false` | Forcer SecuBox comme gateway (escalade) |
|
||||
|
||||
#### 5.2.3 Journalisation
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "OPAD_DHCP_RACE",
|
||||
"timestamp": "2026-05-12T14:35:22.123Z",
|
||||
"src_mac": "aa:bb:cc:dd:ee:ff",
|
||||
"hostname": "suspect-device",
|
||||
"action": "quarantine",
|
||||
"offered_ip": "192.168.99.42",
|
||||
"result": "success",
|
||||
"race_time_ms": 8.1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 RST-I (TCP RST Injection)
|
||||
|
||||
#### 5.3.1 Mécanisme
|
||||
|
||||
La SecuBox analyse les flux TCP établis (via observation de SYN/SYN-ACK) et injecte des **segments RST** avec SEQ/ACK corrects pour terminer immédiatement une connexion identifiée comme malveillante (C2, exfiltration, malware callback).
|
||||
|
||||
**Timing critique :** RST doit arriver avant le prochain segment légitime (fenêtre typique : 50-200ms).
|
||||
|
||||
#### 5.3.2 Paramètres
|
||||
|
||||
| Paramètre | Type | Valeur par défaut | Description |
|
||||
|-----------|------|-------------------|-------------|
|
||||
| `enabled` | bool | `true` | Activer RST-I |
|
||||
| `target_success_rate` | float | `0.90` | Taux de disruption réussie |
|
||||
| `double_ended` | bool | `true` | Envoyer RST aux deux endpoints (client+serveur) |
|
||||
| `timing_window_ms` | int | `100` | Fenêtre d'injection (ms) |
|
||||
| `trigger_sources` | list | `["crowdsec", "suricata"]` | Sources de détection malveillante |
|
||||
|
||||
#### 5.3.3 Journalisation
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "OPAD_RST_INJECT",
|
||||
"timestamp": "2026-05-12T14:40:11.456Z",
|
||||
"src_ip": "192.168.1.42",
|
||||
"dst_ip": "1.2.3.4",
|
||||
"dst_port": 443,
|
||||
"reason": "crowdsec_c2_detected",
|
||||
"double_ended": true,
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 ARP-R (ARP Redirect)
|
||||
|
||||
#### 5.4.1 Mécanisme
|
||||
|
||||
La SecuBox injecte des **réponses ARP falsifiées** (gratuitous ARP ou réponse à ARP who-has) pour rediriger le trafic d'un client suspect vers un captive portal ou un honeypot, sans modifier la configuration du client.
|
||||
|
||||
**Usage typique :** Quarantaine soft d'un device compromis détecté par NAC/AUTH.
|
||||
|
||||
#### 5.4.2 Paramètres
|
||||
|
||||
| Paramètre | Type | Valeur par défaut | Description |
|
||||
|-----------|------|-------------------|-------------|
|
||||
| `enabled` | bool | `false` | Activer ARP-R (désactivé par défaut) |
|
||||
| `target_success_rate` | float | `0.98` | Taux de race gagnée |
|
||||
| `refresh_interval_s` | int | `60` | Intervalle de rafraîchissement (gratuitous ARP) |
|
||||
| `captive_mac` | MAC | `auto` | MAC du captive portal (auto = MAC SecuBox) |
|
||||
| `target_gateway` | bool | `true` | Rediriger les requêtes vers gateway |
|
||||
|
||||
#### 5.4.3 Journalisation
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "OPAD_ARP_REDIRECT",
|
||||
"timestamp": "2026-05-12T14:45:33.789Z",
|
||||
"target_ip": "192.168.1.42",
|
||||
"target_mac": "aa:bb:cc:dd:ee:ff",
|
||||
"spoofed_ip": "192.168.1.1",
|
||||
"captive_mac": "00:11:22:33:44:55",
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Modes opératoires
|
||||
|
||||
### 6.1 Mode **opad-only** (défaut canonique)
|
||||
|
||||
**Description :** SecuBox en observation pure + injection active (DNS-R, RST-I). Aucun forwarding, aucun rôle de gateway.
|
||||
|
||||
**Configuration :**
|
||||
|
||||
```toml
|
||||
[wall.opad]
|
||||
mode = "opad-only"
|
||||
primitives = ["dns-r", "rst-i"]
|
||||
|
||||
[wall.opad.dns-r]
|
||||
enabled = true
|
||||
target_success_rate = 0.99
|
||||
|
||||
[wall.opad.rst-i]
|
||||
enabled = true
|
||||
target_success_rate = 0.90
|
||||
```
|
||||
|
||||
**Propriétés :**
|
||||
- ✅ INV-01 à INV-07 respectés
|
||||
- ✅ Zéro latency
|
||||
- ✅ Retrait transparent
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Mode **opad-with-escalation**
|
||||
|
||||
**Description :** OPAD par défaut, avec possibilité d'activer ponctuellement DHCP-R ou ARP-R pour forcer la SecuBox comme gateway (interception TLS).
|
||||
|
||||
**Configuration :**
|
||||
|
||||
```toml
|
||||
[wall.opad]
|
||||
mode = "opad-with-escalation"
|
||||
escalation_trigger = "manual" # ou "auto" (si AUTH détecte menace critique)
|
||||
|
||||
[wall.opad.dhcp-r]
|
||||
enabled = false # activé à la demande
|
||||
escalate_to_gateway = true
|
||||
|
||||
[wall.opad.arp-r]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
**Workflow d'escalade :**
|
||||
|
||||
1. Détection menace critique (ex: TLS C2 non blockable par DNS-R)
|
||||
2. Event `OPAD_ESCALATE_REQUEST` → journal audit
|
||||
3. Snapshot 4R de la config active
|
||||
4. Activation DHCP-R avec `escalate_to_gateway=true`
|
||||
5. Nouveau DHCP lease force gateway → SecuBox devient in-path
|
||||
6. Après résolution : `opad revert` → rollback 4R → retour opad-only
|
||||
|
||||
**Propriétés :**
|
||||
- ✅ INV-08 respecté (escalade révocable)
|
||||
- ⚠️ INV-01 temporairement violé (mode in-path)
|
||||
- ✅ Traçabilité CSPN complète (logs escalade/revert)
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Mode **legacy-in-path** (déprécié)
|
||||
|
||||
**Description :** Mode bridge transparent (D-2025-IDGP-INLINE). Conservé pour compatibilité, mais non recommandé.
|
||||
|
||||
**Status :** Déprécié depuis v2.4.0. Sera supprimé en v3.0.0.
|
||||
|
||||
**Migration :** Utiliser `opad-with-escalation` pour cas nécessitant interception.
|
||||
|
||||
---
|
||||
|
||||
## 7. Profil de configuration 3-broche
|
||||
|
||||
### 7.1 Structure
|
||||
|
||||
La configuration OPAD suit le modèle **3-broche** (3-prong) :
|
||||
|
||||
```
|
||||
/etc/secubox/wall/
|
||||
├── active/
|
||||
│ ├── observation.toml ← Broche 1: Observation
|
||||
│ ├── injection.toml ← Broche 2: Injection
|
||||
│ └── policy.toml ← Broche 3: Politique
|
||||
├── shadow/
|
||||
│ ├── observation.toml
|
||||
│ ├── injection.toml
|
||||
│ └── policy.toml
|
||||
└── rollback/
|
||||
├── R1/ (timestamp: 2026-05-12T14:00:00Z)
|
||||
├── R2/ (timestamp: 2026-05-12T13:00:00Z)
|
||||
├── R3/ (timestamp: 2026-05-12T12:00:00Z)
|
||||
└── R4/ (timestamp: 2026-05-12T11:00:00Z)
|
||||
```
|
||||
|
||||
### 7.2 Broche 1 : Observation
|
||||
|
||||
**Responsabilité :** Définir les sources d'observation (interfaces, SPAN ports, TAP).
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```toml
|
||||
[observation]
|
||||
interfaces = ["eth1", "eth2"] # Interfaces LAN à observer
|
||||
mode = "promiscuous"
|
||||
bpf_filter = "not port 22" # Exclure SSH management
|
||||
|
||||
[observation.span]
|
||||
enabled = true
|
||||
span_port = "eth0" # Port SPAN du switch
|
||||
vlan_strip = true
|
||||
```
|
||||
|
||||
### 7.3 Broche 2 : Injection
|
||||
|
||||
**Responsabilité :** Définir les primitifs d'injection actifs et leurs paramètres.
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```toml
|
||||
[injection.dns-r]
|
||||
enabled = true
|
||||
target_success_rate = 0.99
|
||||
modes = ["nxdomain", "sinkhole"]
|
||||
sinkhole_ip = "10.254.254.254"
|
||||
|
||||
[injection.rst-i]
|
||||
enabled = true
|
||||
target_success_rate = 0.90
|
||||
double_ended = true
|
||||
|
||||
[injection.dhcp-r]
|
||||
enabled = false
|
||||
|
||||
[injection.arp-r]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
### 7.4 Broche 3 : Politique
|
||||
|
||||
**Responsabilité :** Définir les règles de décision (quand injecter).
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```toml
|
||||
[policy]
|
||||
mode = "opad-only"
|
||||
escalation_trigger = "manual"
|
||||
|
||||
[policy.triggers]
|
||||
# DNS-R : bloquer domaines malveillants
|
||||
dns_blocklists = ["crowdsec", "abuse.ch", "phishing-army"]
|
||||
dns_custom_block = ["evil.example.com", "*.malware.net"]
|
||||
|
||||
# RST-I : terminer connexions C2
|
||||
rst_on_crowdsec_alert = true
|
||||
rst_on_suricata_alert = true
|
||||
rst_confidence_threshold = 0.85
|
||||
|
||||
# DHCP-R : quarantaine MAC suspects
|
||||
dhcp_quarantine_sources = ["auth-guardian"]
|
||||
|
||||
# ARP-R : rediriger devices compromis
|
||||
arp_redirect_sources = ["nac"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Intégration avec les modules
|
||||
|
||||
### 8.1 Table d'intégration
|
||||
|
||||
| Module | Rôle OPAD | Event envoyé vers WALL | Event reçu depuis WALL |
|
||||
|--------|-----------|------------------------|------------------------|
|
||||
| **AUTH** | Fournisseur de décision (ban user → RST-I) | `AUTH_BAN_USER` | `OPAD_RST_SUCCESS` |
|
||||
| **WALL** | Exécuteur OPAD (injection) | — | — |
|
||||
| **BOOT** | Configuration réseau (SPAN setup) | — | `OPAD_INIT_STATUS` |
|
||||
| **MIND** | Analyse comportement → détection anomalies | `MIND_ANOMALY_DETECTED` | `OPAD_INJECT_STATS` |
|
||||
| **ROOT** | Journalisation audit CSPN | — | `OPAD_*` (tous events) |
|
||||
| **MESH** | Sync blacklists entre SecuBox (MirrorNet) | `MESH_BLOCKLIST_UPDATE` | — |
|
||||
|
||||
### 8.2 Flux d'événements
|
||||
|
||||
```
|
||||
┌─────────┐ MIND_ANOMALY_DETECTED ┌──────────┐
|
||||
│ MIND │─────────────────────────────────►│ WALL │
|
||||
└─────────┘ │ (OPAD) │
|
||||
└────┬─────┘
|
||||
┌─────────┐ AUTH_BAN_USER │
|
||||
│ AUTH │─────────────────────────────────►────┤
|
||||
└─────────┘ │
|
||||
│
|
||||
Décision interne
|
||||
(policy.toml)
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Injection │
|
||||
│ (DNS-R/RST-I) │
|
||||
└────────┬───────┘
|
||||
│
|
||||
┌────────────────────────────┼────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ ROOT │ │ MIND │ │ AUTH │
|
||||
│ (Audit) │ │ (Stats) │ │(Feedback)│
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
OPAD_* OPAD_INJECT_STATS OPAD_RST_SUCCESS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Journalisation et audit
|
||||
|
||||
### 9.1 Types d'événements
|
||||
|
||||
| Event | Criticité | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `OPAD_INIT` | INFO | Démarrage module OPAD |
|
||||
| `OPAD_DNS_RACE` | ALERTE | Injection DNS-R |
|
||||
| `OPAD_DHCP_RACE` | ALERTE | Injection DHCP-R |
|
||||
| `OPAD_RST_INJECT` | ALERTE | Injection RST-I |
|
||||
| `OPAD_ARP_REDIRECT` | ALERTE | Injection ARP-R |
|
||||
| `OPAD_INJECT_LOST` | WARN | Race perdue (injection échouée) |
|
||||
| `OPAD_ESCALATE` | CRITICAL | Passage en mode escaladé (in-path) |
|
||||
| `OPAD_REVERT` | INFO | Retour mode opad-only |
|
||||
| `OPAD_CONFIG_SWAP` | INFO | Swap active/shadow |
|
||||
| `OPAD_ROLLBACK` | WARN | Rollback 4R activé |
|
||||
|
||||
### 9.2 Format de log
|
||||
|
||||
**Standard :** JSON structuré, un event par ligne, conforme CSPN.
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-05-12T14:32:01.234Z",
|
||||
"module": "wall",
|
||||
"component": "opad",
|
||||
"event": "OPAD_DNS_RACE",
|
||||
"severity": "alert",
|
||||
"src_ip": "192.168.1.42",
|
||||
"src_mac": "aa:bb:cc:dd:ee:ff",
|
||||
"query": "evil-c2.example.com",
|
||||
"qtype": "A",
|
||||
"action": "sinkhole",
|
||||
"sinkhole_ip": "10.254.254.254",
|
||||
"result": "success",
|
||||
"race_time_ms": 4.2,
|
||||
"trigger_source": "crowdsec",
|
||||
"session_id": "opad-20260512-143201-abc123"
|
||||
}
|
||||
```
|
||||
|
||||
**Destination :** `/var/log/secubox/wall/opad.log` (rotation journalière, retention 90j).
|
||||
|
||||
---
|
||||
|
||||
## 10. Périmètre déclaré
|
||||
|
||||
### 10.1 Couvert (◉)
|
||||
|
||||
| Menace | Primitif | Efficacité |
|
||||
|--------|----------|------------|
|
||||
| Malware DNS (C2, phishing) | DNS-R | 99% |
|
||||
| Connexion TCP malveillante (C2, exfiltration) | RST-I | 90% |
|
||||
| Device compromis (quarantaine) | DHCP-R / ARP-R | 95% |
|
||||
| Blocklists dynamiques (CrowdSec, Suricata) | DNS-R + RST-I | 98% |
|
||||
|
||||
### 10.2 Partiel (◐)
|
||||
|
||||
| Menace | Limitation | Solution alternative |
|
||||
|--------|-----------|----------------------|
|
||||
| TLS C2 (SNI chiffré) | DNS-R inefficace si IP hardcodée | Mode escaladé + TLS interception |
|
||||
| HTTP/3 QUIC | RST-I incompatible (UDP) | Blocage nftables en amont |
|
||||
| P2P mesh (Tor, I2P) | Pas de DNS resolution | DPI + RST-I sur détection protocole |
|
||||
|
||||
### 10.3 Hors portée (✕)
|
||||
|
||||
| Menace | Raison | Module responsable |
|
||||
|--------|--------|-------------------|
|
||||
| TLS interception (MITM) | Nécessite in-path | Mode escaladé (hors OPAD canonique) |
|
||||
| Hard drop (blocage nftables) | Pas d'injection, juste drop | WALL nftables |
|
||||
| VLAN isolation | Configuration switch | BOOT netplan |
|
||||
| QoS / rate-limiting | Nécessite in-path | QOS module (à venir) |
|
||||
| Protection WAN (DDoS) | Pas de surface WAN | Upstream (opérateur) |
|
||||
|
||||
---
|
||||
|
||||
## 11. Historique doctrinal
|
||||
|
||||
### 11.1 Version actuelle
|
||||
|
||||
**CM-WALL-OPAD-2026-05** (v2.4.0)
|
||||
- Status : Canonique
|
||||
- Date : 2026-05-12
|
||||
- Auteur : Gérald Kerma
|
||||
|
||||
### 11.2 Versions dépréciées
|
||||
|
||||
| Référence | Nom | Date | Raison dépréciation |
|
||||
|-----------|-----|------|---------------------|
|
||||
| D-2025-IDGP-INLINE | In-line Data Guardian Protocol | 2025-03-15 | Single point of failure, latency |
|
||||
| D-2024-BRIDGE-TRANSPARENT | Bridge transparent nftables | 2024-06-10 | Complexité opérationnelle, fail-closed |
|
||||
|
||||
### 11.3 Migration depuis D-2025-IDGP-INLINE
|
||||
|
||||
**Checklist :**
|
||||
|
||||
1. ✅ Désactiver `ip_forward` et bridge nftables
|
||||
2. ✅ Configurer SPAN port ou TAP sur switch
|
||||
3. ✅ Créer profil 3-broche (observation + injection + policy)
|
||||
4. ✅ Activer DNS-R et RST-I
|
||||
5. ✅ Tester retrait physique SecuBox → pas de coupure réseau
|
||||
6. ✅ Auditer logs → vérifier INV-03 (ALERTE·DÉPÔT avant injection)
|
||||
|
||||
---
|
||||
|
||||
## 12. Références
|
||||
|
||||
### 12.1 Spécifications techniques
|
||||
|
||||
- **SPEC-WALL-OPAD-2026-05.md** : Spécification complète du module WALL OPAD
|
||||
- **SCHEMA-OPAD-CONFIG.json** : Schéma JSON de validation des fichiers TOML
|
||||
- **MODELS-OPAD-EVENTS.json** : Modèles d'événements (pour parsing logs)
|
||||
|
||||
### 12.2 Documentation complémentaire
|
||||
|
||||
- **CSPN-MATRIX-WALL.md** : Mapping critères ANSSI CSPN ↔ invariants OPAD
|
||||
- **OPS-GUIDE-OPAD.md** : Guide opérationnel (installation, monitoring, troubleshooting)
|
||||
- **TEST-SUITE-OPAD.md** : Suite de tests de validation (pytest + scapy)
|
||||
|
||||
### 12.3 Code source
|
||||
|
||||
- **`packages/secubox-wall/api/opad/`** : Implémentation Python (FastAPI)
|
||||
- **`packages/secubox-wall/daemon/opad.c`** : Daemon C (injection bas-niveau)
|
||||
- **`packages/secubox-wall/scripts/opad-cli.sh`** : CLI d'administration
|
||||
|
||||
---
|
||||
|
||||
## Signature
|
||||
|
||||
**Document canonique validé pour production SecuBox v2.4.0+.**
|
||||
|
||||
**Auteur :** Gérald Kerma (CyberMind)
|
||||
**Date :** 2026-05-12
|
||||
**Référence :** CM-WALL-OPAD-2026-05
|
||||
**Version :** 2.4.0
|
||||
**Status :** Canonique
|
||||
|
||||
---
|
||||
|
||||
**EOF**
|
||||
478
remote-ui/round/agent/api/mode_api.py
Normal file
478
remote-ui/round/agent/api/mode_api.py
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
SecuBox Eye Remote - Mode Control Web API
|
||||
|
||||
Simple HTTP API for manual gadget mode switching.
|
||||
Safer than auto-switching - user-triggered only.
|
||||
|
||||
CyberMind - https://cybermind.fr
|
||||
Author: Gerald Kerma <gandalf@gk2.net>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import urllib.parse
|
||||
|
||||
# Gadget paths
|
||||
GADGET_PATH = Path("/sys/kernel/config/usb_gadget/secubox")
|
||||
GADGET_SCRIPT = Path("/etc/secubox/eye-remote/gadget-setup.sh")
|
||||
|
||||
# Available modes
|
||||
MODES = {
|
||||
"composite": "up", # ECM + ACM + Storage (full)
|
||||
"network": "network", # ECM + ACM only
|
||||
"storage": "storage", # Storage only (U-Boot)
|
||||
"silent": "silent", # Storage + ACM (silent fallback)
|
||||
"hid": "hid", # HID + ACM
|
||||
}
|
||||
|
||||
|
||||
def get_current_mode() -> Dict[str, Any]:
|
||||
"""Get current gadget mode and status."""
|
||||
result = {
|
||||
"mode": "unknown",
|
||||
"functions": [],
|
||||
"udc": None,
|
||||
"udc_state": "unknown",
|
||||
}
|
||||
|
||||
# Check UDC
|
||||
udc_file = GADGET_PATH / "UDC"
|
||||
if udc_file.exists():
|
||||
try:
|
||||
result["udc"] = udc_file.read_text().strip() or None
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check functions
|
||||
configs_path = GADGET_PATH / "configs" / "c.1"
|
||||
if configs_path.exists():
|
||||
for item in configs_path.iterdir():
|
||||
if item.is_symlink():
|
||||
name = item.name
|
||||
if "ecm" in name:
|
||||
result["functions"].append("ecm")
|
||||
elif "acm" in name:
|
||||
result["functions"].append("acm")
|
||||
elif "mass_storage" in name:
|
||||
result["functions"].append("storage")
|
||||
elif "hid" in name:
|
||||
result["functions"].append("hid")
|
||||
elif "rndis" in name:
|
||||
result["functions"].append("rndis")
|
||||
|
||||
# Determine mode from functions
|
||||
funcs = set(result["functions"])
|
||||
if funcs == {"ecm", "acm", "storage"} or funcs == {"ecm", "acm", "storage", "rndis"}:
|
||||
result["mode"] = "composite"
|
||||
elif funcs == {"ecm", "acm"} or funcs == {"ecm", "acm", "rndis"}:
|
||||
result["mode"] = "network"
|
||||
elif funcs == {"storage", "acm"}:
|
||||
result["mode"] = "silent"
|
||||
elif funcs == {"storage"}:
|
||||
result["mode"] = "storage"
|
||||
elif funcs == {"hid", "acm"}:
|
||||
result["mode"] = "hid"
|
||||
elif not funcs:
|
||||
result["mode"] = "none"
|
||||
|
||||
# Check UDC state
|
||||
if result["udc"]:
|
||||
state_file = Path(f"/sys/class/udc/{result['udc']}/state")
|
||||
if state_file.exists():
|
||||
try:
|
||||
result["udc_state"] = state_file.read_text().strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_system_metrics() -> Dict[str, Any]:
|
||||
"""Get Pi Zero system metrics."""
|
||||
metrics = {
|
||||
"cpu_percent": 0,
|
||||
"memory_percent": 0,
|
||||
"disk_percent": 0,
|
||||
"cpu_temp": 0,
|
||||
"load_avg": [0, 0, 0],
|
||||
"uptime_seconds": 0,
|
||||
"hostname": "unknown",
|
||||
}
|
||||
|
||||
try:
|
||||
# CPU usage from /proc/stat (simple approximation)
|
||||
with open("/proc/stat") as f:
|
||||
line = f.readline()
|
||||
parts = line.split()[1:8]
|
||||
total = sum(int(p) for p in parts)
|
||||
idle = int(parts[3])
|
||||
metrics["cpu_percent"] = round(100 * (1 - idle / total), 1) if total > 0 else 0
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Memory from /proc/meminfo
|
||||
with open("/proc/meminfo") as f:
|
||||
mem = {}
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
mem[parts[0].rstrip(":")] = int(parts[1])
|
||||
total = mem.get("MemTotal", 1)
|
||||
avail = mem.get("MemAvailable", mem.get("MemFree", 0))
|
||||
metrics["memory_percent"] = round(100 * (1 - avail / total), 1)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Disk usage
|
||||
st = os.statvfs("/")
|
||||
total = st.f_blocks * st.f_frsize
|
||||
free = st.f_bavail * st.f_frsize
|
||||
metrics["disk_percent"] = round(100 * (1 - free / total), 1) if total > 0 else 0
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# CPU temperature
|
||||
with open("/sys/class/thermal/thermal_zone0/temp") as f:
|
||||
metrics["cpu_temp"] = round(int(f.read().strip()) / 1000, 1)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Load average
|
||||
metrics["load_avg"] = list(os.getloadavg())
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Uptime
|
||||
with open("/proc/uptime") as f:
|
||||
metrics["uptime_seconds"] = int(float(f.read().split()[0]))
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Hostname
|
||||
with open("/etc/hostname") as f:
|
||||
metrics["hostname"] = f.read().strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def switch_mode(mode: str) -> Dict[str, Any]:
|
||||
"""Switch gadget mode using the setup script."""
|
||||
if mode not in MODES:
|
||||
return {"success": False, "error": f"Unknown mode: {mode}"}
|
||||
|
||||
script_arg = MODES[mode]
|
||||
|
||||
if not GADGET_SCRIPT.exists():
|
||||
return {"success": False, "error": "Gadget script not found"}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sudo", str(GADGET_SCRIPT), script_arg],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15
|
||||
)
|
||||
|
||||
duration = (time.time() - start_time) * 1000
|
||||
|
||||
if result.returncode == 0:
|
||||
return {
|
||||
"success": True,
|
||||
"mode": mode,
|
||||
"duration_ms": round(duration, 1),
|
||||
"output": result.stdout.strip()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.stderr.strip() or "Script failed",
|
||||
"returncode": result.returncode
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "error": "Timeout (15s)"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
class ModeAPIHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP request handler for mode API."""
|
||||
|
||||
def _send_json(self, data: Dict, status: int = 200):
|
||||
"""Send JSON response."""
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, indent=2).encode())
|
||||
|
||||
def _send_html(self, html: str, status: int = 200):
|
||||
"""Send HTML response."""
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle GET requests."""
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if path == "/" or path == "/index.html":
|
||||
# Serve control panel
|
||||
self._serve_control_panel()
|
||||
elif path == "/api/status":
|
||||
# Get current status
|
||||
status = get_current_mode()
|
||||
self._send_json(status)
|
||||
elif path == "/api/modes":
|
||||
# List available modes
|
||||
self._send_json({"modes": list(MODES.keys())})
|
||||
elif path == "/health":
|
||||
# Health check for dashboard
|
||||
self._send_json({"status": "ok"})
|
||||
elif path == "/metrics":
|
||||
# System metrics for dashboard
|
||||
metrics = get_system_metrics()
|
||||
metrics["gadget"] = get_current_mode()
|
||||
self._send_json(metrics)
|
||||
else:
|
||||
self._send_json({"error": "Not found"}, 404)
|
||||
|
||||
def do_POST(self):
|
||||
"""Handle POST requests."""
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if path.startswith("/api/switch/"):
|
||||
mode = path.split("/")[-1]
|
||||
result = switch_mode(mode)
|
||||
status_code = 200 if result.get("success") else 400
|
||||
self._send_json(result, status_code)
|
||||
else:
|
||||
self._send_json({"error": "Not found"}, 404)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""Handle CORS preflight."""
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
|
||||
def _serve_control_panel(self):
|
||||
"""Serve the mode control panel HTML."""
|
||||
status = get_current_mode()
|
||||
|
||||
html = f'''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Eye Remote - Mode Control</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: #0a0a0f;
|
||||
color: #e8e6d9;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}}
|
||||
h1 {{
|
||||
color: #c9a84c;
|
||||
text-align: center;
|
||||
}}
|
||||
.status {{
|
||||
background: #1a1a2e;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.mode {{
|
||||
font-size: 24px;
|
||||
color: #00ff41;
|
||||
}}
|
||||
.functions {{
|
||||
color: #00d4ff;
|
||||
margin: 10px 0;
|
||||
}}
|
||||
.buttons {{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
button {{
|
||||
background: #1a1a2e;
|
||||
color: #e8e6d9;
|
||||
border: 2px solid #c9a84c;
|
||||
padding: 15px 25px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
min-width: 120px;
|
||||
}}
|
||||
button:hover {{
|
||||
background: #c9a84c;
|
||||
color: #0a0a0f;
|
||||
}}
|
||||
button:disabled {{
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}}
|
||||
button.active {{
|
||||
background: #00ff41;
|
||||
color: #0a0a0f;
|
||||
border-color: #00ff41;
|
||||
}}
|
||||
.result {{
|
||||
background: #1a1a2e;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
display: none;
|
||||
}}
|
||||
.result.success {{
|
||||
border-left: 4px solid #00ff41;
|
||||
}}
|
||||
.result.error {{
|
||||
border-left: 4px solid #e63946;
|
||||
}}
|
||||
.warning {{
|
||||
background: #3d2010;
|
||||
border: 1px solid #c9a84c;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SecuBox Eye Remote</h1>
|
||||
<h2 style="text-align:center; color:#6b6b7a">Mode Control</h2>
|
||||
|
||||
<div class="status">
|
||||
<div>Current Mode: <span class="mode" id="current-mode">{status['mode'].upper()}</span></div>
|
||||
<div class="functions">Functions: <span id="functions">{', '.join(status['functions']) or 'none'}</span></div>
|
||||
<div>UDC: <span id="udc">{status['udc'] or 'none'}</span> ({status['udc_state']})</div>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
⚠️ Mode switching may cause brief USB disconnection. Network will recover automatically.
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button onclick="switchMode('composite')" id="btn-composite">COMPOSITE</button>
|
||||
<button onclick="switchMode('network')" id="btn-network">NETWORK</button>
|
||||
<button onclick="switchMode('storage')" id="btn-storage">STORAGE</button>
|
||||
<button onclick="switchMode('silent')" id="btn-silent">SILENT</button>
|
||||
<button onclick="switchMode('hid')" id="btn-hid">HID</button>
|
||||
</div>
|
||||
|
||||
<div class="result" id="result"></div>
|
||||
|
||||
<script>
|
||||
function updateButtons() {{
|
||||
const mode = document.getElementById('current-mode').textContent.toLowerCase();
|
||||
document.querySelectorAll('.buttons button').forEach(btn => {{
|
||||
btn.classList.remove('active');
|
||||
if (btn.id === 'btn-' + mode) {{
|
||||
btn.classList.add('active');
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
async function switchMode(mode) {{
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'result';
|
||||
resultDiv.textContent = 'Switching to ' + mode.toUpperCase() + '...';
|
||||
|
||||
// Disable all buttons
|
||||
document.querySelectorAll('.buttons button').forEach(btn => btn.disabled = true);
|
||||
|
||||
try {{
|
||||
const resp = await fetch('/api/switch/' + mode, {{ method: 'POST' }});
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success) {{
|
||||
resultDiv.className = 'result success';
|
||||
resultDiv.textContent = '✓ Switched to ' + mode.toUpperCase() + ' (' + data.duration_ms + 'ms)';
|
||||
document.getElementById('current-mode').textContent = mode.toUpperCase();
|
||||
updateButtons();
|
||||
|
||||
// Refresh status after 2s
|
||||
setTimeout(refreshStatus, 2000);
|
||||
}} else {{
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = '✗ Error: ' + data.error;
|
||||
}}
|
||||
}} catch (e) {{
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = '✗ Connection lost (mode switch may have succeeded)';
|
||||
// Try to refresh after reconnection
|
||||
setTimeout(refreshStatus, 5000);
|
||||
}}
|
||||
|
||||
// Re-enable buttons
|
||||
document.querySelectorAll('.buttons button').forEach(btn => btn.disabled = false);
|
||||
}}
|
||||
|
||||
async function refreshStatus() {{
|
||||
try {{
|
||||
const resp = await fetch('/api/status');
|
||||
const data = await resp.json();
|
||||
document.getElementById('current-mode').textContent = data.mode.toUpperCase();
|
||||
document.getElementById('functions').textContent = data.functions.join(', ') || 'none';
|
||||
document.getElementById('udc').textContent = data.udc || 'none';
|
||||
updateButtons();
|
||||
}} catch (e) {{
|
||||
console.log('Status refresh failed');
|
||||
}}
|
||||
}}
|
||||
|
||||
// Initial button state
|
||||
updateButtons();
|
||||
|
||||
// Auto-refresh every 10s
|
||||
setInterval(refreshStatus, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
self._send_html(html)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Suppress default logging."""
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the mode control API server."""
|
||||
port = 8081 # 8080 used by CrowdSec
|
||||
server = HTTPServer(('0.0.0.0', port), ModeAPIHandler)
|
||||
print(f"Mode Control API running on http://0.0.0.0:{port}")
|
||||
print("Endpoints:")
|
||||
print(" GET / - Control panel")
|
||||
print(" GET /api/status - Current mode")
|
||||
print(" POST /api/switch/<mode> - Switch mode")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -132,6 +132,7 @@ class AutoModeConfig:
|
|||
# Storage
|
||||
storage_image_path: str = "/var/lib/secubox/eye-remote/storage.img"
|
||||
readonly_in_silent_mode: bool = False
|
||||
storage_unmounted_recovery_s: float = 30.0 # Switch back to composite if not mounted
|
||||
|
||||
# Logging
|
||||
log_level: str = "info"
|
||||
|
|
@ -373,6 +374,39 @@ class GadgetController:
|
|||
finally:
|
||||
self._switch_in_progress = False
|
||||
|
||||
def is_storage_mounted(self) -> bool:
|
||||
"""Check if storage is being actively accessed by host.
|
||||
|
||||
Checks the mass_storage function state in configfs to determine
|
||||
if the host is actively using the storage device.
|
||||
"""
|
||||
lun_path = Path("/sys/kernel/config/usb_gadget/secubox/functions/mass_storage.usb0/lun.0")
|
||||
|
||||
if not lun_path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Check if there's active I/O by looking at the file backing
|
||||
file_path = lun_path / "file"
|
||||
if file_path.exists():
|
||||
backing_file = file_path.read_text().strip()
|
||||
if backing_file:
|
||||
# Check if the backing file is open/busy
|
||||
# Using lsof or fuser would be more accurate but heavier
|
||||
# For now, check if nlink > 0 as a proxy
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
log.debug(f"Storage mount check failed: {e}")
|
||||
return False
|
||||
|
||||
def get_storage_io_stats(self) -> Dict[str, int]:
|
||||
"""Get storage I/O statistics if available."""
|
||||
stats = {"reads": 0, "writes": 0}
|
||||
# Stats would be in /sys/kernel/config/usb_gadget/secubox/functions/mass_storage.usb0/lun.0/
|
||||
# but not all kernels expose detailed stats
|
||||
return stats
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Network Prober
|
||||
|
|
@ -685,6 +719,18 @@ class AutoModeController:
|
|||
# No host - stay in storage mode
|
||||
pass
|
||||
|
||||
# Check if storage is mounted by host
|
||||
# If not mounted after timeout, switch back to composite for network
|
||||
time_in_storage = self._time_in_state()
|
||||
if time_in_storage > self._config.storage_unmounted_recovery_s:
|
||||
if not self._gadget.is_storage_mounted():
|
||||
log.info(f"Storage not mounted after {time_in_storage:.0f}s, switching to composite")
|
||||
await self._gadget.switch_mode(
|
||||
GadgetMode.COMPOSITE,
|
||||
self._config.mode_switch_cooldown_s
|
||||
)
|
||||
await self._transition_to(AutoModeState.PROBING_NETWORK)
|
||||
|
||||
async def _handle_silent_storage(self) -> None:
|
||||
"""Handle SILENT_STORAGE state - fallback with wake monitoring."""
|
||||
# Check for wake triggers
|
||||
|
|
@ -692,13 +738,34 @@ class AutoModeController:
|
|||
wake_reason = await self._wakeup_manager.check_wake_triggers()
|
||||
if wake_reason:
|
||||
log.info(f"Wake trigger received: {wake_reason}")
|
||||
# Switch to composite mode for network recovery
|
||||
await self._gadget.switch_mode(
|
||||
GadgetMode.COMPOSITE,
|
||||
self._config.mode_switch_cooldown_s
|
||||
)
|
||||
await self._transition_to(AutoModeState.PROBING_NETWORK)
|
||||
return
|
||||
|
||||
# Also check if host is connected and configured
|
||||
if self._udc.is_configured():
|
||||
# Host might have network support now
|
||||
await self._gadget.switch_mode(
|
||||
GadgetMode.COMPOSITE,
|
||||
self._config.mode_switch_cooldown_s
|
||||
)
|
||||
await self._transition_to(AutoModeState.PROBING_NETWORK)
|
||||
return
|
||||
|
||||
# If storage not being used after timeout, try composite mode
|
||||
time_in_storage = self._time_in_state()
|
||||
if time_in_storage > self._config.storage_unmounted_recovery_s:
|
||||
if not self._gadget.is_storage_mounted():
|
||||
log.info(f"Silent storage unused for {time_in_storage:.0f}s, trying composite")
|
||||
await self._gadget.switch_mode(
|
||||
GadgetMode.COMPOSITE,
|
||||
self._config.mode_switch_cooldown_s
|
||||
)
|
||||
await self._transition_to(AutoModeState.PROBING_NETWORK)
|
||||
|
||||
async def _run_state_machine(self) -> None:
|
||||
"""Main state machine loop."""
|
||||
|
|
|
|||
0
schemas/.gitkeep
Normal file
0
schemas/.gitkeep
Normal file
365
schemas/opad-profile.schema.json
Normal file
365
schemas/opad-profile.schema.json
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://secubox.in/schemas/opad-profile.schema.json",
|
||||
"title": "OPAD Profile Configuration",
|
||||
"description": "JSON Schema for SecuBox-Deb v2.4.0 OPAD (Observe-Perturb-Apply-Decide) 3-prong configuration profile",
|
||||
"type": "object",
|
||||
"required": ["version", "mode", "observation", "injection", "policy"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "Schema version (semver)"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["observe", "enforce"],
|
||||
"description": "Global operation mode: observe (passive) or enforce (active)"
|
||||
},
|
||||
"observation": {
|
||||
"type": "object",
|
||||
"description": "Broche 1: Observation configuration",
|
||||
"required": ["interfaces", "protocols"],
|
||||
"properties": {
|
||||
"interfaces": {
|
||||
"type": "array",
|
||||
"description": "Network interfaces to observe",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+$"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"bpf_filter": {
|
||||
"type": "string",
|
||||
"description": "Optional BPF filter for packet capture",
|
||||
"default": ""
|
||||
},
|
||||
"protocols": {
|
||||
"type": "object",
|
||||
"description": "Protocol observation configuration",
|
||||
"properties": {
|
||||
"dns": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"dhcp": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"arp": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"tcp": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"fingerprinting": {
|
||||
"type": "object",
|
||||
"description": "Device fingerprinting configuration",
|
||||
"properties": {
|
||||
"dhcp_fingerprint": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"ja3": {
|
||||
"type": "boolean",
|
||||
"description": "TLS JA3 fingerprinting",
|
||||
"default": false
|
||||
},
|
||||
"ja4": {
|
||||
"type": "boolean",
|
||||
"description": "TLS JA4 fingerprinting",
|
||||
"default": false
|
||||
},
|
||||
"user_agent": {
|
||||
"type": "boolean",
|
||||
"description": "HTTP User-Agent parsing",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"injection": {
|
||||
"type": "object",
|
||||
"description": "Broche 2: Injection (perturbation) configuration",
|
||||
"properties": {
|
||||
"dns_race": {
|
||||
"$ref": "#/definitions/dns_race_config"
|
||||
},
|
||||
"dhcp_race": {
|
||||
"$ref": "#/definitions/dhcp_race_config"
|
||||
},
|
||||
"rst_inject": {
|
||||
"$ref": "#/definitions/rst_inject_config"
|
||||
},
|
||||
"arp_redirect": {
|
||||
"$ref": "#/definitions/arp_redirect_config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"type": "object",
|
||||
"description": "Broche 3: Policy configuration",
|
||||
"required": ["default_action"],
|
||||
"properties": {
|
||||
"default_action": {
|
||||
"type": "string",
|
||||
"enum": ["allow", "observe", "disrupt"],
|
||||
"description": "Default action for unmatched traffic"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"description": "Policy rules (processed by priority)",
|
||||
"items": {
|
||||
"$ref": "#/definitions/policy_rule"
|
||||
}
|
||||
},
|
||||
"escalation": {
|
||||
"$ref": "#/definitions/escalation_config"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"dns_race_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"target_success_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0.9,
|
||||
"maximum": 1.0,
|
||||
"default": 0.99,
|
||||
"description": "Target success rate for winning DNS race (0.9-1.0)"
|
||||
},
|
||||
"modes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["nxdomain", "sinkhole", "redirect_captive"]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "Enabled DNS race modes"
|
||||
},
|
||||
"sinkhole_ip": {
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
"description": "IP address for sinkhole mode",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 3600,
|
||||
"default": 60,
|
||||
"description": "TTL for injected DNS responses (seconds)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dhcp_race_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"target_success_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0.9,
|
||||
"maximum": 1.0,
|
||||
"default": 0.95,
|
||||
"description": "Target success rate for winning DHCP race"
|
||||
},
|
||||
"quarantine_pool": {
|
||||
"type": "object",
|
||||
"required": ["start", "end", "gateway"],
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
"description": "Start of quarantine IP pool"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
"description": "End of quarantine IP pool"
|
||||
},
|
||||
"lease_time": {
|
||||
"type": "integer",
|
||||
"minimum": 60,
|
||||
"maximum": 86400,
|
||||
"default": 300,
|
||||
"description": "Lease time in seconds"
|
||||
},
|
||||
"gateway": {
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
"description": "Gateway IP for quarantine network"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rst_inject_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"target_success_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0.85,
|
||||
"maximum": 1.0,
|
||||
"default": 0.90,
|
||||
"description": "Target success rate for RST injection (min 0.85)"
|
||||
},
|
||||
"double_ended": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Send RST to both client and server"
|
||||
},
|
||||
"timing_window_ms": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 10,
|
||||
"description": "Timing window for RST injection (milliseconds)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"arp_redirect_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"target_success_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0.95,
|
||||
"maximum": 1.0,
|
||||
"default": 0.98,
|
||||
"description": "Target success rate for ARP redirection (min 0.95)"
|
||||
},
|
||||
"refresh_interval_s": {
|
||||
"type": "integer",
|
||||
"minimum": 10,
|
||||
"maximum": 300,
|
||||
"default": 60,
|
||||
"description": "ARP table refresh interval (seconds)"
|
||||
},
|
||||
"captive_mac": {
|
||||
"type": "string",
|
||||
"pattern": "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$",
|
||||
"description": "MAC address for captive portal gateway"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policy_rule": {
|
||||
"type": "object",
|
||||
"required": ["id", "priority", "action"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique rule identifier"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9999,
|
||||
"description": "Rule priority (0=highest, 9999=lowest)"
|
||||
},
|
||||
"match": {
|
||||
"type": "object",
|
||||
"description": "Match conditions (all conditions are AND-ed)",
|
||||
"properties": {
|
||||
"source_mac": {
|
||||
"type": "string",
|
||||
"pattern": "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$",
|
||||
"description": "Source MAC address"
|
||||
},
|
||||
"dest_domain": {
|
||||
"type": "string",
|
||||
"description": "Destination domain (supports wildcards: *.example.com)"
|
||||
},
|
||||
"dest_ip": {
|
||||
"type": "string",
|
||||
"description": "Destination IP address or CIDR"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["tcp", "udp", "icmp", "any"],
|
||||
"default": "any"
|
||||
},
|
||||
"mind_score_above": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Trigger if MIND adversarial score above threshold"
|
||||
},
|
||||
"device_authorized": {
|
||||
"type": "boolean",
|
||||
"description": "Match only authorized/unauthorized devices"
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"allow",
|
||||
"observe",
|
||||
"dns_nxdomain",
|
||||
"dns_sinkhole",
|
||||
"dns_redirect",
|
||||
"dhcp_quarantine",
|
||||
"tcp_rst",
|
||||
"arp_redirect"
|
||||
],
|
||||
"description": "Action to take on match"
|
||||
},
|
||||
"log_level": {
|
||||
"type": "string",
|
||||
"enum": ["debug", "info", "warning", "error"],
|
||||
"default": "info",
|
||||
"description": "Logging level for this rule"
|
||||
}
|
||||
}
|
||||
},
|
||||
"escalation_config": {
|
||||
"type": "object",
|
||||
"description": "Policy escalation configuration",
|
||||
"properties": {
|
||||
"allow_in_path": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Allow temporary escalation to allow mode"
|
||||
},
|
||||
"require_explicit_consent": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Require user consent for escalation"
|
||||
},
|
||||
"auto_revert_after_s": {
|
||||
"type": "integer",
|
||||
"minimum": 60,
|
||||
"default": 300,
|
||||
"description": "Auto-revert escalation after N seconds (min 60)"
|
||||
},
|
||||
"audit_all_escalations": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Log all escalation events to audit log"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,19 @@ REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
|||
PACKAGES_DIR="${REPO_DIR}/packages"
|
||||
OUTPUT_DIR="${REPO_DIR}/output/debs"
|
||||
|
||||
SUITE="${1:-bookworm}"
|
||||
ARCH="${2:-$(dpkg --print-architecture)}"
|
||||
# Parse positional + flags
|
||||
SUITE="${1:-bookworm}"; shift || true
|
||||
ARCH="${1:-$(dpkg --print-architecture)}"; shift || true
|
||||
|
||||
FILTER=""
|
||||
DRY_RUN=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--filter) FILTER="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
*) err "Unknown flag: $1"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
RED='\033[0;31m'; CYAN='\033[0;36m'; GOLD='\033[0;33m'
|
||||
GREEN='\033[0;32m'; NC='\033[0m'; BOLD='\033[1m'
|
||||
|
|
@ -83,6 +94,22 @@ rm -f *.deb *.changes *.buildinfo 2>/dev/null || true
|
|||
SUCCESS=0
|
||||
FAILED=0
|
||||
|
||||
if [[ -n "$FILTER" ]]; then
|
||||
if [[ ! -f "$FILTER" ]]; then
|
||||
err "Filter manifest not found: $FILTER"
|
||||
exit 1
|
||||
fi
|
||||
mapfile -t ALLOWED < <(jq -r '.[]' "$FILTER")
|
||||
log "Filter active: ${#ALLOWED[@]} packages"
|
||||
fi
|
||||
|
||||
is_allowed() {
|
||||
local pkg="$1"
|
||||
[[ -z "$FILTER" ]] && return 0
|
||||
for a in "${ALLOWED[@]}"; do [[ "$a" == "$pkg" ]] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
for PKG in "${PACKAGES[@]}"; do
|
||||
PKG_DIR="${PACKAGES_DIR}/${PKG}"
|
||||
|
||||
|
|
@ -91,6 +118,14 @@ for PKG in "${PACKAGES[@]}"; do
|
|||
continue
|
||||
fi
|
||||
|
||||
if ! is_allowed "$PKG"; then
|
||||
continue
|
||||
fi
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log "DRY-RUN would build: $PKG"
|
||||
continue
|
||||
fi
|
||||
|
||||
log "Building ${BOLD}${PKG}${NC}..."
|
||||
|
||||
cd "${PKG_DIR}"
|
||||
|
|
@ -131,10 +166,12 @@ for PKG in "${PACKAGES[@]}"; do
|
|||
fi
|
||||
done
|
||||
|
||||
# Déplacer les .deb vers output/debs
|
||||
cd "${PACKAGES_DIR}"
|
||||
mv *.deb "${OUTPUT_DIR}/" 2>/dev/null || true
|
||||
rm -f *.changes *.buildinfo 2>/dev/null || true
|
||||
# Déplacer les .deb vers output/debs (pas en dry-run)
|
||||
if [[ $DRY_RUN -eq 0 ]]; then
|
||||
cd "${PACKAGES_DIR}"
|
||||
mv *.deb "${OUTPUT_DIR}/" 2>/dev/null || true
|
||||
rm -f *.changes *.buildinfo 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Résumé
|
||||
echo ""
|
||||
|
|
@ -143,8 +180,10 @@ echo -e "${GREEN}${BOLD} Build terminé !${NC}"
|
|||
echo ""
|
||||
echo -e " Succès : ${SUCCESS}"
|
||||
echo -e " Échecs : ${FAILED}"
|
||||
echo ""
|
||||
echo -e " Packages dans : ${OUTPUT_DIR}"
|
||||
ls -la "${OUTPUT_DIR}"/*.deb 2>/dev/null | head -20 || true
|
||||
if [[ $DRY_RUN -eq 0 ]]; then
|
||||
echo ""
|
||||
echo -e " Packages dans : ${OUTPUT_DIR}"
|
||||
ls -la "${OUTPUT_DIR}"/*.deb 2>/dev/null | head -20 || true
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${GOLD}${BOLD}════════════════════════════════════════════════════════${NC}"
|
||||
|
|
|
|||
32
scripts/lib/test-helpers.sh
Normal file
32
scripts/lib/test-helpers.sh
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# scripts/lib/test-helpers.sh
|
||||
# Sourced by test scripts. No external deps.
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1" actual="$2" msg="${3:-}"
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
echo "FAIL: ${msg}"
|
||||
echo " expected: $expected"
|
||||
echo " actual: $actual"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local haystack="$1" needle="$2" msg="${3:-}"
|
||||
if [[ "$haystack" != *"$needle"* ]]; then
|
||||
echo "FAIL: ${msg}"
|
||||
echo " haystack: $haystack"
|
||||
echo " needle: $needle"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_file() {
|
||||
local path="$1" msg="${2:-file missing}"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
echo "FAIL: ${msg}: $path"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
pass() { echo "PASS: $*"; }
|
||||
90
scripts/lib/tier-manifest.sh
Normal file
90
scripts/lib/tier-manifest.sh
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# scripts/lib/tier-manifest.sh
|
||||
# Resolve a tier name to a flat JSON array of package names.
|
||||
# Usage (after sourcing): tier_manifest <tier> <out.json>
|
||||
# Tiers: base | tier-lite | tier-standard | tier-pro
|
||||
#
|
||||
# Note: secubox gen emits manifest.yaml (YAML, not JSON).
|
||||
# Package list lives at the top-level .packages[] key.
|
||||
#
|
||||
# Dependencies:
|
||||
# - python3 (>=3.9)
|
||||
# - python3-yaml (Debian package). Install: sudo apt-get install -y python3-yaml
|
||||
|
||||
_secubox_bin() {
|
||||
local bin
|
||||
bin="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/cmd/secubox/secubox"
|
||||
if [[ -x "$bin" ]]; then echo "$bin"; return 0; fi
|
||||
if command -v secubox >/dev/null 2>&1; then command -v secubox; return 0; fi
|
||||
echo "ERROR: secubox binary not found" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
tier_manifest() {
|
||||
local tier="$1" out="$2"
|
||||
|
||||
if [[ -z "$tier" || -z "$out" ]]; then
|
||||
echo "Usage: tier_manifest <tier> <out.json>" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
if [[ "$tier" == "base" ]]; then
|
||||
printf '["secubox-core","secubox-hub"]\n' > "$out"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! python3 -c 'import yaml' 2>/dev/null; then
|
||||
echo "ERROR: tier-manifest.sh requires python3-yaml. Install: sudo apt-get install -y python3-yaml" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sb; sb="$(_secubox_bin)" || return 1
|
||||
local tmpdir; tmpdir="$(mktemp -d)"
|
||||
# Cleanup on any return path (success, error, interrupt under set -e)
|
||||
trap 'rm -rf "$tmpdir"' RETURN
|
||||
|
||||
if ! "$sb" gen --tier "$tier" --board mochabin --out "$tmpdir" >/dev/null 2>&1; then
|
||||
echo "ERROR: secubox gen --tier $tier failed" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# secubox gen emits manifest.yaml (YAML format, not JSON)
|
||||
local mf
|
||||
mf="$(find "$tmpdir" -maxdepth 2 -name 'manifest.yaml' -type f | head -1)"
|
||||
|
||||
if [[ ! -f "$mf" ]]; then
|
||||
echo "ERROR: secubox gen produced no manifest.yaml in $tmpdir" >&2
|
||||
find "$tmpdir" -type f >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse YAML with python3/pyyaml; extract .packages[] as a unique JSON array.
|
||||
python3 - "$mf" "$out" <<'PYEOF'
|
||||
import sys, json, yaml
|
||||
|
||||
manifest_path = sys.argv[1]
|
||||
out_path = sys.argv[2]
|
||||
|
||||
with open(manifest_path, "r") as f:
|
||||
doc = yaml.safe_load(f) or {}
|
||||
|
||||
packages = doc.get("packages", [])
|
||||
if not isinstance(packages, list):
|
||||
print(f"ERROR: .packages is not a list in {manifest_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
unique = []
|
||||
for p in packages:
|
||||
if not isinstance(p, str):
|
||||
print(f"ERROR: non-string entry in .packages[]: {p!r}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
unique.append(p)
|
||||
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(unique, f)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
}
|
||||
131
scripts/render-deploy-artifacts.sh
Executable file
131
scripts/render-deploy-artifacts.sh
Executable file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/render-deploy-artifacts.sh
|
||||
# Generate the nginx vhost, the DEPLOY.md recipe, install.sh, and license
|
||||
# copies inside output/repo/. No network access.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(dirname "$SCRIPT_DIR")"
|
||||
OUT="${1:-$REPO/output/repo}"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
FPR="$(cat "$OUT/FINGERPRINT.txt" 2>/dev/null || echo 'UNKNOWN')"
|
||||
|
||||
cat > "$OUT/nginx-apt.conf" <<'NGINX'
|
||||
# /etc/nginx/sites-available/apt.secubox.in
|
||||
# Drop in, symlink to sites-enabled/, then run certbot --nginx -d apt.secubox.in.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name apt.secubox.in;
|
||||
|
||||
root /var/www/apt.secubox.in;
|
||||
|
||||
# Let certbot serve its challenge before TLS is in place.
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/apt.secubox.in;
|
||||
}
|
||||
|
||||
# MIME types for apt clients.
|
||||
types {
|
||||
application/vnd.debian.binary-package deb;
|
||||
application/pgp-keys gpg;
|
||||
text/plain txt md sha256;
|
||||
}
|
||||
|
||||
# Allow directory listing under /dists and /pool only.
|
||||
location /dists/ { autoindex on; }
|
||||
location /pool/ { autoindex on; }
|
||||
|
||||
# Everything else is served as static.
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/apt.secubox.in.access.log;
|
||||
error_log /var/log/nginx/apt.secubox.in.error.log;
|
||||
}
|
||||
NGINX
|
||||
|
||||
cat > "$OUT/DEPLOY.md" <<EOF
|
||||
# Deploying \`apt.secubox.in\`
|
||||
|
||||
Generated $(date -u +%Y-%m-%dT%H:%M:%SZ). Stage was built locally; this is the
|
||||
out-of-band push recipe.
|
||||
|
||||
## 1. rsync the tree
|
||||
|
||||
\`\`\`bash
|
||||
rsync -avz --delete \\
|
||||
--exclude='db/' --exclude='gpg/' --exclude='conf/' \\
|
||||
$OUT/ deploy@apt.secubox.in:/var/www/apt.secubox.in/
|
||||
\`\`\`
|
||||
|
||||
The \`db/\` and \`conf/\` directories are reprepro state and must NEVER leave
|
||||
the staging host. The public keyring (\`secubox-keyring.gpg\`), the dists
|
||||
tree, and the pool tree are all that need to ship.
|
||||
|
||||
## 2. Install the nginx vhost
|
||||
|
||||
\`\`\`bash
|
||||
scp $OUT/nginx-apt.conf apt.secubox.in:/tmp/
|
||||
ssh apt.secubox.in sudo install -m 0644 /tmp/nginx-apt.conf \\
|
||||
/etc/nginx/sites-available/apt.secubox.in
|
||||
ssh apt.secubox.in sudo ln -sf \\
|
||||
/etc/nginx/sites-available/apt.secubox.in \\
|
||||
/etc/nginx/sites-enabled/apt.secubox.in
|
||||
ssh apt.secubox.in sudo nginx -t
|
||||
ssh apt.secubox.in sudo systemctl reload nginx
|
||||
\`\`\`
|
||||
|
||||
## 3. Issue the TLS certificate
|
||||
|
||||
\`\`\`bash
|
||||
ssh apt.secubox.in sudo certbot --nginx \\
|
||||
-d apt.secubox.in \\
|
||||
--non-interactive --agree-tos \\
|
||||
-m packages@secubox.in
|
||||
\`\`\`
|
||||
|
||||
## 4. Verify
|
||||
|
||||
\`\`\`bash
|
||||
# Cert SAN must include apt.secubox.in (root cause of the previous failure)
|
||||
openssl s_client -servername apt.secubox.in -connect apt.secubox.in:443 \\
|
||||
</dev/null 2>/dev/null \\
|
||||
| openssl x509 -noout -ext subjectAltName
|
||||
|
||||
# Repo install on a clean client
|
||||
curl -fsSL https://apt.secubox.in/install.sh | sudo bash
|
||||
sudo apt-get update
|
||||
\`\`\`
|
||||
|
||||
## Fingerprint
|
||||
|
||||
GPG fingerprint of the publishing key: \`$FPR\`
|
||||
|
||||
The public key is served at:
|
||||
|
||||
https://apt.secubox.in/secubox-keyring.gpg
|
||||
|
||||
License (CMSD-1.0, French authoritative):
|
||||
|
||||
https://apt.secubox.in/LICENCE-CMSD-1.0.md
|
||||
|
||||
EOF
|
||||
|
||||
# Copy install.sh and licenses into the tree
|
||||
install -m 0644 "$REPO/repo/install.sh" "$OUT/install.sh"
|
||||
install -m 0644 "$REPO/LICENCE-CMSD-1.0.md" "$OUT/" 2>/dev/null || \
|
||||
echo "WARN: $REPO/LICENCE-CMSD-1.0.md not found — French license missing" >&2
|
||||
install -m 0644 "$REPO/LICENSE-CMSD-1.0.en.md" "$OUT/" 2>/dev/null || \
|
||||
echo "WARN: $REPO/LICENSE-CMSD-1.0.en.md not found — English license missing" >&2
|
||||
|
||||
echo "Deploy artifacts written under $OUT:"
|
||||
echo " - nginx-apt.conf"
|
||||
echo " - DEPLOY.md"
|
||||
echo " - install.sh"
|
||||
echo " - LICENCE-CMSD-1.0.md (French, authoritative)"
|
||||
echo " - LICENSE-CMSD-1.0.en.md (English, informative)"
|
||||
160
scripts/stage-apt-repo.sh
Executable file
160
scripts/stage-apt-repo.sh
Executable file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/stage-apt-repo.sh
|
||||
# Stage a complete signed APT repo at output/repo/ for amd64 + arm64.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/stage-apt-repo.sh [--tiers "base tier-lite tier-standard tier-pro"]
|
||||
# [--archs "arm64 amd64"]
|
||||
# [--suite bookworm]
|
||||
# [--out output/repo]
|
||||
# [--keep-going]
|
||||
#
|
||||
# Halt-on-tier-failure by default (lower tiers stay published).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(dirname "$SCRIPT_DIR")"
|
||||
# shellcheck source=lib/tier-manifest.sh
|
||||
source "$REPO/scripts/lib/tier-manifest.sh"
|
||||
|
||||
TIERS=(base tier-lite tier-standard tier-pro)
|
||||
ARCHS=(arm64 amd64)
|
||||
SUITE="bookworm"
|
||||
OUT="$REPO/output/repo"
|
||||
KEEP_GOING=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--tiers) IFS=' ' read -ra TIERS <<< "$2"; shift 2 ;;
|
||||
--archs) IFS=' ' read -ra ARCHS <<< "$2"; shift 2 ;;
|
||||
--suite) SUITE="$2"; shift 2 ;;
|
||||
--out) OUT="$2"; shift 2 ;;
|
||||
--keep-going) KEEP_GOING=1; shift ;;
|
||||
*) echo "Unknown flag: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
LOG="$REPO/output/build.log"
|
||||
mkdir -p "$OUT" "$REPO/output/manifests" "$(dirname "$LOG")"
|
||||
: > "$LOG"
|
||||
|
||||
log() { printf '[stage] %s\n' "$*" | tee -a "$LOG" >&2; }
|
||||
fail() { printf '[stage] FAIL %s\n' "$*" | tee -a "$LOG" >&2; exit 1; }
|
||||
|
||||
# ───── 1. GPG bootstrap ─────
|
||||
log "GPG bootstrap → $OUT"
|
||||
bash "$REPO/scripts/stage-gpg-bootstrap.sh" "$OUT" >> "$LOG" 2>&1
|
||||
FPR="$(cat "$OUT/FINGERPRINT.txt")"
|
||||
[[ -z "$FPR" ]] && fail "fingerprint empty"
|
||||
log "GPG fingerprint: $FPR"
|
||||
|
||||
# ───── 2. reprepro conf (production identity) ─────
|
||||
log "Writing reprepro conf"
|
||||
mkdir -p "$OUT/conf"
|
||||
cat > "$OUT/conf/distributions" <<EOF
|
||||
Origin: SecuBox
|
||||
Label: SecuBox
|
||||
Suite: $SUITE
|
||||
Codename: $SUITE
|
||||
Version: 12.0
|
||||
Architectures: arm64 amd64 source
|
||||
Components: main
|
||||
Description: SecuBox Debian packages for Armada/x86_64
|
||||
SignWith: $FPR
|
||||
Contents: percomponent nocompatsymlink
|
||||
EOF
|
||||
|
||||
cat > "$OUT/conf/options" <<EOF
|
||||
verbose
|
||||
basedir $OUT
|
||||
gnupghome ${HOME}/.gnupg/secubox
|
||||
EOF
|
||||
|
||||
# Initialize reprepro DB (idempotent — reprepro export creates dists/ if missing)
|
||||
( cd "$OUT" && reprepro export "$SUITE" ) >> "$LOG" 2>&1 || fail "reprepro export failed"
|
||||
|
||||
# ───── 3. Tier loop ─────
|
||||
declare -A TIER_RESULT
|
||||
for tier in "${TIERS[@]}"; do
|
||||
log "────── Tier: $tier ──────"
|
||||
|
||||
# 3a. Resolve manifest
|
||||
mf="$REPO/output/manifests/${tier}.json"
|
||||
if ! tier_manifest "$tier" "$mf"; then
|
||||
fail "tier-manifest failed for $tier"
|
||||
fi
|
||||
count="$(jq 'length' "$mf")"
|
||||
log " packages: $count"
|
||||
|
||||
tier_failed=0
|
||||
|
||||
# 3b. Build per arch
|
||||
for arch in "${ARCHS[@]}"; do
|
||||
log " build $tier × $arch"
|
||||
if ! bash "$REPO/scripts/build-packages.sh" "$SUITE" "$arch" --filter "$mf" >> "$LOG" 2>&1; then
|
||||
log " build FAILED: $tier × $arch"
|
||||
tier_failed=1
|
||||
[[ $KEEP_GOING -eq 0 ]] && break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $tier_failed -eq 1 ]]; then
|
||||
TIER_RESULT[$tier]="FAILED"
|
||||
[[ $KEEP_GOING -eq 0 ]] && fail "tier $tier failed — halting (lower tiers remain published)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 3c. Publish built debs that belong to this tier
|
||||
pkgs="$(jq -r '.[]' "$mf")"
|
||||
files=()
|
||||
while IFS= read -r p; do
|
||||
[[ -z "$p" ]] && continue
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] && files+=("$f")
|
||||
done < <(ls "$REPO/output/debs/${p}_"*"_arm64.deb" \
|
||||
"$REPO/output/debs/${p}_"*"_amd64.deb" \
|
||||
"$REPO/output/debs/${p}_"*"_all.deb" 2>/dev/null || true)
|
||||
done <<< "$pkgs"
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
log " no .debs to publish for tier $tier (skipping)"
|
||||
TIER_RESULT[$tier]="EMPTY"
|
||||
continue
|
||||
fi
|
||||
|
||||
log " publishing ${#files[@]} files"
|
||||
for f in "${files[@]}"; do
|
||||
( cd "$OUT" && reprepro --silent includedeb "$SUITE" "$f" ) >> "$LOG" 2>&1 || {
|
||||
log " publish FAILED: $f"
|
||||
[[ $KEEP_GOING -eq 0 ]] && fail "publish of $f failed in tier $tier"
|
||||
}
|
||||
done
|
||||
|
||||
# 3d. Check after every tier
|
||||
log " reprepro check"
|
||||
( cd "$OUT" && reprepro check "$SUITE" ) >> "$LOG" 2>&1 || fail "reprepro check failed after tier $tier"
|
||||
|
||||
TIER_RESULT[$tier]="OK"
|
||||
done
|
||||
|
||||
# ───── 4. Manifest summary ─────
|
||||
log "Writing MANIFEST.txt"
|
||||
{
|
||||
echo "SecuBox APT Repo Staging — $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Suite: $SUITE"
|
||||
echo "Fingerprint: $FPR"
|
||||
echo ""
|
||||
echo "Per-tier result:"
|
||||
for t in "${TIERS[@]}"; do
|
||||
printf " %-20s %s\n" "$t" "${TIER_RESULT[$t]:-SKIPPED}"
|
||||
done
|
||||
echo ""
|
||||
echo "Per-arch package count:"
|
||||
for a in arm64 amd64 all; do
|
||||
n=$(find "$OUT/pool" -name "*_${a}.deb" 2>/dev/null | wc -l)
|
||||
printf " %-6s %s\n" "$a" "$n"
|
||||
done
|
||||
} > "$OUT/MANIFEST.txt"
|
||||
|
||||
log "Done. See $OUT/MANIFEST.txt"
|
||||
47
scripts/stage-gpg-bootstrap.sh
Executable file
47
scripts/stage-gpg-bootstrap.sh
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/stage-gpg-bootstrap.sh
|
||||
# Initialize the SecuBox package signing key in ~/.gnupg/secubox/.
|
||||
# Writes:
|
||||
# ~/.gnupg/secubox/ - persistent gnupg home (chmod 700)
|
||||
# $OUT_DIR/secubox-keyring.gpg - public key, ASCII-armored
|
||||
# $OUT_DIR/secubox-keyring.gpg.bin - public key, binary
|
||||
# $OUT_DIR/FINGERPRINT.txt - long fingerprint (no spaces)
|
||||
#
|
||||
# Idempotent: skips key creation if a key for packages@secubox.in already exists.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUT_DIR="${1:-output/repo}"
|
||||
KEY_EMAIL="packages@secubox.in"
|
||||
export GPG_HOME="${HOME}/.gnupg/secubox"
|
||||
export EXPORT_DIR="$OUT_DIR"
|
||||
|
||||
mkdir -p "$OUT_DIR" "$GPG_HOME"
|
||||
chmod 700 "$GPG_HOME"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
bash "$REPO/repo/scripts/generate-gpg-key.sh"
|
||||
|
||||
# The underlying script exits early (without exporting) when the key already
|
||||
# exists. Always export here so the wrapper is idempotent end-to-end.
|
||||
gpg --homedir "$GPG_HOME" --armor --export "$KEY_EMAIL" \
|
||||
> "$OUT_DIR/secubox-keyring.gpg"
|
||||
gpg --homedir "$GPG_HOME" --export "$KEY_EMAIL" \
|
||||
> "$OUT_DIR/secubox-keyring.gpg.bin"
|
||||
|
||||
# Extract long fingerprint (no spaces) — used in reprepro conf/distributions SignWith.
|
||||
gpg --homedir "$GPG_HOME" \
|
||||
--list-keys --with-colons "$KEY_EMAIL" \
|
||||
| awk -F: '/^fpr:/ {print $10; exit}' \
|
||||
> "$OUT_DIR/FINGERPRINT.txt"
|
||||
|
||||
if [[ ! -s "$OUT_DIR/FINGERPRINT.txt" ]]; then
|
||||
echo "ERROR: failed to extract fingerprint" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GPG ready. Fingerprint: $(cat "$OUT_DIR/FINGERPRINT.txt")"
|
||||
echo "Public key (armored): $OUT_DIR/secubox-keyring.gpg"
|
||||
echo "Public key (binary): $OUT_DIR/secubox-keyring.gpg.bin"
|
||||
69
scripts/validate-staged-repo.sh
Executable file
69
scripts/validate-staged-repo.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/validate-staged-repo.sh
|
||||
# Validate output/repo/ end-to-end.
|
||||
# 1. reprepro check
|
||||
# 2. gpg --verify on InRelease
|
||||
# 3. license files match project root byte-for-byte
|
||||
# 4. (best-effort) chroot apt-get update against file:// URL
|
||||
#
|
||||
# Usage: bash scripts/validate-staged-repo.sh [<out_dir>]
|
||||
# SKIP_CHROOT=1 to skip step 4 (e.g., on CI without sudo).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(dirname "$SCRIPT_DIR")"
|
||||
OUT="${1:-$REPO/output/repo}"
|
||||
SUITE="${SUITE:-bookworm}"
|
||||
|
||||
log() { echo "[validate] $*"; }
|
||||
fail() { echo "[validate] FAIL: $*" >&2; exit 1; }
|
||||
|
||||
# 1. reprepro check
|
||||
log "reprepro check"
|
||||
( cd "$OUT" && reprepro check "$SUITE" ) || fail "reprepro check failed"
|
||||
|
||||
# 2. gpg verify
|
||||
log "gpg --verify InRelease"
|
||||
if ! gpg --homedir "$HOME/.gnupg/secubox" --verify "$OUT/dists/$SUITE/InRelease" 2>&1 \
|
||||
| grep -qE "Good signature|Bonne signature"; then
|
||||
fail "InRelease signature did not verify"
|
||||
fi
|
||||
|
||||
# 3. License sanity
|
||||
log "license byte-match"
|
||||
cmp "$REPO/LICENCE-CMSD-1.0.md" "$OUT/LICENCE-CMSD-1.0.md" || fail "French license differs from project root"
|
||||
cmp "$REPO/LICENSE-CMSD-1.0.en.md" "$OUT/LICENSE-CMSD-1.0.en.md" || fail "English license differs from project root"
|
||||
|
||||
# 4. chroot apt-get update (best-effort)
|
||||
CHROOT="$REPO/output/test-chroot"
|
||||
if [[ -z "${SKIP_CHROOT:-}" ]]; then
|
||||
log "chroot apt update (best-effort)"
|
||||
if ! command -v debootstrap >/dev/null; then
|
||||
log " debootstrap missing — skipping chroot test (set SKIP_CHROOT=1 to silence)"
|
||||
else
|
||||
sudo rm -rf "$CHROOT"
|
||||
if ! sudo debootstrap --variant=minbase "$SUITE" "$CHROOT" http://deb.debian.org/debian/ >/dev/null 2>&1; then
|
||||
log " debootstrap failed (network?) — skipping chroot test"
|
||||
else
|
||||
sudo install -d -m 0755 "$CHROOT/etc/apt/sources.list.d" "$CHROOT/usr/share/keyrings"
|
||||
sudo install -m 0644 "$OUT/secubox-keyring.gpg" "$CHROOT/usr/share/keyrings/secubox.gpg"
|
||||
echo "deb [signed-by=/usr/share/keyrings/secubox.gpg] file://$OUT $SUITE main" \
|
||||
| sudo tee "$CHROOT/etc/apt/sources.list.d/secubox.list" >/dev/null
|
||||
sudo mkdir -p "$CHROOT$OUT"
|
||||
sudo mount --bind "$OUT" "$CHROOT$OUT"
|
||||
local_ok=1
|
||||
if ! sudo chroot "$CHROOT" apt-get update 2>&1 | tee "$REPO/output/chroot-update.log" \
|
||||
| grep -qE "Get.*$SUITE|file:.*$SUITE"; then
|
||||
local_ok=0
|
||||
fi
|
||||
sudo umount "$CHROOT$OUT"
|
||||
[[ $local_ok -eq 1 ]] || fail "chroot apt-get update did not see SecuBox repo (see output/chroot-update.log)"
|
||||
log " chroot apt-get update OK"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "chroot test skipped (SKIP_CHROOT set)"
|
||||
fi
|
||||
|
||||
log "All validation checks passed"
|
||||
24
tests/scripts/test-build-filter.sh
Executable file
24
tests/scripts/test-build-filter.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
# shellcheck source=../../scripts/lib/test-helpers.sh
|
||||
source "$REPO/scripts/lib/test-helpers.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
echo '["secubox-core","secubox-hub"]' > "$TMP/manifest.json"
|
||||
|
||||
output="$(bash "$REPO/scripts/build-packages.sh" bookworm amd64 --filter "$TMP/manifest.json" --dry-run 2>&1 || true)"
|
||||
|
||||
assert_contains "$output" "secubox-core" "core should be in dry-run output"
|
||||
assert_contains "$output" "secubox-hub" "hub should be in dry-run output"
|
||||
|
||||
if [[ "$output" == *"secubox-crowdsec"* ]]; then
|
||||
echo "FAIL: crowdsec was NOT filtered out"
|
||||
echo "----- output -----"
|
||||
echo "$output"
|
||||
echo "------------------"
|
||||
exit 1
|
||||
fi
|
||||
pass "filter restricts package set"
|
||||
31
tests/scripts/test-tier-manifest.sh
Executable file
31
tests/scripts/test-tier-manifest.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
source "$REPO/scripts/lib/test-helpers.sh"
|
||||
source "$REPO/scripts/lib/tier-manifest.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# base = hardcoded
|
||||
tier_manifest "base" "$TMP/base.json"
|
||||
assert_file "$TMP/base.json" "base manifest"
|
||||
got="$(jq -r '. | sort | .[]' "$TMP/base.json" | tr '\n' ' ')"
|
||||
assert_eq "secubox-core secubox-hub " "$got" "base packages"
|
||||
|
||||
# tier-lite must produce a manifest with >=3 packages and include secubox-core (via inheritance)
|
||||
tier_manifest "tier-lite" "$TMP/lite.json"
|
||||
assert_file "$TMP/lite.json" "lite manifest"
|
||||
count="$(jq 'length' "$TMP/lite.json")"
|
||||
if [[ "$count" -lt 3 ]]; then
|
||||
echo "FAIL: tier-lite has only $count packages, expected >=3"
|
||||
cat "$TMP/lite.json"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '. | index("secubox-core")' "$TMP/lite.json" >/dev/null; then
|
||||
echo "FAIL: tier-lite missing secubox-core (inheritance broken)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass "tier-manifest base + tier-lite"
|
||||
374
tests/test_opad_schema.py
Normal file
374
tests/test_opad_schema.py
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
"""
|
||||
SecuBox-Deb :: OPAD Schema Tests
|
||||
CyberMind — https://cybermind.fr
|
||||
Author: Gérald Kerma <gandalf@gk2.net>
|
||||
License: Proprietary / ANSSI CSPN candidate
|
||||
|
||||
Test suite for OPAD v2.4.0 JSON Schema and Pydantic models.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft7Validator, ValidationError
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
|
||||
# Import opad models directly to avoid loading secubox_core dependencies
|
||||
opad_models_path = Path(__file__).parent.parent / "common" / "secubox_core" / "opad"
|
||||
sys.path.insert(0, str(opad_models_path))
|
||||
|
||||
from models import (
|
||||
OPADProfile,
|
||||
OPADMode,
|
||||
PolicyAction,
|
||||
DefaultAction,
|
||||
LogLevel,
|
||||
ObservationConfig,
|
||||
ProtocolsConfig,
|
||||
PolicyConfig,
|
||||
PolicyRule,
|
||||
RuleMatch,
|
||||
EscalationConfig,
|
||||
)
|
||||
|
||||
# Load JSON Schema
|
||||
SCHEMA_PATH = Path(__file__).parent.parent / "schemas" / "opad-profile.schema.json"
|
||||
with open(SCHEMA_PATH) as f:
|
||||
JSON_SCHEMA = json.load(f)
|
||||
|
||||
|
||||
# ==================== JSON SCHEMA VALIDATION TESTS ====================
|
||||
|
||||
def test_schema_is_valid_draft7():
|
||||
"""Verify the JSON Schema itself is valid Draft 7."""
|
||||
Draft7Validator.check_schema(JSON_SCHEMA)
|
||||
|
||||
|
||||
def test_minimal_profile_validates():
|
||||
"""Test minimal valid OPAD profile against JSON Schema."""
|
||||
profile = {
|
||||
"version": "2.4.0",
|
||||
"mode": "observe",
|
||||
"observation": {
|
||||
"interfaces": ["eth0"],
|
||||
"protocols": {}
|
||||
},
|
||||
"injection": {},
|
||||
"policy": {
|
||||
"default_action": "observe"
|
||||
}
|
||||
}
|
||||
validator = Draft7Validator(JSON_SCHEMA)
|
||||
validator.validate(profile)
|
||||
|
||||
|
||||
def test_full_profile_validates():
|
||||
"""Test full OPAD profile with all features enabled."""
|
||||
profile = {
|
||||
"version": "2.4.0",
|
||||
"mode": "enforce",
|
||||
"observation": {
|
||||
"interfaces": ["eth0", "eth1"],
|
||||
"bpf_filter": "tcp port 443",
|
||||
"protocols": {
|
||||
"dns": True,
|
||||
"dhcp": True,
|
||||
"arp": True,
|
||||
"tcp": True
|
||||
},
|
||||
"fingerprinting": {
|
||||
"dhcp_fingerprint": True,
|
||||
"ja3": True,
|
||||
"ja4": True,
|
||||
"user_agent": True
|
||||
}
|
||||
},
|
||||
"injection": {
|
||||
"dns_race": {
|
||||
"enabled": True,
|
||||
"target_success_rate": 0.99,
|
||||
"modes": ["nxdomain", "sinkhole"],
|
||||
"sinkhole_ip": "127.0.0.1",
|
||||
"ttl": 60
|
||||
},
|
||||
"dhcp_race": {
|
||||
"enabled": True,
|
||||
"target_success_rate": 0.95,
|
||||
"quarantine_pool": {
|
||||
"start": "10.99.0.1",
|
||||
"end": "10.99.0.254",
|
||||
"lease_time": 300,
|
||||
"gateway": "10.99.0.254"
|
||||
}
|
||||
},
|
||||
"rst_inject": {
|
||||
"enabled": True,
|
||||
"target_success_rate": 0.90,
|
||||
"double_ended": True,
|
||||
"timing_window_ms": 10
|
||||
},
|
||||
"arp_redirect": {
|
||||
"enabled": True,
|
||||
"target_success_rate": 0.98,
|
||||
"refresh_interval_s": 60,
|
||||
"captive_mac": "AA:BB:CC:DD:EE:FF"
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"default_action": "observe",
|
||||
"rules": [
|
||||
{
|
||||
"id": "block-malware",
|
||||
"priority": 10,
|
||||
"match": {
|
||||
"dest_domain": "*.malware.com",
|
||||
"protocol": "tcp"
|
||||
},
|
||||
"action": "dns_nxdomain",
|
||||
"log_level": "warning"
|
||||
}
|
||||
],
|
||||
"escalation": {
|
||||
"allow_in_path": True,
|
||||
"require_explicit_consent": True,
|
||||
"auto_revert_after_s": 300,
|
||||
"audit_all_escalations": True
|
||||
}
|
||||
}
|
||||
}
|
||||
validator = Draft7Validator(JSON_SCHEMA)
|
||||
validator.validate(profile)
|
||||
|
||||
|
||||
def test_missing_interfaces_fails():
|
||||
"""Test that missing interfaces field fails validation."""
|
||||
profile = {
|
||||
"version": "2.4.0",
|
||||
"mode": "observe",
|
||||
"observation": {
|
||||
"protocols": {}
|
||||
},
|
||||
"injection": {},
|
||||
"policy": {
|
||||
"default_action": "observe"
|
||||
}
|
||||
}
|
||||
validator = Draft7Validator(JSON_SCHEMA)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(profile)
|
||||
|
||||
|
||||
def test_invalid_mode_fails():
|
||||
"""Test that invalid mode fails validation."""
|
||||
profile = {
|
||||
"version": "2.4.0",
|
||||
"mode": "invalid_mode",
|
||||
"observation": {
|
||||
"interfaces": ["eth0"],
|
||||
"protocols": {}
|
||||
},
|
||||
"injection": {},
|
||||
"policy": {
|
||||
"default_action": "observe"
|
||||
}
|
||||
}
|
||||
validator = Draft7Validator(JSON_SCHEMA)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(profile)
|
||||
|
||||
|
||||
# ==================== PYDANTIC MODEL TESTS ====================
|
||||
|
||||
def test_minimal_profile_pydantic():
|
||||
"""Test minimal valid OPAD profile with Pydantic."""
|
||||
profile = OPADProfile(
|
||||
version="2.4.0",
|
||||
mode=OPADMode.OBSERVE,
|
||||
observation=ObservationConfig(
|
||||
interfaces=["eth0"]
|
||||
),
|
||||
injection={},
|
||||
policy=PolicyConfig(
|
||||
default_action=DefaultAction.OBSERVE
|
||||
)
|
||||
)
|
||||
assert profile.version == "2.4.0"
|
||||
assert profile.mode == OPADMode.OBSERVE
|
||||
assert profile.observation.interfaces == ["eth0"]
|
||||
assert profile.policy.default_action == DefaultAction.OBSERVE
|
||||
|
||||
|
||||
def test_full_profile_pydantic():
|
||||
"""Test full OPAD profile with Pydantic."""
|
||||
profile = OPADProfile(
|
||||
version="2.4.0",
|
||||
mode=OPADMode.ENFORCE,
|
||||
observation=ObservationConfig(
|
||||
interfaces=["eth0", "eth1"],
|
||||
bpf_filter="tcp port 443",
|
||||
protocols=ProtocolsConfig(dns=True, dhcp=True, arp=True, tcp=True)
|
||||
),
|
||||
injection={
|
||||
"dns_race": {
|
||||
"enabled": True,
|
||||
"target_success_rate": 0.99
|
||||
}
|
||||
},
|
||||
policy=PolicyConfig(
|
||||
default_action=DefaultAction.OBSERVE,
|
||||
rules=[
|
||||
PolicyRule(
|
||||
id="block-malware",
|
||||
priority=10,
|
||||
action=PolicyAction.DNS_NXDOMAIN,
|
||||
match=RuleMatch(dest_domain="*.malware.com")
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
assert len(profile.observation.interfaces) == 2
|
||||
assert profile.injection.dns_race.enabled is True
|
||||
assert len(profile.policy.rules) == 1
|
||||
|
||||
|
||||
def test_invalid_mac_fails():
|
||||
"""Test that invalid MAC address fails Pydantic validation."""
|
||||
with pytest.raises(PydanticValidationError):
|
||||
RuleMatch(source_mac="invalid:mac:address")
|
||||
|
||||
|
||||
def test_invalid_success_rate_fails():
|
||||
"""Test that out-of-range success rate fails validation."""
|
||||
from models import DNSRaceConfig
|
||||
|
||||
# Too low (below 0.9)
|
||||
with pytest.raises(PydanticValidationError):
|
||||
DNSRaceConfig(target_success_rate=0.85)
|
||||
|
||||
# Too high (above 1.0)
|
||||
with pytest.raises(PydanticValidationError):
|
||||
DNSRaceConfig(target_success_rate=1.5)
|
||||
|
||||
|
||||
def test_policy_rule_creation():
|
||||
"""Test PolicyRule creation with various configurations."""
|
||||
rule = PolicyRule(
|
||||
id="test-rule",
|
||||
priority=100,
|
||||
action=PolicyAction.OBSERVE,
|
||||
match=RuleMatch(
|
||||
source_mac="AA:BB:CC:DD:EE:FF",
|
||||
dest_domain="example.com",
|
||||
protocol="tcp"
|
||||
),
|
||||
log_level=LogLevel.INFO
|
||||
)
|
||||
assert rule.id == "test-rule"
|
||||
assert rule.priority == 100
|
||||
assert rule.action == PolicyAction.OBSERVE
|
||||
assert rule.match.source_mac == "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
|
||||
def test_export_json_schema():
|
||||
"""Test that Pydantic models can export JSON Schema."""
|
||||
schema = OPADProfile.model_json_schema()
|
||||
assert schema["title"] == "OPADProfile"
|
||||
assert "properties" in schema
|
||||
assert "version" in schema["properties"]
|
||||
assert "mode" in schema["properties"]
|
||||
|
||||
|
||||
def test_pydantic_validates_same_as_jsonschema():
|
||||
"""Test that Pydantic and JSON Schema validation are equivalent."""
|
||||
profile_dict = {
|
||||
"version": "2.4.0",
|
||||
"mode": "observe",
|
||||
"observation": {
|
||||
"interfaces": ["eth0"],
|
||||
"protocols": {}
|
||||
},
|
||||
"injection": {},
|
||||
"policy": {
|
||||
"default_action": "observe"
|
||||
}
|
||||
}
|
||||
|
||||
# Should validate with JSON Schema
|
||||
validator = Draft7Validator(JSON_SCHEMA)
|
||||
validator.validate(profile_dict)
|
||||
|
||||
# Should also validate with Pydantic
|
||||
profile = OPADProfile(**profile_dict)
|
||||
assert profile.version == "2.4.0"
|
||||
|
||||
|
||||
def test_default_mode_is_observe():
|
||||
"""Test that creating a profile defaults to observe mode when appropriate."""
|
||||
profile = OPADProfile(
|
||||
version="2.4.0",
|
||||
mode=OPADMode.OBSERVE,
|
||||
observation=ObservationConfig(interfaces=["eth0"]),
|
||||
injection={},
|
||||
policy=PolicyConfig(default_action=DefaultAction.OBSERVE)
|
||||
)
|
||||
assert profile.mode == OPADMode.OBSERVE
|
||||
|
||||
|
||||
def test_escalation_requires_consent_by_default():
|
||||
"""Test that escalation requires consent by default."""
|
||||
escalation = EscalationConfig()
|
||||
assert escalation.require_explicit_consent is True
|
||||
assert escalation.audit_all_escalations is True
|
||||
assert escalation.allow_in_path is True
|
||||
|
||||
|
||||
def test_invalid_interface_name_fails():
|
||||
"""Test that invalid interface names fail validation."""
|
||||
with pytest.raises(PydanticValidationError):
|
||||
ObservationConfig(interfaces=["eth0-invalid!"])
|
||||
|
||||
|
||||
def test_rule_priority_range():
|
||||
"""Test that rule priority must be within valid range."""
|
||||
# Valid priority
|
||||
rule = PolicyRule(
|
||||
id="test",
|
||||
priority=5000,
|
||||
action=PolicyAction.ALLOW
|
||||
)
|
||||
assert rule.priority == 5000
|
||||
|
||||
# Invalid priority (too high)
|
||||
with pytest.raises(PydanticValidationError):
|
||||
PolicyRule(
|
||||
id="test",
|
||||
priority=10000,
|
||||
action=PolicyAction.ALLOW
|
||||
)
|
||||
|
||||
|
||||
def test_empty_interfaces_fails():
|
||||
"""Test that empty interfaces list fails validation."""
|
||||
with pytest.raises(PydanticValidationError):
|
||||
ObservationConfig(interfaces=[])
|
||||
|
||||
|
||||
def test_mac_address_format_variations():
|
||||
"""Test that only valid MAC address formats are accepted."""
|
||||
# Valid MAC
|
||||
match = RuleMatch(source_mac="01:23:45:67:89:AB")
|
||||
assert match.source_mac == "01:23:45:67:89:AB"
|
||||
|
||||
# Invalid formats
|
||||
invalid_macs = [
|
||||
"01:23:45:67:89", # Too short
|
||||
"01:23:45:67:89:AB:CD", # Too long
|
||||
"01-23-45-67-89-AB", # Wrong separator
|
||||
"0123456789AB", # No separator
|
||||
"ZZ:ZZ:ZZ:ZZ:ZZ:ZZ", # Invalid hex
|
||||
]
|
||||
for invalid_mac in invalid_macs:
|
||||
with pytest.raises(PydanticValidationError):
|
||||
RuleMatch(source_mac=invalid_mac)
|
||||
Loading…
Reference in New Issue
Block a user