pax_global_header 0000666 0000000 0000000 00000000064 14356765077 0014536 g ustar 00root root 0000000 0000000 52 comment=910291b5cfd461a4ccc485ac0090a4cf5f9492c9 cs-custom-bouncer-0.0.15/ 0000775 0000000 0000000 00000000000 14356765077 0015171 5 ustar 00root root 0000000 0000000 cs-custom-bouncer-0.0.15/.github/ 0000775 0000000 0000000 00000000000 14356765077 0016531 5 ustar 00root root 0000000 0000000 cs-custom-bouncer-0.0.15/.github/release-drafter.yml 0000664 0000000 0000000 00000000056 14356765077 0022322 0 ustar 00root root 0000000 0000000 template: | ## What’s Changed $CHANGES cs-custom-bouncer-0.0.15/.github/workflows/ 0000775 0000000 0000000 00000000000 14356765077 0020566 5 ustar 00root root 0000000 0000000 cs-custom-bouncer-0.0.15/.github/workflows/build-binary-package.yml 0000664 0000000 0000000 00000001254 14356765077 0025265 0 ustar 00root root 0000000 0000000 # .github/workflows/build-docker-image.yml name: build-binary-package on: release: types: prereleased jobs: build-binary-package: name: Build and upload binary package runs-on: ubuntu-latest steps: - name: Set up Go 1.19 uses: actions/setup-go@v1 with: go-version: 1.19 id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Build the binaries run: make release - name: Upload to release uses: JasonEtco/upload-to-release@master with: args: crowdsec-custom-bouncer.tgz application/x-gzip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} cs-custom-bouncer-0.0.15/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000004335 14356765077 0024406 0 ustar 00root root 0000000 0000000 name: "CodeQL" on: push: branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '36 1 * * 1' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 cs-custom-bouncer-0.0.15/.github/workflows/go.yml 0000664 0000000 0000000 00000000657 14356765077 0021726 0 ustar 00root root 0000000 0000000 name: Go on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.19 uses: actions/setup-go@v3 with: go-version: 1.19 id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Build run: make build - name: Test run: go test -v cs-custom-bouncer-0.0.15/.github/workflows/release-drafter.yml 0000664 0000000 0000000 00000001130 14356765077 0024351 0 ustar 00root root 0000000 0000000 name: Release Drafter on: push: # branches to consider in the event; optional, defaults to all branches: - main jobs: update_release_draft: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "main" - uses: release-drafter/release-drafter@v5 with: config-name: release-drafter.yml # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml # config-name: my-config.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} cs-custom-bouncer-0.0.15/.gitignore 0000664 0000000 0000000 00000000415 14356765077 0017161 0 ustar 00root root 0000000 0000000 # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ cs-custom-bouncer-0.0.15/LICENSE 0000664 0000000 0000000 00000002056 14356765077 0016201 0 ustar 00root root 0000000 0000000 MIT License Copyright (c) 2020 crowdsecurity Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cs-custom-bouncer-0.0.15/Makefile 0000664 0000000 0000000 00000003032 14356765077 0016627 0 ustar 00root root 0000000 0000000 # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get PREFIX?="/" PID_DIR = $(PREFIX)"/var/run/" BINARY_NAME=crowdsec-custom-bouncer #Current versioning information from env BUILD_VERSION?="$(shell git describe --tags)" BUILD_TIMESTAMP=$(shell date +%F"_"%T) BUILD_TAG="$(shell git rev-parse HEAD)" export LD_OPTS=-ldflags "-s -w -X github.com/crowdsecurity/cs-custom-bouncer/pkg/version.Version=$(BUILD_VERSION) \ -X github.com/crowdsecurity/cs-custom-bouncer/pkg/version.BuildDate=$(BUILD_TIMESTAMP) \ -X github.com/crowdsecurity/cs-custom-bouncer/pkg/version.Tag=$(BUILD_TAG)" RELDIR = "crowdsec-custom-bouncer-${BUILD_VERSION}" all: clean test build static: clean $(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v -a -tags netgo -ldflags '-w -extldflags "-static"' build: clean $(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v test: @$(GOTEST) -v ./... clean: @rm -f $(BINARY_NAME) @rm -rf ${RELDIR} @rm -f crowdsec-custom-bouncer.tgz || "" .PHONY: release release: build @if [ -z ${BUILD_VERSION} ] ; then BUILD_VERSION="local" ; fi @if [ -d $(RELDIR) ]; then echo "$(RELDIR) already exists, clean" ; exit 1 ; fi @echo Building Release to dir $(RELDIR) @mkdir $(RELDIR)/ @cp $(BINARY_NAME) $(RELDIR)/ @cp -R ./config $(RELDIR)/ @cp ./scripts/install.sh $(RELDIR)/ @cp ./scripts/uninstall.sh $(RELDIR)/ @cp ./scripts/upgrade.sh $(RELDIR)/ @chmod +x $(RELDIR)/install.sh @chmod +x $(RELDIR)/uninstall.sh @chmod +x $(RELDIR)/upgrade.sh @tar cvzf crowdsec-custom-bouncer.tgz $(RELDIR) cs-custom-bouncer-0.0.15/README.md 0000664 0000000 0000000 00000001565 14356765077 0016457 0 ustar 00root root 0000000 0000000
📚 Documentation 💠 Hub 💬 Discourse
# crowdsec-custom-bouncer Crowdsec bouncer written in golang for custom scripts. crowdsec-custom-bouncer will periodically fetch new and expired/removed decisions from CrowdSec Local API and will pass them as arguments to a custom user script. ## Installation Please follow the [official documentation](https://doc.crowdsec.net/docs/bouncers/custom). cs-custom-bouncer-0.0.15/config.go 0000664 0000000 0000000 00000007442 14356765077 0016774 0 ustar 00root root 0000000 0000000 package main import ( "fmt" "io" "os" "time" log "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" "gopkg.in/yaml.v2" "github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/yamlpatch" ) type PrometheusConfig struct { Enabled bool `yaml:"enabled"` ListenAddress string `yaml:"listen_addr"` ListenPort string `yaml:"listen_port"` } type bouncerConfig struct { BinPath string `yaml:"bin_path"` // path to binary PidDir string `yaml:"piddir"` UpdateFrequency string `yaml:"update_frequency"` IncludeScenariosContaining []string `yaml:"include_scenarios_containing"` ExcludeScenariosContaining []string `yaml:"exclude_scenarios_containing"` OnlyIncludeDecisionsFrom []string `yaml:"only_include_decisions_from"` Daemon bool `yaml:"daemonize"` LogMode string `yaml:"log_mode"` LogDir string `yaml:"log_dir"` LogLevel log.Level `yaml:"log_level"` LogMaxSize int `yaml:"log_max_size,omitempty"` LogMaxFiles int `yaml:"log_max_files,omitempty"` LogMaxAge int `yaml:"log_max_age,omitempty"` CompressLogs *bool `yaml:"compress_logs,omitempty"` APIUrl string `yaml:"api_url"` APIKey string `yaml:"api_key"` CacheRetentionDuration time.Duration `yaml:"cache_retention_duration"` FeedViaStdin bool `yaml:"feed_via_stdin"` TotalRetries int `yaml:"total_retries"` PrometheusConfig PrometheusConfig `yaml:"prometheus"` } // mergedConfig() returns the byte content of the patched configuration file (with .yaml.local). func mergedConfig(configPath string) ([]byte, error) { patcher := yamlpatch.NewPatcher(configPath, ".local") data, err := patcher.MergedPatchContent() if err != nil { return nil, err } return data, nil } func newConfig(reader io.Reader) (*bouncerConfig, error) { var LogOutput *lumberjack.Logger //io.Writer config := &bouncerConfig{} fcontent, err := io.ReadAll(reader) if err != nil { return &bouncerConfig{}, err } err = yaml.Unmarshal(fcontent, &config) if err != nil { return &bouncerConfig{}, fmt.Errorf("failed to unmarshal: %w", err) } if config.BinPath == "" { return &bouncerConfig{}, fmt.Errorf("bin_path is not set") } if config.LogMode == "" { return &bouncerConfig{}, fmt.Errorf("log_mode is not net") } _, err = os.Stat(config.BinPath) if os.IsNotExist(err) { return config, fmt.Errorf("binary '%s' doesn't exist", config.BinPath) } /*Configure logging*/ if err := types.SetDefaultLoggerConfig(config.LogMode, config.LogDir, config.LogLevel, config.LogMaxSize, config.LogMaxFiles, config.LogMaxAge, config.CompressLogs, false); err != nil { log.Fatal(err.Error()) } if config.LogMode == "file" { if config.LogDir == "" { config.LogDir = "/var/log/" } LogOutput = &lumberjack.Logger{ Filename: config.LogDir + "/crowdsec-custom-bouncer.log", MaxSize: 500, //megabytes MaxBackups: 3, MaxAge: 28, //days Compress: true, //disabled by default } log.SetOutput(LogOutput) log.SetFormatter(&log.TextFormatter{TimestampFormat: "02-01-2006 15:04:05", FullTimestamp: true}) } else if config.LogMode != "stdout" { return &bouncerConfig{}, fmt.Errorf("log mode '%s' unknown, expecting 'file' or 'stdout'", config.LogMode) } if config.CacheRetentionDuration == 0 { log.Infof("cache_retention_duration defaults to 10 seconds") config.CacheRetentionDuration = time.Duration(10 * time.Second) } return config, nil } cs-custom-bouncer-0.0.15/config/ 0000775 0000000 0000000 00000000000 14356765077 0016436 5 ustar 00root root 0000000 0000000 cs-custom-bouncer-0.0.15/config/crowdsec-custom-bouncer.service 0000664 0000000 0000000 00000000437 14356765077 0024600 0 ustar 00root root 0000000 0000000 [Unit] Description=The custom bouncer for CrowdSec After=syslog.target network.target remote-fs.target nss-lookup.target crowdsec.service [Service] Type=notify ExecStart=${BIN} -c ${CFG}/crowdsec-custom-bouncer.yaml ExecStartPost=/bin/sleep 0.1 [Install] WantedBy=multi-user.target cs-custom-bouncer-0.0.15/config/crowdsec-custom-bouncer.yaml 0000664 0000000 0000000 00000001176 14356765077 0024103 0 ustar 00root root 0000000 0000000 bin_path: ${BINARY_PATH} feed_via_stdin: false # Invokes binary once and feeds incoming decisions to it's stdin. total_retries: 0 # number of times to restart binary. relevant if feed_via_stdin=true . Set to -1 for infinite retries. scenarios_containing: [] scenarios_not_containing: [] origins: [] piddir: /var/run/ update_frequency: 10s cache_retention_duration: 10s daemonize: true log_mode: file log_dir: /var/log/ log_level: info log_compression: true log_max_size: 100 log_max_backups: 3 log_max_age: 30 api_url: http://localhost:8080/ api_key: ${API_KEY} prometheus: enabled: true listen_addr: 127.0.0.1 listen_port: 60602 cs-custom-bouncer-0.0.15/custom.go 0000664 0000000 0000000 00000007232 14356765077 0017036 0 ustar 00root root 0000000 0000000 package main import ( "encoding/json" "fmt" "io" "os/exec" "strconv" "time" log "github.com/sirupsen/logrus" "github.com/crowdsecurity/crowdsec/pkg/models" ) type DecisionKey struct { Value string Type string } type DecisionWithAction struct { models.Decision Action string `json:"action,omitempty"` } type customBouncer struct { path string binaryStdin io.Writer feedViaStdin bool newDecisionValueSet map[DecisionKey]struct{} expiredDecisionValueSet map[DecisionKey]struct{} } func newCustomBouncer(cfg *bouncerConfig) (*customBouncer, error) { return &customBouncer{ path: cfg.BinPath, feedViaStdin: cfg.FeedViaStdin, }, nil } func (c *customBouncer) ResetCache() { cachedDecisionCount := len(c.newDecisionValueSet) + len(c.expiredDecisionValueSet) if cachedDecisionCount != 0 { log.Debugf("resetting cache, clearing %d decisions", cachedDecisionCount) // dont return here, because this could be used to intiate the sets } c.newDecisionValueSet = make(map[DecisionKey]struct{}) c.expiredDecisionValueSet = make(map[DecisionKey]struct{}) } func (c *customBouncer) Init() error { c.ResetCache() return nil } func (c *customBouncer) Add(decision *models.Decision) error { if _, exists := c.newDecisionValueSet[decisionToDecisionKey(decision)]; exists { return nil } banDuration, err := time.ParseDuration(*decision.Duration) if err != nil { return err } log.Debugf("custom [%s] : add ban on %s for %s sec (%s)", c.path, *decision.Value, strconv.Itoa(int(banDuration.Seconds())), *decision.Scenario) var str string if c.feedViaStdin { str, err = serializeDecision(decision, "add") } else { str, err = serializeDecision(decision, "") } if err != nil { log.Warningf("serialize: %s", err) } if c.feedViaStdin { fmt.Fprintln(c.binaryStdin, str) return nil } cmd := exec.Command(c.path, "add", *decision.Value, strconv.Itoa(int(banDuration.Seconds())), *decision.Scenario, str) if out, err := cmd.CombinedOutput(); err != nil { log.Errorf("Error in 'add' command (%s): %v --> %s", cmd.String(), err, string(out)) } c.newDecisionValueSet[decisionToDecisionKey(decision)] = struct{}{} return nil } func (c *customBouncer) Delete(decision *models.Decision) error { if _, exists := c.expiredDecisionValueSet[decisionToDecisionKey(decision)]; exists { return nil } banDuration, err := time.ParseDuration(*decision.Duration) if err != nil { return err } var str string if c.feedViaStdin { str, err = serializeDecision(decision, "del") } else { str, err = serializeDecision(decision, "") } if c.feedViaStdin { fmt.Fprintln(c.binaryStdin, str) return nil } if err != nil { log.Warningf("serialize: %s", err) } log.Debugf("custom [%s] : del ban on %s for %s sec (%s)", c.path, *decision.Value, strconv.Itoa(int(banDuration.Seconds())), *decision.Scenario) cmd := exec.Command(c.path, "del", *decision.Value, strconv.Itoa(int(banDuration.Seconds())), *decision.Scenario, str) if out, err := cmd.CombinedOutput(); err != nil { log.Errorf("Error in 'del' command (%s): %v --> %s", cmd.String(), err, string(out)) } c.expiredDecisionValueSet[decisionToDecisionKey(decision)] = struct{}{} return nil } func (c *customBouncer) ShutDown() error { return nil } func serializeDecision(decision *models.Decision, action string) (string, error) { d := DecisionWithAction{Decision: *decision, Action: action} serbyte, err := json.Marshal(d) if err != nil { return "", fmt.Errorf("serialize error : %s", err) } return string(serbyte), nil } func decisionToDecisionKey(decision *models.Decision) DecisionKey { return DecisionKey{ Value: *decision.Value, Type: *decision.Type, } } cs-custom-bouncer-0.0.15/custom_test.go 0000664 0000000 0000000 00000013027 14356765077 0020074 0 ustar 00root root 0000000 0000000 package main import ( "fmt" "os" "reflect" "strings" "testing" "github.com/crowdsecurity/crowdsec/pkg/models" ) const ( binaryPath = "./tests/custombinary" binaryOutputFile = "./data.txt" ) var ( durationWithUnit = "1200s" durationInSeconds = durationWithUnit[:len(durationWithUnit)-1] sceanario = "crowdsec/bruteforce" ip1 = "1.2.3.4" ip2 = "1.2.3.5" decisionType = "IP" ) type parsedLine struct { action string value string duration string sceanario string } func parseFile(path string) []parsedLine { dat, err := os.ReadFile(binaryOutputFile) parsedLines := make([]parsedLine, 0) if err != nil { panic(err) } for _, line := range strings.Split(string(dat), "\n") { if len(line) == 0 { continue } parsedLines = append(parsedLines, parseLine(line)) } return parsedLines } func parseLine(line string) parsedLine { words := strings.Split(line, " ") return parsedLine{ action: words[0], value: words[1], duration: words[2], sceanario: words[3], } } func cleanup() { if _, err := os.Stat(binaryOutputFile); err != nil { fmt.Println("didnt found the file") return } os.Remove(binaryOutputFile) } func Test_customBouncer_Add(t *testing.T) { type args struct { Decisions []*models.Decision } tests := []struct { name string args args expectedLines []parsedLine }{ { name: "simple, single decision", args: args{ Decisions: []*models.Decision{ { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, }, }, expectedLines: []parsedLine{ { action: "add", value: ip1, duration: durationInSeconds, sceanario: sceanario, }, }, }, { name: "simple, two decisions", args: args{ Decisions: []*models.Decision{ { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, { Duration: &durationWithUnit, Value: &ip2, Scenario: &sceanario, Type: &decisionType, }, }, }, expectedLines: []parsedLine{ { action: "add", value: ip1, duration: durationInSeconds, sceanario: sceanario, }, { action: "add", value: ip2, duration: durationInSeconds, sceanario: sceanario, }, }, }, { name: "duplicates", args: args{ Decisions: []*models.Decision{ { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, }, }, expectedLines: []parsedLine{ { action: "add", value: ip1, duration: durationInSeconds, sceanario: sceanario, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer cleanup() c := &customBouncer{ path: binaryPath, } c.ResetCache() for _, decision := range tt.args.Decisions { err := c.Add(decision) if err != nil { t.Error(err) } } foundData := parseFile(binaryOutputFile) if !reflect.DeepEqual(foundData, tt.expectedLines) { t.Errorf("expected=%v, found=%v", tt.expectedLines, foundData) } }) } } func Test_customBouncer_Delete(t *testing.T) { type args struct { Decisions []*models.Decision } tests := []struct { name string args args expectedLines []parsedLine }{ { name: "simple, single decision", args: args{ Decisions: []*models.Decision{ { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, }, }, expectedLines: []parsedLine{ { action: "del", value: ip1, duration: durationInSeconds, sceanario: sceanario, }, }, }, { name: "simple, two decisions", args: args{ Decisions: []*models.Decision{ { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, { Duration: &durationWithUnit, Value: &ip2, Scenario: &sceanario, Type: &decisionType, }, }, }, expectedLines: []parsedLine{ { action: "del", value: ip1, duration: durationInSeconds, sceanario: sceanario, }, { action: "del", value: ip2, duration: durationInSeconds, sceanario: sceanario, }, }, }, { name: "duplicates", args: args{ Decisions: []*models.Decision{ { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, { Duration: &durationWithUnit, Value: &ip1, Scenario: &sceanario, Type: &decisionType, }, }, }, expectedLines: []parsedLine{ { action: "del", value: ip1, duration: durationInSeconds, sceanario: sceanario, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer cleanup() c := &customBouncer{ path: binaryPath, } c.ResetCache() for _, decision := range tt.args.Decisions { err := c.Delete(decision) if err != nil { t.Error(err) } } foundData := parseFile(binaryOutputFile) if !reflect.DeepEqual(foundData, tt.expectedLines) { t.Errorf("expected=%v, found=%v", tt.expectedLines, foundData) } }) } } cs-custom-bouncer-0.0.15/debian/ 0000775 0000000 0000000 00000000000 14356765077 0016413 5 ustar 00root root 0000000 0000000 cs-custom-bouncer-0.0.15/debian/changelog 0000664 0000000 0000000 00000000245 14356765077 0020266 0 ustar 00root root 0000000 0000000 crowdsec-custom-bouncer (1.0.0) UNRELEASED; urgency=medium * Initial debian packaging -- Shivam Sandbhor