Files
AetherForge/server/internal/db/cred_edges.go

114 lines
3.3 KiB
Go

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()
}