Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding osctrl-api component #28

Merged
merged 5 commits into from
Nov 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ ADMIN_DIR = cmd/admin
ADMIN_NAME = osctrl-admin
ADMIN_CODE = ${ADMIN_DIR:=/*.go}

API_DIR = cmd/api
API_NAME = osctrl-api
API_CODE = ${API_DIR:=/*.go}

CLI_DIR = cmd/cli
CLI_NAME = osctrl-cli
CLI_CODE = ${CLI_DIR:=/*.go}
Expand All @@ -30,6 +34,7 @@ build:
make plugins
make tls
make admin
make api
make cli

# Build TLS endpoint
Expand All @@ -40,6 +45,10 @@ tls:
admin:
go build -o $(OUTPUT)/$(ADMIN_NAME) $(ADMIN_CODE)

# Build API
api:
go build -o $(OUTPUT)/$(API_NAME) $(API_CODE)

# Build the CLI
cli:
go build -o $(OUTPUT)/$(CLI_NAME) $(CLI_CODE)
Expand All @@ -55,6 +64,7 @@ plugins:
clean:
rm -rf $(OUTPUT)/$(TLS_NAME)
rm -rf $(OUTPUT)/$(ADMIN_NAME)
rm -rf $(OUTPUT)/$(API_NAME)
rm -rf $(OUTPUT)/$(CLI_NAME)
rm -rf $(PLUGINS_DIR)/*.so

Expand All @@ -70,6 +80,7 @@ install:
make build
make install_tls
make install_admin
make install_api
make install_cli

# Install TLS server and restart service
Expand All @@ -86,6 +97,13 @@ install_admin:
sudo cp $(OUTPUT)/$(ADMIN_NAME) $(DEST)
sudo systemctl start $(ADMIN_NAME)

# Install API server and restart service
# optional DEST=destination_path
install_api:
sudo systemctl stop $(API_NAME)
sudo cp $(OUTPUT)/$(API_NAME) $(DEST)
sudo systemctl start $(API_NAME)

# Install CLI
# optional DEST=destination_path
install_cli:
Expand All @@ -99,6 +117,10 @@ logs_tls:
logs_admin:
sudo journalctl -f -t $(ADMIN_NAME)

# Display systemd logs for API server
logs_api:
sudo journalctl -f -t $(API_NAME)

# Build docker containers and run them (also generates new certificates)
docker_all:
./docker/dockerize.sh -u -b -f
Expand Down Expand Up @@ -131,6 +153,9 @@ gofmt-tls:
gofmt-admin:
gofmt $(GOFMT_ARGS) ./$(ADMIN_CODE)

gofmt-api:
gofmt $(GOFMT_ARGS) ./$(API_CODE)

gofmt-cli:
gofmt $(GOFMT_ARGS) ./$(CLI_CODE)

Expand All @@ -148,8 +173,12 @@ test:
cd $(TLS_DIR) && go test . -v
# Install dependencies for Admin
cd $(ADMIN_DIR) && go test -i . -v
# Run TLS tests
# Run Admin tests
cd $(ADMIN_DIR) && go test . -v
# Install dependencies for API
cd $(API_DIR) && go test -i . -v
# Run API tests
cd $(API_DIR) && go test . -v
# Install dependencies for CLI
cd $(CLI_DIR) && go test -i . -v
# Run CLI tests
Expand Down
8 changes: 5 additions & 3 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@

VAGRANTFILE_API_VERSION = "2"

IP_ADDRESS = "10.10.10.6"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is hardcoding this ok or should we use hostnames (or configurable values)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the IP address for the vagrant machine. There is no way (that I know of) to configure this via a command and is better to have the IP set to something known. Since the flow is to run vagrant up this should be fine.


Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/bionic64"
config.vm.network "private_network", ip: "10.10.10.6"
config.vm.network "private_network", ip: IP_ADDRESS
# If we want to enroll nodes in the same network
#config.vm.network "forwarded_port", guest: 443, host: 443
config.vm.hostname = "osctrl-Dev"
config.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'"
config.vm.provision "shell" do |s|
s.path = "deploy/provision.sh"
s.args = [
"--nginx", "--postgres", "-E", "--metrics", "--tls-hostname",
"10.10.10.6", "--admin-hostname", "10.10.10.6", "--password", "admin"
"--nginx", "--postgres", "-E", "--metrics", "--all-hostname",
IP_ADDRESS, "--password", "admin"
]
privileged = false
end
Expand Down
2 changes: 1 addition & 1 deletion cmd/admin/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type UserSession struct {
IPAddress string
UserAgent string
ExpiresAt time.Time
Cookie string `gorm:"index"`
Cookie string `gorm:"index"`
Values sessionValues `gorm:"-"`
}

Expand Down
1 change: 1 addition & 0 deletions cmd/admin/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func generateCarveQuery(file string, glob bool) string {
return "SELECT * FROM carves WHERE carve=1 AND path = '" + file + "';"
}

// Helper to verify if a platform is valid
func checkValidPlatform(platform string) bool {
platforms, err := nodesmgr.GetAllPlatforms()
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions cmd/api/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"fmt"
"log"
"time"

"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/jmpsec/osctrl/pkg/types"
"github.com/spf13/viper"
)

// Function to load the DB configuration file and assign to variables
func loadDBConfiguration(file string) (types.JSONConfigurationDB, error) {
var config types.JSONConfigurationDB
log.Printf("Loading %s", file)
// Load file and read config
viper.SetConfigFile(file)
err := viper.ReadInConfig()
if err != nil {
return config, err
}
// Backend values
dbRaw := viper.Sub("db")
err = dbRaw.Unmarshal(&config)
if err != nil {
return config, err
}
// No errors!
return config, nil
}

// Get PostgreSQL DB using GORM
func getDB(file string) *gorm.DB {
// Load DB configuration
dbConfig, err := loadDBConfiguration(file)
if err != nil {
log.Fatalf("Error loading DB configuration %v", err)
}
t := "host=%s port=%s dbname=%s user=%s password=%s sslmode=disable"
postgresDSN := fmt.Sprintf(
t, dbConfig.Host, dbConfig.Port, dbConfig.Name, dbConfig.Username, dbConfig.Password)
db, err := gorm.Open("postgres", postgresDSN)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
// Performance settings for DB access
db.DB().SetMaxIdleConns(dbConfig.MaxIdleConns)
db.DB().SetMaxOpenConns(dbConfig.MaxOpenConns)
db.DB().SetConnMaxLifetime(time.Second * time.Duration(dbConfig.ConnMaxLifetime))

return db
}

// Automigrate of tables
func automigrateDB() error {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this supposed to do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The automigrateDB() function is used by the ORM to create the schema based on the data stored. It is not doing much now, but if we decide to store something in the DB, this will come handy. I am thinking keeping requests per token, so we can enable/enforce some rate limiting?

var err error
return err
}
55 changes: 55 additions & 0 deletions cmd/api/handlers-environments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/jmpsec/osctrl/pkg/settings"
"github.com/jmpsec/osctrl/pkg/utils"
)

// GET Handler for single JSON environment
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean this API is used to validate if an environment exists in the backend?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and returns all data for that environment. Does it make more sense to have a verb to check on a specific environment, passing the name, and return just a boolean?

func apiEnvironmentHandler(w http.ResponseWriter, r *http.Request) {
incMetric(metricAPIReq)
utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false)
vars := mux.Vars(r)
// Extract name
name, ok := vars["name"]
if !ok {
incMetric(metricAPIErr)
apiErrorResponse(w, "error getting name", nil)
return
}
// Get environment by name
env, err := envs.Get(name)
if err != nil {
incMetric(metricAPIErr)
if err.Error() == "record not found" {
log.Printf("environment not found: %s", name)
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusNotFound, ApiErrorResponse{Error: "environment not found"})
} else {
apiErrorResponse(w, "error getting environment", err)
}
return
}
// Header to serve JSON
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusOK, env)
incMetric(metricAPIOK)
}

// GET Handler for multiple JSON environments
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this returns all environments?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yessir.

func apiEnvironmentsHandler(w http.ResponseWriter, r *http.Request) {
incMetric(metricAPIReq)
utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false)
// Get platforms
envAll, err := envs.All()
if err != nil {
incMetric(metricAPIErr)
apiErrorResponse(w, "error getting environments", err)
return
}
// Header to serve JSON
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusOK, envAll)
incMetric(metricAPIOK)
}
61 changes: 61 additions & 0 deletions cmd/api/handlers-nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/jmpsec/osctrl/pkg/settings"
"github.com/jmpsec/osctrl/pkg/utils"
)

// GET Handler for single JSON nodes
func apiNodeHandler(w http.ResponseWriter, r *http.Request) {
incMetric(metricAPIReq)
utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false)
vars := mux.Vars(r)
// Extract uuid
uuid, ok := vars["uuid"]
if !ok {
incMetric(metricAPIErr)
apiErrorResponse(w, "error getting uuid", nil)
return
}
// Get node by UUID
node, err := nodesmgr.GetByUUID(uuid)
if err != nil {
incMetric(metricAPIErr)
if err.Error() == "record not found" {
log.Printf("node not found: %s", uuid)
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusNotFound, ApiErrorResponse{Error: "node not found"})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is what this returns a similar JSON format to successful run? If so that'll make it easier to parse in prospective clients.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the requested node exists, it does return the structure for OsqueryNode{}, with all its fields in it. Does it make more sense to return always a JSON response with one field for message, and another for data?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that should be fine, a client can easily check that the fields are empty :)

} else {
apiErrorResponse(w, "error getting node", err)
}
return
}
// Serialize and serve JSON
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusOK, node)
incMetric(metricAPIOK)
}

// GET Handler for multiple JSON nodes
func apiNodesHandler(w http.ResponseWriter, r *http.Request) {
incMetric(metricAPIReq)
utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false)
// Get nodes
nodes, err := nodesmgr.Gets("all", 0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems an odd API to use over a more easily locked down nodesmgr.GetAllNodes() especially given that GetAllPlatforms() exists.

Any reason why you're doing it this way?

if err != nil {
incMetric(metricAPIErr)
apiErrorResponse(w, "error getting nodes", err)
return
}
if len(nodes) == 0 {
incMetric(metricAPIErr)
log.Printf("no nodes")
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusNotFound, ApiErrorResponse{Error: "no nodes"})
return
}
// Serialize and serve JSON
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusOK, nodes)
incMetric(metricAPIOK)
}
24 changes: 24 additions & 0 deletions cmd/api/handlers-platforms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"net/http"

"github.com/jmpsec/osctrl/pkg/settings"
"github.com/jmpsec/osctrl/pkg/utils"
)

// GET Handler for multiple JSON platforms
func apiPlatformsHandler(w http.ResponseWriter, r *http.Request) {
incMetric(metricAPIReq)
utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false)
// Get platforms
platforms, err := nodesmgr.GetAllPlatforms()
if err != nil {
incMetric(metricAPIErr)
apiErrorResponse(w, "error getting platforms", err)
return
}
// Serialize and serve JSON
apiHTTPResponse(w, JSONApplicationUTF8, http.StatusOK, platforms)
incMetric(metricAPIOK)
}
Loading