support json, jsonnet, yaml formats
- jsonnet, json and yaml support for configuration - json and yaml support for output formats fix #40 fix #27
This commit is contained in:
@ -3,13 +3,14 @@ package app
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -48,8 +49,8 @@ func SendAndRetrieve(data Request) Response {
|
||||
}
|
||||
|
||||
type PatternStatus struct {
|
||||
Matches int `yaml:"matches"`
|
||||
Actions map[string][]string `yaml:"actions"`
|
||||
Matches int `json:"matches,omitempty"`
|
||||
Actions map[string][]string `json:"actions,omitempty"`
|
||||
}
|
||||
type MapPatternStatus map[string]*PatternStatus
|
||||
type MapPatternStatusFlush MapPatternStatus
|
||||
@ -57,53 +58,14 @@ type MapPatternStatusFlush MapPatternStatus
|
||||
type ClientStatus map[string]map[string]MapPatternStatus
|
||||
type ClientStatusFlush ClientStatus
|
||||
|
||||
// This block is made to hide pending_actions when empty
|
||||
// and matches_since_last_trigger when zero
|
||||
type FullPatternStatus PatternStatus
|
||||
type MatchesStatus struct {
|
||||
Matches int `yaml:"matches"`
|
||||
}
|
||||
type ActionsStatus struct {
|
||||
Actions map[string][]string `yaml:"actions"`
|
||||
}
|
||||
|
||||
func (mps MapPatternStatus) MarshalYAML() (interface{}, error) {
|
||||
ret := make(map[string]interface{})
|
||||
for k, v := range mps {
|
||||
if v.Matches == 0 {
|
||||
if len(v.Actions) != 0 {
|
||||
ret[k] = ActionsStatus{v.Actions}
|
||||
}
|
||||
} else {
|
||||
if len(v.Actions) != 0 {
|
||||
ret[k] = v
|
||||
} else {
|
||||
ret[k] = MatchesStatus{v.Matches}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (mps MapPatternStatusFlush) MarshalYAML() (interface{}, error) {
|
||||
var ret interface{}
|
||||
func (mps MapPatternStatusFlush) MarshalJSON() ([]byte, error) {
|
||||
for _, v := range mps {
|
||||
if v.Matches == 0 {
|
||||
if len(v.Actions) != 0 {
|
||||
ret = ActionsStatus{v.Actions}
|
||||
}
|
||||
} else {
|
||||
if len(v.Actions) != 0 {
|
||||
ret = v
|
||||
} else {
|
||||
ret = MatchesStatus{v.Matches}
|
||||
}
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
return ret, nil
|
||||
return []byte(""), nil
|
||||
}
|
||||
|
||||
func (csf ClientStatusFlush) MarshalYAML() (interface{}, error) {
|
||||
func (csf ClientStatusFlush) MarshalJSON() ([]byte, error) {
|
||||
ret := make(map[string]map[string]MapPatternStatusFlush)
|
||||
for k, v := range csf {
|
||||
ret[k] = make(map[string]MapPatternStatusFlush)
|
||||
@ -111,7 +73,7 @@ func (csf ClientStatusFlush) MarshalYAML() (interface{}, error) {
|
||||
ret[k][kk] = MapPatternStatusFlush(vv)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
return json.Marshal(ret)
|
||||
}
|
||||
|
||||
// end block
|
||||
@ -122,13 +84,19 @@ func usage(err string) {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
func ClientShow(streamfilter string) {
|
||||
func ClientShow(streamfilter, format string) {
|
||||
response := SendAndRetrieve(Request{Show, streamfilter})
|
||||
if response.Err != nil {
|
||||
log.Fatalln("Received error from daemon:", response.Err)
|
||||
os.Exit(1)
|
||||
}
|
||||
text, err := yaml.Marshal(response.ClientStatus)
|
||||
var text []byte
|
||||
var err error
|
||||
if format == "json" {
|
||||
text, err = json.MarshalIndent(response.ClientStatus, "", " ")
|
||||
} else {
|
||||
text, err = yaml.Marshal(response.ClientStatus)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to convert daemon binary response to text format:", err)
|
||||
}
|
||||
@ -136,13 +104,19 @@ func ClientShow(streamfilter string) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func ClientFlush(pattern, streamfilter string) {
|
||||
func ClientFlush(pattern, streamfilter, format string) {
|
||||
response := SendAndRetrieve(Request{Flush, pattern})
|
||||
if response.Err != nil {
|
||||
log.Fatalln("Received error from daemon:", response.Err)
|
||||
os.Exit(1)
|
||||
}
|
||||
text, err := yaml.Marshal(ClientStatusFlush(response.ClientStatus))
|
||||
var text []byte
|
||||
var err error
|
||||
if format == "json" {
|
||||
text, err = json.MarshalIndent(ClientStatusFlush(response.ClientStatus), "", " ")
|
||||
} else {
|
||||
text, err = yaml.Marshal(ClientStatusFlush(response.ClientStatus))
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to convert daemon binary response to text format:", err)
|
||||
}
|
||||
|
41
app/main.go
41
app/main.go
@ -57,8 +57,7 @@ func subCommandParse(f *flag.FlagSet, maxRemainingArgs int) {
|
||||
}
|
||||
|
||||
// FIXME add this options for show & flush
|
||||
// -l/--limit .STREAM[.FILTER] # limit to stream and filter
|
||||
// -f/--format yaml|json # (default: yaml)
|
||||
// -l/--limit .STREAM[.FILTER] # limit to stream and filter
|
||||
func basicUsage() {
|
||||
const (
|
||||
bold = "\033[1m"
|
||||
@ -70,9 +69,9 @@ func basicUsage() {
|
||||
# start the daemon
|
||||
|
||||
# options:
|
||||
-c/--config CONFIG_FILE # configuration file (required)
|
||||
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||
# (default: /run/reaction/reaction.sock)
|
||||
-c/--config CONFIG_FILE # configuration file in json, jsonnet or yaml format (required)
|
||||
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||
# (default: /run/reaction/reaction.sock)
|
||||
|
||||
` + bold + `reaction example-conf` + reset + `
|
||||
# print a configuration file example
|
||||
@ -82,17 +81,19 @@ func basicUsage() {
|
||||
# (e.g know what is currenly banned)
|
||||
|
||||
# options:
|
||||
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||
-f/--format yaml|json # (default: yaml)
|
||||
|
||||
` + bold + `reaction flush` + reset + ` TARGET
|
||||
# run currently active matches and pending actions for the specified TARGET
|
||||
# (then show flushed matches and actions)
|
||||
|
||||
# options:
|
||||
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||
-f/--format yaml|json # (default: yaml)
|
||||
|
||||
` + bold + `reaction test-regex` + reset + ` REGEX LINE # test REGEX against LINE
|
||||
cat FILE | ` + bold + `reaction test-regex` + reset + ` REGEX # test REGEX against each line of FILE
|
||||
` + bold + `reaction test-regex` + reset + ` REGEX LINE # test REGEX against LINE
|
||||
cat FILE | ` + bold + `reaction test-regex` + reset + ` REGEX # test REGEX against each line of FILE
|
||||
`)
|
||||
}
|
||||
|
||||
@ -133,13 +134,9 @@ func Main() {
|
||||
queryFormat := addFormatFlag(f)
|
||||
limit := addLimitFlag(f)
|
||||
subCommandParse(f, 0)
|
||||
// if *queryFormat != "yaml" && *queryFormat != "json" {
|
||||
// fmt.Println("only `yaml` and `json` formats are supported.")
|
||||
// f.PrintDefaults()
|
||||
// os.Exit(1)
|
||||
// }
|
||||
if *queryFormat != "yaml" {
|
||||
fmt.Println("for now, only `yaml` format is supported.")
|
||||
if *queryFormat != "yaml" && *queryFormat != "json" {
|
||||
fmt.Println("only yaml and json formats are supported")
|
||||
f.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
if *limit != "" {
|
||||
@ -147,12 +144,18 @@ func Main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
// f.Arg(0) is "" if there is no remaining argument
|
||||
ClientShow(*limit)
|
||||
ClientShow(*limit, *queryFormat)
|
||||
|
||||
case "flush":
|
||||
SocketPath = addSocketFlag(f)
|
||||
queryFormat := addFormatFlag(f)
|
||||
limit := addLimitFlag(f)
|
||||
subCommandParse(f, 1)
|
||||
if *queryFormat != "yaml" && *queryFormat != "json" {
|
||||
fmt.Println("only yaml and json formats are supported")
|
||||
f.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
if f.Arg(0) == "" {
|
||||
fmt.Println("subcommand flush takes one TARGET argument")
|
||||
basicUsage()
|
||||
@ -162,7 +165,7 @@ func Main() {
|
||||
fmt.Println("for now, -l/--limit is not supported")
|
||||
os.Exit(1)
|
||||
}
|
||||
ClientFlush(f.Arg(0), f.Arg(1))
|
||||
ClientFlush(f.Arg(0), *limit, *queryFormat)
|
||||
|
||||
case "test-regex":
|
||||
// socket not needed, no interaction with the daemon
|
||||
@ -178,7 +181,7 @@ func Main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
if f.Arg(1) == "" {
|
||||
fmt.Println("INFO no second argument. reading from stdin.")
|
||||
fmt.Println("INFO no second argument: reading from stdin")
|
||||
|
||||
MatchStdin(regex)
|
||||
} else {
|
||||
|
@ -35,12 +35,12 @@ streams:
|
||||
# <ip> is predefined in the patterns section
|
||||
# ip's regex is inserted in the following regex
|
||||
- authentication failure;.*rhost=<ip>
|
||||
# if retry and retry-period are defined,
|
||||
# if retry and retryperiod are defined,
|
||||
# the actions will only take place if a same pattern is
|
||||
# found `retry` times in a `retry-period` interval
|
||||
# found `retry` times in a `retryperiod` interval
|
||||
retry: 3
|
||||
# format is defined here: https://pkg.go.dev/time#ParseDuration
|
||||
retry-period: 6h
|
||||
retryperiod: 6h
|
||||
# actions are run by the filter when regexes are matched
|
||||
actions:
|
||||
# actions have a user-defined name
|
||||
@ -50,7 +50,7 @@ streams:
|
||||
unban:
|
||||
cmd: *iptablesunban
|
||||
# if after is defined, the action will not take place immediately, but after a specified duration
|
||||
# same format as retry-period
|
||||
# same format as retryperiod
|
||||
after: 48h
|
||||
# let's say reaction is quitting. does it run all those pending commands which had an `after` duration set?
|
||||
# if you want reaction to run those pending commands before exiting, you can set this:
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@ -8,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/google/go-jsonnet"
|
||||
)
|
||||
|
||||
func (c *Conf) setup() {
|
||||
@ -131,13 +132,21 @@ func (c *Conf) setup() {
|
||||
|
||||
func parseConf(filename string) *Conf {
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
data, err := os.Open(filename)
|
||||
if err != nil {
|
||||
log.Fatalln("FATAL Failed to read configuration file:", err)
|
||||
}
|
||||
|
||||
var conf Conf
|
||||
err = yaml.Unmarshal(data, &conf)
|
||||
if filename[len(filename)-4:] == ".yml" || filename[len(filename)-5:] == ".yaml" {
|
||||
err = jsonnet.NewYAMLToJSONDecoder(data).Decode(&conf)
|
||||
} else {
|
||||
var jsondata string
|
||||
jsondata, err = jsonnet.MakeVM().EvaluateFile(filename)
|
||||
if err == nil {
|
||||
err = json.Unmarshal([]byte(jsondata), &conf)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln("FATAL Failed to parse configuration file:", err)
|
||||
}
|
||||
|
48
app/types.go
48
app/types.go
@ -8,54 +8,54 @@ import (
|
||||
)
|
||||
|
||||
type Conf struct {
|
||||
Patterns map[string]*Pattern `yaml:"patterns"`
|
||||
Streams map[string]*Stream `yaml:"streams"`
|
||||
Patterns map[string]*Pattern `json:"patterns"`
|
||||
Streams map[string]*Stream `json:"streams"`
|
||||
}
|
||||
|
||||
type Pattern struct {
|
||||
Regex string `yaml:"regex"`
|
||||
Ignore []string `yaml:"ignore"`
|
||||
Regex string `json:"regex"`
|
||||
Ignore []string `json:"ignore"`
|
||||
|
||||
name string `yaml:"-"`
|
||||
nameWithBraces string `yaml:"-"`
|
||||
name string `json:"-"`
|
||||
nameWithBraces string `json:"-"`
|
||||
}
|
||||
|
||||
// Stream, Filter & Action structures must never be copied.
|
||||
// They're always referenced through pointers
|
||||
|
||||
type Stream struct {
|
||||
name string `yaml:"-"`
|
||||
name string `json:"-"`
|
||||
|
||||
Cmd []string `yaml:"cmd"`
|
||||
Filters map[string]*Filter `yaml:"filters"`
|
||||
Cmd []string `json:"cmd"`
|
||||
Filters map[string]*Filter `json:"filters"`
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
stream *Stream `yaml:"-"`
|
||||
name string `yaml:"-"`
|
||||
stream *Stream `json:"-"`
|
||||
name string `json:"-"`
|
||||
|
||||
Regex []string `yaml:"regex"`
|
||||
compiledRegex []regexp.Regexp `yaml:"-"`
|
||||
pattern *Pattern `yaml:"-"`
|
||||
Regex []string `json:"regex"`
|
||||
compiledRegex []regexp.Regexp `json:"-"`
|
||||
pattern *Pattern `json:"-"`
|
||||
|
||||
Retry int `yaml:"retry"`
|
||||
RetryPeriod string `yaml:"retry-period"`
|
||||
retryDuration time.Duration `yaml:"-"`
|
||||
Retry int `json:"retry"`
|
||||
RetryPeriod string `json:"retryperiod"`
|
||||
retryDuration time.Duration `json:"-"`
|
||||
|
||||
Actions map[string]*Action `yaml:"actions"`
|
||||
Actions map[string]*Action `json:"actions"`
|
||||
longuestActionDuration *time.Duration
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
filter *Filter `yaml:"-"`
|
||||
name string `yaml:"-"`
|
||||
filter *Filter `json:"-"`
|
||||
name string `json:"-"`
|
||||
|
||||
Cmd []string `yaml:"cmd"`
|
||||
Cmd []string `json:"cmd"`
|
||||
|
||||
After string `yaml:"after"`
|
||||
afterDuration time.Duration `yaml:"-"`
|
||||
After string `json:"after"`
|
||||
afterDuration time.Duration `json:"-"`
|
||||
|
||||
OnExit bool `yaml:"onexit"`
|
||||
OnExit bool `json:"onexit"`
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
|
Reference in New Issue
Block a user