Merge branch 'master' into handle_highAvailability
This commit is contained in:
commit
fd0af1ad45
39 changed files with 2048 additions and 775 deletions
7
.github/workflows/bats-hub.yml
vendored
7
.github/workflows/bats-hub.yml
vendored
|
@ -8,9 +8,6 @@ on:
|
|||
GIST_BADGES_ID:
|
||||
required: true
|
||||
|
||||
env:
|
||||
PREFIX_TEST_NAMES_WITH_FILE: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
|
@ -36,7 +33,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
|
@ -50,7 +47,7 @@ jobs:
|
|||
- name: "Run hub tests"
|
||||
run: |
|
||||
./test/bin/generate-hub-tests
|
||||
./test/run-tests test/dyn-bats/${{ matrix.test-file }}
|
||||
./test/run-tests ./test/dyn-bats/${{ matrix.test-file }} --formatter $(pwd)/test/lib/color-formatter
|
||||
|
||||
- name: "Collect hub coverage"
|
||||
run: ./test/bin/collect-hub-coverage >> $GITHUB_ENV
|
||||
|
|
7
.github/workflows/bats-mysql.yml
vendored
7
.github/workflows/bats-mysql.yml
vendored
|
@ -7,9 +7,6 @@ on:
|
|||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
PREFIX_TEST_NAMES_WITH_FILE: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Functional tests"
|
||||
|
@ -39,7 +36,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
|
@ -58,7 +55,7 @@ jobs:
|
|||
MYSQL_USER: root
|
||||
|
||||
- name: "Run tests"
|
||||
run: make bats-test
|
||||
run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
|
||||
env:
|
||||
DB_BACKEND: mysql
|
||||
MYSQL_HOST: 127.0.0.1
|
||||
|
|
7
.github/workflows/bats-postgres.yml
vendored
7
.github/workflows/bats-postgres.yml
vendored
|
@ -3,9 +3,6 @@ name: (sub) Bats / Postgres
|
|||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
PREFIX_TEST_NAMES_WITH_FILE: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Functional tests"
|
||||
|
@ -48,7 +45,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
|
@ -67,7 +64,7 @@ jobs:
|
|||
PGUSER: postgres
|
||||
|
||||
- name: "Run tests (DB_BACKEND: pgx)"
|
||||
run: make bats-test
|
||||
run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
|
||||
env:
|
||||
DB_BACKEND: pgx
|
||||
PGHOST: 127.0.0.1
|
||||
|
|
5
.github/workflows/bats-sqlite-coverage.yml
vendored
5
.github/workflows/bats-sqlite-coverage.yml
vendored
|
@ -4,7 +4,6 @@ on:
|
|||
workflow_call:
|
||||
|
||||
env:
|
||||
PREFIX_TEST_NAMES_WITH_FILE: true
|
||||
TEST_COVERAGE: true
|
||||
|
||||
jobs:
|
||||
|
@ -29,7 +28,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
|
@ -42,7 +41,7 @@ jobs:
|
|||
make clean bats-build bats-fixture BUILD_STATIC=1
|
||||
|
||||
- name: "Run tests"
|
||||
run: make bats-test
|
||||
run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
|
||||
|
||||
- name: "Collect coverage data"
|
||||
run: |
|
||||
|
|
2
.github/workflows/ci-windows-build-msi.yml
vendored
2
.github/workflows/ci-windows-build-msi.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: Build
|
||||
run: make windows_installer BUILD_RE2_WASM=1
|
||||
|
|
16
.github/workflows/codeql-analysis.yml
vendored
16
.github/workflows/codeql-analysis.yml
vendored
|
@ -49,9 +49,15 @@ jobs:
|
|||
# required to pick up tags for BUILD_VERSION
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.7"
|
||||
cache-dependency-path: "**/go.sum"
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
|
@ -71,14 +77,8 @@ jobs:
|
|||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
cache-dependency-path: "**/go.sum"
|
||||
|
||||
- run: |
|
||||
make clean build BUILD_RE2_WASM=1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
2
.github/workflows/go-tests-windows.yml
vendored
2
.github/workflows/go-tests-windows.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
|
2
.github/workflows/go-tests.yml
vendored
2
.github/workflows/go-tests.yml
vendored
|
@ -126,7 +126,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: Create localstack streams
|
||||
run: |
|
||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21.6"
|
||||
go-version: "1.21.7"
|
||||
|
||||
- name: Build the binaries
|
||||
run: |
|
||||
|
|
|
@ -11,7 +11,7 @@ run:
|
|||
linters-settings:
|
||||
cyclop:
|
||||
# lower this after refactoring
|
||||
max-complexity: 70
|
||||
max-complexity: 53
|
||||
|
||||
gci:
|
||||
sections:
|
||||
|
@ -26,7 +26,7 @@ linters-settings:
|
|||
|
||||
gocyclo:
|
||||
# lower this after refactoring
|
||||
min-complexity: 70
|
||||
min-complexity: 49
|
||||
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
|
@ -46,7 +46,7 @@ linters-settings:
|
|||
|
||||
maintidx:
|
||||
# raise this after refactoring
|
||||
under: 9
|
||||
under: 11
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
@ -263,6 +263,10 @@ issues:
|
|||
- perfsprint
|
||||
text: "fmt.Sprintf can be replaced .*"
|
||||
|
||||
- linters:
|
||||
- perfsprint
|
||||
text: "fmt.Errorf can be replaced with errors.New"
|
||||
|
||||
#
|
||||
# Will fix, easy but some neurons required
|
||||
#
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# vim: set ft=dockerfile:
|
||||
FROM golang:1.21.6-alpine3.18 AS build
|
||||
FROM golang:1.21.7-alpine3.18 AS build
|
||||
|
||||
ARG BUILD_VERSION
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# vim: set ft=dockerfile:
|
||||
FROM golang:1.21.6-bookworm AS build
|
||||
FROM golang:1.21.7-bookworm AS build
|
||||
|
||||
ARG BUILD_VERSION
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ stages:
|
|||
- task: GoTool@0
|
||||
displayName: "Install Go"
|
||||
inputs:
|
||||
version: '1.21.6'
|
||||
version: '1.21.7'
|
||||
|
||||
- pwsh: |
|
||||
choco install -y make
|
||||
|
|
|
@ -29,39 +29,46 @@ import (
|
|||
|
||||
func DecisionsFromAlert(alert *models.Alert) string {
|
||||
ret := ""
|
||||
var decMap = make(map[string]int)
|
||||
decMap := make(map[string]int)
|
||||
|
||||
for _, decision := range alert.Decisions {
|
||||
k := *decision.Type
|
||||
if *decision.Simulated {
|
||||
k = fmt.Sprintf("(simul)%s", k)
|
||||
}
|
||||
|
||||
v := decMap[k]
|
||||
decMap[k] = v + 1
|
||||
}
|
||||
|
||||
for k, v := range decMap {
|
||||
if len(ret) > 0 {
|
||||
ret += " "
|
||||
}
|
||||
|
||||
ret += fmt.Sprintf("%s:%d", k, v)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
||||
switch csConfig.Cscli.Output {
|
||||
func (cli *cliAlerts) alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(os.Stdout)
|
||||
header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
|
||||
|
||||
if printMachine {
|
||||
header = append(header, "machine")
|
||||
}
|
||||
err := csvwriter.Write(header)
|
||||
if err != nil {
|
||||
|
||||
if err := csvwriter.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, alertItem := range *alerts {
|
||||
row := []string{
|
||||
fmt.Sprintf("%d", alertItem.ID),
|
||||
strconv.FormatInt(alertItem.ID, 10),
|
||||
*alertItem.Source.Scope,
|
||||
*alertItem.Source.Value,
|
||||
*alertItem.Scenario,
|
||||
|
@ -73,11 +80,12 @@ func alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
|||
if printMachine {
|
||||
row = append(row, alertItem.MachineID)
|
||||
}
|
||||
err := csvwriter.Write(row)
|
||||
if err != nil {
|
||||
|
||||
if err := csvwriter.Write(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
case "json":
|
||||
if *alerts == nil {
|
||||
|
@ -86,6 +94,7 @@ func alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
|||
fmt.Println("[]")
|
||||
return nil
|
||||
}
|
||||
|
||||
x, _ := json.MarshalIndent(alerts, "", " ")
|
||||
fmt.Print(string(x))
|
||||
case "human":
|
||||
|
@ -93,8 +102,10 @@ func alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
|||
fmt.Println("No active alerts")
|
||||
return nil
|
||||
}
|
||||
|
||||
alertsTable(color.Output, alerts, printMachine)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -116,13 +127,13 @@ var alertTemplate = `
|
|||
|
||||
`
|
||||
|
||||
func displayOneAlert(alert *models.Alert, withDetail bool) error {
|
||||
func (cli *cliAlerts) displayOneAlert(alert *models.Alert, withDetail bool) error {
|
||||
tmpl, err := template.New("alert").Parse(alertTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tmpl.Execute(os.Stdout, alert)
|
||||
if err != nil {
|
||||
|
||||
if err = tmpl.Execute(os.Stdout, alert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -133,14 +144,17 @@ func displayOneAlert(alert *models.Alert, withDetail bool) error {
|
|||
sort.Slice(alert.Meta, func(i, j int) bool {
|
||||
return alert.Meta[i].Key < alert.Meta[j].Key
|
||||
})
|
||||
|
||||
table := newTable(color.Output)
|
||||
table.SetRowLines(false)
|
||||
table.SetHeaders("Key", "Value")
|
||||
|
||||
for _, meta := range alert.Meta {
|
||||
var valSlice []string
|
||||
if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
|
||||
return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err)
|
||||
return fmt.Errorf("unknown context value type '%s': %w", meta.Value, err)
|
||||
}
|
||||
|
||||
for _, value := range valSlice {
|
||||
table.AddRow(
|
||||
meta.Key,
|
||||
|
@ -148,11 +162,13 @@ func displayOneAlert(alert *models.Alert, withDetail bool) error {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
if withDetail {
|
||||
fmt.Printf("\n - Events :\n")
|
||||
|
||||
for _, event := range alert.Events {
|
||||
alertEventTable(color.Output, event)
|
||||
}
|
||||
|
@ -163,10 +179,13 @@ func displayOneAlert(alert *models.Alert, withDetail bool) error {
|
|||
|
||||
type cliAlerts struct{
|
||||
client *apiclient.ApiClient
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIAlerts() *cliAlerts {
|
||||
return &cliAlerts{}
|
||||
func NewCLIAlerts(getconfig configGetter) *cliAlerts {
|
||||
return &cliAlerts{
|
||||
cfg: getconfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewCommand() *cobra.Command {
|
||||
|
@ -176,18 +195,18 @@ func (cli *cliAlerts) NewCommand() *cobra.Command {
|
|||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
Aliases: []string{"alert"},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
if err := csConfig.LoadAPIClient(); err != nil {
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := cfg.LoadAPIClient(); err != nil {
|
||||
return fmt.Errorf("loading api client: %w", err)
|
||||
}
|
||||
apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
|
||||
apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url %s: %w", apiURL, err)
|
||||
}
|
||||
cli.client, err = apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: csConfig.API.Client.Credentials.Login,
|
||||
Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
|
||||
MachineID: cfg.API.Client.Credentials.Login,
|
||||
Password: strfmt.Password(cfg.API.Client.Credentials.Password),
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
VersionPrefix: "v1",
|
||||
|
@ -196,6 +215,7 @@ func (cli *cliAlerts) NewCommand() *cobra.Command {
|
|||
if err != nil {
|
||||
return fmt.Errorf("new api client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -221,8 +241,10 @@ func (cli *cliAlerts) NewListCmd() *cobra.Command {
|
|||
IncludeCAPI: new(bool),
|
||||
OriginEquals: new(string),
|
||||
}
|
||||
|
||||
limit := new(int)
|
||||
contained := new(bool)
|
||||
|
||||
var printMachine bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -234,9 +256,7 @@ cscli alerts list --range 1.2.3.0/24
|
|||
cscli alerts list -s crowdsecurity/ssh-bf
|
||||
cscli alerts list --type ban`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
|
||||
alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
|
||||
printHelp(cmd)
|
||||
|
@ -304,40 +324,43 @@ cscli alerts list --type ban`,
|
|||
|
||||
alerts, _, err := cli.client.Alerts.List(context.Background(), alertListFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list alerts: %v", err)
|
||||
return fmt.Errorf("unable to list alerts: %w", err)
|
||||
}
|
||||
|
||||
err = alertsToTable(alerts, printMachine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list alerts: %v", err)
|
||||
if err = cli.alertsToTable(alerts, printMachine); err != nil {
|
||||
return fmt.Errorf("unable to list alerts: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
|
||||
cmd.Flags().StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
|
||||
cmd.Flags().StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
|
||||
cmd.Flags().StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
|
||||
cmd.Flags().StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
|
||||
cmd.Flags().StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
|
||||
cmd.Flags().StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
|
||||
cmd.Flags().StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
|
||||
cmd.Flags().StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
cmd.Flags().StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
|
||||
cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
cmd.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
|
||||
cmd.Flags().IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SortFlags = false
|
||||
flags.BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
|
||||
flags.StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
|
||||
flags.StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
|
||||
flags.StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
|
||||
flags.StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
|
||||
flags.StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
|
||||
flags.StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
|
||||
flags.StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
|
||||
flags.StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
flags.StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
|
||||
flags.BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
flags.BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
|
||||
flags.IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
|
||||
var ActiveDecision *bool
|
||||
var AlertDeleteAll bool
|
||||
var delAlertByID string
|
||||
contained := new(bool)
|
||||
var (
|
||||
ActiveDecision *bool
|
||||
AlertDeleteAll bool
|
||||
delAlertByID string
|
||||
)
|
||||
|
||||
var alertDeleteFilter = apiclient.AlertsDeleteOpts{
|
||||
ScopeEquals: new(string),
|
||||
ValueEquals: new(string),
|
||||
|
@ -345,6 +368,9 @@ func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
|
|||
IPEquals: new(string),
|
||||
RangeEquals: new(string),
|
||||
}
|
||||
|
||||
contained := new(bool)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [filters] [--all]",
|
||||
Short: `Delete alerts
|
||||
|
@ -355,7 +381,7 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
|
|||
DisableAutoGenTag: true,
|
||||
Aliases: []string{"remove"},
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if AlertDeleteAll {
|
||||
return nil
|
||||
}
|
||||
|
@ -368,11 +394,11 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
|
|||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
if !AlertDeleteAll {
|
||||
if err := manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
|
||||
if err = manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
|
||||
alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
|
||||
printHelp(cmd)
|
||||
return err
|
||||
|
@ -410,12 +436,12 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
|
|||
if delAlertByID == "" {
|
||||
alerts, _, err = cli.client.Alerts.Delete(context.Background(), alertDeleteFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete alerts : %v", err)
|
||||
return fmt.Errorf("unable to delete alerts: %w", err)
|
||||
}
|
||||
} else {
|
||||
alerts, _, err = cli.client.Alerts.DeleteOne(context.Background(), delAlertByID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete alert: %v", err)
|
||||
return fmt.Errorf("unable to delete alert: %w", err)
|
||||
}
|
||||
}
|
||||
log.Infof("%s alert(s) deleted", alerts.NbDeleted)
|
||||
|
@ -423,26 +449,31 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
|
|||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
|
||||
cmd.Flags().StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
cmd.Flags().StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
|
||||
cmd.Flags().StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
|
||||
cmd.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
|
||||
cmd.Flags().StringVar(&delAlertByID, "id", "", "alert ID")
|
||||
cmd.Flags().BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
|
||||
cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SortFlags = false
|
||||
flags.StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
|
||||
flags.StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
flags.StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
|
||||
flags.StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
|
||||
flags.StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
|
||||
flags.StringVar(&delAlertByID, "id", "", "alert ID")
|
||||
flags.BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
|
||||
flags.BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
|
||||
var details bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: `inspect "alert_id"`,
|
||||
Short: `Show info about an alert`,
|
||||
Example: `cscli alerts inspect 123`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
if len(args) == 0 {
|
||||
printHelp(cmd)
|
||||
return fmt.Errorf("missing alert_id")
|
||||
|
@ -454,31 +485,32 @@ func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
|
|||
}
|
||||
alert, _, err := cli.client.Alerts.GetByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't find alert with id %s: %s", alertID, err)
|
||||
return fmt.Errorf("can't find alert with id %s: %w", alertID, err)
|
||||
}
|
||||
switch csConfig.Cscli.Output {
|
||||
switch cfg.Cscli.Output {
|
||||
case "human":
|
||||
if err := displayOneAlert(alert, details); err != nil {
|
||||
if err := cli.displayOneAlert(alert, details); err != nil {
|
||||
continue
|
||||
}
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(alert, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err)
|
||||
return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case "raw":
|
||||
data, err := yaml.Marshal(alert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err)
|
||||
return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")
|
||||
|
||||
|
@ -486,27 +518,30 @@ func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func (cli *cliAlerts) NewFlushCmd() *cobra.Command {
|
||||
var maxItems int
|
||||
var maxAge string
|
||||
var (
|
||||
maxItems int
|
||||
maxAge string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: `flush`,
|
||||
Short: `Flush alerts
|
||||
/!\ This command can be used only on the same machine than the local API`,
|
||||
Example: `cscli alerts flush --max-items 1000 --max-age 7d`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
if err := require.LAPI(csConfig); err != nil {
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := database.NewClient(csConfig.DbConfig)
|
||||
db, err := database.NewClient(cfg.DbConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create new database client: %s", err)
|
||||
return fmt.Errorf("unable to create new database client: %w", err)
|
||||
}
|
||||
log.Info("Flushing alerts. !! This may take a long time !!")
|
||||
err = db.FlushAlerts(maxAge, maxItems)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to flush alerts: %s", err)
|
||||
return fmt.Errorf("unable to flush alerts: %w", err)
|
||||
}
|
||||
log.Info("Alerts flushed")
|
||||
|
||||
|
|
|
@ -25,32 +25,53 @@ import (
|
|||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
func NewConsoleCmd() *cobra.Command {
|
||||
var cmdConsole = &cobra.Command{
|
||||
type cliConsole struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIConsole(cfg configGetter) *cliConsole {
|
||||
return &cliConsole{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliConsole) NewCommand() *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "console [action]",
|
||||
Short: "Manage interaction with Crowdsec console (https://app.crowdsec.net)",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := require.LAPI(csConfig); err != nil {
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := require.CAPI(csConfig); err != nil {
|
||||
if err := require.CAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := require.CAPIRegistered(csConfig); err != nil {
|
||||
if err := require.CAPIRegistered(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newEnrollCmd())
|
||||
cmd.AddCommand(cli.newEnableCmd())
|
||||
cmd.AddCommand(cli.newDisableCmd())
|
||||
cmd.AddCommand(cli.newStatusCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newEnrollCmd() *cobra.Command {
|
||||
name := ""
|
||||
overwrite := false
|
||||
tags := []string{}
|
||||
opts := []string{}
|
||||
|
||||
cmdEnroll := &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "enroll [enroll-key]",
|
||||
Short: "Enroll this instance to https://app.crowdsec.net [requires local API]",
|
||||
Long: `
|
||||
|
@ -66,96 +87,107 @@ After running this command your will need to validate the enrollment in the weba
|
|||
valid options are : %s,all (see 'cscli console status' for details)`, strings.Join(csconfig.CONSOLE_CONFIGS, ",")),
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
password := strfmt.Password(csConfig.API.Server.OnlineClient.Credentials.Password)
|
||||
apiURL, err := url.Parse(csConfig.API.Server.OnlineClient.Credentials.URL)
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password)
|
||||
|
||||
apiURL, err := url.Parse(cfg.API.Server.OnlineClient.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse CAPI URL: %s", err)
|
||||
return fmt.Errorf("could not parse CAPI URL: %w", err)
|
||||
}
|
||||
|
||||
hub, err := require.Hub(csConfig, nil, nil)
|
||||
hub, err := require.Hub(cfg, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get installed scenarios: %s", err)
|
||||
return fmt.Errorf("failed to get installed scenarios: %w", err)
|
||||
}
|
||||
|
||||
if len(scenarios) == 0 {
|
||||
scenarios = make([]string, 0)
|
||||
}
|
||||
|
||||
enable_opts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS}
|
||||
enableOpts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS}
|
||||
if len(opts) != 0 {
|
||||
for _, opt := range opts {
|
||||
valid := false
|
||||
if opt == "all" {
|
||||
enable_opts = csconfig.CONSOLE_CONFIGS
|
||||
enableOpts = csconfig.CONSOLE_CONFIGS
|
||||
break
|
||||
}
|
||||
for _, available_opt := range csconfig.CONSOLE_CONFIGS {
|
||||
if opt == available_opt {
|
||||
for _, availableOpt := range csconfig.CONSOLE_CONFIGS {
|
||||
if opt == availableOpt {
|
||||
valid = true
|
||||
enable := true
|
||||
for _, enabled_opt := range enable_opts {
|
||||
if opt == enabled_opt {
|
||||
for _, enabledOpt := range enableOpts {
|
||||
if opt == enabledOpt {
|
||||
enable = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if enable {
|
||||
enable_opts = append(enable_opts, opt)
|
||||
enableOpts = append(enableOpts, opt)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("option %s doesn't exist", opt)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c, _ := apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: csConfig.API.Server.OnlineClient.Credentials.Login,
|
||||
MachineID: cli.cfg().API.Server.OnlineClient.Credentials.Login,
|
||||
Password: password,
|
||||
Scenarios: scenarios,
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
VersionPrefix: "v3",
|
||||
})
|
||||
|
||||
resp, err := c.Auth.EnrollWatcher(context.Background(), args[0], name, tags, overwrite)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not enroll instance: %s", err)
|
||||
return fmt.Errorf("could not enroll instance: %w", err)
|
||||
}
|
||||
|
||||
if resp.Response.StatusCode == 200 && !overwrite {
|
||||
log.Warning("Instance already enrolled. You can use '--overwrite' to force enroll")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := SetConsoleOpts(enable_opts, true); err != nil {
|
||||
if err := cli.setConsoleOpts(enableOpts, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, opt := range enable_opts {
|
||||
for _, opt := range enableOpts {
|
||||
log.Infof("Enabled %s : %s", opt, csconfig.CONSOLE_CONFIGS_HELP[opt])
|
||||
}
|
||||
|
||||
log.Info("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.")
|
||||
log.Info("Please restart crowdsec after accepting the enrollment.")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmdEnroll.Flags().StringVarP(&name, "name", "n", "", "Name to display in the console")
|
||||
cmdEnroll.Flags().BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
|
||||
cmdEnroll.Flags().StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
|
||||
cmdEnroll.Flags().StringSliceVarP(&opts, "enable", "e", opts, "Enable console options")
|
||||
cmdConsole.AddCommand(cmdEnroll)
|
||||
|
||||
var enableAll, disableAll bool
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&name, "name", "n", "", "Name to display in the console")
|
||||
flags.BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
|
||||
flags.StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
|
||||
flags.StringSliceVarP(&opts, "enable", "e", opts, "Enable console options")
|
||||
|
||||
cmdEnable := &cobra.Command{
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newEnableCmd() *cobra.Command {
|
||||
var enableAll bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "enable [option]",
|
||||
Short: "Enable a console option",
|
||||
Example: "sudo cscli console enable tainted",
|
||||
|
@ -163,9 +195,9 @@ After running this command your will need to validate the enrollment in the weba
|
|||
Enable given information push to the central API. Allows to empower the console`,
|
||||
ValidArgs: csconfig.CONSOLE_CONFIGS,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if enableAll {
|
||||
if err := SetConsoleOpts(csconfig.CONSOLE_CONFIGS, true); err != nil {
|
||||
if err := cli.setConsoleOpts(csconfig.CONSOLE_CONFIGS, true); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("All features have been enabled successfully")
|
||||
|
@ -173,19 +205,26 @@ Enable given information push to the central API. Allows to empower the console`
|
|||
if len(args) == 0 {
|
||||
return fmt.Errorf("you must specify at least one feature to enable")
|
||||
}
|
||||
if err := SetConsoleOpts(args, true); err != nil {
|
||||
if err := cli.setConsoleOpts(args, true); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("%v have been enabled", args)
|
||||
}
|
||||
|
||||
log.Infof(ReloadMessage())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmdEnable.Flags().BoolVarP(&enableAll, "all", "a", false, "Enable all console options")
|
||||
cmdConsole.AddCommand(cmdEnable)
|
||||
cmd.Flags().BoolVarP(&enableAll, "all", "a", false, "Enable all console options")
|
||||
|
||||
cmdDisable := &cobra.Command{
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newDisableCmd() *cobra.Command {
|
||||
var disableAll bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "disable [option]",
|
||||
Short: "Disable a console option",
|
||||
Example: "sudo cscli console disable tainted",
|
||||
|
@ -193,47 +232,52 @@ Enable given information push to the central API. Allows to empower the console`
|
|||
Disable given information push to the central API.`,
|
||||
ValidArgs: csconfig.CONSOLE_CONFIGS,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if disableAll {
|
||||
if err := SetConsoleOpts(csconfig.CONSOLE_CONFIGS, false); err != nil {
|
||||
if err := cli.setConsoleOpts(csconfig.CONSOLE_CONFIGS, false); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("All features have been disabled")
|
||||
} else {
|
||||
if err := SetConsoleOpts(args, false); err != nil {
|
||||
if err := cli.setConsoleOpts(args, false); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("%v have been disabled", args)
|
||||
}
|
||||
|
||||
log.Infof(ReloadMessage())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmdDisable.Flags().BoolVarP(&disableAll, "all", "a", false, "Disable all console options")
|
||||
cmdConsole.AddCommand(cmdDisable)
|
||||
cmd.Flags().BoolVarP(&disableAll, "all", "a", false, "Disable all console options")
|
||||
|
||||
cmdConsoleStatus := &cobra.Command{
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newStatusCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Shows status of the console options",
|
||||
Example: `sudo cscli console status`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
switch csConfig.Cscli.Output {
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
consoleCfg := cfg.API.Server.ConsoleConfig
|
||||
switch cfg.Cscli.Output {
|
||||
case "human":
|
||||
cmdConsoleStatusTable(color.Output, *csConfig)
|
||||
cmdConsoleStatusTable(color.Output, *consoleCfg)
|
||||
case "json":
|
||||
c := csConfig.API.Server.ConsoleConfig
|
||||
out := map[string](*bool){
|
||||
csconfig.SEND_MANUAL_SCENARIOS: c.ShareManualDecisions,
|
||||
csconfig.SEND_CUSTOM_SCENARIOS: c.ShareCustomScenarios,
|
||||
csconfig.SEND_TAINTED_SCENARIOS: c.ShareTaintedScenarios,
|
||||
csconfig.SEND_CONTEXT: c.ShareContext,
|
||||
csconfig.CONSOLE_MANAGEMENT: c.ConsoleManagement,
|
||||
csconfig.SEND_MANUAL_SCENARIOS: consoleCfg.ShareManualDecisions,
|
||||
csconfig.SEND_CUSTOM_SCENARIOS: consoleCfg.ShareCustomScenarios,
|
||||
csconfig.SEND_TAINTED_SCENARIOS: consoleCfg.ShareTaintedScenarios,
|
||||
csconfig.SEND_CONTEXT: consoleCfg.ShareContext,
|
||||
csconfig.CONSOLE_MANAGEMENT: consoleCfg.ConsoleManagement,
|
||||
}
|
||||
data, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration: %s", err)
|
||||
return fmt.Errorf("failed to marshal configuration: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
case "raw":
|
||||
|
@ -244,11 +288,11 @@ Disable given information push to the central API.`,
|
|||
}
|
||||
|
||||
rows := [][]string{
|
||||
{csconfig.SEND_MANUAL_SCENARIOS, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareManualDecisions)},
|
||||
{csconfig.SEND_CUSTOM_SCENARIOS, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios)},
|
||||
{csconfig.SEND_TAINTED_SCENARIOS, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios)},
|
||||
{csconfig.SEND_CONTEXT, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareContext)},
|
||||
{csconfig.CONSOLE_MANAGEMENT, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ConsoleManagement)},
|
||||
{csconfig.SEND_MANUAL_SCENARIOS, fmt.Sprintf("%t", *consoleCfg.ShareManualDecisions)},
|
||||
{csconfig.SEND_CUSTOM_SCENARIOS, fmt.Sprintf("%t", *consoleCfg.ShareCustomScenarios)},
|
||||
{csconfig.SEND_TAINTED_SCENARIOS, fmt.Sprintf("%t", *consoleCfg.ShareTaintedScenarios)},
|
||||
{csconfig.SEND_CONTEXT, fmt.Sprintf("%t", *consoleCfg.ShareContext)},
|
||||
{csconfig.CONSOLE_MANAGEMENT, fmt.Sprintf("%t", *consoleCfg.ConsoleManagement)},
|
||||
}
|
||||
for _, row := range rows {
|
||||
err = csvwriter.Write(row)
|
||||
|
@ -258,132 +302,137 @@ Disable given information push to the central API.`,
|
|||
}
|
||||
csvwriter.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmdConsole.AddCommand(cmdConsoleStatus)
|
||||
|
||||
return cmdConsole
|
||||
return cmd
|
||||
}
|
||||
|
||||
func dumpConsoleConfig(c *csconfig.LocalApiServerCfg) error {
|
||||
out, err := yaml.Marshal(c.ConsoleConfig)
|
||||
func (cli *cliConsole) dumpConfig() error {
|
||||
serverCfg := cli.cfg().API.Server
|
||||
|
||||
out, err := yaml.Marshal(serverCfg.ConsoleConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while marshaling ConsoleConfig (for %s): %w", c.ConsoleConfigPath, err)
|
||||
return fmt.Errorf("while marshaling ConsoleConfig (for %s): %w", serverCfg.ConsoleConfigPath, err)
|
||||
}
|
||||
|
||||
if c.ConsoleConfigPath == "" {
|
||||
c.ConsoleConfigPath = csconfig.DefaultConsoleConfigFilePath
|
||||
log.Debugf("Empty console_path, defaulting to %s", c.ConsoleConfigPath)
|
||||
if serverCfg.ConsoleConfigPath == "" {
|
||||
serverCfg.ConsoleConfigPath = csconfig.DefaultConsoleConfigFilePath
|
||||
log.Debugf("Empty console_path, defaulting to %s", serverCfg.ConsoleConfigPath)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(c.ConsoleConfigPath, out, 0o600); err != nil {
|
||||
return fmt.Errorf("while dumping console config to %s: %w", c.ConsoleConfigPath, err)
|
||||
if err := os.WriteFile(serverCfg.ConsoleConfigPath, out, 0o600); err != nil {
|
||||
return fmt.Errorf("while dumping console config to %s: %w", serverCfg.ConsoleConfigPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetConsoleOpts(args []string, wanted bool) error {
|
||||
func (cli *cliConsole) setConsoleOpts(args []string, wanted bool) error {
|
||||
cfg := cli.cfg()
|
||||
consoleCfg := cfg.API.Server.ConsoleConfig
|
||||
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case csconfig.CONSOLE_MANAGEMENT:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if csConfig.API.Server.ConsoleConfig.ConsoleManagement != nil {
|
||||
if *csConfig.API.Server.ConsoleConfig.ConsoleManagement == wanted {
|
||||
if consoleCfg.ConsoleManagement != nil {
|
||||
if *consoleCfg.ConsoleManagement == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
|
||||
*csConfig.API.Server.ConsoleConfig.ConsoleManagement = wanted
|
||||
*consoleCfg.ConsoleManagement = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
|
||||
csConfig.API.Server.ConsoleConfig.ConsoleManagement = ptr.Of(wanted)
|
||||
consoleCfg.ConsoleManagement = ptr.Of(wanted)
|
||||
}
|
||||
|
||||
if csConfig.API.Server.OnlineClient.Credentials != nil {
|
||||
if cfg.API.Server.OnlineClient.Credentials != nil {
|
||||
changed := false
|
||||
if wanted && csConfig.API.Server.OnlineClient.Credentials.PapiURL == "" {
|
||||
if wanted && cfg.API.Server.OnlineClient.Credentials.PapiURL == "" {
|
||||
changed = true
|
||||
csConfig.API.Server.OnlineClient.Credentials.PapiURL = types.PAPIBaseURL
|
||||
} else if !wanted && csConfig.API.Server.OnlineClient.Credentials.PapiURL != "" {
|
||||
cfg.API.Server.OnlineClient.Credentials.PapiURL = types.PAPIBaseURL
|
||||
} else if !wanted && cfg.API.Server.OnlineClient.Credentials.PapiURL != "" {
|
||||
changed = true
|
||||
csConfig.API.Server.OnlineClient.Credentials.PapiURL = ""
|
||||
cfg.API.Server.OnlineClient.Credentials.PapiURL = ""
|
||||
}
|
||||
|
||||
if changed {
|
||||
fileContent, err := yaml.Marshal(csConfig.API.Server.OnlineClient.Credentials)
|
||||
fileContent, err := yaml.Marshal(cfg.API.Server.OnlineClient.Credentials)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal credentials: %s", err)
|
||||
return fmt.Errorf("cannot marshal credentials: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Updating credentials file: %s", csConfig.API.Server.OnlineClient.CredentialsFilePath)
|
||||
log.Infof("Updating credentials file: %s", cfg.API.Server.OnlineClient.CredentialsFilePath)
|
||||
|
||||
err = os.WriteFile(csConfig.API.Server.OnlineClient.CredentialsFilePath, fileContent, 0o600)
|
||||
err = os.WriteFile(cfg.API.Server.OnlineClient.CredentialsFilePath, fileContent, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write credentials file: %s", err)
|
||||
return fmt.Errorf("cannot write credentials file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case csconfig.SEND_CUSTOM_SCENARIOS:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if csConfig.API.Server.ConsoleConfig.ShareCustomScenarios != nil {
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios == wanted {
|
||||
if consoleCfg.ShareCustomScenarios != nil {
|
||||
if *consoleCfg.ShareCustomScenarios == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
|
||||
*csConfig.API.Server.ConsoleConfig.ShareCustomScenarios = wanted
|
||||
*consoleCfg.ShareCustomScenarios = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
|
||||
csConfig.API.Server.ConsoleConfig.ShareCustomScenarios = ptr.Of(wanted)
|
||||
consoleCfg.ShareCustomScenarios = ptr.Of(wanted)
|
||||
}
|
||||
case csconfig.SEND_TAINTED_SCENARIOS:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios != nil {
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios == wanted {
|
||||
if consoleCfg.ShareTaintedScenarios != nil {
|
||||
if *consoleCfg.ShareTaintedScenarios == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
|
||||
*csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios = wanted
|
||||
*consoleCfg.ShareTaintedScenarios = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
|
||||
csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios = ptr.Of(wanted)
|
||||
consoleCfg.ShareTaintedScenarios = ptr.Of(wanted)
|
||||
}
|
||||
case csconfig.SEND_MANUAL_SCENARIOS:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if csConfig.API.Server.ConsoleConfig.ShareManualDecisions != nil {
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions == wanted {
|
||||
if consoleCfg.ShareManualDecisions != nil {
|
||||
if *consoleCfg.ShareManualDecisions == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
|
||||
*csConfig.API.Server.ConsoleConfig.ShareManualDecisions = wanted
|
||||
*consoleCfg.ShareManualDecisions = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
|
||||
csConfig.API.Server.ConsoleConfig.ShareManualDecisions = ptr.Of(wanted)
|
||||
consoleCfg.ShareManualDecisions = ptr.Of(wanted)
|
||||
}
|
||||
case csconfig.SEND_CONTEXT:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if csConfig.API.Server.ConsoleConfig.ShareContext != nil {
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareContext == wanted {
|
||||
if consoleCfg.ShareContext != nil {
|
||||
if *consoleCfg.ShareContext == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_CONTEXT, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted)
|
||||
*csConfig.API.Server.ConsoleConfig.ShareContext = wanted
|
||||
*consoleCfg.ShareContext = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted)
|
||||
csConfig.API.Server.ConsoleConfig.ShareContext = ptr.Of(wanted)
|
||||
consoleCfg.ShareContext = ptr.Of(wanted)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown flag %s", arg)
|
||||
}
|
||||
}
|
||||
|
||||
if err := dumpConsoleConfig(csConfig.API.Server); err != nil {
|
||||
return fmt.Errorf("failed writing console config: %s", err)
|
||||
if err := cli.dumpConfig(); err != nil {
|
||||
return fmt.Errorf("failed writing console config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
)
|
||||
|
||||
func cmdConsoleStatusTable(out io.Writer, csConfig csconfig.Config) {
|
||||
func cmdConsoleStatusTable(out io.Writer, consoleCfg csconfig.ConsoleConfig) {
|
||||
t := newTable(out)
|
||||
t.SetRowLines(false)
|
||||
|
||||
|
@ -18,28 +18,30 @@ func cmdConsoleStatusTable(out io.Writer, csConfig csconfig.Config) {
|
|||
|
||||
for _, option := range csconfig.CONSOLE_CONFIGS {
|
||||
activated := string(emoji.CrossMark)
|
||||
|
||||
switch option {
|
||||
case csconfig.SEND_CUSTOM_SCENARIOS:
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios {
|
||||
if *consoleCfg.ShareCustomScenarios {
|
||||
activated = string(emoji.CheckMarkButton)
|
||||
}
|
||||
case csconfig.SEND_MANUAL_SCENARIOS:
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions {
|
||||
if *consoleCfg.ShareManualDecisions {
|
||||
activated = string(emoji.CheckMarkButton)
|
||||
}
|
||||
case csconfig.SEND_TAINTED_SCENARIOS:
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios {
|
||||
if *consoleCfg.ShareTaintedScenarios {
|
||||
activated = string(emoji.CheckMarkButton)
|
||||
}
|
||||
case csconfig.SEND_CONTEXT:
|
||||
if *csConfig.API.Server.ConsoleConfig.ShareContext {
|
||||
if *consoleCfg.ShareContext {
|
||||
activated = string(emoji.CheckMarkButton)
|
||||
}
|
||||
case csconfig.CONSOLE_MANAGEMENT:
|
||||
if *csConfig.API.Server.ConsoleConfig.ConsoleManagement {
|
||||
if *consoleCfg.ConsoleManagement {
|
||||
activated = string(emoji.CheckMarkButton)
|
||||
}
|
||||
}
|
||||
|
||||
t.AddRow(option, activated, csconfig.CONSOLE_CONFIGS_HELP[option])
|
||||
}
|
||||
|
||||
|
|
|
@ -16,33 +16,53 @@ import (
|
|||
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
|
||||
)
|
||||
|
||||
func GetLineCountForFile(filepath string) (int, error) {
|
||||
func getLineCountForFile(filepath string) (int, error) {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
lc := 0
|
||||
fs := bufio.NewReader(f)
|
||||
|
||||
for {
|
||||
input, err := fs.ReadBytes('\n')
|
||||
if len(input) > 1 {
|
||||
lc++
|
||||
}
|
||||
|
||||
if err != nil && err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
type cliExplain struct{}
|
||||
|
||||
func NewCLIExplain() *cliExplain {
|
||||
return &cliExplain{}
|
||||
type cliExplain struct {
|
||||
cfg configGetter
|
||||
flags struct {
|
||||
logFile string
|
||||
dsn string
|
||||
logLine string
|
||||
logType string
|
||||
details bool
|
||||
skipOk bool
|
||||
onlySuccessfulParsers bool
|
||||
noClean bool
|
||||
crowdsec string
|
||||
labels string
|
||||
}
|
||||
}
|
||||
|
||||
func (cli cliExplain) NewCommand() *cobra.Command {
|
||||
func NewCLIExplain(cfg configGetter) *cliExplain {
|
||||
return &cliExplain{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliExplain) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "explain",
|
||||
Short: "Explain log pipeline",
|
||||
|
@ -57,118 +77,50 @@ tail -n 5 myfile.log | cscli explain --type nginx -f -
|
|||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: cli.run,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
flags := cmd.Flags()
|
||||
|
||||
logFile, err := flags.GetString("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dsn, err := flags.GetString("dsn")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logLine, err := flags.GetString("log")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logType, err := flags.GetString("type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if logLine == "" && logFile == "" && dsn == "" {
|
||||
printHelp(cmd)
|
||||
fmt.Println()
|
||||
return fmt.Errorf("please provide --log, --file or --dsn flag")
|
||||
}
|
||||
if logType == "" {
|
||||
printHelp(cmd)
|
||||
fmt.Println()
|
||||
return fmt.Errorf("please provide --type flag")
|
||||
}
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.run()
|
||||
},
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
fileInfo, _ := os.Stdin.Stat()
|
||||
if logFile == "-" && ((fileInfo.Mode() & os.ModeCharDevice) == os.ModeCharDevice) {
|
||||
if cli.flags.logFile == "-" && ((fileInfo.Mode() & os.ModeCharDevice) == os.ModeCharDevice) {
|
||||
return fmt.Errorf("the option -f - is intended to work with pipes")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringP("file", "f", "", "Log file to test")
|
||||
flags.StringP("dsn", "d", "", "DSN to test")
|
||||
flags.StringP("log", "l", "", "Log line to test")
|
||||
flags.StringP("type", "t", "", "Type of the acquisition to test")
|
||||
flags.String("labels", "", "Additional labels to add to the acquisition format (key:value,key2:value2)")
|
||||
flags.BoolP("verbose", "v", false, "Display individual changes")
|
||||
flags.Bool("failures", false, "Only show failed lines")
|
||||
flags.Bool("only-successful-parsers", false, "Only show successful parsers")
|
||||
flags.String("crowdsec", "crowdsec", "Path to crowdsec")
|
||||
flags.Bool("no-clean", false, "Don't clean runtime environment after tests")
|
||||
flags.StringVarP(&cli.flags.logFile, "file", "f", "", "Log file to test")
|
||||
flags.StringVarP(&cli.flags.dsn, "dsn", "d", "", "DSN to test")
|
||||
flags.StringVarP(&cli.flags.logLine, "log", "l", "", "Log line to test")
|
||||
flags.StringVarP(&cli.flags.logType, "type", "t", "", "Type of the acquisition to test")
|
||||
flags.StringVar(&cli.flags.labels, "labels", "", "Additional labels to add to the acquisition format (key:value,key2:value2)")
|
||||
flags.BoolVarP(&cli.flags.details, "verbose", "v", false, "Display individual changes")
|
||||
flags.BoolVar(&cli.flags.skipOk, "failures", false, "Only show failed lines")
|
||||
flags.BoolVar(&cli.flags.onlySuccessfulParsers, "only-successful-parsers", false, "Only show successful parsers")
|
||||
flags.StringVar(&cli.flags.crowdsec, "crowdsec", "crowdsec", "Path to crowdsec")
|
||||
flags.BoolVar(&cli.flags.noClean, "no-clean", false, "Don't clean runtime environment after tests")
|
||||
|
||||
cmd.MarkFlagRequired("type")
|
||||
cmd.MarkFlagsOneRequired("log", "file", "dsn")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
|
||||
flags := cmd.Flags()
|
||||
func (cli *cliExplain) run() error {
|
||||
logFile := cli.flags.logFile
|
||||
logLine := cli.flags.logLine
|
||||
logType := cli.flags.logType
|
||||
dsn := cli.flags.dsn
|
||||
labels := cli.flags.labels
|
||||
crowdsec := cli.flags.crowdsec
|
||||
|
||||
logFile, err := flags.GetString("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dsn, err := flags.GetString("dsn")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logLine, err := flags.GetString("log")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logType, err := flags.GetString("type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := dumps.DumpOpts{}
|
||||
|
||||
opts.Details, err = flags.GetBool("verbose")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
no_clean, err := flags.GetBool("no-clean")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.SkipOk, err = flags.GetBool("failures")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.ShowNotOkParsers, err = flags.GetBool("only-successful-parsers")
|
||||
opts.ShowNotOkParsers = !opts.ShowNotOkParsers
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crowdsec, err := flags.GetString("crowdsec")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labels, err := flags.GetString("labels")
|
||||
if err != nil {
|
||||
return err
|
||||
opts := dumps.DumpOpts{
|
||||
Details: cli.flags.details,
|
||||
SkipOk: cli.flags.skipOk,
|
||||
ShowNotOkParsers: !cli.flags.onlySuccessfulParsers,
|
||||
}
|
||||
|
||||
var f *os.File
|
||||
|
@ -176,21 +128,25 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
|
|||
// using empty string fallback to /tmp
|
||||
dir, err := os.MkdirTemp("", "cscli_explain")
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't create a temporary directory to store cscli explain result: %s", err)
|
||||
return fmt.Errorf("couldn't create a temporary directory to store cscli explain result: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if no_clean {
|
||||
if cli.flags.noClean {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
log.Errorf("unable to delete temporary directory '%s': %s", dir, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// we create a temporary log file if a log line/stdin has been provided
|
||||
if logLine != "" || logFile == "-" {
|
||||
tmpFile := filepath.Join(dir, "cscli_test_tmp.log")
|
||||
|
||||
f, err = os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -220,6 +176,7 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
|
|||
log.Warnf("Failed to write %d lines to %s", errCount, tmpFile)
|
||||
}
|
||||
}
|
||||
|
||||
f.Close()
|
||||
// this is the file that was going to be read by crowdsec anyway
|
||||
logFile = tmpFile
|
||||
|
@ -230,15 +187,20 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("unable to get absolute path of '%s', exiting", logFile)
|
||||
}
|
||||
|
||||
dsn = fmt.Sprintf("file://%s", absolutePath)
|
||||
lineCount, err := GetLineCountForFile(absolutePath)
|
||||
|
||||
lineCount, err := getLineCountForFile(absolutePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("file %s has %d lines", absolutePath, lineCount)
|
||||
|
||||
if lineCount == 0 {
|
||||
return fmt.Errorf("the log file is empty: %s", absolutePath)
|
||||
}
|
||||
|
||||
if lineCount > 100 {
|
||||
log.Warnf("%s contains %d lines. This may take a lot of resources.", absolutePath, lineCount)
|
||||
}
|
||||
|
@ -249,15 +211,19 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
cmdArgs := []string{"-c", ConfigFilePath, "-type", logType, "-dsn", dsn, "-dump-data", dir, "-no-api"}
|
||||
|
||||
if labels != "" {
|
||||
log.Debugf("adding labels %s", labels)
|
||||
cmdArgs = append(cmdArgs, "-label", labels)
|
||||
}
|
||||
|
||||
crowdsecCmd := exec.Command(crowdsec, cmdArgs...)
|
||||
|
||||
output, err := crowdsecCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println(string(output))
|
||||
return fmt.Errorf("fail to run crowdsec for test: %v", err)
|
||||
|
||||
return fmt.Errorf("fail to run crowdsec for test: %w", err)
|
||||
}
|
||||
|
||||
parserDumpFile := filepath.Join(dir, hubtest.ParserResultFileName)
|
||||
|
@ -265,12 +231,12 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
|
|||
|
||||
parserDump, err := dumps.LoadParserDump(parserDumpFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load parser dump result: %s", err)
|
||||
return fmt.Errorf("unable to load parser dump result: %w", err)
|
||||
}
|
||||
|
||||
bucketStateDump, err := dumps.LoadBucketPourDump(bucketStateDumpFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load bucket dump result: %s", err)
|
||||
return fmt.Errorf("unable to load bucket dump result: %w", err)
|
||||
}
|
||||
|
||||
dumps.DumpTree(*parserDump, *bucketStateDump, opts)
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
@ -13,7 +14,6 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
"slices"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
|
@ -29,15 +29,27 @@ import (
|
|||
|
||||
const LAPIURLPrefix = "v1"
|
||||
|
||||
func runLapiStatus(cmd *cobra.Command, args []string) error {
|
||||
password := strfmt.Password(csConfig.API.Client.Credentials.Password)
|
||||
apiurl, err := url.Parse(csConfig.API.Client.Credentials.URL)
|
||||
login := csConfig.API.Client.Credentials.Login
|
||||
type cliLapi struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLILapi(cfg configGetter) *cliLapi {
|
||||
return &cliLapi{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliLapi) status() error {
|
||||
cfg := cli.cfg()
|
||||
password := strfmt.Password(cfg.API.Client.Credentials.Password)
|
||||
login := cfg.API.Client.Credentials.Login
|
||||
|
||||
apiurl, err := url.Parse(cfg.API.Client.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url: %w", err)
|
||||
}
|
||||
|
||||
hub, err := require.Hub(csConfig, nil, nil)
|
||||
hub, err := require.Hub(cfg, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -54,13 +66,14 @@ func runLapiStatus(cmd *cobra.Command, args []string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("init default client: %w", err)
|
||||
}
|
||||
|
||||
t := models.WatcherAuthRequest{
|
||||
MachineID: &login,
|
||||
Password: &password,
|
||||
Scenarios: scenarios,
|
||||
}
|
||||
|
||||
log.Infof("Loaded credentials from %s", csConfig.API.Client.CredentialsFilePath)
|
||||
log.Infof("Loaded credentials from %s", cfg.API.Client.CredentialsFilePath)
|
||||
log.Infof("Trying to authenticate with username %s on %s", login, apiurl)
|
||||
|
||||
_, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
|
||||
|
@ -69,26 +82,15 @@ func runLapiStatus(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
log.Infof("You can successfully interact with Local API (LAPI)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runLapiRegister(cmd *cobra.Command, args []string) error {
|
||||
flags := cmd.Flags()
|
||||
func (cli *cliLapi) register(apiURL string, outputFile string, machine string) error {
|
||||
var err error
|
||||
|
||||
apiURL, err := flags.GetString("url")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputFile, err := flags.GetString("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lapiUser, err := flags.GetString("machine")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lapiUser := machine
|
||||
cfg := cli.cfg()
|
||||
|
||||
if lapiUser == "" {
|
||||
lapiUser, err = generateID("")
|
||||
|
@ -96,12 +98,15 @@ func runLapiRegister(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("unable to generate machine id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
password := strfmt.Password(generatePassword(passwordLength))
|
||||
|
||||
if apiURL == "" {
|
||||
if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil || csConfig.API.Client.Credentials.URL == "" {
|
||||
if cfg.API.Client == nil || cfg.API.Client.Credentials == nil || cfg.API.Client.Credentials.URL == "" {
|
||||
return fmt.Errorf("no Local API URL. Please provide it in your configuration or with the -u parameter")
|
||||
}
|
||||
apiURL = csConfig.API.Client.Credentials.URL
|
||||
|
||||
apiURL = cfg.API.Client.Credentials.URL
|
||||
}
|
||||
/*URL needs to end with /, but user doesn't care*/
|
||||
if !strings.HasSuffix(apiURL, "/") {
|
||||
|
@ -111,10 +116,12 @@ func runLapiRegister(cmd *cobra.Command, args []string) error {
|
|||
if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") {
|
||||
apiURL = "http://" + apiURL
|
||||
}
|
||||
|
||||
apiurl, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url: %w", err)
|
||||
}
|
||||
|
||||
_, err = apiclient.RegisterClient(&apiclient.Config{
|
||||
MachineID: lapiUser,
|
||||
Password: password,
|
||||
|
@ -130,138 +137,142 @@ func runLapiRegister(cmd *cobra.Command, args []string) error {
|
|||
log.Printf("Successfully registered to Local API (LAPI)")
|
||||
|
||||
var dumpFile string
|
||||
|
||||
if outputFile != "" {
|
||||
dumpFile = outputFile
|
||||
} else if csConfig.API.Client.CredentialsFilePath != "" {
|
||||
dumpFile = csConfig.API.Client.CredentialsFilePath
|
||||
} else if cfg.API.Client.CredentialsFilePath != "" {
|
||||
dumpFile = cfg.API.Client.CredentialsFilePath
|
||||
} else {
|
||||
dumpFile = ""
|
||||
}
|
||||
|
||||
apiCfg := csconfig.ApiCredentialsCfg{
|
||||
Login: lapiUser,
|
||||
Password: password.String(),
|
||||
URL: apiURL,
|
||||
}
|
||||
|
||||
apiConfigDump, err := yaml.Marshal(apiCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal api credentials: %w", err)
|
||||
}
|
||||
|
||||
if dumpFile != "" {
|
||||
err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err)
|
||||
}
|
||||
|
||||
log.Printf("Local API credentials written to '%s'", dumpFile)
|
||||
} else {
|
||||
fmt.Printf("%s\n", string(apiConfigDump))
|
||||
}
|
||||
|
||||
log.Warning(ReloadMessage())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLapiStatusCmd() *cobra.Command {
|
||||
func (cli *cliLapi) newStatusCmd() *cobra.Command {
|
||||
cmdLapiStatus := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check authentication to Local API (LAPI)",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: runLapiStatus,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cli.status()
|
||||
},
|
||||
}
|
||||
|
||||
return cmdLapiStatus
|
||||
}
|
||||
|
||||
func NewLapiRegisterCmd() *cobra.Command {
|
||||
cmdLapiRegister := &cobra.Command{
|
||||
func (cli *cliLapi) newRegisterCmd() *cobra.Command {
|
||||
var (
|
||||
apiURL string
|
||||
outputFile string
|
||||
machine string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a machine to Local API (LAPI)",
|
||||
Long: `Register your machine to the Local API (LAPI).
|
||||
Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: runLapiRegister,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.register(apiURL, outputFile, machine)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmdLapiRegister.Flags()
|
||||
flags.StringP("url", "u", "", "URL of the API (ie. http://127.0.0.1)")
|
||||
flags.StringP("file", "f", "", "output file destination")
|
||||
flags.String("machine", "", "Name of the machine to register with")
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)")
|
||||
flags.StringVarP(&outputFile, "file", "f", "", "output file destination")
|
||||
flags.StringVar(&machine, "machine", "", "Name of the machine to register with")
|
||||
|
||||
return cmdLapiRegister
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewLapiCmd() *cobra.Command {
|
||||
cmdLapi := &cobra.Command{
|
||||
func (cli *cliLapi) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "lapi [action]",
|
||||
Short: "Manage interaction with Local API (LAPI)",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := csConfig.LoadAPIClient(); err != nil {
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
if err := cli.cfg().LoadAPIClient(); err != nil {
|
||||
return fmt.Errorf("loading api client: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdLapi.AddCommand(NewLapiRegisterCmd())
|
||||
cmdLapi.AddCommand(NewLapiStatusCmd())
|
||||
cmdLapi.AddCommand(NewLapiContextCmd())
|
||||
cmd.AddCommand(cli.newRegisterCmd())
|
||||
cmd.AddCommand(cli.newStatusCmd())
|
||||
cmd.AddCommand(cli.newContextCmd())
|
||||
|
||||
return cmdLapi
|
||||
return cmd
|
||||
}
|
||||
|
||||
func AddContext(key string, values []string) error {
|
||||
func (cli *cliLapi) addContext(key string, values []string) error {
|
||||
cfg := cli.cfg()
|
||||
|
||||
if err := alertcontext.ValidateContextExpr(key, values); err != nil {
|
||||
return fmt.Errorf("invalid context configuration :%s", err)
|
||||
return fmt.Errorf("invalid context configuration: %w", err)
|
||||
}
|
||||
if _, ok := csConfig.Crowdsec.ContextToSend[key]; !ok {
|
||||
csConfig.Crowdsec.ContextToSend[key] = make([]string, 0)
|
||||
|
||||
if _, ok := cfg.Crowdsec.ContextToSend[key]; !ok {
|
||||
cfg.Crowdsec.ContextToSend[key] = make([]string, 0)
|
||||
|
||||
log.Infof("key '%s' added", key)
|
||||
}
|
||||
data := csConfig.Crowdsec.ContextToSend[key]
|
||||
|
||||
data := cfg.Crowdsec.ContextToSend[key]
|
||||
|
||||
for _, val := range values {
|
||||
if !slices.Contains(data, val) {
|
||||
log.Infof("value '%s' added to key '%s'", val, key)
|
||||
data = append(data, val)
|
||||
}
|
||||
csConfig.Crowdsec.ContextToSend[key] = data
|
||||
|
||||
cfg.Crowdsec.ContextToSend[key] = data
|
||||
}
|
||||
if err := csConfig.Crowdsec.DumpContextConfigFile(); err != nil {
|
||||
|
||||
if err := cfg.Crowdsec.DumpContextConfigFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLapiContextCmd() *cobra.Command {
|
||||
cmdContext := &cobra.Command{
|
||||
Use: "context [command]",
|
||||
Short: "Manage context to send with alerts",
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := csConfig.LoadCrowdsec(); err != nil {
|
||||
fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", csConfig.Crowdsec.ConsoleContextPath)
|
||||
if err.Error() != fileNotFoundMessage {
|
||||
return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err)
|
||||
}
|
||||
}
|
||||
if csConfig.DisableAgent {
|
||||
return errors.New("agent is disabled and lapi context can only be used on the agent")
|
||||
}
|
||||
func (cli *cliLapi) newContextAddCmd() *cobra.Command {
|
||||
var (
|
||||
keyToAdd string
|
||||
valuesToAdd []string
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
printHelp(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
var keyToAdd string
|
||||
var valuesToAdd []string
|
||||
cmdContextAdd := &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add context to send with alerts. You must specify the output key with the expr value you want",
|
||||
Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip
|
||||
|
@ -269,18 +280,18 @@ cscli lapi context add --key file_source --value evt.Line.Src
|
|||
cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
hub, err := require.Hub(csConfig, nil, nil)
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
hub, err := require.Hub(cli.cfg(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = alertcontext.LoadConsoleContext(csConfig, hub); err != nil {
|
||||
if err = alertcontext.LoadConsoleContext(cli.cfg(), hub); err != nil {
|
||||
return fmt.Errorf("while loading context: %w", err)
|
||||
}
|
||||
|
||||
if keyToAdd != "" {
|
||||
if err := AddContext(keyToAdd, valuesToAdd); err != nil {
|
||||
if err := cli.addContext(keyToAdd, valuesToAdd); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -290,7 +301,7 @@ cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
|
|||
keySlice := strings.Split(v, ".")
|
||||
key := keySlice[len(keySlice)-1]
|
||||
value := []string{v}
|
||||
if err := AddContext(key, value); err != nil {
|
||||
if err := cli.addContext(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -298,31 +309,37 @@ cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
|
|||
return nil
|
||||
},
|
||||
}
|
||||
cmdContextAdd.Flags().StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send")
|
||||
cmdContextAdd.Flags().StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key")
|
||||
cmdContextAdd.MarkFlagRequired("value")
|
||||
cmdContext.AddCommand(cmdContextAdd)
|
||||
|
||||
cmdContextStatus := &cobra.Command{
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send")
|
||||
flags.StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key")
|
||||
cmd.MarkFlagRequired("value")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliLapi) newContextStatusCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "List context to send with alerts",
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
hub, err := require.Hub(csConfig, nil, nil)
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
hub, err := require.Hub(cfg, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = alertcontext.LoadConsoleContext(csConfig, hub); err != nil {
|
||||
if err = alertcontext.LoadConsoleContext(cfg, hub); err != nil {
|
||||
return fmt.Errorf("while loading context: %w", err)
|
||||
}
|
||||
|
||||
if len(csConfig.Crowdsec.ContextToSend) == 0 {
|
||||
if len(cfg.Crowdsec.ContextToSend) == 0 {
|
||||
fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.")
|
||||
return nil
|
||||
}
|
||||
|
||||
dump, err := yaml.Marshal(csConfig.Crowdsec.ContextToSend)
|
||||
dump, err := yaml.Marshal(cfg.Crowdsec.ContextToSend)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show context status: %w", err)
|
||||
}
|
||||
|
@ -332,10 +349,14 @@ cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
|
|||
return nil
|
||||
},
|
||||
}
|
||||
cmdContext.AddCommand(cmdContextStatus)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliLapi) newContextDetectCmd() *cobra.Command {
|
||||
var detectAll bool
|
||||
cmdContextDetect := &cobra.Command{
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "detect",
|
||||
Short: "Detect available fields from the installed parsers",
|
||||
Example: `cscli lapi context detect --all
|
||||
|
@ -343,6 +364,7 @@ cscli lapi context detect crowdsecurity/sshd-logs
|
|||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
if !detectAll && len(args) == 0 {
|
||||
log.Infof("Please provide parsers to detect or --all flag.")
|
||||
printHelp(cmd)
|
||||
|
@ -355,13 +377,13 @@ cscli lapi context detect crowdsecurity/sshd-logs
|
|||
return fmt.Errorf("failed to init expr helpers: %w", err)
|
||||
}
|
||||
|
||||
hub, err := require.Hub(csConfig, nil, nil)
|
||||
hub, err := require.Hub(cfg, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
csParsers := parser.NewParsers(hub)
|
||||
if csParsers, err = parser.LoadParsers(csConfig, csParsers); err != nil {
|
||||
if csParsers, err = parser.LoadParsers(cfg, csParsers); err != nil {
|
||||
return fmt.Errorf("unable to load parsers: %w", err)
|
||||
}
|
||||
|
||||
|
@ -418,47 +440,85 @@ cscli lapi context detect crowdsecurity/sshd-logs
|
|||
return nil
|
||||
},
|
||||
}
|
||||
cmdContextDetect.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser")
|
||||
cmdContext.AddCommand(cmdContextDetect)
|
||||
cmd.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser")
|
||||
|
||||
cmdContextDelete := &cobra.Command{
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliLapi) newContextDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
filePath := csConfig.Crowdsec.ConsoleContextPath
|
||||
filePath := cli.cfg().Crowdsec.ConsoleContextPath
|
||||
if filePath == "" {
|
||||
filePath = "the context file"
|
||||
}
|
||||
fmt.Printf("Command \"delete\" is deprecated, please manually edit %s.", filePath)
|
||||
fmt.Printf("Command 'delete' is deprecated, please manually edit %s.", filePath)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmdContext.AddCommand(cmdContextDelete)
|
||||
|
||||
return cmdContext
|
||||
return cmd
|
||||
}
|
||||
|
||||
func detectStaticField(GrokStatics []parser.ExtraField) []string {
|
||||
func (cli *cliLapi) newContextCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "context [command]",
|
||||
Short: "Manage context to send with alerts",
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := cfg.LoadCrowdsec(); err != nil {
|
||||
fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", cfg.Crowdsec.ConsoleContextPath)
|
||||
if err.Error() != fileNotFoundMessage {
|
||||
return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err)
|
||||
}
|
||||
}
|
||||
if cfg.DisableAgent {
|
||||
return errors.New("agent is disabled and lapi context can only be used on the agent")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
printHelp(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newContextAddCmd())
|
||||
cmd.AddCommand(cli.newContextStatusCmd())
|
||||
cmd.AddCommand(cli.newContextDetectCmd())
|
||||
cmd.AddCommand(cli.newContextDeleteCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func detectStaticField(grokStatics []parser.ExtraField) []string {
|
||||
ret := make([]string, 0)
|
||||
|
||||
for _, static := range GrokStatics {
|
||||
for _, static := range grokStatics {
|
||||
if static.Parsed != "" {
|
||||
fieldName := fmt.Sprintf("evt.Parsed.%s", static.Parsed)
|
||||
if !slices.Contains(ret, fieldName) {
|
||||
ret = append(ret, fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
if static.Meta != "" {
|
||||
fieldName := fmt.Sprintf("evt.Meta.%s", static.Meta)
|
||||
if !slices.Contains(ret, fieldName) {
|
||||
ret = append(ret, fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
if static.TargetByName != "" {
|
||||
fieldName := static.TargetByName
|
||||
if !strings.HasPrefix(fieldName, "evt.") {
|
||||
fieldName = "evt." + fieldName
|
||||
}
|
||||
|
||||
if !slices.Contains(ret, fieldName) {
|
||||
ret = append(ret, fieldName)
|
||||
}
|
||||
|
@ -526,6 +586,7 @@ func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subnode.Grok.RegexpName != "" {
|
||||
grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName)
|
||||
if err == nil {
|
||||
|
|
|
@ -236,17 +236,17 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
|
|||
cmd.AddCommand(NewCLIMetrics(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIDashboard(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIDecisions(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIAlerts().NewCommand())
|
||||
cmd.AddCommand(NewCLIAlerts(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLISimulation(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIBouncers(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIMachines(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLICapi().NewCommand())
|
||||
cmd.AddCommand(NewLapiCmd())
|
||||
cmd.AddCommand(NewCLILapi(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCompletionCmd())
|
||||
cmd.AddCommand(NewConsoleCmd())
|
||||
cmd.AddCommand(NewCLIExplain().NewCommand())
|
||||
cmd.AddCommand(NewCLIConsole(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIExplain(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLIHubTest().NewCommand())
|
||||
cmd.AddCommand(NewCLINotifications().NewCommand())
|
||||
cmd.AddCommand(NewCLINotifications(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLISupport().NewCommand())
|
||||
cmd.AddCommand(NewCLIPapi(cli.cfg).NewCommand())
|
||||
cmd.AddCommand(NewCLICollection().NewCommand())
|
||||
|
|
|
@ -23,14 +23,13 @@ import (
|
|||
"github.com/crowdsecurity/go-cs-lib/ptr"
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csprofiles"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
type NotificationsCfg struct {
|
||||
|
@ -39,13 +38,17 @@ type NotificationsCfg struct {
|
|||
ids []uint
|
||||
}
|
||||
|
||||
type cliNotifications struct{}
|
||||
|
||||
func NewCLINotifications() *cliNotifications {
|
||||
return &cliNotifications{}
|
||||
type cliNotifications struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func (cli cliNotifications) NewCommand() *cobra.Command {
|
||||
func NewCLINotifications(cfg configGetter) *cliNotifications {
|
||||
return &cliNotifications{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliNotifications) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "notifications [action]",
|
||||
Short: "Helper for notification plugin configuration",
|
||||
|
@ -53,14 +56,15 @@ func (cli cliNotifications) NewCommand() *cobra.Command {
|
|||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"notifications", "notification"},
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := require.LAPI(csConfig); err != nil {
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := csConfig.LoadAPIClient(); err != nil {
|
||||
if err := cfg.LoadAPIClient(); err != nil {
|
||||
return fmt.Errorf("loading api client: %w", err)
|
||||
}
|
||||
if err := require.Notifications(csConfig); err != nil {
|
||||
if err := require.Notifications(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -76,67 +80,79 @@ func (cli cliNotifications) NewCommand() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func getPluginConfigs() (map[string]csplugin.PluginConfig, error) {
|
||||
func (cli *cliNotifications) getPluginConfigs() (map[string]csplugin.PluginConfig, error) {
|
||||
cfg := cli.cfg()
|
||||
pcfgs := map[string]csplugin.PluginConfig{}
|
||||
wf := func(path string, info fs.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return fmt.Errorf("error while traversing directory %s: %w", path, err)
|
||||
}
|
||||
name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
|
||||
|
||||
name := filepath.Join(cfg.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
|
||||
if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
|
||||
ts, err := csplugin.ParsePluginConfigFile(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err)
|
||||
}
|
||||
|
||||
for _, t := range ts {
|
||||
csplugin.SetRequiredFields(&t)
|
||||
pcfgs[t.Name] = t
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil {
|
||||
if err := filepath.Walk(cfg.ConfigPaths.NotificationDir, wf); err != nil {
|
||||
return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err)
|
||||
}
|
||||
|
||||
return pcfgs, nil
|
||||
}
|
||||
|
||||
func getProfilesConfigs() (map[string]NotificationsCfg, error) {
|
||||
func (cli *cliNotifications) getProfilesConfigs() (map[string]NotificationsCfg, error) {
|
||||
cfg := cli.cfg()
|
||||
// A bit of a tricky stuf now: reconcile profiles and notification plugins
|
||||
pcfgs, err := getPluginConfigs()
|
||||
pcfgs, err := cli.getPluginConfigs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ncfgs := map[string]NotificationsCfg{}
|
||||
for _, pc := range pcfgs {
|
||||
ncfgs[pc.Name] = NotificationsCfg{
|
||||
Config: pc,
|
||||
}
|
||||
}
|
||||
profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
|
||||
|
||||
profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while extracting profiles from configuration: %w", err)
|
||||
}
|
||||
|
||||
for profileID, profile := range profiles {
|
||||
for _, notif := range profile.Cfg.Notifications {
|
||||
pc, ok := pcfgs[notif]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("notification plugin '%s' does not exist", notif)
|
||||
}
|
||||
|
||||
tmp, ok := ncfgs[pc.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name)
|
||||
}
|
||||
|
||||
tmp.Profiles = append(tmp.Profiles, profile.Cfg)
|
||||
tmp.ids = append(tmp.ids, uint(profileID))
|
||||
ncfgs[pc.Name] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
return ncfgs, nil
|
||||
}
|
||||
|
||||
func (cli cliNotifications) NewListCmd() *cobra.Command {
|
||||
func (cli *cliNotifications) NewListCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list active notifications plugins",
|
||||
|
@ -144,21 +160,22 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
|
|||
Example: `cscli notifications list`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, arg []string) error {
|
||||
ncfgs, err := getProfilesConfigs()
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
ncfgs, err := cli.getProfilesConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't build profiles configuration: %w", err)
|
||||
}
|
||||
|
||||
if csConfig.Cscli.Output == "human" {
|
||||
if cfg.Cscli.Output == "human" {
|
||||
notificationListTable(color.Output, ncfgs)
|
||||
} else if csConfig.Cscli.Output == "json" {
|
||||
} else if cfg.Cscli.Output == "json" {
|
||||
x, err := json.MarshalIndent(ncfgs, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification configuration: %w", err)
|
||||
}
|
||||
fmt.Printf("%s", string(x))
|
||||
} else if csConfig.Cscli.Output == "raw" {
|
||||
} else if cfg.Cscli.Output == "raw" {
|
||||
csvwriter := csv.NewWriter(os.Stdout)
|
||||
err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
|
||||
if err != nil {
|
||||
|
@ -176,6 +193,7 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
|
|||
}
|
||||
csvwriter.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -183,7 +201,7 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func (cli cliNotifications) NewInspectCmd() *cobra.Command {
|
||||
func (cli *cliNotifications) NewInspectCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect",
|
||||
Short: "Inspect active notifications plugin configuration",
|
||||
|
@ -191,36 +209,32 @@ func (cli cliNotifications) NewInspectCmd() *cobra.Command {
|
|||
Example: `cscli notifications inspect <plugin_name>`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "" {
|
||||
return fmt.Errorf("please provide a plugin name to inspect")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ncfgs, err := getProfilesConfigs()
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
ncfgs, err := cli.getProfilesConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't build profiles configuration: %w", err)
|
||||
}
|
||||
cfg, ok := ncfgs[args[0]]
|
||||
ncfg, ok := ncfgs[args[0]]
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin '%s' does not exist or is not active", args[0])
|
||||
}
|
||||
if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" {
|
||||
fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type)
|
||||
fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name)
|
||||
fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut)
|
||||
fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format)
|
||||
for k, v := range cfg.Config.Config {
|
||||
if cfg.Cscli.Output == "human" || cfg.Cscli.Output == "raw" {
|
||||
fmt.Printf(" - %15s: %15s\n", "Type", ncfg.Config.Type)
|
||||
fmt.Printf(" - %15s: %15s\n", "Name", ncfg.Config.Name)
|
||||
fmt.Printf(" - %15s: %15s\n", "Timeout", ncfg.Config.TimeOut)
|
||||
fmt.Printf(" - %15s: %15s\n", "Format", ncfg.Config.Format)
|
||||
for k, v := range ncfg.Config.Config {
|
||||
fmt.Printf(" - %15s: %15v\n", k, v)
|
||||
}
|
||||
} else if csConfig.Cscli.Output == "json" {
|
||||
} else if cfg.Cscli.Output == "json" {
|
||||
x, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification configuration: %w", err)
|
||||
}
|
||||
fmt.Printf("%s", string(x))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -228,12 +242,13 @@ func (cli cliNotifications) NewInspectCmd() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func (cli cliNotifications) NewTestCmd() *cobra.Command {
|
||||
func (cli *cliNotifications) NewTestCmd() *cobra.Command {
|
||||
var (
|
||||
pluginBroker csplugin.PluginBroker
|
||||
pluginTomb tomb.Tomb
|
||||
alertOverride string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "test [plugin name]",
|
||||
Short: "send a generic test alert to notification plugin",
|
||||
|
@ -241,25 +256,26 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
|
|||
Example: `cscli notifications test [plugin_name]`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
pconfigs, err := getPluginConfigs()
|
||||
PreRunE: func(_ *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
pconfigs, err := cli.getPluginConfigs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't build profiles configuration: %w", err)
|
||||
}
|
||||
cfg, ok := pconfigs[args[0]]
|
||||
pcfg, ok := pconfigs[args[0]]
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin name: '%s' does not exist", args[0])
|
||||
}
|
||||
//Create a single profile with plugin name as notification name
|
||||
return pluginBroker.Init(csConfig.PluginConfig, []*csconfig.ProfileCfg{
|
||||
return pluginBroker.Init(cfg.PluginConfig, []*csconfig.ProfileCfg{
|
||||
{
|
||||
Notifications: []string{
|
||||
cfg.Name,
|
||||
pcfg.Name,
|
||||
},
|
||||
},
|
||||
}, csConfig.ConfigPaths)
|
||||
}, cfg.ConfigPaths)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
pluginTomb.Go(func() error {
|
||||
pluginBroker.Run(&pluginTomb)
|
||||
return nil
|
||||
|
@ -298,13 +314,16 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
|
|||
if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal alert override: %w", err)
|
||||
}
|
||||
|
||||
pluginBroker.PluginChannel <- csplugin.ProfileAlert{
|
||||
ProfileID: uint(0),
|
||||
Alert: alert,
|
||||
}
|
||||
|
||||
//time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
|
||||
pluginTomb.Kill(fmt.Errorf("terminating"))
|
||||
pluginTomb.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -313,9 +332,11 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func (cli cliNotifications) NewReinjectCmd() *cobra.Command {
|
||||
var alertOverride string
|
||||
var alert *models.Alert
|
||||
func (cli *cliNotifications) NewReinjectCmd() *cobra.Command {
|
||||
var (
|
||||
alertOverride string
|
||||
alert *models.Alert
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "reinject",
|
||||
|
@ -328,25 +349,30 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
|
|||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
PreRunE: func(_ *cobra.Command, args []string) error {
|
||||
var err error
|
||||
alert, err = FetchAlertFromArgString(args[0])
|
||||
alert, err = cli.fetchAlertFromArgString(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
var (
|
||||
pluginBroker csplugin.PluginBroker
|
||||
pluginTomb tomb.Tomb
|
||||
)
|
||||
|
||||
cfg := cli.cfg()
|
||||
|
||||
if alertOverride != "" {
|
||||
if err := json.Unmarshal([]byte(alertOverride), alert); err != nil {
|
||||
return fmt.Errorf("can't unmarshal data in the alert flag: %w", err)
|
||||
}
|
||||
}
|
||||
err := pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths)
|
||||
|
||||
err := pluginBroker.Init(cfg.PluginConfig, cfg.API.Server.Profiles, cfg.ConfigPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't initialize plugins: %w", err)
|
||||
}
|
||||
|
@ -356,7 +382,7 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
|
|||
return nil
|
||||
})
|
||||
|
||||
profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
|
||||
profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot extract profiles from configuration: %w", err)
|
||||
}
|
||||
|
@ -382,9 +408,9 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
|
|||
default:
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
log.Info("sleeping\n")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if profile.Cfg.OnSuccess == "break" {
|
||||
log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
|
||||
break
|
||||
|
@ -393,6 +419,7 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
|
|||
//time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
|
||||
pluginTomb.Kill(fmt.Errorf("terminating"))
|
||||
pluginTomb.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -401,18 +428,22 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
|
|||
return cmd
|
||||
}
|
||||
|
||||
func FetchAlertFromArgString(toParse string) (*models.Alert, error) {
|
||||
func (cli *cliNotifications) fetchAlertFromArgString(toParse string) (*models.Alert, error) {
|
||||
cfg := cli.cfg()
|
||||
|
||||
id, err := strconv.Atoi(toParse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad alert id %s", toParse)
|
||||
}
|
||||
apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
|
||||
|
||||
apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing the URL of the API: %w", err)
|
||||
}
|
||||
|
||||
client, err := apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: csConfig.API.Client.Credentials.Login,
|
||||
Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
|
||||
MachineID: cfg.API.Client.Credentials.Login,
|
||||
Password: strfmt.Password(cfg.API.Client.Credentials.Password),
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
VersionPrefix: "v1",
|
||||
|
@ -420,9 +451,11 @@ func FetchAlertFromArgString(toParse string) (*models.Alert, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating the client for the API: %w", err)
|
||||
}
|
||||
|
||||
alert, _, err := client.Alerts.GetByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't find alert with id %d: %w", id, err)
|
||||
}
|
||||
|
||||
return alert, nil
|
||||
}
|
||||
|
|
|
@ -56,7 +56,8 @@ func initAPIServer(cConfig *csconfig.Config) (*apiserver.APIServer, error) {
|
|||
return apiServer, nil
|
||||
}
|
||||
|
||||
func serveAPIServer(apiServer *apiserver.APIServer, apiReady chan bool) {
|
||||
func serveAPIServer(apiServer *apiserver.APIServer) {
|
||||
apiReady := make(chan bool, 1)
|
||||
apiTomb.Go(func() error {
|
||||
defer trace.CatchPanic("crowdsec/serveAPIServer")
|
||||
go func() {
|
||||
|
@ -80,6 +81,7 @@ func serveAPIServer(apiServer *apiserver.APIServer, apiReady chan bool) {
|
|||
}
|
||||
return nil
|
||||
})
|
||||
<-apiReady
|
||||
}
|
||||
|
||||
func hasPlugins(profiles []*csconfig.ProfileCfg) bool {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -13,8 +14,8 @@ import (
|
|||
"github.com/crowdsecurity/go-cs-lib/trace"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/alertcontext"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/appsec"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
||||
|
@ -56,63 +57,86 @@ func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.H
|
|||
|
||||
//start go-routines for parsing, buckets pour and outputs.
|
||||
parserWg := &sync.WaitGroup{}
|
||||
|
||||
parsersTomb.Go(func() error {
|
||||
parserWg.Add(1)
|
||||
|
||||
for i := 0; i < cConfig.Crowdsec.ParserRoutinesCount; i++ {
|
||||
parsersTomb.Go(func() error {
|
||||
defer trace.CatchPanic("crowdsec/runParse")
|
||||
|
||||
if err := runParse(inputLineChan, inputEventChan, *parsers.Ctx, parsers.Nodes); err != nil { //this error will never happen as parser.Parse is not able to return errors
|
||||
log.Fatalf("starting parse error : %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
parserWg.Done()
|
||||
|
||||
return nil
|
||||
})
|
||||
parserWg.Wait()
|
||||
|
||||
bucketWg := &sync.WaitGroup{}
|
||||
|
||||
bucketsTomb.Go(func() error {
|
||||
bucketWg.Add(1)
|
||||
/*restore previous state as well if present*/
|
||||
if cConfig.Crowdsec.BucketStateFile != "" {
|
||||
log.Warningf("Restoring buckets state from %s", cConfig.Crowdsec.BucketStateFile)
|
||||
|
||||
if err := leaky.LoadBucketsState(cConfig.Crowdsec.BucketStateFile, buckets, holders); err != nil {
|
||||
return fmt.Errorf("unable to restore buckets : %s", err)
|
||||
return fmt.Errorf("unable to restore buckets: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < cConfig.Crowdsec.BucketsRoutinesCount; i++ {
|
||||
bucketsTomb.Go(func() error {
|
||||
defer trace.CatchPanic("crowdsec/runPour")
|
||||
|
||||
if err := runPour(inputEventChan, holders, buckets, cConfig); err != nil {
|
||||
log.Fatalf("starting pour error : %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
bucketWg.Done()
|
||||
|
||||
return nil
|
||||
})
|
||||
bucketWg.Wait()
|
||||
|
||||
apiClient, err := AuthenticatedLAPIClient(*cConfig.API.Client.Credentials, hub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Starting HeartBeat service")
|
||||
apiClient.HeartBeat.StartHeartBeat(context.Background(), &outputsTomb)
|
||||
|
||||
outputWg := &sync.WaitGroup{}
|
||||
|
||||
outputsTomb.Go(func() error {
|
||||
outputWg.Add(1)
|
||||
|
||||
for i := 0; i < cConfig.Crowdsec.OutputRoutinesCount; i++ {
|
||||
outputsTomb.Go(func() error {
|
||||
defer trace.CatchPanic("crowdsec/runOutput")
|
||||
if err := runOutput(inputEventChan, outputEventChan, buckets, *parsers.Povfwctx, parsers.Povfwnodes, *cConfig.API.Client.Credentials, hub); err != nil {
|
||||
|
||||
if err := runOutput(inputEventChan, outputEventChan, buckets, *parsers.Povfwctx, parsers.Povfwnodes, apiClient); err != nil {
|
||||
log.Fatalf("starting outputs error : %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
outputWg.Done()
|
||||
|
||||
return nil
|
||||
})
|
||||
outputWg.Wait()
|
||||
|
@ -122,16 +146,16 @@ func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.H
|
|||
if cConfig.Prometheus.Level == "aggregated" {
|
||||
aggregated = true
|
||||
}
|
||||
|
||||
if err := acquisition.GetMetrics(dataSources, aggregated); err != nil {
|
||||
return fmt.Errorf("while fetching prometheus metrics for datasources: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.Info("Starting processing data")
|
||||
|
||||
if err := acquisition.StartAcquisition(dataSources, inputLineChan, &acquisTomb); err != nil {
|
||||
log.Fatalf("starting acquisition error : %s", err)
|
||||
return err
|
||||
return fmt.Errorf("starting acquisition error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -140,11 +164,13 @@ func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.H
|
|||
func serveCrowdsec(parsers *parser.Parsers, cConfig *csconfig.Config, hub *cwhub.Hub, agentReady chan bool) {
|
||||
crowdsecTomb.Go(func() error {
|
||||
defer trace.CatchPanic("crowdsec/serveCrowdsec")
|
||||
|
||||
go func() {
|
||||
defer trace.CatchPanic("crowdsec/runCrowdsec")
|
||||
// this logs every time, even at config reload
|
||||
log.Debugf("running agent after %s ms", time.Since(crowdsecT0))
|
||||
agentReady <- true
|
||||
|
||||
if err := runCrowdsec(cConfig, parsers, hub); err != nil {
|
||||
log.Fatalf("unable to start crowdsec routines: %s", err)
|
||||
}
|
||||
|
@ -156,16 +182,20 @@ func serveCrowdsec(parsers *parser.Parsers, cConfig *csconfig.Config, hub *cwhub
|
|||
*/
|
||||
waitOnTomb()
|
||||
log.Debugf("Shutting down crowdsec routines")
|
||||
|
||||
if err := ShutdownCrowdsecRoutines(); err != nil {
|
||||
log.Fatalf("unable to shutdown crowdsec routines: %s", err)
|
||||
}
|
||||
|
||||
log.Debugf("everything is dead, return crowdsecTomb")
|
||||
|
||||
if dumpStates {
|
||||
dumpParserState()
|
||||
dumpOverflowState()
|
||||
dumpBucketsPour()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -175,55 +205,65 @@ func dumpBucketsPour() {
|
|||
if err != nil {
|
||||
log.Fatalf("open: %s", err)
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(leaky.BucketPourCache)
|
||||
if err != nil {
|
||||
log.Fatalf("marshal: %s", err)
|
||||
}
|
||||
|
||||
b, err := fd.Write(out)
|
||||
if err != nil {
|
||||
log.Fatalf("write: %s", err)
|
||||
}
|
||||
|
||||
log.Tracef("wrote %d bytes", b)
|
||||
|
||||
if err := fd.Close(); err != nil {
|
||||
log.Fatalf(" close: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpParserState() {
|
||||
|
||||
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "parser-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("open: %s", err)
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(parser.StageParseCache)
|
||||
if err != nil {
|
||||
log.Fatalf("marshal: %s", err)
|
||||
}
|
||||
|
||||
b, err := fd.Write(out)
|
||||
if err != nil {
|
||||
log.Fatalf("write: %s", err)
|
||||
}
|
||||
|
||||
log.Tracef("wrote %d bytes", b)
|
||||
|
||||
if err := fd.Close(); err != nil {
|
||||
log.Fatalf(" close: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpOverflowState() {
|
||||
|
||||
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "bucket-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("open: %s", err)
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(bucketOverflows)
|
||||
if err != nil {
|
||||
log.Fatalf("marshal: %s", err)
|
||||
}
|
||||
|
||||
b, err := fd.Write(out)
|
||||
if err != nil {
|
||||
log.Fatalf("write: %s", err)
|
||||
}
|
||||
|
||||
log.Tracef("wrote %d bytes", b)
|
||||
|
||||
if err := fd.Close(); err != nil {
|
||||
log.Fatalf(" close: %s", err)
|
||||
}
|
||||
|
|
92
cmd/crowdsec/lapiclient.go
Normal file
92
cmd/crowdsec/lapiclient.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
)
|
||||
|
||||
func AuthenticatedLAPIClient(credentials csconfig.ApiCredentialsCfg, hub *cwhub.Hub) (*apiclient.ApiClient, error) {
|
||||
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading list of installed hub scenarios: %w", err)
|
||||
}
|
||||
|
||||
appsecRules, err := hub.GetInstalledItemNames(cwhub.APPSEC_RULES)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading list of installed hub appsec rules: %w", err)
|
||||
}
|
||||
|
||||
installedScenariosAndAppsecRules := make([]string, 0, len(scenarios)+len(appsecRules))
|
||||
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, scenarios...)
|
||||
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, appsecRules...)
|
||||
|
||||
apiURL, err := url.Parse(credentials.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing api url ('%s'): %w", credentials.URL, err)
|
||||
}
|
||||
|
||||
papiURL, err := url.Parse(credentials.PapiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing polling api url ('%s'): %w", credentials.PapiURL, err)
|
||||
}
|
||||
|
||||
password := strfmt.Password(credentials.Password)
|
||||
|
||||
client, err := apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: credentials.Login,
|
||||
Password: password,
|
||||
Scenarios: installedScenariosAndAppsecRules,
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
PapiURL: papiURL,
|
||||
VersionPrefix: "v1",
|
||||
UpdateScenario: func() ([]string, error) {
|
||||
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appsecRules, err := hub.GetInstalledItemNames(cwhub.APPSEC_RULES)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]string, 0, len(scenarios)+len(appsecRules))
|
||||
ret = append(ret, scenarios...)
|
||||
ret = append(ret, appsecRules...)
|
||||
|
||||
return ret, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new client api: %w", err)
|
||||
}
|
||||
|
||||
authResp, _, err := client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
|
||||
MachineID: &credentials.Login,
|
||||
Password: &password,
|
||||
Scenarios: installedScenariosAndAppsecRules,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authenticate watcher (%s): %w", credentials.Login, err)
|
||||
}
|
||||
|
||||
var expiration time.Time
|
||||
if err := expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse jwt expiration: %w", err)
|
||||
}
|
||||
|
||||
client.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
|
||||
client.GetClient().Transport.(*apiclient.JWTTransport).Expiration = expiration
|
||||
|
||||
return client, nil
|
||||
}
|
|
@ -114,13 +114,17 @@ func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.Ha
|
|||
}
|
||||
|
||||
decisionsFilters := make(map[string][]string, 0)
|
||||
|
||||
decisions, err := dbClient.QueryDecisionCountByScenario(decisionsFilters)
|
||||
if err != nil {
|
||||
log.Errorf("Error querying decisions for metrics: %v", err)
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
globalActiveDecisions.Reset()
|
||||
|
||||
for _, d := range decisions {
|
||||
globalActiveDecisions.With(prometheus.Labels{"reason": d.Scenario, "origin": d.Origin, "action": d.Type}).Set(float64(d.Count))
|
||||
}
|
||||
|
@ -136,6 +140,7 @@ func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.Ha
|
|||
if err != nil {
|
||||
log.Errorf("Error querying alerts for metrics: %v", err)
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -173,11 +178,12 @@ func registerPrometheus(config *csconfig.PrometheusCfg) {
|
|||
globalActiveDecisions, globalAlerts, parser.NodesWlHitsOk, parser.NodesWlHits,
|
||||
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client, apiReady chan bool, agentReady chan bool) {
|
||||
func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client, agentReady chan bool) {
|
||||
<-agentReady
|
||||
|
||||
if !config.Enabled {
|
||||
return
|
||||
}
|
||||
|
@ -185,9 +191,8 @@ func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client,
|
|||
defer trace.CatchPanic("crowdsec/servePrometheus")
|
||||
|
||||
http.Handle("/metrics", computeDynamicMetrics(promhttp.Handler(), dbClient))
|
||||
<-apiReady
|
||||
<-agentReady
|
||||
log.Debugf("serving metrics after %s ms", time.Since(crowdsecT0))
|
||||
|
||||
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", config.ListenAddr, config.ListenPort), nil); err != nil {
|
||||
log.Warningf("prometheus: %s", err)
|
||||
}
|
||||
|
|
|
@ -3,18 +3,12 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/parser"
|
||||
|
@ -22,7 +16,6 @@ import (
|
|||
)
|
||||
|
||||
func dedupAlerts(alerts []types.RuntimeAlert) ([]*models.Alert, error) {
|
||||
|
||||
var dedupCache []*models.Alert
|
||||
|
||||
for idx, alert := range alerts {
|
||||
|
@ -32,16 +25,21 @@ func dedupAlerts(alerts []types.RuntimeAlert) ([]*models.Alert, error) {
|
|||
dedupCache = append(dedupCache, alert.Alert)
|
||||
continue
|
||||
}
|
||||
|
||||
for k, src := range alert.Sources {
|
||||
refsrc := *alert.Alert //copy
|
||||
|
||||
log.Tracef("source[%s]", k)
|
||||
|
||||
refsrc.Source = &src
|
||||
dedupCache = append(dedupCache, &refsrc)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dedupCache) != len(alerts) {
|
||||
log.Tracef("went from %d to %d alerts", len(alerts), len(dedupCache))
|
||||
}
|
||||
|
||||
return dedupCache, nil
|
||||
}
|
||||
|
||||
|
@ -52,93 +50,25 @@ func PushAlerts(alerts []types.RuntimeAlert, client *apiclient.ApiClient) error
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to transform alerts for api: %w", err)
|
||||
}
|
||||
|
||||
_, _, err = client.Alerts.Add(ctx, alertsToPush)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed sending alert to LAPI: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var bucketOverflows []types.Event
|
||||
|
||||
func runOutput(input chan types.Event, overflow chan types.Event, buckets *leaky.Buckets,
|
||||
postOverflowCTX parser.UnixParserCtx, postOverflowNodes []parser.Node,
|
||||
apiConfig csconfig.ApiCredentialsCfg, hub *cwhub.Hub) error {
|
||||
func runOutput(input chan types.Event, overflow chan types.Event, buckets *leaky.Buckets, postOverflowCTX parser.UnixParserCtx,
|
||||
postOverflowNodes []parser.Node, client *apiclient.ApiClient) error {
|
||||
var (
|
||||
cache []types.RuntimeAlert
|
||||
cacheMutex sync.Mutex
|
||||
)
|
||||
|
||||
var err error
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
|
||||
var cache []types.RuntimeAlert
|
||||
var cacheMutex sync.Mutex
|
||||
|
||||
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading list of installed hub scenarios: %w", err)
|
||||
}
|
||||
|
||||
appsecRules, err := hub.GetInstalledItemNames(cwhub.APPSEC_RULES)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading list of installed hub appsec rules: %w", err)
|
||||
}
|
||||
|
||||
installedScenariosAndAppsecRules := make([]string, 0, len(scenarios)+len(appsecRules))
|
||||
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, scenarios...)
|
||||
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, appsecRules...)
|
||||
|
||||
apiURL, err := url.Parse(apiConfig.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url ('%s'): %w", apiConfig.URL, err)
|
||||
}
|
||||
papiURL, err := url.Parse(apiConfig.PapiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing polling api url ('%s'): %w", apiConfig.PapiURL, err)
|
||||
}
|
||||
password := strfmt.Password(apiConfig.Password)
|
||||
|
||||
Client, err := apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: apiConfig.Login,
|
||||
Password: password,
|
||||
Scenarios: installedScenariosAndAppsecRules,
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
PapiURL: papiURL,
|
||||
VersionPrefix: "v1",
|
||||
UpdateScenario: func() ([]string, error) {
|
||||
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appsecRules, err := hub.GetInstalledItemNames(cwhub.APPSEC_RULES)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]string, 0, len(scenarios)+len(appsecRules))
|
||||
ret = append(ret, scenarios...)
|
||||
ret = append(ret, appsecRules...)
|
||||
return ret, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("new client api: %w", err)
|
||||
}
|
||||
authResp, _, err := Client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
|
||||
MachineID: &apiConfig.Login,
|
||||
Password: &password,
|
||||
Scenarios: installedScenariosAndAppsecRules,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("authenticate watcher (%s): %w", apiConfig.Login, err)
|
||||
}
|
||||
|
||||
if err := Client.GetClient().Transport.(*apiclient.JWTTransport).Expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
|
||||
return fmt.Errorf("unable to parse jwt expiration: %w", err)
|
||||
}
|
||||
|
||||
Client.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
|
||||
|
||||
//start the heartbeat service
|
||||
log.Debugf("Starting HeartBeat service")
|
||||
Client.HeartBeat.StartHeartBeat(context.Background(), &outputsTomb)
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
|
@ -149,7 +79,7 @@ LOOP:
|
|||
newcache := make([]types.RuntimeAlert, 0)
|
||||
cache = newcache
|
||||
cacheMutex.Unlock()
|
||||
if err := PushAlerts(cachecopy, Client); err != nil {
|
||||
if err := PushAlerts(cachecopy, client); err != nil {
|
||||
log.Errorf("while pushing to api : %s", err)
|
||||
//just push back the events to the queue
|
||||
cacheMutex.Lock()
|
||||
|
@ -162,10 +92,11 @@ LOOP:
|
|||
cacheMutex.Lock()
|
||||
cachecopy := cache
|
||||
cacheMutex.Unlock()
|
||||
if err := PushAlerts(cachecopy, Client); err != nil {
|
||||
if err := PushAlerts(cachecopy, client); err != nil {
|
||||
log.Errorf("while pushing leftovers to api : %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
break LOOP
|
||||
case event := <-overflow:
|
||||
/*if alert is empty and mapKey is present, the overflow is just to cleanup bucket*/
|
||||
|
@ -176,7 +107,7 @@ LOOP:
|
|||
/* process post overflow parser nodes */
|
||||
event, err := parser.Parse(postOverflowCTX, event, postOverflowNodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("postoverflow failed : %s", err)
|
||||
return fmt.Errorf("postoverflow failed: %w", err)
|
||||
}
|
||||
log.Printf("%s", *event.Overflow.Alert.Message)
|
||||
//if the Alert is nil, it's to signal bucket is ready for GC, don't track this
|
||||
|
@ -206,6 +137,6 @@ LOOP:
|
|||
}
|
||||
|
||||
ticker.Stop()
|
||||
return nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ func StartRunSvc() error {
|
|||
|
||||
log.Infof("Crowdsec %s", version.String())
|
||||
|
||||
apiReady := make(chan bool, 1)
|
||||
agentReady := make(chan bool, 1)
|
||||
|
||||
// Enable profiling early
|
||||
|
@ -46,14 +45,19 @@ func StartRunSvc() error {
|
|||
dbClient, err = database.NewClient(cConfig.DbConfig)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create database client: %s", err)
|
||||
return fmt.Errorf("unable to create database client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
registerPrometheus(cConfig.Prometheus)
|
||||
|
||||
go servePrometheus(cConfig.Prometheus, dbClient, apiReady, agentReady)
|
||||
go servePrometheus(cConfig.Prometheus, dbClient, agentReady)
|
||||
} else {
|
||||
// avoid leaking the channel
|
||||
go func() {
|
||||
<-agentReady
|
||||
}()
|
||||
}
|
||||
|
||||
return Serve(cConfig, apiReady, agentReady)
|
||||
return Serve(cConfig, agentReady)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,6 @@ func WindowsRun() error {
|
|||
|
||||
log.Infof("Crowdsec %s", version.String())
|
||||
|
||||
apiReady := make(chan bool, 1)
|
||||
agentReady := make(chan bool, 1)
|
||||
|
||||
// Enable profiling early
|
||||
|
@ -85,11 +84,11 @@ func WindowsRun() error {
|
|||
dbClient, err = database.NewClient(cConfig.DbConfig)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create database client: %s", err)
|
||||
return fmt.Errorf("unable to create database client: %w", err)
|
||||
}
|
||||
}
|
||||
registerPrometheus(cConfig.Prometheus)
|
||||
go servePrometheus(cConfig.Prometheus, dbClient, apiReady, agentReady)
|
||||
go servePrometheus(cConfig.Prometheus, dbClient, agentReady)
|
||||
}
|
||||
return Serve(cConfig, apiReady, agentReady)
|
||||
return Serve(cConfig, agentReady)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,9 @@ func debugHandler(sig os.Signal, cConfig *csconfig.Config) error {
|
|||
if err := leaky.ShutdownAllBuckets(buckets); err != nil {
|
||||
log.Warningf("Failed to shut down routines : %s", err)
|
||||
}
|
||||
|
||||
log.Printf("Shutdown is finished, buckets are in %s", tmpFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -66,15 +68,16 @@ func reloadHandler(sig os.Signal) (*csconfig.Config, error) {
|
|||
if !cConfig.DisableAPI {
|
||||
if flags.DisableCAPI {
|
||||
log.Warningf("Communication with CrowdSec Central API disabled from args")
|
||||
|
||||
cConfig.API.Server.OnlineClient = nil
|
||||
}
|
||||
|
||||
apiServer, err := initAPIServer(cConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to init api server: %w", err)
|
||||
}
|
||||
|
||||
apiReady := make(chan bool, 1)
|
||||
serveAPIServer(apiServer, apiReady)
|
||||
serveAPIServer(apiServer)
|
||||
}
|
||||
|
||||
if !cConfig.DisableAgent {
|
||||
|
@ -110,6 +113,7 @@ func reloadHandler(sig os.Signal) (*csconfig.Config, error) {
|
|||
log.Warningf("Failed to delete temp file (%s) : %s", tmpFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
return cConfig, nil
|
||||
}
|
||||
|
||||
|
@ -117,10 +121,12 @@ func ShutdownCrowdsecRoutines() error {
|
|||
var reterr error
|
||||
|
||||
log.Debugf("Shutting down crowdsec sub-routines")
|
||||
|
||||
if len(dataSources) > 0 {
|
||||
acquisTomb.Kill(nil)
|
||||
log.Debugf("waiting for acquisition to finish")
|
||||
drainChan(inputLineChan)
|
||||
|
||||
if err := acquisTomb.Wait(); err != nil {
|
||||
log.Warningf("Acquisition returned error : %s", err)
|
||||
reterr = err
|
||||
|
@ -130,6 +136,7 @@ func ShutdownCrowdsecRoutines() error {
|
|||
log.Debugf("acquisition is finished, wait for parser/bucket/ouputs.")
|
||||
parsersTomb.Kill(nil)
|
||||
drainChan(inputEventChan)
|
||||
|
||||
if err := parsersTomb.Wait(); err != nil {
|
||||
log.Warningf("Parsers returned error : %s", err)
|
||||
reterr = err
|
||||
|
@ -160,6 +167,7 @@ func ShutdownCrowdsecRoutines() error {
|
|||
log.Warningf("Outputs returned error : %s", err)
|
||||
reterr = err
|
||||
}
|
||||
|
||||
log.Debugf("outputs are done")
|
||||
case <-time.After(3 * time.Second):
|
||||
// this can happen if outputs are stuck in a http retry loop
|
||||
|
@ -181,6 +189,7 @@ func shutdownAPI() error {
|
|||
}
|
||||
|
||||
log.Debugf("done")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -193,6 +202,7 @@ func shutdownCrowdsec() error {
|
|||
}
|
||||
|
||||
log.Debugf("done")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -292,10 +302,11 @@ func HandleSignals(cConfig *csconfig.Config) error {
|
|||
if err == nil {
|
||||
log.Warning("Crowdsec service shutting down")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) error {
|
||||
func Serve(cConfig *csconfig.Config, agentReady chan bool) error {
|
||||
acquisTomb = tomb.Tomb{}
|
||||
parsersTomb = tomb.Tomb{}
|
||||
bucketsTomb = tomb.Tomb{}
|
||||
|
@ -325,6 +336,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
|
||||
if cConfig.API.CTI != nil && *cConfig.API.CTI.Enabled {
|
||||
log.Infof("Crowdsec CTI helper enabled")
|
||||
|
||||
if err := exprhelpers.InitCrowdsecCTI(cConfig.API.CTI.Key, cConfig.API.CTI.CacheTimeout, cConfig.API.CTI.CacheSize, cConfig.API.CTI.LogLevel); err != nil {
|
||||
return fmt.Errorf("failed to init crowdsec cti: %w", err)
|
||||
}
|
||||
|
@ -337,6 +349,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
|
||||
if flags.DisableCAPI {
|
||||
log.Warningf("Communication with CrowdSec Central API disabled from args")
|
||||
|
||||
cConfig.API.Server.OnlineClient = nil
|
||||
}
|
||||
|
||||
|
@ -346,10 +359,8 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
}
|
||||
|
||||
if !flags.TestMode {
|
||||
serveAPIServer(apiServer, apiReady)
|
||||
serveAPIServer(apiServer)
|
||||
}
|
||||
} else {
|
||||
apiReady <- true
|
||||
}
|
||||
|
||||
if !cConfig.DisableAgent {
|
||||
|
@ -366,6 +377,8 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
// if it's just linting, we're done
|
||||
if !flags.TestMode {
|
||||
serveCrowdsec(csParsers, cConfig, hub, agentReady)
|
||||
} else {
|
||||
agentReady <- true
|
||||
}
|
||||
} else {
|
||||
agentReady <- true
|
||||
|
@ -395,6 +408,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
|
||||
for _, ch := range waitChans {
|
||||
<-ch
|
||||
|
||||
switch ch {
|
||||
case apiTomb.Dead():
|
||||
log.Infof("api shutdown")
|
||||
|
@ -402,5 +416,6 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
|
|||
log.Infof("crowdsec shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
4
go.mod
4
go.mod
|
@ -77,7 +77,7 @@ require (
|
|||
github.com/shirou/gopsutil/v3 v3.23.5
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/slack-go/slack v0.12.2
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
|
||||
github.com/wasilibs/go-re2 v1.3.0
|
||||
|
@ -108,7 +108,7 @@ require (
|
|||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/corazawaf/libinjection-go v0.1.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/creack/pty v1.1.18 // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
|
|
14
go.sum
14
go.sum
|
@ -91,21 +91,17 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
|
|||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/crowdsecurity/coraza/v3 v3.0.0-20231213144607-41d5358da94f h1:FkOB9aDw0xzDd14pTarGRLsUNAymONq3dc7zhvsXElg=
|
||||
github.com/crowdsecurity/coraza/v3 v3.0.0-20231213144607-41d5358da94f/go.mod h1:TrU7Li+z2RHNrPy0TKJ6R65V6Yzpan2sTIRryJJyJso=
|
||||
github.com/crowdsecurity/coraza/v3 v3.0.0-20240108124027-a62b8d8e5607 h1:hyrYw3h8clMcRL2u5ooZ3tmwnmJftmhb9Ws1MKmavvI=
|
||||
github.com/crowdsecurity/coraza/v3 v3.0.0-20240108124027-a62b8d8e5607/go.mod h1:br36fEqurGYZQGit+iDYsIzW0FF6VufMbDzyyLxEuPA=
|
||||
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
|
||||
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.5 h1:eVLW+BRj3ZYn0xt5/xmgzfbbB8EBo32gM4+WpQQk2e8=
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.5/go.mod h1:8FMKNGsh3hMZi2SEv6P15PURhEJnZV431XjzzBSuf0k=
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.6 h1:Ef6MylXe0GaJE9vrfvxEdbHb31+JUP1os+murPz7Pos=
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.6/go.mod h1:8FMKNGsh3hMZi2SEv6P15PURhEJnZV431XjzzBSuf0k=
|
||||
github.com/crowdsecurity/grokky v0.2.1 h1:t4VYnDlAd0RjDM2SlILalbwfCrQxtJSMGdQOR0zwkE4=
|
||||
|
@ -640,8 +636,8 @@ github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
|||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
@ -809,8 +805,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
@ -354,15 +354,17 @@ func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
w.InChan <- parsedRequest
|
||||
|
||||
/*
|
||||
response is a copy of w.AppSecRuntime.Response that is safe to use.
|
||||
As OutOfBand might still be running, the original one can be modified
|
||||
*/
|
||||
response := <-parsedRequest.ResponseChannel
|
||||
statusCode := http.StatusOK
|
||||
|
||||
if response.InBandInterrupt {
|
||||
statusCode = http.StatusForbidden
|
||||
AppsecBlockCounter.With(prometheus.Labels{"source": parsedRequest.RemoteAddrNormalized, "appsec_engine": parsedRequest.AppsecEngine}).Inc()
|
||||
}
|
||||
|
||||
appsecResponse := w.AppsecRuntime.GenerateResponse(response, logger)
|
||||
statusCode, appsecResponse := w.AppsecRuntime.GenerateResponse(response, logger)
|
||||
logger.Debugf("Response: %+v", appsecResponse)
|
||||
|
||||
rw.WriteHeader(statusCode)
|
||||
|
|
|
@ -226,7 +226,8 @@ func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
|
|||
if in := request.Tx.Interruption(); in != nil {
|
||||
r.logger.Debugf("inband rules matched : %d", in.RuleID)
|
||||
r.AppsecRuntime.Response.InBandInterrupt = true
|
||||
r.AppsecRuntime.Response.HTTPResponseCode = r.AppsecRuntime.Config.BlockedHTTPCode
|
||||
r.AppsecRuntime.Response.BouncerHTTPResponseCode = r.AppsecRuntime.Config.BouncerBlockedHTTPCode
|
||||
r.AppsecRuntime.Response.UserHTTPResponseCode = r.AppsecRuntime.Config.UserBlockedHTTPCode
|
||||
r.AppsecRuntime.Response.Action = r.AppsecRuntime.DefaultRemediation
|
||||
|
||||
if _, ok := r.AppsecRuntime.RemediationById[in.RuleID]; ok {
|
||||
|
@ -252,7 +253,9 @@ func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
|
|||
r.logger.Errorf("unable to generate appsec event : %s", err)
|
||||
return
|
||||
}
|
||||
r.outChan <- *appsecOvlfw
|
||||
if appsecOvlfw != nil {
|
||||
r.outChan <- *appsecOvlfw
|
||||
}
|
||||
}
|
||||
|
||||
// Should the in band match trigger an event ?
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package appsecacquisition
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -21,16 +22,21 @@ Missing tests (wip):
|
|||
*/
|
||||
|
||||
type appsecRuleTest struct {
|
||||
name string
|
||||
expected_load_ok bool
|
||||
inband_rules []appsec_rule.CustomRule
|
||||
outofband_rules []appsec_rule.CustomRule
|
||||
on_load []appsec.Hook
|
||||
pre_eval []appsec.Hook
|
||||
post_eval []appsec.Hook
|
||||
on_match []appsec.Hook
|
||||
input_request appsec.ParsedRequest
|
||||
output_asserts func(events []types.Event, responses []appsec.AppsecTempResponse)
|
||||
name string
|
||||
expected_load_ok bool
|
||||
inband_rules []appsec_rule.CustomRule
|
||||
outofband_rules []appsec_rule.CustomRule
|
||||
on_load []appsec.Hook
|
||||
pre_eval []appsec.Hook
|
||||
post_eval []appsec.Hook
|
||||
on_match []appsec.Hook
|
||||
BouncerBlockedHTTPCode int
|
||||
UserBlockedHTTPCode int
|
||||
UserPassedHTTPCode int
|
||||
DefaultRemediation string
|
||||
DefaultPassAction string
|
||||
input_request appsec.ParsedRequest
|
||||
output_asserts func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int)
|
||||
}
|
||||
|
||||
func TestAppsecOnMatchHooks(t *testing.T) {
|
||||
|
@ -53,13 +59,14 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
require.Equal(t, types.LOG, events[1].Type)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, 403, responses[0].HTTPResponseCode)
|
||||
require.Equal(t, "ban", responses[0].Action)
|
||||
require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
|
||||
require.Equal(t, 403, responses[0].UserHTTPResponseCode)
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
|
||||
},
|
||||
},
|
||||
|
@ -84,17 +91,18 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
require.Equal(t, types.LOG, events[1].Type)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, 413, responses[0].HTTPResponseCode)
|
||||
require.Equal(t, "ban", responses[0].Action)
|
||||
require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
|
||||
require.Equal(t, 413, responses[0].UserHTTPResponseCode)
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on_match: change action to another standard one (log)",
|
||||
name: "on_match: change action to a non standard one (log)",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
|
@ -114,7 +122,7 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
require.Equal(t, types.LOG, events[1].Type)
|
||||
|
@ -143,16 +151,16 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
require.Equal(t, types.LOG, events[1].Type)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, "allow", responses[0].Action)
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on_match: change action to another standard one (deny/ban/block)",
|
||||
name: "on_match: change action to another standard one (ban)",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
|
@ -164,7 +172,7 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
},
|
||||
},
|
||||
on_match: []appsec.Hook{
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetRemediation('deny')"}},
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetRemediation('ban')"}},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
|
@ -172,10 +180,10 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, responses, 1)
|
||||
//note: SetAction normalizes deny, ban and block to ban
|
||||
require.Equal(t, "ban", responses[0].Action)
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -199,10 +207,10 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, responses, 1)
|
||||
//note: SetAction normalizes deny, ban and block to ban
|
||||
require.Equal(t, "captcha", responses[0].Action)
|
||||
require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -226,7 +234,7 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
require.Equal(t, types.LOG, events[1].Type)
|
||||
|
@ -255,11 +263,11 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 1)
|
||||
require.Equal(t, types.LOG, events[0].Type)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, "ban", responses[0].Action)
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -283,11 +291,11 @@ func TestAppsecOnMatchHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 1)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, "ban", responses[0].Action)
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -328,7 +336,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Len(t, responses, 1)
|
||||
require.False(t, responses[0].InBandInterrupt)
|
||||
|
@ -356,7 +364,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
|
||||
|
@ -391,7 +399,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Len(t, responses, 1)
|
||||
require.False(t, responses[0].InBandInterrupt)
|
||||
|
@ -419,7 +427,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Len(t, responses, 1)
|
||||
require.False(t, responses[0].InBandInterrupt)
|
||||
|
@ -447,7 +455,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Len(t, responses, 1)
|
||||
require.False(t, responses[0].InBandInterrupt)
|
||||
|
@ -472,7 +480,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 1)
|
||||
require.Equal(t, types.LOG, events[0].Type)
|
||||
require.True(t, events[0].Appsec.HasOutBandMatches)
|
||||
|
@ -506,7 +514,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, "foobar", responses[0].Action)
|
||||
|
@ -533,7 +541,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, "foobar", responses[0].Action)
|
||||
|
@ -560,10 +568,12 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Len(t, responses, 1)
|
||||
require.Equal(t, "foobar", responses[0].Action)
|
||||
require.Equal(t, "foobar", appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -574,6 +584,473 @@ func TestAppsecPreEvalHooks(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppsecRemediationConfigHooks(t *testing.T) {
|
||||
|
||||
tests := []appsecRuleTest{
|
||||
{
|
||||
name: "Basic matching rule",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SetRemediation",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
on_match: []appsec.Hook{{Apply: []string{"SetRemediation('captcha')"}}}, //rule ID is generated at runtime. If you change rule, it will break the test (:
|
||||
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SetRemediation",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
on_match: []appsec.Hook{{Apply: []string{"SetReturnCode(418)"}}}, //rule ID is generated at runtime. If you change rule, it will break the test (:
|
||||
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
loadAppSecEngine(test, t)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestOnMatchRemediationHooks(t *testing.T) {
|
||||
tests := []appsecRuleTest{
|
||||
{
|
||||
name: "set remediation to allow with on_match hook",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
on_match: []appsec.Hook{
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetRemediation('allow')"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set remediation to captcha + custom user code with on_match hook",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
DefaultRemediation: appsec.AllowRemediation,
|
||||
on_match: []appsec.Hook{
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetRemediation('captcha')", "SetReturnCode(418)"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
spew.Dump(responses)
|
||||
spew.Dump(appsecResponse)
|
||||
|
||||
log.Errorf("http status : %d", statusCode)
|
||||
require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
loadAppSecEngine(test, t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppsecDefaultPassRemediation(t *testing.T) {
|
||||
|
||||
tests := []appsecRuleTest{
|
||||
{
|
||||
name: "Basic non-matching rule",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/",
|
||||
Args: url.Values{"foo": []string{"tutu"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultPassAction: pass",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/",
|
||||
Args: url.Values{"foo": []string{"tutu"}},
|
||||
},
|
||||
DefaultPassAction: "allow",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultPassAction: captcha",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/",
|
||||
Args: url.Values{"foo": []string{"tutu"}},
|
||||
},
|
||||
DefaultPassAction: "captcha",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode) //@tko: body is captcha, but as it's 200, captcha won't be showed to user
|
||||
require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultPassHTTPCode: 200",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/",
|
||||
Args: url.Values{"foo": []string{"tutu"}},
|
||||
},
|
||||
UserPassedHTTPCode: 200,
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultPassHTTPCode: 200",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/",
|
||||
Args: url.Values{"foo": []string{"tutu"}},
|
||||
},
|
||||
UserPassedHTTPCode: 418,
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
loadAppSecEngine(test, t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppsecDefaultRemediation(t *testing.T) {
|
||||
|
||||
tests := []appsecRuleTest{
|
||||
{
|
||||
name: "Basic matching rule",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule1",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default remediation to ban (default)",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
DefaultRemediation: "ban",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default remediation to allow",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
DefaultRemediation: "allow",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default remediation to captcha",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
DefaultRemediation: "captcha",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom user HTTP code",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
UserBlockedHTTPCode: 418,
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom remediation + HTTP code",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
UserBlockedHTTPCode: 418,
|
||||
DefaultRemediation: "foobar",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, "foobar", responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, "foobar", appsecResponse.Action)
|
||||
require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
loadAppSecEngine(test, t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppsecRuleMatches(t *testing.T) {
|
||||
|
||||
/*
|
||||
|
@ -601,7 +1078,7 @@ func TestAppsecRuleMatches(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Len(t, events, 2)
|
||||
require.Equal(t, types.APPSEC, events[0].Type)
|
||||
|
||||
|
@ -632,13 +1109,172 @@ func TestAppsecRuleMatches(t *testing.T) {
|
|||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"tutu"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Len(t, responses, 1)
|
||||
require.False(t, responses[0].InBandInterrupt)
|
||||
require.False(t, responses[0].OutOfBandInterrupt)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default remediation to allow",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
DefaultRemediation: "allow",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default remediation to captcha",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
DefaultRemediation: "captcha",
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no default remediation / custom user HTTP code",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"toto"}},
|
||||
},
|
||||
UserBlockedHTTPCode: 418,
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Equal(t, appsec.BanRemediation, responses[0].Action)
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
|
||||
require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no match but try to set remediation to captcha with on_match hook",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
on_match: []appsec.Hook{
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetRemediation('captcha')"}},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"bla"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no match but try to set user HTTP code with on_match hook",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
on_match: []appsec.Hook{
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetReturnCode(418)"}},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"bla"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no match but try to set remediation with pre_eval hook",
|
||||
expected_load_ok: true,
|
||||
inband_rules: []appsec_rule.CustomRule{
|
||||
{
|
||||
Name: "rule42",
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
pre_eval: []appsec.Hook{
|
||||
{Filter: "IsInBand == true", Apply: []string{"SetRemediationByName('rule42', 'captcha')"}},
|
||||
},
|
||||
input_request: appsec.ParsedRequest{
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Method: "GET",
|
||||
URI: "/urllll",
|
||||
Args: url.Values{"foo": []string{"bla"}},
|
||||
},
|
||||
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
|
||||
require.Empty(t, events)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -678,7 +1314,16 @@ func loadAppSecEngine(test appsecRuleTest, t *testing.T) {
|
|||
outofbandRules = append(outofbandRules, strRule)
|
||||
}
|
||||
|
||||
appsecCfg := appsec.AppsecConfig{Logger: logger, OnLoad: test.on_load, PreEval: test.pre_eval, PostEval: test.post_eval, OnMatch: test.on_match}
|
||||
appsecCfg := appsec.AppsecConfig{Logger: logger,
|
||||
OnLoad: test.on_load,
|
||||
PreEval: test.pre_eval,
|
||||
PostEval: test.post_eval,
|
||||
OnMatch: test.on_match,
|
||||
BouncerBlockedHTTPCode: test.BouncerBlockedHTTPCode,
|
||||
UserBlockedHTTPCode: test.UserBlockedHTTPCode,
|
||||
UserPassedHTTPCode: test.UserPassedHTTPCode,
|
||||
DefaultRemediation: test.DefaultRemediation,
|
||||
DefaultPassAction: test.DefaultPassAction}
|
||||
AppsecRuntime, err := appsecCfg.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to build appsec runtime : %s", err)
|
||||
|
@ -724,8 +1369,10 @@ func loadAppSecEngine(test appsecRuleTest, t *testing.T) {
|
|||
|
||||
runner.handleRequest(&input)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
http_status, appsecResponse := AppsecRuntime.GenerateResponse(OutputResponses[0], logger)
|
||||
log.Infof("events : %s", spew.Sdump(OutputEvents))
|
||||
log.Infof("responses : %s", spew.Sdump(OutputResponses))
|
||||
test.output_asserts(OutputEvents, OutputResponses)
|
||||
test.output_asserts(OutputEvents, OutputResponses, appsecResponse, http_status)
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ type LokiClient struct {
|
|||
t *tomb.Tomb
|
||||
fail_start time.Time
|
||||
currentTickerInterval time.Duration
|
||||
requestHeaders map[string]string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
@ -116,7 +117,7 @@ func (lc *LokiClient) queryRange(uri string, ctx context.Context, c chan *LokiQu
|
|||
case <-lc.t.Dying():
|
||||
return lc.t.Err()
|
||||
case <-ticker.C:
|
||||
resp, err := http.Get(uri)
|
||||
resp, err := lc.Get(uri)
|
||||
if err != nil {
|
||||
if ok := lc.shouldRetry(); !ok {
|
||||
return errors.Wrapf(err, "error querying range")
|
||||
|
@ -127,6 +128,7 @@ func (lc *LokiClient) queryRange(uri string, ctx context.Context, c chan *LokiQu
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
lc.Logger.Warnf("bad HTTP response code for query range: %d", resp.StatusCode)
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if ok := lc.shouldRetry(); !ok {
|
||||
|
@ -215,7 +217,7 @@ func (lc *LokiClient) Ready(ctx context.Context) error {
|
|||
return lc.t.Err()
|
||||
case <-tick.C:
|
||||
lc.Logger.Debug("Checking if Loki is ready")
|
||||
resp, err := http.Get(url)
|
||||
resp, err := lc.Get(url)
|
||||
if err != nil {
|
||||
lc.Logger.Warnf("Error checking if Loki is ready: %s", err)
|
||||
continue
|
||||
|
@ -251,10 +253,9 @@ func (lc *LokiClient) Tail(ctx context.Context) (chan *LokiResponse, error) {
|
|||
}
|
||||
|
||||
requestHeader := http.Header{}
|
||||
for k, v := range lc.config.Headers {
|
||||
for k, v := range lc.requestHeaders {
|
||||
requestHeader.Add(k, v)
|
||||
}
|
||||
requestHeader.Set("User-Agent", "Crowdsec "+cwversion.VersionStr())
|
||||
lc.Logger.Infof("Connecting to %s", u)
|
||||
conn, _, err := dialer.Dial(u, requestHeader)
|
||||
|
||||
|
@ -293,16 +294,6 @@ func (lc *LokiClient) QueryRange(ctx context.Context, infinite bool) chan *LokiQ
|
|||
|
||||
lc.Logger.Debugf("Since: %s (%s)", lc.config.Since, time.Now().Add(-lc.config.Since))
|
||||
|
||||
requestHeader := http.Header{}
|
||||
for k, v := range lc.config.Headers {
|
||||
requestHeader.Add(k, v)
|
||||
}
|
||||
|
||||
if lc.config.Username != "" || lc.config.Password != "" {
|
||||
requestHeader.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(lc.config.Username+":"+lc.config.Password)))
|
||||
}
|
||||
|
||||
requestHeader.Set("User-Agent", "Crowdsec "+cwversion.VersionStr())
|
||||
lc.Logger.Infof("Connecting to %s", url)
|
||||
lc.t.Go(func() error {
|
||||
return lc.queryRange(url, ctx, c, infinite)
|
||||
|
@ -310,6 +301,26 @@ func (lc *LokiClient) QueryRange(ctx context.Context, infinite bool) chan *LokiQ
|
|||
return c
|
||||
}
|
||||
|
||||
func NewLokiClient(config Config) *LokiClient {
|
||||
return &LokiClient{Logger: log.WithField("component", "lokiclient"), config: config}
|
||||
// Create a wrapper for http.Get to be able to set headers and auth
|
||||
func (lc *LokiClient) Get(url string) (*http.Response, error) {
|
||||
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range lc.requestHeaders {
|
||||
request.Header.Add(k, v)
|
||||
}
|
||||
return http.DefaultClient.Do(request)
|
||||
}
|
||||
|
||||
func NewLokiClient(config Config) *LokiClient {
|
||||
headers := make(map[string]string)
|
||||
for k, v := range config.Headers {
|
||||
headers[k] = v
|
||||
}
|
||||
if config.Username != "" || config.Password != "" {
|
||||
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(config.Username+":"+config.Password))
|
||||
}
|
||||
headers["User-Agent"] = "Crowdsec " + cwversion.VersionStr()
|
||||
return &LokiClient{Logger: log.WithField("component", "lokiclient"), config: config, requestHeaders: headers}
|
||||
}
|
||||
|
|
|
@ -276,10 +276,17 @@ func feedLoki(logger *log.Entry, n int, title string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := http.Post("http://127.0.0.1:3100/loki/api/v1/push", "application/json", bytes.NewBuffer(buff))
|
||||
req, err := http.NewRequest(http.MethodPost, "http://127.0.0.1:3100/loki/api/v1/push", bytes.NewBuffer(buff))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Scope-OrgID", "1234")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
logger.Error(string(b))
|
||||
|
@ -306,6 +313,8 @@ mode: cat
|
|||
source: loki
|
||||
url: http://127.0.0.1:3100
|
||||
query: '{server="demo",key="%s"}'
|
||||
headers:
|
||||
x-scope-orgid: "1234"
|
||||
since: 1h
|
||||
`, title),
|
||||
},
|
||||
|
@ -362,26 +371,26 @@ func TestStreamingAcquisition(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "Bad port",
|
||||
config: `
|
||||
mode: tail
|
||||
config: `mode: tail
|
||||
source: loki
|
||||
url: http://127.0.0.1:3101
|
||||
url: "http://127.0.0.1:3101"
|
||||
headers:
|
||||
x-scope-orgid: "1234"
|
||||
query: >
|
||||
{server="demo"}
|
||||
`, // No Loki server here
|
||||
{server="demo"}`, // No Loki server here
|
||||
expectedErr: "",
|
||||
streamErr: `loki is not ready: context deadline exceeded`,
|
||||
expectedLines: 0,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
config: `
|
||||
mode: tail
|
||||
config: `mode: tail
|
||||
source: loki
|
||||
url: http://127.0.0.1:3100
|
||||
url: "http://127.0.0.1:3100"
|
||||
headers:
|
||||
x-scope-orgid: "1234"
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
{server="demo"}`,
|
||||
expectedErr: "",
|
||||
streamErr: "",
|
||||
expectedLines: 20,
|
||||
|
@ -456,6 +465,8 @@ func TestStopStreaming(t *testing.T) {
|
|||
mode: tail
|
||||
source: loki
|
||||
url: http://127.0.0.1:3100
|
||||
headers:
|
||||
x-scope-orgid: "1234"
|
||||
query: >
|
||||
{server="demo"}
|
||||
`
|
||||
|
|
|
@ -2,6 +2,7 @@ package appsec
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
|
@ -30,6 +31,12 @@ const (
|
|||
hookOnMatch
|
||||
)
|
||||
|
||||
const (
|
||||
BanRemediation = "ban"
|
||||
CaptchaRemediation = "captcha"
|
||||
AllowRemediation = "allow"
|
||||
)
|
||||
|
||||
func (h *Hook) Build(hookStage int) error {
|
||||
|
||||
ctx := map[string]interface{}{}
|
||||
|
@ -62,12 +69,13 @@ func (h *Hook) Build(hookStage int) error {
|
|||
}
|
||||
|
||||
type AppsecTempResponse struct {
|
||||
InBandInterrupt bool
|
||||
OutOfBandInterrupt bool
|
||||
Action string //allow, deny, captcha, log
|
||||
HTTPResponseCode int
|
||||
SendEvent bool //do we send an internal event on rule match
|
||||
SendAlert bool //do we send an alert on rule match
|
||||
InBandInterrupt bool
|
||||
OutOfBandInterrupt bool
|
||||
Action string //allow, deny, captcha, log
|
||||
UserHTTPResponseCode int //The response code to send to the user
|
||||
BouncerHTTPResponseCode int //The response code to send to the remediation component
|
||||
SendEvent bool //do we send an internal event on rule match
|
||||
SendAlert bool //do we send an alert on rule match
|
||||
}
|
||||
|
||||
type AppsecSubEngineOpts struct {
|
||||
|
@ -110,31 +118,33 @@ type AppsecRuntimeConfig struct {
|
|||
}
|
||||
|
||||
type AppsecConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
OutOfBandRules []string `yaml:"outofband_rules"`
|
||||
InBandRules []string `yaml:"inband_rules"`
|
||||
DefaultRemediation string `yaml:"default_remediation"`
|
||||
DefaultPassAction string `yaml:"default_pass_action"`
|
||||
BlockedHTTPCode int `yaml:"blocked_http_code"`
|
||||
PassedHTTPCode int `yaml:"passed_http_code"`
|
||||
OnLoad []Hook `yaml:"on_load"`
|
||||
PreEval []Hook `yaml:"pre_eval"`
|
||||
PostEval []Hook `yaml:"post_eval"`
|
||||
OnMatch []Hook `yaml:"on_match"`
|
||||
VariablesTracking []string `yaml:"variables_tracking"`
|
||||
InbandOptions AppsecSubEngineOpts `yaml:"inband_options"`
|
||||
OutOfBandOptions AppsecSubEngineOpts `yaml:"outofband_options"`
|
||||
Name string `yaml:"name"`
|
||||
OutOfBandRules []string `yaml:"outofband_rules"`
|
||||
InBandRules []string `yaml:"inband_rules"`
|
||||
DefaultRemediation string `yaml:"default_remediation"`
|
||||
DefaultPassAction string `yaml:"default_pass_action"`
|
||||
BouncerBlockedHTTPCode int `yaml:"blocked_http_code"` //returned to the bouncer
|
||||
BouncerPassedHTTPCode int `yaml:"passed_http_code"` //returned to the bouncer
|
||||
UserBlockedHTTPCode int `yaml:"user_blocked_http_code"` //returned to the user
|
||||
UserPassedHTTPCode int `yaml:"user_passed_http_code"` //returned to the user
|
||||
|
||||
OnLoad []Hook `yaml:"on_load"`
|
||||
PreEval []Hook `yaml:"pre_eval"`
|
||||
PostEval []Hook `yaml:"post_eval"`
|
||||
OnMatch []Hook `yaml:"on_match"`
|
||||
VariablesTracking []string `yaml:"variables_tracking"`
|
||||
InbandOptions AppsecSubEngineOpts `yaml:"inband_options"`
|
||||
OutOfBandOptions AppsecSubEngineOpts `yaml:"outofband_options"`
|
||||
|
||||
LogLevel *log.Level `yaml:"log_level"`
|
||||
Logger *log.Entry `yaml:"-"`
|
||||
}
|
||||
|
||||
func (w *AppsecRuntimeConfig) ClearResponse() {
|
||||
w.Logger.Debugf("#-> %p", w)
|
||||
w.Response = AppsecTempResponse{}
|
||||
w.Logger.Debugf("-> %p", w.Config)
|
||||
w.Response.Action = w.Config.DefaultPassAction
|
||||
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
||||
w.Response.BouncerHTTPResponseCode = w.Config.BouncerPassedHTTPCode
|
||||
w.Response.UserHTTPResponseCode = w.Config.UserPassedHTTPCode
|
||||
w.Response.SendEvent = true
|
||||
w.Response.SendAlert = true
|
||||
}
|
||||
|
@ -191,24 +201,35 @@ func (wc *AppsecConfig) GetDataDir() string {
|
|||
|
||||
func (wc *AppsecConfig) Build() (*AppsecRuntimeConfig, error) {
|
||||
ret := &AppsecRuntimeConfig{Logger: wc.Logger.WithField("component", "appsec_runtime_config")}
|
||||
//set the defaults
|
||||
switch wc.DefaultRemediation {
|
||||
case "":
|
||||
wc.DefaultRemediation = "ban"
|
||||
case "ban", "captcha", "log":
|
||||
//those are the officially supported remediation(s)
|
||||
default:
|
||||
wc.Logger.Warningf("default '%s' remediation of %s is none of [ban,captcha,log] ensure bouncer compatbility!", wc.DefaultRemediation, wc.Name)
|
||||
|
||||
if wc.BouncerBlockedHTTPCode == 0 {
|
||||
wc.BouncerBlockedHTTPCode = http.StatusForbidden
|
||||
}
|
||||
if wc.BlockedHTTPCode == 0 {
|
||||
wc.BlockedHTTPCode = 403
|
||||
if wc.BouncerPassedHTTPCode == 0 {
|
||||
wc.BouncerPassedHTTPCode = http.StatusOK
|
||||
}
|
||||
if wc.PassedHTTPCode == 0 {
|
||||
wc.PassedHTTPCode = 200
|
||||
|
||||
if wc.UserBlockedHTTPCode == 0 {
|
||||
wc.UserBlockedHTTPCode = http.StatusForbidden
|
||||
}
|
||||
if wc.UserPassedHTTPCode == 0 {
|
||||
wc.UserPassedHTTPCode = http.StatusOK
|
||||
}
|
||||
if wc.DefaultPassAction == "" {
|
||||
wc.DefaultPassAction = "allow"
|
||||
wc.DefaultPassAction = AllowRemediation
|
||||
}
|
||||
if wc.DefaultRemediation == "" {
|
||||
wc.DefaultRemediation = BanRemediation
|
||||
}
|
||||
|
||||
//set the defaults
|
||||
switch wc.DefaultRemediation {
|
||||
case BanRemediation, CaptchaRemediation, AllowRemediation:
|
||||
//those are the officially supported remediation(s)
|
||||
default:
|
||||
wc.Logger.Warningf("default '%s' remediation of %s is none of [%s,%s,%s] ensure bouncer compatbility!", wc.DefaultRemediation, wc.Name, BanRemediation, CaptchaRemediation, AllowRemediation)
|
||||
}
|
||||
|
||||
ret.Name = wc.Name
|
||||
ret.Config = wc
|
||||
ret.DefaultRemediation = wc.DefaultRemediation
|
||||
|
@ -553,27 +574,13 @@ func (w *AppsecRuntimeConfig) SetActionByName(name string, action string) error
|
|||
func (w *AppsecRuntimeConfig) SetAction(action string) error {
|
||||
//log.Infof("setting to %s", action)
|
||||
w.Logger.Debugf("setting action to %s", action)
|
||||
switch action {
|
||||
case "allow":
|
||||
w.Response.Action = action
|
||||
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
||||
//@tko how should we handle this ? it seems bouncer only understand bans, but it might be misleading ?
|
||||
case "deny", "ban", "block":
|
||||
w.Response.Action = "ban"
|
||||
case "log":
|
||||
w.Response.Action = action
|
||||
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
||||
case "captcha":
|
||||
w.Response.Action = action
|
||||
default:
|
||||
w.Response.Action = action
|
||||
}
|
||||
w.Response.Action = action
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *AppsecRuntimeConfig) SetHTTPCode(code int) error {
|
||||
w.Logger.Debugf("setting http code to %d", code)
|
||||
w.Response.HTTPResponseCode = code
|
||||
w.Response.UserHTTPResponseCode = code
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -582,24 +589,23 @@ type BodyResponse struct {
|
|||
HTTPStatus int `json:"http_status"`
|
||||
}
|
||||
|
||||
func (w *AppsecRuntimeConfig) GenerateResponse(response AppsecTempResponse, logger *log.Entry) BodyResponse {
|
||||
resp := BodyResponse{}
|
||||
//if there is no interrupt, we should allow with default code
|
||||
if !response.InBandInterrupt {
|
||||
resp.Action = w.Config.DefaultPassAction
|
||||
resp.HTTPStatus = w.Config.PassedHTTPCode
|
||||
return resp
|
||||
}
|
||||
resp.Action = response.Action
|
||||
if resp.Action == "" {
|
||||
resp.Action = w.Config.DefaultRemediation
|
||||
}
|
||||
logger.Debugf("action is %s", resp.Action)
|
||||
func (w *AppsecRuntimeConfig) GenerateResponse(response AppsecTempResponse, logger *log.Entry) (int, BodyResponse) {
|
||||
var bouncerStatusCode int
|
||||
|
||||
resp.HTTPStatus = response.HTTPResponseCode
|
||||
if resp.HTTPStatus == 0 {
|
||||
resp.HTTPStatus = w.Config.BlockedHTTPCode
|
||||
resp := BodyResponse{Action: response.Action}
|
||||
if response.Action == AllowRemediation {
|
||||
resp.HTTPStatus = w.Config.UserPassedHTTPCode
|
||||
bouncerStatusCode = w.Config.BouncerPassedHTTPCode
|
||||
} else { //ban, captcha and anything else
|
||||
resp.HTTPStatus = response.UserHTTPResponseCode
|
||||
if resp.HTTPStatus == 0 {
|
||||
resp.HTTPStatus = w.Config.UserBlockedHTTPCode
|
||||
}
|
||||
bouncerStatusCode = response.BouncerHTTPResponseCode
|
||||
if bouncerStatusCode == 0 {
|
||||
bouncerStatusCode = w.Config.BouncerBlockedHTTPCode
|
||||
}
|
||||
}
|
||||
logger.Debugf("http status is %d", resp.HTTPStatus)
|
||||
return resp
|
||||
|
||||
return bouncerStatusCode, resp
|
||||
}
|
||||
|
|
|
@ -75,6 +75,9 @@ teardown() {
|
|||
|
||||
rune -0 ./instance-crowdsec start-pid
|
||||
PID="$output"
|
||||
|
||||
sleep .5
|
||||
|
||||
assert_file_exists "$log_old"
|
||||
assert_file_contains "$log_old" "Starting processing data"
|
||||
|
||||
|
|
|
@ -41,10 +41,23 @@ teardown() {
|
|||
echo -e "---\nfilename: ${tmpfile}\nlabels:\n type: syslog\n" >>"${ACQUIS_YAML}"
|
||||
|
||||
./instance-crowdsec start
|
||||
|
||||
sleep 0.2
|
||||
|
||||
fake_log >>"${tmpfile}"
|
||||
sleep 2
|
||||
|
||||
sleep 0.2
|
||||
|
||||
rm -f -- "${tmpfile}"
|
||||
rune -0 cscli decisions list -o json
|
||||
rune -0 jq -r '.[].decisions[0].value' <(output)
|
||||
assert_output '1.1.1.172'
|
||||
|
||||
found=0
|
||||
# this may take some time in CI
|
||||
for _ in $(seq 1 10); do
|
||||
if cscli decisions list -o json | jq -r '.[].decisions[0].value' | grep -q '1.1.1.172'; then
|
||||
found=1
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
assert_equal 1 "${found}"
|
||||
}
|
||||
|
|
355
test/lib/color-formatter
Executable file
355
test/lib/color-formatter
Executable file
|
@ -0,0 +1,355 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Taken from pretty formatter, minus the cursor movements.
|
||||
# Used in gihtub workflows CI where color is allowed.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# shellcheck source=lib/bats-core/formatter.bash
|
||||
source "$BATS_ROOT/lib/bats-core/formatter.bash"
|
||||
|
||||
BASE_PATH=.
|
||||
BATS_ENABLE_TIMING=
|
||||
|
||||
while [[ "$#" -ne 0 ]]; do
|
||||
case "$1" in
|
||||
-T)
|
||||
BATS_ENABLE_TIMING="-T"
|
||||
;;
|
||||
--base-path)
|
||||
shift
|
||||
normalize_base_path BASE_PATH "$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
update_count_column_width() {
|
||||
count_column_width=$((${#count} * 2 + 2))
|
||||
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
|
||||
# additional space for ' in %s sec'
|
||||
count_column_width=$((count_column_width + ${#SECONDS} + 8))
|
||||
fi
|
||||
# also update dependent value
|
||||
update_count_column_left
|
||||
}
|
||||
|
||||
update_screen_width() {
|
||||
screen_width="$(tput cols)"
|
||||
# also update dependent value
|
||||
update_count_column_left
|
||||
}
|
||||
|
||||
update_count_column_left() {
|
||||
count_column_left=$((screen_width - count_column_width))
|
||||
}
|
||||
|
||||
# avoid unset variables
|
||||
count=0
|
||||
screen_width=80
|
||||
update_count_column_width
|
||||
#update_screen_width
|
||||
test_result=
|
||||
|
||||
#trap update_screen_width WINCH
|
||||
|
||||
begin() {
|
||||
test_result= # reset to avoid carrying over result state from previous test
|
||||
line_backoff_count=0
|
||||
#go_to_column 0
|
||||
#update_count_column_width
|
||||
#buffer_with_truncation $((count_column_left - 1)) ' %s' "$name"
|
||||
#clear_to_end_of_line
|
||||
#go_to_column $count_column_left
|
||||
#if [[ -n "$BATS_ENABLE_TIMING" ]]; then
|
||||
# buffer "%${#count}s/${count} in %s sec" "$index" "$SECONDS"
|
||||
#else
|
||||
# buffer "%${#count}s/${count}" "$index"
|
||||
#fi
|
||||
#go_to_column 1
|
||||
buffer "%${#count}s" "$index"
|
||||
}
|
||||
|
||||
finish_test() {
|
||||
#move_up $line_backoff_count
|
||||
#go_to_column 0
|
||||
buffer "$@"
|
||||
if [[ -n "${TIMEOUT-}" ]]; then
|
||||
set_color 2
|
||||
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
|
||||
buffer ' [%s (timeout: %s)]' "$TIMING" "$TIMEOUT"
|
||||
else
|
||||
buffer ' [timeout: %s]' "$TIMEOUT"
|
||||
fi
|
||||
else
|
||||
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
|
||||
set_color 2
|
||||
buffer ' [%s]' "$TIMING"
|
||||
fi
|
||||
fi
|
||||
advance
|
||||
move_down $((line_backoff_count - 1))
|
||||
}
|
||||
|
||||
pass() {
|
||||
local TIMING="${1:-}"
|
||||
finish_test ' ✓ %s' "$name"
|
||||
test_result=pass
|
||||
}
|
||||
|
||||
skip() {
|
||||
local reason="$1" TIMING="${2:-}"
|
||||
if [[ -n "$reason" ]]; then
|
||||
reason=": $reason"
|
||||
fi
|
||||
finish_test ' - %s (skipped%s)' "$name" "$reason"
|
||||
test_result=skip
|
||||
}
|
||||
|
||||
fail() {
|
||||
local TIMING="${1:-}"
|
||||
set_color 1 bold
|
||||
finish_test ' ✗ %s' "$name"
|
||||
test_result=fail
|
||||
}
|
||||
|
||||
timeout() {
|
||||
local TIMING="${1:-}"
|
||||
set_color 3 bold
|
||||
TIMEOUT="${2:-}" finish_test ' ✗ %s' "$name"
|
||||
test_result=timeout
|
||||
}
|
||||
|
||||
log() {
|
||||
case ${test_result} in
|
||||
pass)
|
||||
clear_color
|
||||
;;
|
||||
fail)
|
||||
set_color 1
|
||||
;;
|
||||
timeout)
|
||||
set_color 3
|
||||
;;
|
||||
esac
|
||||
buffer ' %s\n' "$1"
|
||||
clear_color
|
||||
}
|
||||
|
||||
summary() {
|
||||
if [ "$failures" -eq 0 ]; then
|
||||
set_color 2 bold
|
||||
else
|
||||
set_color 1 bold
|
||||
fi
|
||||
|
||||
buffer '\n%d test' "$count"
|
||||
if [[ "$count" -ne 1 ]]; then
|
||||
buffer 's'
|
||||
fi
|
||||
|
||||
buffer ', %d failure' "$failures"
|
||||
if [[ "$failures" -ne 1 ]]; then
|
||||
buffer 's'
|
||||
fi
|
||||
|
||||
if [[ "$skipped" -gt 0 ]]; then
|
||||
buffer ', %d skipped' "$skipped"
|
||||
fi
|
||||
|
||||
if ((timed_out > 0)); then
|
||||
buffer ', %d timed out' "$timed_out"
|
||||
fi
|
||||
|
||||
not_run=$((count - passed - failures - skipped - timed_out))
|
||||
if [[ "$not_run" -gt 0 ]]; then
|
||||
buffer ', %d not run' "$not_run"
|
||||
fi
|
||||
|
||||
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
|
||||
buffer " in $SECONDS seconds"
|
||||
fi
|
||||
|
||||
buffer '\n'
|
||||
clear_color
|
||||
}
|
||||
|
||||
buffer_with_truncation() {
|
||||
local width="$1"
|
||||
shift
|
||||
local string
|
||||
|
||||
# shellcheck disable=SC2059
|
||||
printf -v 'string' -- "$@"
|
||||
|
||||
if [[ "${#string}" -gt "$width" ]]; then
|
||||
buffer '%s...' "${string:0:$((width - 4))}"
|
||||
else
|
||||
buffer '%s' "$string"
|
||||
fi
|
||||
}
|
||||
|
||||
move_up() {
|
||||
if [[ $1 -gt 0 ]]; then # avoid moving if we got 0
|
||||
buffer '\x1B[%dA' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
move_down() {
|
||||
if [[ $1 -gt 0 ]]; then # avoid moving if we got 0
|
||||
buffer '\x1B[%dB' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
go_to_column() {
|
||||
local column="$1"
|
||||
buffer '\x1B[%dG' $((column + 1))
|
||||
}
|
||||
|
||||
clear_to_end_of_line() {
|
||||
buffer '\x1B[K'
|
||||
}
|
||||
|
||||
advance() {
|
||||
clear_to_end_of_line
|
||||
buffer '\n'
|
||||
clear_color
|
||||
}
|
||||
|
||||
set_color() {
|
||||
local color="$1"
|
||||
local weight=22
|
||||
|
||||
if [[ "${2:-}" == 'bold' ]]; then
|
||||
weight=1
|
||||
fi
|
||||
buffer '\x1B[%d;%dm' "$((30 + color))" "$weight"
|
||||
}
|
||||
|
||||
clear_color() {
|
||||
buffer '\x1B[0m'
|
||||
}
|
||||
|
||||
_buffer=
|
||||
|
||||
buffer() {
|
||||
local content
|
||||
# shellcheck disable=SC2059
|
||||
printf -v content -- "$@"
|
||||
_buffer+="$content"
|
||||
}
|
||||
|
||||
prefix_buffer_with() {
|
||||
local old_buffer="$_buffer"
|
||||
_buffer=''
|
||||
"$@"
|
||||
_buffer="$_buffer$old_buffer"
|
||||
}
|
||||
|
||||
flush() {
|
||||
printf '%s' "$_buffer"
|
||||
_buffer=
|
||||
}
|
||||
|
||||
finish() {
|
||||
flush
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
trap finish EXIT
|
||||
trap '' INT
|
||||
|
||||
bats_tap_stream_plan() {
|
||||
count="$1"
|
||||
index=0
|
||||
passed=0
|
||||
failures=0
|
||||
skipped=0
|
||||
timed_out=0
|
||||
name=
|
||||
update_count_column_width
|
||||
}
|
||||
|
||||
bats_tap_stream_begin() {
|
||||
index="$1"
|
||||
name="$2"
|
||||
begin
|
||||
flush
|
||||
}
|
||||
|
||||
bats_tap_stream_ok() {
|
||||
index="$1"
|
||||
name="$2"
|
||||
((++passed))
|
||||
|
||||
pass "${BATS_FORMATTER_TEST_DURATION:-}"
|
||||
}
|
||||
|
||||
bats_tap_stream_skipped() {
|
||||
index="$1"
|
||||
name="$2"
|
||||
((++skipped))
|
||||
skip "$3" "${BATS_FORMATTER_TEST_DURATION:-}"
|
||||
}
|
||||
|
||||
bats_tap_stream_not_ok() {
|
||||
index="$1"
|
||||
name="$2"
|
||||
|
||||
if [[ ${BATS_FORMATTER_TEST_TIMEOUT-x} != x ]]; then
|
||||
timeout "${BATS_FORMATTER_TEST_DURATION:-}" "${BATS_FORMATTER_TEST_TIMEOUT}s"
|
||||
((++timed_out))
|
||||
else
|
||||
fail "${BATS_FORMATTER_TEST_DURATION:-}"
|
||||
((++failures))
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
bats_tap_stream_comment() { # <comment> <scope>
|
||||
local scope=$2
|
||||
# count the lines we printed after the begin text,
|
||||
if [[ $line_backoff_count -eq 0 && $scope == begin ]]; then
|
||||
# if this is the first line after begin, go down one line
|
||||
buffer "\n"
|
||||
((++line_backoff_count)) # prefix-increment to avoid "error" due to returning 0
|
||||
fi
|
||||
|
||||
((++line_backoff_count))
|
||||
((line_backoff_count += ${#1} / screen_width)) # account for linebreaks due to length
|
||||
log "$1"
|
||||
}
|
||||
|
||||
bats_tap_stream_suite() {
|
||||
#test_file="$1"
|
||||
line_backoff_count=0
|
||||
index=
|
||||
# indicate filename for failures
|
||||
local file_name="${1#"$BASE_PATH"}"
|
||||
name="File $file_name"
|
||||
set_color 4 bold
|
||||
buffer "%s\n" "$file_name"
|
||||
clear_color
|
||||
}
|
||||
|
||||
line_backoff_count=0
|
||||
bats_tap_stream_unknown() { # <full line> <scope>
|
||||
local scope=$2
|
||||
# count the lines we printed after the begin text, (or after suite, in case of syntax errors)
|
||||
if [[ $line_backoff_count -eq 0 && ($scope == begin || $scope == suite) ]]; then
|
||||
# if this is the first line after begin, go down one line
|
||||
buffer "\n"
|
||||
((++line_backoff_count)) # prefix-increment to avoid "error" due to returning 0
|
||||
fi
|
||||
|
||||
((++line_backoff_count))
|
||||
((line_backoff_count += ${#1} / screen_width)) # account for linebreaks due to length
|
||||
buffer "%s\n" "$1"
|
||||
flush
|
||||
}
|
||||
|
||||
bats_parse_internal_extended_tap
|
||||
|
||||
summary
|
Loading…
Add table
Reference in a new issue