feat: fleet ops, KEV scan, tunnels, beacon fallback, persistence

Extend owned-fleet control with scheduled tasks, audit log, file browser,
HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge
options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
This commit is contained in:
AetherForge
2026-06-04 09:34:33 -07:00
parent d52479c9a6
commit 5fc601b564
111 changed files with 5845 additions and 116 deletions

View File

@@ -182,7 +182,7 @@ func (e *Evaluator) fire(ev AlertEvent, cooldownKey string) {
log.Printf("[Alert] %s: %s", ev.Type, ev.Message)
s := e.settings()
if s.EnabledForAlertType(ev.Type) {
NotifyAll(s.NotifyConfig, "AetherForge "+ev.Type, ev.Message)
NotifyAllEvent(s.NotifyConfig, ev.Type, "AetherForge "+ev.Type, ev.Message)
}
if e.broadcast != nil {
e.broadcast(ev)

View File

@@ -0,0 +1,56 @@
package alerts
import (
"encoding/json"
"strconv"
"strings"
)
// kevExposurePayload mirrors agent KEVScanReport JSON.
type kevExposurePayload struct {
ExposedCount int `json:"exposed_count"`
CriticalCount int `json:"critical_count"`
LikelyCount int `json:"likely_count"`
RiskScore int `json:"risk_score"`
Summary string `json:"summary"`
Findings []struct {
CVE string `json:"cve"`
Name string `json:"name"`
Status string `json:"status"`
Severity string `json:"severity"`
Detail string `json:"detail"`
} `json:"findings"`
}
const EventKEVExposure = "kev_exposure"
// NotifyKEVFromSysCheck parses a full_sys_check message and sends Telegram if enabled.
func NotifyKEVFromSysCheck(n *Notifier, agentName, message string) {
if n == nil || strings.TrimSpace(message) == "" {
return
}
var report struct {
KEV *kevExposurePayload `json:"kev_exposure"`
}
if err := json.Unmarshal([]byte(message), &report); err != nil || report.KEV == nil {
return
}
k := report.KEV
if k.ExposedCount == 0 && k.CriticalCount == 0 {
return
}
s := n.settings()
if !s.Events.KEVExposure {
return
}
body := agentName + ": " + k.Summary
if body == agentName+": " {
body = agentName + ": KEV exposure indicators — exposed=" + strconv.Itoa(k.ExposedCount) + " critical=" + strconv.Itoa(k.CriticalCount)
}
for _, f := range k.Findings {
if f.Status == "exposed" && f.Severity == "critical" {
body += "\n• " + f.CVE + " " + f.Name
}
}
n.Emit(EventKEVExposure, "AetherForge KEV alert", body)
}

View File

@@ -0,0 +1,18 @@
package alerts
import "testing"
func TestNotifyKEVFromSysCheckNoPanic(t *testing.T) {
n := NewNotifier(func() Settings {
return NewSettings(NotifyConfig{}, EventToggles{KEVExposure: true})
})
msg := `{"kev_exposure":{"exposed_count":1,"critical_count":1,"summary":"test","findings":[{"cve":"CVE-2021-26855","name":"ProxyLogon","status":"exposed","severity":"critical"}]}}`
NotifyKEVFromSysCheck(n, "worker-1", msg)
}
func TestNotifyKEVSkipsWhenClear(t *testing.T) {
n := NewNotifier(func() Settings {
return NewSettings(NotifyConfig{TelegramBotToken: "x", TelegramChatID: "1"}, EventToggles{KEVExposure: true})
})
NotifyKEVFromSysCheck(n, "w", `{"kev_exposure":{"exposed_count":0,"critical_count":0}}`)
}

View File

@@ -19,11 +19,11 @@ func (n *Notifier) Emit(event string, title, body string) {
if !n.eventEnabled(s, event) {
return
}
if s.TelegramBotToken == "" && s.TelegramChatID == "" && !s.EmailEnabled {
if s.TelegramBotToken == "" && s.TelegramChatID == "" && !s.EmailEnabled && s.WebhookURL == "" {
return
}
log.Printf("[Notify] %s: %s", event, body)
NotifyAll(s.NotifyConfig, title, body)
NotifyAllEvent(s.NotifyConfig, event, title, body)
}
func (n *Notifier) eventEnabled(s Settings, event string) bool {
@@ -34,6 +34,8 @@ func (n *Notifier) eventEnabled(s Settings, event string) bool {
return s.Events.AgentReconnect
case EventBuildComplete:
return s.Events.BuildComplete
case EventKEVExposure:
return s.Events.KEVExposure
case "offline":
return s.Events.AgentOffline
case "hashrate_drop":

View File

@@ -14,6 +14,7 @@ import (
type NotifyConfig struct {
TelegramBotToken string
TelegramChatID string
WebhookURL string
EmailEnabled bool
SMTPHost string
SMTPPort int
@@ -89,7 +90,41 @@ func SendEmail(cfg NotifyConfig, subject, body string) error {
return smtp.SendMail(addr, auth, from, []string{cfg.EmailTo}, []byte(msg))
}
func SendWebhook(cfg NotifyConfig, event, subject, text string) error {
if cfg.WebhookURL == "" {
return nil
}
payload, _ := json.Marshal(map[string]string{
"event": event,
"title": subject,
"message": text,
})
req, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("webhook status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}
func NotifyAll(cfg NotifyConfig, subject, text string) {
_ = SendTelegram(cfg, subject+": "+text)
_ = SendEmail(cfg, subject, text)
}
// NotifyAllEvent sends to Telegram, email, and optional operator webhook.
func NotifyAllEvent(cfg NotifyConfig, event, subject, text string) {
_ = SendTelegram(cfg, subject+": "+text)
_ = SendEmail(cfg, subject, text)
_ = SendWebhook(cfg, event, subject, text)
}

View File

@@ -8,6 +8,7 @@ type EventToggles struct {
HashrateDrop bool
RejectionRate bool
BuildComplete bool
KEVExposure bool
}
// Settings combines delivery credentials with per-event toggles.