Skip to content

Commit

Permalink
Merge pull request #311 from jmpsec/api-node-actions
Browse files Browse the repository at this point in the history
Node actions in `osctrl-api`
  • Loading branch information
javuto authored Oct 19, 2022
2 parents bfca823 + 2c15aad commit c72ecd2
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 50 deletions.
25 changes: 0 additions & 25 deletions admin/handlers/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
154 changes: 149 additions & 5 deletions api/handlers-nodes.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
14 changes: 10 additions & 4 deletions api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
33 changes: 29 additions & 4 deletions cli/api-node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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
}
25 changes: 24 additions & 1 deletion cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func init() {
Aliases: []string{"s"},
Value: false,
Usage: "Silent mode",
Destination: &prettyFlag,
Destination: &silentFlag,
},
}
// Initialize CLI flags commands
Expand Down Expand Up @@ -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"},
Expand Down
Loading

0 comments on commit c72ecd2

Please sign in to comment.