Initial implementation of osqueryd enrollment + tests (#36)
*EnrollHost now generates a node key and stores host information into the DB * Unit and integration tests Closes #6
This commit is contained in:
committed by
GitHub
parent
4db4e95b38
commit
809a010a1d
+77
-5
@@ -1,11 +1,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/kolide/kolide-ose/config"
|
||||
"github.com/kolide/kolide-ose/errors"
|
||||
)
|
||||
|
||||
@@ -146,10 +149,9 @@ type Decorator struct {
|
||||
Query string
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
type OsqueryEnrollPostBody struct {
|
||||
EnrollSecret string `json:"enroll_secret" validate:"required"`
|
||||
EnrollSecret string `json:"enroll_secret" validate:"required"`
|
||||
HostIdentifier string `json:"host_identifier" validate:"required"`
|
||||
}
|
||||
|
||||
type OsqueryConfigPostBody struct {
|
||||
@@ -188,6 +190,56 @@ type OsqueryDistributedWritePostBody struct {
|
||||
Queries map[string][]map[string]string `json:"queries" validate:"required"`
|
||||
}
|
||||
|
||||
// Generate a node key using NodeKeySize random bytes Base64 encoded
|
||||
func newNodeKey() (string, error) {
|
||||
return generateRandomText(config.Osquery.NodeKeySize)
|
||||
}
|
||||
|
||||
// Enroll a host. Even if this is an existing host, a new node key should be
|
||||
// generated and saved to the DB.
|
||||
func EnrollHost(db *gorm.DB, uuid, hostName, ipAddress, platform string) (*Host, error) {
|
||||
host := Host{UUID: uuid}
|
||||
err := db.Where(&host).First(&host).Error
|
||||
if err != nil {
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
// Create new Host
|
||||
host = Host{
|
||||
UUID: uuid,
|
||||
HostName: hostName,
|
||||
IPAddress: ipAddress,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new key each enrollment
|
||||
host.NodeKey, err = newNodeKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update these fields if provided
|
||||
if hostName != "" {
|
||||
host.HostName = hostName
|
||||
}
|
||||
if ipAddress != "" {
|
||||
host.IPAddress = ipAddress
|
||||
}
|
||||
if platform != "" {
|
||||
host.Platform = platform
|
||||
}
|
||||
|
||||
if err := db.Save(&host).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
func OsqueryEnroll(c *gin.Context) {
|
||||
var body OsqueryEnrollPostBody
|
||||
err := ParseAndValidateJSON(c, &body)
|
||||
@@ -195,11 +247,31 @@ func OsqueryEnroll(c *gin.Context) {
|
||||
errors.ReturnError(c, err)
|
||||
return
|
||||
}
|
||||
logrus.Debugf("OsqueryEnroll: %s", body.EnrollSecret)
|
||||
|
||||
if body.EnrollSecret != config.Osquery.EnrollSecret {
|
||||
errors.ReturnError(
|
||||
c,
|
||||
errors.NewWithStatus(http.StatusUnauthorized,
|
||||
"Node key invalid",
|
||||
fmt.Sprintf("Invalid node secret provided: %s", body.EnrollSecret),
|
||||
))
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
db := GetDB(c)
|
||||
|
||||
host, err := EnrollHost(db, body.HostIdentifier, "", "", "")
|
||||
if err != nil {
|
||||
errors.ReturnError(c, errors.DatabaseError(err))
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Debugf("New host created: %+v", host)
|
||||
|
||||
c.JSON(http.StatusOK,
|
||||
gin.H{
|
||||
"node_key": "7",
|
||||
"node_key": host.NodeKey,
|
||||
"node_invalid": false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/kolide/kolide-ose/config"
|
||||
"github.com/kolide/kolide-ose/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIntegrationEnrollHostBadSecret(t *testing.T) {
|
||||
var req IntegrationRequests
|
||||
req.New(t)
|
||||
|
||||
config.Osquery.EnrollSecret = "super secret"
|
||||
|
||||
// Check that a bad enroll secret causes the appropriate error code and
|
||||
// error JSON to be returned
|
||||
|
||||
resp := req.EnrollHost("bad secret", "fake_uuid")
|
||||
|
||||
if resp.Code != http.StatusUnauthorized {
|
||||
t.Error("Should error with invalid enroll secret")
|
||||
}
|
||||
|
||||
var body map[string]interface{}
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &body)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON decode error: %s JSON contents:\n %s", err.Error(), resp.Body.Bytes())
|
||||
}
|
||||
|
||||
if _, ok := body["node_key"]; ok {
|
||||
t.Errorf("Should not return node key when secret is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEnrollHostMissingIdentifier(t *testing.T) {
|
||||
var req IntegrationRequests
|
||||
req.New(t)
|
||||
|
||||
config.Osquery.EnrollSecret = "super secret"
|
||||
|
||||
// Check that an empty host identifier causes the appropriate error code and
|
||||
// error JSON to be returned
|
||||
|
||||
resp := req.EnrollHost("super secret", "")
|
||||
|
||||
if resp.Code != errors.StatusUnprocessableEntity {
|
||||
t.Error("Should error with missing host identifier")
|
||||
}
|
||||
|
||||
var body map[string]interface{}
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &body)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON decode error: %s JSON contents:\n %s", err.Error(), resp.Body.Bytes())
|
||||
}
|
||||
|
||||
assert.Equal(t, "Validation error", body["message"])
|
||||
}
|
||||
|
||||
func TestIntegrationEnrollHostGood(t *testing.T) {
|
||||
var req IntegrationRequests
|
||||
req.New(t)
|
||||
|
||||
config.Osquery.EnrollSecret = "super secret"
|
||||
|
||||
// Make a good request and verify that a node key is returned. Also
|
||||
// check that the DB recorded the information.
|
||||
|
||||
resp := req.EnrollHost("super secret", "fake_host_1")
|
||||
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error("Status should be StatusOK")
|
||||
}
|
||||
|
||||
t.Logf("Response body:\n%s", string(resp.Body.Bytes()))
|
||||
|
||||
var body map[string]interface{}
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &body)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON decode error: %s JSON contents:\n %s", err.Error(), resp.Body.Bytes())
|
||||
}
|
||||
|
||||
if _, ok := body["error"]; ok {
|
||||
t.Errorf("Unexpected error message: %s", body["error"])
|
||||
}
|
||||
|
||||
if invalid, ok := body["node_invalid"]; ok && invalid == true {
|
||||
t.Errorf("Expected node_invalid = false")
|
||||
}
|
||||
|
||||
nodeKey, ok := body["node_key"]
|
||||
if !ok || nodeKey == "" {
|
||||
t.Errorf("Expected node_key")
|
||||
}
|
||||
|
||||
var host Host
|
||||
err = req.db.Where("uuid = ?", "fake_host_1").First(&host).Error
|
||||
if err != nil {
|
||||
t.Fatalf("Host not saved to DB: %s", err.Error())
|
||||
}
|
||||
|
||||
if host.NodeKey != nodeKey {
|
||||
t.Errorf("Saved node key different than response key, %s != %s",
|
||||
host.NodeKey, nodeKey)
|
||||
}
|
||||
|
||||
// Enroll again and check that node key changes
|
||||
|
||||
resp = req.EnrollHost("super secret", "fake_host_1")
|
||||
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error("Status should be StatusOK")
|
||||
}
|
||||
|
||||
t.Logf("Response body:\n%s", string(resp.Body.Bytes()))
|
||||
|
||||
body = map[string]interface{}{}
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &body)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON decode error: %s JSON contents:\n %s", err.Error(), resp.Body.Bytes())
|
||||
}
|
||||
|
||||
if _, ok := body["error"]; ok {
|
||||
t.Errorf("Unexpected error message: %s", body["error"])
|
||||
}
|
||||
|
||||
if invalid, ok := body["node_invalid"]; ok && invalid == true {
|
||||
t.Errorf("Expected node_invalid = false")
|
||||
}
|
||||
|
||||
newNodeKey, ok := body["node_key"]
|
||||
if !ok || nodeKey == "" {
|
||||
t.Errorf("Expected node_key")
|
||||
}
|
||||
|
||||
if newNodeKey == nodeKey {
|
||||
t.Errorf("Node key should have changed, %s == %s", newNodeKey, nodeKey)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnrollHost(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
|
||||
expect := Host{
|
||||
UUID: "uuid123",
|
||||
HostName: "fakehostname",
|
||||
IPAddress: "192.168.1.1",
|
||||
Platform: "Mac OSX",
|
||||
}
|
||||
|
||||
host, err := EnrollHost(db, expect.UUID, expect.HostName, expect.IPAddress, expect.Platform)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if host.UUID != expect.UUID {
|
||||
t.Errorf("UUID not as expected: %s != %s", host.UUID, expect.UUID)
|
||||
}
|
||||
|
||||
if host.HostName != expect.HostName {
|
||||
t.Errorf("HostName not as expected: %s != %s", host.HostName, expect.HostName)
|
||||
}
|
||||
|
||||
if host.IPAddress != expect.IPAddress {
|
||||
t.Errorf("IPAddress not as expected: %s != %s", host.IPAddress, expect.IPAddress)
|
||||
}
|
||||
|
||||
if host.Platform != expect.Platform {
|
||||
t.Errorf("Platform not as expected: %s != %s", host.Platform, expect.Platform)
|
||||
}
|
||||
|
||||
if host.NodeKey == "" {
|
||||
t.Error("Node key was not set")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReEnrollHost(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
|
||||
expect := Host{
|
||||
UUID: "uuid123",
|
||||
HostName: "fakehostname",
|
||||
IPAddress: "192.168.1.1",
|
||||
Platform: "Mac OSX",
|
||||
}
|
||||
|
||||
host, err := EnrollHost(db, expect.UUID, expect.HostName, expect.IPAddress, expect.Platform)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// Save the node key to check that it changed
|
||||
oldNodeKey := host.NodeKey
|
||||
|
||||
expect.HostName = "newhostname"
|
||||
|
||||
host, err = EnrollHost(db, expect.UUID, expect.HostName, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if host.UUID != expect.UUID {
|
||||
t.Errorf("UUID not as expected: %s != %s", host.UUID, expect.UUID)
|
||||
}
|
||||
|
||||
if host.HostName != expect.HostName {
|
||||
t.Errorf("HostName not as expected: %s != %s", host.HostName, expect.HostName)
|
||||
}
|
||||
|
||||
if host.IPAddress != expect.IPAddress {
|
||||
t.Errorf("IPAddress not as expected: %s != %s", host.IPAddress, expect.IPAddress)
|
||||
}
|
||||
|
||||
if host.Platform != expect.Platform {
|
||||
t.Errorf("Platform not as expected: %s != %s", host.Platform, expect.Platform)
|
||||
}
|
||||
|
||||
if host.NodeKey == "" {
|
||||
t.Error("Node key was not set")
|
||||
}
|
||||
|
||||
if host.NodeKey == oldNodeKey {
|
||||
t.Error("Node key should have changed")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -489,3 +489,23 @@ func (req *IntegrationRequests) SetAdminStateAndCheckUser(username string, admin
|
||||
resp := req.SetAdminState(username, admin, session)
|
||||
req.CheckUser(username, resp.Email, resp.Name, admin, resp.NeedsPasswordReset, resp.Enabled)
|
||||
}
|
||||
|
||||
func (req *IntegrationRequests) EnrollHost(enrollSecret, hostIdentifier string) *httptest.ResponseRecorder {
|
||||
response := httptest.NewRecorder()
|
||||
body, err := json.Marshal(OsqueryEnrollPostBody{
|
||||
EnrollSecret: enrollSecret,
|
||||
HostIdentifier: hostIdentifier,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
req.t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
buff.Write(body)
|
||||
request, _ := http.NewRequest("POST", "/api/v1/osquery/enroll", buff)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
req.r.ServeHTTP(response, request)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
+20
-6
@@ -27,10 +27,16 @@ type AppConfigData struct {
|
||||
SessionExpirationSeconds float64 `json:"session_expiration_seconds"`
|
||||
}
|
||||
|
||||
type OsqueryConfigData struct {
|
||||
EnrollSecret string `json:"enroll_secret"`
|
||||
NodeKeySize int `json:"node_key_size"`
|
||||
}
|
||||
|
||||
type configData struct {
|
||||
MySQL MySQLConfigData `json:"mysql"`
|
||||
Server ServerConfigData `json:"server"`
|
||||
App AppConfigData `json:"app"`
|
||||
MySQL MySQLConfigData `json:"mysql"`
|
||||
Server ServerConfigData `json:"server"`
|
||||
App AppConfigData `json:"app"`
|
||||
Osquery OsqueryConfigData `json:"osquery"`
|
||||
}
|
||||
|
||||
var defaultMySQLConfigData = MySQLConfigData{
|
||||
@@ -55,6 +61,11 @@ var defaultAppConfigData = AppConfigData{
|
||||
SessionExpirationSeconds: 60 * 60 * 24 * 90,
|
||||
}
|
||||
|
||||
var defaultOsqueryConfigData = OsqueryConfigData{
|
||||
EnrollSecret: "bad secret",
|
||||
NodeKeySize: 24,
|
||||
}
|
||||
|
||||
var defaultConfigData = configData{
|
||||
MySQL: defaultMySQLConfigData,
|
||||
Server: defaultServerConfigData,
|
||||
@@ -62,15 +73,17 @@ var defaultConfigData = configData{
|
||||
}
|
||||
|
||||
var (
|
||||
MySQL MySQLConfigData
|
||||
Server ServerConfigData
|
||||
App AppConfigData
|
||||
MySQL MySQLConfigData
|
||||
Server ServerConfigData
|
||||
App AppConfigData
|
||||
Osquery OsqueryConfigData
|
||||
)
|
||||
|
||||
func init() {
|
||||
MySQL = defaultMySQLConfigData
|
||||
Server = defaultServerConfigData
|
||||
App = defaultAppConfigData
|
||||
Osquery = defaultOsqueryConfigData
|
||||
}
|
||||
|
||||
func LoadConfig(path string) error {
|
||||
@@ -86,5 +99,6 @@ func LoadConfig(path string) error {
|
||||
MySQL = config.MySQL
|
||||
App = config.App
|
||||
Server = config.Server
|
||||
Osquery = config.Osquery
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -149,8 +149,6 @@ func TestReturnErrorValidationError(t *testing.T) {
|
||||
t.Errorf("Should respond with 422, got %d", resp.Code)
|
||||
}
|
||||
|
||||
t.Log(resp.Body.String())
|
||||
|
||||
var bodyJson map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &bodyJson); err != nil {
|
||||
t.Errorf("Error unmarshaling JSON: %s", err.Error())
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"session_key_size": 64
|
||||
},
|
||||
"osquery": {
|
||||
"enroll_secret": "super secure"
|
||||
"enroll_secret": "super secure",
|
||||
"node_key_size": 24
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
--tls_hostname=dockerhost:8080
|
||||
--tls_server_certs=/etc/osquery/kolide.crt
|
||||
--verbose=true
|
||||
--host_identifier=uuid
|
||||
Reference in New Issue
Block a user