bastion 0.60.0
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 3m36s
ci / fuzz (push) Successful in 3m35s

This commit is contained in:
Privatae
2026-11-02 00:10:58 +00:00
parent 4a5c310482
commit 4f8285ef18
100 changed files with 11537 additions and 840 deletions
+272
View File
@@ -0,0 +1,272 @@
package main
// uimachine.go — the sections of This machine (plan Addendum 4, step 4):
// scheduled jobs that start an agent, the MCP tool servers configured here
// and what is wrong with them, and what runs that the baseline does not
// hold. Each loads once when the page opens (hx-trigger="load"), apart from
// the machine's state above them, which refreshes every three seconds: these
// read files and walk processes, and none of them changes that fast.
//
// Recording a baseline takes the owner's code. It decides what counts as
// normal here, and everything in it stops being reported, so it is the act
// an agent clicking through a signed-in console would most want.
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"
)
type uiSchedule struct {
Agent string `json:"agent"`
Schedule string `json:"schedule"`
Where string `json:"where"`
User string `json:"user,omitempty"`
Command string `json:"command"`
}
func collectSchedules() ([]uiSchedule, bool) {
entries, scan := scanSchedulers()
var out []uiSchedule
for _, e := range entries {
args, _ := redactSecretArgs(strings.Fields(e.Command))
out = append(out, uiSchedule{
Agent: familyLabel(e.Signature), Schedule: cronWords(e.Schedule), Where: e.Kind + " " + e.Name,
User: e.User, Command: cleanText(strings.Join(args, " "), 160),
})
}
return out, scan.Supported
}
type uiToolGroup struct {
Client string `json:"client"`
Servers []uiToolServer `json:"servers"`
Bad int `json:"with_problems"`
}
// toolProblems is one tool server's problems in plain words: the same
// sentences an agent's page uses (toolServersFor).
func toolProblems(s mcpServerObs) []string {
if s.Disabled {
return nil
}
var p []string
if s.SecretInPlaintext {
p = append(p, "a key is written in plain text in "+s.Path)
}
if s.PackagePinned == "no" && s.PackageName != "" {
p = append(p, "runs "+s.PackageName+" with no fixed version, so whatever is published next is what runs")
}
if s.RunningState == "running_not_declared" {
p = append(p, "running, but not in any settings file this machine can read")
}
return p
}
func collectToolGroups() []uiToolGroup {
servers, _ := readMCPConfigs()
by := map[string]*uiToolGroup{}
var order []string
for _, s := range servers {
c := s.Client
if c == "" {
c = "not in any settings file"
}
g, ok := by[c]
if !ok {
g = &uiToolGroup{Client: familyLabel(c)}
by[c] = g
order = append(order, c)
}
name := s.Name
if name == "" {
name = s.PackageName
}
t := uiToolServer{Name: name, Where: s.Path, Problems: toolProblems(s)}
if len(t.Problems) > 0 {
g.Bad++
}
g.Servers = append(g.Servers, t)
}
sort.Strings(order)
var out []uiToolGroup
for _, c := range order {
g := by[c]
sort.SliceStable(g.Servers, func(i, j int) bool { return len(g.Servers[i].Problems) > len(g.Servers[j].Problems) })
out = append(out, *g)
}
return out
}
// ── the baseline ────────────────────────────────────────────────────────────
type uiBaselineView struct {
Flash string
FlashClass string
Has bool
TakenAt string
Entries int
Note string
OwnerCode bool
Compared bool
ComparedAt string
State string
Findings []string
WalkFailed string
}
// lastDelta keeps the last comparison for the page: a delta pass walks every
// process and digests their files, so it runs when asked, not on every load.
var lastDelta struct {
sync.Mutex
r *deltaReport
at time.Time
}
func collectBaselineView() uiBaselineView {
v := uiBaselineView{OwnerCode: stepupEnrolled(existingMachineFactorDir())}
if f, ok, err := baselineLoad(); err == nil {
v.Has, v.TakenAt, v.Entries, v.Note = true, f.TakenAt, len(f.Entries), baselineIntegrityNote(f, ok)
}
lastDelta.Lock()
r, at := lastDelta.r, lastDelta.at
lastDelta.Unlock()
if r != nil {
v.Compared, v.ComparedAt, v.State, v.WalkFailed = true, at.Local().Format("15:04:05"), r.Baseline, r.WalkFailed
for _, ev := range r.Findings {
v.Findings = append(v.Findings, ev.Level+": "+deltaOneLine(ev))
}
}
return v
}
func (u *uiServer) machineRoutes(mux *http.ServeMux) {
mux.HandleFunc("/fragments/machine/schedules", u.guarded(u.machineSchedules))
mux.HandleFunc("/fragments/machine/tools", u.guarded(u.machineTools))
mux.HandleFunc("/fragments/machine/baseline", u.guarded(u.machineBaseline))
mux.HandleFunc("/machine/compare", u.guarded(u.machineCompare))
mux.HandleFunc("/machine/baseline", u.guarded(u.machineRecordBaseline))
mux.HandleFunc("/machine/report", u.guarded(u.machineReport))
}
// machineReport writes the report (report.go) and hands it to the browser as
// a file. A POST: writing it walks every process and compares the baseline.
func (u *uiServer) machineReport(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
now := uiNow()
rep := buildReport(now, now.Add(-7*24*time.Hour))
name := "bastion-report-" + now.Format("2006-01-02-1504") + ".md"
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Content-Type-Options", "nosniff")
renderReportMarkdown(w, rep)
}
func (u *uiServer) machineSchedules(w http.ResponseWriter, r *http.Request) {
s, ok := collectSchedules()
u.renderView(w, "machine-schedules", map[string]any{"Items": s, "Supported": ok})
}
func (u *uiServer) machineTools(w http.ResponseWriter, r *http.Request) {
u.renderView(w, "machine-tools", collectToolGroups())
}
func (u *uiServer) machineBaseline(w http.ResponseWriter, r *http.Request) {
u.renderView(w, "machine-baseline", collectBaselineView())
}
func (u *uiServer) machineCompare(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
rep := deltaPass(&digestCache{}, time.Now(), deltaRoleOperator)
lastDelta.Lock()
lastDelta.r, lastDelta.at = &rep, time.Now()
lastDelta.Unlock()
u.renderView(w, "machine-baseline", collectBaselineView())
}
func (u *uiServer) machineRecordBaseline(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
flash, class := recordBaselineFromConsole(r.FormValue("code"))
v := collectBaselineView()
v.Flash, v.FlashClass = flash, class
u.renderView(w, "machine-baseline", v)
}
// recordBaselineFromConsole takes the owner's code, then the same baseline
// `observe:baseline --force` takes (collectBaseline, baselineSave).
func recordBaselineFromConsole(code string) (string, string) {
dir := existingMachineFactorDir()
if !stepupEnrolled(dir) {
return "Set up your code under Settings first. Recording a baseline takes it.", "warn"
}
code = strings.TrimSpace(code)
if code == "" {
return "Type the code from your authenticator.", "warn"
}
approval, err := machineCodeSpend(dir, uiNow(), func() (string, error) {
return stepupIssueApproval(dir, code, "local_baseline")
})
if err != nil {
return "Not recorded: " + err.Error() + ".", "bad"
}
if err := stepupConsumeApproval(dir, approval, "local_baseline"); err != nil {
return "Not recorded: " + err.Error() + ".", "bad"
}
f, _, _, _, err := collectBaseline()
if err != nil {
return "Not recorded: " + err.Error() + ".", "bad"
}
if err := baselineSave(f); err != nil {
return "Not recorded: " + err.Error() + ".", "bad"
}
dcnAudit(dir, "local_baseline", 0, "", "recorded: "+f.TakenAt)
lastDelta.Lock()
lastDelta.r = nil
lastDelta.Unlock()
return "Recorded. What runs here now is the baseline; anything new from here on is reported.", "ok"
}
// cronWords says the common cron schedules in words; anything else stays as
// written, which is what the file says.
func cronWords(spec string) string {
f := strings.Fields(spec)
if len(f) != 5 {
return spec
}
min, hour, dom, mon, dow := f[0], f[1], f[2], f[3], f[4]
num := func(s string) (int, bool) {
n, err := strconv.Atoi(s)
return n, err == nil
}
days := map[string]string{"0": "Sunday", "1": "Monday", "2": "Tuesday", "3": "Wednesday", "4": "Thursday",
"5": "Friday", "6": "Saturday", "7": "Sunday"}
m, mOK := num(min)
h, hOK := num(hour)
switch {
case strings.HasPrefix(min, "*/") && hour == "*" && dom == "*" && mon == "*" && dow == "*":
return "every " + strings.TrimPrefix(min, "*/") + " minutes"
case min == "*" && hour == "*" && dom == "*" && mon == "*" && dow == "*":
return "every minute"
case mOK && hour == "*" && dom == "*" && mon == "*" && dow == "*":
return fmt.Sprintf("every hour at :%02d", m)
case mOK && hOK && dom == "*" && mon == "*" && dow == "*":
return fmt.Sprintf("every day at %02d:%02d", h, m)
case mOK && hOK && dom == "*" && mon == "*" && days[dow] != "":
return fmt.Sprintf("every %s at %02d:%02d", days[dow], h, m)
}
return spec
}