114 lines
3.3 KiB
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()
|
|
}
|