Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,15 @@ func (s *Server) handleRuntimeDecisions(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusInternalServerError, fmt.Sprintf("read runtime decision history: %v", err))
return
}
// trace_path is an absolute host path to the per-decision audit file. It is
// not retrievable via the API and leaks the server's workdir layout, so on
// the public/redacted posture strip it from the listing (consistent with the
// report sanitizer). Operators with shell access can read the file directly.
if redactRuntimeDetailsEnabled() {
for i := range events {
events[i].TracePath = ""
}
}
writeJSON(w, http.StatusOK, map[string]any{"records": events})
}

Expand Down
55 changes: 55 additions & 0 deletions internal/api/server_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,61 @@ func TestReadEndpointsRejectUnauthenticated(t *testing.T) {
}
}

// TestRuntimeDecisionsRedactTracePath confirms the public decisions listing
// strips the absolute host trace_path when redaction is on (demo posture), and
// keeps it when redaction is off. trace_path leaks the server workdir layout and
// is not retrievable via the API.
func TestRuntimeDecisionsRedactTracePath(t *testing.T) {
t.Setenv(envAllowAnonymousRead, "true")
workDir := t.TempDir()
if _, err := runtime.PersistDecisionTrace(workDir, runtime.DecisionTrace{
Source: "api",
Operation: "select",
ArtifactName: "aegis",
Status: "success",
}); err != nil {
t.Fatalf("persist decision trace: %v", err)
}
s := &Server{cfg: Config{WorkDir: workDir}}

decode := func() []runtime.DecisionEvent {
req := httptest.NewRequest(http.MethodGet, "/api/runtime/decisions", http.NoBody)
rec := httptest.NewRecorder()
s.handleRuntimeDecisions(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
var body struct {
Records []runtime.DecisionEvent `json:"records"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("decode decisions: %v", err)
}
if len(body.Records) == 0 {
t.Fatalf("expected at least one decision record")
}
return body.Records
}

t.Setenv(envRedactRuntime, "true")
for _, rec := range decode() {
if rec.TracePath != "" {
t.Fatalf("expected trace_path redacted, got %q", rec.TracePath)
}
}

t.Setenv(envRedactRuntime, "false")
sawPath := false
for _, rec := range decode() {
if rec.TracePath != "" {
sawPath = true
}
}
if !sawPath {
t.Fatalf("expected trace_path present when redaction is disabled")
}
}

// TestReadEndpointsAllowAuthenticated confirms that supplying the configured
// API key restores access to the read surface after H-1b's auth gate.
func TestReadEndpointsAllowAuthenticated(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/runtime/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ type DecisionEvent struct {
RequestedVersion string `json:"requested_version,omitempty"`
SelectedVersion string `json:"selected_version,omitempty"`
ExecutionStatus string `json:"execution_status,omitempty"`
TracePath string `json:"trace_path"`
TracePath string `json:"trace_path,omitempty"`
}

type DecisionPersistResult struct {
Expand Down
Loading