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