-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathmain.go
More file actions
1528 lines (1500 loc) · 73 KB
/
Copy pathmain.go
File metadata and controls
1528 lines (1500 loc) · 73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package main
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/jmpsec/osctrl/cmd/api/handlers"
"github.com/jmpsec/osctrl/pkg/activity"
"github.com/jmpsec/osctrl/pkg/alerts"
"github.com/jmpsec/osctrl/pkg/auditlog"
"github.com/jmpsec/osctrl/pkg/authproviders"
"github.com/jmpsec/osctrl/pkg/backend"
"github.com/jmpsec/osctrl/pkg/cache"
"github.com/jmpsec/osctrl/pkg/carves"
"github.com/jmpsec/osctrl/pkg/config"
"github.com/jmpsec/osctrl/pkg/console"
"github.com/jmpsec/osctrl/pkg/environments"
"github.com/jmpsec/osctrl/pkg/fileexplorer"
"github.com/jmpsec/osctrl/pkg/geoip"
"github.com/jmpsec/osctrl/pkg/health"
"github.com/jmpsec/osctrl/pkg/logging"
"github.com/jmpsec/osctrl/pkg/logsinks"
"github.com/jmpsec/osctrl/pkg/mfa"
"github.com/jmpsec/osctrl/pkg/nodes"
"github.com/jmpsec/osctrl/pkg/osquery"
"github.com/jmpsec/osctrl/pkg/posture"
"github.com/jmpsec/osctrl/pkg/queries"
"github.com/jmpsec/osctrl/pkg/ratelimit"
"github.com/jmpsec/osctrl/pkg/servicecommands"
"github.com/jmpsec/osctrl/pkg/serviceconfig"
"github.com/jmpsec/osctrl/pkg/settings"
"github.com/jmpsec/osctrl/pkg/tags"
"github.com/jmpsec/osctrl/pkg/types"
"github.com/jmpsec/osctrl/pkg/users"
"github.com/jmpsec/osctrl/pkg/utils"
"github.com/jmpsec/osctrl/pkg/version"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"github.com/spf13/viper"
)
const (
// Project name
projectName = "osctrl"
// Service name
serviceName = projectName + "-" + config.ServiceAPI
// Service description
serviceDescription = "API service for osctrl"
// Application description
appDescription = serviceDescription + ", a fast and efficient osquery management"
// Default refreshing interval in seconds
defaultRefresh int = 300
// restartDrainTimeout bounds how long a restart waits for in-flight
// requests — including the apply response that triggered it — before
// the process exits.
restartDrainTimeout = 5 * time.Second
)
// Build-time metadata (overridden via -ldflags "-X main.buildVersion=... -X main.buildCommit=... -X main.buildDate=...")
var (
buildVersion = version.OsctrlVersion
buildCommit = "unknown"
buildDate = "unknown"
)
// Paths
const (
// HTTP health path
healthPath = "/health"
// HTTP errors path
errorPath = "/error"
forbiddenPath = "/forbidden"
// API checks path
checksNoAuthPath = "/checks-no-auth"
checksAuthPath = "/checks-auth"
// API prefix path
apiPrefixPath = "/api"
// API version path
apiVersionPath = "/v1"
// API login path
apiLoginPath = "/login"
apiMFAPath = "/mfa"
// API nodes path
apiNodesPath = "/nodes"
// API queries path
apiQueriesPath = "/queries"
// API saved queries path
apiSavedQueriesPath = "/saved-queries"
// API users path
apiUsersPath = "/users"
// API all queries path
apiAllQueriesPath = "/all-queries"
// API carves path
apiCarvesPath = "/carves"
// API platforms path
apiPlatformsPath = "/platforms"
// API environments path
apiEnvironmentsPath = "/environments"
// API tags path
apiTagsPath = "/tags"
// API settings path
apiSettingsPath = "/settings"
// API service config path
apiServiceConfigPath = "/service-config"
// API log sinks path
apiLogSinksPath = "/log-sinks"
apiAlertsPath = "/alerts"
// API health / system status path
apiHealthPath = "/health"
apiAuthProvidersPath = "/auth-providers"
// API features path
apiFeaturesPath = "/features"
// API file explorer path
apiFileExplorerPath = "/file-explorer"
// API audit logs path
apiAuditLogsPath = "/audit-logs"
apiMCPPath = "/mcp"
// API logs path
apiLogsPath = "/logs"
// API stats path
apiStatsPath = "/stats"
// API osquery path
apiOsqueryPath = "/osquery"
// API auth methods / federated login path. Global (no
// {env}) because OIDC on osctrl-api is a single, deployment-
// wide identity surface; env scoping happens at the user-
// permissions layer, not the auth layer.
apiAuthPath = "/auth"
)
// Global variables
var (
err error
db *backend.DBManager
redis *cache.RedisManager
apiUsers *users.UserManager
tagsmgr *tags.TagManager
settingsmgr *settings.Settings
envs *environments.EnvManager
nodesmgr *nodes.NodeManager
queriesmgr *queries.Queries
consolemgr *console.Manager
fileexplorermgr *fileexplorer.Manager
filecarves *carves.Carves
handlersApi *handlers.HandlersApi
app *cli.Command
flags []cli.Flag
serviceConfiguration config.APIConfiguration
// FIXME this struct is temporary until we refactor to write settings to the DB
flagParams *config.ServiceParameters
auditLog *auditlog.AuditLogManager
osqueryTables []types.OsqueryTable
)
// Valid values for auth and logging in configuration
var validAuth = map[string]bool{
config.AuthNone: true,
config.AuthJWT: true,
}
// Function to load the configuration from a single YAML file
func loadYAMLConfiguration(file string) (config.APIConfiguration, error) {
var cfg config.APIConfiguration
v := viper.New()
// saml.forceAuthn defaults to true — same as the --saml-force-authn
// flag. Without this the YAML path would silently land on false for
// any config file that omits the key, which most operators read as
// "logout didn't work" (the IdP silently re-auths from its own SSO
// cookie). A zero-value bool can't distinguish "absent" from
// "explicitly false", so the default has to live here.
v.SetDefault("saml.forceAuthn", true)
// Load file and read config
v.SetConfigFile(file)
v.SetConfigType(config.YAMLConfigType)
if err := v.ReadInConfig(); err != nil {
return cfg, err
}
// Unmarshal into struct
if err := v.Unmarshal(&cfg); err != nil {
return cfg, err
}
// Warn when the file predates (or postdates) the schema this binary
// understands — usually new fields were added since the file was
// written and are silently missing from it.
if msg := config.ConfigVersionWarning(cfg.Version); msg != "" {
log.Warn().Msg(msg)
}
// Check if values are valid
if !validAuth[cfg.Service.Auth] {
return cfg, fmt.Errorf("invalid auth method: '%s'", cfg.Service.Auth)
}
if cfg.RateLimits != nil {
if err := config.ValidateRateLimits(*cfg.RateLimits, "login", "preAuth", "serviceConfigApply"); err != nil {
return cfg, err
}
}
// No errors!
return cfg, nil
}
// Initialization code
func init() {
// Initialize default flagParams
flagParams = &config.ServiceParameters{
Service: &config.YAMLConfigurationService{},
DB: &config.YAMLConfigurationDB{},
Redis: &config.YAMLConfigurationRedis{},
JWT: &config.YAMLConfigurationJWT{},
OIDC: &config.YAMLConfigurationOIDC{},
SAML: &config.YAMLConfigurationSAML{},
MCP: &config.YAMLConfigurationMCP{},
TLS: &config.YAMLConfigurationTLS{},
Osquery: &config.YAMLConfigurationOsquery{},
Logger: &config.YAMLConfigurationLogger{
DB: &config.YAMLConfigurationDB{},
S3: &config.S3Logger{},
Graylog: &config.GraylogLogger{},
Elastic: &config.ElasticLogger{},
Splunk: &config.SplunkLogger{},
Logstash: &config.LogstashLogger{},
Kinesis: &config.KinesisLogger{},
Kafka: &config.KafkaLogger{},
Local: &config.LocalLogger{},
},
Carver: &config.YAMLConfigurationCarver{
S3: &config.S3Carver{},
Local: &config.LocalCarver{},
},
Debug: &config.YAMLConfigurationDebug{},
RateLimits: config.DefaultRateLimitsPtr(),
}
// Initialize CLI flags using the config package
flags = config.InitAPIFlags(flagParams)
}
// Retrieve latest release information and compare
func checkLatestRelease() {
log.Info().Msg("Checking for the latest release...")
latest, err := version.RetrieveVersionData(version.VersionDataURL)
if err != nil {
log.Err(err).Msg("Error retrieving latest release information")
return
}
if version.CheckSuggestedRelease(latest.SuggestedRelease) {
log.Info().Msgf("%s (%s) is up to date with the suggested release (%s), latest %s", serviceName, buildVersion, latest.SuggestedRelease, latest.LatestRelease)
} else {
log.Info().Msgf("Please upgrade %s to at least %s. Latest version available: %s (current: %s)", serviceName, latest.SuggestedRelease, latest.LatestRelease, buildVersion)
log.Info().Msgf("Release notes: %s", latest.MoreInformation)
}
}
// guardAuthMode refuses to start the API with --auth=none unless the operator
// explicitly opts in via OSCTRL_INSECURE_NO_AUTH=1. When the opt-in is set,
// every 60s a loud warning is logged so the deployment cannot drift into
// "auth-off forever" without anyone noticing.
//
// The warning goroutine watches the supplied context so a future graceful
// shutdown path can cancel it cleanly. Today the API has no shutdown signal
// handling so the context never fires — that's acceptable; we get the
// no-leak property for free when shutdown is added.
func guardAuthMode(ctx context.Context, auth string) {
if auth != config.AuthNone {
return
}
if os.Getenv("OSCTRL_INSECURE_NO_AUTH") != "1" {
log.Fatal().Msg("auth=none is disabled by default. Set OSCTRL_INSECURE_NO_AUTH=1 to opt in for local development only — every request will be served as super-admin")
}
go func() {
log.Warn().Msg("INSECURE: osctrl-api running with auth=none — every request is served as super-admin. DO NOT use in production")
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
log.Warn().Msg("INSECURE: osctrl-api running with auth=none — every request is served as super-admin. DO NOT use in production")
}
}
}()
}
// Go go!
func osctrlAPIService() {
// Refuse to run unauthenticated unless the operator explicitly opts in.
guardAuthMode(context.Background(), flagParams.Service.Auth)
// Configure forwarding-header trust. Empty (default) means utils.GetIP
// ignores X-Forwarded-For / X-Real-IP and always uses RemoteAddr, so
// an internet attacker can't spoof IPs to defeat rate-limits or
// poison the audit log.
if tp := strings.TrimSpace(flagParams.Service.TrustedProxies); tp != "" {
utils.SetTrustedProxies(strings.Split(tp, ","))
log.Info().Msgf("Trusting forwarding headers from: %s", tp)
}
// ////////////////////////////// Backend
log.Info().Msg("Initializing backend...")
for {
db, err = backend.CreateDBManager(flagParams.DB)
if db != nil {
log.Info().Msg("Connection to backend successful!")
break
}
if err != nil {
log.Err(err).Msg("Failed to connect to backend")
if flagParams.DB.ConnRetry == 0 {
log.Fatal().Msg("Connection to backend failed and no retry was set")
}
}
log.Info().Msgf("Backend NOT ready! Retrying in %d seconds...\n", flagParams.DB.ConnRetry)
time.Sleep(time.Duration(flagParams.DB.ConnRetry) * time.Second)
}
// ////////////////////////////// Cache
log.Info().Msg("Initializing cache...")
for {
redis, err = cache.CreateRedisManager(*flagParams.Redis)
if redis != nil {
log.Info().Msg("Connection to cache successful!")
break
}
if err != nil {
log.Err(err).Msg("Failed to connect to cache")
if flagParams.Redis.ConnRetry == 0 {
log.Fatal().Msg("Connection to cache failed and no retry was set")
}
}
log.Info().Msgf("Cache NOT ready! Retrying in %d seconds...\n", flagParams.Redis.ConnRetry)
time.Sleep(time.Duration(flagParams.Redis.ConnRetry) * time.Second)
}
log.Info().Msg("Initialize users")
apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT)
log.Info().Msg("Initialize tags")
tagsmgr = tags.CreateTagManager(db.Conn)
geoIPResolver, err := geoip.New(flagParams.Service.GeoIPDBPath)
if err != nil {
log.Warn().Err(err).Msg("GeoIP resolver not loaded, country codes will be empty")
geoIPResolver = nil
}
log.Info().Msg("Initialize environment")
envs = environments.CreateEnvironment(db.Conn)
envCache := environments.NewRedisEnvCache(*envs, redis.Client)
log.Info().Msg("Environment cache wired to Redis")
// DB health monitor: when enabled, pings the DB on a fixed
// interval and switches EnvCache into stale-serve mode after N
// consecutive failures, so the API keeps responding to read-only
// env lookups (used by handlers that hit EnvCache) during a DB
// outage. Writes still require the DB and will fail. See
// pkg/backend/health.go.
var dbHealth *backend.DBHealth
if flagParams.Service.DBHealthCheck {
interval := time.Duration(flagParams.Service.DBHealthInterval) * time.Second
threshold := uint32(flagParams.Service.DBHealthThreshold)
dbHealth = backend.NewDBHealth(db, interval, threshold)
dbHealth.Start()
envCache.SetDBHealth(dbHealth)
log.Info().
Dur("interval", interval).
Uint32("threshold", threshold).
Msg("DB health monitor enabled — EnvCache will stale-serve on DB outage")
}
// Security & compliance posture system (disabled by default)
var posturemgr *posture.PostureManager
if flagParams.Service.PostureEnabled {
posturemgr = posture.NewPostureManager(db.Conn)
log.Info().Msg("Posture system enabled")
} else {
log.Info().Msg("Posture system disabled (enable with --posture-enabled)")
}
// Alerting subsystem (disabled by default). When disabled the
// alert tables are not created and the alert API routes are not
// registered.
var alertsMgr *alerts.Manager
if flagParams.Service.AlertsEnabled {
alertsMgr = alerts.NewManager(db.Conn)
log.Info().Msg("Alerting system enabled")
} else {
log.Info().Msg("Alerting system disabled (enable with --alerts-enabled)")
}
// Health / system status subsystem (disabled by default). When
// disabled the service_status table is not created and the health
// API routes are not registered.
var healthMgr *health.Manager
var healthVersions *health.VersionCache
if flagParams.Service.HealthEnabled {
healthMgr = health.NewManager(db.Conn)
healthVersions = health.NewVersionCache(buildVersion)
// Seed from the boot check, then refresh daily. This is an external
// HTTP call, so it must never happen on the request path.
go func() {
if err := healthVersions.Refresh(version.VersionDataURL); err != nil {
log.Err(err).Msg("error retrieving version data for health")
}
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
if err := healthVersions.Refresh(version.VersionDataURL); err != nil {
log.Err(err).Msg("error refreshing version data for health")
}
}
}()
log.Info().Msg("Health system enabled")
} else {
log.Info().Msg("Health system disabled (enable with --health-enabled)")
}
// Initialize settings
// Multi-factor authentication for password logins. The manager owns its
// tables and is always available; WebAuthn additionally needs a relying
// party id and origin, and stays nil (TOTP-only) when neither the
// config nor the service host provides one.
mfamgr := mfa.NewManager(db.Conn)
var webAuthn *mfa.WebAuthn
rpID := flagParams.Service.MFARPID
if rpID == "" {
rpID = flagParams.Service.Host
}
origins := splitAndTrim(flagParams.Service.MFAOrigins)
if len(origins) == 0 && rpID != "" {
origins = []string{"https://" + rpID}
}
if rpID != "" && len(origins) > 0 {
webAuthn, err = mfa.NewWebAuthn(mfamgr, rpID, mfaIssuerName(flagParams), origins)
if err != nil {
log.Err(err).Msg("WebAuthn disabled — passkeys and security keys will not be offered")
webAuthn = nil
} else {
log.Info().Str("rpid", rpID).Strs("origins", origins).Msg("WebAuthn enabled")
}
} else {
log.Warn().Msg("WebAuthn disabled — set --host or --mfa-rpid to enable passkeys and security keys")
}
if flagParams.Service.MFARequired {
log.Info().Msg("Multi-factor authentication is required for password logins")
}
log.Info().Msg("Initialize settings")
settingsmgr = settings.NewSettings(db.Conn)
log.Info().Msg("Initialize nodes")
nodesmgr = nodes.CreateNodes(db.Conn)
log.Info().Msg("Initialize queries")
queriesmgr = queries.CreateQueries(db.Conn)
queriesmgr.Cache = queries.NewQueryDispatchCache(redis.Client, 0)
log.Info().Msg("Initialize console")
consolemgr = console.NewManager(db.Conn, queriesmgr)
sessionHints := cache.NewSessionHints(redis.Client)
consolemgr.SessionHints = sessionHints
log.Info().Msg("Initialize file explorer")
fileexplorermgr = fileexplorer.NewManager(db.Conn, queriesmgr)
fileexplorermgr.SessionHints = sessionHints
// Construct the log reader. When the TLS logger ships logs to S3 the
// osquery_*_data tables are empty, so the API/console/file-explorer
// read logs back from S3 instead. For every other logger the data
// lives in the DB and the legacy GORM-backed reader is used.
var logReader logging.LogReader
if flagParams.Logger != nil && flagParams.Logger.Type == config.LoggingS3 && flagParams.Logger.S3 != nil {
log.Info().Msg("Logger is S3 — initializing S3-backed log reader")
s3Logger, err := logging.CreateLoggerS3(flagParams.Logger.S3)
if err != nil {
log.Fatal().Err(err).Msg("Error initializing S3 log reader")
}
logReader = logging.NewS3LogReader(s3Logger.Client, s3Logger.S3Config.Bucket)
} else {
logReader = logging.NewDBLogReader(db.Conn)
}
consolemgr.SetLogReader(logReader)
fileexplorermgr.SetLogReader(logReader)
log.Info().Msg("Initialize carves")
filecarves = carves.CreateFileCarves(db.Conn, flagParams.Carver.Type, nil)
log.Info().Msg("Loading service settings")
if err := loadingSettings(settingsmgr, flagParams); err != nil {
log.Fatal().Msgf("Error loading settings - %v", err)
}
log.Info().Msg("Seeding service config from YAML")
serviceConfigMgr := serviceconfig.NewServiceConfigManager(db.Conn)
serviceCommandMgr := servicecommands.NewManager(db.Conn)
// Log sinks manager shares the service-config feature gate: when
// serviceConfigEnabled is false the routes are not registered and
// the SPA hides the section. The manager is still constructed so
// the table is migrated; rows can be edited directly in the DB or
// YAML and picked up on the next osctrl-tls boot/reload.
logSinksMgr := logsinks.NewLogSinksManager(db.Conn)
// Auth providers manager — shares the service-config feature gate.
authProvidersMgr := authproviders.NewAuthProviderManager(db.Conn)
if err := authProvidersMgr.Seed(flagParams); err != nil {
log.Fatal().Err(err).Msg("Error seeding auth providers")
}
// Build live providers from the DB. Fail-fast if an enabled
// provider's IdP is unreachable — same posture as the old
// InitOIDC/InitSAML calls.
var authProviderRegistry *handlers.AuthProviderRegistry
providerEntries, err := authProvidersMgr.BuildProviders(context.Background())
if err != nil {
log.Fatal().Err(err).Msg("Error building auth providers from DB")
}
authProviderRegistry = handlers.NewAuthProviderRegistry(providerEntries)
if err := serviceConfigMgr.Seed(config.ServiceAPI, flagParams, settings.NoEnvironmentID); err != nil {
log.Fatal().Msgf("Error seeding service config - %v", err)
}
log.Info().Msg("Resolving service config from DB")
if err := serviceConfigMgr.Resolve(config.ServiceAPI, flagParams, settings.NoEnvironmentID); err != nil {
log.Fatal().Msgf("Error resolving service config - %v", err)
}
// Report whether this process can write its own config file. Only this
// process can know — osctrl-tls runs elsewhere and reports its own.
if err := serviceConfigMgr.ReportFile(config.ServiceAPI, flagParams.ConfigFilePath()); err != nil {
log.Err(err).Msg("Error reporting service config file status")
}
// Resolve the per-feature enabled flags. They default to true
// when nil (backwards compat: service-config-enabled controls
// the master gate, and each sub-feature can be independently
// disabled by setting its flag to false).
logSinksEnabled := true
if flagParams.Service.LogSinksEnabled != nil {
logSinksEnabled = *flagParams.Service.LogSinksEnabled
}
authProvidersEnabled := true
if flagParams.Service.AuthProvidersEnabled != nil {
authProvidersEnabled = *flagParams.Service.AuthProvidersEnabled
}
if !flagParams.Service.ServiceConfigEnabled {
log.Info().Msg("Service config API is disabled (enable with --service-config-enabled) — sections are still seeded and resolved, change the service_config rows or the YAML file directly")
}
if !logSinksEnabled {
log.Info().Msg("Log sinks API is disabled (enable with --log-sinks-enabled) — rows can still be changed directly in the database")
}
if !authProvidersEnabled {
log.Info().Msg("Auth providers API is disabled (enable with --auth-providers-enabled) — rows can still be changed directly in the database")
}
if flagParams.RateLimits == nil {
flagParams.RateLimits = config.DefaultRateLimitsPtr()
}
if err := config.ValidateRateLimits(*flagParams.RateLimits, "login", "preAuth", "serviceConfigApply"); err != nil {
log.Fatal().Msgf("Invalid rate limit configuration - %v", err)
}
// Initialize audit log manager
if flagParams.Service.AuditLog {
log.Info().Msg("Initialize audit log")
}
auditLog, err = auditlog.CreateAuditLogManager(db.Conn, serviceName, flagParams.Service.AuditLog)
if err != nil {
log.Fatal().Msgf("Error initializing audit log manager - %v", err)
}
// Load osquery tables schema (best-effort; an empty slice is fine if the file doesn't exist)
if flagParams.Osquery.TablesFile != "" {
log.Info().Msgf("Loading osquery tables from %s", flagParams.Osquery.TablesFile)
osqueryTables, err = osquery.LoadTables(flagParams.Osquery.TablesFile)
if err != nil {
log.Warn().Msgf("Failed to load osquery tables: %v", err)
osqueryTables = []types.OsqueryTable{}
}
}
// Initialize Admin handlers before router
log.Info().Msg("Initializing handlers")
// Initialize the global OIDC provider BEFORE constructing the
// handlers struct. We need to fail fast if --oidc-enabled is set
// but discovery fails — running an SPA login page that links to a
// broken IdP route is worse than refusing to start.
if flagParams.OIDC != nil && flagParams.OIDC.Enabled {
log.Info().Msg("OIDC enabled — discovering provider")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := handlers.InitOIDC(ctx, *flagParams.OIDC); err != nil {
cancel()
log.Fatal().Err(err).Msg("Can not initialize OIDC")
}
cancel()
}
// Same fail-fast posture for SAML — refuse to start if metadata
// fetch fails so we never serve an SPA login button that links
// to a broken /auth/saml/login route.
if flagParams.SAML != nil && flagParams.SAML.Enabled {
log.Info().Msg("SAML enabled — fetching IdP metadata")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := handlers.InitSAML(ctx, *flagParams.SAML, flagParams.SAML.EntityID, flagParams.SAML.ACSURL); err != nil {
cancel()
log.Fatal().Err(err).Msg("Can not initialize SAML")
}
cancel()
}
// Restart channel for the service-config apply endpoint. When the
// operator clicks "Apply & Restart", the handler signals this channel
// and the main goroutine shuts down gracefully so the process manager
// restarts the service with the DB-edited config.
restartCh := make(chan struct{}, 1)
// Persisting this service's config file happens in this process — the
// YAML loader lives here, and no other service can reach the file.
persistConfig := func() error {
path := flagParams.ConfigFilePath()
loaded, err := loadYAMLConfiguration(path)
if err != nil {
return fmt.Errorf("reload %s: %w", path, err)
}
return serviceConfigMgr.PersistToFile(config.ServiceAPI, path, loadedYAMLToServiceParams(loaded, path), settings.NoEnvironmentID)
}
handlersApi = handlers.CreateHandlersApi(
handlers.WithDB(db.Conn),
handlers.WithLogReader(logReader),
handlers.WithEnvs(envs),
handlers.WithEnvCache(envCache),
handlers.WithUsers(apiUsers),
handlers.WithTags(tagsmgr),
handlers.WithNodes(nodesmgr),
handlers.WithQueries(queriesmgr),
handlers.WithConsole(consolemgr),
handlers.WithFileExplorer(fileexplorermgr),
handlers.WithCarves(filecarves),
handlers.WithSettings(settingsmgr),
handlers.WithServiceConfig(serviceConfigMgr),
handlers.WithLogSinks(logSinksMgr),
handlers.WithAlerts(alertsMgr),
handlers.WithHealth(healthMgr),
handlers.WithHealthVersions(healthVersions),
handlers.WithStartedAt(time.Now()),
handlers.WithRedis(redis),
handlers.WithAuthProviders(authProviderRegistry, authProvidersMgr),
handlers.WithServiceConfigEnabled(flagParams.Service.ServiceConfigEnabled),
handlers.WithLogSinksEnabled(logSinksEnabled),
handlers.WithAuthProvidersEnabled(authProvidersEnabled),
handlers.WithServiceCommands(serviceCommandMgr),
handlers.WithConfigPersist(persistConfig),
handlers.WithActivityReader(activity.NewRedisStore(redis.Client, activity.DefaultPrefix, activity.DefaultRetentionDays, 8*24*time.Hour)),
handlers.WithGeoIP(geoIPResolver),
handlers.WithPosture(posturemgr),
handlers.WithPostureEnabled(flagParams.Service.PostureEnabled),
handlers.WithMFA(mfamgr, webAuthn, flagParams.Service.MFARequired, mfaIssuerName(flagParams)),
handlers.WithVersion(buildVersion),
handlers.WithName(serviceName),
handlers.WithAuditLog(auditLog),
handlers.WithDebugHTTP(flagParams.Debug),
handlers.WithOsqueryTables(osqueryTables),
handlers.WithOsqueryValues(*flagParams.Osquery),
handlers.WithJWTSecret([]byte(flagParams.JWT.JWTSecret)),
handlers.WithOIDC(flagParams.OIDC != nil && flagParams.OIDC.Enabled),
handlers.WithSAML(flagParams.SAML != nil && flagParams.SAML.Enabled),
handlers.WithDBHealth(dbHealth), // nil when DB health monitor disabled
handlers.WithRestartCh(restartCh),
)
// ///////////////////////// API
log.Info().Msg("Initializing router")
// Create router for API endpoint
muxAPI := http.NewServeMux()
// API: root
muxAPI.HandleFunc("GET /", handlersApi.RootHandler)
// API: testing
muxAPI.HandleFunc("GET "+healthPath, handlersApi.HealthHandler)
// API: error
muxAPI.HandleFunc("GET "+errorPath, handlersApi.ErrorHandler)
// API: forbidden
muxAPI.HandleFunc("GET "+forbiddenPath, handlersApi.ForbiddenHandler)
// API: check status
muxAPI.HandleFunc("GET "+_apiPath(checksNoAuthPath), handlersApi.CheckHandlerNoAuth)
// ///////////////////////// UNAUTHENTICATED
// Login is the only password-acceptance surface on the API. By default
// it is capped to 10 attempts per IP per minute and 429s the rest.
// Rejections are audit-logged inside the LoginHandler / RateLimit
// middleware so SoC tooling sees the spray.
//
loginLimiter := ratelimit.NewFromConfig(flagParams.RateLimits.Login)
loginRateLimit := loginLimiter.HTTPMiddleware(ratelimit.KeyByIP, func(r *http.Request, key string) {
handlersApi.AuditLog.FailedLogin("", utils.GetIP(r), "rate limit exceeded")
})
muxAPI.Handle("POST "+_apiPath(apiLoginPath)+"/{env}", loginRateLimit(http.HandlerFunc(handlersApi.LoginHandler)))
muxAPI.Handle("POST "+_apiPath(apiLoginPath), loginRateLimit(http.HandlerFunc(handlersApi.LoginHandler)))
// Second factor. Same rate limiter as the password step: an attacker
// who has the password must not get unlimited guesses at a 6-digit code.
muxAPI.Handle("POST "+_apiPath(apiLoginPath)+"/mfa", loginRateLimit(http.HandlerFunc(handlersApi.LoginMFAHandler)))
muxAPI.Handle("POST "+_apiPath(apiLoginPath)+"/mfa/webauthn/begin", loginRateLimit(http.HandlerFunc(handlersApi.LoginMFAWebAuthnBeginHandler)))
muxAPI.Handle("POST "+_apiPath(apiLoginPath)+"/mfa/webauthn/finish", loginRateLimit(http.HandlerFunc(handlersApi.LoginMFAWebAuthnFinishHandler)))
muxAPI.Handle("POST "+_apiPath(apiLoginPath)+"/mfa/enroll/begin", loginRateLimit(http.HandlerFunc(handlersApi.LoginMFAEnrollBeginHandler)))
muxAPI.Handle("POST "+_apiPath(apiLoginPath)+"/mfa/enroll/finish", loginRateLimit(http.HandlerFunc(handlersApi.LoginMFAEnrollFinishHandler)))
// Read-only pre-auth endpoints (env list for the login picker).
// The env list is the one piece of data the login page legitimately
// needs before the user has a session, so it stays pre-auth —
// rate-limited at 60/min/IP by default to block low-effort scanning probes.
// React strict-mode / browser reloads easily exceed 10/min during
// normal use, so the preAuth budget is more permissive than the
// credential-spray budget on /login.
preAuthLimiter := ratelimit.NewFromConfig(flagParams.RateLimits.PreAuth)
preAuthRateLimit := preAuthLimiter.HTTPMiddleware(ratelimit.KeyByIP, nil)
muxAPI.Handle("GET "+_apiPath(apiLoginPath)+"/environments", preAuthRateLimit(http.HandlerFunc(handlersApi.LoginEnvironmentsHandler)))
// Auth-methods discovery: the SPA polls this to decide whether to
// render an "OIDC" button alongside the password form. Read-only,
// no secrets in the response, pre-auth rate limit.
muxAPI.Handle("GET "+_apiPath(apiAuthPath)+"/methods", preAuthRateLimit(http.HandlerFunc(handlersApi.AuthMethodsHandler)))
// Logout: unauthenticated by design — an expired-token user
// must be able to log out without first re-authenticating.
// Server-side cookie expiry + APIToken revocation happen here.
// Worst case if forged: invalidates the legitimate user's
// token, which is exactly what logout should do.
muxAPI.Handle("POST "+_apiPath("/logout"), preAuthRateLimit(http.HandlerFunc(handlersApi.LogoutHandler)))
// OIDC login + callback. Registered ONLY when --oidc-enabled is
// set, so a misconfigured deploy doesn't expose 500-returning
// stubs. The handlers themselves also guard with `oidcProvider
// == nil` for defense in depth, but route-level gating keeps the
// public route table accurate.
//
// The login endpoint shares the strict login rate limit (10/min/IP)
// with the password endpoint — both are credential surfaces from
// the attacker's POV. Callback uses the looser pre-auth limit
// because it carries IdP-signed material and is harder to spam
// usefully.
if flagParams.OIDC != nil && flagParams.OIDC.Enabled {
muxAPI.Handle("GET "+_apiPath(apiAuthPath)+"/oidc/login", loginRateLimit(http.HandlerFunc(handlersApi.OIDCLoginHandler)))
muxAPI.Handle("GET "+_apiPath(apiAuthPath)+"/oidc/callback", preAuthRateLimit(http.HandlerFunc(handlersApi.OIDCCallbackHandler)))
}
// SAML routes follow the OIDC posture: gated by --saml-enabled, login
// on the strict limiter, ACS on the looser limiter (the SAMLResponse
// is IdP-signed so spamming the endpoint without a real assertion
// gets rejected at the crypto layer anyway), metadata pre-auth and
// public by design (SP metadata is meant to be machine-readable for
// IdP-side registration).
if flagParams.SAML != nil && flagParams.SAML.Enabled {
muxAPI.Handle("GET "+_apiPath(apiAuthPath)+"/saml/login", loginRateLimit(http.HandlerFunc(handlersApi.SAMLLoginHandler)))
muxAPI.Handle("POST "+_apiPath(apiAuthPath)+"/saml/acs", preAuthRateLimit(http.HandlerFunc(handlersApi.SAMLACSHandler)))
muxAPI.Handle("GET "+_apiPath(apiAuthPath)+"/saml/metadata", preAuthRateLimit(http.HandlerFunc(handlersApi.SAMLMetadataHandler)))
}
// ///////////////////////// AUTHENTICATED
// API: check auth
muxAPI.Handle(
"GET "+_apiPath(checksAuthPath), handlerAuthCheck(http.HandlerFunc(handlersApi.CheckHandlerAuth), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: deployment feature switches
muxAPI.Handle(
"GET "+_apiPath(apiFeaturesPath),
handlerAuthCheck(http.HandlerFunc(handlersApi.FeaturesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: nodes by environment
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}/all",
handlerAuthCheck(http.HandlerFunc(handlersApi.AllNodesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}/active",
handlerAuthCheck(http.HandlerFunc(handlersApi.ActiveNodesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}/inactive",
handlerAuthCheck(http.HandlerFunc(handlersApi.InactiveNodesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}/node/{node}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodeHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiNodesPath)+"/{env}/delete",
handlerAuthCheck(http.HandlerFunc(handlersApi.DeleteNodeHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiNodesPath)+"/{env}/tag",
handlerAuthCheck(http.HandlerFunc(handlersApi.TagNodeHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiNodesPath)+"/lookup",
handlerAuthCheck(http.HandlerFunc(handlersApi.LookupNodeHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: paginated nodes — canonical SPA endpoint
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodesPagedHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
if flagParams.Service.PostureEnabled {
// API: node posture (security & compliance data)
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}/node/{uuid}/posture",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodePostureHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiNodesPath)+"/{env}/node/{uuid}/posture/score",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodePostureScoreHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: posture profiles (predefined check templates)
muxAPI.Handle(
"GET "+_apiPath("/posture")+"/profiles",
handlerAuthCheck(http.HandlerFunc(handlersApi.PostureProfilesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath("/posture")+"/profiles/{id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.PostureProfileHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
}
// API: node logs
muxAPI.Handle(
"GET "+_apiPath(apiLogsPath)+"/{type}/{env}/{uuid}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodeLogsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: cross-env dashboard stats
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath),
handlerAuthCheck(http.HandlerFunc(handlersApi.StatsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: fleet-wide osquery version breakdown for dashboard's hygiene panel.
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/osquery-versions",
handlerAuthCheck(http.HandlerFunc(handlersApi.OsqueryVersionsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: per-env activity heatmap (15-min audit-log buckets across N hours).
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.EnvActivityHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: per-node activity heatmap (status/result/query/carve buckets).
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/node/{env}/{uuid}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodeActivityHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// Batch variant — accepts ?uuids=a,b,c (up to 100). Returns a map keyed by
// uuid. Lets the Nodes table render a per-row sparkline without firing N
// parallel HTTP requests.
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/node-batch/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodeActivityBatchHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: Redis-backed per-node activity series (config + read/write split).
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/node-tiles/{env}/{uuid}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodeActivityTilesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: Redis-backed per-node activity tiles batch (Nodes table heatmap).
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/node-tiles-batch/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.NodeActivityTilesBatchHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: Redis-backed per-env activity series.
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/env-tiles/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.EnvActivityTilesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: nodes reporting the most osquery ERROR status logs (dashboard
// reported-errors drill-down).
muxAPI.Handle(
"GET "+_apiPath(apiStatsPath)+"/activity/error-nodes/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.EnvErrorNodesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: queries by environment
if flagParams.Osquery.Query {
// Sample-templates library (post-auth). Pre-auth exposure
// of the SQL-template starter pack uniquely fingerprints
// the deployment as osctrl and reveals operator-internal
// data to anonymous callers; both are useless to the
// SPA's only legitimate consumer (the post-login
// queries/new form) at pre-auth time.
muxAPI.Handle(
"GET "+_apiPath(apiQueriesPath)+"/samples",
handlerAuthCheck(http.HandlerFunc(handlersApi.QuerySamplesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiQueriesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.AllQueriesShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiQueriesPath)+"/{env}/list/{target}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueryListHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiQueriesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueriesRunHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiQueriesPath)+"/{env}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueryShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiQueriesPath)+"/{env}/results/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueryResultsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// CSV export for query results
muxAPI.Handle(
"GET "+_apiPath(apiQueriesPath)+"/{env}/results/csv/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueryResultsCSVHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiAllQueriesPath+"/{env}"),
handlerAuthCheck(http.HandlerFunc(handlersApi.AllQueriesShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiQueriesPath)+"/{env}/{action}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueriesActionHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
if flagParams.Osquery.Console {
// API: per-node console
muxAPI.Handle(
"POST "+_apiPath("/console")+"/{env}/nodes/{uuid}/sessions",
handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath("/console")+"/{env}/sessions/{session_id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"DELETE "+_apiPath("/console")+"/{env}/sessions/{session_id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionDeleteHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands",
handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands/{command_id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands/{command_id}/results",
handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandResultsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
}
if flagParams.Osquery.FileExplorer {
// API: per-node file explorer
muxAPI.Handle(
"POST "+_apiPath(apiFileExplorerPath)+"/{env}/nodes/{uuid}/sessions",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerSessionCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerSessionShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"DELETE "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerSessionDeleteHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}/list",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerListHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}/stat",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerStatHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}/requests/{request_id}",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerRequestShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}/requests/{request_id}/results",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerRequestResultsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiFileExplorerPath)+"/{env}/sessions/{session_id}/requests/{request_id}/metadata",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerPrimingResultsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
}
// API: saved queries (Track 4)
muxAPI.Handle(
"GET "+_apiPath(apiSavedQueriesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.SavedQueriesListHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiSavedQueriesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.SavedQueryCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"PATCH "+_apiPath(apiSavedQueriesPath)+"/{env}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.SavedQueryUpdateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"DELETE "+_apiPath(apiSavedQueriesPath)+"/{env}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.SavedQueryDeleteHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
}
// API: osquery schema tables (globally available to authenticated users)
muxAPI.Handle(
"GET "+_apiPath(apiOsqueryPath)+"/tables",
handlerAuthCheck(http.HandlerFunc(handlersApi.OsqueryTablesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: carves by environment
if flagParams.Osquery.Carve {
// Sample carve-targets library (post-auth). The carve-path
// list is a shopping list of high-value exfiltration
// locations (/etc/passwd, \Windows\System32\config\SAM,
// browser keychains, etc.) that anonymous callers have no
// legitimate reason to see.
muxAPI.Handle(
"GET "+_apiPath(apiCarvesPath)+"/samples",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarveSamplesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiCarvesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarveListHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiCarvesPath)+"/{env}/queries/{target}",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarveQueriesHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiCarvesPath)+"/{env}/list",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarveListHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiCarvesPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarvesRunHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiCarvesPath)+"/{env}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarveShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiCarvesPath)+"/{env}/archive/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarveArchiveHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiCarvesPath)+"/{env}/{action}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.CarvesActionHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
}
// API: users
muxAPI.Handle(
"GET "+_apiPath(apiUsersPath)+"/me",
handlerAuthCheck(http.HandlerFunc(handlersApi.MeHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"PATCH "+_apiPath(apiUsersPath)+"/me",
handlerAuthCheck(http.HandlerFunc(handlersApi.MePatchHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiUsersPath)+"/me/password",
handlerAuthCheck(http.HandlerFunc(handlersApi.MePasswordHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiUsersPath)+"/{username}",
handlerAuthCheck(http.HandlerFunc(handlersApi.UserHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiUsersPath),
handlerAuthCheck(http.HandlerFunc(handlersApi.UsersHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"GET "+_apiPath(apiUsersPath)+"/{username}/permissions",
handlerAuthCheck(http.HandlerFunc(handlersApi.GetUserPermissionsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiUsersPath)+"/{username}/permissions",
handlerAuthCheck(http.HandlerFunc(handlersApi.SetUserPermissionsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiUsersPath)+"/{username}/permissions/all",
handlerAuthCheck(http.HandlerFunc(handlersApi.SetUserPermissionsAllHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiUsersPath)+"/{username}/token/refresh",
handlerAuthCheck(http.HandlerFunc(handlersApi.RefreshUserTokenHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"DELETE "+_apiPath(apiUsersPath)+"/{username}/token",
handlerAuthCheck(http.HandlerFunc(handlersApi.DeleteUserTokenHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiUsersPath)+"/{username}/{action}",
handlerAuthCheck(http.HandlerFunc(handlersApi.UserActionHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: platforms
muxAPI.Handle(
"GET "+_apiPath(apiPlatformsPath)+"/{env}",
handlerAuthCheck(http.HandlerFunc(handlersApi.PlatformsEnvHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
// API: environments
muxAPI.Handle(
"GET "+_apiPath(apiEnvironmentsPath),
handlerAuthCheck(http.HandlerFunc(handlersApi.EnvironmentsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
muxAPI.Handle(
"POST "+_apiPath(apiEnvironmentsPath)+"/actions",
handlerAuthCheck(http.HandlerFunc(handlersApi.EnvActionsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))