mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 08:34:40 +01:00
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:
parent
89dc6abbcd
commit
102b65e6c3
3 changed files with 321 additions and 0 deletions
69
daemon/log/formats/json.go
Normal file
69
daemon/log/formats/json.go
Normal 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
|
||||
}
|
68
daemon/log/formats/rfc3164.go
Normal file
68
daemon/log/formats/rfc3164.go
Normal 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
|
||||
}
|
184
daemon/log/loggers/remote.go
Normal file
184
daemon/log/loggers/remote.go
Normal 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)
|
||||
}
|
Loading…
Add table
Reference in a new issue