package db import ( "fmt" "strings" "time" ) // CredEdge records a lateral spread credential attempt (profile hash ref only — no secrets). type CredEdge struct { ID int64 `json:"id"` Host string `json:"host"` Subnet string `json:"subnet"` CredentialProfileID string `json:"credential_profile_id"` Success bool `json:"success"` Method string `json:"method"` AgentID string `json:"agent_id"` CreatedAt time.Time `json:"created_at"` } // CredGraphSubnetRow aggregates cred_edges per /24 for the Emberwake graph UI. type CredGraphSubnetRow struct { Subnet string `json:"subnet"` EdgeCount int `json:"edges"` SuccessCount int `json:"success_count"` FailCount int `json:"fail_count"` } // CredProfileAffinity ranks credential profiles that succeeded on a subnet. type CredProfileAffinity struct { CredentialProfileID string `json:"credential_profile_id"` SuccessCount int `json:"success_count"` LastSuccessAt string `json:"last_success_at,omitempty"` } func (d *Database) InsertCredEdge(host, subnet, profileID, method, agentID string, success bool) error { host = strings.TrimSpace(host) subnet = strings.TrimSpace(subnet) profileID = strings.TrimSpace(profileID) if host == "" || subnet == "" || profileID == "" { return fmt.Errorf("cred edge requires host, subnet, and credential_profile_id") } _, err := d.Exec( `INSERT INTO cred_edges (host, subnet, credential_profile_id, success, method, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, host, subnet, profileID, boolToInt(success), strings.TrimSpace(method), strings.TrimSpace(agentID), time.Now().UTC(), ) return err } func (d *Database) ListCredProfileAffinity(subnet string) ([]CredProfileAffinity, error) { subnet = strings.TrimSpace(subnet) if subnet == "" { return nil, nil } rows, err := d.Query(` SELECT credential_profile_id, COUNT(*) AS wins, MAX(created_at) AS last_ok FROM cred_edges WHERE subnet = ? AND success = 1 GROUP BY credential_profile_id ORDER BY last_ok DESC, wins DESC`, subnet, ) if err != nil { return nil, err } defer rows.Close() var out []CredProfileAffinity for rows.Next() { var row CredProfileAffinity var lastOK string if err := rows.Scan(&row.CredentialProfileID, &row.SuccessCount, &lastOK); err != nil { return nil, err } if strings.TrimSpace(lastOK) != "" { if parsed, parseErr := time.Parse(time.RFC3339, lastOK); parseErr == nil { row.LastSuccessAt = parsed.UTC().Format(time.RFC3339) } else { row.LastSuccessAt = lastOK } } out = append(out, row) } return out, rows.Err() } func (d *Database) ListCredGraphBySubnet() ([]CredGraphSubnetRow, error) { rows, err := d.Query(` SELECT subnet, COUNT(*) AS edge_count, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS ok_cnt, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS fail_cnt FROM cred_edges GROUP BY subnet ORDER BY edge_count DESC, subnet ASC`) if err != nil { return nil, err } defer rows.Close() var out []CredGraphSubnetRow for rows.Next() { var row CredGraphSubnetRow if err := rows.Scan(&row.Subnet, &row.EdgeCount, &row.SuccessCount, &row.FailCount); err != nil { return nil, err } out = append(out, row) } return out, rows.Err() }