From 2c15aad150136f50bf2882a6547f0ae8627e33ab Mon Sep 17 00:00:00 2001 From: Javier Marcos <1271349+javuto@users.noreply.github.com> Date: Thu, 20 Oct 2022 01:18:08 +0300 Subject: [PATCH] Node actions in osctrl-api --- admin/handlers/post.go | 25 ------- api/handlers-nodes.go | 154 +++++++++++++++++++++++++++++++++++++++-- api/main.go | 14 ++-- cli/api-node.go | 33 +++++++-- cli/main.go | 25 ++++++- cli/node.go | 66 +++++++++++++++--- nodes/nodes.go | 10 +++ types/types.go | 10 +++ 8 files changed, 287 insertions(+), 50 deletions(-) diff --git a/admin/handlers/post.go b/admin/handlers/post.go index 47b18f61..815b4a5f 100644 --- a/admin/handlers/post.go +++ b/admin/handlers/post.go @@ -1009,31 +1009,6 @@ func (h *HandlersAdmin) NodeActionsPOSTHandler(w http.ResponseWriter, r *http.Re h.Inc(metricAdminErr) return } - case "archive": - if h.Settings.DebugService(settings.ServiceAdmin) { - log.Printf("DebugService: archiving node") - } - adminOKResponse(w, "node archived successfully") - case "tag": - okCount := 0 - errCount := 0 - for _, u := range m.UUIDs { - if err := h.Nodes.ArchiveDeleteByUUID(u); err != nil { - errCount++ - if h.Settings.DebugService(settings.ServiceAdmin) { - log.Printf("DebugService: error tagging node %s %v", u, err) - } - } else { - okCount++ - } - } - if errCount == 0 { - adminOKResponse(w, fmt.Sprintf("%d Node(s) have been deleted successfully", okCount)) - } else { - adminErrorResponse(w, fmt.Sprintf("Error deleting %d node(s)", errCount), http.StatusInternalServerError, nil) - h.Inc(metricAdminErr) - return - } } // Serialize and send response if h.Settings.DebugService(settings.ServiceAdmin) { diff --git a/api/handlers-nodes.go b/api/handlers-nodes.go index 404f3ac6..303184df 100644 --- a/api/handlers-nodes.go +++ b/api/handlers-nodes.go @@ -1,12 +1,14 @@ package main import ( + "encoding/json" "fmt" "log" "net/http" "github.com/gorilla/mux" "github.com/jmpsec/osctrl/settings" + "github.com/jmpsec/osctrl/types" "github.com/jmpsec/osctrl/users" "github.com/jmpsec/osctrl/utils" ) @@ -36,6 +38,13 @@ func apiNodeHandler(w http.ResponseWriter, r *http.Request) { incMetric(metricAPINodesErr) return } + // Get context data and check access + ctx := r.Context().Value(contextKey(contextAPI)).(contextValue) + if !apiUsers.CheckPermissions(ctx[ctxUser], users.UserLevel, env.UUID) { + apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser])) + incMetric(metricAPINodesErr) + return + } // Extract host identifier for node nodeVar, ok := vars["node"] if !ok { @@ -55,23 +64,108 @@ func apiNodeHandler(w http.ResponseWriter, r *http.Request) { incMetric(metricAPINodesErr) return } + // Serialize and serve JSON + if settingsmgr.DebugService(settings.ServiceAPI) { + log.Printf("DebugService: Returned node %s", nodeVar) + } + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, node) + incMetric(metricAPINodesOK) +} + +// GET Handler for active JSON nodes +func apiActiveNodesHandler(w http.ResponseWriter, r *http.Request) { + incMetric(metricAPINodesReq) + utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false) + vars := mux.Vars(r) + // Extract environment + envVar, ok := vars["env"] + if !ok { + apiErrorResponse(w, "error with environment", http.StatusInternalServerError, nil) + incMetric(metricAPINodesErr) + return + } + // Get environment + env, err := envs.Get(envVar) + if err != nil { + apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, nil) + incMetric(metricAPINodesErr) + return + } // Get context data and check access ctx := r.Context().Value(contextKey(contextAPI)).(contextValue) - if !apiUsers.CheckPermissions(ctx[ctxUser], users.UserLevel, env.UUID) { + if !apiUsers.CheckPermissions(ctx[ctxUser], users.AdminLevel, env.UUID) { apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser])) incMetric(metricAPINodesErr) return } + // Get nodes + nodes, err := nodesmgr.Gets("active", 24) + if err != nil { + apiErrorResponse(w, "error getting nodes", http.StatusInternalServerError, err) + incMetric(metricAPINodesErr) + return + } + if len(nodes) == 0 { + apiErrorResponse(w, "no nodes", http.StatusNotFound, nil) + incMetric(metricAPINodesErr) + return + } // Serialize and serve JSON if settingsmgr.DebugService(settings.ServiceAPI) { - log.Printf("DebugService: Returned node %s", nodeVar) + log.Println("DebugService: Returned nodes") } - utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, node) + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, nodes) incMetric(metricAPINodesOK) } -// GET Handler for multiple JSON nodes -func apiNodesHandler(w http.ResponseWriter, r *http.Request) { +// GET Handler for inactive JSON nodes +func apiInactiveNodesHandler(w http.ResponseWriter, r *http.Request) { + incMetric(metricAPINodesReq) + utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false) + vars := mux.Vars(r) + // Extract environment + envVar, ok := vars["env"] + if !ok { + apiErrorResponse(w, "error with environment", http.StatusInternalServerError, nil) + incMetric(metricAPINodesErr) + return + } + // Get environment + env, err := envs.Get(envVar) + if err != nil { + apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, nil) + incMetric(metricAPINodesErr) + return + } + // Get context data and check access + ctx := r.Context().Value(contextKey(contextAPI)).(contextValue) + if !apiUsers.CheckPermissions(ctx[ctxUser], users.AdminLevel, env.UUID) { + apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser])) + incMetric(metricAPINodesErr) + return + } + // Get nodes + nodes, err := nodesmgr.Gets("inactive", 24) + if err != nil { + apiErrorResponse(w, "error getting nodes", http.StatusInternalServerError, err) + incMetric(metricAPINodesErr) + return + } + if len(nodes) == 0 { + apiErrorResponse(w, "no nodes", http.StatusNotFound, nil) + incMetric(metricAPINodesErr) + return + } + // Serialize and serve JSON + if settingsmgr.DebugService(settings.ServiceAPI) { + log.Println("DebugService: Returned nodes") + } + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, nodes) + incMetric(metricAPINodesOK) +} + +// GET Handler for all JSON nodes +func apiAllNodesHandler(w http.ResponseWriter, r *http.Request) { incMetric(metricAPINodesReq) utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false) vars := mux.Vars(r) @@ -115,3 +209,53 @@ func apiNodesHandler(w http.ResponseWriter, r *http.Request) { utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, nodes) incMetric(metricAPINodesOK) } + +// POST Handler to delete single node +func apiDeleteNodeHandler(w http.ResponseWriter, r *http.Request) { + incMetric(metricAPINodesReq) + utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false) + vars := mux.Vars(r) + // Extract environment + envVar, ok := vars["env"] + if !ok { + apiErrorResponse(w, "error with environment", http.StatusInternalServerError, nil) + incMetric(metricAPINodesErr) + return + } + // Get environment + env, err := envs.Get(envVar) + if err != nil { + apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, nil) + incMetric(metricAPINodesErr) + return + } + // Get context data and check access + ctx := r.Context().Value(contextKey(contextAPI)).(contextValue) + if !apiUsers.CheckPermissions(ctx[ctxUser], users.AdminLevel, env.UUID) { + apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser])) + incMetric(metricAPINodesErr) + return + } + var n types.ApiNodeGenericRequest + // Parse request JSON body + if err := json.NewDecoder(r.Body).Decode(&n); err != nil { + apiErrorResponse(w, "error parsing POST body", http.StatusInternalServerError, err) + incMetric(metricAPINodesErr) + return + } + if err := nodesmgr.ArchiveDeleteByUUID(n.UUID); err != nil { + if err.Error() == "record not found" { + apiErrorResponse(w, "node not found", http.StatusNotFound, err) + } else { + apiErrorResponse(w, "error getting node", http.StatusInternalServerError, err) + } + incMetric(metricAPINodesErr) + return + } + // Serialize and serve JSON + if settingsmgr.DebugService(settings.ServiceAPI) { + log.Printf("DebugService: Returned node %s", n.UUID) + } + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, types.ApiGenericResponse{Message: "node deleted"}) + incMetric(metricAPINodesOK) +} diff --git a/api/main.go b/api/main.go index 2c9d4f7e..1e61f713 100644 --- a/api/main.go +++ b/api/main.go @@ -483,10 +483,16 @@ func osctrlAPIService() { // ///////////////////////// AUTHENTICATED // API: nodes by environment - routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/{node}", handlerAuthCheck(http.HandlerFunc(apiNodeHandler))).Methods("GET") - routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/{node}/", handlerAuthCheck(http.HandlerFunc(apiNodeHandler))).Methods("GET") - routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}", handlerAuthCheck(http.HandlerFunc(apiNodesHandler))).Methods("GET") - routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/", handlerAuthCheck(http.HandlerFunc(apiNodesHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/node/{node}", handlerAuthCheck(http.HandlerFunc(apiNodeHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/node/{node}/", handlerAuthCheck(http.HandlerFunc(apiNodeHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/delete", handlerAuthCheck(http.HandlerFunc(apiDeleteNodeHandler))).Methods("POST") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/delete/", handlerAuthCheck(http.HandlerFunc(apiDeleteNodeHandler))).Methods("POST") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/all", handlerAuthCheck(http.HandlerFunc(apiAllNodesHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/all/", handlerAuthCheck(http.HandlerFunc(apiAllNodesHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/active", handlerAuthCheck(http.HandlerFunc(apiActiveNodesHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/active/", handlerAuthCheck(http.HandlerFunc(apiActiveNodesHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/inactive", handlerAuthCheck(http.HandlerFunc(apiInactiveNodesHandler))).Methods("GET") + routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/inactive/", handlerAuthCheck(http.HandlerFunc(apiInactiveNodesHandler))).Methods("GET") // API: queries by environment routerAPI.Handle(_apiPath(apiQueriesPath)+"/{env}", handlerAuthCheck(http.HandlerFunc(apiAllQueriesShowHandler))).Methods("GET") routerAPI.Handle(_apiPath(apiQueriesPath)+"/{env}/", handlerAuthCheck(http.HandlerFunc(apiAllQueriesShowHandler))).Methods("GET") diff --git a/cli/api-node.go b/cli/api-node.go index 418b0445..624fdeab 100644 --- a/cli/api-node.go +++ b/cli/api-node.go @@ -3,14 +3,17 @@ package main import ( "encoding/json" "fmt" + "log" + "strings" "github.com/jmpsec/osctrl/nodes" + "github.com/jmpsec/osctrl/types" ) // GetNodes to retrieve nodes from osctrl -func (api *OsctrlAPI) GetNodes(env string) ([]nodes.OsqueryNode, error) { +func (api *OsctrlAPI) GetNodes(env, target string) ([]nodes.OsqueryNode, error) { var nds []nodes.OsqueryNode - reqURL := fmt.Sprintf("%s%s%s/%s", api.Configuration.URL, APIPath, APINodes, env) + reqURL := fmt.Sprintf("%s%s%s/%s/%s", api.Configuration.URL, APIPath, APINodes, env, target) rawNodes, err := api.GetGeneric(reqURL, nil) if err != nil { return nds, fmt.Errorf("error api request - %v - %s", err, string(rawNodes)) @@ -24,7 +27,7 @@ func (api *OsctrlAPI) GetNodes(env string) ([]nodes.OsqueryNode, error) { // GetNode to retrieve one node from osctrl func (api *OsctrlAPI) GetNode(env, identifier string) (nodes.OsqueryNode, error) { var node nodes.OsqueryNode - reqURL := fmt.Sprintf("%s%s%s/%s/%s", api.Configuration.URL, APIPath, APINodes, env, identifier) + reqURL := fmt.Sprintf("%s%s%s/%s/node/%s", api.Configuration.URL, APIPath, APINodes, env, identifier) rawNode, err := api.GetGeneric(reqURL, nil) if err != nil { return node, fmt.Errorf("error api request - %v - %s", err, string(rawNode)) @@ -36,6 +39,28 @@ func (api *OsctrlAPI) GetNode(env, identifier string) (nodes.OsqueryNode, error) } // DeleteNode to delete node from osctrl -func (api *OsctrlAPI) DeleteNode(identifier string) error { +func (api *OsctrlAPI) DeleteNode(env, identifier string) error { + n := types.ApiNodeGenericRequest{ + UUID: identifier, + } + var r types.ApiGenericResponse + reqURL := fmt.Sprintf("%s%s%s/%s/delete", api.Configuration.URL, APIPath, APINodes, env) + jsonMessage, err := json.Marshal(n) + if err != nil { + log.Printf("error marshaling data %s", err) + } + jsonParam := strings.NewReader(string(jsonMessage)) + rawN, err := api.PostGeneric(reqURL, jsonParam) + if err != nil { + return fmt.Errorf("error api request - %v - %s", err, string(rawN)) + } + if err := json.Unmarshal(rawN, &r); err != nil { + return fmt.Errorf("can not parse body - %v", err) + } + return nil +} + +// TagNode to tag node in osctrl +func (api *OsctrlAPI) TagNode(env, identifier, tag string) error { return nil } diff --git a/cli/main.go b/cli/main.go index 7dd5a5ab..ded744cc 100644 --- a/cli/main.go +++ b/cli/main.go @@ -203,7 +203,7 @@ func init() { Aliases: []string{"s"}, Value: false, Usage: "Silent mode", - Destination: &prettyFlag, + Destination: &silentFlag, }, } // Initialize CLI flags commands @@ -991,6 +991,29 @@ func init() { }, Action: cliWrapper(deleteNode), }, + { + Name: "tag", + Aliases: []string{"t"}, + Usage: "Tag an existing node", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "uuid, u", + Aliases: []string{"u"}, + Usage: "Node UUID to be tagged", + }, + &cli.StringFlag{ + Name: "env", + Aliases: []string{"e"}, + Usage: "Environment to be used", + }, + &cli.StringFlag{ + Name: "tag-value", + Aliases: []string{"T"}, + Usage: "Tag value to be used. It will be created if does not exist", + }, + }, + Action: cliWrapper(tagNode), + }, { Name: "list", Aliases: []string{"l"}, diff --git a/cli/node.go b/cli/node.go index 05d20586..0f31489b 100644 --- a/cli/node.go +++ b/cli/node.go @@ -61,12 +61,12 @@ func listNodes(c *cli.Context) error { if dbFlag { nds, err = nodesmgr.Gets(target, settingsmgr.InactiveHours()) if err != nil { - return err + return fmt.Errorf("❌ error getting nodes - %s", err) } } else if apiFlag { - nds, err = osctrlAPI.GetNodes(env) + nds, err = osctrlAPI.GetNodes(env, target) if err != nil { - return err + return fmt.Errorf("❌ error getting nodes - %s", err) } } header := []string{ @@ -83,14 +83,14 @@ func listNodes(c *cli.Context) error { if jsonFlag { jsonRaw, err := json.Marshal(nds) if err != nil { - return err + return fmt.Errorf("❌ error marshaling - %s", err) } fmt.Println(string(jsonRaw)) } else if csvFlag { data := nodesToData(nds, header) w := csv.NewWriter(os.Stdout) if err := w.WriteAll(data); err != nil { - return err + return fmt.Errorf("❌ error writting csv - %s", err) } } else if prettyFlag { table := tablewriter.NewWriter(os.Stdout) @@ -114,17 +114,61 @@ func deleteNode(c *cli.Context) error { fmt.Println("❌ uuid is required") os.Exit(1) } - env := c.String("name") + env := c.String("env") if env == "" { fmt.Println("❌ environment is required") os.Exit(1) } if dbFlag { if err := nodesmgr.ArchiveDeleteByUUID(uuid); err != nil { - return err + return fmt.Errorf("❌ error deleting - %s", err) } } else if apiFlag { + if err := osctrlAPI.DeleteNode(env, uuid); err != nil { + return fmt.Errorf("❌ error deleting node - %s", err) + } + } + if !silentFlag { + fmt.Println("✅ node was deleted successfully") + } + return nil +} +func tagNode(c *cli.Context) error { + // Get values from flags + uuid := c.String("uuid") + if uuid == "" { + fmt.Println("❌ uuid is required") + os.Exit(1) + } + env := c.String("env") + if env == "" { + fmt.Println("❌ environment is required") + os.Exit(1) + } + tag := c.String("tag-value") + if env == "" { + fmt.Println("❌ tag is required") + os.Exit(1) + } + if dbFlag { + e, err := envs.Get(env) + if err != nil { + return fmt.Errorf("❌ error env get - %s", err) + } + n, err := nodesmgr.GetByUUIDEnv(uuid, e.ID) + if err != nil { + return fmt.Errorf("❌ error get uuid - %s", err) + } + if tagsmgr.Exists(tag) { + if err := tagsmgr.TagNode(tag, n, appName, false); err != nil { + return fmt.Errorf("❌ error tagging - %s", err) + } + } + } else if apiFlag { + if err := osctrlAPI.TagNode(env, uuid, tag); err != nil { + return fmt.Errorf("❌ error tagging node - %s", err) + } } if !silentFlag { fmt.Println("✅ node was deleted successfully") @@ -148,12 +192,12 @@ func showNode(c *cli.Context) error { if dbFlag { node, err = nodesmgr.GetByUUID(uuid) if err != nil { - return err + return fmt.Errorf("❌ error getting node - %s", err) } } else if apiFlag { node, err = osctrlAPI.GetNode(env, uuid) if err != nil { - return err + return fmt.Errorf("❌ error getting node - %s", err) } } header := []string{ @@ -170,14 +214,14 @@ func showNode(c *cli.Context) error { if jsonFlag { jsonRaw, err := json.Marshal(node) if err != nil { - return err + return fmt.Errorf("❌ error marshaling - %s", err) } fmt.Println(string(jsonRaw)) } else if csvFlag { data := nodeToData(node, nil) w := csv.NewWriter(os.Stdout) if err := w.WriteAll(data); err != nil { - return err + return fmt.Errorf("❌ error writting csv - %s", err) } } else if prettyFlag { table := tablewriter.NewWriter(os.Stdout) diff --git a/nodes/nodes.go b/nodes/nodes.go index da085daf..345eb2ac 100644 --- a/nodes/nodes.go +++ b/nodes/nodes.go @@ -190,6 +190,16 @@ func (n *NodeManager) GetByUUID(uuid string) (OsqueryNode, error) { return node, nil } +// GetByUUIDEnv to retrieve full node object from DB, by uuid and environment ID +// UUID is expected uppercase +func (n *NodeManager) GetByUUIDEnv(uuid string, envid uint) (OsqueryNode, error) { + var node OsqueryNode + if err := n.DB.Where("uuid = ? AND environment_id = ?", strings.ToUpper(uuid), envid).First(&node).Error; err != nil { + return node, err + } + return node, nil +} + // GetBySelector to retrieve target nodes by selector func (n *NodeManager) GetBySelector(stype, selector, target string, hours int64) ([]OsqueryNode, error) { var nodes []OsqueryNode diff --git a/types/types.go b/types/types.go index d4852c35..e295b81d 100644 --- a/types/types.go +++ b/types/types.go @@ -108,6 +108,11 @@ type ApiDistributedCarveRequest struct { Path string `json:"path"` } +// ApiNodeGenericRequest to receive generic node requests +type ApiNodeGenericRequest struct { + UUID string `json:"uuid"` +} + // ApiErrorResponse to be returned to API requests with the error message type ApiErrorResponse struct { Error string `json:"error"` @@ -117,3 +122,8 @@ type ApiErrorResponse struct { type ApiQueriesResponse struct { Name string `json:"query_name"` } + +// ApiGenericResponse to be returned to API requests for anything +type ApiGenericResponse struct { + Message string `json:"message"` +}