Skip to content

Commit

Permalink
feat: fortisiem integration (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
davideimola authored Aug 19, 2024
1 parent 8935f9a commit 5cb8d1f
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 56 deletions.
8 changes: 8 additions & 0 deletions .proto/redcarbon/agents_public/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package redcarbon.agents_public.v1;
message AgentConfiguration {
optional QRadarJobConfiguration qradar_job_configuration = 1;
optional SentinelOneJobConfiguration sentinelone_job_configuration = 2;
optional FortiSIEMJobConfiguration fortisiem_job_configuration = 3;
}

message QRadarJobConfiguration {
Expand All @@ -18,3 +19,10 @@ message SentinelOneJobConfiguration {
string token = 2;
bool verify_ssl = 3;
}

message FortiSIEMJobConfiguration {
string host = 1;
string username = 2;
string password = 3;
bool verify_ssl = 4;
}
2 changes: 1 addition & 1 deletion internal/routines/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (r RoutineConfig) UpdateRoutine(ctx context.Context) {
return
}

if build.Version == "DEV" {
if build.Version == "DEV" || strings.Contains(build.Version, "SNAPSHOT") {
logrus.Info("Skipping update as the agent is running in a development status")
return
}
Expand Down
119 changes: 119 additions & 0 deletions internal/services/fortisiem/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package fortisiem

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)

type FortiSIEMClient struct {
client *http.Client
username string
password string
url string
}

func NewFortiSIEMClient(url, username, password string, verifySSL bool) FortiSIEMClient {
return FortiSIEMClient{
username: username,
password: password,
url: url,
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifySSL},
},
},
}
}

func (f *FortiSIEMClient) FetchAlerts(ctx context.Context, start, end time.Time) ([]map[string]interface{}, error) {
req, err := f.createHTTPRequest(ctx, http.MethodGet, "/phoenix/rest/pub/incident")
if err != nil {
return nil, err
}

q := req.URL.Query()

q.Add("timeFrom", strconv.FormatInt(start.UTC().UnixMilli(), 10))
q.Add("timeTo", strconv.FormatInt(end.UTC().UnixMilli(), 10))

req.URL.RawQuery = q.Encode()

res, err := f.client.Do(req)
if err != nil {
return nil, err
}

response, err := parseResponse(res)
if err != nil {
return nil, err
}

if response.Pages == 1 {
return response.Data, nil
}

alerts := response.Data
queryId := response.QueryID

for page := 2; page <= response.Pages; page++ {
reqP, err := f.createHTTPRequest(ctx, http.MethodGet, fmt.Sprintf("/phoenix/rest/pub/incident/%s/%d", queryId, page))
if err != nil {
return nil, err
}

resP, err := f.client.Do(reqP)
if err != nil {
return nil, err
}

responseP, err := parseResponse(resP)
if err != nil {
return nil, err
}

alerts = append(alerts, responseP.Data...)
}

return alerts, nil
}

func parseResponse(res *http.Response) (FortiSIEMFetchAlertsResponse, error) {
var response FortiSIEMFetchAlertsResponse

defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return FortiSIEMFetchAlertsResponse{}, fmt.Errorf("status code %d", res.StatusCode)
}

err := json.NewDecoder(res.Body).Decode(&response)
if err != nil {
return FortiSIEMFetchAlertsResponse{}, err
}

return response, nil
}

func (f *FortiSIEMClient) createHTTPRequest(ctx context.Context, method string, path string) (*http.Request, error) {
r, err := url.JoinPath(f.url, path)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, method, r, nil)
if err != nil {
return nil, err
}

req.SetBasicAuth(f.username, f.password)
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Requested-By", "RedCarbon Agent")

return req, nil
}
13 changes: 13 additions & 0 deletions internal/services/fortisiem/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package fortisiem

type FortiSIEMFetchAlertsResponse struct {
Data []map[string]interface{} `json:"data"`
Pages int `json:"pages"`
QueryID string `json:"queryId"`
}

type Incident struct {
IncidentID int `json:"incidentId"`
IncidentTitle string `json:"incidentTitle"`
EventSeverity int `json:"eventSeverity"`
}
106 changes: 106 additions & 0 deletions internal/services/fortisiem_srv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package services

import (
"connectrpc.com/connect"
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"pkg.redcarbon.ai/internal/services/fortisiem"
agents_publicv1 "pkg.redcarbon.ai/proto/redcarbon/agents_public/v1"
"pkg.redcarbon.ai/proto/redcarbon/agents_public/v1/agents_publicv1connect"
)

type srvFortiSIEM struct {
cli fortisiem.FortiSIEMClient
agentsCli agents_publicv1connect.AgentsPublicAPIsV1SrvClient
}

func newFortiSIEMService(conf *agents_publicv1.FortiSIEMJobConfiguration, agentsCli agents_publicv1connect.AgentsPublicAPIsV1SrvClient) Service {
return &srvFortiSIEM{
cli: fortisiem.NewFortiSIEMClient(conf.Host, conf.Username, conf.Password, conf.VerifySsl),
agentsCli: agentsCli,
}
}

func (s srvFortiSIEM) RunService(ctx context.Context) {
l := logrus.WithFields(logrus.Fields{
"service": "fortisiem",
"trace": uuid.NewString(),
})

l.Info("Starting FortiSIEM service")
start, end := retrieveSearchTimeRangeForKey("fortisiem")

alerts, err := s.cli.FetchAlerts(ctx, start, end)
if err != nil {
l.WithError(err).Error("Error while fetching the alerts")
return
}

l.Infof("Found %d alerts", len(alerts))

for _, alert := range alerts {
incident, err := s.buildIncidentToIngest(alert)
if err != nil {
l.WithError(err).Warn("Error while building the incident to ingest for alert")
}

req := connect.NewRequest(incident)
req.Header().Set("authorization", fmt.Sprintf("ApiToken %s", viper.Get("auth.access_token")))

_, err = s.agentsCli.IngestIncident(ctx, req)
if err != nil {
l.WithError(err).Error("failed to ingest incident")
continue
}
}

viper.Set("fortisiem.last_execution", end)

if err := viper.WriteConfig(); err != nil {
l.WithError(err).Error("failed to write config")
}

l.Info("FortiSIEM service completed")
}

func (s srvFortiSIEM) buildIncidentToIngest(incident map[string]interface{}) (*agents_publicv1.IngestIncidentRequest, error) {
iStr, err := json.Marshal(incident)
if err != nil {
return nil, err
}

var inc fortisiem.Incident

err = json.Unmarshal(iStr, &inc)
if err != nil {
return nil, err
}

idStr := fmt.Sprintf("%d", inc.IncidentID)

return &agents_publicv1.IngestIncidentRequest{
Title: inc.IncidentTitle,
Description: inc.IncidentTitle,
RawData: string(iStr),
Severity: mapFortiSIEMSeverity(inc.EventSeverity),
Origin: "fortisiem",
OriginalId: &idStr,
OriginalUrl: nil,
}, nil
}

func mapFortiSIEMSeverity(sev int) uint32 {
if sev >= 1 && sev <= 4 {
return uint32(10)
}

if sev >= 5 && sev <= 8 {
return uint32(40)
}

return uint32(70)
}
4 changes: 4 additions & 0 deletions internal/services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@ func NewServicesFromConfig(agentsCli agents_publicv1connect.AgentsPublicAPIsV1Sr
services = append(services, newSentinelOneService(config.GetSentineloneJobConfiguration(), agentsCli))
}

if config.GetFortisiemJobConfiguration() != nil {
services = append(services, newFortiSIEMService(config.GetFortisiemJobConfiguration(), agentsCli))
}

return services
}
Loading

0 comments on commit 5cb8d1f

Please sign in to comment.