Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
albertollamaso committed Nov 11, 2022
1 parent 8470d2c commit 39bf423
Show file tree
Hide file tree
Showing 14 changed files with 620 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @albertollamaso
15 changes: 15 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
All Submissions:

* [ ] Have you followed the guidelines in our Contributing document?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change?

### New Feature Submissions:

1. [ ] Does your submission pass tests?
2. [ ] Have you lint your code locally before submission?

### Changes to Core Features:

* [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
* [ ] Have you written new tests for your core changes, as applicable?
* [ ] Have you successfully run tests with your changes locally?
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

# Falco config file
config/

# Plugin binary
libnomad.so

# IDE
.vscode
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SHELL=/bin/bash -o pipefail
GO ?= go

NAME := nomad
OUTPUT := lib$(NAME).so

ifeq ($(DEBUG), 1)
GODEBUGFLAGS= GODEBUG=cgocheck=2
else
GODEBUGFLAGS= GODEBUG=cgocheck=0
endif

all: $(OUTPUT)

clean:
@rm -f $(OUTPUT)

$(OUTPUT):
@$(GODEBUGFLAGS) $(GO) build -buildmode=c-shared -o $(OUTPUT) ./plugin
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Falcosecurity Nomad Plugin

This repositry contains the Nomad plugin, which can fetch event stream containing [nomad](https://www.nomadproject.io/) events, parse the events, and emit sinsp/scap events (e.g. the events used by Falco) for each nomad event.

## Event Source
The event source for nomad events is the `/event/stream` endpoint used to stream events generated by Nomad.

## Supported Fields
Here is the current set of supported fields:


| NAME | TYPE | ARG | DESCRIPTION |
|-----------------------------------|----------|------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `nomad.index` | `uint64` | None | The index of the nomad event. |
| `nomad.alloc.name` | `string` | None | The name of the nomad allocation. |
| `nomad.alloc.namespace` | `string` | None | The namespace of the allocation. |
| `nomad.alloc.jobID` | `string` | None | The job ID of the allocation. |
| `nomad.alloc.clientStatus` | `string` | None | The client status of the allocation. |
| `nomad.alloc.images` | `string (list)` | None | The list of container images on allocations. |
| `nomad.alloc.images.tags` | `string (list)` | None | The tags of each container image on allocations. |
| `nomad.alloc.images.repositories` | `string (list)` | None | The container repositories used on allocations container images. |
| `nomad.alloc.taskStates.type` | `string (list)` | None | The state of the task on the allocations. |
| `nomad.alloc.res.cpu` | `uint64` | None | The CPU required to run this allocation in MHz. |
| `nomad.alloc.res.cores` | `uint64` | None | The number of CPU cores to reserve for the allocation. |
| `nomad.alloc.res.diskMB` | `uint64` | None | the amount of disk required for the allocation. |
| `nomad.alloc.res.iops` | `uint64` | None | the number of iops required for the allocation. |
| `nomad.alloc.res.memoryMB` | `uint64` | None | The memory required in MB for the allocation. |
| `nomad.alloc.res.memoryMaxMB` | `uint64` | None | The maximum memory the allocation may use. |
| `nomad.event.topic` | `string` | None | The topic of the nomad event. |
| `nomad.event.type` | `string` | None | The type of the nomad event. |
## Configuration


### `falco.yaml` Example

```yaml
plugins:
- name: nomad
library_path: libnomad.so
init_config:
address: http://127.0.0.1:4646
token: ""
namespace: "*"

# Optional. If not specified the first entry in plugins is used.
load_plugins: [nomad, json]
```
19 changes: 19 additions & 0 deletions pkg/nomad/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nomad

// Defining a type for the plugin configuration.
// In this simple example, users can define the starting value the event
// counter. the `jsonschema` tags is used to automatically generate a
// JSON Schema definition, so that the framework can perform automatic
// validations.
type PluginConfig struct {
Address string `json:"address" jsonschema:"title=Nomad address,description=The address of the Nomad server.,default=http://localhost:4646"`
Token string `json:"token" jsonschema:"title=Nomad token,description=The token to use to connect to the Nomad server.,default="`
Namespace string `json:"namespace" jsonschema:"title=Nomad namespace,description=The namespace to use to connect to the Nomad server.,default=*"`
}

// Resets sets the configuration to its default values
func (p *PluginConfig) Reset() {
p.Address = "http://localhost:4646"
p.Token = ""
p.Namespace = "*"
}
112 changes: 112 additions & 0 deletions pkg/nomad/extract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package nomad

import (
"encoding/json"
"fmt"
"strconv"

"github.com/falcosecurity/plugin-sdk-go/pkg/sdk"
"github.com/hashicorp/nomad/api"
)

// This method is mandatory the field extraction capability.
// If the Extract method is defined, the framework expects an Fields method
// to be specified too.
func (p *Plugin) Extract(req sdk.ExtractRequest, evt sdk.EventReader) error {
var event api.Event
encoder := json.NewDecoder(evt.Reader())
if err := encoder.Decode(&event); err != nil {
return err
}

switch req.Field() {
case "nomad.index":
req.SetValue(event.Index)
case "nomad.alloc.name":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(string(alloc.Name))
}
case "nomad.alloc.namespace":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(string(alloc.Namespace))
}
case "nomad.alloc.jobID":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(string(alloc.JobID))
}
case "nomad.alloc.clientStatus":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(string(alloc.ClientStatus))
}
case "nomad.alloc.taskStates.type":
var driver []string
alloc, err := event.Allocation()
if err == nil {
for _, task := range alloc.TaskStates {
for _, taskEvent := range task.Events {
if taskEvent.Type == "Driver" {
driver = append(driver, taskEvent.Type)
}
}
}
}
req.SetValue(driver)
case "nomad.alloc.res.cpu":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(uint64(*alloc.Resources.CPU))
}
case "nomad.alloc.res.cores":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(uint64(*alloc.Resources.Cores))
}
case "nomad.alloc.res.diskMB":
alloc := event.Payload["Allocation"]
valStr := fmt.Sprintf("%v", alloc.(map[string]interface{})["Resources"].(map[string]interface{})["DiskMB"]) // bug in nomad api. event.Allocation() returns <nil> for alloc.Resources.DiskMB
value, _ := strconv.ParseUint(valStr, 10, 64)
req.SetValue(value)
case "nomad.alloc.res.iops":
alloc, err := event.Allocation()
if err == nil {
req.SetValue(uint64(*alloc.Resources.IOPS))
}
case "nomad.alloc.res.memoryMB":
alloc := event.Payload["Allocation"]
valStr := fmt.Sprintf("%v", alloc.(map[string]interface{})["Resources"].(map[string]interface{})["MemoryMB"]) // bug in nomad api. event.Allocation() returns <nil> for alloc.Resources.MemoryMB
value, _ := strconv.ParseUint(valStr, 10, 64)
req.SetValue(value)
case "nomad.alloc.res.memoryMaxMB":
alloc := event.Payload["Allocation"]
valStr := fmt.Sprintf("%v", alloc.(map[string]interface{})["Resources"].(map[string]interface{})["MemoryMaxMB"]) // bug in nomad api. event.Allocation() returns <nil> for alloc.Resources.MemoryMaxMB
value, _ := strconv.ParseUint(valStr, 10, 64)
req.SetValue(value)
case "nomad.alloc.images":
images, err := getAllocImages(&event)
if err == nil {
req.SetValue(images)
}
case "nomad.alloc.images.tags":
tags, err := getAllocTags(&event)
if err == nil {
req.SetValue(tags)
}
case "nomad.alloc.images.repositories":
repos, err := getAllocRepos(&event)
if err == nil {
req.SetValue(repos)
}
case "nomad.event.topic":
req.SetValue(string(event.Topic))
case "nomad.event.type":
req.SetValue(event.Type)
default:
return fmt.Errorf("unsupported field: %s", req.Field())
}

return nil
}
31 changes: 31 additions & 0 deletions pkg/nomad/fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package nomad

import (
"github.com/falcosecurity/plugin-sdk-go/pkg/sdk"
)

// Fields return the list of extractor fields exported by this plugin.
// This method is mandatory the field extraction capability.
// If the Fields method is defined, the framework expects an Extract method
// to be specified too.
func (p *Plugin) Fields() []sdk.FieldEntry {
return []sdk.FieldEntry{
{Type: "uint64", Name: "nomad.index", Display: "Event index", Desc: "the index of the nomad event."},
{Type: "string", Name: "nomad.alloc.name", Display: "Allocation name", Desc: "the name of the nomad allocation."},
{Type: "string", Name: "nomad.alloc.namespace", Display: "Allocation namespace", Desc: "the namespace of the allocation."},
{Type: "string", Name: "nomad.alloc.jobID", Display: "Allocation Job ID", Desc: "the job ID of the allocation."},
{Type: "string", Name: "nomad.alloc.clientStatus", Display: "Allocation client status", Desc: "the client status of the allocation."},
{Type: "string", Name: "nomad.alloc.images", Display: "Allocation container images", Desc: "the list of container images on allocations.", IsList: true},
{Type: "string", Name: "nomad.alloc.images.tags", Display: "Allocation container tags", Desc: "the tags of each container image on allocations.", IsList: true},
{Type: "string", Name: "nomad.alloc.images.repositories", Display: "Allocation container repositories", Desc: "the container repositories used on allocations container images.", IsList: true},
{Type: "string", Name: "nomad.alloc.taskStates.type", Display: "Allocation Task State", Desc: "the state of the task on the allocations.", IsList: true},
{Type: "uint64", Name: "nomad.alloc.res.cpu", Display: "Allocation CPU Resources", Desc: "the CPU required to run this allocation in MHz."},
{Type: "uint64", Name: "nomad.alloc.res.cores", Display: "Allocation CPU Cores Resources", Desc: "the number of CPU cores to reserve for the allocation."},
{Type: "uint64", Name: "nomad.alloc.res.diskMB", Display: "Allocation Disk in MB Resources", Desc: "the amount of disk required for the allocation."},
{Type: "uint64", Name: "nomad.alloc.res.iops", Display: "Allocation IOPS Resources", Desc: "the number of iops required for the allocation."},
{Type: "uint64", Name: "nomad.alloc.res.memoryMB", Display: "Allocation Memory in MB Resources", Desc: "the memory required in MB for the allocation."},
{Type: "uint64", Name: "nomad.alloc.res.memoryMaxMB", Display: "Allocation Max Memory in MB Resources", Desc: "the maximum memory the allocation may use."},
{Type: "string", Name: "nomad.event.topic", Display: "Nomad Event Topic", Desc: "the topic of the nomad event."},
{Type: "string", Name: "nomad.event.type", Display: "Nomad Event type", Desc: "the type of the nomad event."},
}
}
87 changes: 87 additions & 0 deletions pkg/nomad/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package nomad

import (
"fmt"
"strings"

"github.com/hashicorp/nomad/api"
)

func getAllocImages(evt *api.Event) ([]string, error) {
alloc, err := evt.Allocation()
if err != nil {
return nil, err
}

var images []string
for _, task := range alloc.TaskStates {
for _, taskEvent := range task.Events {
image := taskEvent.Details["image"]
fmt.Println("------------------")
fmt.Println(image)
fmt.Println("------------------")
tokens := strings.Split(image, ":")
if strings.Contains(tokens[0], "/") {
// cases
// [registry]/[repository_name]/[repo_path_component]/[image]:[tag] (repository name could have two or more path components, they must be separated by a forward slash (“/”).)
// [registry]/[repository_name]/[image]:[tag]
// [registry]/[image]:[tag]
tokens = strings.Split(tokens[0], "/")
images = append(images, tokens[len(tokens)-1])
} else {
// case [image]:[tag]
images = append(images, tokens[0])
}
}
}

return images, nil
}

func getAllocTags(evt *api.Event) ([]string, error) {
alloc, err := evt.Allocation()
if err != nil {
return nil, err
}

var tags []string
for _, task := range alloc.TaskStates {
for _, taskEvent := range task.Events {
tokens := strings.Split(taskEvent.Details["image"], ":")
tags = append(tags, tokens[len(tokens)-1])
}
}

return tags, nil
}

func getAllocRepos(evt *api.Event) ([]string, error) {
alloc, err := evt.Allocation()
if err != nil {
return nil, err
}

var repos []string
for _, task := range alloc.TaskStates {
for _, taskEvent := range task.Events {
tokens := strings.Split(taskEvent.Details["image"], ":")
if len(tokens) == 2 {
if strings.Contains(tokens[0], "/") {
// cases
// [registry]/[repository_name]/[repo_path_component]/[image]:[tag] (repository name could have two or more path components, they must be separated by a forward slash (“/”).)
// [registry]/[repository_name]/[image]:[tag]
// [registry]/[image]:[tag]
tokens = strings.Split(tokens[0], "/")
tokens = tokens[:len(tokens)-1]
tokenStr := strings.Join(tokens, "/")
repos = append(repos, tokenStr)
} else {
// case [image]:[tag]
repos = append(repos, "docker.io")
}
}
}
}

return repos, nil
}
Loading

0 comments on commit 39bf423

Please sign in to comment.