Skip to content

Commit 4c90dea

Browse files
Michel Osswaldmichiosw
authored andcommitted
feat(guard): redesign dashboard UI
1 parent 520c3b6 commit 4c90dea

62 files changed

Lines changed: 5211 additions & 1005 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"shadcn": {
4+
"command": "sh",
5+
"args": ["-c", "cd web/guard-dashboard && exec npx shadcn@latest mcp"]
6+
}
7+
}
8+
}

internal/guard/app/server/server.go

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,35 @@ import (
55
"encoding/json"
66
"fmt"
77
"io/fs"
8+
"mime"
89
"net/http"
10+
"net/url"
11+
"path/filepath"
912
"strings"
1013
"time"
1114

1215
"github.com/kontext-security/kontext-cli/internal/guard/judge"
16+
"github.com/kontext-security/kontext-cli/internal/guard/policy"
17+
"github.com/kontext-security/kontext-cli/internal/guard/policyconfig"
1318
"github.com/kontext-security/kontext-cli/internal/guard/risk"
1419
"github.com/kontext-security/kontext-cli/internal/guard/store/sqlite"
1520
dashboardassets "github.com/kontext-security/kontext-cli/internal/guard/web/assets"
1621
"github.com/kontext-security/kontext-cli/internal/runtimecore"
1722
)
1823

1924
const (
20-
DefaultAddr = "127.0.0.1:4765"
25+
DefaultAddr = "127.0.0.1:4765"
26+
devDashboardOrigin = "http://127.0.0.1:5173"
27+
jsonContentType = "application/json"
28+
unsupportedContentType = "policy profile requests require application/json"
29+
untrustedProfileOrigin = "untrusted policy profile origin"
2130
)
2231

2332
type Server struct {
24-
store *sqlite.Store
25-
core *runtimecore.Core
26-
mux *http.ServeMux
33+
store *sqlite.Store
34+
policyStore *policyconfig.Store
35+
core *runtimecore.Core
36+
mux *http.ServeMux
2737
}
2838

2939
type ProcessResponse struct {
@@ -38,6 +48,23 @@ type Options struct {
3848
Judge judge.Judge
3949
}
4050

51+
type PolicyProfileResponse struct {
52+
Profile policy.Profile `json:"profile"`
53+
RecommendedProfile policy.Profile `json:"recommended_profile"`
54+
Version string `json:"version"`
55+
RulePack string `json:"rule_pack"`
56+
RulePackVersion string `json:"rule_pack_version"`
57+
ConfigDigest string `json:"config_digest"`
58+
ActivationID string `json:"activation_id"`
59+
Source string `json:"source"`
60+
Status string `json:"status"`
61+
LoadedAt time.Time `json:"loaded_at"`
62+
}
63+
64+
type ActivatePolicyProfileRequest struct {
65+
Profile policy.Profile `json:"profile"`
66+
}
67+
4168
func NewServer(store *sqlite.Store, scorer risk.Scorer) (*Server, error) {
4269
return NewServerWithOptions(store, Options{Scorer: scorer})
4370
}
@@ -50,15 +77,33 @@ func NewServerWithOptions(store *sqlite.Store, opts Options) (*Server, error) {
5077
// A nil interface uses the default local risk policy; callers must not pass a
5178
// typed-nil provider because it still satisfies the PolicyProvider interface.
5279
func NewServerWithPolicy(store *sqlite.Store, policy PolicyProvider) (*Server, error) {
80+
policyStore, err := openPolicyStoreForSQLite(store)
81+
if err != nil {
82+
return nil, err
83+
}
84+
return NewServerWithPolicyConfig(store, policy, policyStore)
85+
}
86+
87+
func NewServerWithPolicyConfig(store *sqlite.Store, policy PolicyProvider, policyStore *policyconfig.Store) (*Server, error) {
5388
if policy == nil {
5489
policy = NewRiskPolicyProvider(nil)
5590
}
91+
if policyStore == nil {
92+
var err error
93+
policyStore, err = openPolicyStoreForSQLite(store)
94+
if err != nil {
95+
return nil, err
96+
}
97+
}
98+
if _, err := policyStore.Load(context.Background()); err != nil {
99+
return nil, fmt.Errorf("load policy config: %w", err)
100+
}
56101
runtime := newGuardHookRuntime(store, policy)
57102
core, err := runtimecore.New(runtime)
58103
if err != nil {
59104
return nil, fmt.Errorf("create runtime core: %w", err)
60105
}
61-
server := &Server{store: store, core: core, mux: http.NewServeMux()}
106+
server := &Server{store: store, policyStore: policyStore, core: core, mux: http.NewServeMux()}
62107
server.routes()
63108
return server, nil
64109
}
@@ -88,6 +133,8 @@ func (s *Server) routes() {
88133
s.mux.HandleFunc("GET /api/summary", s.handleSummary)
89134
s.mux.HandleFunc("GET /api/sessions", s.handleSessions)
90135
s.mux.HandleFunc("GET /api/sessions/", s.handleSession)
136+
s.mux.HandleFunc("GET /api/policy/profile", s.handlePolicyProfile)
137+
s.mux.HandleFunc("POST /api/policy/profile", s.handleActivatePolicyProfile)
91138
s.mux.HandleFunc("GET /", s.handleDashboard)
92139
}
93140

@@ -168,6 +215,38 @@ func (s *Server) handleSessions(w http.ResponseWriter, r *http.Request) {
168215
writeJSON(w, http.StatusOK, sessions)
169216
}
170217

218+
func (s *Server) handlePolicyProfile(w http.ResponseWriter, _ *http.Request) {
219+
writeJSON(w, http.StatusOK, policyProfileResponse(s.policyStore.Current()))
220+
}
221+
222+
func (s *Server) handleActivatePolicyProfile(w http.ResponseWriter, r *http.Request) {
223+
if !trustedPolicyProfileRequest(r) {
224+
writeError(w, http.StatusForbidden, untrustedProfileOrigin)
225+
return
226+
}
227+
if !hasJSONContentType(r) {
228+
writeError(w, http.StatusUnsupportedMediaType, unsupportedContentType)
229+
return
230+
}
231+
var req ActivatePolicyProfileRequest
232+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
233+
writeError(w, http.StatusBadRequest, "invalid policy profile request")
234+
return
235+
}
236+
switch req.Profile {
237+
case policy.ProfileRelaxed, policy.ProfileBalanced, policy.ProfileStrict:
238+
default:
239+
writeError(w, http.StatusBadRequest, "unknown policy profile")
240+
return
241+
}
242+
snapshot, err := s.policyStore.ActivateProfile(r.Context(), req.Profile)
243+
if err != nil {
244+
writeError(w, http.StatusInternalServerError, fmt.Sprintf("activate policy profile: %v", err))
245+
return
246+
}
247+
writeJSON(w, http.StatusOK, policyProfileResponse(snapshot))
248+
}
249+
171250
func (s *Server) handleSession(w http.ResponseWriter, r *http.Request) {
172251
trimmed := strings.TrimPrefix(r.URL.Path, "/api/sessions/")
173252
parts := strings.Split(strings.Trim(trimmed, "/"), "/")
@@ -205,6 +284,21 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
205284
http.FileServer(http.FS(dist)).ServeHTTP(w, r)
206285
}
207286

287+
func policyProfileResponse(snapshot policyconfig.Snapshot) PolicyProfileResponse {
288+
return PolicyProfileResponse{
289+
Profile: snapshot.Config.Profile,
290+
RecommendedProfile: policy.ProfileBalanced,
291+
Version: snapshot.PolicyVersion,
292+
RulePack: snapshot.RulePack,
293+
RulePackVersion: snapshot.RulePackVersion,
294+
ConfigDigest: snapshot.ConfigDigest,
295+
ActivationID: snapshot.ActivationID,
296+
Source: string(snapshot.Source),
297+
Status: string(snapshot.Status),
298+
LoadedAt: snapshot.LoadedAt,
299+
}
300+
}
301+
208302
func writeJSON(w http.ResponseWriter, status int, value any) {
209303
w.Header().Set("Content-Type", "application/json")
210304
w.WriteHeader(status)
@@ -215,6 +309,33 @@ func writeError(w http.ResponseWriter, status int, message string) {
215309
writeJSON(w, status, map[string]string{"error": message})
216310
}
217311

312+
func trustedPolicyProfileRequest(r *http.Request) bool {
313+
origin := r.Header.Get("Origin")
314+
if origin == "" {
315+
return true
316+
}
317+
if origin == devDashboardOrigin {
318+
return true
319+
}
320+
parsed, err := url.Parse(origin)
321+
if err != nil {
322+
return false
323+
}
324+
return parsed.Scheme == "http" && parsed.Host == r.Host
325+
}
326+
327+
func hasJSONContentType(r *http.Request) bool {
328+
contentType := r.Header.Get("Content-Type")
329+
if contentType == "" {
330+
return false
331+
}
332+
mediaType, _, err := mime.ParseMediaType(contentType)
333+
if err != nil {
334+
return false
335+
}
336+
return strings.EqualFold(mediaType, jsonContentType)
337+
}
338+
218339
func withCORS(next http.Handler) http.Handler {
219340
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
220341
w.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1:5173")
@@ -228,6 +349,13 @@ func withCORS(next http.Handler) http.Handler {
228349
})
229350
}
230351

352+
func openPolicyStoreForSQLite(store *sqlite.Store) (*policyconfig.Store, error) {
353+
if store == nil || store.Path() == "" {
354+
return nil, fmt.Errorf("policy config requires sqlite store path")
355+
}
356+
return policyconfig.Open(filepath.Dir(store.Path()))
357+
}
358+
231359
func OpenDefaultServer(dbPath string, scorer risk.Scorer) (*Server, func() error, error) {
232360
return OpenDefaultServerWithOptions(dbPath, Options{Scorer: scorer})
233361
}

0 commit comments

Comments
 (0)