mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-16 00:47:29 +02:00
216 lines
7.9 KiB
Go
216 lines
7.9 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// VulnerabilityAlertSubscription is the single source of truth shared by Web
|
|
// settings and robot commands. Alerts are opt-in and user scoped.
|
|
type VulnerabilityAlertSubscription struct {
|
|
UserID string `json:"user_id"`
|
|
Enabled bool `json:"enabled"`
|
|
MinSeverity string `json:"min_severity"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type VulnerabilityAlertRecipient struct {
|
|
UserID string
|
|
Platform string
|
|
ExternalUserID string
|
|
}
|
|
|
|
type VulnerabilityAlertDelivery struct {
|
|
ID int64
|
|
Vulnerability *Vulnerability
|
|
UserID string
|
|
Platform string
|
|
ExternalUserID string
|
|
Attempts int
|
|
}
|
|
|
|
var vulnerabilitySeverityRank = map[string]int{
|
|
"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4,
|
|
}
|
|
|
|
func NormalizeVulnerabilityAlertSeverity(value string) (string, error) {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
value = "high"
|
|
}
|
|
if _, ok := vulnerabilitySeverityRank[value]; !ok {
|
|
return "", fmt.Errorf("invalid minimum severity %q", value)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (db *DB) GetVulnerabilityAlertSubscription(userID string) (*VulnerabilityAlertSubscription, error) {
|
|
userID = strings.TrimSpace(userID)
|
|
var sub VulnerabilityAlertSubscription
|
|
var enabled int
|
|
var createdAt, updatedAt string
|
|
err := db.QueryRow(`SELECT user_id, enabled, min_severity, created_at, updated_at
|
|
FROM vulnerability_alert_subscriptions WHERE user_id = ?`, userID).
|
|
Scan(&sub.UserID, &enabled, &sub.MinSeverity, &createdAt, &updatedAt)
|
|
if err == sql.ErrNoRows {
|
|
now := time.Now()
|
|
return &VulnerabilityAlertSubscription{UserID: userID, MinSeverity: "high", CreatedAt: now, UpdatedAt: now}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sub.Enabled = enabled != 0
|
|
sub.CreatedAt = parseDBTime(createdAt)
|
|
sub.UpdatedAt = parseDBTime(updatedAt)
|
|
return &sub, nil
|
|
}
|
|
|
|
func (db *DB) UpsertVulnerabilityAlertSubscription(userID string, enabled bool, minSeverity string) (*VulnerabilityAlertSubscription, error) {
|
|
userID = strings.TrimSpace(userID)
|
|
if userID == "" {
|
|
return nil, fmt.Errorf("user id is required")
|
|
}
|
|
severity, err := NormalizeVulnerabilityAlertSeverity(minSeverity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
_, err = db.Exec(`INSERT INTO vulnerability_alert_subscriptions
|
|
(user_id, enabled, min_severity, created_at, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id) DO UPDATE SET enabled = excluded.enabled,
|
|
min_severity = excluded.min_severity, updated_at = excluded.updated_at`,
|
|
userID, boolToInt(enabled), severity, now, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return db.GetVulnerabilityAlertSubscription(userID)
|
|
}
|
|
|
|
// ListVulnerabilityAlertRecipients applies the same RBAC ownership/assignment
|
|
// boundaries as the vulnerability list, then expands only enabled robot bindings.
|
|
func (db *DB) ListVulnerabilityAlertRecipients(vuln *Vulnerability) ([]VulnerabilityAlertRecipient, error) {
|
|
if vuln == nil {
|
|
return nil, nil
|
|
}
|
|
rank, ok := vulnerabilitySeverityRank[strings.ToLower(strings.TrimSpace(vuln.Severity))]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
rows, err := db.Query(`
|
|
SELECT DISTINCT s.user_id, b.platform, b.external_user_id, s.min_severity
|
|
FROM vulnerability_alert_subscriptions s
|
|
JOIN rbac_users u ON u.id = s.user_id AND u.enabled = 1
|
|
JOIN robot_user_bindings b ON b.rbac_user_id = s.user_id AND b.enabled = 1
|
|
WHERE s.enabled = 1 AND (
|
|
EXISTS (SELECT 1 FROM vulnerabilities v WHERE v.id = ? AND v.owner_user_id = s.user_id)
|
|
OR EXISTS (SELECT 1 FROM rbac_resource_assignments ra WHERE ra.user_id = s.user_id AND ra.resource_type = 'vulnerability' AND ra.resource_id = ?)
|
|
OR (? <> '' AND (EXISTS (SELECT 1 FROM projects p WHERE p.id = ? AND p.owner_user_id = s.user_id)
|
|
OR EXISTS (SELECT 1 FROM rbac_resource_assignments pra WHERE pra.user_id = s.user_id AND pra.resource_type = 'project' AND pra.resource_id = ?)))
|
|
OR (? <> '' AND (EXISTS (SELECT 1 FROM conversations c WHERE c.id = ? AND c.owner_user_id = s.user_id)
|
|
OR EXISTS (SELECT 1 FROM rbac_resource_assignments cra WHERE cra.user_id = s.user_id AND cra.resource_type = 'conversation' AND cra.resource_id = ?)))
|
|
)`, vuln.ID, vuln.ID, vuln.ProjectID, vuln.ProjectID, vuln.ProjectID,
|
|
vuln.ConversationID, vuln.ConversationID, vuln.ConversationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]VulnerabilityAlertRecipient, 0)
|
|
for rows.Next() {
|
|
var recipient VulnerabilityAlertRecipient
|
|
var minimum string
|
|
if err := rows.Scan(&recipient.UserID, &recipient.Platform, &recipient.ExternalUserID, &minimum); err != nil {
|
|
return nil, err
|
|
}
|
|
if rank >= vulnerabilitySeverityRank[minimum] {
|
|
out = append(out, recipient)
|
|
}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (db *DB) SetVulnerabilityCreatedHook(hook func(*Vulnerability)) {
|
|
db.vulnerabilityCreatedHook = hook
|
|
}
|
|
|
|
// NotifyVulnerabilityCreated must be called after resource ownership has been
|
|
// committed. Delivery runs asynchronously and never delays the write path.
|
|
func (db *DB) NotifyVulnerabilityCreated(vulnerability *Vulnerability) {
|
|
if db == nil || vulnerability == nil || db.vulnerabilityCreatedHook == nil {
|
|
return
|
|
}
|
|
created := *vulnerability
|
|
go db.vulnerabilityCreatedHook(&created)
|
|
}
|
|
|
|
func (db *DB) EnqueueVulnerabilityAlertDeliveries(vulnerabilityID string, recipients []VulnerabilityAlertRecipient) error {
|
|
now := time.Now()
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
for _, r := range recipients {
|
|
if _, err := tx.Exec(`INSERT INTO vulnerability_alert_deliveries
|
|
(vulnerability_id, user_id, platform, external_user_id, status, attempts, next_attempt_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, 'pending', 0, ?, ?, ?)
|
|
ON CONFLICT(vulnerability_id, platform, external_user_id) DO NOTHING`,
|
|
vulnerabilityID, r.UserID, r.Platform, r.ExternalUserID, now, now, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (db *DB) ListDueVulnerabilityAlertDeliveries(limit int) ([]VulnerabilityAlertDelivery, error) {
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
rows, err := db.Query(`SELECT d.id, d.user_id, d.platform, d.external_user_id, d.attempts,
|
|
v.id, COALESCE(v.conversation_id,''), COALESCE(v.project_id,''), v.title, COALESCE(v.description,''),
|
|
v.severity, v.status, COALESCE(v.vulnerability_type,''), COALESCE(v.target,''),
|
|
COALESCE(v.impact,''), COALESCE(v.recommendation,''), v.created_at, v.updated_at
|
|
FROM vulnerability_alert_deliveries d JOIN vulnerabilities v ON v.id = d.vulnerability_id
|
|
WHERE d.status IN ('pending','retry') AND d.next_attempt_at <= ?
|
|
ORDER BY d.next_attempt_at, d.id LIMIT ?`, time.Now(), limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []VulnerabilityAlertDelivery
|
|
for rows.Next() {
|
|
var d VulnerabilityAlertDelivery
|
|
v := &Vulnerability{}
|
|
if err := rows.Scan(&d.ID, &d.UserID, &d.Platform, &d.ExternalUserID, &d.Attempts,
|
|
&v.ID, &v.ConversationID, &v.ProjectID, &v.Title, &v.Description, &v.Severity, &v.Status,
|
|
&v.Type, &v.Target, &v.Impact, &v.Recommendation, &v.CreatedAt, &v.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
d.Vulnerability = v
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (db *DB) MarkVulnerabilityAlertDeliverySent(id int64) error {
|
|
_, err := db.Exec(`UPDATE vulnerability_alert_deliveries SET status='sent', attempts=attempts+1, last_error='', updated_at=? WHERE id=?`, time.Now(), id)
|
|
return err
|
|
}
|
|
|
|
func (db *DB) MarkVulnerabilityAlertDeliveryFailed(id int64, attempts int, sendErr error) error {
|
|
status := "retry"
|
|
if attempts >= 5 {
|
|
status = "failed"
|
|
}
|
|
delay := time.Minute * time.Duration(1<<min(attempts, 6))
|
|
message := ""
|
|
if sendErr != nil {
|
|
message = sendErr.Error()
|
|
}
|
|
_, err := db.Exec(`UPDATE vulnerability_alert_deliveries SET status=?, attempts=?, next_attempt_at=?, last_error=?, updated_at=? WHERE id=?`,
|
|
status, attempts, time.Now().Add(delay), message, time.Now(), id)
|
|
return err
|
|
}
|