568 lines
14 KiB
Go
568 lines
14 KiB
Go
package rules
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"iwarma.ru/console/correlator/events"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"unicode"
|
|
|
|
"iwarma.ru/console/correlator/config"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const (
|
|
FirewallActionType = "firewall"
|
|
)
|
|
|
|
type ArmaBool bool
|
|
|
|
func (b ArmaBool) ToInterface() interface{} {
|
|
if b == true {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func (b *ArmaBool) ParseInterface(v interface{}) error {
|
|
switch value := v.(type) {
|
|
case int:
|
|
if value >= 1 {
|
|
*b = true
|
|
} else {
|
|
*b = false
|
|
}
|
|
case bool:
|
|
*b = ArmaBool(value)
|
|
case nil:
|
|
return errors.New("no value")
|
|
default:
|
|
return fmt.Errorf("can't parse type: %T with value %v", value, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type FirewallAction struct {
|
|
Enabled ArmaBool
|
|
Action string
|
|
Quick ArmaBool
|
|
Interface string
|
|
Direction string
|
|
IPProtocol string
|
|
Protocol string
|
|
SourceNet string
|
|
SourceNot ArmaBool
|
|
SourcePort string
|
|
DestinationNet string
|
|
DestinationNot ArmaBool
|
|
DestinationPort string
|
|
Log ArmaBool
|
|
Description string
|
|
Gateway string
|
|
Sequence string
|
|
|
|
// Where to send request
|
|
url struct {
|
|
ip string
|
|
scheme string
|
|
key string
|
|
secret string
|
|
}
|
|
|
|
templates struct {
|
|
sourceNet *template.Template
|
|
sourcePort *template.Template
|
|
destinationNet *template.Template
|
|
destinationPort *template.Template
|
|
}
|
|
|
|
client *http.Client
|
|
}
|
|
|
|
const (
|
|
responseSaved = "saved"
|
|
)
|
|
|
|
func (action *FirewallAction) GetType() string {
|
|
return FirewallActionType
|
|
}
|
|
|
|
func (action FirewallAction) ToInterface() (map[string]interface{}, error) {
|
|
result := make(map[string]interface{})
|
|
|
|
result["type"] = FirewallActionType
|
|
result["enabled"] = action.Enabled.ToInterface()
|
|
result["action"] = action.Action
|
|
result["quick"] = action.Quick.ToInterface()
|
|
result["interface"] = action.Interface
|
|
result["interface"] = fmt.Sprintf("[%v]", action.Interface)
|
|
result["description"] = action.Description
|
|
result["direction"] = action.Direction
|
|
result["ipprotocol"] = action.IPProtocol
|
|
result["protocol"] = action.Protocol
|
|
result["source_net"] = action.SourceNet
|
|
result["source_not"] = action.SourceNot.ToInterface()
|
|
result["source_port"] = action.SourcePort
|
|
result["destination_net"] = action.DestinationNet
|
|
result["destination_port"] = action.DestinationPort
|
|
result["destination_not"] = action.DestinationNot.ToInterface()
|
|
result["log"] = action.Log.ToInterface()
|
|
result["gateway"] = action.Gateway
|
|
result["sequence"] = action.Sequence
|
|
|
|
url := make(map[string]interface{})
|
|
url["ip"] = action.url.ip
|
|
url["key"] = action.url.key
|
|
url["scheme"] = action.url.scheme
|
|
url["secret"] = action.url.secret
|
|
|
|
result["sensor"] = url
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (action *FirewallAction) ParseInterface(v interface{}) error {
|
|
m, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("can't parse %v from %T", v, v)
|
|
}
|
|
|
|
t, ok := m["type"].(string)
|
|
if !ok {
|
|
return errors.New("no type")
|
|
}
|
|
|
|
if t != FirewallActionType {
|
|
return fmt.Errorf("bad type, expect %v got %v", ExecActionType, t)
|
|
}
|
|
|
|
err := action.Enabled.ParseInterface(m["enabled"])
|
|
if err != nil {
|
|
return fmt.Errorf("can't parse \"enabled\" option: %v", err)
|
|
}
|
|
|
|
action.Action, ok = m["action"].(string)
|
|
if !ok {
|
|
return errors.New("no action")
|
|
}
|
|
|
|
if _, ok = m["quick"]; ok {
|
|
err = action.Quick.ParseInterface(m["quick"])
|
|
if err != nil {
|
|
return fmt.Errorf("can't parse \"quick\" option: %v", err)
|
|
}
|
|
}
|
|
|
|
if value, ok := m["interface"]; ok {
|
|
switch v := value.(type) {
|
|
case string:
|
|
action.Interface = v
|
|
case []interface{}:
|
|
arr := make([]string, len(v))
|
|
for i := range v {
|
|
arr[i] = fmt.Sprintf("%v", v[i])
|
|
}
|
|
|
|
action.Interface = strings.Join(arr, ",")
|
|
case []string:
|
|
action.Interface = strings.Join(v, ",")
|
|
default:
|
|
return fmt.Errorf("bad interface type: %T (%v)", v, v)
|
|
}
|
|
} else {
|
|
return errors.New("no interface field")
|
|
}
|
|
|
|
action.IPProtocol, ok = m["ipprotocol"].(string)
|
|
if !ok {
|
|
return errors.New("no IPProtocol")
|
|
}
|
|
|
|
action.Protocol, ok = m["protocol"].(string)
|
|
if !ok {
|
|
return errors.New("no protocol")
|
|
}
|
|
|
|
action.Direction, ok = m["direction"].(string)
|
|
if !ok {
|
|
return errors.New("no direction")
|
|
}
|
|
|
|
action.SourceNet, ok = m["source_net"].(string)
|
|
if !ok {
|
|
return errors.New("no source net")
|
|
}
|
|
|
|
if _, ok = m["source_not"]; ok {
|
|
err = action.SourceNot.ParseInterface(m["source_not"])
|
|
if err != nil {
|
|
return fmt.Errorf("can't parse \"source_not\" option: %v", err)
|
|
}
|
|
}
|
|
|
|
action.DestinationNet, ok = m["destination_net"].(string)
|
|
if !ok {
|
|
return errors.New("no destination net")
|
|
}
|
|
|
|
if _, ok = m["destination_not"]; ok {
|
|
err = action.DestinationNot.ParseInterface(m["destination_not"])
|
|
if err != nil {
|
|
return fmt.Errorf("can't parse \"destination_not\" option: %v", err)
|
|
}
|
|
}
|
|
|
|
if _, ok = m["log"]; ok {
|
|
err = action.Log.ParseInterface(m["log"])
|
|
if err != nil {
|
|
return fmt.Errorf("can't parse \"log\" option: %v", err)
|
|
}
|
|
}
|
|
|
|
if _, ok = m["sequence"]; ok {
|
|
switch v := m["sequence"].(type) {
|
|
case string:
|
|
action.Sequence = v
|
|
case int:
|
|
action.Sequence = fmt.Sprintf("%v", v)
|
|
default:
|
|
return fmt.Errorf("can't parse type %T with value of %v", m["sequence"], m["sequence"])
|
|
}
|
|
}
|
|
|
|
if urlInterface, ok := m["sensor"]; ok {
|
|
url, ok := urlInterface.(map[string]interface{})
|
|
if !ok {
|
|
return errors.New("can't convert sensor value")
|
|
}
|
|
|
|
action.url.ip, ok = url["ip"].(string)
|
|
if !ok {
|
|
return errors.New("no Firewall target IP set")
|
|
}
|
|
|
|
action.url.key, ok = url["key"].(string)
|
|
if !ok {
|
|
return errors.New("no Firewall target key set")
|
|
}
|
|
|
|
action.url.scheme, ok = url["scheme"].(string)
|
|
if !ok {
|
|
return errors.New("no Firewall target scheme set")
|
|
}
|
|
|
|
action.url.secret, ok = url["secret"].(string)
|
|
if !ok {
|
|
return errors.New("no Firewall target secret set")
|
|
}
|
|
} else {
|
|
return errors.New("no sensor field")
|
|
}
|
|
|
|
// Not required
|
|
action.Description, _ = m["description"].(string)
|
|
action.SourcePort, _ = m["source_port"].(string)
|
|
action.DestinationPort, _ = m["destination_port"].(string)
|
|
action.Gateway, _ = m["gateway"].(string)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Create rule structure for ARMAIF
|
|
func (action FirewallAction) toRule() (map[string]interface{}, map[string]interface{}, error) {
|
|
res := make(map[string]interface{})
|
|
var err error
|
|
|
|
res["enabled"] = action.Enabled.ToInterface()
|
|
res["sequence"], err = strconv.Atoi(action.Sequence)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
res["action"] = action.Action
|
|
res["quick"] = action.Quick.ToInterface()
|
|
res["interface"] = action.Interface
|
|
res["direction"] = action.Direction
|
|
res["ipprotocol"] = action.IPProtocol
|
|
res["protocol"] = action.Protocol
|
|
res["source_net"] = action.SourceNet
|
|
res["source_port"] = action.SourcePort
|
|
res["source_not"] = action.SourceNot.ToInterface()
|
|
res["destination_net"] = action.DestinationNet
|
|
res["destination_port"] = action.DestinationPort
|
|
res["destination_not"] = action.DestinationNot.ToInterface()
|
|
res["gateway"] = action.Gateway
|
|
res["log"] = action.Log.ToInterface()
|
|
res["description"] = action.Description
|
|
|
|
rule := make(map[string]interface{})
|
|
rule["rule"] = res
|
|
|
|
return rule, res, nil
|
|
}
|
|
|
|
func (action *FirewallAction) ApplyRule() error {
|
|
cl := log.WithFields(log.Fields{"type": FirewallActionType, "part": "ApplyRule"})
|
|
|
|
request, err := http.NewRequest("POST", fmt.Sprintf("%v://%v/api/firewall/filter/apply/", action.url.scheme, action.url.ip), nil)
|
|
if err != nil {
|
|
cl.Errorf("Can't create request: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Set headers
|
|
request.SetBasicAuth(action.url.key, action.url.secret)
|
|
|
|
// Do request
|
|
response, err := action.client.Do(request)
|
|
if err != nil {
|
|
cl.Errorf("Can't send request: %v", err)
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// Check result
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
cl.Errorf("Can't read response body: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Dump request and response
|
|
if viper.GetBool(config.DebugDumpRequest) {
|
|
// No need to wait
|
|
go DumpNetwork("firewall", *request.URL, nil, body, response.StatusCode)
|
|
}
|
|
|
|
// Check status code
|
|
if response.StatusCode != http.StatusOK {
|
|
cl.Errorf("Bad status code: %v", response.Status)
|
|
return fmt.Errorf("bad server response code: %v", response.Status)
|
|
}
|
|
|
|
// Check response
|
|
var resp map[string]interface{}
|
|
err = json.Unmarshal(body, &resp)
|
|
if err != nil {
|
|
cl.Errorf("Can't unmarshall response: %v", err)
|
|
return fmt.Errorf("can't unmarshall response: %v", err)
|
|
}
|
|
|
|
statusInt, ok := resp["status"]
|
|
if !ok {
|
|
cl.Errorf("No status field: %v", resp)
|
|
return fmt.Errorf("no status field: %v", resp)
|
|
}
|
|
|
|
status, ok := statusInt.(string)
|
|
if !ok {
|
|
cl.Error("Can't convert status to string")
|
|
return errors.New("can't convert status to string")
|
|
}
|
|
|
|
// Trim all \n and so on
|
|
status = strings.TrimFunc(status, func(r rune) bool {
|
|
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
|
|
})
|
|
|
|
if status == "OK" {
|
|
return nil
|
|
}
|
|
|
|
cl.Errorf("Bad status value: %v", status)
|
|
return fmt.Errorf("bad status value: %v", status)
|
|
}
|
|
|
|
func (action *FirewallAction) Perform(events *[]*events.Event) error {
|
|
cl := log.WithFields(log.Fields{"type": FirewallActionType, "event_count": len(*events)})
|
|
cl.Debug("Start action")
|
|
defer cl.Debug("End action")
|
|
|
|
if events == nil || len(*events) == 0 {
|
|
cl.Error("No events")
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
|
|
// Check if we need to prepare templates
|
|
if action.templates.sourceNet == nil {
|
|
action.templates.sourceNet, err = template.New("SourceNet").Parse(action.SourceNet)
|
|
if err != nil {
|
|
cl.Errorf("Can't create source net template: %v", err)
|
|
return err
|
|
}
|
|
|
|
action.templates.sourcePort, err = template.New("SourcePort").Parse(action.SourcePort)
|
|
if err != nil {
|
|
cl.Errorf("Can't create source port template: %v", err)
|
|
return err
|
|
}
|
|
|
|
action.templates.destinationNet, err = template.New("DestinationNet").Parse(action.DestinationNet)
|
|
if err != nil {
|
|
cl.Errorf("Can't create destination net template: %v", err)
|
|
return err
|
|
}
|
|
|
|
action.templates.destinationPort, err = template.New("DestinationPort").Parse(action.DestinationPort)
|
|
if err != nil {
|
|
cl.Errorf("Can't create destination port template: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Actual action
|
|
for _, event := range *events {
|
|
|
|
// Prepare body
|
|
ibody, data, err := action.toRule()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Render templates
|
|
if _, ok := data["source_net"]; ok {
|
|
data["source_net"], err = renderTemplate(action.templates.sourceNet, event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, ok := data["source_port"]; ok {
|
|
data["source_port"], err = renderTemplate(action.templates.sourcePort, event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, ok := data["destination_net"]; ok {
|
|
data["destination_net"], err = renderTemplate(action.templates.destinationNet, event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, ok := data["destination_port"]; ok {
|
|
data["destination_port"], err = renderTemplate(action.templates.destinationPort, event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check if we have a http client
|
|
if action.client == nil {
|
|
if viper.GetBool(config.ActionFirewallRuleIgnoreSSLErrors) {
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
action.client = &http.Client{Transport: transport}
|
|
cl.Debug("Disable SSL validation")
|
|
} else {
|
|
action.client = &http.Client{}
|
|
}
|
|
}
|
|
|
|
// Prepare body
|
|
jsonBody, err := json.Marshal(ibody)
|
|
if err != nil {
|
|
cl.Errorf("Can't marshall request body: %v", err)
|
|
return err
|
|
}
|
|
|
|
request, err := http.NewRequest("POST", fmt.Sprintf("%v://%v/api/firewall/filter/addRule/", action.url.scheme, action.url.ip), bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
cl.Errorf("Can't create request: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Set headers
|
|
request.SetBasicAuth(action.url.key, action.url.secret)
|
|
request.Header.Set("Content-type", "application/json")
|
|
|
|
// Do request
|
|
response, err := action.client.Do(request)
|
|
if err != nil {
|
|
cl.Errorf("Can't send request: %v", err)
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// Check result
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
cl.Errorf("Can't read response body: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Dump request and response
|
|
if viper.GetBool(config.DebugDumpRequest) {
|
|
// No need to wait
|
|
go DumpNetwork("firewall", *request.URL, ibody, body, response.StatusCode)
|
|
}
|
|
|
|
// Check status code
|
|
if response.StatusCode != http.StatusOK {
|
|
cl.Errorf("Bad status code: %v", response.Status)
|
|
return fmt.Errorf("bad server response code: %v", response.Status)
|
|
}
|
|
|
|
// Check response
|
|
var resp map[string]interface{}
|
|
err = json.Unmarshal(body, &resp)
|
|
if err != nil {
|
|
cl.Errorf("Can't unmarshall response: %v", err)
|
|
return fmt.Errorf("can't unmarshall response: %v", err)
|
|
}
|
|
|
|
res, ok := resp["result"]
|
|
if !ok {
|
|
cl.Errorf("Bad ARMAIF response: %v", string(body))
|
|
return fmt.Errorf("bad ARMAIF response: %v", string(body))
|
|
}
|
|
|
|
resVal, ok := res.(string)
|
|
if !ok {
|
|
cl.Errorf("Bad ARMAIF response type: %T - %v", resVal, resVal)
|
|
return fmt.Errorf("bad ARMAIF response type: %T - %v", resVal, resVal)
|
|
}
|
|
|
|
if resVal != responseSaved {
|
|
cl.Errorf(`Bad armaif response. Expect "%v", got ""%v"`, responseSaved, resVal)
|
|
cl.Debugf(`Got response: %v`, string(body))
|
|
return fmt.Errorf("bad armaif response. Expect \"%v\", got \"%v\"", responseSaved, resVal)
|
|
}
|
|
}
|
|
|
|
return action.ApplyRule()
|
|
}
|
|
|
|
func (action FirewallAction) MarshalJSON() ([]byte, error) {
|
|
data, err := action.ToInterface()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return json.Marshal(data)
|
|
}
|
|
|
|
func (action *FirewallAction) UnmarshalJSON(b []byte) error {
|
|
var data interface{}
|
|
err := json.Unmarshal(b, &data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return action.ParseInterface(data)
|
|
}
|