diff --git a/cmd/admin/handlers/get.go b/cmd/admin/handlers/get.go index 965996d7..34d8c882 100644 --- a/cmd/admin/handlers/get.go +++ b/cmd/admin/handlers/get.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "os" + "strings" "github.com/jmpsec/osctrl/cmd/admin/sessions" "github.com/jmpsec/osctrl/pkg/carves" @@ -150,4 +151,6 @@ func (h *HandlersAdmin) CarvesDownloadHandler(w http.ResponseWriter, r *http.Req fileReader, _ = os.Open(archived.File) _, _ = io.Copy(w, fileReader) } + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } diff --git a/cmd/admin/handlers/handlers.go b/cmd/admin/handlers/handlers.go index 771743b4..827bd0d5 100644 --- a/cmd/admin/handlers/handlers.go +++ b/cmd/admin/handlers/handlers.go @@ -2,6 +2,7 @@ package handlers import ( "github.com/jmpsec/osctrl/cmd/admin/sessions" + "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/backend" "github.com/jmpsec/osctrl/pkg/cache" "github.com/jmpsec/osctrl/pkg/carves" @@ -43,6 +44,7 @@ type HandlersAdmin struct { OptimizedUI bool OsqueryTables []types.OsqueryTable AdminConfig *config.JSONConfigurationService + AuditLog *auditlog.AuditLogManager DBLogger *logging.LoggerDB DebugHTTP *zerolog.Logger DebugHTTPConfig *config.DebugHTTPConfiguration @@ -161,6 +163,12 @@ func WithAdminConfig(config *config.JSONConfigurationService) HandlersOption { } } +func WithAuditLog(auditLog *auditlog.AuditLogManager) HandlersOption { + return func(h *HandlersAdmin) { + h.AuditLog = auditLog + } +} + func WithDBLogger(dbfile string, config *backend.JSONConfigurationDB) HandlersOption { return func(h *HandlersAdmin) { if dbfile == "" { diff --git a/cmd/admin/handlers/json-audit.go b/cmd/admin/handlers/json-audit.go new file mode 100644 index 00000000..ec51a64b --- /dev/null +++ b/cmd/admin/handlers/json-audit.go @@ -0,0 +1,79 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/jmpsec/osctrl/cmd/admin/sessions" + "github.com/jmpsec/osctrl/pkg/auditlog" + "github.com/jmpsec/osctrl/pkg/environments" + "github.com/jmpsec/osctrl/pkg/users" + "github.com/jmpsec/osctrl/pkg/utils" + "github.com/rs/zerolog/log" +) + +// AuditLogJSON to be used to populate JSON data for audit logs +type AuditLogJSON struct { + Service string `json:"service"` + Username string `json:"username"` + Line string `json:"line"` + SourceIP string `json:"sourceip"` + LogType string `json:"logtype"` + Severity string `json:"severity"` + Env string `json:"environment"` + When CreationTimes `json:"when"` +} + +// ReturnedAudit to return a JSON with audit logs +type ReturnedAudit struct { + Data []AuditLogJSON `json:"data"` +} + +// JSONAuditLogHandler for audit logs in JSON +func (h *HandlersAdmin) JSONAuditLogHandler(w http.ResponseWriter, r *http.Request) { + if h.DebugHTTPConfig.Enabled { + utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody) + } + // Get context data + ctx := r.Context().Value(sessions.ContextKey(sessions.CtxSession)).(sessions.ContextValue) + // Check permissions + if !h.Users.CheckPermissions(ctx[sessions.CtxUser], users.AdminLevel, users.NoEnvironment) { + adminErrorResponse(w, fmt.Sprintf("%s has insufficient permissions", ctx[sessions.CtxUser]), http.StatusForbidden, nil) + return + } + // Get all environments + envs, err := h.Envs.All() + if err != nil { + log.Err(err).Msg("error getting environments") + return + } + // Get audit logs + auditLogs, err := h.AuditLog.GetAll() + if err != nil { + log.Err(err).Msg("error getting audit logs") + return + } + // Prepare data to be returned + var auditLogsJSON []AuditLogJSON + for _, logEntry := range auditLogs { + auditLogsJSON = append(auditLogsJSON, AuditLogJSON{ + Service: logEntry.Service, + Username: logEntry.Username, + Line: logEntry.Line, + SourceIP: logEntry.SourceIP, + LogType: auditlog.LogTypeToString(logEntry.LogType), + Severity: auditlog.SeverityToString(logEntry.Severity), + Env: environments.EnvironmentFinderID(logEntry.EnvironmentID, envs, false), + When: CreationTimes{ + Display: utils.PastFutureTimes(logEntry.CreatedAt), + // Use Unix timestamp in seconds + Timestamp: utils.TimeTimestamp(logEntry.CreatedAt), + }, + }) + } + returned := ReturnedAudit{ + Data: auditLogsJSON, + } + // Serve JSON + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, returned) +} diff --git a/cmd/admin/handlers/post.go b/cmd/admin/handlers/post.go index f4a67a02..95a5595a 100644 --- a/cmd/admin/handlers/post.go +++ b/cmd/admin/handlers/post.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/jmpsec/osctrl/cmd/admin/sessions" + "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/handlers" "github.com/jmpsec/osctrl/pkg/nodes" "github.com/jmpsec/osctrl/pkg/queries" @@ -45,7 +46,7 @@ func (h *HandlersAdmin) LoginPOSTHandler(w http.ResponseWriter, r *http.Request) return } // Serialize and send response - log.Debug().Msg("Login response sent") + h.AuditLog.NewLogin(user.Username, strings.Split(r.RemoteAddr, ":")[0]) adminOKResponse(w, "/dashboard") } @@ -74,7 +75,7 @@ func (h *HandlersAdmin) LogoutPOSTHandler(w http.ResponseWriter, r *http.Request return } // Serialize and send response - log.Debug().Msg("Logout response sent") + h.AuditLog.NewLogout(ctx[sessions.CtxUser], strings.Split(r.RemoteAddr, ":")[0]) adminOKResponse(w, "OK") } @@ -172,7 +173,7 @@ func (h *HandlersAdmin) QueryRunPOSTHandler(w http.ResponseWriter, r *http.Reque } } // Serialize and send response - log.Debug().Msg("Query run response sent") + h.AuditLog.NewQuery(ctx[sessions.CtxUser], q.Query, strings.Split(r.RemoteAddr, ":")[0], env.ID) adminOKResponse(w, "OK") } @@ -262,7 +263,7 @@ func (h *HandlersAdmin) CarvesRunPOSTHandler(w http.ResponseWriter, r *http.Requ return } // Serialize and send response - log.Debug().Msg("Carve run response sent") + h.AuditLog.NewCarve(ctx[sessions.CtxUser], c.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) adminOKResponse(w, "OK") } @@ -345,8 +346,7 @@ func (h *HandlersAdmin) QueryActionsPOSTHandler(w http.ResponseWriter, r *http.R } adminOKResponse(w, "queries delete successfully") } - // Serialize and send response - log.Debug().Msg("Query run response sent") + h.AuditLog.QueryAction(ctx[sessions.CtxUser], q.Action, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // CarvesActionsPOSTHandler - Handler for POST requests to carves @@ -402,8 +402,7 @@ func (h *HandlersAdmin) CarvesActionsPOSTHandler(w http.ResponseWriter, r *http. log.Debug().Msg("testing action") adminOKResponse(w, "test successful") } - // Serialize and send response - log.Debug().Msg("Carves action response sent") + h.AuditLog.CarveAction(ctx[sessions.CtxUser], q.Action, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // ConfPOSTHandler for POST requests for saving configuration @@ -466,8 +465,8 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request) adminErrorResponse(w, "error saving configuration parts", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update configuration", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Send response - log.Debug().Msg("Configuration response sent") adminOKResponse(w, "configuration saved successfully") return } @@ -489,8 +488,8 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request) adminErrorResponse(w, "error updating configuration", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update options", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Send response - log.Debug().Msg("Options response sent") adminOKResponse(w, "options saved successfully") return } @@ -512,8 +511,8 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request) adminErrorResponse(w, "error updating configuration", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update schedule", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Send response - log.Debug().Msg("Schedule response sent") adminOKResponse(w, "schedule saved successfully") return } @@ -535,8 +534,8 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request) adminErrorResponse(w, "error updating configuration", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update packs", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Send response - log.Debug().Msg("Packs response sent") adminOKResponse(w, "packs saved successfully") return } @@ -558,8 +557,8 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request) adminErrorResponse(w, "error updating configuration", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update decorators", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Send response - log.Debug().Msg("Decorators response sent") adminOKResponse(w, "decorators saved successfully") return } @@ -581,8 +580,8 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request) adminErrorResponse(w, "error updating configuration", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update ATC", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Send response - log.Debug().Msg("ATC response sent") adminOKResponse(w, "ATC saved successfully") return } @@ -644,8 +643,8 @@ func (h *HandlersAdmin) IntervalsPOSTHandler(w http.ResponseWriter, r *http.Requ adminErrorResponse(w, "error updating flags", http.StatusInternalServerError, err) return } + h.AuditLog.ConfAction(ctx[sessions.CtxUser], "update intervals", strings.Split(r.RemoteAddr, ":")[0], env.ID) // Serialize and send response - log.Debug().Msg("Intervals response sent") adminOKResponse(w, "intervals saved successfully") } @@ -741,8 +740,7 @@ func (h *HandlersAdmin) ExpirationPOSTHandler(w http.ResponseWriter, r *http.Req adminOKResponse(w, "link set to not expire successfully") } } - // Serialize and send response - log.Debug().Msg("Expiration response sent") + h.AuditLog.ConfAction(ctx[sessions.CtxUser], fmt.Sprintf("%s:%s", e.Type, e.Action), strings.Split(r.RemoteAddr, ":")[0], env.ID) } // NodeActionsPOSTHandler for POST requests for multi node action @@ -787,8 +785,7 @@ func (h *HandlersAdmin) NodeActionsPOSTHandler(w http.ResponseWriter, r *http.Re return } } - // Serialize and send response - log.Debug().Msg("Multi-node action response sent") + h.AuditLog.NodeAction(ctx[sessions.CtxUser], m.Action, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // EnvsPOSTHandler for POST request for /environments @@ -878,8 +875,7 @@ func (h *HandlersAdmin) EnvsPOSTHandler(w http.ResponseWriter, r *http.Request) } adminOKResponse(w, "debug changed successfully") } - // Serialize and send response - log.Debug().Msg("Environments response sent") + h.AuditLog.EnvAction(ctx[sessions.CtxUser], fmt.Sprintf("%s - %s", c.Action, c.Name), strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // SettingsPOSTHandler for POST request for /settings @@ -963,8 +959,7 @@ func (h *HandlersAdmin) SettingsPOSTHandler(w http.ResponseWriter, r *http.Reque } adminOKResponse(w, "setting deleted successfully") } - // Serialize and send response - log.Debug().Msg("Settings response sent") + h.AuditLog.SettingsAction(ctx[sessions.CtxUser], fmt.Sprintf("%s - %s", s.Action, s.Name), strings.Split(r.RemoteAddr, ":")[0]) } // UsersPOSTHandler for POST request for /users @@ -1112,8 +1107,7 @@ func (h *HandlersAdmin) UsersPOSTHandler(w http.ResponseWriter, r *http.Request) adminOKResponse(w, "service changed successfully") } } - // Serialize and send response - log.Debug().Msg("Users response sent") + h.AuditLog.UserAction(ctx[sessions.CtxUser], fmt.Sprintf("%s - %s", u.Action, u.Username), strings.Split(r.RemoteAddr, ":")[0]) } // TagsPOSTHandler for POST request for /tags @@ -1205,8 +1199,7 @@ func (h *HandlersAdmin) TagsPOSTHandler(w http.ResponseWriter, r *http.Request) } adminOKResponse(w, "tag removed successfully") } - // Serialize and send response - log.Debug().Msg("Tags response sent") + h.AuditLog.TagAction(ctx[sessions.CtxUser], fmt.Sprintf("%s - %s", t.Action, t.Name), strings.Split(r.RemoteAddr, ":")[0], env.ID) } // TagNodesPOSTHandler for POST request for /tags/nodes @@ -1263,8 +1256,9 @@ func (h *HandlersAdmin) TagNodesPOSTHandler(w http.ResponseWriter, r *http.Reque return } } + aMsg := fmt.Sprintf("tags processed: add %d, remove %d", len(t.TagsAdd), len(t.TagsRemove)) + h.AuditLog.TagAction(ctx[sessions.CtxUser], aMsg, strings.Split(r.RemoteAddr, ":")[0], toBeProcessed[0].EnvironmentID) // Serialize and send response - log.Debug().Msg("Tags response sent") adminOKResponse(w, "tags processed successfully") } @@ -1322,8 +1316,8 @@ func (h *HandlersAdmin) PermissionsPOSTHandler(w http.ResponseWriter, r *http.Re return } } + h.AuditLog.UserAction(ctx[sessions.CtxUser], fmt.Sprintf("permissions - %s", usernameVar), strings.Split(r.RemoteAddr, ":")[0]) // Serialize and send response - log.Debug().Msg("Users response sent") adminOKResponse(w, "permissions updated successfully") } @@ -1423,8 +1417,8 @@ func (h *HandlersAdmin) EnrollPOSTHandler(w http.ResponseWriter, r *http.Request } } } + h.AuditLog.EnvAction(ctx[sessions.CtxUser], fmt.Sprintf("%s - %s", e.Action, env.Name), strings.Split(r.RemoteAddr, ":")[0], env.ID) // Serialize and send response - log.Debug().Msg("Configuration response sent") adminOKResponse(w, "enroll data saved") } @@ -1491,8 +1485,7 @@ func (h *HandlersAdmin) EditProfilePOSTHandler(w http.ResponseWriter, r *http.Re } adminOKResponse(w, "profiled updated successfully") } - // Serialize and send response - log.Debug().Msg("Edit profile response sent") + h.AuditLog.UserAction(ctx[sessions.CtxUser], fmt.Sprintf("%s - %s", u.Action, u.Username), strings.Split(r.RemoteAddr, ":")[0]) } // SavedQueriesPOSTHandler for POST requests to save queries @@ -1520,6 +1513,4 @@ func (h *HandlersAdmin) SavedQueriesPOSTHandler(w http.ResponseWriter, r *http.R case "edit": adminOKResponse(w, "query saved successfully") } - // Serialize and send response - log.Debug().Msg("Saved query response sent") } diff --git a/cmd/admin/handlers/templates.go b/cmd/admin/handlers/templates.go index 54fd0bf9..32ac875b 100644 --- a/cmd/admin/handlers/templates.go +++ b/cmd/admin/handlers/templates.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/jmpsec/osctrl/cmd/admin/sessions" + "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/carves" "github.com/jmpsec/osctrl/pkg/environments" "github.com/jmpsec/osctrl/pkg/nodes" @@ -37,7 +38,6 @@ var validTarget = map[string]bool{ } // TemplateMetadata - Helper to prepare template metadata -// TODO until a better implementation, all users are admin func (h *HandlersAdmin) TemplateMetadata(ctx sessions.ContextValue, metadata types.BuildMetadata, admin bool) TemplateMetadata { return TemplateMetadata{ Username: ctx[sessions.CtxUser], @@ -88,7 +88,7 @@ func (h *HandlersAdmin) LoginHandler(w http.ResponseWriter, r *http.Request) { log.Err(err).Msg("template error") return } - log.Debug().Msg("Login template served") + h.AuditLog.Visit("anonymous", r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], settings.NoEnvironmentID) } // EnvironmentHandler for environment view of the table @@ -169,7 +169,8 @@ func (h *HandlersAdmin) EnvironmentHandler(w http.ResponseWriter, r *http.Reques log.Err(err).Msg("template error") return } - log.Debug().Msg("Environment table template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // QueryRunGETHandler for GET requests to run queries @@ -269,7 +270,8 @@ func (h *HandlersAdmin) QueryRunGETHandler(w http.ResponseWriter, r *http.Reques log.Err(err).Msg("template error") return } - log.Debug().Msg("Query run template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // QueryListGETHandler for GET requests to queries @@ -339,7 +341,8 @@ func (h *HandlersAdmin) QueryListGETHandler(w http.ResponseWriter, r *http.Reque log.Err(err).Msg("template error") return } - log.Debug().Msg("Query list template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // SavedQueriesGETHandler for GET requests to queries @@ -403,7 +406,8 @@ func (h *HandlersAdmin) SavedQueriesGETHandler(w http.ResponseWriter, r *http.Re log.Err(err).Msg("template error") return } - log.Debug().Msg("Query list template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // CarvesRunGETHandler for GET requests to run file carves @@ -497,7 +501,8 @@ func (h *HandlersAdmin) CarvesRunGETHandler(w http.ResponseWriter, r *http.Reque log.Err(err).Msg("template error") return } - log.Debug().Msg("Query run template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // CarvesListGETHandler for GET requests to carves @@ -568,7 +573,8 @@ func (h *HandlersAdmin) CarvesListGETHandler(w http.ResponseWriter, r *http.Requ log.Err(err).Msg("template error") return } - log.Debug().Msg("Carve list template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // QueryLogsHandler for GET requests to see query results by name @@ -658,7 +664,8 @@ func (h *HandlersAdmin) QueryLogsHandler(w http.ResponseWriter, r *http.Request) log.Err(err).Msg("template error") return } - log.Debug().Msg("Query logs template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // CarvesDetailsHandler for GET requests to see carves details by name @@ -763,7 +770,8 @@ func (h *HandlersAdmin) CarvesDetailsHandler(w http.ResponseWriter, r *http.Requ log.Err(err).Msg("template error") return } - log.Debug().Msg("Carve details template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // ConfGETHandler for GET requests for /conf @@ -834,7 +842,8 @@ func (h *HandlersAdmin) ConfGETHandler(w http.ResponseWriter, r *http.Request) { log.Err(err).Msg("template error") return } - log.Debug().Msg("Conf template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // EnrollGETHandler for GET requests for /enroll @@ -929,7 +938,8 @@ func (h *HandlersAdmin) EnrollGETHandler(w http.ResponseWriter, r *http.Request) log.Err(err).Msg("template error") return } - log.Debug().Msg("Enroll template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // EnrollGETHandler for GET requests for /enroll @@ -1007,6 +1017,8 @@ func (h *HandlersAdmin) EnrollDownloadHandler(w http.ResponseWriter, r *http.Req utils.HTTPDownload(w, description, fName, int64(len(toDownload))) w.WriteHeader(http.StatusOK) _, _ = io.Copy(w, bytes.NewReader(toDownload)) + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // NodeHandler for node view @@ -1126,7 +1138,8 @@ func (h *HandlersAdmin) NodeHandler(w http.ResponseWriter, r *http.Request) { log.Err(err).Msg("template error") return } - log.Debug().Msg("Node template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], env.ID) } // EnvsGETHandler for GET requests for /env @@ -1170,7 +1183,8 @@ func (h *HandlersAdmin) EnvsGETHandler(w http.ResponseWriter, r *http.Request) { log.Err(err).Msg("template error") return } - log.Debug().Msg("Environments template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // SettingsGETHandler for GET requests for /settings @@ -1244,7 +1258,8 @@ func (h *HandlersAdmin) SettingsGETHandler(w http.ResponseWriter, r *http.Reques log.Err(err).Msg("template error") return } - log.Debug().Msg("Settings template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // UsersGETHandler for GET requests for /users @@ -1307,7 +1322,8 @@ func (h *HandlersAdmin) UsersGETHandler(w http.ResponseWriter, r *http.Request) log.Err(err).Msg("template error") return } - log.Debug().Msg("Users template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // UsersTokensGETHandler for GET requests for /users/tokens @@ -1362,7 +1378,8 @@ func (h *HandlersAdmin) UsersTokensGETHandler(w http.ResponseWriter, r *http.Req log.Err(err).Msg("template error") return } - log.Debug().Msg("Users template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // TagsGETHandler for GET requests for /tags @@ -1420,7 +1437,8 @@ func (h *HandlersAdmin) TagsGETHandler(w http.ResponseWriter, r *http.Request) { log.Err(err).Msg("template error") return } - log.Debug().Msg("Tags template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // EditProfileGETHandler for user profile edit @@ -1476,7 +1494,8 @@ func (h *HandlersAdmin) EditProfileGETHandler(w http.ResponseWriter, r *http.Req log.Err(err).Msg("template error") return } - log.Debug().Msg("Profile template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } // DashboardGETHandler for dashboard page @@ -1537,5 +1556,56 @@ func (h *HandlersAdmin) DashboardGETHandler(w http.ResponseWriter, r *http.Reque log.Err(err).Msg("template error") return } - log.Debug().Msg("Dashboard template served") + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) +} + +// AuditLogsGETHandler for GET requests for /audit-logs +func (h *HandlersAdmin) AuditLogsGETHandler(w http.ResponseWriter, r *http.Request) { + if h.DebugHTTPConfig.Enabled { + utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody) + } + // Get context data + ctx := r.Context().Value(sessions.ContextKey(sessions.CtxSession)).(sessions.ContextValue) + // Check permissions + if !h.Users.CheckPermissions(ctx[sessions.CtxUser], users.AdminLevel, users.NoEnvironment) { + log.Info().Msgf("%s has insufficient permissions", ctx[sessions.CtxUser]) + return + } + // Custom functions to handle formatting + funcMap := template.FuncMap{ + "pastFutureTimes": utils.PastFutureTimes, + "inFutureTime": utils.InFutureTime, + } + // Prepare template + tempateFiles := h.NewTemplateFiles(h.TemplatesFolder, "audit.html").filepaths + t, err := template.New("audit.html").Funcs(funcMap).ParseFiles(tempateFiles...) + if err != nil { + log.Err(err).Msg("error getting audit log template") + return + } + // Get stats for all environments + envAll, err := h.Envs.All() + if err != nil { + log.Err(err).Msg("error getting environments") + return + } + // Get if the user is admin + user, err := h.Users.Get(ctx[sessions.CtxUser]) + if err != nil { + log.Err(err).Msg("error getting user") + return + } + // Prepare template data + templateData := AuditLogTemplateData{ + Title: "Audit logs for all environments", + Metadata: h.TemplateMetadata(ctx, h.ServiceMetadata, user.Admin), + Environments: h.allowedEnvironments(ctx[sessions.CtxUser], envAll), + } + if err := t.Execute(w, templateData); err != nil { + log.Err(err).Msg("template error") + return + } + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) } diff --git a/cmd/admin/handlers/tokens.go b/cmd/admin/handlers/tokens.go index 4d2e06b4..494d0121 100644 --- a/cmd/admin/handlers/tokens.go +++ b/cmd/admin/handlers/tokens.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "github.com/jmpsec/osctrl/cmd/admin/sessions" + "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/users" "github.com/jmpsec/osctrl/pkg/utils" "github.com/rs/zerolog/log" @@ -50,6 +52,8 @@ func (h *HandlersAdmin) TokensGETHandler(w http.ResponseWriter, r *http.Request) ExpiresTS: utils.TimeTimestamp(user.TokenExpire), } } + // Audit log visit + h.AuditLog.Visit(ctx[sessions.CtxUser], r.URL.Path, strings.Split(r.RemoteAddr, ":")[0], auditlog.NoEnvironment) // Serve JSON utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, returned) } @@ -105,6 +109,8 @@ func (h *HandlersAdmin) TokensPOSTHandler(w http.ResponseWriter, r *http.Request ExpirationTS: utils.TimeTimestamp(exp), Expiration: utils.PastFutureTimes(exp), } + // Audit log token creation + h.AuditLog.NewToken(ctx[sessions.CtxUser], strings.Split(r.RemoteAddr, ":")[0]) // Serialize and serve JSON utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, response) } diff --git a/cmd/admin/handlers/types-templates.go b/cmd/admin/handlers/types-templates.go index e3f71581..ec037197 100644 --- a/cmd/admin/handlers/types-templates.go +++ b/cmd/admin/handlers/types-templates.go @@ -229,6 +229,14 @@ type TagsTemplateData struct { LeftMetadata AsideLeftMetadata } +// AuditLogTemplateData for passing data to the audit log template +type AuditLogTemplateData struct { + Title string + Environments []environments.TLSEnvironment + Metadata TemplateMetadata + LeftMetadata AsideLeftMetadata +} + // NodeTemplateData for passing data to the query template type NodeTemplateData struct { Title string diff --git a/cmd/admin/main.go b/cmd/admin/main.go index 0e1750f9..ba5dd1d0 100644 --- a/cmd/admin/main.go +++ b/cmd/admin/main.go @@ -14,6 +14,7 @@ import ( "github.com/crewjam/saml/samlsp" "github.com/jmpsec/osctrl/cmd/admin/handlers" "github.com/jmpsec/osctrl/cmd/admin/sessions" + "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/backend" "github.com/jmpsec/osctrl/pkg/cache" "github.com/jmpsec/osctrl/pkg/carves" @@ -98,6 +99,7 @@ var ( // FIXME this is nasty and should not be a global but here we are osqueryTables []types.OsqueryTable handlersAdmin *handlers.HandlersAdmin + auditLog *auditlog.AuditLogManager ) // SAML variables @@ -302,6 +304,12 @@ func osctrlAdminService() { } } + // Initialize audit log manager + auditLog, err = auditlog.CreateAuditLogManager(db.Conn, serviceName, flagParams.AuditLog) + if err != nil { + log.Fatal().Msgf("Error initializing audit log manager - %v", err) + } + // Initialize Admin handlers before router log.Info().Msg("Initializing handlers") handlersAdmin = handlers.CreateHandlersAdmin( @@ -327,6 +335,7 @@ func osctrlAdminService() { handlers.WithCarvesFolder(flagParams.CarvedDir), handlers.WithOptimizedUI(flagParams.OptimizeUI), handlers.WithAdminConfig(&flagParams.ConfigValues), + handlers.WithAuditLog(auditLog), handlers.WithDBLogger(flagParams.LoggerFile, loggerDBConfig), handlers.WithDebugHTTP(&flagParams.DebugHTTPValues), ) @@ -397,6 +406,12 @@ func osctrlAdminService() { adminMux.Handle( "GET /json/tags", handlerAuthCheck(http.HandlerFunc(handlersAdmin.JSONTagsHandler), flagParams.ConfigValues.Auth)) + // Admin: JSON data for audit logs + if flagParams.AuditLog { + adminMux.Handle( + "GET /json/audit-logs", + handlerAuthCheck(http.HandlerFunc(handlersAdmin.JSONAuditLogHandler), flagParams.ConfigValues.Auth)) + } // Admin: table for environments adminMux.Handle( "GET /environment/{env}/{target}", @@ -541,6 +556,12 @@ func osctrlAdminService() { adminMux.Handle( "POST /profile", handlerAuthCheck(http.HandlerFunc(handlersAdmin.EditProfilePOSTHandler), flagParams.ConfigValues.Auth)) + // Admin: audit logs + if flagParams.AuditLog { + adminMux.Handle( + "GET /audit-logs", + handlerAuthCheck(http.HandlerFunc(handlersAdmin.AuditLogsGETHandler), flagParams.ConfigValues.Auth)) + } // Admin: dashboard and search bar adminMux.Handle( "GET /dashboard", diff --git a/cmd/admin/templates/audit.html b/cmd/admin/templates/audit.html new file mode 100644 index 00000000..15291dd4 --- /dev/null +++ b/cmd/admin/templates/audit.html @@ -0,0 +1,179 @@ + + + + {{ $metadata := .Metadata }} + {{ $leftmeta := .LeftMetadata }} + + {{ template "page-head" . }} + + + + {{ template "page-header" . }} + +
+ + {{ template "page-aside-left" . }} + +
+ +
+ +
+ + +
+
+ Audit Logs +
+ Refresh in 30 seconds + + +
+
+
+ + + + + + + + + + + + + + +
ServiceUserLogIP AddressLogTypeSeverityEnvironmentWhen
+
+
+ + {{ template "page-modals" . }} + +
+ +
+ + {{ if $metadata.Admin }} + {{ template "page-aside-right" . }} + {{ end }} + +
+ + {{ template "page-js" . }} + + + + + + diff --git a/cmd/admin/templates/components/page-aside-right.html b/cmd/admin/templates/components/page-aside-right.html index 25247736..d187e104 100644 --- a/cmd/admin/templates/components/page-aside-right.html +++ b/cmd/admin/templates/components/page-aside-right.html @@ -17,6 +17,16 @@
Admin Server Settings
Settings for the Admin service
+
+ + + +
+
+ View the Audit Logs +