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>
217 lines
7.1 KiB
Bash
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 ']'
|
|
}
|