diff --git a/cmd/api/handlers/nodes.go b/cmd/api/handlers/nodes.go index 31ee629e..1854e29e 100644 --- a/cmd/api/handlers/nodes.go +++ b/cmd/api/handlers/nodes.go @@ -217,3 +217,37 @@ func (h *HandlersApi) DeleteNodeHandler(w http.ResponseWriter, r *http.Request) log.Debug().Msgf("Returned node %s", n.UUID) utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, types.ApiGenericResponse{Message: "node deleted"}) } + +// LookupNodeHandler - POST Handler to lookup a node by identifier +func (h *HandlersApi) LookupNodeHandler(w http.ResponseWriter, r *http.Request) { + // Debug HTTP if enabled + if h.DebugHTTPConfig.Enabled { + utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody) + } + // Get context data and check access + ctx := r.Context().Value(ContextKey(contextAPI)).(ContextValue) + if !h.Users.CheckPermissions(ctx[ctxUser], users.AdminLevel, users.NoEnvironment) { + apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser])) + return + } + var l types.ApiLookupRequest + // Parse request JSON body + if err := json.NewDecoder(r.Body).Decode(&l); err != nil { + apiErrorResponse(w, "error parsing POST body", http.StatusInternalServerError, err) + return + } + if l.Identifier == "" { + apiErrorResponse(w, "error with identifier", http.StatusBadRequest, nil) + return + } + n, err := h.Nodes.GetByIdentifier(l.Identifier) + if 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) + } + } + // Serialize and serve JSON + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, _nodeToApiLookupResponse(n)) +} diff --git a/cmd/api/handlers/utils.go b/cmd/api/handlers/utils.go index 83fffd4d..ff725889 100644 --- a/cmd/api/handlers/utils.go +++ b/cmd/api/handlers/utils.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/jmpsec/osctrl/pkg/logging" + "github.com/jmpsec/osctrl/pkg/nodes" "github.com/jmpsec/osctrl/pkg/types" "github.com/jmpsec/osctrl/pkg/utils" "github.com/rs/zerolog/log" @@ -53,3 +54,20 @@ func checkValidPlatform(platforms []string, platform string) bool { } return false } + +// Helper to convert a node into a ApiLookupResponse +func _nodeToApiLookupResponse(node nodes.OsqueryNode) types.ApiLookupResponse { + return types.ApiLookupResponse{ + UUID: node.UUID, + Platform: node.Platform, + PlatformVersion: node.PlatformVersion, + OsqueryVersion: node.OsqueryVersion, + Hostname: node.Hostname, + Localname: node.Localname, + IPAddress: node.IPAddress, + Username: node.Username, + Environment: node.Environment, + HardwareSerial: node.HardwareSerial, + LastSeen: node.LastSeen.String(), + } +} diff --git a/cmd/api/main.go b/cmd/api/main.go index f9a53435..7f30a0de 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -239,6 +239,9 @@ func osctrlAPIService() { muxAPI.Handle( "POST "+_apiPath(apiNodesPath)+"/{env}/delete", handlerAuthCheck(http.HandlerFunc(handlersApi.DeleteNodeHandler), flagParams.ConfigValues.Auth, flagParams.JWTConfigValues.JWTSecret)) + muxAPI.Handle( + "POST "+_apiPath(apiNodesPath)+"/lookup", + handlerAuthCheck(http.HandlerFunc(handlersApi.LookupNodeHandler), flagParams.ConfigValues.Auth, flagParams.JWTConfigValues.JWTSecret)) // API: queries by environment muxAPI.Handle( "GET "+_apiPath(apiQueriesPath)+"/{env}", diff --git a/osctrl-api.yaml b/osctrl-api.yaml index 8767fd1d..a2e0dcee 100644 --- a/osctrl-api.yaml +++ b/osctrl-api.yaml @@ -304,6 +304,52 @@ paths: security: - Authorization: - admin + /nodes/lookup: + post: + tags: + - nodes + summary: Lookup node by identifier + description: Looks up an enrolled node by identifier (UUID, hostname or localname) + operationId: LookupNodeHandler + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ApiLookupRequest" + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiLookupResponse" + 400: + description: bad request + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + 403: + description: no access + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + 404: + description: no nodes + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + 500: + description: error deleting node + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + security: + - Authorization: + - admin /queries/{env}: get: tags: @@ -2412,6 +2458,41 @@ components: TagType: type: integer format: uint32 + ApiLookupRequest: + type: object + properties: + Identifier: + type: string + ApiLookupResponse: + type: object + properties: + ID: + type: integer + format: int32 + UUID: + type: string + Hostname: + type: string + Localname: + type: string + IPAddress: + type: string + Username: + type: string + HardwareSerial: + type: string + Platform: + type: string + PlatformVersion: + type: string + OsqueryVersion: + type: string + Environment: + type: string + EnvironmentUUID: + type: string + LastSeen: + type: string MapEnvByID: type: object properties: diff --git a/pkg/types/types.go b/pkg/types/types.go index 25210bb4..1d681aad 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -109,3 +109,25 @@ type ApiTagsRequest struct { EnvUUID string `json:"env_uuid"` TagType uint `json:"tagtype"` } + +// ApiLookupRequest to receive lookup requests +type ApiLookupRequest struct { + Identifier string `json:"identifier"` +} + +// ApiLookupResponse to be returned to API lookup requests +type ApiLookupResponse struct { + ID uint `json:"id"` + UUID string `json:"uuid"` + Hostname string `json:"hostname"` + Localname string `json:"localname"` + IPAddress string `json:"ip_address"` + Username string `json:"username"` + HardwareSerial string `json:"hardware_serial"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + OsqueryVersion string `json:"osquery_version"` + Environment string `json:"environment"` + EnvironmentUUID string `json:"environment_uuid"` + LastSeen string `json:"last_seen"` +}