@@ -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
1924const (
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
2332type 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
2939type 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+
4168func 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.
5279func 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+
171250func (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+
208302func 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+
218339func 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+
231359func OpenDefaultServer (dbPath string , scorer risk.Scorer ) (* Server , func () error , error ) {
232360 return OpenDefaultServerWithOptions (dbPath , Options {Scorer : scorer })
233361}
0 commit comments