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:
15
agent/stats/cpu_common.go
Normal file
15
agent/stats/cpu_common.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package stats
|
||||
|
||||
func cpuBusyPercentFromDeltas(idleDelta, totalDelta float64) float64 {
|
||||
if totalDelta <= 0 {
|
||||
return 0
|
||||
}
|
||||
busyPct := (1.0 - idleDelta/totalDelta) * 100
|
||||
if busyPct < 0 {
|
||||
return 0
|
||||
}
|
||||
if busyPct > 100 {
|
||||
return 100
|
||||
}
|
||||
return busyPct
|
||||
}
|
||||
62
agent/stats/cpu_darwin.go
Normal file
62
agent/stats/cpu_darwin.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//go:build darwin
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (r *Reporter) SystemCPUPercent() float64 {
|
||||
idle, total, ok := readDarwinCPUSample()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !r.hasSample {
|
||||
r.lastIdle = idle
|
||||
r.lastTotal = total
|
||||
r.hasSample = true
|
||||
return 0
|
||||
}
|
||||
|
||||
idleDelta := float64(idle - r.lastIdle)
|
||||
totalDelta := float64(total - r.lastTotal)
|
||||
r.lastIdle = idle
|
||||
r.lastTotal = total
|
||||
|
||||
return cpuBusyPercentFromDeltas(idleDelta, totalDelta)
|
||||
}
|
||||
|
||||
func readDarwinCPUSample() (idle, total uint64, ok bool) {
|
||||
out, err := exec.Command("sysctl", "-n", "kern.cp_time").Output()
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return parseDarwinCPTimes(string(out))
|
||||
}
|
||||
|
||||
func parseDarwinCPTimes(raw string) (idle, total uint64, ok bool) {
|
||||
parts := strings.Fields(strings.TrimSpace(raw))
|
||||
if len(parts) < 4 {
|
||||
return 0, 0, false
|
||||
}
|
||||
var values []uint64
|
||||
for _, p := range parts {
|
||||
v, err := strconv.ParseUint(p, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
values = append(values, v)
|
||||
}
|
||||
for _, v := range values {
|
||||
total += v
|
||||
}
|
||||
// user, nice, sys, idle[, intr]
|
||||
idle = values[3]
|
||||
return idle, total, true
|
||||
}
|
||||
72
agent/stats/cpu_linux.go
Normal file
72
agent/stats/cpu_linux.go
Normal file
@@ -0,0 +1,72 @@
|
||||
//go:build linux
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (r *Reporter) SystemCPUPercent() float64 {
|
||||
idle, total, ok := readProcStatCPUSample()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !r.hasSample {
|
||||
r.lastIdle = idle
|
||||
r.lastTotal = total
|
||||
r.hasSample = true
|
||||
return 0
|
||||
}
|
||||
|
||||
idleDelta := float64(idle - r.lastIdle)
|
||||
totalDelta := float64(total - r.lastTotal)
|
||||
r.lastIdle = idle
|
||||
r.lastTotal = total
|
||||
|
||||
return cpuBusyPercentFromDeltas(idleDelta, totalDelta)
|
||||
}
|
||||
|
||||
func readProcStatCPUSample() (idle, total uint64, ok bool) {
|
||||
f, err := os.Open("/proc/stat")
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sc := bufio.NewScanner(f)
|
||||
if !sc.Scan() {
|
||||
return 0, 0, false
|
||||
}
|
||||
return parseProcStatCPU(sc.Text())
|
||||
}
|
||||
|
||||
func parseProcStatCPU(line string) (idle, total uint64, ok bool) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 || fields[0] != "cpu" {
|
||||
return 0, 0, false
|
||||
}
|
||||
var values []uint64
|
||||
for _, f := range fields[1:] {
|
||||
v, err := strconv.ParseUint(f, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
values = append(values, v)
|
||||
}
|
||||
for _, v := range values {
|
||||
total += v
|
||||
}
|
||||
// idle + iowait (index 3 and 4 when present)
|
||||
idle = values[3]
|
||||
if len(values) > 4 {
|
||||
idle += values[4]
|
||||
}
|
||||
return idle, total, true
|
||||
}
|
||||
34
agent/stats/cpu_linux_test.go
Normal file
34
agent/stats/cpu_linux_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build linux
|
||||
|
||||
package stats
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseProcStatCPU(t *testing.T) {
|
||||
idle, total, ok := parseProcStatCPU("cpu 4705 0 1234 8123 45 0 12 0 0 0")
|
||||
if !ok {
|
||||
t.Fatal("expected ok")
|
||||
}
|
||||
if idle != 8123+45 {
|
||||
t.Fatalf("idle=%d", idle)
|
||||
}
|
||||
wantTotal := uint64(4705 + 0 + 1234 + 8123 + 45 + 0 + 12 + 0 + 0 + 0)
|
||||
if total != wantTotal {
|
||||
t.Fatalf("total=%d want %d", total, wantTotal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProcStatCPUBadLine(t *testing.T) {
|
||||
if _, _, ok := parseProcStatCPU("meminfo"); ok {
|
||||
t.Fatal("expected false for non-cpu line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemCPUPercentLinuxSecondSample(t *testing.T) {
|
||||
r := NewReporter()
|
||||
_ = r.SystemCPUPercent() // first sample seeds baseline
|
||||
pct := r.SystemCPUPercent()
|
||||
if pct < 0 || pct > 100 {
|
||||
t.Fatalf("cpu percent out of range: %v", pct)
|
||||
}
|
||||
}
|
||||
7
agent/stats/cpu_stub.go
Normal file
7
agent/stats/cpu_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows && !linux && !darwin
|
||||
|
||||
package stats
|
||||
|
||||
func (r *Reporter) SystemCPUPercent() float64 {
|
||||
return 0
|
||||
}
|
||||
@@ -17,20 +17,6 @@ func filetimeToUint64(ft filetime) uint64 {
|
||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||
}
|
||||
|
||||
func cpuBusyPercentFromDeltas(idleDelta, totalDelta float64) float64 {
|
||||
if totalDelta <= 0 {
|
||||
return 0
|
||||
}
|
||||
busyPct := (1.0 - idleDelta/totalDelta) * 100
|
||||
if busyPct < 0 {
|
||||
return 0
|
||||
}
|
||||
if busyPct > 100 {
|
||||
return 100
|
||||
}
|
||||
return busyPct
|
||||
}
|
||||
|
||||
func (r *Reporter) SystemCPUPercent() float64 {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -10,6 +10,10 @@ import (
|
||||
|
||||
type Reporter struct {
|
||||
mu sync.Mutex
|
||||
|
||||
lastIdle uint64
|
||||
lastTotal uint64
|
||||
hasSample bool
|
||||
}
|
||||
|
||||
func NewReporter() *Reporter {
|
||||
@@ -45,6 +49,3 @@ func (r *Reporter) TotalMemoryMB() uint64 {
|
||||
return total / (1024 * 1024)
|
||||
}
|
||||
|
||||
func (r *Reporter) SystemCPUPercent() float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user