secubox-openwrt/package/secubox/secubox-cve-triage/files/usr/lib/cve-triage/collector.sh
CyberMind-FR 44493ebfe3 feat: Add CVE Triage Agent and Vortex DNS, fix webmail login
New Packages:
- secubox-cve-triage: AI-powered CVE analysis and vulnerability management
  - NVD API integration for CVE data
  - CrowdSec CVE alert correlation
  - LocalAI-powered impact analysis
  - Approval workflow for patch recommendations
  - Multi-source monitoring (opkg, LXC, Docker)

- luci-app-cve-triage: Dashboard with alerts, pending queue, risk score

- secubox-vortex-dns: Meshed multi-dynamic subdomain delegation
  - Master/slave hierarchical DNS delegation
  - Wildcard domain management
  - First Peek auto-registration
  - Gossip-based exposure config sync
  - Submastering for nested hierarchies

Fixes:
- Webmail 401 login: config.docker.inc.php was overriding IMAP host
  to ssl://mail.secubox.in:993 which Docker couldn't reach
- Fixed mailctl webmail configure to use socat proxy (172.17.0.1:10143)

Documentation:
- Added LXC cgroup:mixed fix to FAQ-TROUBLESHOOTING.md
- Updated CLAUDE.md to include FAQ consultation at startup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:19:54 +01:00

217 lines
7.1 KiB
Bash

#!/bin/sh
# CVE Triage - Collector Module
# Collects installed packages and CVE data
CACHE_DIR="/var/cache/cve-triage"
# Initialize cache directory
init_cache() {
mkdir -p "$CACHE_DIR"
}
# Collect installed opkg packages with versions
collect_opkg_packages() {
opkg list-installed 2>/dev/null | while read -r pkg rest; do
local version=$(echo "$rest" | awk '{print $1}')
printf '{"name":"%s","version":"%s","source":"opkg"}\n' "$pkg" "$version"
done | json_slurp
}
# Collect LXC container packages (if containers exist)
collect_lxc_packages() {
local packages='[]'
for container_dir in /srv/lxc/*/; do
[ -d "$container_dir" ] || continue
local name=$(basename "$container_dir")
# Check if container has dpkg
if [ -f "${container_dir}rootfs/var/lib/dpkg/status" ]; then
grep -E "^Package:|^Version:" "${container_dir}rootfs/var/lib/dpkg/status" 2>/dev/null | \
paste - - | sed 's/Package: //; s/Version: //' | while read -r pkg ver; do
printf '{"name":"%s","version":"%s","source":"lxc:%s"}\n' "$pkg" "$ver" "$name"
done
fi
# Check if container has apk (Alpine)
if [ -f "${container_dir}rootfs/lib/apk/db/installed" ]; then
awk '/^P:/{pkg=$2} /^V:/{print pkg,$2}' "${container_dir}rootfs/lib/apk/db/installed" 2>/dev/null | \
while read -r pkg ver; do
printf '{"name":"%s","version":"%s","source":"lxc:%s"}\n' "$pkg" "$ver" "$name"
done
fi
done | json_slurp
}
# Collect Docker image info
collect_docker_images() {
command -v docker >/dev/null 2>&1 || return
docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | while read -r image; do
printf '{"name":"%s","version":"","source":"docker"}\n' "$image"
done | json_slurp
}
# Collect all installed packages
collect_all_packages() {
init_cache
local cache_file="$CACHE_DIR/packages.json"
{
echo '{"timestamp":"'"$(date -Iseconds)"'","packages":['
local first=1
# opkg packages
if [ "$(uci_get packages.opkg.enabled)" = "1" ]; then
collect_opkg_packages | jsonfilter -e '@[*]' 2>/dev/null | while read -r pkg; do
[ $first -eq 0 ] && printf ','
first=0
printf '%s' "$pkg"
done
fi
# LXC packages
if [ "$(uci_get packages.lxc.enabled)" = "1" ]; then
collect_lxc_packages | jsonfilter -e '@[*]' 2>/dev/null | while read -r pkg; do
[ $first -eq 0 ] && printf ','
first=0
printf '%s' "$pkg"
done
fi
# Docker images
if [ "$(uci_get packages.docker.enabled)" = "1" ]; then
collect_docker_images | jsonfilter -e '@[*]' 2>/dev/null | while read -r pkg; do
[ $first -eq 0 ] && printf ','
first=0
printf '%s' "$pkg"
done
fi
echo ']}'
} > "$cache_file"
cat "$cache_file"
}
# Fetch CVEs from CrowdSec alerts (filter CVE-related scenarios)
collect_crowdsec_cves() {
local cscli_path=$(command -v cscli 2>/dev/null)
[ -z "$cscli_path" ] && return
# Get recent alerts with CVE in scenario name
$cscli_path alerts list -o json 2>/dev/null | \
jsonfilter -e '@[*]' 2>/dev/null | \
grep -i "cve" | \
while read -r alert; do
local scenario=$(echo "$alert" | jsonfilter -e '@.scenario' 2>/dev/null)
local cve_id=$(echo "$scenario" | grep -oE 'CVE-[0-9]{4}-[0-9]+' | head -1)
[ -n "$cve_id" ] || continue
local ip=$(echo "$alert" | jsonfilter -e '@.source.ip' 2>/dev/null)
local timestamp=$(echo "$alert" | jsonfilter -e '@.created_at' 2>/dev/null)
printf '{"cve":"%s","scenario":"%s","source_ip":"%s","detected":"%s","source":"crowdsec"}\n' \
"$cve_id" "$scenario" "$ip" "$timestamp"
done | json_slurp
}
# Fetch CVE data from NVD API (with caching)
fetch_nvd_cves() {
local keywords="$1"
local cache_file="$CACHE_DIR/nvd_recent.json"
local cache_hours=$(uci_get source.nvd.cache_hours)
cache_hours="${cache_hours:-24}"
# Check cache freshness
if [ -f "$cache_file" ]; then
local cache_age=$(( $(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0) ))
local max_age=$((cache_hours * 3600))
if [ $cache_age -lt $max_age ]; then
cat "$cache_file"
return
fi
fi
# Fetch from NVD API (last 7 days, high severity)
local nvd_url=$(uci_get source.nvd.url)
nvd_url="${nvd_url:-https://services.nvd.nist.gov/rest/json/cves/2.0}"
local end_date=$(date -u +%Y-%m-%dT%H:%M:%S.000)
local start_date=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S.000 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S.000)
# NVD API requires date range
local api_url="${nvd_url}?pubStartDate=${start_date}&pubEndDate=${end_date}&cvssV3Severity=HIGH"
log_info "Fetching CVEs from NVD API..."
local response=$(wget -q -O - --timeout=30 "$api_url" 2>/dev/null)
if [ -n "$response" ]; then
echo "$response" > "$cache_file"
echo "$response"
else
log_warn "Failed to fetch NVD data"
[ -f "$cache_file" ] && cat "$cache_file"
fi
}
# Parse NVD response to extract CVE details
parse_nvd_cves() {
local nvd_data="$1"
echo "$nvd_data" | jsonfilter -e '@.vulnerabilities[*]' 2>/dev/null | while read -r vuln; do
local cve_id=$(echo "$vuln" | jsonfilter -e '@.cve.id' 2>/dev/null)
local description=$(echo "$vuln" | jsonfilter -e '@.cve.descriptions[0].value' 2>/dev/null | head -c 500)
local published=$(echo "$vuln" | jsonfilter -e '@.cve.published' 2>/dev/null)
# Get CVSS score (try v3.1, then v3.0, then v2)
local cvss=$(echo "$vuln" | jsonfilter -e '@.cve.metrics.cvssMetricV31[0].cvssData.baseScore' 2>/dev/null)
[ -z "$cvss" ] && cvss=$(echo "$vuln" | jsonfilter -e '@.cve.metrics.cvssMetricV30[0].cvssData.baseScore' 2>/dev/null)
[ -z "$cvss" ] && cvss=$(echo "$vuln" | jsonfilter -e '@.cve.metrics.cvssMetricV2[0].cvssData.baseScore' 2>/dev/null)
[ -z "$cvss" ] && cvss="0"
local severity="low"
[ "$(echo "$cvss >= 4.0" | bc 2>/dev/null || echo 0)" = "1" ] && severity="medium"
[ "$(echo "$cvss >= 7.0" | bc 2>/dev/null || echo 0)" = "1" ] && severity="high"
[ "$(echo "$cvss >= 9.0" | bc 2>/dev/null || echo 0)" = "1" ] && severity="critical"
# Escape description for JSON
description=$(echo "$description" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
printf '{"cve":"%s","cvss":%s,"severity":"%s","published":"%s","description":"%s"}\n' \
"$cve_id" "$cvss" "$severity" "$published" "$description"
done | json_slurp
}
# Match CVEs to installed packages (basic keyword matching)
match_cves_to_packages() {
local cves="$1"
local packages="$2"
echo "$cves" | jsonfilter -e '@[*]' 2>/dev/null | while read -r cve; do
local cve_id=$(echo "$cve" | jsonfilter -e '@.cve' 2>/dev/null)
local description=$(echo "$cve" | jsonfilter -e '@.description' 2>/dev/null | tr '[:upper:]' '[:lower:]')
# Check if any installed package name appears in the CVE description
echo "$packages" | jsonfilter -e '@.packages[*].name' 2>/dev/null | while read -r pkg_name; do
pkg_lower=$(echo "$pkg_name" | tr '[:upper:]' '[:lower:]')
if echo "$description" | grep -qw "$pkg_lower"; then
echo "$cve" | sed "s/}$/,\"affected_package\":\"$pkg_name\"}/"
fi
done
done | json_slurp
}
# Build JSON array from newline-delimited JSON objects
json_slurp() {
printf '['
local first=1
while IFS= read -r line; do
[ -z "$line" ] && continue
echo "$line" | jsonfilter -e '@' >/dev/null 2>&1 || continue
[ $first -eq 0 ] && printf ','
first=0
printf '%s' "$line"
done
printf ']'
}