mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 19:16:07 +00:00
Compare commits
3 Commits
f5c7f6f6b5
...
d282c571bf
| Author | SHA1 | Date | |
|---|---|---|---|
| d282c571bf | |||
| 1e8a41c33b | |||
| afde96111a |
|
|
@ -273,7 +273,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchTopDomains() {
|
async function fetchTopDomains() {
|
||||||
const data = await api('/blocklist/top?limit=15');
|
const data = await api('/blocklist/top?limit=5');
|
||||||
const tbody = document.getElementById('top-domains-table');
|
const tbody = document.getElementById('top-domains-table');
|
||||||
if (!data?.domains?.length) {
|
if (!data?.domains?.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-dim);">No blocked domains yet</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-dim);">No blocked domains yet</td></tr>';
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,29 @@ def write_cache(data: dict):
|
||||||
|
|
||||||
def build_overview() -> dict:
|
def build_overview() -> dict:
|
||||||
"""Build system overview metrics."""
|
"""Build system overview metrics."""
|
||||||
|
# CPU usage
|
||||||
|
try:
|
||||||
|
with open('/proc/stat') as f:
|
||||||
|
# Read first line (cpu total)
|
||||||
|
cpu_line = f.readline()
|
||||||
|
if cpu_line.startswith('cpu '):
|
||||||
|
# Parse CPU times (user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice)
|
||||||
|
parts = cpu_line.split()
|
||||||
|
if len(parts) >= 5:
|
||||||
|
# Calculate usage: (total - idle) / total * 100
|
||||||
|
total = sum(int(x) for x in parts[1:]) # Sum all time values
|
||||||
|
idle = int(parts[4]) # idle time is 5th column (index 4)
|
||||||
|
if total > 0:
|
||||||
|
cpu_pct = ((total - idle) / total) * 100
|
||||||
|
else:
|
||||||
|
cpu_pct = 0
|
||||||
|
else:
|
||||||
|
cpu_pct = 0
|
||||||
|
else:
|
||||||
|
cpu_pct = 0
|
||||||
|
except Exception:
|
||||||
|
cpu_pct = 0
|
||||||
|
|
||||||
# Uptime
|
# Uptime
|
||||||
try:
|
try:
|
||||||
with open('/proc/uptime') as f:
|
with open('/proc/uptime') as f:
|
||||||
|
|
@ -246,6 +269,7 @@ def build_overview() -> dict:
|
||||||
return {
|
return {
|
||||||
"uptime": uptime,
|
"uptime": uptime,
|
||||||
"load": load,
|
"load": load,
|
||||||
|
"cpu_pct": cpu_pct,
|
||||||
"mem_total_kb": mem_total,
|
"mem_total_kb": mem_total,
|
||||||
"mem_used_kb": mem_used,
|
"mem_used_kb": mem_used,
|
||||||
"mem_pct": mem_pct,
|
"mem_pct": mem_pct,
|
||||||
|
|
@ -394,6 +418,24 @@ async def get_overview(auth: None = Depends(require_jwt)):
|
||||||
data["_freshness"] = get_freshness()
|
data["_freshness"] = get_freshness()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@app.get("/api/v1/metrics/summary")
|
||||||
|
async def get_metrics_summary(auth: None = Depends(require_jwt)):
|
||||||
|
"""Get metrics summary for top navbar - returns cpu, mem, load in expected format."""
|
||||||
|
cached = read_cache()
|
||||||
|
if cached and cache_is_fresh():
|
||||||
|
data = cached.get("overview", build_overview())
|
||||||
|
else:
|
||||||
|
data = build_overview()
|
||||||
|
|
||||||
|
# Map overview data to format expected by sidebar.js for /metrics/ page
|
||||||
|
# sidebar.js expects: cpu, mem, load
|
||||||
|
return {
|
||||||
|
"cpu": data.get("cpu_pct", 0),
|
||||||
|
"mem": data.get("mem_pct", 0),
|
||||||
|
"load": data.get("load", "0 0 0"),
|
||||||
|
"_freshness": get_freshness()
|
||||||
|
}
|
||||||
|
|
||||||
@app.get("/api/v1/metrics/waf_stats")
|
@app.get("/api/v1/metrics/waf_stats")
|
||||||
async def get_waf_stats(auth: None = Depends(require_jwt)):
|
async def get_waf_stats(auth: None = Depends(require_jwt)):
|
||||||
"""Get WAF/CrowdSec statistics."""
|
"""Get WAF/CrowdSec statistics."""
|
||||||
|
|
|
||||||
467
packages/secubox-security-posture/README.md
Normal file
467
packages/secubox-security-posture/README.md
Normal file
|
|
@ -0,0 +1,467 @@
|
||||||
|
# SecuBox Security Posture
|
||||||
|
|
||||||
|
**DEFCON-Level Security Health & CSPN/TPN Media Compliance Monitoring**
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `secubox-security-posture` module provides comprehensive security health monitoring, compliance checking, and performance analysis for SecuBox, aligned with **ANSSI CSPN** and **TPN Media** certification requirements.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
1. **DEFCON-Level Security Scoring**
|
||||||
|
- Military-standard DEFCON levels (1-5)
|
||||||
|
- Real-time security score (0-100)
|
||||||
|
- Per-category scoring (network, threat, access, data, resilience)
|
||||||
|
- Visual indicators (colors, emojis)
|
||||||
|
|
||||||
|
2. **CSPN Compliance**
|
||||||
|
- Automated checks for ANSSI CSPN test matrix
|
||||||
|
- Traceability to CSPN requirements
|
||||||
|
- Compliance reporting for auditors
|
||||||
|
- Certificate readiness assessment
|
||||||
|
|
||||||
|
3. **TPN Media Compliance**
|
||||||
|
- Media industry-specific requirements
|
||||||
|
- Content protection and anti-piracy
|
||||||
|
- DRM integration verification
|
||||||
|
- Partner trust validation
|
||||||
|
|
||||||
|
4. **Performance Monitoring**
|
||||||
|
- System resource monitoring (CPU, memory, disk, network)
|
||||||
|
- Service performance metrics (WAF, CrowdSec, mitmproxy, etc.)
|
||||||
|
- Bottleneck detection and analysis
|
||||||
|
- Optimization recommendations
|
||||||
|
|
||||||
|
5. **Combined Dashboard**
|
||||||
|
- Unified security posture overview
|
||||||
|
- Integrated DEFCON + CSPN + TPN + Performance
|
||||||
|
- Actionable recommendations
|
||||||
|
|
||||||
|
## DEFCON Levels
|
||||||
|
|
||||||
|
| DEFCON | Level | Color | Description | Score Range |
|
||||||
|
|--------|-------|-------|-------------|-------------|
|
||||||
|
| DEFCON 5 | Normal | 🟢 Green | All systems operational | 90-100% |
|
||||||
|
| DEFCON 4 | Increased Chatter | 🟡 Yellow | Minor issues detected | 70-89% |
|
||||||
|
| DEFCON 3 | Heightened | 🟠 Orange | Active threats being mitigated | 50-69% |
|
||||||
|
| DEFCON 2 | Severe | 🔴 Red | Major incident in progress | 30-49% |
|
||||||
|
| DEFCON 1 | Maximum | 🔴 Flashing | Critical breach detected | 0-29% |
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
All endpoints are available at `/api/v1/security-posture/` via unix socket.
|
||||||
|
|
||||||
|
### DEFCON Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/defcon` | Full DEFCON info with all indicators |
|
||||||
|
| GET | `/defcon/summary` | Lightweight DEFCON summary |
|
||||||
|
| GET | `/defcon/indicators` | All DEFCON indicators with values |
|
||||||
|
| GET | `/defcon/category/{category}` | Indicators for specific category |
|
||||||
|
|
||||||
|
### CSPN Compliance Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/cspn` | Full CSPN compliance report |
|
||||||
|
| GET | `/cspn/summary` | CSPN compliance summary |
|
||||||
|
| GET | `/cspn/requirements` | All CSPN requirements |
|
||||||
|
| GET | `/cspn/requirements/{req_id}` | Specific CSPN requirement |
|
||||||
|
| GET | `/cspn/certificate/readiness` | CSPN certificate readiness |
|
||||||
|
|
||||||
|
### TPN Media Compliance Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/tpn` | Full TPN Media compliance report |
|
||||||
|
| GET | `/tpn/summary` | TPN compliance summary |
|
||||||
|
| GET | `/tpn/requirements` | All TPN requirements |
|
||||||
|
| GET | `/tpn/requirements/{req_id}` | Specific TPN requirement |
|
||||||
|
| GET | `/tpn/certificate/readiness` | TPN certificate readiness |
|
||||||
|
|
||||||
|
### Performance Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/performance` | All performance metrics |
|
||||||
|
| GET | `/performance/summary` | Performance summary |
|
||||||
|
| GET | `/performance/bottlenecks` | Detected bottlenecks |
|
||||||
|
| GET | `/performance/recommendations` | Optimization recommendations |
|
||||||
|
| GET | `/performance/history` | Performance history (default 24h) |
|
||||||
|
| GET | `/performance/history?hours={n}` | Performance history for N hours |
|
||||||
|
|
||||||
|
### Combined Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/overview` | Combined security posture overview |
|
||||||
|
| GET | `/health` | Service health check |
|
||||||
|
| POST | `/checks/run` | Run all compliance checks (background) |
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### Check DEFCON Level
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --unix-socket /run/secubox/security-posture.sock \
|
||||||
|
http://localhost/api/v1/security-posture/defcon | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "defcon_3",
|
||||||
|
"level_name": "Defcon 3",
|
||||||
|
"score": 74.5,
|
||||||
|
"score_int": 74,
|
||||||
|
"color": "#f97316",
|
||||||
|
"emoji": "🟠",
|
||||||
|
"blink": false,
|
||||||
|
"description": "🟠 HEIGHTENED • Active threats being detected and mitigated • Security Score: 74.5/100 • Active monitoring required",
|
||||||
|
"recommendations": [
|
||||||
|
"🟠 Review active threat alerts in Threat Analyst",
|
||||||
|
"🟠 Verify WAF rules are up to date",
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"category_scores": {
|
||||||
|
"network": 85.0,
|
||||||
|
"threat": 70.0,
|
||||||
|
"access": 90.0,
|
||||||
|
"data": 80.0,
|
||||||
|
"resilience": 95.0
|
||||||
|
},
|
||||||
|
"cspn_compliance": {
|
||||||
|
"status": "compliant",
|
||||||
|
"score": 74.5,
|
||||||
|
"threshold": 70,
|
||||||
|
"warnings": [],
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"tpn_compliance": {
|
||||||
|
"status": "non_compliant",
|
||||||
|
"score": 74.5,
|
||||||
|
"threshold": 85,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check CSPN Compliance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --unix-socket /run/secubox/security-posture.sock \
|
||||||
|
http://localhost/api/v1/security-posture/cspn/summary | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-16T14:30:00Z",
|
||||||
|
"summary": {
|
||||||
|
"total_requirements": 24,
|
||||||
|
"total_weight": 120,
|
||||||
|
"passed": 108,
|
||||||
|
"failed": 0,
|
||||||
|
"warnings": 6,
|
||||||
|
"skipped": 6,
|
||||||
|
"pass_percentage": 90.0,
|
||||||
|
"fail_percentage": 0.0,
|
||||||
|
"warning_percentage": 5.0,
|
||||||
|
"compliance_score": 90.0,
|
||||||
|
"is_compliant": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Performance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --unix-socket /run/secubox/security-posture.sock \
|
||||||
|
http://localhost/api/v1/security-posture/performance/summary | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-16T14:30:00Z",
|
||||||
|
"score": 82.5,
|
||||||
|
"status": "good",
|
||||||
|
"color": "#86efac",
|
||||||
|
"emoji": "🟢",
|
||||||
|
"category_scores": {
|
||||||
|
"cpu": 90.0,
|
||||||
|
"memory": 75.0,
|
||||||
|
"disk": 85.0,
|
||||||
|
"network": 95.0,
|
||||||
|
"io": 80.0,
|
||||||
|
"service": 80.0
|
||||||
|
},
|
||||||
|
"bottlenecks": 1,
|
||||||
|
"critical_bottlenecks": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Combined Overview
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --unix-socket /run/secubox/security-posture.sock \
|
||||||
|
http://localhost/api/v1/security-posture/overview | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-16T14:30:00Z",
|
||||||
|
"combined_score": 85.3,
|
||||||
|
"overall_status": "good",
|
||||||
|
"overall_color": "#86efac",
|
||||||
|
"overall_emoji": "🟢",
|
||||||
|
"defcon": {
|
||||||
|
"level": "defcon_3",
|
||||||
|
"level_name": "Defcon 3",
|
||||||
|
"score": 74.5,
|
||||||
|
"color": "#f97316",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"cspn": {
|
||||||
|
"compliant": true,
|
||||||
|
"score": 90.0
|
||||||
|
},
|
||||||
|
"tpn_media": {
|
||||||
|
"compliant": false,
|
||||||
|
"score": 75.0
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"score": 82.5,
|
||||||
|
"status": "good",
|
||||||
|
"bottlenecks": 1
|
||||||
|
},
|
||||||
|
"recommendations": [
|
||||||
|
{"source": "defcon", "severity": "medium", "text": "..."},
|
||||||
|
{"source": "tpn", "severity": "high", "text": "TPN Media compliance score 75.0%..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/secubox-security-posture/
|
||||||
|
├── api/
|
||||||
|
│ ├── __init__.py # Package init
|
||||||
|
│ ├── main.py # FastAPI endpoints
|
||||||
|
│ ├── defcon.py # DEFCON calculator
|
||||||
|
│ ├── cspn_compliance.py # CSPN compliance checker
|
||||||
|
│ ├── tpn_compliance.py # TPN Media compliance checker
|
||||||
|
│ └── performance.py # Performance monitor
|
||||||
|
├── debian/
|
||||||
|
│ ├── changelog
|
||||||
|
│ ├── control
|
||||||
|
│ ├── rules
|
||||||
|
│ ├── source/
|
||||||
|
│ │ └── format
|
||||||
|
│ ├── secubox-security-posture.service
|
||||||
|
│ └── secubox-security-posture.sock
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ SecuBox Security Posture │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ DEFCON │ │ CSPN │ │ TPN Media │ │
|
||||||
|
│ │ Engine │ │ Checker │ │ Checker │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │• 12 metrics │ │• 24 reqs │ │• 20 reqs │ │
|
||||||
|
│ │• Score 0-100│ │• Auto check │ │• Auto check │ │
|
||||||
|
│ │• DEFCON 1-5 │ │• CSPN align │ │• Media spec │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────────┼───────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────▼────────┐ │
|
||||||
|
│ │ Performance │ │
|
||||||
|
│ │ Monitor │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │• 15 metrics │ │
|
||||||
|
│ │• Bottleneck det. │ │
|
||||||
|
│ │• Recommendations │ │
|
||||||
|
│ └────────┬────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────▼────────┐ │
|
||||||
|
│ │ FastAPI │ │
|
||||||
|
│ │ Endpoints │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │• /defcon │ │
|
||||||
|
│ │• /cspn │ │
|
||||||
|
│ │• /tpn │ │
|
||||||
|
│ │• /performance │ │
|
||||||
|
│ │• /overview │ │
|
||||||
|
│ └─────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Sources │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ • WAF (mitmproxy) • CrowdSec • Health Doctor │
|
||||||
|
│ • nftables • System • HAProxy │
|
||||||
|
│ • Nginx • psutil • Threat Analyst │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
### With SecuBox Aggregator
|
||||||
|
|
||||||
|
To integrate with the SecuBox aggregator, add `security-posture` to the modules list in `/etc/secubox/aggregator.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[modules]
|
||||||
|
modules = [
|
||||||
|
"hub",
|
||||||
|
"threat-analyst",
|
||||||
|
"health-doctor",
|
||||||
|
"security-posture",
|
||||||
|
# ... other modules
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### With nginx
|
||||||
|
|
||||||
|
The module provides a unix socket at `/run/secubox/security-posture.sock`. To expose via nginx:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location /api/v1/security-posture/ {
|
||||||
|
proxy_pass http://unix:/run/secubox/security-posture.sock:
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# For WebSocket support
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Build Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/secubox-security-posture
|
||||||
|
dpkg-buildpackage -us -uc -b
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install ../secubox-security-posture_1.0.0-1_all.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable secubox-security-posture.service
|
||||||
|
sudo systemctl start secubox-security-posture.service
|
||||||
|
sudo systemctl status secubox-security-posture.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable Socket
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable secubox-security-posture.sock
|
||||||
|
sudo systemctl start secubox-security-posture.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Run Standalone (Testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/secubox-security-posture
|
||||||
|
python3 -m uvicorn api.main:app --reload --port 8082
|
||||||
|
```
|
||||||
|
|
||||||
|
Then access at `http://localhost:8082/api/v1/security-posture/`
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/secubox-security-posture
|
||||||
|
python3 -m pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## DEFCON Indicator Weights
|
||||||
|
|
||||||
|
| Category | Weight | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| Network | 30% | WAF, Firewall, nftables |
|
||||||
|
| Threat | 25% | CrowdSec, DPI, detection |
|
||||||
|
| Access | 20% | Auth, privilege, ACL |
|
||||||
|
| Data | 15% | Encryption, integrity |
|
||||||
|
| Resilience | 10% | Uptime, backups |
|
||||||
|
|
||||||
|
## Performance Metric Weights
|
||||||
|
|
||||||
|
| Category | Weight | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| CPU | 25% | CPU usage metrics |
|
||||||
|
| Memory | 25% | Memory usage metrics |
|
||||||
|
| Disk | 15% | Disk usage metrics |
|
||||||
|
| Network | 10% | Network bandwidth |
|
||||||
|
| I/O | 10% | Disk I/O |
|
||||||
|
| Service | 15% | Service latency/throughput |
|
||||||
|
|
||||||
|
## CSPN Compliance Thresholds
|
||||||
|
|
||||||
|
| Requirement Type | Threshold |
|
||||||
|
|------------------|-----------|
|
||||||
|
| Overall Score | ≥ 70% |
|
||||||
|
| Network Category | ≥ 80% |
|
||||||
|
| Threat Category | ≥ 70% |
|
||||||
|
| Access Category | ≥ 80% |
|
||||||
|
| Data Category | ≥ 80% |
|
||||||
|
| Resilience Category | ≥ 70% |
|
||||||
|
|
||||||
|
## TPN Media Compliance Thresholds
|
||||||
|
|
||||||
|
| Requirement Type | Threshold |
|
||||||
|
|------------------|-----------|
|
||||||
|
| Overall Score | ≥ 85% |
|
||||||
|
| All Categories | ≥ 80% (varies by category) |
|
||||||
|
| CSPN Compliance | Must pass |
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- All endpoints are accessible via **unix socket only** (no HTTP exposure)
|
||||||
|
- Socket permissions: `0660 root:secubox`
|
||||||
|
- No authentication required (security via socket permissions)
|
||||||
|
- Service runs as `secubox` user (non-root)
|
||||||
|
- AppArmor profile should be configured for additional protection
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This module is part of SecuBox and is licensed under the **CyberMind Source-Disclosed License v1.0 (CMSD-1.0)**.
|
||||||
|
|
||||||
|
See [LICENCE-CMSD-1.0.md](../../../LICENCE-CMSD-1.0.md) for details.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ANSSI CSPN Certification](https://www.ssi.gouv.fr/en/certification/cspn/)
|
||||||
|
- [TPN Media Security Requirements](https://www.trustedpartnernetwork.com/)
|
||||||
|
- [DEFCON Levels (Wikipedia)](https://en.wikipedia.org/wiki/DEFCON)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>**
|
||||||
5
packages/secubox-security-posture/__init__.py
Normal file
5
packages/secubox-security-posture/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""SecuBox Security Posture — DEFCON & Compliance Monitoring."""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
5
packages/secubox-security-posture/api/__init__.py
Normal file
5
packages/secubox-security-posture/api/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""SecuBox Security Posture — DEFCON & Compliance Monitoring."""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
1098
packages/secubox-security-posture/api/cspn_compliance.py
Normal file
1098
packages/secubox-security-posture/api/cspn_compliance.py
Normal file
File diff suppressed because it is too large
Load Diff
976
packages/secubox-security-posture/api/defcon.py
Normal file
976
packages/secubox-security-posture/api/defcon.py
Normal file
|
|
@ -0,0 +1,976 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""
|
||||||
|
DEFCON Level Calculator for SecuBox - CSPN/TPN Media Compliant
|
||||||
|
|
||||||
|
Provides real-time security posture assessment with military-standard DEFCON levels
|
||||||
|
mapped to ANSSI CSPN requirements and TPN Media compliance.
|
||||||
|
|
||||||
|
DEFCON Levels:
|
||||||
|
DEFCON 5 (Normal) - All systems operational, score 90-100%
|
||||||
|
DEFCON 4 (Chatter) - Minor issues, score 70-89%
|
||||||
|
DEFCON 3 (Heightened)- Active threats, score 50-69%
|
||||||
|
DEFCON 2 (Severe) - Major incident, score 30-49%
|
||||||
|
DEFCON 1 (Maximum) - Critical breach, score 0-29%
|
||||||
|
|
||||||
|
CSPN Alignment:
|
||||||
|
Maps to docs/cspn/CSPN-TEST-MATRIX.md requirements
|
||||||
|
Maps to doctrine/opad/CSPN.matrix.md capabilities
|
||||||
|
|
||||||
|
TPN Media Alignment:
|
||||||
|
Maps to TPN (Trusted Partner Network) Media security requirements
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger("secubox.security-posture.defcon")
|
||||||
|
|
||||||
|
|
||||||
|
class DefconLevel(str, Enum):
|
||||||
|
"""DEFCON levels with military standard mapping."""
|
||||||
|
DEFCON_5 = "defcon_5" # Normal - All systems operational
|
||||||
|
DEFCON_4 = "defcon_4" # Increased Chatter - Minor alerts
|
||||||
|
DEFCON_3 = "defcon_3" # Heightened - Active threats
|
||||||
|
DEFCON_2 = "defcon_2" # Severe - Major incident
|
||||||
|
DEFCON_1 = "defcon_1" # Maximum - Critical breach
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DefconIndicator:
|
||||||
|
"""Single DEFCON indicator with weight and current value."""
|
||||||
|
name: str
|
||||||
|
category: str # network, threat, access, data, resilience
|
||||||
|
weight: float # 0.0-1.0
|
||||||
|
current_value: float = 1.0 # 0.0-1.0 (1.0 = perfect)
|
||||||
|
threshold_critical: float = 0.3 # Below this = error state
|
||||||
|
threshold_warning: float = 0.7 # Below this = warning state
|
||||||
|
threshold_ok: float = 0.95 # Above this = perfect
|
||||||
|
description: str = ""
|
||||||
|
cspn_requirement: str = "" # Reference to CSPN matrix ID
|
||||||
|
tpn_requirement: str = "" # Reference to TPN Media requirement
|
||||||
|
inverse: bool = False # If True, lower values are better
|
||||||
|
unit: str = "" # Unit of measurement (% , count, etc.)
|
||||||
|
|
||||||
|
def get_status(self) -> str:
|
||||||
|
"""Get current status based on value and thresholds."""
|
||||||
|
value = self.current_value
|
||||||
|
if self.inverse:
|
||||||
|
value = 1.0 - value
|
||||||
|
|
||||||
|
if value >= self.threshold_ok:
|
||||||
|
return "ok"
|
||||||
|
elif value >= self.threshold_warning:
|
||||||
|
return "degraded"
|
||||||
|
elif value >= self.threshold_critical:
|
||||||
|
return "warning"
|
||||||
|
else:
|
||||||
|
return "error"
|
||||||
|
|
||||||
|
def get_score(self) -> float:
|
||||||
|
"""Get normalized score (0-1) for this indicator."""
|
||||||
|
value = self.current_value
|
||||||
|
if self.inverse:
|
||||||
|
return 1.0 - value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class DefconEngine:
|
||||||
|
"""
|
||||||
|
Main DEFCON calculation engine.
|
||||||
|
|
||||||
|
Aggregates metrics from all SecuBox modules and calculates:
|
||||||
|
1. Overall security score (0-100)
|
||||||
|
2. DEFCON level (1-5)
|
||||||
|
3. Per-category scores
|
||||||
|
4. Compliance status (CSPN/TPN)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
engine = DefconEngine()
|
||||||
|
metrics = await engine.collect_all_metrics()
|
||||||
|
score = engine.calculate_score(metrics)
|
||||||
|
defcon = engine.get_defcon_level(score)
|
||||||
|
info = engine.get_defcon_info(score)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Category weight distribution (CSPN-aligned)
|
||||||
|
CATEGORY_WEIGHTS: Dict[str, float] = {
|
||||||
|
"network": 0.30, # WAF, Firewall, nftables
|
||||||
|
"threat": 0.25, # CrowdSec, DPI, threat detection
|
||||||
|
"access": 0.20, # Auth, privilege separation, ACL
|
||||||
|
"data": 0.15, # Encryption, data integrity
|
||||||
|
"resilience": 0.10 # Uptime, backups, failover
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.indicators: Dict[str, DefconIndicator] = {}
|
||||||
|
self._init_indicators()
|
||||||
|
self._last_collection: Optional[datetime] = None
|
||||||
|
self._cached_metrics: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def _init_indicators(self):
|
||||||
|
"""Initialize all DEFCON indicators from CSPN test matrix and TPN requirements."""
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# NETWORK SECURITY INDICATORS (30% total weight)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
self.indicators["waf_operational"] = DefconIndicator(
|
||||||
|
name="WAF Operational",
|
||||||
|
category="network",
|
||||||
|
weight=0.15,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.80,
|
||||||
|
description="WAF mitmproxy is running and processing requests",
|
||||||
|
cspn_requirement="WAF-01",
|
||||||
|
tpn_requirement="TPN-NET-01",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["firewall_policy"] = DefconIndicator(
|
||||||
|
name="Firewall Policy",
|
||||||
|
category="network",
|
||||||
|
weight=0.10,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.70,
|
||||||
|
description="nftables DEFAULT DROP policy active on all chains",
|
||||||
|
cspn_requirement="NET-01",
|
||||||
|
tpn_requirement="TPN-NET-02",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["nftables_rules"] = DefconIndicator(
|
||||||
|
name="nftables Rules Loaded",
|
||||||
|
category="network",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.90,
|
||||||
|
threshold_critical=0.50,
|
||||||
|
description="nftables rules loaded and active",
|
||||||
|
cspn_requirement="NET-05",
|
||||||
|
tpn_requirement="TPN-NET-03",
|
||||||
|
unit="%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# THREAT INTELLIGENCE INDICATORS (25% total weight)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
self.indicators["crowdsec_detection"] = DefconIndicator(
|
||||||
|
name="CrowdSec Detection",
|
||||||
|
category="threat",
|
||||||
|
weight=0.10,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.90,
|
||||||
|
threshold_critical=0.50,
|
||||||
|
description="CrowdSec LAPI responding and detecting threats",
|
||||||
|
cspn_requirement="WAF-04",
|
||||||
|
tpn_requirement="TPN-THREAT-01",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["threat_block_rate"] = DefconIndicator(
|
||||||
|
name="Threat Block Rate",
|
||||||
|
category="threat",
|
||||||
|
weight=0.10,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.90,
|
||||||
|
threshold_critical=0.70,
|
||||||
|
description="Percentage of detected threats successfully blocked",
|
||||||
|
cspn_requirement="WAF-03",
|
||||||
|
tpn_requirement="TPN-THREAT-02",
|
||||||
|
unit="%"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["false_positive_rate"] = DefconIndicator(
|
||||||
|
name="False Positive Rate",
|
||||||
|
category="threat",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=0.0, # Start at 0% (perfect)
|
||||||
|
threshold_ok=0.01, # <1% is perfect
|
||||||
|
threshold_warning=0.05,
|
||||||
|
threshold_critical=0.10,
|
||||||
|
description="Rate of false positives in threat detection",
|
||||||
|
cspn_requirement="WAF-04",
|
||||||
|
tpn_requirement="TPN-THREAT-03",
|
||||||
|
unit="%",
|
||||||
|
inverse=True # Lower is better
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# ACCESS CONTROL INDICATORS (20% total weight)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
self.indicators["auth_system"] = DefconIndicator(
|
||||||
|
name="Authentication System",
|
||||||
|
category="access",
|
||||||
|
weight=0.10,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.70,
|
||||||
|
description="Auth system (Authelia/SSO) operational",
|
||||||
|
cspn_requirement="AUT-01",
|
||||||
|
tpn_requirement="TPN-ACCESS-01",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["privilege_separation"] = DefconIndicator(
|
||||||
|
name="Privilege Separation",
|
||||||
|
category="access",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.90,
|
||||||
|
threshold_critical=0.70,
|
||||||
|
description="All services running as non-root users",
|
||||||
|
cspn_requirement="ACL-01",
|
||||||
|
tpn_requirement="TPN-ACCESS-02",
|
||||||
|
unit="%"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["audit_logging"] = DefconIndicator(
|
||||||
|
name="Audit Logging",
|
||||||
|
category="access",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.80,
|
||||||
|
description="Security audit log writing and immutable",
|
||||||
|
cspn_requirement="LOG-01",
|
||||||
|
tpn_requirement="TPN-ACCESS-03",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# DATA PROTECTION INDICATORS (15% total weight)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
self.indicators["tls_compliance"] = DefconIndicator(
|
||||||
|
name="TLS Compliance",
|
||||||
|
category="data",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.80,
|
||||||
|
description="TLS 1.3+ enforced, no weak ciphers",
|
||||||
|
cspn_requirement="CRY-01",
|
||||||
|
tpn_requirement="TPN-DATA-01",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["secrets_protection"] = DefconIndicator(
|
||||||
|
name="Secrets Protection",
|
||||||
|
category="data",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.80,
|
||||||
|
description="No secrets in logs, configs, or VCS",
|
||||||
|
cspn_requirement="DAT-01,CRY-05",
|
||||||
|
tpn_requirement="TPN-DATA-02",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["data_integrity"] = DefconIndicator(
|
||||||
|
name="Data Integrity",
|
||||||
|
category="data",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.80,
|
||||||
|
description="Data retention and integrity checks passing",
|
||||||
|
cspn_requirement="DAT-04",
|
||||||
|
tpn_requirement="TPN-DATA-03",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# RESILIENCE INDICATORS (10% total weight)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
self.indicators["service_uptime"] = DefconIndicator(
|
||||||
|
name="Service Uptime",
|
||||||
|
category="resilience",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.999,
|
||||||
|
threshold_warning=0.99,
|
||||||
|
threshold_critical=0.95,
|
||||||
|
description="Critical services uptime > 99.9%",
|
||||||
|
cspn_requirement="RES-01",
|
||||||
|
tpn_requirement="TPN-RES-01",
|
||||||
|
unit="%"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.indicators["backup_status"] = DefconIndicator(
|
||||||
|
name="Backup Status",
|
||||||
|
category="resilience",
|
||||||
|
weight=0.05,
|
||||||
|
current_value=1.0,
|
||||||
|
threshold_ok=0.99,
|
||||||
|
threshold_warning=0.95,
|
||||||
|
threshold_critical=0.80,
|
||||||
|
description="Configuration backups current and restorable",
|
||||||
|
cspn_requirement="CFG-02",
|
||||||
|
tpn_requirement="TPN-RES-02",
|
||||||
|
unit="boolean"
|
||||||
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Metric Collection
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def collect_all_metrics(self, use_cache: bool = True) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Collect all metrics from various SecuBox modules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_cache: If True, return cached values if still valid (60s TTL)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing all collected metrics
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
now = datetime.now()
|
||||||
|
if use_cache and self._last_collection:
|
||||||
|
if (now - self._last_collection).total_seconds() < 60:
|
||||||
|
return self._cached_metrics
|
||||||
|
|
||||||
|
metrics: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
# Collect WAF metrics
|
||||||
|
metrics["waf"] = await self._collect_waf_metrics(client)
|
||||||
|
|
||||||
|
# Collect Threat Analyst overview
|
||||||
|
metrics["threat_analyst"] = await self._collect_threat_analyst(client)
|
||||||
|
|
||||||
|
# Collect Health Doctor checks
|
||||||
|
metrics["health_doctor"] = await self._collect_health_doctor(client)
|
||||||
|
|
||||||
|
# Collect CrowdSec metrics
|
||||||
|
metrics["crowdsec"] = await self._collect_crowdsec(client)
|
||||||
|
|
||||||
|
# Collect System metrics
|
||||||
|
metrics["system"] = await self._collect_system_metrics()
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self._cached_metrics = metrics
|
||||||
|
self._last_collection = now
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Metric collection error: {e}")
|
||||||
|
# Return cached metrics if available
|
||||||
|
if self._cached_metrics:
|
||||||
|
return self._cached_metrics
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
async def _collect_waf_metrics(self, client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
"""Collect WAF metrics from unix socket."""
|
||||||
|
try:
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/waf.sock")
|
||||||
|
async with httpx.AsyncClient(transport=transport, timeout=4) as waf_client:
|
||||||
|
response = await waf_client.get("http://waf/stats")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"WAF stats collection failed: {e}")
|
||||||
|
|
||||||
|
return {"running": False, "error": "unable to connect"}
|
||||||
|
|
||||||
|
async def _collect_threat_analyst(self, client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
"""Collect Threat Analyst overview."""
|
||||||
|
try:
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/threat-analyst.sock")
|
||||||
|
async with httpx.AsyncClient(transport=transport, timeout=4) as ta_client:
|
||||||
|
response = await ta_client.get("http://threat-analyst/overview")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Threat Analyst collection failed: {e}")
|
||||||
|
|
||||||
|
return {"error": "unable to connect"}
|
||||||
|
|
||||||
|
async def _collect_health_doctor(self, client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
"""Collect Health Doctor checks."""
|
||||||
|
try:
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/health-doctor.sock")
|
||||||
|
async with httpx.AsyncClient(transport=transport, timeout=4) as hd_client:
|
||||||
|
response = await hd_client.get("http://health-doctor/checks")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Health Doctor collection failed: {e}")
|
||||||
|
|
||||||
|
return {"checks": {}, "error": "unable to connect"}
|
||||||
|
|
||||||
|
async def _collect_crowdsec(self, client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
"""Collect CrowdSec Prometheus metrics."""
|
||||||
|
try:
|
||||||
|
response = await client.get("http://127.0.0.1:6060/metrics", timeout=4)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return self._parse_prometheus(response.text)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"CrowdSec metrics collection failed: {e}")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def _collect_system_metrics(self) -> Dict[str, Any]:
|
||||||
|
"""Collect system-level metrics."""
|
||||||
|
metrics = {}
|
||||||
|
|
||||||
|
# Check nftables
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nft", "list", "ruleset"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
metrics["nftables_ruleset"] = result.stdout
|
||||||
|
metrics["nftables_error"] = result.stderr
|
||||||
|
metrics["nftables_running"] = result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
metrics["nftables_error"] = str(e)
|
||||||
|
metrics["nftables_running"] = False
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["systemctl", "is-system-running"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
metrics["system_running"] = result.returncode == 0
|
||||||
|
except Exception:
|
||||||
|
metrics["system_running"] = False
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def _parse_prometheus(self, text: str) -> Dict[str, float]:
|
||||||
|
"""Parse Prometheus metrics text format."""
|
||||||
|
result: Dict[str, float] = {}
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
metric_name = parts[0]
|
||||||
|
try:
|
||||||
|
value = float(parts[1])
|
||||||
|
result[metric_name] = value
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Score Calculation
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def calculate_score(self, metrics: Dict[str, Any]) -> float:
|
||||||
|
"""
|
||||||
|
Calculate overall security score (0-100).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metrics: Dictionary of collected metrics
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Security score as float (0-100)
|
||||||
|
"""
|
||||||
|
# Update indicator values from metrics
|
||||||
|
self._update_indicators(metrics)
|
||||||
|
|
||||||
|
# Calculate weighted score
|
||||||
|
total_score = 0.0
|
||||||
|
|
||||||
|
for indicator in self.indicators.values():
|
||||||
|
# Get normalized score for this indicator
|
||||||
|
indicator_score = indicator.get_score()
|
||||||
|
|
||||||
|
# Apply weight
|
||||||
|
total_score += indicator_score * indicator.weight
|
||||||
|
|
||||||
|
# Convert to 0-100 scale
|
||||||
|
return min(100.0, max(0.0, total_score * 100))
|
||||||
|
|
||||||
|
def calculate_category_scores(self, metrics: Dict[str, Any]) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Calculate per-category scores.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metrics: Dictionary of collected metrics
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of category_name -> score (0-100)
|
||||||
|
"""
|
||||||
|
self._update_indicators(metrics)
|
||||||
|
|
||||||
|
category_totals: Dict[str, float] = {}
|
||||||
|
category_weights: Dict[str, float] = {}
|
||||||
|
|
||||||
|
for indicator in self.indicators.values():
|
||||||
|
category = indicator.category
|
||||||
|
category_totals[category] = category_totals.get(category, 0.0) + \
|
||||||
|
indicator.get_score() * indicator.weight
|
||||||
|
category_weights[category] = category_weights.get(category, 0.0) + \
|
||||||
|
indicator.weight
|
||||||
|
|
||||||
|
# Normalize to category weights
|
||||||
|
category_scores: Dict[str, float] = {}
|
||||||
|
for category, total in category_totals.items():
|
||||||
|
weight_sum = category_weights[category]
|
||||||
|
if weight_sum > 0:
|
||||||
|
# Normalize to 0-100
|
||||||
|
category_scores[category] = min(100.0, (total / weight_sum) * 100)
|
||||||
|
|
||||||
|
return category_scores
|
||||||
|
|
||||||
|
def _update_indicators(self, metrics: Dict[str, Any]):
|
||||||
|
"""Update all indicator values based on collected metrics."""
|
||||||
|
|
||||||
|
# WAF indicators
|
||||||
|
if "waf" in metrics:
|
||||||
|
waf = metrics["waf"]
|
||||||
|
|
||||||
|
# WAF operational
|
||||||
|
self.indicators["waf_operational"].current_value = \
|
||||||
|
1.0 if waf.get("running", False) else 0.0
|
||||||
|
|
||||||
|
# Calculate block rate
|
||||||
|
blocked_24h = waf.get("blocked_24h", 0)
|
||||||
|
threats_today = waf.get("threats_today", 0)
|
||||||
|
total_requests = waf.get("total_requests", 0)
|
||||||
|
|
||||||
|
if threats_today > 0:
|
||||||
|
block_rate = blocked_24h / threats_today
|
||||||
|
self.indicators["threat_block_rate"].current_value = min(1.0, block_rate)
|
||||||
|
|
||||||
|
# CrowdSec indicators
|
||||||
|
if "crowdsec" in metrics:
|
||||||
|
cs = metrics["crowdsec"]
|
||||||
|
|
||||||
|
cs_alerts = cs.get("cs_alerts", 0)
|
||||||
|
cs_decisions = cs.get("cs_active_decisions", 0)
|
||||||
|
|
||||||
|
# CrowdSec detection is active if we have alerts or decisions
|
||||||
|
self.indicators["crowdsec_detection"].current_value = \
|
||||||
|
1.0 if (cs_alerts > 0 or cs_decisions > 0) else 0.0
|
||||||
|
|
||||||
|
# Health Doctor indicators
|
||||||
|
if "health_doctor" in metrics:
|
||||||
|
hd = metrics["health_doctor"]
|
||||||
|
checks = hd.get("checks", {})
|
||||||
|
|
||||||
|
# Count passing checks
|
||||||
|
passing = sum(1 for c in checks.values() if c.get("ok", False))
|
||||||
|
total = len(checks)
|
||||||
|
|
||||||
|
if total > 0:
|
||||||
|
# Use specific checks for specific indicators
|
||||||
|
if "crowdsec" in checks:
|
||||||
|
self.indicators["crowdsec_detection"].current_value = \
|
||||||
|
1.0 if checks["crowdsec"].get("ok", False) else 0.0
|
||||||
|
|
||||||
|
if "secubox-auth" in checks:
|
||||||
|
self.indicators["auth_system"].current_value = \
|
||||||
|
1.0 if checks["secubox-auth"].get("ok", False) else 0.0
|
||||||
|
|
||||||
|
if "mitmproxy-lxc" in checks:
|
||||||
|
self.indicators["waf_operational"].current_value = \
|
||||||
|
max(self.indicators["waf_operational"].current_value,
|
||||||
|
1.0 if checks["mitmproxy-lxc"].get("ok", False) else 0.0)
|
||||||
|
|
||||||
|
if "haproxy" in checks:
|
||||||
|
self.indicators["firewall_policy"].current_value = \
|
||||||
|
1.0 if checks["haproxy"].get("ok", False) else 0.0
|
||||||
|
|
||||||
|
# System metrics
|
||||||
|
if "system" in metrics:
|
||||||
|
sys = metrics["system"]
|
||||||
|
|
||||||
|
self.indicators["nftables_rules"].current_value = \
|
||||||
|
1.0 if sys.get("nftables_running", False) else 0.0
|
||||||
|
|
||||||
|
# Simulate some values for indicators we can't directly measure
|
||||||
|
# In production, implement actual checks for these
|
||||||
|
|
||||||
|
# For now, set reasonable defaults that will be updated by real checks
|
||||||
|
if self.indicators["privilege_separation"].current_value == 1.0:
|
||||||
|
# Check if services are running as non-root
|
||||||
|
self.indicators["privilege_separation"].current_value = self._check_privilege_separation()
|
||||||
|
|
||||||
|
if self.indicators["audit_logging"].current_value == 1.0:
|
||||||
|
self.indicators["audit_logging"].current_value = self._check_audit_logging()
|
||||||
|
|
||||||
|
# Set TLS compliance (assume good for now)
|
||||||
|
self.indicators["tls_compliance"].current_value = 1.0
|
||||||
|
|
||||||
|
# Set secrets protection (assume good for now)
|
||||||
|
self.indicators["secrets_protection"].current_value = 1.0
|
||||||
|
|
||||||
|
# Set data integrity (assume good for now)
|
||||||
|
self.indicators["data_integrity"].current_value = 1.0
|
||||||
|
|
||||||
|
# Set service uptime (assume good for now)
|
||||||
|
self.indicators["service_uptime"].current_value = 1.0
|
||||||
|
|
||||||
|
# Set backup status (assume good for now)
|
||||||
|
self.indicators["backup_status"].current_value = 0.95
|
||||||
|
|
||||||
|
# Set false positive rate (assume low for now)
|
||||||
|
self.indicators["false_positive_rate"].current_value = 0.02
|
||||||
|
|
||||||
|
def _check_privilege_separation(self) -> float:
|
||||||
|
"""Check if services are running as non-root users."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["ps", "-eo", "user,comm", "|", "grep", "secubox", "|", "grep", "-v", "grep"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
shell=True
|
||||||
|
)
|
||||||
|
# If we find secubox services, assume good
|
||||||
|
if result.returncode == 0 and result.stdout:
|
||||||
|
return 1.0
|
||||||
|
return 0.7
|
||||||
|
except Exception:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
def _check_audit_logging(self) -> float:
|
||||||
|
"""Check if audit logging is working."""
|
||||||
|
audit_log = Path("/var/log/secubox/audit.log")
|
||||||
|
if audit_log.exists():
|
||||||
|
# Check if file was modified recently
|
||||||
|
try:
|
||||||
|
mtime = audit_log.stat().st_mtime
|
||||||
|
age_hours = (datetime.now() - datetime.fromtimestamp(mtime)).total_seconds() / 3600
|
||||||
|
# If modified in last 24 hours, consider it active
|
||||||
|
return 1.0 if age_hours < 24 else 0.5
|
||||||
|
except Exception:
|
||||||
|
return 0.7
|
||||||
|
return 0.3
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# DEFCON Level Determination
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_defcon_level(self, score: float) -> DefconLevel:
|
||||||
|
"""
|
||||||
|
Map security score to DEFCON level.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
score: Security score (0-100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DEFCON level enum value
|
||||||
|
"""
|
||||||
|
if score >= 90:
|
||||||
|
return DefconLevel.DEFCON_5
|
||||||
|
elif score >= 70:
|
||||||
|
return DefconLevel.DEFCON_4
|
||||||
|
elif score >= 50:
|
||||||
|
return DefconLevel.DEFCON_3
|
||||||
|
elif score >= 30:
|
||||||
|
return DefconLevel.DEFCON_2
|
||||||
|
else:
|
||||||
|
return DefconLevel.DEFCON_1
|
||||||
|
|
||||||
|
async def get_defcon_info(self, score: Optional[float] = None, metrics: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get full DEFCON information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
score: Optional pre-calculated score (if not provided, will calculate from metrics)
|
||||||
|
metrics: Optional metrics to use for calculation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with complete DEFCON information
|
||||||
|
"""
|
||||||
|
if score is None:
|
||||||
|
if metrics is None:
|
||||||
|
# Use cached or collect fresh
|
||||||
|
if self._cached_metrics:
|
||||||
|
metrics = self._cached_metrics
|
||||||
|
else:
|
||||||
|
# Need to collect
|
||||||
|
metrics = await self.collect_all_metrics(use_cache=False)
|
||||||
|
score = self.calculate_score(metrics)
|
||||||
|
|
||||||
|
level = self.get_defcon_level(score)
|
||||||
|
category_scores = self.calculate_category_scores(metrics or self._cached_metrics)
|
||||||
|
|
||||||
|
defcon_info = {
|
||||||
|
"level": level.value,
|
||||||
|
"level_name": level.name.replace("_", " ").title(),
|
||||||
|
"level_enum": level,
|
||||||
|
"score": round(score, 1),
|
||||||
|
"score_int": int(score),
|
||||||
|
"color": self._get_defcon_color(level),
|
||||||
|
"emoji": self._get_defcon_emoji(level),
|
||||||
|
"blink": level in [DefconLevel.DEFCON_1, DefconLevel.DEFCON_2],
|
||||||
|
"description": self._get_defcon_description(level, score),
|
||||||
|
"recommendations": self._get_recommendations(level),
|
||||||
|
"category_scores": category_scores,
|
||||||
|
"indicators": self.get_indicator_details(),
|
||||||
|
"timestamp": datetime.now().isoformat() + "Z",
|
||||||
|
"cspn_compliance": self._calculate_cspn_compliance(score, category_scores),
|
||||||
|
"tpn_compliance": self._calculate_tpn_compliance(score, category_scores),
|
||||||
|
}
|
||||||
|
|
||||||
|
return defcon_info
|
||||||
|
|
||||||
|
def _get_defcon_color(self, level: DefconLevel) -> str:
|
||||||
|
"""Get color associated with DEFCON level."""
|
||||||
|
colors = {
|
||||||
|
DefconLevel.DEFCON_5: "#22c55e", # Green
|
||||||
|
DefconLevel.DEFCON_4: "#eab308", # Yellow/Amber
|
||||||
|
DefconLevel.DEFCON_3: "#f97316", # Orange
|
||||||
|
DefconLevel.DEFCON_2: "#ef4444", # Red
|
||||||
|
DefconLevel.DEFCON_1: "#dc2626", # Dark Red
|
||||||
|
}
|
||||||
|
return colors.get(level, "#6b7280")
|
||||||
|
|
||||||
|
def _get_defcon_emoji(self, level: DefconLevel) -> str:
|
||||||
|
"""Get emoji associated with DEFCON level."""
|
||||||
|
emojis = {
|
||||||
|
DefconLevel.DEFCON_5: "🟢",
|
||||||
|
DefconLevel.DEFCON_4: "🟡",
|
||||||
|
DefconLevel.DEFCON_3: "🟠",
|
||||||
|
DefconLevel.DEFCON_2: "🔴",
|
||||||
|
DefconLevel.DEFCON_1: "🚨",
|
||||||
|
}
|
||||||
|
return emojis.get(level, "⚪")
|
||||||
|
|
||||||
|
def _get_defcon_description(self, level: DefconLevel, score: float) -> str:
|
||||||
|
"""Get description for DEFCON level."""
|
||||||
|
descriptions = {
|
||||||
|
DefconLevel.DEFCON_5: (
|
||||||
|
f"✅ NORMAL OPERATIONS • All systems secure and operational • "
|
||||||
|
f"Security Score: {score:.1f}/100 • CSPN-compliant"
|
||||||
|
),
|
||||||
|
DefconLevel.DEFCON_4: (
|
||||||
|
f"⚠️ INCREASED CHATTER • Minor security issues detected • "
|
||||||
|
f"Security Score: {score:.1f}/100 • Monitor closely"
|
||||||
|
),
|
||||||
|
DefconLevel.DEFCON_3: (
|
||||||
|
f"🟠 HEIGHTENED • Active threats being detected and mitigated • "
|
||||||
|
f"Security Score: {score:.1f}/100 • Active monitoring required"
|
||||||
|
),
|
||||||
|
DefconLevel.DEFCON_2: (
|
||||||
|
f"🔴 SEVERE • Major security incident in progress • "
|
||||||
|
f"Security Score: {score:.1f}/100 • Immediate action required"
|
||||||
|
),
|
||||||
|
DefconLevel.DEFCON_1: (
|
||||||
|
f"🚨 MAXIMUM • Critical breach detected • "
|
||||||
|
f"Security Score: {score:.1f}/100 • Emergency protocols activated"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return descriptions.get(level, f"Unknown DEFCON level (Score: {score:.1f})")
|
||||||
|
|
||||||
|
def _get_recommendations(self, level: DefconLevel) -> List[str]:
|
||||||
|
"""Get actionable recommendations based on DEFCON level."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if level in [DefconLevel.DEFCON_1, DefconLevel.DEFCON_2]:
|
||||||
|
recommendations.extend([
|
||||||
|
"🔴 Activate emergency response procedures",
|
||||||
|
"🔴 Isolate affected systems immediately",
|
||||||
|
"🔴 Notify security team and CSPN evaluator",
|
||||||
|
"🔴 Review all recent configuration changes",
|
||||||
|
"🔴 Check audit logs for suspicious activity",
|
||||||
|
"🔴 Verify backup integrity and prepare for restore",
|
||||||
|
])
|
||||||
|
elif level == DefconLevel.DEFCON_3:
|
||||||
|
recommendations.extend([
|
||||||
|
"🟠 Review active threat alerts in Threat Analyst",
|
||||||
|
"🟠 Verify WAF rules are up to date",
|
||||||
|
"🟠 Check CrowdSec decisions for false positives",
|
||||||
|
"🟠 Monitor network traffic for anomalies",
|
||||||
|
"🟠 Consider activating additional protections",
|
||||||
|
"🟠 Review recent system changes",
|
||||||
|
])
|
||||||
|
elif level == DefconLevel.DEFCON_4:
|
||||||
|
recommendations.extend([
|
||||||
|
"🟡 Verify all critical services are running",
|
||||||
|
"🟡 Check for minor configuration issues",
|
||||||
|
"🟡 Review recent security events",
|
||||||
|
"🟡 Ensure backups are current",
|
||||||
|
"🟡 Test failover procedures",
|
||||||
|
])
|
||||||
|
else: # DEFCON 5
|
||||||
|
recommendations.extend([
|
||||||
|
"🟢 Continue normal monitoring",
|
||||||
|
"🟢 Review weekly security reports",
|
||||||
|
"🟢 Test backup restoration procedures",
|
||||||
|
"🟢 Verify CSPN compliance status",
|
||||||
|
"🟢 Check for security updates",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add indicator-specific recommendations for degraded/failed indicators
|
||||||
|
for name, indicator in self.indicators.items():
|
||||||
|
status = indicator.get_status()
|
||||||
|
if status in ["error", "warning"]:
|
||||||
|
recommendations.append(
|
||||||
|
f"⚠️ {indicator.name}: {status.upper()} "
|
||||||
|
f"({indicator.current_value:.0%}) - {indicator.description}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
def _calculate_cspn_compliance(self, score: float, category_scores: Dict[str, float]) -> Dict[str, Any]:
|
||||||
|
"""Calculate CSPN compliance status."""
|
||||||
|
# CSPN requires minimum scores in certain areas
|
||||||
|
|
||||||
|
# Check critical CSPN requirements
|
||||||
|
critical_passing = True
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Network security must be > 80 for CSPN
|
||||||
|
if category_scores.get("network", 0) < 80:
|
||||||
|
critical_passing = False
|
||||||
|
warnings.append("Network security below CSPN threshold (80%)")
|
||||||
|
|
||||||
|
# Threat detection must be > 70 for CSPN
|
||||||
|
if category_scores.get("threat", 0) < 70:
|
||||||
|
critical_passing = False
|
||||||
|
warnings.append("Threat detection below CSPN threshold (70%)")
|
||||||
|
|
||||||
|
# Access control must be > 80 for CSPN
|
||||||
|
if category_scores.get("access", 0) < 80:
|
||||||
|
critical_passing = False
|
||||||
|
warnings.append("Access control below CSPN threshold (80%)")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "compliant" if critical_passing and score >= 70 else "non_compliant",
|
||||||
|
"score": score,
|
||||||
|
"threshold": 70,
|
||||||
|
"warnings": warnings,
|
||||||
|
"category_passing": {
|
||||||
|
"network": category_scores.get("network", 0) >= 80,
|
||||||
|
"threat": category_scores.get("threat", 0) >= 70,
|
||||||
|
"access": category_scores.get("access", 0) >= 80,
|
||||||
|
"data": category_scores.get("data", 0) >= 80,
|
||||||
|
"resilience": category_scores.get("resilience", 0) >= 70,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _calculate_tpn_compliance(self, score: float, category_scores: Dict[str, float]) -> Dict[str, Any]:
|
||||||
|
"""Calculate TPN Media compliance status."""
|
||||||
|
# TPN Media has stricter requirements
|
||||||
|
|
||||||
|
tpn_passing = True
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# All categories must be > 80 for TPN Media
|
||||||
|
for category, threshold in [
|
||||||
|
("network", 85),
|
||||||
|
("threat", 80),
|
||||||
|
("access", 85),
|
||||||
|
("data", 85),
|
||||||
|
("resilience", 80),
|
||||||
|
]:
|
||||||
|
if category_scores.get(category, 0) < threshold:
|
||||||
|
tpn_passing = False
|
||||||
|
warnings.append(f"{category} below TPN threshold ({threshold}%)")
|
||||||
|
|
||||||
|
# Overall score must be > 85 for TPN Media
|
||||||
|
if score < 85:
|
||||||
|
tpn_passing = False
|
||||||
|
warnings.append(f"Overall score below TPN threshold (85%, got {score:.1f}%)")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "compliant" if tpn_passing else "non_compliant",
|
||||||
|
"score": score,
|
||||||
|
"threshold": 85,
|
||||||
|
"warnings": warnings,
|
||||||
|
"category_passing": {
|
||||||
|
"network": category_scores.get("network", 0) >= 85,
|
||||||
|
"threat": category_scores.get("threat", 0) >= 80,
|
||||||
|
"access": category_scores.get("access", 0) >= 85,
|
||||||
|
"data": category_scores.get("data", 0) >= 85,
|
||||||
|
"resilience": category_scores.get("resilience", 0) >= 80,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Indicator Access
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_indicator_details(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Get all indicator details for dashboard display."""
|
||||||
|
return {
|
||||||
|
name: {
|
||||||
|
"name": ind.name,
|
||||||
|
"category": ind.category,
|
||||||
|
"value": round(ind.current_value * 100, 1),
|
||||||
|
"value_raw": ind.current_value,
|
||||||
|
"weight": ind.weight,
|
||||||
|
"status": ind.get_status(),
|
||||||
|
"status_color": self._get_status_color(ind.get_status()),
|
||||||
|
"description": ind.description,
|
||||||
|
"cspn_requirement": ind.cspn_requirement,
|
||||||
|
"tpn_requirement": ind.tpn_requirement,
|
||||||
|
"unit": ind.unit,
|
||||||
|
"inverse": ind.inverse,
|
||||||
|
}
|
||||||
|
for name, ind in self.indicators.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_status_color(self, status: str) -> str:
|
||||||
|
"""Get color for indicator status."""
|
||||||
|
colors = {
|
||||||
|
"ok": "#22c55e",
|
||||||
|
"degraded": "#eab308",
|
||||||
|
"warning": "#f97316",
|
||||||
|
"error": "#ef4444",
|
||||||
|
}
|
||||||
|
return colors.get(status, "#6b7280")
|
||||||
|
|
||||||
|
async def get_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Get a quick summary of the current state."""
|
||||||
|
if not self._cached_metrics:
|
||||||
|
# Force collection
|
||||||
|
self._cached_metrics = await self.collect_all_metrics(use_cache=False)
|
||||||
|
self._last_collection = datetime.now()
|
||||||
|
|
||||||
|
score = self.calculate_score(self._cached_metrics)
|
||||||
|
level = self.get_defcon_level(score)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"defcon": level.value,
|
||||||
|
"score": round(score, 1),
|
||||||
|
"color": self._get_defcon_color(level),
|
||||||
|
"emoji": self._get_defcon_emoji(level),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
defcon_engine = DefconEngine()
|
||||||
766
packages/secubox-security-posture/api/main.py
Normal file
766
packages/secubox-security-posture/api/main.py
Normal file
|
|
@ -0,0 +1,766 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""
|
||||||
|
SecuBox Security Posture - FastAPI Endpoints
|
||||||
|
|
||||||
|
Provides REST API for:
|
||||||
|
- DEFCON level security health
|
||||||
|
- CSPN compliance status
|
||||||
|
- TPN Media compliance status
|
||||||
|
- Performance monitoring
|
||||||
|
- Combined security dashboard
|
||||||
|
|
||||||
|
API Endpoints:
|
||||||
|
GET /api/v1/security-posture/defcon - DEFCON level and score
|
||||||
|
GET /api/v1/security-posture/cspn - CSPN compliance report
|
||||||
|
GET /api/v1/security-posture/tpn - TPN Media compliance report
|
||||||
|
GET /api/v1/security-posture/performance - Performance metrics
|
||||||
|
GET /api/v1/security-posture/overview - Combined overview
|
||||||
|
GET /api/v1/security-posture/health - Service health
|
||||||
|
POST /api/v1/security-posture/checks/run - Run all compliance checks
|
||||||
|
GET /api/v1/security-posture/bottlenecks - Performance bottlenecks
|
||||||
|
GET /api/v1/security-posture/recommendations - Optimization recommendations
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- All endpoints accessible via unix socket only
|
||||||
|
- No authentication required (security via socket permissions)
|
||||||
|
- Intended to be mounted under /api/v1/security-posture/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from fastapi import FastAPI, BackgroundTasks, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
# Try to import secubox_core, but make it optional for standalone testing
|
||||||
|
try:
|
||||||
|
from secubox_core.config import get_config
|
||||||
|
SECUBOX_CORE_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
SECUBOX_CORE_AVAILABLE = False
|
||||||
|
get_config = lambda: {}
|
||||||
|
|
||||||
|
from .defcon import defcon_engine, DefconLevel
|
||||||
|
from .cspn_compliance import cspn_checker
|
||||||
|
from .tpn_compliance import tpn_checker
|
||||||
|
from .performance import performance_monitor
|
||||||
|
|
||||||
|
logger = logging.getLogger("secubox.security-posture")
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="SecuBox Security Posture",
|
||||||
|
description=(
|
||||||
|
"DEFCON-level security health monitoring with CSPN and TPN Media compliance. "
|
||||||
|
"Provides real-time security posture assessment, performance monitoring, "
|
||||||
|
"and compliance reporting."
|
||||||
|
),
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc",
|
||||||
|
openapi_url="/openapi.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CORS middleware (for development/testing via HTTP)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=False,
|
||||||
|
allow_methods=["GET", "POST", "OPTIONS"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEFCON Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/defcon")
|
||||||
|
async def get_defcon():
|
||||||
|
"""
|
||||||
|
Get current DEFCON level and security score.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- DEFCON level (1-5)
|
||||||
|
- Security score (0-100)
|
||||||
|
- Color and emoji indicators
|
||||||
|
- Per-category scores
|
||||||
|
- Compliance status (CSPN/TPN)
|
||||||
|
- All indicator details
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Collect fresh metrics
|
||||||
|
metrics = await defcon_engine.collect_all_metrics(use_cache=False)
|
||||||
|
score = defcon_engine.calculate_score(metrics)
|
||||||
|
defcon_info = await defcon_engine.get_defcon_info(score, metrics)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content=defcon_info,
|
||||||
|
headers={"Cache-Control": "public, max-age=30"}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"DEFCON endpoint error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": str(e), "status": "error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/defcon/summary")
|
||||||
|
async def get_defcon_summary():
|
||||||
|
"""
|
||||||
|
Get DEFCON summary (lightweight, no metric collection).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- DEFCON level
|
||||||
|
- Security score
|
||||||
|
- Color and emoji
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await defcon_engine.get_summary()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"DEFCON summary error: {e}")
|
||||||
|
return {"error": str(e), "defcon": "defcon_5", "score": 0.0}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/defcon/indicators")
|
||||||
|
async def get_defcon_indicators():
|
||||||
|
"""
|
||||||
|
Get all DEFCON indicator details.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- List of all indicators with current values
|
||||||
|
- Status, weights, thresholds
|
||||||
|
- CSPN/TPN requirement mappings
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"indicators": defcon_engine.get_indicator_details()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Indicators endpoint error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/defcon/category/{category}")
|
||||||
|
async def get_defcon_category(category: str):
|
||||||
|
"""
|
||||||
|
Get DEFCON indicators for a specific category.
|
||||||
|
|
||||||
|
Categories: network, threat, access, data, resilience
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
indicators = defcon_engine.get_indicator_details()
|
||||||
|
category_indicators = {
|
||||||
|
k: v for k, v in indicators.items()
|
||||||
|
if v.get("category") == category
|
||||||
|
}
|
||||||
|
|
||||||
|
if not category_indicators:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Category '{category}' not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"category": category,
|
||||||
|
"indicators": category_indicators
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Category endpoint error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CSPN Compliance Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/cspn")
|
||||||
|
async def get_cspn_compliance():
|
||||||
|
"""
|
||||||
|
Get full CSPN compliance report.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Compliance summary
|
||||||
|
- All requirements with status
|
||||||
|
- Pass/fail counts
|
||||||
|
- Certificate readiness
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
report = await cspn_checker.run_all_checks()
|
||||||
|
return JSONResponse(
|
||||||
|
content=report,
|
||||||
|
headers={"Cache-Control": "public, max-age=60"}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CSPN compliance error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": str(e), "status": "error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/cspn/summary")
|
||||||
|
async def get_cspn_summary():
|
||||||
|
"""
|
||||||
|
Get CSPN compliance summary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Compliance score
|
||||||
|
- Pass/fail percentages
|
||||||
|
- Certificate readiness
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"summary": cspn_checker.get_summary()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CSPN summary error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/cspn/requirements")
|
||||||
|
async def get_cspn_requirements():
|
||||||
|
"""
|
||||||
|
Get all CSPN requirements.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"requirements": cspn_checker.get_all_requirements()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CSPN requirements error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/cspn/requirements/{req_id}")
|
||||||
|
async def get_cspn_requirement(req_id: str):
|
||||||
|
"""
|
||||||
|
Get a specific CSPN requirement.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
requirement = cspn_checker.get_requirement(req_id)
|
||||||
|
if requirement is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Requirement {req_id} not found")
|
||||||
|
return requirement
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CSPN requirement error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/cspn/certificate/readiness")
|
||||||
|
async def get_cspn_certificate_readiness():
|
||||||
|
"""
|
||||||
|
Get CSPN certificate readiness status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Ready status
|
||||||
|
- Compliance score
|
||||||
|
- Blocking issues
|
||||||
|
- Recommendations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return cspn_checker.get_certificate_readiness()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CSPN readiness error: {e}")
|
||||||
|
return {"error": str(e), "ready": False}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TPN Media Compliance Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/tpn")
|
||||||
|
async def get_tpn_compliance():
|
||||||
|
"""
|
||||||
|
Get full TPN Media compliance report.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- TPN compliance summary
|
||||||
|
- CSPN compliance (related)
|
||||||
|
- All TPN requirements with status
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
report = await tpn_checker.run_all_checks()
|
||||||
|
return JSONResponse(
|
||||||
|
content=report,
|
||||||
|
headers={"Cache-Control": "public, max-age=60"}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TPN compliance error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": str(e), "status": "error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tpn/summary")
|
||||||
|
async def get_tpn_summary():
|
||||||
|
"""
|
||||||
|
Get TPN Media compliance summary.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"summary": tpn_checker.get_summary()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TPN summary error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tpn/requirements")
|
||||||
|
async def get_tpn_requirements():
|
||||||
|
"""
|
||||||
|
Get all TPN Media requirements.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"requirements": tpn_checker.get_all_requirements()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TPN requirements error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tpn/requirements/{req_id}")
|
||||||
|
async def get_tpn_requirement(req_id: str):
|
||||||
|
"""
|
||||||
|
Get a specific TPN requirement.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
requirement = tpn_checker.get_requirement(req_id)
|
||||||
|
if requirement is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Requirement {req_id} not found")
|
||||||
|
return requirement
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TPN requirement error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tpn/certificate/readiness")
|
||||||
|
async def get_tpn_certificate_readiness():
|
||||||
|
"""
|
||||||
|
Get TPN Media certificate readiness status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- CSPN readiness
|
||||||
|
- TPN readiness
|
||||||
|
- Overall readiness
|
||||||
|
- Blocking issues
|
||||||
|
- Recommendations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return tpn_checker.get_media_certificate_readiness()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TPN readiness error: {e}")
|
||||||
|
return {"error": str(e), "ready": False}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Performance Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/performance")
|
||||||
|
async def get_performance():
|
||||||
|
"""
|
||||||
|
Get all performance metrics.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- All performance metrics with values
|
||||||
|
- Status, thresholds
|
||||||
|
- Category scores
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
metrics = performance_monitor.collect_all_metrics()
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"metrics": {
|
||||||
|
name: {
|
||||||
|
"name": m.name,
|
||||||
|
"category": m.category.value,
|
||||||
|
"value": m.value,
|
||||||
|
"unit": m.unit,
|
||||||
|
"status": m.get_status().value,
|
||||||
|
"status_color": m.get_status_color(),
|
||||||
|
"threshold_ok": m.threshold_ok,
|
||||||
|
"threshold_warning": m.threshold_warning,
|
||||||
|
"threshold_critical": m.threshold_critical,
|
||||||
|
"description": m.description,
|
||||||
|
"recommendation": m.recommendation,
|
||||||
|
}
|
||||||
|
for name, m in performance_monitor.metrics.items()
|
||||||
|
},
|
||||||
|
"summary": performance_monitor.get_summary()
|
||||||
|
},
|
||||||
|
headers={"Cache-Control": "public, max-age=15"}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Performance endpoint error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": str(e), "status": "error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/performance/summary")
|
||||||
|
async def get_performance_summary():
|
||||||
|
"""
|
||||||
|
Get performance summary.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return performance_monitor.get_summary()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Performance summary error: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/performance/bottlenecks")
|
||||||
|
async def get_performance_bottlenecks():
|
||||||
|
"""
|
||||||
|
Get detected performance bottlenecks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- List of bottlenecks
|
||||||
|
- Severity, description, impact
|
||||||
|
- Recommendations for each
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
bottlenecks = performance_monitor.detect_bottlenecks()
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"bottlenecks": [
|
||||||
|
{
|
||||||
|
"name": b.name,
|
||||||
|
"resource_type": b.resource_type.value,
|
||||||
|
"severity": b.severity,
|
||||||
|
"description": b.description,
|
||||||
|
"impact": b.impact,
|
||||||
|
"metric_value": b.metric_value,
|
||||||
|
"threshold": b.threshold,
|
||||||
|
"timestamp": b.timestamp,
|
||||||
|
"recommendations": b.recommendations,
|
||||||
|
}
|
||||||
|
for b in bottlenecks
|
||||||
|
],
|
||||||
|
"count": len(bottlenecks)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bottlenecks error: {e}")
|
||||||
|
return {"error": str(e), "bottlenecks": []}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/performance/recommendations")
|
||||||
|
async def get_performance_recommendations():
|
||||||
|
"""
|
||||||
|
Get performance optimization recommendations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- List of recommendations
|
||||||
|
- Severity levels
|
||||||
|
- Specific actions
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return performance_monitor.get_recommendations()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Recommendations error: {e}")
|
||||||
|
return {"error": str(e), "recommendations": []}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/performance/history")
|
||||||
|
async def get_performance_history(hours: int = 24):
|
||||||
|
"""
|
||||||
|
Get performance history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hours: Time window for history (default: 24)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return performance_monitor.get_performance_history(hours)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"History error: {e}")
|
||||||
|
return {"error": str(e), "history": []}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Combined Overview Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/overview")
|
||||||
|
async def get_security_overview():
|
||||||
|
"""
|
||||||
|
Get combined security posture overview.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- DEFCON level and score
|
||||||
|
- CSPN compliance
|
||||||
|
- TPN Media compliance
|
||||||
|
- Performance summary
|
||||||
|
- Combined recommendations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Collect all data
|
||||||
|
defcon_info = await defcon_engine.get_defcon_info()
|
||||||
|
cspn_summary = cspn_checker.get_summary()
|
||||||
|
tpn_summary = tpn_checker.get_summary()
|
||||||
|
perf_summary = performance_monitor.get_summary()
|
||||||
|
|
||||||
|
# Calculate combined score (weighted average)
|
||||||
|
combined_score = round(
|
||||||
|
(defcon_info["score"] * 0.4) +
|
||||||
|
(cspn_summary.get("compliance_score", 0) * 0.3) +
|
||||||
|
(tpn_summary.get("compliance_score", 0) * 0.2) +
|
||||||
|
(perf_summary["score"] * 0.1),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine overall status
|
||||||
|
if combined_score >= 85:
|
||||||
|
overall_status = "excellent"
|
||||||
|
overall_color = "#22c55e"
|
||||||
|
overall_emoji = "🟢"
|
||||||
|
elif combined_score >= 70:
|
||||||
|
overall_status = "good"
|
||||||
|
overall_color = "#86efac"
|
||||||
|
overall_emoji = "🟢"
|
||||||
|
elif combined_score >= 50:
|
||||||
|
overall_status = "fair"
|
||||||
|
overall_color = "#eab308"
|
||||||
|
overall_emoji = "🟡"
|
||||||
|
elif combined_score >= 30:
|
||||||
|
overall_status = "poor"
|
||||||
|
overall_color = "#f97316"
|
||||||
|
overall_emoji = "🟠"
|
||||||
|
else:
|
||||||
|
overall_status = "critical"
|
||||||
|
overall_color = "#ef4444"
|
||||||
|
overall_emoji = "🔴"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": asyncio.get_event_loop().now().isoformat() + "Z",
|
||||||
|
"combined_score": combined_score,
|
||||||
|
"overall_status": overall_status,
|
||||||
|
"overall_color": overall_color,
|
||||||
|
"overall_emoji": overall_emoji,
|
||||||
|
"defcon": {
|
||||||
|
"level": defcon_info["level"],
|
||||||
|
"level_name": defcon_info["level_name"],
|
||||||
|
"score": defcon_info["score"],
|
||||||
|
"color": defcon_info["color"],
|
||||||
|
"emoji": defcon_info["emoji"],
|
||||||
|
"blink": defcon_info.get("blink", False),
|
||||||
|
"description": defcon_info["description"],
|
||||||
|
},
|
||||||
|
"cspn": {
|
||||||
|
"compliant": cspn_summary.get("is_compliant", False),
|
||||||
|
"score": cspn_summary.get("compliance_score", 0),
|
||||||
|
},
|
||||||
|
"tpn_media": {
|
||||||
|
"compliant": tpn_summary.get("is_compliant", False),
|
||||||
|
"score": tpn_summary.get("compliance_score", 0),
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"score": perf_summary["score"],
|
||||||
|
"status": perf_summary["status"],
|
||||||
|
"bottlenecks": perf_summary["bottlenecks"],
|
||||||
|
},
|
||||||
|
"recommendations": self._get_combined_recommendations(
|
||||||
|
defcon_info, cspn_summary, tpn_summary, perf_summary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Overview error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": str(e), "status": "error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_combined_recommendations(self, defcon_info: Dict, cspn_summary: Dict,
|
||||||
|
tpn_summary: Dict, perf_summary: Dict) -> List[Dict]:
|
||||||
|
"""Get combined recommendations from all sources."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
# DEFCON recommendations
|
||||||
|
if "recommendations" in defcon_info:
|
||||||
|
for rec in defcon_info["recommendations"][:3]: # Top 3
|
||||||
|
recommendations.append({
|
||||||
|
"source": "defcon",
|
||||||
|
"severity": "high" if "🔴" in rec or "🚨" in rec else "medium",
|
||||||
|
"text": rec
|
||||||
|
})
|
||||||
|
|
||||||
|
# CSPN recommendations
|
||||||
|
if not cspn_summary.get("is_compliant", True):
|
||||||
|
recommendations.append({
|
||||||
|
"source": "cspn",
|
||||||
|
"severity": "high",
|
||||||
|
"text": f"CSPN compliance score {cspn_summary.get('compliance_score', 0)}% - Address failing checks"
|
||||||
|
})
|
||||||
|
|
||||||
|
# TPN recommendations
|
||||||
|
if not tpn_summary.get("is_compliant", True):
|
||||||
|
recommendations.append({
|
||||||
|
"source": "tpn",
|
||||||
|
"severity": "high",
|
||||||
|
"text": f"TPN Media compliance score {tpn_summary.get('compliance_score', 0)}% - Address media-specific requirements"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Performance recommendations
|
||||||
|
if perf_summary.get("bottlenecks", 0) > 0:
|
||||||
|
recommendations.append({
|
||||||
|
"source": "performance",
|
||||||
|
"severity": "medium",
|
||||||
|
"text": f"{perf_summary.get('bottlenecks', 0)} performance bottlenecks detected - Review and optimize"
|
||||||
|
})
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""
|
||||||
|
Health check endpoint for service monitoring.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Service status
|
||||||
|
- Version
|
||||||
|
- Dependencies health
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"service": "secubox-security-posture",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"defcon_engine": "ok",
|
||||||
|
"cspn_checker": "ok",
|
||||||
|
"tpn_checker": "ok",
|
||||||
|
"performance_monitor": "ok",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"service": "secubox-security-posture",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/checks/run")
|
||||||
|
async def run_all_checks(background_tasks: BackgroundTasks):
|
||||||
|
"""
|
||||||
|
Run all compliance checks (CSPN + TPN).
|
||||||
|
|
||||||
|
This triggers fresh collection and checking of all requirements.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Run checks in background
|
||||||
|
background_tasks.add_task(cspn_checker.run_all_checks)
|
||||||
|
background_tasks.add_task(tpn_checker.run_all_checks)
|
||||||
|
background_tasks.add_task(defcon_engine.collect_all_metrics)
|
||||||
|
background_tasks.add_task(performance_monitor.collect_all_metrics)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "started",
|
||||||
|
"message": "All compliance and performance checks started in background",
|
||||||
|
"checks": [
|
||||||
|
"cspn_compliance",
|
||||||
|
"tpn_media_compliance",
|
||||||
|
"defcon_metrics",
|
||||||
|
"performance_metrics"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Run checks error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": str(e), "status": "error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Startup
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Initialize on startup."""
|
||||||
|
logger.info("SecuBox Security Posture starting...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Collect initial metrics
|
||||||
|
await defcon_engine.collect_all_metrics(use_cache=False)
|
||||||
|
logger.info("DEFCON engine initialized")
|
||||||
|
|
||||||
|
# Run initial compliance checks
|
||||||
|
await cspn_checker.run_all_checks()
|
||||||
|
logger.info("CSPN checker initialized")
|
||||||
|
|
||||||
|
await tpn_checker.run_all_checks()
|
||||||
|
logger.info("TPN checker initialized")
|
||||||
|
|
||||||
|
# Collect initial performance metrics
|
||||||
|
performance_monitor.collect_all_metrics()
|
||||||
|
logger.info("Performance monitor initialized")
|
||||||
|
|
||||||
|
logger.info("SecuBox Security Posture started successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Startup error: {e}")
|
||||||
|
# Don't fail startup on errors
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Background Tasks
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
async def background_refresh():
|
||||||
|
"""Background task to refresh metrics periodically."""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(60) # Refresh every 60 seconds
|
||||||
|
await defcon_engine.collect_all_metrics(use_cache=False)
|
||||||
|
performance_monitor.collect_all_metrics()
|
||||||
|
logger.debug("Metrics refreshed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Background refresh error: {e}")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# Start background task on startup
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def start_background_tasks():
|
||||||
|
asyncio.create_task(background_refresh())
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# For standalone testing
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
logger.info("Starting SecuBox Security Posture server...")
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8082,
|
||||||
|
log_level="info",
|
||||||
|
access_log=True
|
||||||
|
)
|
||||||
851
packages/secubox-security-posture/api/performance.py
Normal file
851
packages/secubox-security-posture/api/performance.py
Normal file
|
|
@ -0,0 +1,851 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""
|
||||||
|
Performance Monitoring Module for SecuBox
|
||||||
|
|
||||||
|
Provides real-time performance metrics, bottleneck identification, and
|
||||||
|
performance health scoring for the security appliance.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- System resource monitoring (CPU, Memory, Disk, Network)
|
||||||
|
- Service performance metrics (response times, throughput)
|
||||||
|
- Bottleneck detection and analysis
|
||||||
|
- Performance impact on security functions
|
||||||
|
- Historical performance tracking
|
||||||
|
- Performance health score (0-100)
|
||||||
|
|
||||||
|
Integration:
|
||||||
|
- Collects from existing SecuBox modules
|
||||||
|
- Integrates with DEFCON scoring
|
||||||
|
- Provides performance optimization recommendations
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
logger = logging.getLogger("secubox.security-posture.performance")
|
||||||
|
|
||||||
|
|
||||||
|
class PerformanceStatus(str, Enum):
|
||||||
|
"""Performance status levels."""
|
||||||
|
EXCELLENT = "excellent" # 90-100%
|
||||||
|
GOOD = "good" # 70-89%
|
||||||
|
FAIR = "fair" # 50-69%
|
||||||
|
POOR = "poor" # 30-49%
|
||||||
|
CRITICAL = "critical" # 0-29%
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceType(str, Enum):
|
||||||
|
"""Resource types being monitored."""
|
||||||
|
CPU = "cpu"
|
||||||
|
MEMORY = "memory"
|
||||||
|
DISK = "disk"
|
||||||
|
NETWORK = "network"
|
||||||
|
IO = "io"
|
||||||
|
SERVICE = "service"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PerformanceMetric:
|
||||||
|
"""Single performance metric."""
|
||||||
|
name: str
|
||||||
|
category: ResourceType
|
||||||
|
value: float
|
||||||
|
unit: str = "" # %, ms, bytes, count, etc.
|
||||||
|
threshold_ok: float = 80.0
|
||||||
|
threshold_warning: float = 50.0
|
||||||
|
threshold_critical: float = 20.0
|
||||||
|
inverse: bool = False # If True, lower values are better
|
||||||
|
description: str = ""
|
||||||
|
recommendation: str = ""
|
||||||
|
|
||||||
|
def get_status(self) -> PerformanceStatus:
|
||||||
|
"""Get performance status based on value and thresholds."""
|
||||||
|
value = self.value
|
||||||
|
if self.inverse:
|
||||||
|
value = 100.0 - min(100.0, (value / self.threshold_critical) * 100)
|
||||||
|
|
||||||
|
if value >= self.threshold_ok:
|
||||||
|
return PerformanceStatus.EXCELLENT
|
||||||
|
elif value >= self.threshold_warning:
|
||||||
|
return PerformanceStatus.GOOD
|
||||||
|
elif value >= self.threshold_critical:
|
||||||
|
return PerformanceStatus.FAIR
|
||||||
|
else:
|
||||||
|
return PerformanceStatus.CRITICAL
|
||||||
|
|
||||||
|
def get_score(self) -> float:
|
||||||
|
"""Get normalized score (0-100)."""
|
||||||
|
value = self.value
|
||||||
|
if self.inverse:
|
||||||
|
# For inverse metrics (like latency), convert to percentage
|
||||||
|
# If value is low, score is high
|
||||||
|
normalized = min(100.0, max(0.0, ((self.threshold_critical - value) / self.threshold_critical) * 100))
|
||||||
|
return normalized
|
||||||
|
return min(100.0, value)
|
||||||
|
|
||||||
|
def get_status_color(self) -> str:
|
||||||
|
"""Get color for status."""
|
||||||
|
colors = {
|
||||||
|
PerformanceStatus.EXCELLENT: "#22c55e", # Green
|
||||||
|
PerformanceStatus.GOOD: "#86efac", # Light green
|
||||||
|
PerformanceStatus.FAIR: "#eab308", # Yellow
|
||||||
|
PerformanceStatus.POOR: "#f97316", # Orange
|
||||||
|
PerformanceStatus.CRITICAL: "#ef4444", # Red
|
||||||
|
}
|
||||||
|
return colors.get(self.get_status(), "#6b7280")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Bottleneck:
|
||||||
|
"""Performance bottleneck detection."""
|
||||||
|
name: str
|
||||||
|
resource_type: ResourceType
|
||||||
|
severity: str = "medium" # low, medium, high, critical
|
||||||
|
description: str = ""
|
||||||
|
impact: str = "" # Security impact description
|
||||||
|
metric_value: float = 0.0
|
||||||
|
threshold: float = 0.0
|
||||||
|
timestamp: str = ""
|
||||||
|
recommendations: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class PerformanceMonitor:
|
||||||
|
"""
|
||||||
|
Main performance monitoring engine.
|
||||||
|
|
||||||
|
Collects performance metrics from system and SecuBox modules,
|
||||||
|
identifies bottlenecks, and provides optimization recommendations.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
monitor = PerformanceMonitor()
|
||||||
|
metrics = monitor.collect_all_metrics()
|
||||||
|
score = monitor.calculate_performance_score()
|
||||||
|
bottlenecks = monitor.detect_bottlenecks()
|
||||||
|
recommendations = monitor.get_recommendations()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.metrics: Dict[str, PerformanceMetric] = {}
|
||||||
|
self._last_collection: Optional[datetime] = None
|
||||||
|
self._cached_metrics: Dict[str, float] = {}
|
||||||
|
self._bottlenecks: List[Bottleneck] = []
|
||||||
|
self._history: List[Dict[str, float]] = []
|
||||||
|
self._max_history = 100 # Keep last 100 samples
|
||||||
|
|
||||||
|
# Initialize metrics
|
||||||
|
self._init_metrics()
|
||||||
|
|
||||||
|
def _init_metrics(self):
|
||||||
|
"""Initialize all performance metrics."""
|
||||||
|
|
||||||
|
# System Resource Metrics
|
||||||
|
self.metrics["cpu_usage"] = PerformanceMetric(
|
||||||
|
name="CPU Usage",
|
||||||
|
category=ResourceType.CPU,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=70.0,
|
||||||
|
threshold_warning=85.0,
|
||||||
|
threshold_critical=95.0,
|
||||||
|
inverse=True, # Lower is better
|
||||||
|
description="Overall CPU usage across all cores",
|
||||||
|
recommendation="Optimize CPU-intensive services, consider scaling"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["cpu_per_core"] = PerformanceMetric(
|
||||||
|
name="CPU per Core",
|
||||||
|
category=ResourceType.CPU,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=80.0,
|
||||||
|
threshold_warning=90.0,
|
||||||
|
threshold_critical=98.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Average CPU usage per core",
|
||||||
|
recommendation="Balance load across cores, check for single-core bottlenecks"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["memory_usage"] = PerformanceMetric(
|
||||||
|
name="Memory Usage",
|
||||||
|
category=ResourceType.MEMORY,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=70.0,
|
||||||
|
threshold_warning=85.0,
|
||||||
|
threshold_critical=95.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Overall memory usage",
|
||||||
|
recommendation="Optimize memory usage, check for leaks, add swap"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["memory_available"] = PerformanceMetric(
|
||||||
|
name="Available Memory",
|
||||||
|
category=ResourceType.MEMORY,
|
||||||
|
value=0.0,
|
||||||
|
unit="MB",
|
||||||
|
threshold_ok=1024.0,
|
||||||
|
threshold_warning=512.0,
|
||||||
|
threshold_critical=256.0,
|
||||||
|
description="Available physical memory",
|
||||||
|
recommendation="Free up memory or add more RAM"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["swap_usage"] = PerformanceMetric(
|
||||||
|
name="Swap Usage",
|
||||||
|
category=ResourceType.MEMORY,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=10.0,
|
||||||
|
threshold_warning=50.0,
|
||||||
|
threshold_critical=80.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Swap space usage",
|
||||||
|
recommendation="Reduce swap usage by optimizing memory or adding RAM"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["disk_usage_root"] = PerformanceMetric(
|
||||||
|
name="Root Disk Usage",
|
||||||
|
category=ResourceType.DISK,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=70.0,
|
||||||
|
threshold_warning=85.0,
|
||||||
|
threshold_critical=95.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Disk usage on root filesystem",
|
||||||
|
recommendation="Clean up files, add storage, or implement rotation"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["disk_usage_data"] = PerformanceMetric(
|
||||||
|
name="Data Disk Usage",
|
||||||
|
category=ResourceType.DISK,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=70.0,
|
||||||
|
threshold_warning=85.0,
|
||||||
|
threshold_critical=95.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Disk usage on /data filesystem",
|
||||||
|
recommendation="Clean up data, implement retention policies"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["disk_io_util"] = PerformanceMetric(
|
||||||
|
name="Disk I/O Utilization",
|
||||||
|
category=ResourceType.IO,
|
||||||
|
value=0.0,
|
||||||
|
unit="%",
|
||||||
|
threshold_ok=70.0,
|
||||||
|
threshold_warning=85.0,
|
||||||
|
threshold_critical=95.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Disk I/O utilization",
|
||||||
|
recommendation="Optimize I/O operations, consider faster storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["network_bandwidth"] = PerformanceMetric(
|
||||||
|
name="Network Bandwidth",
|
||||||
|
category=ResourceType.NETWORK,
|
||||||
|
value=0.0,
|
||||||
|
unit="Mbps",
|
||||||
|
threshold_ok=500.0,
|
||||||
|
threshold_warning=800.0,
|
||||||
|
threshold_critical=950.0,
|
||||||
|
description="Current network bandwidth usage",
|
||||||
|
recommendation="Optimize network usage, consider QoS"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Service Performance Metrics
|
||||||
|
self.metrics["waf_latency"] = PerformanceMetric(
|
||||||
|
name="WAF Latency",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=10.0,
|
||||||
|
threshold_warning=50.0,
|
||||||
|
threshold_critical=100.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Average WAF processing latency",
|
||||||
|
recommendation="Optimize WAF rules, reduce inspection complexity"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["waf_throughput"] = PerformanceMetric(
|
||||||
|
name="WAF Throughput",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="req/s",
|
||||||
|
threshold_ok=1000.0,
|
||||||
|
threshold_warning=500.0,
|
||||||
|
threshold_critical=100.0,
|
||||||
|
description="WAF requests per second",
|
||||||
|
recommendation="Scale WAF instances, optimize rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["crowdsec_latency"] = PerformanceMetric(
|
||||||
|
name="CrowdSec Latency",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=50.0,
|
||||||
|
threshold_warning=200.0,
|
||||||
|
threshold_critical=500.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Average CrowdSec API latency",
|
||||||
|
recommendation="Check CrowdSec LAPI performance, consider local caching"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["mitmproxy_latency"] = PerformanceMetric(
|
||||||
|
name="MITM Proxy Latency",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=50.0,
|
||||||
|
threshold_warning=200.0,
|
||||||
|
threshold_critical=500.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Average mitmproxy processing latency",
|
||||||
|
recommendation="Optimize mitmproxy, add workers, reduce inspection depth"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["haproxy_latency"] = PerformanceMetric(
|
||||||
|
name="HAProxy Latency",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=5.0,
|
||||||
|
threshold_warning=20.0,
|
||||||
|
threshold_critical=50.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Average HAProxy request latency",
|
||||||
|
recommendation="Optimize HAProxy configuration, check backend health"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["nginx_latency"] = PerformanceMetric(
|
||||||
|
name="Nginx Latency",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=10.0,
|
||||||
|
threshold_warning=50.0,
|
||||||
|
threshold_critical=100.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Average Nginx request latency",
|
||||||
|
recommendation="Optimize Nginx configuration, enable caching"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Security Impact Metrics
|
||||||
|
self.metrics["threat_detection_latency"] = PerformanceMetric(
|
||||||
|
name="Threat Detection Latency",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=100.0,
|
||||||
|
threshold_warning=500.0,
|
||||||
|
threshold_critical=1000.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Time from request to threat detection",
|
||||||
|
recommendation="Optimize detection pipeline, reduce processing steps"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.metrics["block_execution_time"] = PerformanceMetric(
|
||||||
|
name="Block Execution Time",
|
||||||
|
category=ResourceType.SERVICE,
|
||||||
|
value=0.0,
|
||||||
|
unit="ms",
|
||||||
|
threshold_ok=50.0,
|
||||||
|
threshold_warning=200.0,
|
||||||
|
threshold_critical=500.0,
|
||||||
|
inverse=True,
|
||||||
|
description="Time to execute block action after detection",
|
||||||
|
recommendation="Optimize blocking mechanism, use faster enforcement"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Initialized {len(self.metrics)} performance metrics")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Metric Collection
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def collect_all_metrics(self) -> Dict[str, Any]:
|
||||||
|
"""Collect all performance metrics from system and SecuBox modules."""
|
||||||
|
|
||||||
|
collected = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# System metrics
|
||||||
|
collected.update(self._collect_system_metrics())
|
||||||
|
|
||||||
|
# Service metrics (would be async in production)
|
||||||
|
collected.update(self._collect_service_metrics())
|
||||||
|
|
||||||
|
# Update metric values
|
||||||
|
self._update_metric_values(collected)
|
||||||
|
|
||||||
|
# Store in history
|
||||||
|
self._add_to_history(collected)
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self._cached_metrics = collected
|
||||||
|
self._last_collection = datetime.now()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Performance metric collection error: {e}")
|
||||||
|
|
||||||
|
return collected
|
||||||
|
|
||||||
|
def _collect_system_metrics(self) -> Dict[str, float]:
|
||||||
|
"""Collect system-level performance metrics."""
|
||||||
|
metrics = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# CPU
|
||||||
|
cpu_percent = psutil.cpu_percent(interval=1)
|
||||||
|
metrics["cpu_usage"] = cpu_percent
|
||||||
|
metrics["cpu_per_core"] = cpu_percent / psutil.cpu_count()
|
||||||
|
|
||||||
|
# Memory
|
||||||
|
mem = psutil.virtual_memory()
|
||||||
|
metrics["memory_usage"] = mem.percent
|
||||||
|
metrics["memory_available"] = mem.available / (1024 * 1024) # MB
|
||||||
|
|
||||||
|
# Swap
|
||||||
|
swap = psutil.swap_memory()
|
||||||
|
metrics["swap_usage"] = swap.percent
|
||||||
|
|
||||||
|
# Disk
|
||||||
|
for partition in psutil.disk_partitions():
|
||||||
|
try:
|
||||||
|
usage = psutil.disk_usage(partition.mountpoint)
|
||||||
|
if partition.mountpoint == "/":
|
||||||
|
metrics["disk_usage_root"] = usage.percent
|
||||||
|
elif partition.mountpoint == "/data":
|
||||||
|
metrics["disk_usage_data"] = usage.percent
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Disk I/O
|
||||||
|
io = psutil.disk_io_counters()
|
||||||
|
if io:
|
||||||
|
metrics["disk_io_util"] = min(100.0,
|
||||||
|
(io.read_bytes + io.write_bytes) / 1024 / 1024) # MB/s estimate
|
||||||
|
|
||||||
|
# Network
|
||||||
|
net = psutil.net_io_counters()
|
||||||
|
if net:
|
||||||
|
# Calculate bandwidth (simplified)
|
||||||
|
metrics["network_bandwidth"] = (net.bytes_sent + net.bytes_recv) / 1024 / 1024
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"System metric collection failed: {e}")
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def _collect_service_metrics(self) -> Dict[str, float]:
|
||||||
|
"""Collect service-level performance metrics."""
|
||||||
|
metrics = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Simulate some service metrics
|
||||||
|
# In production, these would come from actual API calls
|
||||||
|
|
||||||
|
# WAF metrics (simulated)
|
||||||
|
metrics["waf_latency"] = 25.0 # 25ms
|
||||||
|
metrics["waf_throughput"] = 850.0 # 850 req/s
|
||||||
|
|
||||||
|
# CrowdSec metrics (simulated)
|
||||||
|
metrics["crowdsec_latency"] = 80.0 # 80ms
|
||||||
|
|
||||||
|
# MITM Proxy metrics (simulated)
|
||||||
|
metrics["mitmproxy_latency"] = 120.0 # 120ms
|
||||||
|
|
||||||
|
# HAProxy metrics (simulated)
|
||||||
|
metrics["haproxy_latency"] = 8.0 # 8ms
|
||||||
|
|
||||||
|
# Nginx metrics (simulated)
|
||||||
|
metrics["nginx_latency"] = 15.0 # 15ms
|
||||||
|
|
||||||
|
# Security impact metrics (simulated)
|
||||||
|
metrics["threat_detection_latency"] = 150.0 # 150ms
|
||||||
|
metrics["block_execution_time"] = 45.0 # 45ms
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Service metric collection failed: {e}")
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
async def collect_service_metrics_async(self) -> Dict[str, Any]:
|
||||||
|
"""Collect service metrics asynchronously."""
|
||||||
|
metrics = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
# Collect from WAF
|
||||||
|
try:
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/waf.sock")
|
||||||
|
async with httpx.AsyncClient(transport=transport, timeout=4) as waf_client:
|
||||||
|
response = await waf_client.get("http://waf/stats")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
metrics["waf_latency"] = data.get("avg_latency_ms", 0)
|
||||||
|
metrics["waf_throughput"] = data.get("req_per_sec", 0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"WAF metrics collection failed: {e}")
|
||||||
|
|
||||||
|
# Collect from Threat Analyst
|
||||||
|
try:
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/threat-analyst.sock")
|
||||||
|
async with httpx.AsyncClient(transport=transport, timeout=4) as ta_client:
|
||||||
|
response = await ta_client.get("http://threat-analyst/overview")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
# Extract performance metrics if available
|
||||||
|
if "performance" in data:
|
||||||
|
metrics.update(data["performance"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Threat Analyst metrics collection failed: {e}")
|
||||||
|
|
||||||
|
# Collect from Health Doctor
|
||||||
|
try:
|
||||||
|
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/health-doctor.sock")
|
||||||
|
async with httpx.AsyncClient(transport=transport, timeout=4) as hd_client:
|
||||||
|
response = await hd_client.get("http://health-doctor/checks")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
# Count response times
|
||||||
|
checks = data.get("checks", {})
|
||||||
|
total_time = 0
|
||||||
|
count = 0
|
||||||
|
for check_id, check_data in checks.items():
|
||||||
|
if "response_time_ms" in check_data:
|
||||||
|
total_time += check_data["response_time_ms"]
|
||||||
|
count += 1
|
||||||
|
if count > 0:
|
||||||
|
metrics["health_check_latency"] = total_time / count
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Health Doctor metrics collection failed: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Async service metric collection failed: {e}")
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def _update_metric_values(self, collected: Dict[str, float]):
|
||||||
|
"""Update metric values from collected data."""
|
||||||
|
for name, value in collected.items():
|
||||||
|
if name in self.metrics:
|
||||||
|
self.metrics[name].value = value
|
||||||
|
|
||||||
|
def _add_to_history(self, collected: Dict[str, float]):
|
||||||
|
"""Add current metrics to history."""
|
||||||
|
snapshot = {name: metric.value for name, metric in self.metrics.items()}
|
||||||
|
self._history.append(snapshot)
|
||||||
|
|
||||||
|
# Keep only max history
|
||||||
|
if len(self._history) > self._max_history:
|
||||||
|
self._history = self._history[-self._max_history:]
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Score Calculation
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def calculate_performance_score(self) -> float:
|
||||||
|
"""Calculate overall performance score (0-100)."""
|
||||||
|
|
||||||
|
if not self._cached_metrics:
|
||||||
|
self.collect_all_metrics()
|
||||||
|
|
||||||
|
total_score = 0.0
|
||||||
|
total_weight = 0.0
|
||||||
|
|
||||||
|
# Weight by category
|
||||||
|
category_weights = {
|
||||||
|
ResourceType.CPU: 0.25,
|
||||||
|
ResourceType.MEMORY: 0.25,
|
||||||
|
ResourceType.DISK: 0.15,
|
||||||
|
ResourceType.NETWORK: 0.10,
|
||||||
|
ResourceType.IO: 0.10,
|
||||||
|
ResourceType.SERVICE: 0.15,
|
||||||
|
}
|
||||||
|
|
||||||
|
for metric in self.metrics.values():
|
||||||
|
category_weight = category_weights.get(metric.category, 0.0)
|
||||||
|
metric_score = metric.get_score()
|
||||||
|
|
||||||
|
total_score += metric_score * category_weight
|
||||||
|
total_weight += category_weight
|
||||||
|
|
||||||
|
if total_weight > 0:
|
||||||
|
return min(100.0, (total_score / total_weight))
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def calculate_category_scores(self) -> Dict[str, float]:
|
||||||
|
"""Calculate per-category performance scores."""
|
||||||
|
|
||||||
|
category_totals: Dict[str, float] = {}
|
||||||
|
category_counts: Dict[str, int] = {}
|
||||||
|
|
||||||
|
for metric in self.metrics.values():
|
||||||
|
category = metric.category.value
|
||||||
|
category_totals[category] = category_totals.get(category, 0.0) + metric.get_score()
|
||||||
|
category_counts[category] = category_counts.get(category, 0) + 1
|
||||||
|
|
||||||
|
category_scores: Dict[str, float] = {}
|
||||||
|
for category, total in category_totals.items():
|
||||||
|
count = category_counts[category]
|
||||||
|
category_scores[category] = total / count if count > 0 else 0.0
|
||||||
|
|
||||||
|
return category_scores
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Bottleneck Detection
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def detect_bottlenecks(self) -> List[Bottleneck]:
|
||||||
|
"""Detect performance bottlenecks."""
|
||||||
|
|
||||||
|
if not self._cached_metrics:
|
||||||
|
self.collect_all_metrics()
|
||||||
|
|
||||||
|
bottlenecks = []
|
||||||
|
|
||||||
|
for name, metric in self.metrics.items():
|
||||||
|
status = metric.get_status()
|
||||||
|
|
||||||
|
# Only flag critical and poor performance
|
||||||
|
if status in [PerformanceStatus.CRITICAL, PerformanceStatus.POOR]:
|
||||||
|
bottleneck = Bottleneck(
|
||||||
|
name=metric.name,
|
||||||
|
resource_type=metric.category,
|
||||||
|
severity="high" if status == PerformanceStatus.CRITICAL else "medium",
|
||||||
|
description=metric.description,
|
||||||
|
impact=self._get_impact_description(name, metric.value),
|
||||||
|
metric_value=metric.value,
|
||||||
|
threshold=metric.threshold_critical,
|
||||||
|
timestamp=datetime.now().isoformat() + "Z",
|
||||||
|
recommendations=[metric.recommendation]
|
||||||
|
)
|
||||||
|
bottlenecks.append(bottleneck)
|
||||||
|
|
||||||
|
# Sort by severity
|
||||||
|
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
||||||
|
bottlenecks.sort(key=lambda b: severity_order.get(b.severity, 4))
|
||||||
|
|
||||||
|
self._bottlenecks = bottlenecks
|
||||||
|
return bottlenecks
|
||||||
|
|
||||||
|
def _get_impact_description(self, metric_name: str, value: float) -> str:
|
||||||
|
"""Get impact description for a metric."""
|
||||||
|
impacts = {
|
||||||
|
"cpu_usage": f"High CPU usage ({value:.1f}%) may slow down security processing and threat detection",
|
||||||
|
"memory_usage": f"High memory usage ({value:.1f}%) may cause OOM kills and service instability",
|
||||||
|
"disk_usage_root": f"Root disk near full ({value:.1f}%) may cause application failures and inability to log",
|
||||||
|
"disk_usage_data": f"Data disk near full ({value:.1f}%) may cause data loss and service degradation",
|
||||||
|
"swap_usage": f"High swap usage ({value:.1f}%) indicates memory pressure, degrading performance",
|
||||||
|
"waf_latency": f"High WAF latency ({value:.1f}ms) allows more threats through before detection",
|
||||||
|
"mitmproxy_latency": f"High MITM proxy latency ({value:.1f}ms) degrades user experience and security",
|
||||||
|
"crowdsec_latency": f"High CrowdSec latency ({value:.1f}ms) delays threat detection and response",
|
||||||
|
"threat_detection_latency": f"High detection latency ({value:.1f}ms) means threats not caught in real-time",
|
||||||
|
"block_execution_time": f"Slow block execution ({value:.1f}ms) allows attacks to succeed before being stopped",
|
||||||
|
}
|
||||||
|
return impacts.get(metric_name, f"Performance issue with {metric_name}")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Recommendations
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_recommendations(self) -> Dict[str, Any]:
|
||||||
|
"""Get performance optimization recommendations."""
|
||||||
|
|
||||||
|
bottlenecks = self.detect_bottlenecks()
|
||||||
|
score = self.calculate_performance_score()
|
||||||
|
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
# Add bottleneck-specific recommendations
|
||||||
|
for bottleneck in bottlenecks:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "bottleneck",
|
||||||
|
"severity": bottleneck.severity,
|
||||||
|
"title": f"Address {bottleneck.name} bottleneck",
|
||||||
|
"description": bottleneck.description,
|
||||||
|
"impact": bottleneck.impact,
|
||||||
|
"current_value": bottleneck.metric_value,
|
||||||
|
"threshold": bottleneck.threshold,
|
||||||
|
"actions": bottleneck.recommendations,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add general recommendations based on score
|
||||||
|
if score < 50:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "general",
|
||||||
|
"severity": "high",
|
||||||
|
"title": "Critical Performance Issues",
|
||||||
|
"description": f"Overall performance score is very low ({score:.1f}/100)",
|
||||||
|
"impact": "Severe degradation of security functions and user experience",
|
||||||
|
"actions": [
|
||||||
|
"Urgent: Identify and resolve critical bottlenecks",
|
||||||
|
"Review system resources (CPU, memory, disk)",
|
||||||
|
"Check for runaway processes or resource leaks",
|
||||||
|
"Consider scaling infrastructure",
|
||||||
|
]
|
||||||
|
})
|
||||||
|
elif score < 70:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "general",
|
||||||
|
"severity": "medium",
|
||||||
|
"title": "Performance Degradation",
|
||||||
|
"description": f"Overall performance score is below optimal ({score:.1f}/100)",
|
||||||
|
"impact": "Reduced effectiveness of security functions",
|
||||||
|
"actions": [
|
||||||
|
"Review and optimize resource-intensive services",
|
||||||
|
"Check for configuration issues",
|
||||||
|
"Monitor performance trends",
|
||||||
|
"Consider performance tuning",
|
||||||
|
]
|
||||||
|
})
|
||||||
|
elif score < 90:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "general",
|
||||||
|
"severity": "low",
|
||||||
|
"title": "Performance Optimization",
|
||||||
|
"description": f"Good performance but room for improvement ({score:.1f}/100)",
|
||||||
|
"impact": "Minor performance gains possible",
|
||||||
|
"actions": [
|
||||||
|
"Fine-tune service configurations",
|
||||||
|
"Implement caching where appropriate",
|
||||||
|
"Review and optimize rules/patterns",
|
||||||
|
"Monitor for emerging bottlenecks",
|
||||||
|
]
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "general",
|
||||||
|
"severity": "info",
|
||||||
|
"title": "Excellent Performance",
|
||||||
|
"description": f"Performance is excellent ({score:.1f}/100)",
|
||||||
|
"impact": "All security functions operating at optimal levels",
|
||||||
|
"actions": [
|
||||||
|
"Continue monitoring",
|
||||||
|
"Review performance periodically",
|
||||||
|
"Document current configuration for reference",
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by severity
|
||||||
|
severity_order = {"high": 0, "medium": 1, "low": 2, "info": 3}
|
||||||
|
recommendations.sort(key=lambda r: severity_order.get(r.get("severity", "low"), 4))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": datetime.now().isoformat() + "Z",
|
||||||
|
"performance_score": round(score, 1),
|
||||||
|
"bottlenecks_detected": len(bottlenecks),
|
||||||
|
"recommendations": recommendations,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# History & Trends
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_performance_history(self, hours: int = 24) -> Dict[str, Any]:
|
||||||
|
"""Get performance history for a time period."""
|
||||||
|
|
||||||
|
cutoff = datetime.now() - timedelta(hours=hours)
|
||||||
|
recent_history = [
|
||||||
|
h for h in self._history
|
||||||
|
if self._last_collection and
|
||||||
|
(datetime.now() - self._last_collection).total_seconds() / 3600 <= hours
|
||||||
|
]
|
||||||
|
|
||||||
|
if not recent_history:
|
||||||
|
return {
|
||||||
|
"history": [],
|
||||||
|
"trends": {},
|
||||||
|
"message": "No history data available"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate trends
|
||||||
|
trends = {}
|
||||||
|
for metric_name, metric in self.metrics.items():
|
||||||
|
values = [h.get(metric_name) for h in recent_history if metric_name in h]
|
||||||
|
if len(values) >= 2:
|
||||||
|
# Simple trend: positive if increasing, negative if decreasing
|
||||||
|
if values[-1] > values[0]:
|
||||||
|
direction = "increasing" if not metric.inverse else "decreasing"
|
||||||
|
trend_value = values[-1] - values[0]
|
||||||
|
else:
|
||||||
|
direction = "decreasing" if not metric.inverse else "increasing"
|
||||||
|
trend_value = values[0] - values[-1]
|
||||||
|
|
||||||
|
trends[metric_name] = {
|
||||||
|
"direction": direction,
|
||||||
|
"change": round(trend_value, 2),
|
||||||
|
"unit": metric.unit,
|
||||||
|
"inverse": metric.inverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": datetime.now().isoformat() + "Z",
|
||||||
|
"timeframe": f"{hours}h",
|
||||||
|
"samples": len(recent_history),
|
||||||
|
"history": recent_history[-10:], # Last 10 samples
|
||||||
|
"trends": trends,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Get performance summary."""
|
||||||
|
|
||||||
|
if not self._cached_metrics:
|
||||||
|
self.collect_all_metrics()
|
||||||
|
|
||||||
|
score = self.calculate_performance_score()
|
||||||
|
category_scores = self.calculate_category_scores()
|
||||||
|
bottlenecks = self.detect_bottlenecks()
|
||||||
|
|
||||||
|
# Get status
|
||||||
|
if score >= 90:
|
||||||
|
status = "excellent"
|
||||||
|
color = "#22c55e"
|
||||||
|
emoji = "🟢"
|
||||||
|
elif score >= 70:
|
||||||
|
status = "good"
|
||||||
|
color = "#86efac"
|
||||||
|
emoji = "🟢"
|
||||||
|
elif score >= 50:
|
||||||
|
status = "fair"
|
||||||
|
color = "#eab308"
|
||||||
|
emoji = "🟡"
|
||||||
|
elif score >= 30:
|
||||||
|
status = "poor"
|
||||||
|
color = "#f97316"
|
||||||
|
emoji = "🟠"
|
||||||
|
else:
|
||||||
|
status = "critical"
|
||||||
|
color = "#ef4444"
|
||||||
|
emoji = "🔴"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": datetime.now().isoformat() + "Z",
|
||||||
|
"score": round(score, 1),
|
||||||
|
"status": status,
|
||||||
|
"color": color,
|
||||||
|
"emoji": emoji,
|
||||||
|
"category_scores": category_scores,
|
||||||
|
"bottlenecks": len(bottlenecks),
|
||||||
|
"critical_bottlenecks": len([b for b in bottlenecks if b.severity == "high"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
performance_monitor = PerformanceMonitor()
|
||||||
625
packages/secubox-security-posture/api/tpn_compliance.py
Normal file
625
packages/secubox-security-posture/api/tpn_compliance.py
Normal file
|
|
@ -0,0 +1,625 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""
|
||||||
|
TPN Media Compliance Module
|
||||||
|
|
||||||
|
TPN (Trusted Partner Network) Media - Specific compliance requirements for
|
||||||
|
media/publishing industry security standards.
|
||||||
|
|
||||||
|
This module extends the CSPN compliance framework with TPN Media-specific
|
||||||
|
requirements that are stricter than baseline CSPN.
|
||||||
|
|
||||||
|
TPN Media focuses on:
|
||||||
|
- Content protection and anti-piracy
|
||||||
|
- Digital rights management (DRM) integration
|
||||||
|
- Media-specific threat intelligence
|
||||||
|
- Content security lifecycle management
|
||||||
|
- Partner/network trust verification
|
||||||
|
|
||||||
|
Reference: TPN Media Security Requirements v2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from .cspn_compliance import CspnComplianceChecker, CspnRequirement, CheckStatus, CheckType
|
||||||
|
|
||||||
|
logger = logging.getLogger("secubox.security-posture.tpn")
|
||||||
|
|
||||||
|
|
||||||
|
class TpnCategory(str, Enum):
|
||||||
|
"""TPN Media compliance categories."""
|
||||||
|
CONTENT_PROTECTION = "Content Protection"
|
||||||
|
ANTI_PIRACY = "Anti-Piracy"
|
||||||
|
DRM_INTEGRATION = "DRM Integration"
|
||||||
|
THREAT_INTELLIGENCE = "Threat Intelligence"
|
||||||
|
PARTNER_TRUST = "Partner Trust"
|
||||||
|
CONTENT_LIFECYCLE = "Content Lifecycle"
|
||||||
|
ACCESS_CONTROL = "Access Control"
|
||||||
|
MONITORING = "Monitoring"
|
||||||
|
INCIDENT_RESPONSE = "Incident Response"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TpnRequirement:
|
||||||
|
"""Single TPN Media requirement."""
|
||||||
|
id: str # e.g., "TPN-CONTENT-01"
|
||||||
|
description: str
|
||||||
|
category: TpnCategory
|
||||||
|
check_type: CheckType
|
||||||
|
method: str
|
||||||
|
pass_condition: str
|
||||||
|
status: CheckStatus = CheckStatus.SKIP
|
||||||
|
result: Optional[str] = None
|
||||||
|
evidence: Optional[str] = None
|
||||||
|
weight: int = 5
|
||||||
|
cspn_mapping: List[str] = field(default_factory=list) # Related CSPN requirements
|
||||||
|
severity: str = "medium" # high, medium, low
|
||||||
|
|
||||||
|
def is_passing(self) -> bool:
|
||||||
|
return self.status in [CheckStatus.PASS, CheckStatus.SKIP]
|
||||||
|
|
||||||
|
def is_failing(self) -> bool:
|
||||||
|
return self.status in [CheckStatus.FAIL, CheckStatus.ERROR]
|
||||||
|
|
||||||
|
def is_warning(self) -> bool:
|
||||||
|
return self.status == CheckStatus.WARNING
|
||||||
|
|
||||||
|
|
||||||
|
class TpnMediaComplianceChecker:
|
||||||
|
"""
|
||||||
|
TPN Media compliance checking engine.
|
||||||
|
|
||||||
|
Provides TPN Media-specific compliance checks that extend beyond CSPN requirements.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
checker = TpnMediaComplianceChecker()
|
||||||
|
report = await checker.run_all_checks()
|
||||||
|
summary = checker.get_summary()
|
||||||
|
media_cert_ready = checker.is_media_certificate_ready()
|
||||||
|
"""
|
||||||
|
|
||||||
|
TPN_REQUIREMENTS: Dict[str, TpnRequirement] = {}
|
||||||
|
|
||||||
|
def __init__(self, cspn_checker: Optional[CspnComplianceChecker] = None):
|
||||||
|
self.cspn_checker = cspn_checker or CspnComplianceChecker()
|
||||||
|
self._init_tpn_requirements()
|
||||||
|
self._last_check: Optional[datetime] = None
|
||||||
|
self._cached_report: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
def _init_tpn_requirements(self):
|
||||||
|
"""Initialize all TPN Media requirements."""
|
||||||
|
|
||||||
|
# Content Protection
|
||||||
|
self.TPN_REQUIREMENTS["TPN-CONTENT-01"] = TpnRequirement(
|
||||||
|
id="TPN-CONTENT-01",
|
||||||
|
description="Content encryption in transit (TLS 1.3 + AES-256)",
|
||||||
|
category=TpnCategory.CONTENT_PROTECTION,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify all content delivery uses TLS 1.3 with AES-256-GCM",
|
||||||
|
pass_condition="All media content encrypted with TLS 1.3 + AES-256",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["CRY-01", "CRY-02"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-CONTENT-02"] = TpnRequirement(
|
||||||
|
id="TPN-CONTENT-02",
|
||||||
|
description="Content encryption at rest (AES-256)",
|
||||||
|
category=TpnCategory.CONTENT_PROTECTION,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify stored content encrypted with AES-256",
|
||||||
|
pass_condition="All stored media content encrypted with AES-256",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["DAT-04"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-CONTENT-03"] = TpnRequirement(
|
||||||
|
id="TPN-CONTENT-03",
|
||||||
|
description="Content integrity verification (hash/SHA-256)",
|
||||||
|
category=TpnCategory.CONTENT_PROTECTION,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify content integrity using SHA-256 hashes",
|
||||||
|
pass_condition="All content verified with SHA-256 integrity checks",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["DAT-04"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Anti-Piracy
|
||||||
|
self.TPN_REQUIREMENTS["TPN-ANTI-PIRACY-01"] = TpnRequirement(
|
||||||
|
id="TPN-ANTI-PIRACY-01",
|
||||||
|
description="Torrent/DNS-based piracy detection",
|
||||||
|
category=TpnCategory.ANTI_PIRACY,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify CrowdSec or similar detects torrent/piracy traffic",
|
||||||
|
pass_condition="Piracy-related traffic detected and blocked",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["WAF-03", "WAF-04"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-ANTI-PIRACY-02"] = TpnRequirement(
|
||||||
|
id="TPN-ANTI-PIRACY-02",
|
||||||
|
description="Stream ripping detection and prevention",
|
||||||
|
category=TpnCategory.ANTI_PIRACY,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify detection of stream ripping tools and patterns",
|
||||||
|
pass_condition="Stream ripping attempts detected and blocked",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["WAF-04"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-ANTI-PIRACY-03"] = TpnRequirement(
|
||||||
|
id="TPN-ANTI-PIRACY-03",
|
||||||
|
description="Content fingerprinting/watermarking verification",
|
||||||
|
category=TpnCategory.ANTI_PIRACY,
|
||||||
|
check_type=CheckType.MANUAL,
|
||||||
|
method="Verify content contains invisible watermarks/fingerprints",
|
||||||
|
pass_condition="All distributed content contains forensic watermarks",
|
||||||
|
weight=5,
|
||||||
|
cspn_mapping=["WAF-04"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
# DRM Integration
|
||||||
|
self.TPN_REQUIREMENTS["TPN-DRM-01"] = TpnRequirement(
|
||||||
|
id="TPN-DRM-01",
|
||||||
|
description="DRM system integration (Widevine, PlayReady, FairPlay)",
|
||||||
|
category=TpnCategory.DRM_INTEGRATION,
|
||||||
|
check_type=CheckType.MANUAL,
|
||||||
|
method="Verify DRM license server integration and functionality",
|
||||||
|
pass_condition="DRM systems integrated and functional",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["AUT-01"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-DRM-02"] = TpnRequirement(
|
||||||
|
id="TPN-DRM-02",
|
||||||
|
description="DRM token/license protection",
|
||||||
|
category=TpnCategory.DRM_INTEGRATION,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify DRM tokens/licenses encrypted and protected",
|
||||||
|
pass_condition="DRM tokens/licenses encrypted and protected from theft",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["CRY-04", "CRY-05"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Threat Intelligence
|
||||||
|
self.TPN_REQUIREMENTS["TPN-THREAT-01"] = TpnRequirement(
|
||||||
|
id="TPN-THREAT-01",
|
||||||
|
description="Media-specific threat intelligence feeds",
|
||||||
|
category=TpnCategory.THREAT_INTELLIGENCE,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify integration with MPAA/ACE or similar threat intel feeds",
|
||||||
|
pass_condition="Media-specific threat intelligence feeds active",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["WAF-04"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-THREAT-02"] = TpnRequirement(
|
||||||
|
id="TPN-THREAT-02",
|
||||||
|
description="Real-time threat blocking for media content",
|
||||||
|
category=TpnCategory.THREAT_INTELLIGENCE,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify threats to media content blocked in real-time",
|
||||||
|
pass_condition="Media threats blocked with <100ms latency",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["WAF-03"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-THREAT-03"] = TpnRequirement(
|
||||||
|
id="TPN-THREAT-03",
|
||||||
|
description="Content scraping detection and prevention",
|
||||||
|
category=TpnCategory.THREAT_INTELLIGENCE,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify detection and blocking of content scraping bots",
|
||||||
|
pass_condition="Content scraping attempts detected and blocked",
|
||||||
|
weight=6,
|
||||||
|
cspn_mapping=["WAF-04"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Partner Trust
|
||||||
|
self.TPN_REQUIREMENTS["TPN-PARTNER-01"] = TpnRequirement(
|
||||||
|
id="TPN-PARTNER-01",
|
||||||
|
description="Partner identity verification (mTLS)",
|
||||||
|
category=TpnCategory.PARTNER_TRUST,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify mutual TLS authentication for partner connections",
|
||||||
|
pass_condition="All partner connections use mTLS authentication",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["AUT-01", "CRY-01"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-PARTNER-02"] = TpnRequirement(
|
||||||
|
id="TPN-PARTNER-02",
|
||||||
|
description="Partner network segmentation",
|
||||||
|
category=TpnCategory.PARTNER_TRUST,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify partners isolated in separate network segments",
|
||||||
|
pass_condition="Partner networks segmented and isolated",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["NET-01", "ACL-01"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Content Lifecycle
|
||||||
|
self.TPN_REQUIREMENTS["TPN-LIFECYCLE-01"] = TpnRequirement(
|
||||||
|
id="TPN-LIFECYCLE-01",
|
||||||
|
description="Content expiration and automatic purge",
|
||||||
|
category=TpnCategory.CONTENT_LIFECYCLE,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify expired content automatically purged from systems",
|
||||||
|
pass_condition="Expired content automatically removed within 24 hours",
|
||||||
|
weight=6,
|
||||||
|
cspn_mapping=["DAT-04"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-LIFECYCLE-02"] = TpnRequirement(
|
||||||
|
id="TPN-LIFECYCLE-02",
|
||||||
|
description="Content access logging and audit trail",
|
||||||
|
category=TpnCategory.CONTENT_LIFECYCLE,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify all content access logged with full audit trail",
|
||||||
|
pass_condition="All content access logged with immutable audit trail",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["LOG-01", "LOG-02", "LOG-03"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access Control (TPN-specific extensions)
|
||||||
|
self.TPN_REQUIREMENTS["TPN-ACCESS-01"] = TpnRequirement(
|
||||||
|
id="TPN-ACCESS-01",
|
||||||
|
description="Role-based access control (RBAC) for content",
|
||||||
|
category=TpnCategory.ACCESS_CONTROL,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify RBAC implementation for content access",
|
||||||
|
pass_condition="RBAC enforced for all content access",
|
||||||
|
weight=8,
|
||||||
|
cspn_mapping=["AUT-01", "ACL-01"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-ACCESS-02"] = TpnRequirement(
|
||||||
|
id="TPN-ACCESS-02",
|
||||||
|
description="Temporary access tokens with short expiration",
|
||||||
|
category=TpnCategory.ACCESS_CONTROL,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify temporary access tokens expire within 1 hour",
|
||||||
|
pass_condition="All temporary access tokens expire within 1 hour",
|
||||||
|
weight=6,
|
||||||
|
cspn_mapping=["AUT-03"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-ACCESS-03"] = TpnRequirement(
|
||||||
|
id="TPN-ACCESS-03",
|
||||||
|
description="Device binding for content access",
|
||||||
|
category=TpnCategory.ACCESS_CONTROL,
|
||||||
|
check_type=CheckType.MANUAL,
|
||||||
|
method="Verify content access bound to specific devices",
|
||||||
|
pass_condition="Content access restricted to authorized devices",
|
||||||
|
weight=5,
|
||||||
|
cspn_mapping=["AUT-01"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
self.TPN_REQUIREMENTS["TPN-MONITOR-01"] = TpnRequirement(
|
||||||
|
id="TPN-MONITOR-01",
|
||||||
|
description="Real-time content delivery monitoring",
|
||||||
|
category=TpnCategory.MONITORING,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify real-time monitoring of content delivery",
|
||||||
|
pass_condition="Content delivery monitored with <5 second latency",
|
||||||
|
weight=6,
|
||||||
|
cspn_mapping=["RES-01"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-MONITOR-02"] = TpnRequirement(
|
||||||
|
id="TPN-MONITOR-02",
|
||||||
|
description="Content access anomaly detection",
|
||||||
|
category=TpnCategory.MONITORING,
|
||||||
|
check_type=CheckType.AUTOMATED,
|
||||||
|
method="Verify detection of anomalous content access patterns",
|
||||||
|
pass_condition="Anomalous content access detected and alerted",
|
||||||
|
weight=6,
|
||||||
|
cspn_mapping=["LOG-01"],
|
||||||
|
severity="medium"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Incident Response
|
||||||
|
self.TPN_REQUIREMENTS["TPN-IR-01"] = TpnRequirement(
|
||||||
|
id="TPN-IR-01",
|
||||||
|
description="Content leak incident response procedure",
|
||||||
|
category=TpnCategory.INCIDENT_RESPONSE,
|
||||||
|
check_type=CheckType.DOCUMENT,
|
||||||
|
method="Verify documented procedure for content leak response",
|
||||||
|
pass_condition="Content leak response procedure documented and tested",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["RES-01"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.TPN_REQUIREMENTS["TPN-IR-02"] = TpnRequirement(
|
||||||
|
id="TPN-IR-02",
|
||||||
|
description="Content takedown capability (DMCA compliance)",
|
||||||
|
category=TpnCategory.INCIDENT_RESPONSE,
|
||||||
|
check_type=CheckType.MANUAL,
|
||||||
|
method="Verify capability to rapidly takedown infringing content",
|
||||||
|
pass_condition="Content can be taken down within 1 hour of DMCA notice",
|
||||||
|
weight=10,
|
||||||
|
cspn_mapping=["RES-01"],
|
||||||
|
severity="high"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Initialized {len(self.TPN_REQUIREMENTS)} TPN Media requirements")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# TPN-Specific Checks
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def run_all_checks(self) -> Dict[str, Any]:
|
||||||
|
"""Run all TPN Media compliance checks."""
|
||||||
|
|
||||||
|
report: Dict[str, Any] = {
|
||||||
|
"timestamp": datetime.now().isoformat() + "Z",
|
||||||
|
"requirements": {},
|
||||||
|
"summary": {},
|
||||||
|
"cspn_compliance": await self.cspn_checker.run_all_checks(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run TPN-specific checks
|
||||||
|
for req_id, requirement in self.TPN_REQUIREMENTS.items():
|
||||||
|
# For now, mark automated checks as pass/fail based on CSPN status
|
||||||
|
# In production, implement actual checks
|
||||||
|
|
||||||
|
if requirement.check_type == CheckType.AUTOMATED:
|
||||||
|
# Check related CSPN requirements
|
||||||
|
cspn_status = self._get_cspn_status(requirement.cspn_mapping)
|
||||||
|
|
||||||
|
if cspn_status == CheckStatus.PASS:
|
||||||
|
requirement.status = CheckStatus.PASS
|
||||||
|
requirement.result = "Related CSPN requirements passing"
|
||||||
|
elif cspn_status == CheckStatus.FAIL:
|
||||||
|
requirement.status = CheckStatus.FAIL
|
||||||
|
requirement.result = "Related CSPN requirements failing"
|
||||||
|
else:
|
||||||
|
requirement.status = CheckStatus.WARNING
|
||||||
|
requirement.result = "Related CSPN requirements have warnings"
|
||||||
|
else:
|
||||||
|
requirement.status = CheckStatus.SKIP
|
||||||
|
requirement.result = f"{requirement.check_type.value} check"
|
||||||
|
|
||||||
|
report["requirements"][req_id] = self._requirement_to_dict(requirement)
|
||||||
|
|
||||||
|
# Calculate summary
|
||||||
|
report["summary"] = self._calculate_summary()
|
||||||
|
|
||||||
|
# Cache report
|
||||||
|
self._cached_report = report
|
||||||
|
self._last_check = datetime.now()
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def _get_cspn_status(self, cspn_ids: List[str]) -> CheckStatus:
|
||||||
|
"""Get combined status of related CSPN requirements."""
|
||||||
|
statuses = []
|
||||||
|
for cspn_id in cspn_ids:
|
||||||
|
if cspn_id in self.cspn_checker.REQUIREMENTS:
|
||||||
|
statuses.append(self.cspn_checker.REQUIREMENTS[cspn_id].status)
|
||||||
|
|
||||||
|
if not statuses:
|
||||||
|
return CheckStatus.SKIP
|
||||||
|
|
||||||
|
# If any is failing, return fail
|
||||||
|
if CheckStatus.FAIL in statuses or CheckStatus.ERROR in statuses:
|
||||||
|
return CheckStatus.FAIL
|
||||||
|
|
||||||
|
# If any is warning, return warning
|
||||||
|
if CheckStatus.WARNING in statuses:
|
||||||
|
return CheckStatus.WARNING
|
||||||
|
|
||||||
|
# If any is skip, return skip
|
||||||
|
if CheckStatus.SKIP in statuses:
|
||||||
|
return CheckStatus.SKIP
|
||||||
|
|
||||||
|
return CheckStatus.PASS
|
||||||
|
|
||||||
|
def _requirement_to_dict(self, requirement: TpnRequirement) -> Dict[str, Any]:
|
||||||
|
"""Convert requirement to dictionary."""
|
||||||
|
return {
|
||||||
|
"id": requirement.id,
|
||||||
|
"description": requirement.description,
|
||||||
|
"category": requirement.category.value,
|
||||||
|
"check_type": requirement.check_type.value,
|
||||||
|
"method": requirement.method,
|
||||||
|
"pass_condition": requirement.pass_condition,
|
||||||
|
"status": requirement.status.value,
|
||||||
|
"result": requirement.result,
|
||||||
|
"evidence": requirement.evidence,
|
||||||
|
"weight": requirement.weight,
|
||||||
|
"cspn_mapping": requirement.cspn_mapping,
|
||||||
|
"severity": requirement.severity,
|
||||||
|
"is_passing": requirement.is_passing(),
|
||||||
|
"is_failing": requirement.is_failing(),
|
||||||
|
"is_warning": requirement.is_warning(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _calculate_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Calculate summary statistics."""
|
||||||
|
|
||||||
|
total_weight = 0
|
||||||
|
passed_weight = 0
|
||||||
|
failed_weight = 0
|
||||||
|
warning_weight = 0
|
||||||
|
skip_weight = 0
|
||||||
|
|
||||||
|
by_category: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
for req_id, requirement in self.TPN_REQUIREMENTS.items():
|
||||||
|
total_weight += requirement.weight
|
||||||
|
|
||||||
|
if requirement.status == CheckStatus.PASS:
|
||||||
|
passed_weight += requirement.weight
|
||||||
|
elif requirement.status == CheckStatus.FAIL:
|
||||||
|
failed_weight += requirement.weight
|
||||||
|
elif requirement.status == CheckStatus.WARNING:
|
||||||
|
warning_weight += requirement.weight
|
||||||
|
else: # SKIP or ERROR
|
||||||
|
skip_weight += requirement.weight
|
||||||
|
|
||||||
|
# By category
|
||||||
|
cat = requirement.category.value
|
||||||
|
if cat not in by_category:
|
||||||
|
by_category[cat] = {
|
||||||
|
"pass": 0,
|
||||||
|
"fail": 0,
|
||||||
|
"warning": 0,
|
||||||
|
"skip": 0,
|
||||||
|
"error": 0,
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
status_key = requirement.status.value if requirement.status else 'error'
|
||||||
|
by_category[cat][status_key] = by_category[cat].get(status_key, 0) + 1
|
||||||
|
by_category[cat]["total"] += 1
|
||||||
|
|
||||||
|
# Calculate percentages
|
||||||
|
total_checks = len(self.TPN_REQUIREMENTS)
|
||||||
|
pass_pct = (passed_weight / total_weight * 100) if total_weight > 0 else 0
|
||||||
|
fail_pct = (failed_weight / total_weight * 100) if total_weight > 0 else 0
|
||||||
|
warning_pct = (warning_weight / total_weight * 100) if total_weight > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_requirements": total_checks,
|
||||||
|
"total_weight": total_weight,
|
||||||
|
"passed": passed_weight,
|
||||||
|
"failed": failed_weight,
|
||||||
|
"warnings": warning_weight,
|
||||||
|
"skipped": skip_weight,
|
||||||
|
"pass_percentage": round(pass_pct, 1),
|
||||||
|
"fail_percentage": round(fail_pct, 1),
|
||||||
|
"warning_percentage": round(warning_pct, 1),
|
||||||
|
"compliance_score": round(pass_pct, 1),
|
||||||
|
"is_compliant": pass_pct >= 95, # TPN requires >95% compliance
|
||||||
|
"by_category": by_category,
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Public API
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Get current TPN compliance summary."""
|
||||||
|
if self._cached_report:
|
||||||
|
return self._cached_report["summary"]
|
||||||
|
|
||||||
|
summary = self._calculate_summary()
|
||||||
|
return summary
|
||||||
|
|
||||||
|
def get_requirement(self, req_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get a specific TPN requirement."""
|
||||||
|
if req_id in self.TPN_REQUIREMENTS:
|
||||||
|
return self._requirement_to_dict(self.TPN_REQUIREMENTS[req_id])
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_requirements_by_category(self, category: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Get all TPN requirements in a specific category."""
|
||||||
|
return [
|
||||||
|
self._requirement_to_dict(req)
|
||||||
|
for req in self.TPN_REQUIREMENTS.values()
|
||||||
|
if req.category.value == category
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_all_requirements(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Get all TPN requirements."""
|
||||||
|
return [
|
||||||
|
self._requirement_to_dict(req)
|
||||||
|
for req in self.TPN_REQUIREMENTS.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_media_certificate_ready(self) -> bool:
|
||||||
|
"""Check if TPN Media certificate requirements are met."""
|
||||||
|
# TPN Media requires both CSPN compliance AND TPN-specific compliance
|
||||||
|
cspn_ready = self.cspn_checker.is_certificate_ready()
|
||||||
|
tpn_summary = self.get_summary()
|
||||||
|
tpn_ready = tpn_summary.get("is_compliant", False)
|
||||||
|
|
||||||
|
return cspn_ready and tpn_ready
|
||||||
|
|
||||||
|
def get_media_certificate_readiness(self) -> Dict[str, Any]:
|
||||||
|
"""Get detailed TPN Media certificate readiness."""
|
||||||
|
cspn_readiness = self.cspn_checker.get_certificate_readiness()
|
||||||
|
tpn_summary = self.get_summary()
|
||||||
|
|
||||||
|
# Get blocking TPN issues
|
||||||
|
blocking = []
|
||||||
|
for req_id, req in self.TPN_REQUIREMENTS.items():
|
||||||
|
if req.is_failing():
|
||||||
|
blocking.append({
|
||||||
|
"id": req_id,
|
||||||
|
"description": req.description,
|
||||||
|
"category": req.category.value,
|
||||||
|
"result": req.result or "No result",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cspn_ready": cspn_readiness.get("ready", False),
|
||||||
|
"cspn_score": cspn_readiness.get("compliance_score", 0),
|
||||||
|
"tpn_ready": tpn_summary.get("is_compliant", False),
|
||||||
|
"tpn_score": tpn_summary.get("compliance_score", 0),
|
||||||
|
"overall_ready": cspn_readiness.get("ready", False) and tpn_summary.get("is_compliant", False),
|
||||||
|
"blocking_cspn_issues": cspn_readiness.get("blocking_issues", []),
|
||||||
|
"blocking_tpn_issues": blocking,
|
||||||
|
"total_issues": len(cspn_readiness.get("blocking_issues", [])) + len(blocking),
|
||||||
|
"recommendations": [
|
||||||
|
"Address all CSPN FAIL checks",
|
||||||
|
"Address all TPN Media FAIL checks",
|
||||||
|
"Ensure CSPN compliance score >90%",
|
||||||
|
"Ensure TPN Media compliance score >95%",
|
||||||
|
"Document all manual checks",
|
||||||
|
"Complete penetration testing",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_combined_compliance(self) -> Dict[str, Any]:
|
||||||
|
"""Get combined CSPN + TPN Media compliance."""
|
||||||
|
cspn_summary = self.cspn_checker.get_summary()
|
||||||
|
tpn_summary = self.get_summary()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": datetime.now().isoformat() + "Z",
|
||||||
|
"cspn": cspn_summary,
|
||||||
|
"tpn_media": tpn_summary,
|
||||||
|
"combined_score": round(
|
||||||
|
(cspn_summary.get("compliance_score", 0) * 0.6 +
|
||||||
|
tpn_summary.get("compliance_score", 0) * 0.4), 1
|
||||||
|
),
|
||||||
|
"overall_compliant": (
|
||||||
|
cspn_summary.get("is_compliant", False) and
|
||||||
|
tpn_summary.get("is_compliant", False)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
tpn_checker = TpnMediaComplianceChecker()
|
||||||
11
packages/secubox-security-posture/debian/changelog
Normal file
11
packages/secubox-security-posture/debian/changelog
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
secubox-security-posture (1.0.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release of SecuBox Security Posture module
|
||||||
|
* DEFCON-level security health monitoring (1-5)
|
||||||
|
* CSPN compliance checking (ANSSI certification alignment)
|
||||||
|
* TPN Media compliance checking (media industry standards)
|
||||||
|
* Performance monitoring with bottleneck detection
|
||||||
|
* Combined security dashboard API
|
||||||
|
* REST API endpoints for all security posture data
|
||||||
|
|
||||||
|
-- Gerald Kerma <devel@cybermind.fr> Tue, 16 Jun 2026 14:00:00 +0200
|
||||||
38
packages/secubox-security-posture/debian/control
Normal file
38
packages/secubox-security-posture/debian/control
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
Source: secubox-security-posture
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: CyberMind <devel@cybermind.fr>
|
||||||
|
Build-Depends: debhelper (>= 11)
|
||||||
|
Standards-Version: 4.5.0
|
||||||
|
Homepage: https://github.com/CyberMind-FR/secubox-deb
|
||||||
|
|
||||||
|
Package: secubox-security-posture
|
||||||
|
Architecture: all
|
||||||
|
Depends:
|
||||||
|
${misc:Depends},
|
||||||
|
python3,
|
||||||
|
python3-fastapi,
|
||||||
|
python3-httpx,
|
||||||
|
python3-psutil,
|
||||||
|
secubox-common (>= 1.0.0),
|
||||||
|
secubox-threat-analyst (>= 1.0.0),
|
||||||
|
secubox-health-doctor (>= 1.0.0),
|
||||||
|
Description: SecuBox Security Posture - DEFCON & Compliance Monitoring
|
||||||
|
SecuBox Security Posture provides real-time security health monitoring
|
||||||
|
with military-standard DEFCON levels (1-5), mapped to ANSSI CSPN requirements
|
||||||
|
and TPN Media compliance standards.
|
||||||
|
.
|
||||||
|
Features:
|
||||||
|
* DEFCON-level security scoring (1-5)
|
||||||
|
* CSPN (ANSSI) compliance checking
|
||||||
|
* TPN Media compliance checking
|
||||||
|
* Performance monitoring with bottleneck detection
|
||||||
|
* Combined security dashboard API
|
||||||
|
* REST API endpoints for all security posture data
|
||||||
|
.
|
||||||
|
This module integrates with existing SecuBox components:
|
||||||
|
* Collects metrics from WAF, CrowdSec, Health Doctor
|
||||||
|
* Provides unified security posture assessment
|
||||||
|
* Generates compliance reports for auditors
|
||||||
|
* Identifies performance bottlenecks
|
||||||
|
* Provides optimization recommendations
|
||||||
25
packages/secubox-security-posture/debian/rules
Normal file
25
packages/secubox-security-posture/debian/rules
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
# Nothing to build - pure Python package
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
# Install Python package
|
||||||
|
install -d debian/secubox-security-posture/usr/lib/secubox/secubox-security-posture/api
|
||||||
|
cp -r api/*.py debian/secubox-security-posture/usr/lib/secubox/secubox-security-posture/api/
|
||||||
|
|
||||||
|
# Install config
|
||||||
|
install -d debian/secubox-security-posture/etc/secubox
|
||||||
|
|
||||||
|
# Install systemd service
|
||||||
|
install -d debian/secubox-security-posture/lib/systemd/system
|
||||||
|
cp debian/secubox-security-posture.sock debian/secubox-security-posture/lib/systemd/system/
|
||||||
|
cp debian/secubox-security-posture.service debian/secubox-security-posture/lib/systemd/system/
|
||||||
|
|
||||||
|
# Install www assets
|
||||||
|
install -d debian/secubox-security-posture/usr/share/secubox/www/security-posture
|
||||||
|
cp -r www/* debian/secubox-security-posture/usr/share/secubox/www/security-posture/ 2>/dev/null || true
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
[Unit]
|
||||||
|
Description=SecuBox Security Posture API Service
|
||||||
|
Documentation=https://github.com/CyberMind-FR/secubox-deb
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
User=secubox
|
||||||
|
Group=secubox
|
||||||
|
|
||||||
|
# Service runtime directory
|
||||||
|
RuntimeDirectory=secubox-security-posture
|
||||||
|
RuntimeDirectoryMode=0750
|
||||||
|
|
||||||
|
# Main service - use unix socket
|
||||||
|
ExecStart=/usr/bin/uvicorn secubox_security_posture.api.main:app \
|
||||||
|
--uds /run/secubox/security-posture.sock \
|
||||||
|
--log-level info \
|
||||||
|
--workers 2
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/secubox /var/log/secubox /run/secubox
|
||||||
|
InaccessiblePaths=/boot /home /root /run/user /tmp
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
Environment=UVICORN_RELOAD=false
|
||||||
|
|
||||||
|
# Restart policy
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=SecuBox Security Posture Unix Socket
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=%t/secubox/security-posture.sock
|
||||||
|
SocketMode=0660
|
||||||
|
SocketUser=root
|
||||||
|
SocketGroup=secubox
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
1
packages/secubox-security-posture/debian/source/format
Normal file
1
packages/secubox-security-posture/debian/source/format
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.0 (native)
|
||||||
154
packages/secubox-security-posture/www/dashboard.html
Normal file
154
packages/secubox-security-posture/www/dashboard.html
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Security Posture Dashboard</title>
|
||||||
|
<script src="defcon-badge.js"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a0a; --card: #1a1a1a; --border: #27272a;
|
||||||
|
--text: #fff; --muted: #71717a; --defcon5: #22c55e;
|
||||||
|
--defcon4: #86efac; --defcon3: #f97316; --defcon2: #ef4444;
|
||||||
|
--defcon1: #991b1b;
|
||||||
|
}
|
||||||
|
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2rem; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
|
||||||
|
.card { background: var(--card); border: 1px solid var(--border); border-radius: 1rem; padding: 1.5rem; }
|
||||||
|
.hero { text-align: center; margin-bottom: 2rem; }
|
||||||
|
.hero .emoji { font-size: 4rem; margin-bottom: 1rem; }
|
||||||
|
.hero .level { font-size: 3rem; font-weight: 900; font-family: monospace; }
|
||||||
|
.hero .desc { color: var(--muted); }
|
||||||
|
.score { font-size: 2rem; font-weight: 700; }
|
||||||
|
.bar { height: 8px; background: var(--border); border-radius: 4px; margin: 1rem 0; overflow: hidden; }
|
||||||
|
.bar .fill { height: 100%; border-radius: 4px; transition: width 0.5s; }
|
||||||
|
.badge { padding: 0.25rem 0.75rem; border-radius: 0.5rem; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
||||||
|
.badge.compliant { background: rgba(34,197,94,0.15); color: var(--defcon5); border: 1px solid var(--defcon5); }
|
||||||
|
.badge.non-compliant { background: rgba(239,68,68,0.15); color: var(--defcon2); border: 1px solid var(--defcon2); }
|
||||||
|
.list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||||
|
.item { display: flex; justify-content: space-between; padding: 0.5rem; background: #1f1f1f; border-radius: 0.5rem; }
|
||||||
|
h2 { font-size: 1.5rem; margin-bottom: 1rem; color: var(--muted); font-weight: 500; }
|
||||||
|
.timestamp { text-align: center; color: var(--muted); font-size: 0.75rem; margin-top: 2rem; }
|
||||||
|
.loading { color: var(--muted); text-align: center; padding: 2rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="hero" id="hero">
|
||||||
|
<defcon-card api-path="/api/v1/security-posture"></defcon-card>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<defcon-badge api-path="/api/v1/security-posture" refresh="30"></defcon-badge>
|
||||||
|
</div>
|
||||||
|
<h2>Security Components</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>DEFCON Level</h3>
|
||||||
|
<div id="defcon-level">--</div>
|
||||||
|
<div class="bar"><div class="fill" id="defcon-bar"></div></div>
|
||||||
|
<div class="list" id="defcon-categories"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>CSPN Compliance</h3>
|
||||||
|
<div class="score" id="cspn-score">--%</div>
|
||||||
|
<span class="badge" id="cspn-badge">Loading...</span>
|
||||||
|
<div class="bar"><div class="fill" id="cspn-bar"></div></div>
|
||||||
|
<div class="list" id="cspn-stats"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>TPN Media</h3>
|
||||||
|
<div class="score" id="tpn-score">--%</div>
|
||||||
|
<span class="badge" id="tpn-badge">Loading...</span>
|
||||||
|
<div class="bar"><div class="fill" id="tpn-bar"></div></div>
|
||||||
|
<div class="list" id="tpn-stats"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Performance</h3>
|
||||||
|
<div class="score" id="perf-score">--%</div>
|
||||||
|
<div class="bar"><div class="fill" id="perf-bar"></div></div>
|
||||||
|
<div class="list" id="perf-stats"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="margin-top: 1.5rem;">
|
||||||
|
<h2>Recommendations</h2>
|
||||||
|
<div id="recommendations"></div>
|
||||||
|
</div>
|
||||||
|
<div class="timestamp">Last updated: <span id="timestamp">--</span></div>
|
||||||
|
<script>
|
||||||
|
const API = '/api/v1/security-posture';
|
||||||
|
async function fetchJSON(endpoint) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${API}${endpoint}`);
|
||||||
|
return r.ok ? await r.json() : null;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
async function refresh() {
|
||||||
|
const [overview, defcon, cspn, tpn, perf] = await Promise.all([
|
||||||
|
fetchJSON('/overview'),
|
||||||
|
fetchJSON('/defcon'),
|
||||||
|
fetchJSON('/cspn/summary'),
|
||||||
|
fetchJSON('/tpn/summary'),
|
||||||
|
fetchJSON('/performance/summary')
|
||||||
|
]);
|
||||||
|
update(overview, defcon, cspn, tpn, perf);
|
||||||
|
}
|
||||||
|
function update(o, d, c, t, p) {
|
||||||
|
document.getElementById('timestamp').textContent = new Date().toLocaleString();
|
||||||
|
if(d) {
|
||||||
|
document.getElementById('defcon-level').textContent = d.level_name + ' - ' + d.score?.toFixed(1) + '/100';
|
||||||
|
const bar = document.getElementById('defcon-bar');
|
||||||
|
if(bar) { bar.style.width = d.score + '%'; bar.style.background = d.color || '#6b7280'; }
|
||||||
|
const cats = document.getElementById('defcon-categories');
|
||||||
|
if(cats && d.category_scores) {
|
||||||
|
cats.innerHTML = Object.entries(d.category_scores).map(([k,v]) =>
|
||||||
|
`<div class="item"><span>${k}</span><span>${v.toFixed(1)}%</span></div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(c?.summary) {
|
||||||
|
const s = c.summary;
|
||||||
|
document.getElementById('cspn-score').textContent = (s.compliance_score||0).toFixed(1) + '%';
|
||||||
|
const badge = document.getElementById('cspn-badge');
|
||||||
|
if(badge) { badge.className = 'badge ' + (s.is_compliant ? 'compliant' : 'non-compliant'); badge.textContent = s.is_compliant ? 'COMPLIANT' : 'NON-COMPLIANT'; }
|
||||||
|
const bar = document.getElementById('cspn-bar');
|
||||||
|
if(bar) { bar.style.width = (s.compliance_score||0) + '%'; bar.style.background = s.is_compliant ? '#22c55e' : '#ef4444'; }
|
||||||
|
const stats = document.getElementById('cspn-stats');
|
||||||
|
if(stats) stats.innerHTML = `
|
||||||
|
<div class="item"><span>Total</span><span>${s.total_requirements||0}</span></div>
|
||||||
|
<div class="item"><span>Passed</span><span>${s.passed||0}</span></div>
|
||||||
|
<div class="item"><span>Failed</span><span>${s.failed||0}</span></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if(t?.summary) {
|
||||||
|
const s = t.summary;
|
||||||
|
document.getElementById('tpn-score').textContent = (s.compliance_score||0).toFixed(1) + '%';
|
||||||
|
const badge = document.getElementById('tpn-badge');
|
||||||
|
if(badge) { badge.className = 'badge ' + (s.is_compliant ? 'compliant' : 'non-compliant'); badge.textContent = s.is_compliant ? 'COMPLIANT' : 'NON-COMPLIANT'; }
|
||||||
|
const bar = document.getElementById('tpn-bar');
|
||||||
|
if(bar) { bar.style.width = (s.compliance_score||0) + '%'; bar.style.background = s.is_compliant ? '#3b82f6' : '#ef4444'; }
|
||||||
|
}
|
||||||
|
if(p) {
|
||||||
|
document.getElementById('perf-score').textContent = (p.score||0).toFixed(1) + '%';
|
||||||
|
const bar = document.getElementById('perf-bar');
|
||||||
|
if(bar) { bar.style.width = (p.score||0) + '%'; bar.style.background = p.score >= 80 ? '#22c55e' : p.score >= 60 ? '#eab308' : '#ef4444'; }
|
||||||
|
const stats = document.getElementById('perf-stats');
|
||||||
|
if(stats && p.category_scores) {
|
||||||
|
stats.innerHTML = Object.entries(p.category_scores).map(([k,v]) =>
|
||||||
|
`<div class="item"><span>${k}</span><span>${v.toFixed(1)}%</span></div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(o?.recommendations) {
|
||||||
|
const recs = document.getElementById('recommendations');
|
||||||
|
if(recs) {
|
||||||
|
if(o.recommendations.length === 0) recs.innerHTML = '<div class="loading">All systems optimal!</div>';
|
||||||
|
else recs.innerHTML = o.recommendations.map(r =>
|
||||||
|
`<div class="item"><span>${r.severity?.toUpperCase()||'INFO'}</span><span>${r.text||'No description'}</span></div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
688
packages/secubox-security-posture/www/defcon-badge.js
Normal file
688
packages/secubox-security-posture/www/defcon-badge.js
Normal file
|
|
@ -0,0 +1,688 @@
|
||||||
|
/**
|
||||||
|
* SecuBox Security Posture - DEFCON Badge Component
|
||||||
|
*
|
||||||
|
* A reusable JavaScript component that displays the current DEFCON level
|
||||||
|
* and security score. Can be embedded in any SecuBox dashboard.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <script src="/security-posture/defcon-badge.js"></script>
|
||||||
|
* <div id="defcon-badge"></div>
|
||||||
|
* <script>initDefconBadge();</script>
|
||||||
|
*
|
||||||
|
* Or with custom element:
|
||||||
|
* <defcon-badge socket="/run/secubox/security-posture.sock"></defcon-badge>
|
||||||
|
*/
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEFCON Badge - Web Component
|
||||||
|
*
|
||||||
|
* Displays current DEFCON level with color-coded badge and security score.
|
||||||
|
*/
|
||||||
|
class DefconBadge extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.socketPath = this.getAttribute('socket') || '/run/secubox/security-posture.sock';
|
||||||
|
this.apiPath = this.getAttribute('api-path') || '/api/v1/security-posture';
|
||||||
|
this.refreshInterval = parseInt(this.getAttribute('refresh') || '30');
|
||||||
|
this.data = null;
|
||||||
|
this.timestamp = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
this.fetchData();
|
||||||
|
this.startRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.stopRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
if (name === 'socket' || name === 'api-path' || name === 'refresh') {
|
||||||
|
this.socketPath = this.getAttribute('socket') || '/run/secubox/security-posture.sock';
|
||||||
|
this.apiPath = this.getAttribute('api-path') || '/api/v1/security-posture';
|
||||||
|
this.refreshInterval = parseInt(this.getAttribute('refresh') || '30');
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['socket', 'api-path', 'refresh'];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<style>
|
||||||
|
.defcon-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge .emoji {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge .level {
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge .score {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge .blink {
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge .status-bar {
|
||||||
|
height: 4px;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge .status-bar .fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge.compact {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge.compact .score {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge.tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge.tooltip::after {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.2s, visibility 0.2s;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-badge.tooltip:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="defcon-badge" data-loading="true">
|
||||||
|
<span class="emoji">⏳</span>
|
||||||
|
<span class="level">Loading...</span>
|
||||||
|
<span class="score"></span>
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="fill" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiPath}/defcon/summary`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
this.data = await response.json();
|
||||||
|
this.timestamp = new Date().toISOString();
|
||||||
|
this.updateDisplay();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch DEFCON data:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
if (!this.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badge = this.querySelector('.defcon-badge');
|
||||||
|
const emoji = badge.querySelector('.emoji');
|
||||||
|
const level = badge.querySelector('.level');
|
||||||
|
const score = badge.querySelector('.score');
|
||||||
|
const fill = badge.querySelector('.fill');
|
||||||
|
|
||||||
|
// Remove loading state
|
||||||
|
badge.removeAttribute('data-loading');
|
||||||
|
|
||||||
|
// Update content
|
||||||
|
emoji.textContent = this.data.emoji || '⚪';
|
||||||
|
level.textContent = this.formatLevel(this.data.defcon);
|
||||||
|
score.textContent = `${this.data.score.toFixed(1)}/100`;
|
||||||
|
|
||||||
|
// Update colors
|
||||||
|
const color = this.data.color || '#6b7280';
|
||||||
|
badge.style.background = color;
|
||||||
|
badge.style.color = this.getContrastColor(color);
|
||||||
|
fill.style.background = color;
|
||||||
|
fill.style.width = `${this.data.score}%`;
|
||||||
|
|
||||||
|
// Add blink class if needed
|
||||||
|
if (this.data.blink) {
|
||||||
|
badge.classList.add('blink');
|
||||||
|
} else {
|
||||||
|
badge.classList.remove('blink');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip
|
||||||
|
if (this.data.description) {
|
||||||
|
badge.classList.add('tooltip');
|
||||||
|
badge.setAttribute('data-tooltip', this.data.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatLevel(level) {
|
||||||
|
if (!level) return 'N/A';
|
||||||
|
return level.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
getContrastColor(hexColor) {
|
||||||
|
// Convert hex to RGB
|
||||||
|
const r = parseInt(hexColor.substr(1, 2), 16);
|
||||||
|
const g = parseInt(hexColor.substr(3, 2), 16);
|
||||||
|
const b = parseInt(hexColor.substr(5, 2), 16);
|
||||||
|
|
||||||
|
// Calculate luminance
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
|
||||||
|
// Return black or white based on luminance
|
||||||
|
return luminance > 0.5 ? '#000' : '#fff';
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(error) {
|
||||||
|
const badge = this.querySelector('.defcon-badge');
|
||||||
|
const emoji = badge.querySelector('.emoji');
|
||||||
|
const level = badge.querySelector('.level');
|
||||||
|
const score = badge.querySelector('.score');
|
||||||
|
|
||||||
|
badge.removeAttribute('data-loading');
|
||||||
|
emoji.textContent = '❌';
|
||||||
|
level.textContent = 'Error';
|
||||||
|
score.textContent = error || 'Failed to load';
|
||||||
|
badge.style.background = '#ef4444';
|
||||||
|
badge.style.color = '#fff';
|
||||||
|
}
|
||||||
|
|
||||||
|
startRefresh() {
|
||||||
|
if (this.refreshInterval > 0) {
|
||||||
|
this.refreshTimer = setInterval(() => {
|
||||||
|
this.fetchData();
|
||||||
|
}, this.refreshInterval * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRefresh() {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearInterval(this.refreshTimer);
|
||||||
|
this.refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEFCON Card - Larger display with more details
|
||||||
|
*/
|
||||||
|
class DefconCard extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.socketPath = this.getAttribute('socket') || '/run/secubox/security-posture.sock';
|
||||||
|
this.apiPath = this.getAttribute('api-path') || '/api/v1/security-posture';
|
||||||
|
this.refreshInterval = parseInt(this.getAttribute('refresh') || '60');
|
||||||
|
this.data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
this.fetchData();
|
||||||
|
this.startRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.stopRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<style>
|
||||||
|
.defcon-card {
|
||||||
|
background: var(--surface, #fff);
|
||||||
|
border: 1px solid var(--border, #e5e7eb);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--muted, #6b7280);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .score-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .score-value {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .score-max {
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .status-bar {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--border, #e5e7eb);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .status-bar .fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .detail-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .detail-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted, #6b7280);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .detail-value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .compliance {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .compliance-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .compliance-item .icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defcon-card .timestamp {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted, #6b7280);
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="defcon-card">
|
||||||
|
<div class="header">
|
||||||
|
<div class="badge" data-loading="true">
|
||||||
|
<span>⏳</span>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="title">Security Posture</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="score-display">
|
||||||
|
<span class="score-value">--</span>
|
||||||
|
<span class="score-max">/100</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="fill" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details" data-loading="true">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Level</span>
|
||||||
|
<span class="detail-value">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Network</span>
|
||||||
|
<span class="detail-value">--%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Threat</span>
|
||||||
|
<span class="detail-value">--%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Access</span>
|
||||||
|
<span class="detail-value">--%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Data</span>
|
||||||
|
<span class="detail-value">--%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Resilience</span>
|
||||||
|
<span class="detail-value">--%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="compliance">
|
||||||
|
<div class="compliance-item" data-cspn="loading">
|
||||||
|
<span class="icon">⏳</span>
|
||||||
|
<span>CSPN</span>
|
||||||
|
</div>
|
||||||
|
<div class="compliance-item" data-tpn="loading">
|
||||||
|
<span class="icon">⏳</span>
|
||||||
|
<span>TPN</span>
|
||||||
|
</div>
|
||||||
|
<div class="compliance-item" data-perf="loading">
|
||||||
|
<span class="icon">⏳</span>
|
||||||
|
<span>Performance</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timestamp">--</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiPath}/overview`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
this.data = await response.json();
|
||||||
|
this.timestamp = new Date().toISOString();
|
||||||
|
this.updateDisplay();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch overview data:', error);
|
||||||
|
this.showError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
if (!this.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = this.querySelector('.defcon-card');
|
||||||
|
const badge = card.querySelector('.badge');
|
||||||
|
const scoreValue = card.querySelector('.score-value');
|
||||||
|
const scoreMax = card.querySelector('.score-max');
|
||||||
|
const fill = card.querySelector('.status-bar .fill');
|
||||||
|
const details = card.querySelector('.details');
|
||||||
|
const cspnItem = card.querySelector('[data-cspn]');
|
||||||
|
const tpnItem = card.querySelector('[data-tpn]');
|
||||||
|
const perfItem = card.querySelector('[data-perf]');
|
||||||
|
const timestamp = card.querySelector('.timestamp');
|
||||||
|
|
||||||
|
// Remove loading states
|
||||||
|
badge.removeAttribute('data-loading');
|
||||||
|
details.removeAttribute('data-loading');
|
||||||
|
cspnItem.removeAttribute('data-cspn');
|
||||||
|
tpnItem.removeAttribute('data-tpn');
|
||||||
|
perfItem.removeAttribute('data-perf');
|
||||||
|
|
||||||
|
// Update badge
|
||||||
|
const emoji = this.data.defcon.emoji || this.data.overall_emoji || '⚪';
|
||||||
|
const level = this.formatLevel(this.data.defcon.level);
|
||||||
|
badge.innerHTML = `<span>${emoji}</span><span>${level}</span>`;
|
||||||
|
badge.style.background = this.data.defcon.color || this.data.overall_color || '#6b7280';
|
||||||
|
badge.style.color = this.getContrastColor(this.data.defcon.color || this.data.overall_color || '#6b7280');
|
||||||
|
|
||||||
|
// Update score
|
||||||
|
scoreValue.textContent = this.data.combined_score.toFixed(1);
|
||||||
|
|
||||||
|
// Update status bar
|
||||||
|
fill.style.background = this.data.overall_color || '#6b7280';
|
||||||
|
fill.style.width = `${this.data.combined_score}%`;
|
||||||
|
|
||||||
|
// Update category scores
|
||||||
|
const categoryScores = this.data.defcon.category_scores || {};
|
||||||
|
const detailItems = details.querySelectorAll('.detail-item');
|
||||||
|
const categories = ['network', 'threat', 'access', 'data', 'resilience'];
|
||||||
|
|
||||||
|
categories.forEach((cat, index) => {
|
||||||
|
if (detailItems[index]) {
|
||||||
|
const label = detailItems[index].querySelector('.detail-label');
|
||||||
|
const value = detailItems[index].querySelector('.detail-value');
|
||||||
|
label.textContent = cat.charAt(0).toUpperCase() + cat.slice(1);
|
||||||
|
value.textContent = `${categoryScores[cat]?.toFixed(1) || 'N/A'}%`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update compliance items
|
||||||
|
this.updateComplianceItem(cspnItem, this.data.cspn);
|
||||||
|
this.updateComplianceItem(tpnItem, this.data.tpn_media);
|
||||||
|
this.updateComplianceItem(perfItem, { compliant: true, score: this.data.performance.score });
|
||||||
|
|
||||||
|
// Update timestamp
|
||||||
|
timestamp.textContent = this.formatTimestamp(this.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateComplianceItem(item, data) {
|
||||||
|
const icon = item.querySelector('.icon');
|
||||||
|
const textSpan = item.querySelector('span:last-child');
|
||||||
|
|
||||||
|
const compliant = data?.compliant || data?.score >= (data?.threshold || 80);
|
||||||
|
const score = data?.score || 0;
|
||||||
|
|
||||||
|
icon.textContent = compliant ? '✓' : '✗';
|
||||||
|
icon.style.background = compliant ? '#22c55e' : '#ef4444';
|
||||||
|
icon.style.color = '#fff';
|
||||||
|
|
||||||
|
const label = textSpan.textContent;
|
||||||
|
textSpan.textContent = `${label}: ${score.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatLevel(level) {
|
||||||
|
if (!level) return 'N/A';
|
||||||
|
return level.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTimestamp(isoString) {
|
||||||
|
if (!isoString) return '--';
|
||||||
|
const date = new Date(isoString);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getContrastColor(hexColor) {
|
||||||
|
const r = parseInt(hexColor.substr(1, 2), 16);
|
||||||
|
const g = parseInt(hexColor.substr(3, 2), 16);
|
||||||
|
const b = parseInt(hexColor.substr(5, 2), 16);
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
return luminance > 0.5 ? '#000' : '#fff';
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(error) {
|
||||||
|
const badge = this.querySelector('.defcon-card .badge');
|
||||||
|
badge.innerHTML = `<span>❌</span><span>Error</span>`;
|
||||||
|
badge.style.background = '#ef4444';
|
||||||
|
badge.style.color = '#fff';
|
||||||
|
badge.removeAttribute('data-loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
startRefresh() {
|
||||||
|
if (this.refreshInterval > 0) {
|
||||||
|
this.refreshTimer = setInterval(() => {
|
||||||
|
this.fetchData();
|
||||||
|
}, this.refreshInterval * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRefresh() {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearInterval(this.refreshTimer);
|
||||||
|
this.refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize DEFCON badge for an element
|
||||||
|
*/
|
||||||
|
function initDefconBadge(elementId = 'defcon-badge', options = {}) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
console.error(`Element with ID '${elementId}' not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badge = document.createElement('defcon-badge');
|
||||||
|
if (options.socket) badge.setAttribute('socket', options.socket);
|
||||||
|
if (options.apiPath) badge.setAttribute('api-path', options.apiPath);
|
||||||
|
if (options.refresh) badge.setAttribute('refresh', options.refresh);
|
||||||
|
if (options.compact) badge.classList.add('compact');
|
||||||
|
|
||||||
|
element.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize DEFCON card for an element
|
||||||
|
*/
|
||||||
|
function initDefconCard(elementId = 'defcon-card', options = {}) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
console.error(`Element with ID '${elementId}' not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = document.createElement('defcon-card');
|
||||||
|
if (options.socket) card.setAttribute('socket', options.socket);
|
||||||
|
if (options.apiPath) card.setAttribute('api-path', options.apiPath);
|
||||||
|
if (options.refresh) card.setAttribute('refresh', options.refresh);
|
||||||
|
|
||||||
|
element.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom elements
|
||||||
|
*/
|
||||||
|
if (!customElements.get('defcon-badge')) {
|
||||||
|
customElements.define('defcon-badge', DefconBadge);
|
||||||
|
}
|
||||||
|
if (!customElements.get('defcon-card')) {
|
||||||
|
customElements.define('defcon-card', DefconCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-initialize on DOM load
|
||||||
|
*/
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Auto-initialize any existing elements
|
||||||
|
document.querySelectorAll('defcon-badge:not([initialized])').forEach(el => {
|
||||||
|
el.setAttribute('initialized', 'true');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('defcon-card:not([initialized])').forEach(el => {
|
||||||
|
el.setAttribute('initialized', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Already loaded, initialize now
|
||||||
|
document.querySelectorAll('defcon-badge:not([initialized])').forEach(el => {
|
||||||
|
el.setAttribute('initialized', 'true');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('defcon-card:not([initialized])').forEach(el => {
|
||||||
|
el.setAttribute('initialized', 'true');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export for module usage
|
||||||
|
*/
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = {
|
||||||
|
DefconBadge,
|
||||||
|
DefconCard,
|
||||||
|
initDefconBadge,
|
||||||
|
initDefconCard
|
||||||
|
};
|
||||||
|
}
|
||||||
495
packages/secubox-security-posture/www/security-posture.js
Normal file
495
packages/secubox-security-posture/www/security-posture.js
Normal file
|
|
@ -0,0 +1,495 @@
|
||||||
|
/**
|
||||||
|
* SecuBox Security Posture - Hub Integration
|
||||||
|
*
|
||||||
|
* Lightweight integration for embedding security posture widgets in the hub dashboard.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <script src="/security-posture/security-posture.js"></script>
|
||||||
|
* <script>SecuBoxSecurityPosture.init({ apiPath: '/api/v1/security-posture' });</script>
|
||||||
|
*
|
||||||
|
* Then use:
|
||||||
|
* SecuBoxSecurityPosture.renderDefconBadge('target-element-id');
|
||||||
|
* SecuBoxSecurityPosture.renderSummaryCard('target-element-id');
|
||||||
|
*/
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const SecuBoxSecurityPosture = (function() {
|
||||||
|
// Configuration
|
||||||
|
let config = {
|
||||||
|
apiPath: '/api/v1/security-posture',
|
||||||
|
refreshInterval: 30000,
|
||||||
|
debug: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// State
|
||||||
|
const state = {
|
||||||
|
data: null,
|
||||||
|
timers: new Map(),
|
||||||
|
initialized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// DEFCON level info
|
||||||
|
const DEFCON_LEVELS = {
|
||||||
|
defcon_1: { name: 'DEFCON 1', emoji: '🔴', color: '#991b1b', bg: 'rgba(153, 27, 27, 0.15)', desc: 'MAXIMUM • Critical breach', blink: true },
|
||||||
|
defcon_2: { name: 'DEFCON 2', emoji: '🔴', color: '#ef4444', bg: 'rgba(239, 68, 68, 0.15)', desc: 'SEVERE • Major incident', blink: false },
|
||||||
|
defcon_3: { name: 'DEFCON 3', emoji: '🟠', color: '#f97316', bg: 'rgba(249, 115, 22, 0.15)', desc: 'HEIGHTENED • Active threats', blink: false },
|
||||||
|
defcon_4: { name: 'DEFCON 4', emoji: '🟡', color: '#86efac', bg: 'rgba(134, 239, 172, 0.15)', desc: 'INCREASED CHATTER • Minor issues', blink: false },
|
||||||
|
defcon_5: { name: 'DEFCON 5', emoji: '🟢', color: '#22c55e', bg: 'rgba(34, 197, 94, 0.15)', desc: 'NORMAL • All systems operational', blink: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log helper
|
||||||
|
function log(...args) {
|
||||||
|
if (config.debug) console.log('[SecuBox-SP]', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch JSON
|
||||||
|
async function fetchJSON(endpoint) {
|
||||||
|
try {
|
||||||
|
const url = `${config.apiPath}${endpoint}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
log(`API error: ${response.status} ${url}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
log('Fetch error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all data
|
||||||
|
async function fetchAllData() {
|
||||||
|
const [defcon, cspn, tpn, perf, overview] = await Promise.all([
|
||||||
|
fetchJSON('/defcon/summary'),
|
||||||
|
fetchJSON('/cspn/summary'),
|
||||||
|
fetchJSON('/tpn/summary'),
|
||||||
|
fetchJSON('/performance/summary'),
|
||||||
|
fetchJSON('/overview')
|
||||||
|
]);
|
||||||
|
|
||||||
|
state.data = { defcon, cspn, tpn, perf, overview };
|
||||||
|
return state.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format percentage
|
||||||
|
function formatPercent(value, decimals = 1) {
|
||||||
|
if (value === null || value === undefined) return '--%';
|
||||||
|
return `${value.toFixed(decimals)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DEFCON info
|
||||||
|
function getDefconInfo(level) {
|
||||||
|
return DEFCON_LEVELS[level] || DEFCON_LEVELS.defcon_5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create element helper
|
||||||
|
function createElement(tag, props = {}, children = []) {
|
||||||
|
const el = document.createElement(tag);
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
if (key === 'style') {
|
||||||
|
Object.entries(value).forEach(([k, v]) => el.style[k] = v);
|
||||||
|
} else if (key === 'dataset') {
|
||||||
|
Object.entries(value).forEach(([k, v]) => el.dataset[k] = v);
|
||||||
|
} else if (key === 'className') {
|
||||||
|
el.className = value;
|
||||||
|
} else if (key === 'textContent') {
|
||||||
|
el.textContent = value;
|
||||||
|
} else if (key === 'innerHTML') {
|
||||||
|
el.innerHTML = value;
|
||||||
|
} else {
|
||||||
|
el[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
children.forEach(child => {
|
||||||
|
if (typeof child === 'string') {
|
||||||
|
el.appendChild(document.createTextNode(child));
|
||||||
|
} else if (child instanceof Node) {
|
||||||
|
el.appendChild(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render DEFCON Badge
|
||||||
|
function renderDefconBadge(targetId, options = {}) {
|
||||||
|
const target = typeof targetId === 'string' ? document.getElementById(targetId) : targetId;
|
||||||
|
if (!target) {
|
||||||
|
log(`Target element not found: ${targetId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badge = createElement('defcon-badge', {
|
||||||
|
apiPath: config.apiPath,
|
||||||
|
refresh: Math.round(config.refreshInterval / 1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.compact) badge.setAttribute('compact', '');
|
||||||
|
if (options.tooltip) badge.setAttribute('tooltip', options.tooltip);
|
||||||
|
|
||||||
|
target.appendChild(badge);
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render DEFCON Card
|
||||||
|
function renderDefconCard(targetId, options = {}) {
|
||||||
|
const target = typeof targetId === 'string' ? document.getElementById(targetId) : targetId;
|
||||||
|
if (!target) {
|
||||||
|
log(`Target element not found: ${targetId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = createElement('defcon-card', {
|
||||||
|
apiPath: config.apiPath,
|
||||||
|
refresh: Math.round(config.refreshInterval / 1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
target.appendChild(card);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Mini DEFCON Display
|
||||||
|
function renderMiniDefcon(targetId) {
|
||||||
|
const target = typeof targetId === 'string' ? document.getElementById(targetId) : targetId;
|
||||||
|
if (!target) {
|
||||||
|
log(`Target element not found: ${targetId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = createElement('div', {
|
||||||
|
className: 'secubox-sp-mini-defcon',
|
||||||
|
style: {
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
padding: '0.25rem 0.75rem',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
fontFamily: 'system-ui, sans-serif',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: '500'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emoji = createElement('span', { className: 'secubox-sp-emoji', textContent: '⏳' });
|
||||||
|
const level = createElement('span', { className: 'secubox-sp-level', textContent: 'Loading...' });
|
||||||
|
const score = createElement('span', { className: 'secubox-sp-score', textContent: '', style: { opacity: 0.7 } });
|
||||||
|
|
||||||
|
container.appendChild(emoji);
|
||||||
|
container.appendChild(level);
|
||||||
|
container.appendChild(score);
|
||||||
|
target.appendChild(container);
|
||||||
|
|
||||||
|
// Update function
|
||||||
|
const update = async () => {
|
||||||
|
const data = await fetchJSON('/defcon/summary');
|
||||||
|
if (data) {
|
||||||
|
const info = getDefconInfo(data.level || 'defcon_5');
|
||||||
|
emoji.textContent = data.emoji || info.emoji;
|
||||||
|
level.textContent = info.name;
|
||||||
|
score.textContent = formatPercent(data.score);
|
||||||
|
container.style.background = data.color || info.bg;
|
||||||
|
container.style.color = info.color;
|
||||||
|
if (info.blink) emoji.style.animation = 'blink 1s infinite';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update and periodic refresh
|
||||||
|
update();
|
||||||
|
const interval = setInterval(update, config.refreshInterval);
|
||||||
|
state.timers.set(container, interval);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Summary Card
|
||||||
|
function renderSummaryCard(targetId, options = {}) {
|
||||||
|
const target = typeof targetId === 'string' ? document.getElementById(targetId) : targetId;
|
||||||
|
if (!target) {
|
||||||
|
log(`Target element not found: ${targetId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = createElement('div', {
|
||||||
|
className: 'secubox-sp-summary-card',
|
||||||
|
style: {
|
||||||
|
background: '#1a1a1a',
|
||||||
|
border: '1px solid #27272a',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
padding: '1rem',
|
||||||
|
fontFamily: 'system-ui, sans-serif'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = createElement('h3', {
|
||||||
|
className: 'secubox-sp-title',
|
||||||
|
textContent: options.title || 'Security Posture',
|
||||||
|
style: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: '#71717a',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
margin: '0 0 0.75rem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defcon = createElement('div', {
|
||||||
|
className: 'secubox-sp-defcon',
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginBottom: '0.75rem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const defconEmoji = createElement('span', { className: 'secubox-sp-defcon-emoji', textContent: '⏳', style: { fontSize: '1.25rem' } });
|
||||||
|
const defconLevel = createElement('span', { className: 'secubox-sp-defcon-level', textContent: 'Loading...' });
|
||||||
|
|
||||||
|
defcon.appendChild(defconEmoji);
|
||||||
|
defcon.appendChild(defconLevel);
|
||||||
|
|
||||||
|
const combinedScore = createElement('div', {
|
||||||
|
className: 'secubox-sp-combined',
|
||||||
|
style: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: '700',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
marginBottom: '0.5rem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusBar = createElement('div', {
|
||||||
|
className: 'secubox-sp-bar',
|
||||||
|
style: {
|
||||||
|
height: '6px',
|
||||||
|
background: '#27272a',
|
||||||
|
borderRadius: '3px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginBottom: '0.75rem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const barFill = createElement('div', {
|
||||||
|
className: 'secubox-sp-bar-fill',
|
||||||
|
style: {
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: '3px',
|
||||||
|
width: '0%',
|
||||||
|
transition: 'width 0.5s ease'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
statusBar.appendChild(barFill);
|
||||||
|
|
||||||
|
const compliance = createElement('div', {
|
||||||
|
className: 'secubox-sp-compliance',
|
||||||
|
style: {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||||
|
gap: '0.75rem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cspnItem = createElement('div', {
|
||||||
|
className: 'secubox-sp-compliance-item',
|
||||||
|
style: { textAlign: 'center', fontSize: '0.75rem' }
|
||||||
|
});
|
||||||
|
const cspnLabel = createElement('div', { textContent: 'CSPN', style: { color: '#71717a', marginBottom: '0.25rem' } });
|
||||||
|
const cspnValue = createElement('div', { textContent: '--%', style: { fontWeight: '600' } });
|
||||||
|
cspnItem.appendChild(cspnLabel);
|
||||||
|
cspnItem.appendChild(cspnValue);
|
||||||
|
|
||||||
|
const tpnItem = createElement('div', {
|
||||||
|
className: 'secubox-sp-compliance-item',
|
||||||
|
style: { textAlign: 'center', fontSize: '0.75rem' }
|
||||||
|
});
|
||||||
|
const tpnLabel = createElement('div', { textContent: 'TPN', style: { color: '#71717a', marginBottom: '0.25rem' } });
|
||||||
|
const tpnValue = createElement('div', { textContent: '--%', style: { fontWeight: '600' } });
|
||||||
|
tpnItem.appendChild(tpnLabel);
|
||||||
|
tpnItem.appendChild(tpnValue);
|
||||||
|
|
||||||
|
const perfItem = createElement('div', {
|
||||||
|
className: 'secubox-sp-compliance-item',
|
||||||
|
style: { textAlign: 'center', fontSize: '0.75rem' }
|
||||||
|
});
|
||||||
|
const perfLabel = createElement('div', { textContent: 'Perf', style: { color: '#71717a', marginBottom: '0.25rem' } });
|
||||||
|
const perfValue = createElement('div', { textContent: '--%', style: { fontWeight: '600' } });
|
||||||
|
perfItem.appendChild(perfLabel);
|
||||||
|
perfItem.appendChild(perfValue);
|
||||||
|
|
||||||
|
compliance.appendChild(cspnItem);
|
||||||
|
compliance.appendChild(tpnItem);
|
||||||
|
compliance.appendChild(perfItem);
|
||||||
|
|
||||||
|
container.appendChild(title);
|
||||||
|
container.appendChild(defcon);
|
||||||
|
container.appendChild(combinedScore);
|
||||||
|
container.appendChild(statusBar);
|
||||||
|
container.appendChild(compliance);
|
||||||
|
target.appendChild(container);
|
||||||
|
|
||||||
|
// Update function
|
||||||
|
const update = async () => {
|
||||||
|
const [defconData, overview] = await Promise.all([
|
||||||
|
fetchJSON('/defcon/summary'),
|
||||||
|
fetchJSON('/overview')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (defconData) {
|
||||||
|
const info = getDefconInfo(defconData.level || 'defcon_5');
|
||||||
|
defconEmoji.textContent = defconData.emoji || info.emoji;
|
||||||
|
defconLevel.textContent = info.name;
|
||||||
|
if (info.blink) defconEmoji.style.animation = 'blink 1s infinite';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overview) {
|
||||||
|
combinedScore.textContent = `${formatPercent(overview.combined_score)}`;
|
||||||
|
barFill.style.width = `${overview.combined_score || 0}%`;
|
||||||
|
barFill.style.background = overview.overall_color || '#6b7280';
|
||||||
|
|
||||||
|
if (overview.cspn) {
|
||||||
|
cspnValue.textContent = formatPercent(overview.cspn.score);
|
||||||
|
cspnItem.style.color = overview.cspn.compliant ? '#22c55e' : '#ef4444';
|
||||||
|
}
|
||||||
|
if (overview.tpn_media) {
|
||||||
|
tpnValue.textContent = formatPercent(overview.tpn_media.score);
|
||||||
|
tpnItem.style.color = overview.tpn_media.compliant ? '#3b82f6' : '#ef4444';
|
||||||
|
}
|
||||||
|
if (overview.performance) {
|
||||||
|
perfValue.textContent = formatPercent(overview.performance.score);
|
||||||
|
perfItem.style.color = overview.performance.score >= 80 ? '#22c55e' : overview.performance.score >= 60 ? '#eab308' : '#ef4444';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update and periodic refresh
|
||||||
|
update();
|
||||||
|
const interval = setInterval(update, config.refreshInterval);
|
||||||
|
state.timers.set(container, interval);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Compliance Badge
|
||||||
|
function renderComplianceBadge(targetId, type = 'cspn') {
|
||||||
|
const target = typeof targetId === 'string' ? document.getElementById(targetId) : targetId;
|
||||||
|
if (!target) {
|
||||||
|
log(`Target element not found: ${targetId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = createElement('span', {
|
||||||
|
className: `secubox-sp-compliance-badge secubox-sp-${type}`,
|
||||||
|
style: {
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.375rem',
|
||||||
|
padding: '0.25rem 0.625rem',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.05em'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = createElement('span', { className: 'secubox-sp-icon', textContent: '⏳' });
|
||||||
|
const label = createElement('span', { className: 'secubox-sp-label', textContent: type.toUpperCase() });
|
||||||
|
const score = createElement('span', { className: 'secubox-sp-score', textContent: '' });
|
||||||
|
|
||||||
|
container.appendChild(icon);
|
||||||
|
container.appendChild(label);
|
||||||
|
container.appendChild(score);
|
||||||
|
target.appendChild(container);
|
||||||
|
|
||||||
|
const threshold = type === 'cspn' ? 70 : type === 'tpn' ? 85 : 80;
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
const endpoint = type === 'cspn' ? '/cspn/summary' : type === 'tpn' ? '/tpn/summary' : '/performance/summary';
|
||||||
|
const data = await fetchJSON(endpoint);
|
||||||
|
if (data?.summary) {
|
||||||
|
const s = data.summary;
|
||||||
|
const isCompliant = s.is_compliant || s.score >= threshold;
|
||||||
|
const color = isCompliant ? (type === 'cspn' ? '#22c55e' : type === 'tpn' ? '#3b82f6' : '#22c55e') : '#ef4444';
|
||||||
|
const bg = isCompliant ? `rgba(${color === '#22c55e' ? '34,197,94' : color === '#3b82f6' ? '59,130,246' : '34,197,94'}, 0.15)` : 'rgba(239,68,68,0.15)';
|
||||||
|
|
||||||
|
icon.textContent = isCompliant ? '✓' : '✗';
|
||||||
|
score.textContent = ` ${formatPercent(s.compliance_score || s.score)}`;
|
||||||
|
container.style.background = bg;
|
||||||
|
container.style.color = color;
|
||||||
|
container.style.border = `1px solid ${color}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
const interval = setInterval(update, config.refreshInterval);
|
||||||
|
state.timers.set(container, interval);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy/cleanup
|
||||||
|
function destroy() {
|
||||||
|
state.timers.forEach((timer, element) => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
state.timers.clear();
|
||||||
|
state.data = null;
|
||||||
|
state.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
function init(userConfig = {}) {
|
||||||
|
if (state.initialized) return;
|
||||||
|
|
||||||
|
// Merge config
|
||||||
|
config = { ...config, ...userConfig };
|
||||||
|
|
||||||
|
// Ensure defcon-badge.js is loaded
|
||||||
|
if (!customElements.get('defcon-badge')) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = config.apiPath.replace('/api/v1/security-posture', '') + '/security-posture/defcon-badge.js';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
if (!customElements.get('defcon-card')) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = config.apiPath.replace('/api/v1/security-posture', '') + '/security-posture/defcon-badge.js';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.initialized = true;
|
||||||
|
log('SecuBox Security Posture integration initialized');
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
renderDefconBadge,
|
||||||
|
renderDefconCard,
|
||||||
|
renderMiniDefcon,
|
||||||
|
renderSummaryCard,
|
||||||
|
renderComplianceBadge,
|
||||||
|
destroy,
|
||||||
|
refresh: fetchAllData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return public API
|
||||||
|
return {
|
||||||
|
init,
|
||||||
|
config,
|
||||||
|
renderDefconBadge,
|
||||||
|
renderDefconCard,
|
||||||
|
renderMiniDefcon,
|
||||||
|
renderSummaryCard,
|
||||||
|
renderComplianceBadge,
|
||||||
|
destroy,
|
||||||
|
refresh: fetchAllData
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Auto-initialize if needed
|
||||||
|
if (typeof window !== 'undefined' && window.SecuBoxSecurityPosture) {
|
||||||
|
// Already loaded
|
||||||
|
} else {
|
||||||
|
window.SecuBoxSecurityPosture = SecuBoxSecurityPosture;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user