Skip to content

Commit 66e0bc8

Browse files
authored
Merge pull request #1 from albertollamaso/init
init
2 parents 8470d2c + 39bf423 commit 66e0bc8

File tree

14 files changed

+620
-0
lines changed

14 files changed

+620
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @albertollamaso

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
All Submissions:
2+
3+
* [ ] Have you followed the guidelines in our Contributing document?
4+
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change?
5+
6+
### New Feature Submissions:
7+
8+
1. [ ] Does your submission pass tests?
9+
2. [ ] Have you lint your code locally before submission?
10+
11+
### Changes to Core Features:
12+
13+
* [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
14+
* [ ] Have you written new tests for your core changes, as applicable?
15+
* [ ] Have you successfully run tests with your changes locally?

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, built with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool, specifically when used with LiteIDE
12+
*.out
13+
14+
# Dependency directories (remove the comment below to include it)
15+
# vendor/
16+
17+
# Go workspace file
18+
go.work
19+
20+
# Falco config file
21+
config/
22+
23+
# Plugin binary
24+
libnomad.so
25+
26+
# IDE
27+
.vscode

Makefile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
SHELL=/bin/bash -o pipefail
2+
GO ?= go
3+
4+
NAME := nomad
5+
OUTPUT := lib$(NAME).so
6+
7+
ifeq ($(DEBUG), 1)
8+
GODEBUGFLAGS= GODEBUG=cgocheck=2
9+
else
10+
GODEBUGFLAGS= GODEBUG=cgocheck=0
11+
endif
12+
13+
all: $(OUTPUT)
14+
15+
clean:
16+
@rm -f $(OUTPUT)
17+
18+
$(OUTPUT):
19+
@$(GODEBUGFLAGS) $(GO) build -buildmode=c-shared -o $(OUTPUT) ./plugin

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Falcosecurity Nomad Plugin
2+
3+
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.
4+
5+
## Event Source
6+
The event source for nomad events is the `/event/stream` endpoint used to stream events generated by Nomad.
7+
8+
## Supported Fields
9+
Here is the current set of supported fields:
10+
11+
12+
| NAME | TYPE | ARG | DESCRIPTION |
13+
|-----------------------------------|----------|------|------------------------------------------------------------------------------------------------------------------------------------------------------|
14+
| `nomad.index` | `uint64` | None | The index of the nomad event. |
15+
| `nomad.alloc.name` | `string` | None | The name of the nomad allocation. |
16+
| `nomad.alloc.namespace` | `string` | None | The namespace of the allocation. |
17+
| `nomad.alloc.jobID` | `string` | None | The job ID of the allocation. |
18+
| `nomad.alloc.clientStatus` | `string` | None | The client status of the allocation. |
19+
| `nomad.alloc.images` | `string (list)` | None | The list of container images on allocations. |
20+
| `nomad.alloc.images.tags` | `string (list)` | None | The tags of each container image on allocations. |
21+
| `nomad.alloc.images.repositories` | `string (list)` | None | The container repositories used on allocations container images. |
22+
| `nomad.alloc.taskStates.type` | `string (list)` | None | The state of the task on the allocations. |
23+
| `nomad.alloc.res.cpu` | `uint64` | None | The CPU required to run this allocation in MHz. |
24+
| `nomad.alloc.res.cores` | `uint64` | None | The number of CPU cores to reserve for the allocation. |
25+
| `nomad.alloc.res.diskMB` | `uint64` | None | the amount of disk required for the allocation. |
26+
| `nomad.alloc.res.iops` | `uint64` | None | the number of iops required for the allocation. |
27+
| `nomad.alloc.res.memoryMB` | `uint64` | None | The memory required in MB for the allocation. |
28+
| `nomad.alloc.res.memoryMaxMB` | `uint64` | None | The maximum memory the allocation may use. |
29+
| `nomad.event.topic` | `string` | None | The topic of the nomad event. |
30+
| `nomad.event.type` | `string` | None | The type of the nomad event. |
31+
## Configuration
32+
33+
34+
### `falco.yaml` Example
35+
36+
```yaml
37+
plugins:
38+
- name: nomad
39+
library_path: libnomad.so
40+
init_config:
41+
address: http://127.0.0.1:4646
42+
token: ""
43+
namespace: "*"
44+
45+
# Optional. If not specified the first entry in plugins is used.
46+
load_plugins: [nomad, json]
47+
```
48+

pkg/nomad/config.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package nomad
2+
3+
// Defining a type for the plugin configuration.
4+
// In this simple example, users can define the starting value the event
5+
// counter. the `jsonschema` tags is used to automatically generate a
6+
// JSON Schema definition, so that the framework can perform automatic
7+
// validations.
8+
type PluginConfig struct {
9+
Address string `json:"address" jsonschema:"title=Nomad address,description=The address of the Nomad server.,default=http://localhost:4646"`
10+
Token string `json:"token" jsonschema:"title=Nomad token,description=The token to use to connect to the Nomad server.,default="`
11+
Namespace string `json:"namespace" jsonschema:"title=Nomad namespace,description=The namespace to use to connect to the Nomad server.,default=*"`
12+
}
13+
14+
// Resets sets the configuration to its default values
15+
func (p *PluginConfig) Reset() {
16+
p.Address = "http://localhost:4646"
17+
p.Token = ""
18+
p.Namespace = "*"
19+
}

pkg/nomad/extract.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package nomad
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/falcosecurity/plugin-sdk-go/pkg/sdk"
9+
"github.com/hashicorp/nomad/api"
10+
)
11+
12+
// This method is mandatory the field extraction capability.
13+
// If the Extract method is defined, the framework expects an Fields method
14+
// to be specified too.
15+
func (p *Plugin) Extract(req sdk.ExtractRequest, evt sdk.EventReader) error {
16+
var event api.Event
17+
encoder := json.NewDecoder(evt.Reader())
18+
if err := encoder.Decode(&event); err != nil {
19+
return err
20+
}
21+
22+
switch req.Field() {
23+
case "nomad.index":
24+
req.SetValue(event.Index)
25+
case "nomad.alloc.name":
26+
alloc, err := event.Allocation()
27+
if err == nil {
28+
req.SetValue(string(alloc.Name))
29+
}
30+
case "nomad.alloc.namespace":
31+
alloc, err := event.Allocation()
32+
if err == nil {
33+
req.SetValue(string(alloc.Namespace))
34+
}
35+
case "nomad.alloc.jobID":
36+
alloc, err := event.Allocation()
37+
if err == nil {
38+
req.SetValue(string(alloc.JobID))
39+
}
40+
case "nomad.alloc.clientStatus":
41+
alloc, err := event.Allocation()
42+
if err == nil {
43+
req.SetValue(string(alloc.ClientStatus))
44+
}
45+
case "nomad.alloc.taskStates.type":
46+
var driver []string
47+
alloc, err := event.Allocation()
48+
if err == nil {
49+
for _, task := range alloc.TaskStates {
50+
for _, taskEvent := range task.Events {
51+
if taskEvent.Type == "Driver" {
52+
driver = append(driver, taskEvent.Type)
53+
}
54+
}
55+
}
56+
}
57+
req.SetValue(driver)
58+
case "nomad.alloc.res.cpu":
59+
alloc, err := event.Allocation()
60+
if err == nil {
61+
req.SetValue(uint64(*alloc.Resources.CPU))
62+
}
63+
case "nomad.alloc.res.cores":
64+
alloc, err := event.Allocation()
65+
if err == nil {
66+
req.SetValue(uint64(*alloc.Resources.Cores))
67+
}
68+
case "nomad.alloc.res.diskMB":
69+
alloc := event.Payload["Allocation"]
70+
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
71+
value, _ := strconv.ParseUint(valStr, 10, 64)
72+
req.SetValue(value)
73+
case "nomad.alloc.res.iops":
74+
alloc, err := event.Allocation()
75+
if err == nil {
76+
req.SetValue(uint64(*alloc.Resources.IOPS))
77+
}
78+
case "nomad.alloc.res.memoryMB":
79+
alloc := event.Payload["Allocation"]
80+
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
81+
value, _ := strconv.ParseUint(valStr, 10, 64)
82+
req.SetValue(value)
83+
case "nomad.alloc.res.memoryMaxMB":
84+
alloc := event.Payload["Allocation"]
85+
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
86+
value, _ := strconv.ParseUint(valStr, 10, 64)
87+
req.SetValue(value)
88+
case "nomad.alloc.images":
89+
images, err := getAllocImages(&event)
90+
if err == nil {
91+
req.SetValue(images)
92+
}
93+
case "nomad.alloc.images.tags":
94+
tags, err := getAllocTags(&event)
95+
if err == nil {
96+
req.SetValue(tags)
97+
}
98+
case "nomad.alloc.images.repositories":
99+
repos, err := getAllocRepos(&event)
100+
if err == nil {
101+
req.SetValue(repos)
102+
}
103+
case "nomad.event.topic":
104+
req.SetValue(string(event.Topic))
105+
case "nomad.event.type":
106+
req.SetValue(event.Type)
107+
default:
108+
return fmt.Errorf("unsupported field: %s", req.Field())
109+
}
110+
111+
return nil
112+
}

pkg/nomad/fields.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package nomad
2+
3+
import (
4+
"github.com/falcosecurity/plugin-sdk-go/pkg/sdk"
5+
)
6+
7+
// Fields return the list of extractor fields exported by this plugin.
8+
// This method is mandatory the field extraction capability.
9+
// If the Fields method is defined, the framework expects an Extract method
10+
// to be specified too.
11+
func (p *Plugin) Fields() []sdk.FieldEntry {
12+
return []sdk.FieldEntry{
13+
{Type: "uint64", Name: "nomad.index", Display: "Event index", Desc: "the index of the nomad event."},
14+
{Type: "string", Name: "nomad.alloc.name", Display: "Allocation name", Desc: "the name of the nomad allocation."},
15+
{Type: "string", Name: "nomad.alloc.namespace", Display: "Allocation namespace", Desc: "the namespace of the allocation."},
16+
{Type: "string", Name: "nomad.alloc.jobID", Display: "Allocation Job ID", Desc: "the job ID of the allocation."},
17+
{Type: "string", Name: "nomad.alloc.clientStatus", Display: "Allocation client status", Desc: "the client status of the allocation."},
18+
{Type: "string", Name: "nomad.alloc.images", Display: "Allocation container images", Desc: "the list of container images on allocations.", IsList: true},
19+
{Type: "string", Name: "nomad.alloc.images.tags", Display: "Allocation container tags", Desc: "the tags of each container image on allocations.", IsList: true},
20+
{Type: "string", Name: "nomad.alloc.images.repositories", Display: "Allocation container repositories", Desc: "the container repositories used on allocations container images.", IsList: true},
21+
{Type: "string", Name: "nomad.alloc.taskStates.type", Display: "Allocation Task State", Desc: "the state of the task on the allocations.", IsList: true},
22+
{Type: "uint64", Name: "nomad.alloc.res.cpu", Display: "Allocation CPU Resources", Desc: "the CPU required to run this allocation in MHz."},
23+
{Type: "uint64", Name: "nomad.alloc.res.cores", Display: "Allocation CPU Cores Resources", Desc: "the number of CPU cores to reserve for the allocation."},
24+
{Type: "uint64", Name: "nomad.alloc.res.diskMB", Display: "Allocation Disk in MB Resources", Desc: "the amount of disk required for the allocation."},
25+
{Type: "uint64", Name: "nomad.alloc.res.iops", Display: "Allocation IOPS Resources", Desc: "the number of iops required for the allocation."},
26+
{Type: "uint64", Name: "nomad.alloc.res.memoryMB", Display: "Allocation Memory in MB Resources", Desc: "the memory required in MB for the allocation."},
27+
{Type: "uint64", Name: "nomad.alloc.res.memoryMaxMB", Display: "Allocation Max Memory in MB Resources", Desc: "the maximum memory the allocation may use."},
28+
{Type: "string", Name: "nomad.event.topic", Display: "Nomad Event Topic", Desc: "the topic of the nomad event."},
29+
{Type: "string", Name: "nomad.event.type", Display: "Nomad Event type", Desc: "the type of the nomad event."},
30+
}
31+
}

pkg/nomad/helpers.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package nomad
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/hashicorp/nomad/api"
8+
)
9+
10+
func getAllocImages(evt *api.Event) ([]string, error) {
11+
alloc, err := evt.Allocation()
12+
if err != nil {
13+
return nil, err
14+
}
15+
16+
var images []string
17+
for _, task := range alloc.TaskStates {
18+
for _, taskEvent := range task.Events {
19+
image := taskEvent.Details["image"]
20+
fmt.Println("------------------")
21+
fmt.Println(image)
22+
fmt.Println("------------------")
23+
tokens := strings.Split(image, ":")
24+
if strings.Contains(tokens[0], "/") {
25+
// cases
26+
// [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 (“/”).)
27+
// [registry]/[repository_name]/[image]:[tag]
28+
// [registry]/[image]:[tag]
29+
tokens = strings.Split(tokens[0], "/")
30+
images = append(images, tokens[len(tokens)-1])
31+
} else {
32+
// case [image]:[tag]
33+
images = append(images, tokens[0])
34+
}
35+
}
36+
}
37+
38+
return images, nil
39+
}
40+
41+
func getAllocTags(evt *api.Event) ([]string, error) {
42+
alloc, err := evt.Allocation()
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
var tags []string
48+
for _, task := range alloc.TaskStates {
49+
for _, taskEvent := range task.Events {
50+
tokens := strings.Split(taskEvent.Details["image"], ":")
51+
tags = append(tags, tokens[len(tokens)-1])
52+
}
53+
}
54+
55+
return tags, nil
56+
}
57+
58+
func getAllocRepos(evt *api.Event) ([]string, error) {
59+
alloc, err := evt.Allocation()
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
var repos []string
65+
for _, task := range alloc.TaskStates {
66+
for _, taskEvent := range task.Events {
67+
tokens := strings.Split(taskEvent.Details["image"], ":")
68+
if len(tokens) == 2 {
69+
if strings.Contains(tokens[0], "/") {
70+
// cases
71+
// [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 (“/”).)
72+
// [registry]/[repository_name]/[image]:[tag]
73+
// [registry]/[image]:[tag]
74+
tokens = strings.Split(tokens[0], "/")
75+
tokens = tokens[:len(tokens)-1]
76+
tokenStr := strings.Join(tokens, "/")
77+
repos = append(repos, tokenStr)
78+
} else {
79+
// case [image]:[tag]
80+
repos = append(repos, "docker.io")
81+
}
82+
}
83+
}
84+
}
85+
86+
return repos, nil
87+
}

0 commit comments

Comments
 (0)