added new generic remote logger and new formats

- Added new generic remote logger to send events to remote servers.
- Added new formats RFC3164 and JSON.

Configuration example to send events to logstash using the tcp input
plugin, in json format:
 "Loggers": [
    {
        "Name": "remote",
        "Server": "127.0.0.1:3333",
        "Protocol": "tcp",
        "Workers": 5,
        "Format": "json",
        "Tag": "opensnitch"
    },
 ]

logstash configuration, saving events under document.*:
 input {
    tcp {
        port => 3333
        codec => json_lines {
            target => "[document]"
        }
    }
 }

You can also use the syslog input plugin:
 "Loggers": [
    {
        "Name": "remote",
        "Server": "127.0.0.1:5140",
        "Protocol": "tcp",
        "Workers": 5,
        "Format": "rfc3164",
        "Tag": "opensnitch"
    },
 ]

logstash's syslog input plugin configuration:
 input {
    syslog {
        port => 5140
    }
}

Note: you'll need a grok filter to parse and extract the fields.

See: #947
This commit is contained in:
Gustavo Iñiguez Goia 2023-05-29 13:49:38 +02:00
parent 89dc6abbcd
commit 102b65e6c3
Failed to generate hash of commit
3 changed files with 321 additions and 0 deletions

View file

@ -0,0 +1,69 @@
package formats
import (
"encoding/json"
"fmt"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
)
// JSON name of the output format, used in our json config
const JSON = "json"
// events types
const (
EvConnection = iota
EvExec
)
// JSONEventFormat object to be sent to the remote service.
// TODO: Expand as needed: ebpf events, etc.
type JSONEventFormat struct {
Rule string `json:"Rule"`
Action string `json:"Action"`
Event interface{} `json:"Event"`
Type uint8 `json:"Type"`
}
// NewJSON returns a new Json format, to send events as json.
// The json is the protobuffer in json format.
func NewJSON() *JSONEventFormat {
return &JSONEventFormat{}
}
// Transform takes input arguments and formats them to JSON format.
func (j *JSONEventFormat) Transform(args ...interface{}) (out string) {
p := args[0]
jObj := &JSONEventFormat{}
values := p.([]interface{})
for n, val := range values {
switch val.(type) {
// TODO:
// case *protocol.Rule:
// case *protocol.Process:
// case *protocol.Alerts:
case *protocol.Connection:
// XXX: All fields of the Connection object are sent, is this what we want?
// or should we send an anonymous json?
jObj.Event = val.(*protocol.Connection)
jObj.Type = EvConnection
case string:
// action
// rule name
if n == 1 {
jObj.Action = val.(string)
} else if n == 2 {
jObj.Rule = val.(string)
}
}
}
rawCfg, err := json.Marshal(&jObj)
if err != nil {
return
}
out = fmt.Sprint(string(rawCfg), "\n\n")
return
}

View file

@ -0,0 +1,68 @@
package formats
import (
"fmt"
"log/syslog"
"os"
"time"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
)
// RFC3164 name of the output format, used in our json config
const RFC3164 = "rfc3164"
// Rfc3164 object
type Rfc3164 struct {
seq int
}
// NewRfc3164 returns a new Rfc3164 object, that transforms a message to
// RFC3164 format.
func NewRfc3164() *Rfc3164 {
return &Rfc3164{}
}
// Transform takes input arguments and formats them to RFC3164 format.
func (r *Rfc3164) Transform(args ...interface{}) (out string) {
hostname := ""
tag := ""
arg1 := args[0]
// we can do this better. Think.
if len(args) > 1 {
hostname = args[1].(string)
tag = args[2].(string)
}
values := arg1.([]interface{})
for n, val := range values {
switch val.(type) {
case *protocol.Connection:
con := val.(*protocol.Connection)
out = fmt.Sprint(out,
" SRC=\"", con.SrcIp, "\"",
" SPT=\"", con.SrcPort, "\"",
" DST=\"", con.DstIp, "\"",
" DSTHOST=\"", con.DstHost, "\"",
" DPT=\"", con.DstPort, "\"",
" PROTO=\"", con.Protocol, "\"",
" PID=\"", con.ProcessId, "\"",
" UID=\"", con.UserId, "\"",
//" COMM=", con.ProcessComm, "\"",
" PATH=\"", con.ProcessPath, "\"",
" CMDLINE=\"", con.ProcessArgs, "\"",
" CWD=\"", con.ProcessCwd, "\"",
)
default:
out = fmt.Sprint(out, " ARG", n, "=\"", val, "\"")
}
}
out = fmt.Sprintf("<%d>%s %s %s[%d]: [%s]\n",
syslog.LOG_NOTICE|syslog.LOG_DAEMON,
time.Now().Format(time.RFC3339),
hostname,
tag,
os.Getpid(),
out[1:])
return
}

View file

@ -0,0 +1,184 @@
package loggers
import (
"fmt"
"log/syslog"
"net"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/log/formats"
)
const (
LOGGER_REMOTE = "remote"
)
// Remote defines the logger that writes events to a generic remote server.
// It can write to the local or a remote daemon, UDP or TCP.
// It supports writing events in RFC5424, RFC3164, CSV and JSON formats.
type Remote struct {
Name string
Tag string
Hostname string
Writer *syslog.Writer
logFormat formats.LoggerFormat
cfg *LoggerConfig
netConn net.Conn
Timeout time.Duration
errors uint32
maxErrors uint32
status uint32
mu *sync.RWMutex
}
// NewRemote returns a new object that manipulates and prints outbound connections
// to a remote syslog server, with the given format (RFC5424 by default)
func NewRemote(cfg *LoggerConfig) (*Remote, error) {
var err error
log.Info("NewRemote logger: %v", cfg)
sys := &Remote{
mu: &sync.RWMutex{},
}
sys.Name = LOGGER_REMOTE
sys.cfg = cfg
// list of allowed formats for this logger
sys.logFormat = formats.NewRfc5424()
if cfg.Format == formats.RFC3164 {
sys.logFormat = formats.NewRfc3164()
} else if cfg.Format == formats.JSON {
sys.logFormat = formats.NewJSON()
} else if cfg.Format == formats.CSV {
sys.logFormat = formats.NewCSV()
}
sys.Tag = logTag
if cfg.Tag != "" {
sys.Tag = cfg.Tag
}
sys.Hostname, err = os.Hostname()
if err != nil {
sys.Hostname = "localhost"
}
if cfg.WriteTimeout == "" {
cfg.WriteTimeout = writeTimeout
}
sys.Timeout = (time.Second * 15)
if err = sys.Open(); err != nil {
log.Error("Error loading logger: %s", err)
return nil, err
}
log.Info("[%s] initialized: %v", sys.Name, cfg)
return sys, err
}
// Open opens a new connection with a server or with the daemon.
func (s *Remote) Open() (err error) {
atomic.StoreUint32(&s.errors, 0)
if s.cfg.Server == "" {
return fmt.Errorf("[%s] Server address must not be empty", s.Name)
}
s.mu.Lock()
s.netConn, err = s.Dial(s.cfg.Protocol, s.cfg.Server, s.Timeout*5)
s.mu.Unlock()
if err == nil {
atomic.StoreUint32(&s.status, CONNECTED)
}
return err
}
// Dial opens a new connection with a remote server.
func (s *Remote) Dial(proto, addr string, connTimeout time.Duration) (netConn net.Conn, err error) {
switch proto {
case "udp", "tcp":
netConn, err = net.DialTimeout(proto, addr, connTimeout)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("[%s] Network protocol %s not supported", s.Name, proto)
}
return netConn, nil
}
// Close closes the writer object
func (s *Remote) Close() (err error) {
s.mu.RLock()
if s.netConn != nil {
err = s.netConn.Close()
//s.netConn.conn = nil
}
s.mu.RUnlock()
atomic.StoreUint32(&s.status, DISCONNECTED)
return
}
// ReOpen tries to reestablish the connection with the writer
func (s *Remote) ReOpen() {
if atomic.LoadUint32(&s.status) == CONNECTING {
return
}
atomic.StoreUint32(&s.status, CONNECTING)
if err := s.Close(); err != nil {
log.Debug("[%s] error closing Close(): %s", s.Name, err)
}
if err := s.Open(); err != nil {
log.Debug("[%s] ReOpen() error: %s", s.Name, err)
} else {
log.Debug("[%s] ReOpen() ok", s.Name)
}
}
// Transform transforms data for proper ingestion.
func (s *Remote) Transform(args ...interface{}) (out string) {
if s.logFormat != nil {
args = append(args, s.Hostname)
args = append(args, s.Tag)
out = s.logFormat.Transform(args...)
}
return
}
func (s *Remote) Write(msg string) {
deadline := time.Now().Add(s.Timeout)
// BUG: it's fairly common to have write timeouts via udp/tcp.
// Reopening the connection with the server helps to resume sending events to the server,
// and have a continuous stream of events. Otherwise it'd stop working.
// I haven't figured out yet why these write errors ocurr.
s.mu.Lock()
s.netConn.SetWriteDeadline(deadline)
_, err := s.netConn.Write([]byte(msg))
s.mu.Unlock()
if err == nil {
return
}
log.Debug("[%s] %s write error: %v", s.Name, s.cfg.Protocol, err.(net.Error))
atomic.AddUint32(&s.errors, 1)
if atomic.LoadUint32(&s.errors) > maxAllowedErrors {
s.ReOpen()
return
}
}
func (s *Remote) formatLine(msg string) string {
nl := ""
if !strings.HasSuffix(msg, "\n") {
nl = "\n"
}
return fmt.Sprintf("%s%s", msg, nl)
}