From 283f2567bef8bfdaba6cfb69bac7183220ff2a2f Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 15:15:50 +0100 Subject: [PATCH] feat(security): Add security stats and Gitea mirror commands Security Stats: - Add get_security_stats RPCD method for quick overview - Track WAN drops, firewall rejects, CrowdSec bans - Add secubox-stats CLI tool for quick stats check Gitea Mirror Commands: - Add mirror-sync to trigger mirror repository sync - Add mirror-list to show all mirrored repos - Add mirror-create to create new mirrors from GitHub URLs - Add repo-list to list all repositories - Requires API token: uci set gitea.main.api_token= Co-Authored-By: Claude Opus 4.5 --- .../Makefile | 4 + .../root/usr/bin/secubox-stats | 5 + .../rpcd/luci.secubox-security-threats | 72 +++++ .../secubox-app-gitea/files/usr/sbin/giteactl | 248 +++++++++++++++++- 4 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 package/secubox/luci-app-secubox-security-threats/root/usr/bin/secubox-stats diff --git a/package/secubox/luci-app-secubox-security-threats/Makefile b/package/secubox/luci-app-secubox-security-threats/Makefile index d10ac075..ea0b769d 100644 --- a/package/secubox/luci-app-secubox-security-threats/Makefile +++ b/package/secubox/luci-app-secubox-security-threats/Makefile @@ -22,6 +22,10 @@ define Package/luci-app-secubox-security-threats/conffiles endef define Package/luci-app-secubox-security-threats/install + # CLI tool + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./root/usr/bin/secubox-stats $(1)/usr/bin/ + # RPCD backend (MUST be 755 for ubus calls) $(INSTALL_DIR) $(1)/usr/libexec/rpcd $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.secubox-security-threats $(1)/usr/libexec/rpcd/ diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/bin/secubox-stats b/package/secubox/luci-app-secubox-security-threats/root/usr/bin/secubox-stats new file mode 100644 index 00000000..920695a4 --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/bin/secubox-stats @@ -0,0 +1,5 @@ +#!/bin/sh +# SecuBox Security Stats - Quick overview +# Copyright (C) 2026 CyberMind.fr + +ubus call luci.secubox-security-threats get_security_stats 2>/dev/null | jsonfilter -e '@' 2>/dev/null || echo '{"error": "RPCD not available"}' diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats index 06836a57..eccc35c1 100755 --- a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats @@ -262,6 +262,72 @@ check_block_rules() { config_foreach check_rule_match block_rule "$category" "$risks" "$score" "$ip" } +# ============================================================================== +# SECURITY STATS (Quick Overview) +# ============================================================================== + +# Get overall security statistics from all sources +get_security_stats() { + local wan_drops=0 + local fw_rejects=0 + local cs_bans=0 + local cs_alerts_24h=0 + local haproxy_conns=0 + local invalid_conns=0 + + # WAN dropped packets (from kernel stats) + if [ -f /sys/class/net/br-wan/statistics/rx_dropped ]; then + wan_drops=$(cat /sys/class/net/br-wan/statistics/rx_dropped 2>/dev/null) + elif [ -f /sys/class/net/eth1/statistics/rx_dropped ]; then + wan_drops=$(cat /sys/class/net/eth1/statistics/rx_dropped 2>/dev/null) + fi + wan_drops=${wan_drops:-0} + + # Firewall rejects from logs (last 24h) + fw_rejects=$(logread 2>/dev/null | grep -c "reject\|drop" || echo 0) + fw_rejects=$(echo "$fw_rejects" | tr -d '\n') + fw_rejects=${fw_rejects:-0} + + # CrowdSec active bans + if [ -x "$CSCLI" ]; then + cs_bans=$($CSCLI decisions list -o json 2>/dev/null | grep -c '"id":' || echo 0) + cs_bans=$(echo "$cs_bans" | tr -d '\n') + cs_bans=${cs_bans:-0} + + # CrowdSec alerts in last 24h + cs_alerts_24h=$($CSCLI alerts list -o json --since 24h 2>/dev/null | grep -c '"id":' || echo 0) + cs_alerts_24h=$(echo "$cs_alerts_24h" | tr -d '\n') + cs_alerts_24h=${cs_alerts_24h:-0} + fi + + # Invalid connections (conntrack) + if [ -f /proc/net/nf_conntrack ]; then + invalid_conns=$(grep -c "INVALID\|UNREPLIED" /proc/net/nf_conntrack 2>/dev/null || echo 0) + fi + invalid_conns=$(echo "$invalid_conns" | tr -d '\n') + invalid_conns=${invalid_conns:-0} + + # HAProxy connections (if running in LXC) + if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then + haproxy_conns=$(lxc-attach -n haproxy -- sh -c 'echo "show stat" | socat stdio /var/run/haproxy/admin.sock 2>/dev/null | tail -n+2 | awk -F, "{sum+=\$8} END {print sum}"' 2>/dev/null || echo 0) + fi + haproxy_conns=$(echo "$haproxy_conns" | tr -d '\n') + haproxy_conns=${haproxy_conns:-0} + + # Output JSON + cat << EOF +{ + "wan_dropped": $wan_drops, + "firewall_rejects": $fw_rejects, + "crowdsec_bans": $cs_bans, + "crowdsec_alerts_24h": $cs_alerts_24h, + "invalid_connections": $invalid_conns, + "haproxy_connections": $haproxy_conns, + "timestamp": "$(date -Iseconds)" +} +EOF +} + # ============================================================================== # STATISTICS # ============================================================================== @@ -304,6 +370,8 @@ case "$1" in list) # List available methods json_init + json_add_object "get_security_stats" + json_close_object json_add_object "status" json_close_object json_add_object "get_active_threats" @@ -334,6 +402,10 @@ case "$1" in call) case "$2" in + get_security_stats) + get_security_stats + ;; + status) json_init json_add_boolean "enabled" 1 diff --git a/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl b/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl index 18358d55..45cc1edb 100644 --- a/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl +++ b/package/secubox/secubox-app-gitea/files/usr/sbin/giteactl @@ -91,6 +91,15 @@ Commands: --password --email + mirror-sync Sync a mirrored repository + mirror-list List all mirrored repositories + mirror-create Create a new mirror from URL + --name + --url + --owner (default: first admin user) + + repo-list List all repositories + service-run Start service (used by init) service-stop Stop service (used by init) @@ -718,6 +727,235 @@ cmd_admin_create_user() { fi } +# Get Gitea API token (from admin user or config) +get_api_token() { + local token + token="$(uci_get main.api_token)" + if [ -n "$token" ]; then + echo "$token" + return 0 + fi + + # Try to get token from container + if lxc_running; then + token=$(lxc-attach -n "$LXC_NAME" -- cat /data/api_token 2>/dev/null) + if [ -n "$token" ]; then + echo "$token" + return 0 + fi + fi + + return 1 +} + +# Get Gitea API URL +get_api_url() { + load_config + echo "http://127.0.0.1:${http_port}/api/v1" +} + +# Make Gitea API call +gitea_api() { + local method="$1" + local endpoint="$2" + local data="$3" + local token + + token=$(get_api_token) || { + log_error "No API token configured. Set with: uci set gitea.main.api_token=" + log_error "Generate token in Gitea: Settings → Applications → Generate Token" + return 1 + } + + local api_url=$(get_api_url) + local url="${api_url}${endpoint}" + + if [ "$method" = "GET" ]; then + wget -q -O- --header="Authorization: token $token" "$url" 2>/dev/null + elif [ "$method" = "POST" ]; then + if [ -n "$data" ]; then + wget -q -O- --header="Authorization: token $token" \ + --header="Content-Type: application/json" \ + --post-data="$data" "$url" 2>/dev/null + else + wget -q -O- --header="Authorization: token $token" \ + --post-data="" "$url" 2>/dev/null + fi + fi +} + +cmd_mirror_sync() { + load_config + local repo_name="$1" + + if [ -z "$repo_name" ]; then + log_error "Usage: giteactl mirror-sync or " + return 1 + fi + + if ! lxc_running; then + log_error "Gitea container is not running" + return 1 + fi + + # If no owner specified, try to find the repo + if ! echo "$repo_name" | grep -q "/"; then + # Search for repo in all users + local found_owner + found_owner=$(gitea_api GET "/repos/search?q=$repo_name" 2>/dev/null | \ + jsonfilter -e '@.data[0].owner.login' 2>/dev/null) + if [ -n "$found_owner" ]; then + repo_name="${found_owner}/${repo_name}" + else + log_error "Repository not found: $repo_name" + log_error "Specify full path: owner/repo" + return 1 + fi + fi + + log_info "Syncing mirror: $repo_name" + + # Trigger mirror sync via API + local result + result=$(gitea_api POST "/repos/${repo_name}/mirror-sync" 2>&1) + + if [ $? -eq 0 ]; then + log_info "Mirror sync triggered for $repo_name" + log_info "Check progress in Gitea web UI" + else + log_error "Failed to sync mirror: $result" + + # Try alternative: use gitea command directly in container + log_info "Trying direct sync via container..." + lxc-attach -n "$LXC_NAME" -- su-exec git /usr/local/bin/gitea admin repo-sync-releases \ + --config /data/custom/conf/app.ini 2>/dev/null || true + + return 1 + fi +} + +cmd_mirror_list() { + load_config + + if ! lxc_running; then + log_error "Gitea container is not running" + return 1 + fi + + log_info "Fetching mirror repositories..." + + local repos + repos=$(gitea_api GET "/repos/search?mirror=true&limit=50" 2>/dev/null) + + if [ -z "$repos" ]; then + echo "No mirrored repositories found (or API token not set)" + return 1 + fi + + echo "" + echo "Mirrored Repositories:" + echo "======================" + echo "$repos" | jsonfilter -e '@.data[*]' 2>/dev/null | while read repo; do + local name=$(echo "$repo" | jsonfilter -e '@.full_name' 2>/dev/null) + local url=$(echo "$repo" | jsonfilter -e '@.original_url' 2>/dev/null) + local updated=$(echo "$repo" | jsonfilter -e '@.updated_at' 2>/dev/null) + echo " $name" + echo " Source: $url" + echo " Updated: $updated" + echo "" + done +} + +cmd_mirror_create() { + load_config + + local name="" + local url="" + local owner="" + + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + --name) name="$2"; shift 2 ;; + --url) url="$2"; shift 2 ;; + --owner) owner="$2"; shift 2 ;; + *) shift ;; + esac + done + + if [ -z "$name" ] || [ -z "$url" ]; then + log_error "Usage: giteactl mirror-create --name --url [--owner ]" + return 1 + fi + + if ! lxc_running; then + log_error "Gitea container is not running" + return 1 + fi + + # Get default owner if not specified + if [ -z "$owner" ]; then + owner=$(gitea_api GET "/user" 2>/dev/null | jsonfilter -e '@.login' 2>/dev/null) + if [ -z "$owner" ]; then + log_error "Could not determine owner. Specify with --owner" + return 1 + fi + fi + + log_info "Creating mirror repository: $owner/$name from $url" + + local data=$(cat <&1) + + if echo "$result" | grep -q '"id":'; then + log_info "Mirror created successfully: $owner/$name" + log_info "First sync in progress..." + else + log_error "Failed to create mirror: $result" + return 1 + fi +} + +cmd_repo_list() { + load_config + + if ! lxc_running; then + log_error "Gitea container is not running" + return 1 + fi + + local repos + repos=$(gitea_api GET "/repos/search?limit=100" 2>/dev/null) + + if [ -z "$repos" ]; then + echo "No repositories found (or API token not set)" + return 1 + fi + + echo "" + echo "Repositories:" + echo "=============" + echo "$repos" | jsonfilter -e '@.data[*].full_name' 2>/dev/null | while read name; do + local is_mirror=$(echo "$repos" | jsonfilter -e "@.data[?(@.full_name=='$name')].mirror" 2>/dev/null) + if [ "$is_mirror" = "true" ]; then + echo " [mirror] $name" + else + echo " $name" + fi + done +} + cmd_service_run() { require_root load_config @@ -751,7 +989,11 @@ case "${1:-}" in *) echo "Usage: giteactl admin create-user --username --password --email "; exit 1 ;; esac ;; - service-run) shift; cmd_service_run "$@" ;; - service-stop) shift; cmd_service_stop "$@" ;; - *) usage ;; + mirror-sync) shift; cmd_mirror_sync "$@" ;; + mirror-list) shift; cmd_mirror_list "$@" ;; + mirror-create) shift; cmd_mirror_create "$@" ;; + repo-list) shift; cmd_repo_list "$@" ;; + service-run) shift; cmd_service_run "$@" ;; + service-stop) shift; cmd_service_stop "$@" ;; + *) usage ;; esac