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:
Zachary Wasserman
2016-08-11 13:50:03 -07:00
committed by GitHub
parent 4db4e95b38
commit 809a010a1d
8 changed files with 356 additions and 14 deletions
+77 -5
View File
@@ -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,
})
}
+143
View File
@@ -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)
}
}
+93
View File
@@ -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")
}
}
+20
View File
@@ -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
View File
@@ -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
}
-2
View File
@@ -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())
+2 -1
View File
@@ -17,6 +17,7 @@
"session_key_size": 64
},
"osquery": {
"enroll_secret": "super secure"
"enroll_secret": "super secure",
"node_key_size": 24
}
}
+1
View File
@@ -5,3 +5,4 @@
--tls_hostname=dockerhost:8080
--tls_server_certs=/etc/osquery/kolide.crt
--verbose=true
--host_identifier=uuid