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

initial thehive reporting setup #237

Draft
wants to merge 6 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions docker/soarca/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ services:
MQTT_BROKER: "mosquitto"
MQTT_PORT: 1883
HTTP_SKIP_CERT_VALIDATION: false
THEHIVE_API_TOKEN: your_token
THEHIVE_URI: http://localhost:9000
networks:
- db-net
- mqtt-net
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package connector

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"soarca/internal/reporter/downstream_reporter/thehive/schemas"
"soarca/logger"
"soarca/models/cacao"
"strings"
"time"
)

var (
component = reflect.TypeOf(TheHiveConnector{}).PkgPath()
log *logger.Log
)

func init() {
log = logger.Logger(component, logger.Info, "", logger.Json)
}

type ITheHiveConnector interface {
Hello() string
PostNewCase(caseId string, playbook cacao.Playbook) (string, error)
}

// The TheHive connector itself

type TheHiveConnector struct {
baseUrl string
apiKey string
}

func New(theHiveEndpoint string, theHiveApiKey string) *TheHiveConnector {
return &TheHiveConnector{baseUrl: theHiveEndpoint, apiKey: theHiveApiKey}
}

func (theHiveConnector *TheHiveConnector) sendRequest(method string, url string, body interface{}) ([]byte, error) {

// Replace double slashes in the URL after http(s)://
parts := strings.SplitN(url, "//", 2)
cleanedPath := strings.ReplaceAll(parts[1], "//", "/")
url = parts[0] + "//" + cleanedPath

log.Tracef("sending request: %s %s", method, url)

var requestBody io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("error marshalling JSON: %v", err)
}
requestBody = bytes.NewBuffer(jsonData)
}
log.Debugf("request body: %s", requestBody)
fmt.Printf("request body: %s", requestBody)

req, err := http.NewRequest(method, url, requestBody)
if err != nil {
log.Error(err)
return nil, err
}

req.Header.Add("Authorization", "Bearer "+theHiveConnector.apiKey)
req.Header.Add("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error(err)
return nil, err
}
defer resp.Body.Close()

respbody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("received non-2xx status code: %d\nURL: %s", resp.StatusCode, url)
}

return respbody, nil
}

func (theHiveConnector *TheHiveConnector) Hello() string {

url := theHiveConnector.baseUrl + "/user/current"

body, err := theHiveConnector.sendRequest("GET", url, nil)
if err != nil {
return "error"
}

return (string(body))
}

func (theHiveConnector *TheHiveConnector) PostNewCase(caseId string, playbook cacao.Playbook) (string, error) {
log.Tracef("posting new case to thehive. case ID %s, playbook %+v", caseId, playbook)

url := theHiveConnector.baseUrl + "/case"
var tasks []schemas.Task
for _, step := range playbook.Workflow {
task := schemas.Task{
Title: step.Name,
Description: step.Description,
}
tasks = append(tasks, task)
}

// Add execution ID and playbook ID to tags (first and second tags)
data := schemas.Case{
Title: playbook.Name,
Description: playbook.Description,
StartDate: time.Now().Unix(),
Tags: playbook.Labels,
Tasks: tasks,
}

// data_bytes, err := json.Marshal(data)
// if err != nil {
// return "", err
// }

body, err := theHiveConnector.sendRequest("POST", url, data)
if err != nil {
return "", err
}

// TODO: cleanup
var resp_map map[string]interface{}
err = json.Unmarshal(body, &resp_map)
if err != nil {
return "", err
}
fmt.Println(resp_map)
// Print the map
pretty, err := json.MarshalIndent(resp_map, "", " ")
if err != nil {
return "", err
}
fmt.Println(string(pretty))

// Return the HTTP status code and the response body
return string(body), nil
}
52 changes: 52 additions & 0 deletions internal/reporter/downstream_reporter/thehive/schemas/schemas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package schemas

type Task struct {
Title string `bson:"title" json:"title" validate:"required" example:"Task 1"`
Group string `bson:"group,omitempty" json:"group,omitempty" example:"Group 1"`
Description string `bson:"description,omitempty" json:"description,omitempty" example:"Description of task 1"`
Status string `bson:"status,omitempty" json:"status,omitempty" example:"Open"`
Flag bool `bson:"flag,omitempty" json:"flag,omitempty" example:"true"`
StartDate int64 `bson:"startDate,omitempty" json:"startDate,omitempty" example:"1640000000000"`
EndDate int64 `bson:"endDate,omitempty" json:"endDate,omitempty" example:"1640000000000"`
Order int `bson:"order,omitempty" json:"order,omitempty" example:"1"`
DueDate int64 `bson:"dueDate,omitempty" json:"dueDate,omitempty" example:"1640000000000"`
Assignee string `bson:"assignee,omitempty" json:"assignee,omitempty" example:"Jane Doe"`
Mandatory bool `bson:"mandatory,omitempty" json:"mandatory,omitempty" example:"true"`
}

type Page struct {
Title string `bson:"title" json:"title" example:"Page 1"`
Content string `bson:"content" json:"content" example:"Content of page 1"`
Order int `bson:"order" json:"order" example:"1"`
Category string `bson:"category" json:"category" example:"Category 1"`
}

type SharingParameter struct {
Organisation string `bson:"organisation" json:"organisation" example:"~354"`
Share bool `bson:"share" json:"share" example:"true"`
Profile string `bson:"profile" json:"profile" example:"analyst"`
TaskRule string `bson:"taskRule" json:"taskRule" example:"Sharing rule applied on the case"`
ObservableRule string `bson:"observableRule" json:"observableRule" example:"Sharing rule applied on the case"`
}

type Case struct {
Title string `bson:"title" json:"title" validate:"required,min=1,max=512" example:"Example Case"`
Description string `bson:"description" json:"description" validate:"required,max=1048576"`
Severity int `bson:"severity,omitempty" json:"severity,omitempty" validate:"min=1,max=4" example:"2"`
StartDate int64 `bson:"startDate,omitempty" json:"startDate,omitempty" example:"1640000000000"`
EndDate int64 `bson:"endDate,omitempty" json:"endDate,omitempty" example:"1640000000000"`
Tags []string `bson:"tags,omitempty" json:"tags,omitempty" validate:"max=128,dive,min=1,max=128" example:"[\"example\", \"test\"]"`
Flag bool `bson:"flag,omitempty" json:"flag,omitempty" example:"false"`
TLP int `bson:"tlp,omitempty" json:"tlp,omitempty" validate:"min=0,max=4" example:"2"`
PAP int `bson:"pap,omitempty" json:"pap,omitempty" validate:"min=0,max=3" example:"2"`
Status string `bson:"status,omitempty" json:"status,omitempty" validate:"min=1,max=64" example:"New"`
Summary string `bson:"summary,omitempty" json:"summary,omitempty" validate:"max=1048576" example:"Summary of the case"`
Assignee string `bson:"assignee,omitempty" json:"assignee,omitempty" validate:"max=128" example:"John Doe"`
CustomFields interface{} `bson:"customFields,omitempty" json:"customFields,omitempty" example:"{\"property1\":null,\"property2\":null}"`
CaseTemplate string `bson:"caseTemplate,omitempty" json:"caseTemplate,omitempty" validate:"max=128" example:"Template1"`
Tasks []Task `bson:"tasks,omitempty" json:"tasks,omitempty"`
Pages []Page `bson:"pages,omitempty" json:"pages,omitempty"`
SharingParameters []SharingParameter `bson:"sharingParameters,omitempty" json:"sharingParameters,omitempty"`
TaskRule string `bson:"taskRule,omitempty" json:"taskRule,omitempty" validate:"max=128" example:"Task rule"`
ObservableRule string `bson:"observableRule,omitempty" json:"observableRule,omitempty" validate:"max=128" example:"Observable rule"`
}
44 changes: 44 additions & 0 deletions internal/reporter/downstream_reporter/thehive/thehive_reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package thehive

import (
"soarca/models/cacao"

"soarca/internal/reporter/downstream_reporter/thehive/connector"

"github.com/google/uuid"
)

type TheHiveReporter struct {
connector connector.ITheHiveConnector
}

func New(connector connector.ITheHiveConnector) *TheHiveReporter {
return &TheHiveReporter{connector: connector}
}

// TODO: add structures to handle Execution ID to TheHive IDs mapping

func (theHiveReporter *TheHiveReporter) ConnectorTest() string {
return theHiveReporter.connector.Hello()
}

// Creates a new *case* in The Hive with related triggering metadata
func (theHiveReporter *TheHiveReporter) ReportWorkflowStart(executionId uuid.UUID, playbook cacao.Playbook) error {
_, err := theHiveReporter.connector.PostNewCase(executionId.String(), playbook)
return err
}

// Marks case closure according to workflow execution. Also reports all variables, and data
func (theHiveReporter *TheHiveReporter) ReportWorkflowEnd(executionId uuid.UUID, playbook cacao.Playbook, err error) error {
return nil
}

// Adds *event* to case
func (theHiveReporter *TheHiveReporter) ReportStepStart(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables) error {
return nil
}

// Populates event with step execution information
func (theHiveReporter *TheHiveReporter) ReportStepEnd(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables, err error) error {
return nil
}
132 changes: 132 additions & 0 deletions test/manual/thehive_reporter/thehive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package thehive_test

import (
"bufio"
"fmt"
"os"
"soarca/internal/reporter/downstream_reporter/thehive"
"soarca/internal/reporter/downstream_reporter/thehive/connector"
"soarca/models/cacao"
"strings"
"testing"

"github.com/google/uuid"
)

// Microsoft Copilot provided code to get .env local file and extract variables values
func LoadEnv(envVar string) (string, error) {
file, err := os.Open(".env")
if err != nil {
return "", err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, envVar+"=") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
return strings.Trim(parts[1], `"`), nil
}
}
}

if err := scanner.Err(); err != nil {
return "", err
}

return "", fmt.Errorf("variable %s not found", envVar)
}

func TestTheHiveConnection(t *testing.T) {
thehive_api_tkn, err := LoadEnv("THEHIVE_TEST_API_TOKEN")
if err != nil {
t.Fail()
}
thehive_api_base_uri, err := LoadEnv("THEHIVE_TEST_API_BASE_URI")
if err != nil {
t.Fail()
}
thr := thehive.New(connector.New(thehive_api_base_uri, thehive_api_tkn))
str := thr.ConnectorTest()
fmt.Println(str)
}

func TestTheHiveOpenCase(t *testing.T) {
thehive_api_tkn, err := LoadEnv("THEHIVE_TEST_API_TOKEN")
if err != nil {
t.Fail()
}
thehive_api_base_uri, err := LoadEnv("THEHIVE_TEST_API_BASE_URI")
if err != nil {
t.Fail()
}
thr := thehive.New(connector.New(thehive_api_base_uri, thehive_api_tkn))

expectedCommand := cacao.Command{
Type: "ssh",
Command: "ssh ls -la",
}

expectedVariables := cacao.Variable{
Type: "string",
Name: "var1",
Value: "testing",
}

step1 := cacao.Step{
Type: "action",
ID: "action--test",
Name: "ssh-tests",
Description: "test step",
StepVariables: cacao.NewVariables(expectedVariables),
Commands: []cacao.Command{expectedCommand},
Cases: map[string]string{},
OnCompletion: "end--test",
Agent: "agent1",
Targets: []string{"target1"},
}

end := cacao.Step{
Type: "end",
ID: "end--test",
Name: "end step",
}

expectedAuth := cacao.AuthenticationInformation{
Name: "user",
ID: "auth1",
}

expectedTarget := cacao.AgentTarget{
Name: "sometarget",
AuthInfoIdentifier: "auth1",
ID: "target1",
}

expectedAgent := cacao.AgentTarget{
Type: "soarca",
Name: "soarca-ssh",
}

playbook := cacao.Playbook{
ID: "test",
Type: "test",
Name: "ssh-test-playbook",
Description: "Playbook description",
WorkflowStart: step1.ID,
AuthenticationInfoDefinitions: map[string]cacao.AuthenticationInformation{"id": expectedAuth},
AgentDefinitions: map[string]cacao.AgentTarget{"agent1": expectedAgent},
TargetDefinitions: map[string]cacao.AgentTarget{"target1": expectedTarget},

Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end},
}
executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0")

err = thr.ReportWorkflowStart(executionId0, playbook)
if err != nil {
fmt.Println(err)
t.Fail()
}
}