From 0449ec1868d934095d2bc39aa65ad007be4f603c Mon Sep 17 00:00:00 2001 From: blotus Date: Tue, 17 May 2022 18:14:59 +0800 Subject: [PATCH] Windows Support (#1159) --- .github/workflows/ci-go-test-windows.yml | 33 ++ .github/workflows/ci-windows-build-msi.yml | 37 ++ .../workflows/ci_golangci-lint-windows.yml | 32 ++ .gitignore | 11 + Makefile | 184 +++++----- azure-pipelines.yml | 103 ++++++ cmd/crowdsec-cli/Makefile | 16 +- cmd/crowdsec-cli/completion.go | 24 +- cmd/crowdsec-cli/configfile.go | 5 + cmd/crowdsec-cli/configfile_windows.go | 6 + cmd/crowdsec-cli/messages.go | 7 +- cmd/crowdsec/Makefile | 18 +- cmd/crowdsec/api.go | 4 +- cmd/crowdsec/configfile.go | 5 + cmd/crowdsec/configfile_windows.go | 3 + cmd/crowdsec/main.go | 47 +-- cmd/crowdsec/run_in_svc.go | 61 ++++ cmd/crowdsec/run_in_svc_windows.go | 101 ++++++ cmd/crowdsec/serve.go | 7 +- cmd/crowdsec/win_service.go | 87 +++++ cmd/crowdsec/win_service_install.go | 95 ++++++ cmd/crowdsec/win_service_manage.go | 64 ++++ config/acquis_win.yaml | 8 + config/config_win.yaml | 49 +++ config/config_win_no_lapi.yaml | 28 ++ go.mod | 12 +- go.sum | 18 +- make_chocolatey.ps1 | 18 + make_installer.ps1 | 20 ++ pkg/acquisition/acquisition.go | 16 +- .../configuration/configuration.go | 13 +- .../modules/cloudwatch/cloudwatch.go | 8 +- .../modules/cloudwatch/cloudwatch_test.go | 16 + pkg/acquisition/modules/docker/docker.go | 9 +- pkg/acquisition/modules/docker/docker_test.go | 10 +- pkg/acquisition/modules/file/file.go | 15 +- pkg/acquisition/modules/file/file_test.go | 95 ++++-- pkg/acquisition/modules/file/tailline.go | 7 + .../modules/file/tailline_windows.go | 9 + .../modules/journalctl/journalctl.go | 7 +- .../modules/journalctl/journalctl_test.go | 13 + pkg/acquisition/modules/kinesis/kinesis.go | 7 +- .../modules/kinesis/kinesis_test.go | 13 + pkg/acquisition/modules/syslog/syslog.go | 6 +- pkg/acquisition/modules/syslog/syslog_test.go | 16 +- .../modules/wineventlog/wineventlog.go | 59 ++++ .../modules/wineventlog/wineventlog_test.go | 233 +++++++++++++ .../wineventlog/wineventlog_windows.go | 320 ++++++++++++++++++ pkg/apiclient/client_test.go | 7 +- pkg/apiserver/alerts_test.go | 27 +- pkg/apiserver/api_key_test.go | 4 +- pkg/apiserver/apic_test.go | 11 +- pkg/apiserver/apiserver_test.go | 76 ++--- pkg/apiserver/jwt_test.go | 4 +- pkg/apiserver/machines_test.go | 16 +- pkg/apiserver/testutils.go | 9 + pkg/apiserver/testutils_windows.go | 7 + pkg/csconfig/config_test.go | 11 +- pkg/csconfig/simulation_test.go | 48 ++- pkg/csplugin/broker.go | 112 +----- pkg/csplugin/broker_test.go | 23 +- pkg/csplugin/broker_win_test.go | 257 ++++++++++++++ pkg/csplugin/utils.go | 150 ++++++++ pkg/csplugin/utils_windows.go | 242 +++++++++++++ pkg/csplugin/watcher_test.go | 7 + pkg/cstest/hubtest_item.go | 6 +- pkg/cwhub/cwhub_test.go | 5 +- pkg/cwhub/download.go | 1 + pkg/cwhub/loader.go | 21 +- pkg/cwhub/path_separator_windows.go | 23 ++ pkg/cwhub/pathseparator.go | 24 ++ pkg/time/rate/rate_test.go | 3 + pkg/types/utils.go | 2 +- platform/freebsd.mk | 1 + platform/linux.mk | 1 + platform/unix_common.mk | 18 + platform/windows.mk | 32 ++ plugins/notifications/dummy/Makefile | 11 +- plugins/notifications/email/Makefile | 11 +- plugins/notifications/http/Makefile | 13 +- plugins/notifications/slack/Makefile | 10 +- plugins/notifications/splunk/Makefile | 14 +- scripts/check_go_version.ps1 | 19 ++ scripts/test_env.ps1 | 90 +++++ tests/bats.mk | 5 +- windows/Chocolatey/crowdsec/ReadMe.md | 133 ++++++++ windows/Chocolatey/crowdsec/crowdsec.nuspec | 43 +++ windows/Chocolatey/crowdsec/tools/LICENSE.txt | 26 ++ .../crowdsec/tools/VERIFICATION.txt | 9 + .../crowdsec/tools/chocolateybeforemodify.ps1 | 1 + .../crowdsec/tools/chocolateyinstall.ps1 | 29 ++ .../crowdsec/tools/chocolateyuninstall.ps1 | 30 ++ windows/install_dev_windows.ps1 | 7 + windows/install_installer_windows.ps1 | 2 + windows/installer/WixUI_HK.wxs | 65 ++++ windows/installer/crowdsec_icon.ico | Bin 0 -> 4286 bytes windows/installer/crowdsec_msi_top_banner.bmp | Bin 0 -> 114514 bytes windows/installer/installer_dialog.bmp | Bin 0 -> 155914 bytes windows/installer/product.wxs | 165 +++++++++ windows/windows.md | 32 ++ 100 files changed, 3401 insertions(+), 437 deletions(-) create mode 100644 .github/workflows/ci-go-test-windows.yml create mode 100644 .github/workflows/ci-windows-build-msi.yml create mode 100644 .github/workflows/ci_golangci-lint-windows.yml create mode 100644 azure-pipelines.yml create mode 100644 cmd/crowdsec-cli/configfile.go create mode 100644 cmd/crowdsec-cli/configfile_windows.go create mode 100644 cmd/crowdsec/configfile.go create mode 100644 cmd/crowdsec/configfile_windows.go create mode 100644 cmd/crowdsec/run_in_svc.go create mode 100644 cmd/crowdsec/run_in_svc_windows.go create mode 100644 cmd/crowdsec/win_service.go create mode 100644 cmd/crowdsec/win_service_install.go create mode 100644 cmd/crowdsec/win_service_manage.go create mode 100644 config/acquis_win.yaml create mode 100644 config/config_win.yaml create mode 100644 config/config_win_no_lapi.yaml create mode 100644 make_chocolatey.ps1 create mode 100644 make_installer.ps1 create mode 100644 pkg/acquisition/modules/file/tailline.go create mode 100644 pkg/acquisition/modules/file/tailline_windows.go create mode 100644 pkg/acquisition/modules/wineventlog/wineventlog.go create mode 100644 pkg/acquisition/modules/wineventlog/wineventlog_test.go create mode 100644 pkg/acquisition/modules/wineventlog/wineventlog_windows.go create mode 100644 pkg/apiserver/testutils.go create mode 100644 pkg/apiserver/testutils_windows.go create mode 100644 pkg/csplugin/broker_win_test.go create mode 100644 pkg/csplugin/utils.go create mode 100644 pkg/csplugin/utils_windows.go create mode 100644 pkg/cwhub/path_separator_windows.go create mode 100644 pkg/cwhub/pathseparator.go create mode 100644 platform/unix_common.mk create mode 100644 platform/windows.mk create mode 100644 scripts/check_go_version.ps1 create mode 100644 scripts/test_env.ps1 create mode 100644 windows/Chocolatey/crowdsec/ReadMe.md create mode 100644 windows/Chocolatey/crowdsec/crowdsec.nuspec create mode 100644 windows/Chocolatey/crowdsec/tools/LICENSE.txt create mode 100644 windows/Chocolatey/crowdsec/tools/VERIFICATION.txt create mode 100644 windows/Chocolatey/crowdsec/tools/chocolateybeforemodify.ps1 create mode 100644 windows/Chocolatey/crowdsec/tools/chocolateyinstall.ps1 create mode 100644 windows/Chocolatey/crowdsec/tools/chocolateyuninstall.ps1 create mode 100644 windows/install_dev_windows.ps1 create mode 100644 windows/install_installer_windows.ps1 create mode 100644 windows/installer/WixUI_HK.wxs create mode 100644 windows/installer/crowdsec_icon.ico create mode 100644 windows/installer/crowdsec_msi_top_banner.bmp create mode 100644 windows/installer/installer_dialog.bmp create mode 100644 windows/installer/product.wxs create mode 100644 windows/windows.md diff --git a/.github/workflows/ci-go-test-windows.yml b/.github/workflows/ci-go-test-windows.yml new file mode 100644 index 000000000..126e03037 --- /dev/null +++ b/.github/workflows/ci-go-test-windows.yml @@ -0,0 +1,33 @@ +name: tests-windows + +on: + push: + branches: [ master ] + paths-ignore: + - 'docs/**' + - 'mkdocs.yml' + - 'README.md' + pull_request: + branches: [ master ] + paths-ignore: + - 'docs/**' + - 'mkdocs.yml' + - 'README.md' + +jobs: + + build: + name: Build + runs-on: windows-2022 + steps: + - name: Set up Go 1.17 + uses: actions/setup-go@v1 + with: + go-version: 1.17 + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Build + run: make build && go get -u github.com/jandelgado/gcov2lcov + - name: All tests + run: go test -coverprofile coverage.out -covermode=atomic ./... diff --git a/.github/workflows/ci-windows-build-msi.yml b/.github/workflows/ci-windows-build-msi.yml new file mode 100644 index 000000000..214465d85 --- /dev/null +++ b/.github/workflows/ci-windows-build-msi.yml @@ -0,0 +1,37 @@ +name: build-msi + +on: + pull_request: + branches: [ master ] + paths-ignore: + - 'docs/**' + - 'mkdocs.yml' + - 'README.md' + +jobs: + + build: + name: Build + runs-on: windows-2019 + steps: + - name: Set up Go 1.17 + uses: actions/setup-go@v1 + with: + go-version: 1.17 + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - id: get_latest_release + uses: pozetroninc/github-action-get-latest-release@master + with: + repository: crowdsecurity/crowdsec + excludes: draft + - id: set_release_in_env + run: echo "BUILD_VERSION=${{ steps.get_latest_release.outputs.release }}" >> $env:GITHUB_ENV + - name: Build + run: make windows_installer + - name: Upload MSI + uses: actions/upload-artifact@v2 + with: + path: crowdsec*msi + name: crowdsec.msi diff --git a/.github/workflows/ci_golangci-lint-windows.yml b/.github/workflows/ci_golangci-lint-windows.yml new file mode 100644 index 000000000..be2c59d14 --- /dev/null +++ b/.github/workflows/ci_golangci-lint-windows.yml @@ -0,0 +1,32 @@ +name: golangci-lint-windows +on: + push: + tags: + - v* + branches: + - master + paths-ignore: + - 'docs/**' + - 'mkdocs.yml' + - 'README.md' + pull_request: + paths-ignore: + - 'docs/**' + - 'mkdocs.yml' + - 'README.md' +jobs: + golangci: + name: lint-windows + runs-on: windows-2022 + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: v1.45.2 + # Optional: golangci-lint command line arguments. + args: --issues-exit-code=0 --timeout 5m + only-new-issues: true + + diff --git a/.gitignore b/.gitignore index f67aa1c47..0ccbb993d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,14 @@ plugins/notifications/slack/notification-slack plugins/notifications/splunk/notification-splunk plugins/notifications/email/notification-email plugins/notifications/dummy/notification-dummy + +#test binaries +pkg/csplugin/tests/cs_plugin_test* + +#release stuff +crowdsec-v* +pkg/cwhub/hubdir/.index.json +msi +*.msi +*.nukpg +*.tgz diff --git a/Makefile b/Makefile index dfb47091e..e924c4239 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,13 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +ROOT= $(shell (Get-Location).Path) +SYSTEM=windows +EXT=.exe +else +ROOT?= $(shell pwd) SYSTEM?= $(shell uname -s | tr '[A-Z]' '[a-z]') +endif ifneq ("$(wildcard $(CURDIR)/platform/$(SYSTEM).mk)", "") include $(CURDIR)/platform/$(SYSTEM).mk @@ -6,58 +15,50 @@ else include $(CURDIR)/platform/linux.mk endif -CROWDSEC_FOLDER = "./cmd/crowdsec" -CSCLI_FOLDER = "./cmd/crowdsec-cli/" +ifneq ($(OS),Windows_NT) + include $(ROOT)/platform/unix_common.mk +endif -HTTP_PLUGIN_FOLDER = "./plugins/notifications/http" -SLACK_PLUGIN_FOLDER = "./plugins/notifications/slack" -SPLUNK_PLUGIN_FOLDER = "./plugins/notifications/splunk" -EMAIL_PLUGIN_FOLDER = "./plugins/notifications/email" -DUMMY_PLUGIN_FOLDER = "./plugins/notifications/dummy" +CROWDSEC_FOLDER = ./cmd/crowdsec +CSCLI_FOLDER = ./cmd/crowdsec-cli/ -HTTP_PLUGIN_BIN = "notification-http" -SLACK_PLUGIN_BIN = "notification-slack" -SPLUNK_PLUGIN_BIN = "notification-splunk" -EMAIL_PLUGIN_BIN = "notification-email" -DUMMY_PLUGIN_BIN= "notification-dummy" +HTTP_PLUGIN_FOLDER = ./plugins/notifications/http +SLACK_PLUGIN_FOLDER = ./plugins/notifications/slack +SPLUNK_PLUGIN_FOLDER = ./plugins/notifications/splunk +EMAIL_PLUGIN_FOLDER = ./plugins/notifications/email +DUMMY_PLUGIN_FOLDER = ./plugins/notifications/dummy -HTTP_PLUGIN_CONFIG = "http.yaml" -SLACK_PLUGIN_CONFIG = "slack.yaml" -SPLUNK_PLUGIN_CONFIG = "splunk.yaml" -EMAIL_PLUGIN_CONFIG = "email.yaml" +HTTP_PLUGIN_BIN = notification-http$(EXT) +SLACK_PLUGIN_BIN = notification-slack$(EXT) +SPLUNK_PLUGIN_BIN = notification-splunk$(EXT) +EMAIL_PLUGIN_BIN = notification-email$(EXT) +DUMMY_PLUGIN_BIN= notification-dummy$(EXT) -CROWDSEC_BIN = "crowdsec" -CSCLI_BIN = "cscli" -BUILD_CMD = "build" +HTTP_PLUGIN_CONFIG = http.yaml +SLACK_PLUGIN_CONFIG = slack.yaml +SPLUNK_PLUGIN_CONFIG = splunk.yaml +EMAIL_PLUGIN_CONFIG = email.yaml + +CROWDSEC_BIN = crowdsec$(EXT) +CSCLI_BIN = cscli$(EXT) +BUILD_CMD = build GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) -# Golang version info -GO_MAJOR_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1) -GO_MINOR_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2) MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1 MINIMUM_SUPPORTED_GO_MINOR_VERSION = 17 GO_VERSION_VALIDATION_ERR_MSG = Golang version ($(BUILD_GOVERSION)) is not supported, please use at least $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION).$(MINIMUM_SUPPORTED_GO_MINOR_VERSION) -# Current versioning information from env -BUILD_VERSION ?= "$(shell git describe --tags)" -BUILD_GOVERSION = "$(shell go version | cut -d " " -f3 | sed -E 's/[go]+//g')" -BUILD_CODENAME = "alphaga" -BUILD_TIMESTAMP = $(shell date +%F"_"%T) -BUILD_TAG ?= "$(shell git rev-parse HEAD)" -DEFAULT_CONFIGDIR ?= "/etc/crowdsec" -DEFAULT_DATADIR ?= "/var/lib/crowdsec/data" -BINCOVER_TESTING ?= false - LD_OPTS_VARS= \ -X github.com/crowdsecurity/crowdsec/cmd/crowdsec/main.bincoverTesting=$(BINCOVER_TESTING) \ -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=$(BUILD_VERSION) \ -X github.com/crowdsecurity/crowdsec/pkg/cwversion.BuildDate=$(BUILD_TIMESTAMP) \ -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Codename=$(BUILD_CODENAME) \ -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Tag=$(BUILD_TAG) \ --X github.com/crowdsecurity/crowdsec/pkg/csconfig.defaultConfigDir=$(DEFAULT_CONFIGDIR) \ --X github.com/crowdsecurity/crowdsec/pkg/csconfig.defaultDataDir=$(DEFAULT_DATADIR) +-X github.com/crowdsecurity/crowdsec/pkg/cwversion.GoVersion=$(BUILD_GOVERSION) \ +-X 'github.com/crowdsecurity/crowdsec/pkg/csconfig.defaultConfigDir=$(DEFAULT_CONFIGDIR)' \ +-X 'github.com/crowdsecurity/crowdsec/pkg/csconfig.defaultDataDir=$(DEFAULT_DATADIR)' export LD_OPTS=-ldflags "-s -w $(LD_OPTS_VARS)" export LD_OPTS_STATIC=-ldflags "-s -w $(LD_OPTS_VARS) -extldflags '-static'" @@ -82,6 +83,7 @@ plugins: http-plugin slack-plugin splunk-plugin email-plugin dummy-plugin plugins_static: http-plugin_static slack-plugin_static splunk-plugin_static email-plugin_static dummy-plugin_static goversion: +ifneq ($(OS),Windows_NT) @if [ $(GO_MAJOR_VERSION) -gt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ exit 0 ;\ elif [ $(GO_MAJOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ @@ -91,105 +93,110 @@ goversion: echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\ exit 1; \ fi +else +#This needs Set-ExecutionPolicy -Scope CurrentUser Unrestricted + @$(ROOT)/scripts/check_go_version.ps1 $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) $(MINIMUM_SUPPORTED_GO_MINOR_VERSION) +endif .PHONY: clean clean: testclean - @$(MAKE) -C $(CROWDSEC_FOLDER) clean --no-print-directory - @$(MAKE) -C $(CSCLI_FOLDER) clean --no-print-directory - @$(RM) $(CROWDSEC_BIN) - @$(RM) $(CSCLI_BIN) - @$(RM) *.log - @$(RM) crowdsec-release.tgz - @$(RM) crowdsec-release-static.tgz - @$(RM) $(HTTP_PLUGIN_FOLDER)/$(HTTP_PLUGIN_BIN) - @$(RM) $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_BIN) - @$(RM) $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_BIN) - @$(RM) $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_BIN) - @$(RM) $(DUMMY_PLUGIN_FOLDER)/$(DUMMY_PLUGIN_BIN) + @$(MAKE) -C $(CROWDSEC_FOLDER) clean --no-print-directory RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" + @$(MAKE) -C $(CSCLI_FOLDER) clean --no-print-directory RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" + @$(RM) $(CROWDSEC_BIN) $(WIN_IGNORE_ERR) + @$(RM) $(CSCLI_BIN) $(WIN_IGNORE_ERR) + @$(RM) *.log $(WIN_IGNORE_ERR) + @$(RM) crowdsec-release.tgz $(WIN_IGNORE_ERR) + @$(RM) crowdsec-release-static.tgz $(WIN_IGNORE_ERR) + @$(RM) $(HTTP_PLUGIN_FOLDER)/$(HTTP_PLUGIN_BIN) $(WIN_IGNORE_ERR) + @$(RM) $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_BIN) $(WIN_IGNORE_ERR) + @$(RM) $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_BIN) $(WIN_IGNORE_ERR) + @$(RM) $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_BIN) $(WIN_IGNORE_ERR) + @$(RM) $(DUMMY_PLUGIN_FOLDER)/$(DUMMY_PLUGIN_BIN) $(WIN_IGNORE_ERR) cscli: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CSCLI_FOLDER) build --no-print-directory + @$(MAKE) -C $(CSCLI_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" cscli-bincover: goversion @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CSCLI_FOLDER) build-bincover --no-print-directory crowdsec: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CROWDSEC_FOLDER) build --no-print-directory + @$(MAKE) -C $(CROWDSEC_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" crowdsec-bincover: goversion @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CROWDSEC_FOLDER) build-bincover --no-print-directory http-plugin: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(HTTP_PLUGIN_FOLDER) build --no-print-directory + @$(MAKE) -C $(HTTP_PLUGIN_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" slack-plugin: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(SLACK_PLUGIN_FOLDER) build --no-print-directory + @$(MAKE) -C $(SLACK_PLUGIN_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" splunk-plugin: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(SPLUNK_PLUGIN_FOLDER) build --no-print-directory + @$(MAKE) -C $(SPLUNK_PLUGIN_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" email-plugin: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(EMAIL_PLUGIN_FOLDER) build --no-print-directory + @$(MAKE) -C $(EMAIL_PLUGIN_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" dummy-plugin: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(DUMMY_PLUGIN_FOLDER) build --no-print-directory + $(MAKE) -C $(DUMMY_PLUGIN_FOLDER) build --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" cscli_static: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CSCLI_FOLDER) static --no-print-directory + @$(MAKE) -C $(CSCLI_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" crowdsec_static: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CROWDSEC_FOLDER) static --no-print-directory + @$(MAKE) -C $(CROWDSEC_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" http-plugin_static: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(HTTP_PLUGIN_FOLDER) static --no-print-directory + @$(MAKE) -C $(HTTP_PLUGIN_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" slack-plugin_static: goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(SLACK_PLUGIN_FOLDER) static --no-print-directory + @$(MAKE) -C $(SLACK_PLUGIN_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" splunk-plugin_static:goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(SPLUNK_PLUGIN_FOLDER) static --no-print-directory + @$(MAKE) -C $(SPLUNK_PLUGIN_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" email-plugin_static:goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(EMAIL_PLUGIN_FOLDER) static --no-print-directory + @$(MAKE) -C $(EMAIL_PLUGIN_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" dummy-plugin_static:goversion - @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(DUMMY_PLUGIN_FOLDER) static --no-print-directory + $(MAKE) -C $(DUMMY_PLUGIN_FOLDER) static --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)" .PHONY: testclean testclean: bats-clean - @$(RM) pkg/apiserver/ent - @$(RM) -r pkg/cwhub/hubdir + @$(RM) pkg/apiserver/ent $(WIN_IGNORE_ERR) + @$(RM) pkg/cwhub/hubdir $(WIN_IGNORE_ERR) .PHONY: test test: goversion $(GOTEST) $(LD_OPTS) ./... package-common: - @echo Building Release to dir $(RELDIR) - @mkdir -p $(RELDIR)/cmd/crowdsec - @mkdir -p $(RELDIR)/cmd/crowdsec-cli - @mkdir -p $(RELDIR)/$(subst ./,,$(HTTP_PLUGIN_FOLDER)) - @mkdir -p $(RELDIR)/$(subst ./,,$(SLACK_PLUGIN_FOLDER)) - @mkdir -p $(RELDIR)/$(subst ./,,$(SPLUNK_PLUGIN_FOLDER)) - @mkdir -p $(RELDIR)/$(subst ./,,$(EMAIL_PLUGIN_FOLDER)) + @echo "Building Release to dir $(RELDIR)" + @$(MKDIR) $(RELDIR)/cmd/crowdsec + @$(MKDIR) $(RELDIR)/cmd/crowdsec-cli + @$(MKDIR) $(RELDIR)/$(subst ./,,$(HTTP_PLUGIN_FOLDER)) + @$(MKDIR) $(RELDIR)/$(subst ./,,$(SLACK_PLUGIN_FOLDER)) + @$(MKDIR) $(RELDIR)/$(subst ./,,$(SPLUNK_PLUGIN_FOLDER)) + @$(MKDIR) $(RELDIR)/$(subst ./,,$(EMAIL_PLUGIN_FOLDER)) - @cp $(CROWDSEC_FOLDER)/$(CROWDSEC_BIN) $(RELDIR)/cmd/crowdsec - @cp $(CSCLI_FOLDER)/$(CSCLI_BIN) $(RELDIR)/cmd/crowdsec-cli + @$(CP) $(CROWDSEC_FOLDER)/$(CROWDSEC_BIN) $(RELDIR)/cmd/crowdsec + @$(CP) $(CSCLI_FOLDER)/$(CSCLI_BIN) $(RELDIR)/cmd/crowdsec-cli - @cp $(HTTP_PLUGIN_FOLDER)/$(HTTP_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(HTTP_PLUGIN_FOLDER)) - @cp $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(SLACK_PLUGIN_FOLDER)) - @cp $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(SPLUNK_PLUGIN_FOLDER)) - @cp $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(EMAIL_PLUGIN_FOLDER)) + @$(CP) $(HTTP_PLUGIN_FOLDER)/$(HTTP_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(HTTP_PLUGIN_FOLDER)) + @$(CP) $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(SLACK_PLUGIN_FOLDER)) + @$(CP) $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(SPLUNK_PLUGIN_FOLDER)) + @$(CP) $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_BIN) $(RELDIR)/$(subst ./,,$(EMAIL_PLUGIN_FOLDER)) - @cp $(HTTP_PLUGIN_FOLDER)/$(HTTP_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(HTTP_PLUGIN_FOLDER)) - @cp $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(SLACK_PLUGIN_FOLDER)) - @cp $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(SPLUNK_PLUGIN_FOLDER)) - @cp $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(EMAIL_PLUGIN_FOLDER)) + @$(CP) $(HTTP_PLUGIN_FOLDER)/$(HTTP_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(HTTP_PLUGIN_FOLDER)) + @$(CP) $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(SLACK_PLUGIN_FOLDER)) + @$(CP) $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(SPLUNK_PLUGIN_FOLDER)) + @$(CP) $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_CONFIG) $(RELDIR)/$(subst ./,,$(EMAIL_PLUGIN_FOLDER)) - @cp -R ./config $(RELDIR) - @cp wizard.sh $(RELDIR) - @cp scripts/test_env.sh $(RELDIR) + @$(CPR) ./config $(RELDIR) + @$(CP) wizard.sh $(RELDIR) + @$(CP) scripts/test_env.sh $(RELDIR) + @$(CP) scripts/test_env.ps1 $(RELDIR) .PHONY: package package: package-common @@ -200,7 +207,11 @@ package_static: package-common .PHONY: check_release check_release: +ifneq ($(OS),Windows_NT) @if [ -d $(RELDIR) ]; then echo "$(RELDIR) already exists, abort" ; exit 1 ; fi +else + @if (Test-Path -Path $(RELDIR)) { echo "$(RELDIR) already exists, abort" ; exit 1 ; } +endif .PHONY: release release: check_release build package @@ -208,5 +219,12 @@ release: check_release build package .PHONY: release_static release_static: check_release static package_static -include tests/bats.mk +.PHONY: windows_installer +windows_installer: build + @.\make_installer.ps1 -version $(BUILD_VERSION) +.PHONY: chocolatey +chocolatey: windows_installer + @.\make_chocolatey.ps1 -version $(BUILD_VERSION) + +include tests/bats.mk \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..a68d807e0 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,103 @@ +trigger: + tags: + include: + - "v*" + branches: + exclude: + - "*" +pr: none + +pool: + vmImage: windows-latest + +stages: + - stage: Build + jobs: + - job: + displayName: "Build" + steps: + - task: DotNetCoreCLI@2 + displayName: "Install SignClient" + inputs: + command: 'custom' + custom: 'tool' + arguments: 'install --global SignClient --version 1.3.155' + - task: GoTool@0 + displayName: "Install Go 1.17" + inputs: + version: '1.17.9' + + - pwsh: | + choco install -y jq + choco install -y make + displayName: "Install builds deps" + - task: PowerShell@2 + inputs: + targetType: 'inline' + pwsh: true + #we are not calling make windows_installer because we want to sign the binaries before they are added to the MSI + script: | + make build + - task: AzureKeyVault@2 + inputs: + azureSubscription: 'Azure subscription 1(8a93ab40-7e99-445e-ad47-0f6a3e2ef546)' + KeyVaultName: 'CodeSigningSecrets' + SecretsFilter: 'CodeSigningUser,CodeSigningPassword' + RunAsPreJob: false + + - task: DownloadSEcureFile@1 + inputs: + secureFile: appsettings.json + + - pwsh: | + SignClient.exe Sign --name "crowdsec-binaries" ` + --input "**/*.exe" --config (Join-Path -Path $(Agent.TempDirectory) -ChildPath "appsettings.json") ` + --user $(CodeSigningUser) --secret '$(CodeSigningPassword)' + displayName: "Sign Crowdsec binaries + plugins" + + - pwsh: | + $build_version=(git describe --tags (git rev-list --tags --max-count=1)).Substring(1) + .\make_installer.ps1 -version $build_version + Write-Host "##vso[task.setvariable variable=BuildVersion;isOutput=true]$build_version" + displayName: "Build Crowdsec MSI" + name: BuildMSI + + - pwsh: | + SignClient.exe Sign --name "crowdsec-msi" ` + --input "*.msi" --config (Join-Path -Path $(Agent.TempDirectory) -ChildPath "appsettings.json") ` + --user $(CodeSigningUser) --secret '$(CodeSigningPassword)' + displayName: "Sign Crowdsec MSI" + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(Build.Repository.LocalPath)\\crowdsec_$(BuildMSI.BuildVersion).msi' + ArtifactName: 'crowdsec.msi' + publishLocation: 'Container' + displayName: "Upload MSI artifact" + - stage: Publish + dependsOn: Build + jobs: + - deployment: "Publish" + displayName: "Publish to GitHub" + environment: github + strategy: + runOnce: + deploy: + steps: + - bash: | + tag=$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/crowdsecurity/crowdsec/releases | jq -r '. | map(select(.prerelease==true)) | sort_by(.created_at) | reverse | .[0].tag_name') + echo "##vso[task.setvariable variable=LatestPreRelease;isOutput=true]$tag" + name: GetLatestPrelease + - task: GitHubRelease@1 + inputs: + gitHubConnection: "github.com_blotus" + repositoryName: '$(Build.Repository.Name)' + action: 'edit' + tag: '$(GetLatestPrelease.LatestPreRelease)' + assetUploadMode: 'replace' + addChangeLog: false + isPreRelease: true #we force prerelease because the pipeline is invoked on tag creation, which happens when we do a prerelease + #the .. is an ugly hack, but I can't find the var that gives D:\a\1 ... + assets: | + $(Build.ArtifactStagingDirectory)\..\crowdsec.msi + condition: ne(variables['GetLatestPrelease.LatestPreRelease'], '') diff --git a/cmd/crowdsec-cli/Makefile b/cmd/crowdsec-cli/Makefile index 6f94bbcda..3076c3b76 100644 --- a/cmd/crowdsec-cli/Makefile +++ b/cmd/crowdsec-cli/Makefile @@ -1,11 +1,17 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get - -BINARY_NAME=cscli +BINARY_NAME=cscli$(EXT) # names longer than 15 chars break 'pgrep' BINARY_NAME_COVER=$(BINARY_NAME).cover PREFIX?="/" @@ -32,8 +38,8 @@ install-bin: @install -v -m 755 -D "$(BINARY_NAME)" "$(BIN_PREFIX)/$(BINARY_NAME)" || exit uninstall: - @$(RM) -r $(CSCLI_CONFIG) - @$(RM) -r $(BIN_PREFIX)$(BINARY_NAME) + @$(RM) $(CSCLI_CONFIG) $(WIN_IGNORE_ERR) + @$(RM) $(BIN_PREFIX)$(BINARY_NAME) $(WIN_IGNORE_ERR) clean: - @$(RM) $(BINARY_NAME) $(BINARY_NAME_COVER) + @$(RM) $(BINARY_NAME) $(BINARY_NAME_COVER) $(WIN_IGNORE_ERR) diff --git a/cmd/crowdsec-cli/completion.go b/cmd/crowdsec-cli/completion.go index 056c5c103..c5604d36e 100644 --- a/cmd/crowdsec-cli/completion.go +++ b/cmd/crowdsec-cli/completion.go @@ -9,7 +9,7 @@ import ( func NewCompletionCmd() *cobra.Command { var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh]", + Use: "completion [bash|zsh|powershell|fish]", Short: "Generate completion script", Long: `To load completions: @@ -49,10 +49,25 @@ func NewCompletionCmd() *cobra.Command { $ cscli completion zsh > "${fpath[1]}/_cscli" # You will need to start a new shell for this setup to take effect. + +### fish: +` + "```shell" + ` + $ cscli completion fish | source + + # To load completions for each session, execute once: + $ cscli completion fish > ~/.config/fish/completions/cscli.fish +` + "```" + ` +### PowerShell: +` + "```powershell" + ` + PS> cscli completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> cscli completion powershell > cscli.ps1 + # and source this file from your PowerShell profile. ` + "```", DisableFlagsInUseLine: true, DisableAutoGenTag: true, - ValidArgs: []string{"bash", "zsh"}, + ValidArgs: []string{"bash", "zsh", "powershell", "fish"}, Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { switch args[0] { @@ -60,9 +75,10 @@ func NewCompletionCmd() *cobra.Command { cmd.Root().GenBashCompletion(os.Stdout) case "zsh": cmd.Root().GenZshCompletion(os.Stdout) - /*case "fish": + case "powershell": + cmd.Root().GenPowerShellCompletion(os.Stdout) + case "fish": cmd.Root().GenFishCompletion(os.Stdout, true) - */ } }, } diff --git a/cmd/crowdsec-cli/configfile.go b/cmd/crowdsec-cli/configfile.go new file mode 100644 index 000000000..c61c41611 --- /dev/null +++ b/cmd/crowdsec-cli/configfile.go @@ -0,0 +1,5 @@ +// +build linux freebsd netbsd openbsd solaris !windows + +package main + +const DefaultConfigFile = "/etc/crowdsec/config.yaml" diff --git a/cmd/crowdsec-cli/configfile_windows.go b/cmd/crowdsec-cli/configfile_windows.go new file mode 100644 index 000000000..dcb4d92b3 --- /dev/null +++ b/cmd/crowdsec-cli/configfile_windows.go @@ -0,0 +1,6 @@ +//go:build windows +// +build windows + +package main + +const DefaultConfigFile = "C:\\ProgramData\\CrowdSec\\config\\config.yaml" diff --git a/cmd/crowdsec-cli/messages.go b/cmd/crowdsec-cli/messages.go index 1634f0dc8..2e6740258 100644 --- a/cmd/crowdsec-cli/messages.go +++ b/cmd/crowdsec-cli/messages.go @@ -15,9 +15,12 @@ func ReloadMessage() string { var reloadCmd string - if runtime.GOOS == "freebsd" { + switch runtime.GOOS { + case "windows": + return "Please restart the crowdsec service for the new configuration to be effective." + case "freebsd": reloadCmd = ReloadCmdFreebsd - } else { + default: reloadCmd = ReloadCmdLinux } diff --git a/cmd/crowdsec/Makefile b/cmd/crowdsec/Makefile index 4d9a90c61..7a9db1c59 100644 --- a/cmd/crowdsec/Makefile +++ b/cmd/crowdsec/Makefile @@ -1,3 +1,9 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build @@ -5,7 +11,7 @@ GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get -CROWDSEC_BIN=crowdsec +CROWDSEC_BIN=crowdsec$(EXT) # names longer than 15 chars break 'pgrep' CROWDSEC_BIN_COVER=$(CROWDSEC_BIN).cover PREFIX?="/" @@ -32,7 +38,7 @@ test: $(GOTEST) $(LD_OPTS) -v ./... clean: - @$(RM) $(CROWDSEC_BIN) $(CROWDSEC_BIN).test $(CROWDSEC_BIN_COVER) + @$(RM) $(CROWDSEC_BIN) $(CROWDSEC_BIN).test $(CROWDSEC_BIN_COVER) $(WIN_IGNORE_ERR) .PHONY: install install: install-conf install-bin @@ -67,7 +73,7 @@ systemd: install .PHONY: uninstall uninstall: - $(RM) -r "$(CFG_PREFIX)" - $(RM) -r "$(DATA_PREFIX)" - $(RM) "$(BIN_PREFIX)/$(CROWDSEC_BIN)" - $(RM) "$(SYSTEMD_PATH_FILE)" + $(RM) $(CFG_PREFIX) $(WIN_IGNORE_ERR) + $(RM) $(DATA_PREFIX) $(WIN_IGNORE_ERR) + $(RM) "$(BIN_PREFIX)/$(CROWDSEC_BIN)" $(WIN_IGNORE_ERR) + $(RM) "$(SYSTEMD_PATH_FILE)" $(WIN_IGNORE_ERR) diff --git a/cmd/crowdsec/api.go b/cmd/crowdsec/api.go index 41cb5e2b8..2dbce31b0 100644 --- a/cmd/crowdsec/api.go +++ b/cmd/crowdsec/api.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "runtime" "github.com/crowdsecurity/crowdsec/pkg/apiserver" "github.com/crowdsecurity/crowdsec/pkg/csconfig" @@ -18,7 +19,8 @@ func initAPIServer(cConfig *csconfig.Config) (*apiserver.APIServer, error) { if hasPlugins(cConfig.API.Server.Profiles) { log.Info("initiating plugin broker") - if cConfig.PluginConfig == nil { + //On windows, the plugins are always run as medium-integrity processes, so we don't care about plugin_config + if cConfig.PluginConfig == nil && runtime.GOOS != "windows" { return nil, fmt.Errorf("plugins are enabled, but the plugin_config section is missing in the configuration") } if cConfig.ConfigPaths.NotificationDir == "" { diff --git a/cmd/crowdsec/configfile.go b/cmd/crowdsec/configfile.go new file mode 100644 index 000000000..76bd159f1 --- /dev/null +++ b/cmd/crowdsec/configfile.go @@ -0,0 +1,5 @@ +// +build linux freebsd netbsd openbsd solaris !windows + +package main + +const DefaultConfigFile = "/etc/crowdsec/config.yaml" diff --git a/cmd/crowdsec/configfile_windows.go b/cmd/crowdsec/configfile_windows.go new file mode 100644 index 000000000..91d316e29 --- /dev/null +++ b/cmd/crowdsec/configfile_windows.go @@ -0,0 +1,3 @@ +package main + +const DefaultConfigFile = "C:\\ProgramData\\CrowdSec\\config\\config.yaml" diff --git a/cmd/crowdsec/main.go b/cmd/crowdsec/main.go index bd0ca51b2..e2c31efb8 100644 --- a/cmd/crowdsec/main.go +++ b/cmd/crowdsec/main.go @@ -10,7 +10,6 @@ import ( _ "net/http/pprof" "time" - "github.com/confluentinc/bincover" "github.com/crowdsecurity/crowdsec/pkg/acquisition" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csplugin" @@ -23,7 +22,6 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/writer" "gopkg.in/tomb.v2" ) @@ -65,6 +63,7 @@ type Flags struct { TestMode bool DisableAgent bool DisableAPI bool + WinSvc string } type labelsMap map[string]string @@ -190,8 +189,8 @@ func (f *Flags) Parse() { flag.BoolVar(&f.TestMode, "t", false, "only test configs") flag.BoolVar(&f.DisableAgent, "no-cs", false, "disable crowdsec agent") flag.BoolVar(&f.DisableAPI, "no-api", false, "disable local API") + flag.StringVar(&f.WinSvc, "winsvc", "", "Windows service Action : Install, Remove etc..") flag.StringVar(&dumpFolder, "dump-data", "", "dump parsers/buckets raw outputs") - flag.Parse() } @@ -262,10 +261,6 @@ func LoadConfig(cConfig *csconfig.Config) error { } func main() { - var ( - cConfig *csconfig.Config - err error - ) defer types.CatchPanic("crowdsec/main") @@ -278,41 +273,5 @@ func main() { cwversion.Show() os.Exit(0) } - log.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr - Writer: os.Stderr, - LogLevels: []log.Level{ - log.PanicLevel, - log.FatalLevel, - }, - }) - - cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI) - if err != nil { - log.Fatalf(err.Error()) - } - if err := LoadConfig(cConfig); err != nil { - log.Fatalf(err.Error()) - } - // Configure logging - if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel, - cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil { - log.Fatal(err.Error()) - } - - log.Infof("Crowdsec %s", cwversion.VersionStr()) - - // Enable profiling early - if cConfig.Prometheus != nil { - go registerPrometheus(cConfig.Prometheus) - } - - if exitCode, err := Serve(cConfig); err != nil { - if err != nil { - log.Errorf(err.Error()) - if !bincoverTesting { - os.Exit(exitCode) - } - bincover.ExitCode = exitCode - } - } + StartRunSvc() } diff --git a/cmd/crowdsec/run_in_svc.go b/cmd/crowdsec/run_in_svc.go new file mode 100644 index 000000000..d1197aeda --- /dev/null +++ b/cmd/crowdsec/run_in_svc.go @@ -0,0 +1,61 @@ +//go:build linux || freebsd || netbsd || openbsd || solaris || !windows +// +build linux freebsd netbsd openbsd solaris !windows + +package main + +import ( + "os" + + "github.com/confluentinc/bincover" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/cwversion" + "github.com/crowdsecurity/crowdsec/pkg/types" + log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/writer" +) + +func StartRunSvc() { + + var ( + cConfig *csconfig.Config + err error + ) + + log.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr + Writer: os.Stderr, + LogLevels: []log.Level{ + log.PanicLevel, + log.FatalLevel, + }, + }) + + cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI) + if err != nil { + log.Fatalf(err.Error()) + } + if err := LoadConfig(cConfig); err != nil { + log.Fatalf(err.Error()) + } + // Configure logging + if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel, + cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil { + log.Fatal(err.Error()) + } + + log.Infof("Crowdsec %s", cwversion.VersionStr()) + + // Enable profiling early + if cConfig.Prometheus != nil { + go registerPrometheus(cConfig.Prometheus) + } + + if exitCode, err := Serve(cConfig); err != nil { + if err != nil { + log.Errorf(err.Error()) + if !bincoverTesting { + os.Exit(exitCode) + } + bincover.ExitCode = exitCode + } + } +} diff --git a/cmd/crowdsec/run_in_svc_windows.go b/cmd/crowdsec/run_in_svc_windows.go new file mode 100644 index 000000000..af29b34fe --- /dev/null +++ b/cmd/crowdsec/run_in_svc_windows.go @@ -0,0 +1,101 @@ +package main + +import ( + "os" + + "github.com/confluentinc/bincover" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/cwversion" + "github.com/crowdsecurity/crowdsec/pkg/types" + log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/writer" + "golang.org/x/sys/windows/svc" +) + +func StartRunSvc() { + + const svcName = "CrowdSec" + const svcDescription = "Crowdsec IPS/IDS" + + isRunninginService, err := svc.IsWindowsService() + if err != nil { + log.Fatalf("failed to determine if we are running in windows service mode: %v", err) + } + if isRunninginService { + runService(svcName) + return + } + + if flags.WinSvc == "Install" { + err = installService(svcName, svcDescription) + if err != nil { + log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err) + } + } else if flags.WinSvc == "Remove" { + err = removeService(svcName) + if err != nil { + log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err) + } + } else if flags.WinSvc == "Start" { + err = startService(svcName) + if err != nil { + log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err) + } + } else if flags.WinSvc == "Stop" { + err = controlService(svcName, svc.Stop, svc.Stopped) + if err != nil { + log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err) + } + } else if flags.WinSvc == "" { + WindowsRun() + } else { + log.Fatalf("Invalid value for winsvc parameter: %s", flags.WinSvc) + } + +} + +func WindowsRun() { + + var ( + cConfig *csconfig.Config + err error + ) + + log.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr + Writer: os.Stderr, + LogLevels: []log.Level{ + log.PanicLevel, + log.FatalLevel, + }, + }) + + cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI) + if err != nil { + log.Fatalf(err.Error()) + } + if err := LoadConfig(cConfig); err != nil { + log.Fatalf(err.Error()) + } + // Configure logging + if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel, + cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil { + log.Fatal(err.Error()) + } + + log.Infof("Crowdsec %s", cwversion.VersionStr()) + + // Enable profiling early + if cConfig.Prometheus != nil { + go registerPrometheus(cConfig.Prometheus) + } + + if exitCode, err := Serve(cConfig); err != nil { + if err != nil { + log.Errorf(err.Error()) + if !bincoverTesting { + os.Exit(exitCode) + } + bincover.ExitCode = exitCode + } + } +} diff --git a/cmd/crowdsec/serve.go b/cmd/crowdsec/serve.go index 42676ad39..9150b5784 100644 --- a/cmd/crowdsec/serve.go +++ b/cmd/crowdsec/serve.go @@ -179,10 +179,11 @@ func shutdown(sig os.Signal, cConfig *csconfig.Config) error { func HandleSignals(cConfig *csconfig.Config) int { signalChan := make(chan os.Signal, 1) + //We add os.Interrupt mostly to ease windows dev, it allows to simulate a clean shutdown when running in the console signal.Notify(signalChan, syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM) + syscall.SIGTERM, + os.Interrupt) exitChan := make(chan int) go func() { @@ -200,7 +201,7 @@ func HandleSignals(cConfig *csconfig.Config) int { log.Fatalf("Reload handler failure : %s", err) } // ctrl+C, kill -SIGINT XXXX, kill -SIGTERM XXXX - case syscall.SIGINT, syscall.SIGTERM: + case os.Interrupt, syscall.SIGTERM: log.Warningf("SIGTERM received, shutting down") if err := shutdown(s, cConfig); err != nil { log.Fatalf("failed shutdown : %s", err) diff --git a/cmd/crowdsec/win_service.go b/cmd/crowdsec/win_service.go new file mode 100644 index 000000000..bfd7f4048 --- /dev/null +++ b/cmd/crowdsec/win_service.go @@ -0,0 +1,87 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package main + +import ( + "time" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/types" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows/svc" +) + +type crowdsec_winservice struct { + config *csconfig.Config +} + +func (m *crowdsec_winservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + changes <- svc.Status{State: svc.StartPending} + fasttick := time.Tick(500 * time.Millisecond) + slowtick := time.Tick(2 * time.Second) + tick := fasttick + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + go WindowsRun() + +loop: + for { + select { + case <-tick: + + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + changes <- svc.Status{State: svc.StopPending} + err := shutdown(nil, m.config) + if err != nil { + log.Errorf("Error while shutting down: %s", err) + //don't return, we still want to notify windows that we are stopped ? + } + break loop + case svc.Pause: + changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} + tick = slowtick + case svc.Continue: + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + tick = fasttick + default: + log.Errorf("unexpected control request #%d", c) + } + } + } + changes <- svc.Status{State: svc.Stopped} + return +} + +func runService(name string) { + cConfig, err := csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI) + if err != nil { + log.Fatalf(err.Error()) + } + if err := LoadConfig(cConfig); err != nil { + log.Fatalf(err.Error()) + } + // Configure logging + if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel, + cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil { + log.Fatal(err.Error()) + } + log.Infof("starting %s service", name) + + winsvc := crowdsec_winservice{config: cConfig} + + err = svc.Run(name, &winsvc) + if err != nil { + log.Errorf("%s service failed: %s", name, err) + return + } + log.Infof("%s service stopped", name) +} diff --git a/cmd/crowdsec/win_service_install.go b/cmd/crowdsec/win_service_install.go new file mode 100644 index 000000000..b497bc931 --- /dev/null +++ b/cmd/crowdsec/win_service_install.go @@ -0,0 +1,95 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package main + +import ( + "fmt" + "os" + "path/filepath" + + "golang.org/x/sys/windows/svc/mgr" +) + +func exePath() (string, error) { + var err error + prog := os.Args[0] + p, err := filepath.Abs(prog) + if err != nil { + return "", err + } + fi, err := os.Stat(p) + if err == nil { + if !fi.Mode().IsDir() { + return p, nil + } + err = fmt.Errorf("%s is directory", p) + } + if filepath.Ext(p) == "" { + var fi os.FileInfo + + p += ".exe" + fi, err = os.Stat(p) + if err == nil { + if !fi.Mode().IsDir() { + return p, nil + } + err = fmt.Errorf("%s is directory", p) + } + } + return "", err +} + +func installService(name, desc string) error { + exepath, err := exePath() + if err != nil { + return err + } + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err == nil { + s.Close() + return fmt.Errorf("service %s already exists", name) + } + s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: desc}, "is", "auto-started") + if err != nil { + return err + } + defer s.Close() + /*err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info) + if err != nil { + s.Delete() + return fmt.Errorf("SetupEventLogSource() failed: %s", err) + }*/ + return nil +} + +func removeService(name string) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("service %s is not installed", name) + } + defer s.Close() + err = s.Delete() + if err != nil { + return err + } + /*err = eventlog.Remove(name) + if err != nil { + return fmt.Errorf("RemoveEventLogSource() failed: %s", err) + }*/ + return nil +} diff --git a/cmd/crowdsec/win_service_manage.go b/cmd/crowdsec/win_service_manage.go new file mode 100644 index 000000000..4aa115273 --- /dev/null +++ b/cmd/crowdsec/win_service_manage.go @@ -0,0 +1,64 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package main + +import ( + "fmt" + "time" + + //"time" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +func startService(name string) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("could not access service: %v", err) + } + defer s.Close() + err = s.Start("is", "manual-started") + if err != nil { + return fmt.Errorf("could not start service: %v", err) + } + return nil +} + +func controlService(name string, c svc.Cmd, to svc.State) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("could not access service: %v", err) + } + defer s.Close() + status, err := s.Control(c) + if err != nil { + return fmt.Errorf("could not send control=%d: %v", c, err) + } + timeout := time.Now().Add(10 * time.Second) + for status.State != to { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for service to go to state=%d", to) + } + time.Sleep(300 * time.Millisecond) + status, err = s.Query() + if err != nil { + return fmt.Errorf("could not retrieve service status: %v", err) + } + } + return nil +} diff --git a/config/acquis_win.yaml b/config/acquis_win.yaml new file mode 100644 index 000000000..a22dc260e --- /dev/null +++ b/config/acquis_win.yaml @@ -0,0 +1,8 @@ +source: wineventlog +event_channel: Security +event_ids: + - 4625 + - 4623 +event_level: information +labels: + type: eventlog \ No newline at end of file diff --git a/config/config_win.yaml b/config/config_win.yaml new file mode 100644 index 000000000..10721e62e --- /dev/null +++ b/config/config_win.yaml @@ -0,0 +1,49 @@ +common: + daemonize: true + log_media: file + log_level: info + log_dir: C:\ProgramData\CrowdSec\log\ + working_dir: . +config_paths: + config_dir: C:\ProgramData\CrowdSec\config\ + data_dir: C:\ProgramData\CrowdSec\data\ + simulation_path: C:\ProgramData\CrowdSec\config\simulation.yaml + hub_dir: C:\ProgramData\CrowdSec\hub\ + index_path: C:\ProgramData\CrowdSec\hub\.index.json + plugin_dir: C:\ProgramData\CrowdSec\plugins\ + notification_dir: C:\ProgramData\CrowdSec\config\notifications\ +crowdsec_service: + acquisition_path: C:\ProgramData\CrowdSec\config\acquis.yaml + parser_routines: 1 +cscli: + output: human +db_config: + log_level: info + type: sqlite + db_path: C:\ProgramData\CrowdSec\data\crowdsec.db + #user: + #password: + #db_name: + #host: + #port: + flush: + max_items: 5000 + max_age: 7d +api: + client: + insecure_skip_verify: false + credentials_path: C:\ProgramData\CrowdSec\config\local_api_credentials.yaml + server: + log_level: info + listen_uri: 127.0.0.1:8080 + profiles_path: C:\ProgramData\Crowdsec\config\profiles.yaml + online_client: # Crowdsec API credentials (to push signals and receive bad IPs) + credentials_path: C:\ProgramData\CrowdSec\config\online_api_credentials.yaml +# tls: +# cert_file: /etc/crowdsec/ssl/cert.pem +# key_file: /etc/crowdsec/ssl/key.pem +prometheus: + enabled: true + level: full + listen_addr: 127.0.0.1 + listen_port: 6060 diff --git a/config/config_win_no_lapi.yaml b/config/config_win_no_lapi.yaml new file mode 100644 index 000000000..35c7f2c6f --- /dev/null +++ b/config/config_win_no_lapi.yaml @@ -0,0 +1,28 @@ +common: + daemonize: true + log_media: file + log_level: info + log_dir: C:\ProgramData\CrowdSec\log\ + working_dir: . +config_paths: + config_dir: C:\ProgramData\CrowdSec\config\ + data_dir: C:\ProgramData\CrowdSec\data\ + simulation_path: C:\ProgramData\CrowdSec\config\simulation.yaml + hub_dir: C:\ProgramData\CrowdSec\hub\ + index_path: C:\ProgramData\CrowdSec\hub\.index.json + plugin_dir: C:\ProgramData\CrowdSec\plugins\ + notification_dir: C:\ProgramData\CrowdSec\config\notifications\ +crowdsec_service: + acquisition_path: C:\ProgramData\CrowdSec\config\acquis.yaml + parser_routines: 1 +cscli: + output: human +api: + client: + insecure_skip_verify: false + credentials_path: C:\ProgramData\CrowdSec\config\local_api_credentials.yaml +prometheus: + enabled: true + level: full + listen_addr: 127.0.0.1 + listen_port: 6060 diff --git a/go.mod b/go.mod index 0fd97eff0..c20a13682 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( entgo.io/ent v0.10.1 github.com/AlecAivazis/survey/v2 v2.2.7 github.com/Masterminds/sprig v2.22.0+incompatible + github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 github.com/alexliesenfeld/health v0.5.1 github.com/antonmedv/expr v1.8.9 @@ -68,10 +69,15 @@ require ( ) require ( - ariga.io/atlas v0.3.7 // indirect + github.com/beevik/etree v1.1.0 + github.com/google/winops v0.0.0-20211216095627-f0e86eb1453b + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad +) + +require ( + ariga.io/atlas v0.3.7-0.20220303204946-787354f533c3 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/Microsoft/go-winio v0.5.2 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/agext/levenshtein v1.2.3 // indirect @@ -100,6 +106,7 @@ require ( github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/gorilla/mux v1.7.3 // indirect @@ -153,7 +160,6 @@ require ( go.mongodb.org/mongo-driver v1.9.0 // indirect golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 68e0fb2a8..578e817f8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ -ariga.io/atlas v0.3.7 h1:lAISv2cc69QWzrvhlAc22Em1cbBMhJ0Xlcd8p3p3tHM= -ariga.io/atlas v0.3.7/go.mod h1:yWGf4VPiD4SW83+kAqzD624txN9VKoJC+bpVXr2pKJA= +ariga.io/atlas v0.3.7-0.20220303204946-787354f533c3 h1:fjG4oFCQEfGrRi0QoxWcH2OO28CE6VYa6DkIr3yDySU= +ariga.io/atlas v0.3.7-0.20220303204946-787354f533c3/go.mod h1:yWGf4VPiD4SW83+kAqzD624txN9VKoJC+bpVXr2pKJA= +bitbucket.org/creachadair/stringset v0.0.9 h1:L4vld9nzPt90UZNrXjNelTshD74ps4P5NGs3Iq6yN3o= +bitbucket.org/creachadair/stringset v0.0.9/go.mod h1:t+4WcQ4+PXTa8aQdNKe40ZP6iwesoMFWAxPGd3UGjyY= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -133,6 +135,7 @@ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pq github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creachadair/staticfile v0.1.3/go.mod h1:a3qySzCIXEprDGxk6tSxSI+dBBdLzqeBOMhZ+o2d3pM= 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.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= @@ -318,6 +321,7 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -326,6 +330,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU= +github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -367,11 +373,13 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -387,12 +395,15 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/winops v0.0.0-20211216095627-f0e86eb1453b h1:THwEE9J2wPxF3BZm7WjLCASMcM7ctFzqLpTsCGh7gDY= +github.com/google/winops v0.0.0-20211216095627-f0e86eb1453b/go.mod h1:ShbX8v8clPm/3chw9zHVwtW3QhrFpL8mXOwNxClt4pg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/groob/plist v0.0.0-20210519001750-9f754062e6d6/go.mod h1:itkABA+w2cw7x5nYUS/pLRef6ludkZKOigbROmCTaFw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo= @@ -746,7 +757,6 @@ go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= -go.mongodb.org/mongo-driver v1.4.4 h1:bsPHfODES+/yx2PCWzUYMH8xj6PVniPI8DQrsJuSXSs= go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.mongodb.org/mongo-driver v1.9.0 h1:f3aLGJvQmBl8d9S40IL+jEyBC6hfLPbJjv9t5hEM9ck= go.mongodb.org/mongo-driver v1.9.0/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= @@ -935,7 +945,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/make_chocolatey.ps1 b/make_chocolatey.ps1 new file mode 100644 index 000000000..b09029926 --- /dev/null +++ b/make_chocolatey.ps1 @@ -0,0 +1,18 @@ +param ( + $version +) +if ($version.StartsWith("v")) +{ + $version = $version.Substring(1) +} + +#Pre-releases will be like 1.4.0-rc1, remove everything after the dash as it does not conform to the MSI versioning scheme +if ($version.Contains("-")) +{ + $version = $version.Substring(0, $version.IndexOf("-")) +} + +Set-Location .\windows\Chocolatey\crowdsec +Copy-Item ..\..\..\crowdsec_$version.msi tools\crowdsec.msi + +choco pack \ No newline at end of file diff --git a/make_installer.ps1 b/make_installer.ps1 new file mode 100644 index 000000000..fe41e23b0 --- /dev/null +++ b/make_installer.ps1 @@ -0,0 +1,20 @@ +param ( + $version +) +$env:Path += ";C:\Program Files (x86)\WiX Toolset v3.11\bin" +if ($version.StartsWith("v")) +{ + $version = $version.Substring(1) +} + +#Pre-releases will be like 1.4.0-rc1, remove everything after the dash as it does not conform to the MSI versioning scheme +if ($version.Contains("-")) +{ + $version = $version.Substring(0, $version.IndexOf("-")) +} + +Remove-Item -Force -Recurse -Path .\msi -ErrorAction SilentlyContinue +#we only harvest the patterns dir, as we want to handle differently some yaml files in the config directory, and I really don't want to write xlst filters to exclude the files :( +heat.exe dir config\patterns -nologo -cg CrowdsecPatterns -dr PatternsDir -g1 -gg -sf -srd -scom -sreg -out "msi\fragment.wxs" +candle.exe -arch x64 -dSourceDir=config\patterns -dVersion="$version" -out msi\ msi\fragment.wxs windows\installer\WixUI_HK.wxs windows\installer\product.wxs +light.exe -b .\config\patterns -ext WixUIExtension -ext WixUtilExtension -sacl -spdb -out crowdsec_$version.msi msi\fragment.wixobj msi\WixUI_HK.wixobj msi\product.wixobj \ No newline at end of file diff --git a/pkg/acquisition/acquisition.go b/pkg/acquisition/acquisition.go index a04af21f8..93c956538 100644 --- a/pkg/acquisition/acquisition.go +++ b/pkg/acquisition/acquisition.go @@ -13,6 +13,7 @@ import ( journalctlacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/journalctl" kinesisacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/kinesis" syslogacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/syslog" + wineventlogacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/wineventlog" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/types" "github.com/pkg/errors" @@ -65,6 +66,10 @@ var AcquisitionSources = []struct { name: "kinesis", iface: func() DataSource { return &kinesisacquisition.KinesisSource{} }, }, + { + name: "wineventlog", + iface: func() DataSource { return &wineventlogacquisition.WinEventLogSource{} }, + }, } func GetDataSourceIface(dataSourceType string) DataSource { @@ -171,6 +176,7 @@ func LoadAcquisitionFromFile(config *csconfig.CrowdsecServiceCfg) ([]DataSource, dec.SetStrict(true) for { var sub configuration.DataSourceCommonCfg + var idx int err = dec.Decode(&sub) if err != nil { if err == io.EOF { @@ -188,21 +194,23 @@ func LoadAcquisitionFromFile(config *csconfig.CrowdsecServiceCfg) ([]DataSource, if len(sub.Labels) == 0 { if sub.Source == "" { log.Debugf("skipping empty item in %s", acquisFile) + idx += 1 continue } - return nil, fmt.Errorf("missing labels in %s", acquisFile) + return nil, fmt.Errorf("missing labels in %s (position: %d)", acquisFile, idx) } if sub.Source == "" { - return nil, fmt.Errorf("data source type is empty ('source') in %s", acquisFile) + return nil, fmt.Errorf("data source type is empty ('source') in %s (position: %d)", acquisFile, idx) } if GetDataSourceIface(sub.Source) == nil { - return nil, fmt.Errorf("unknown data source %s in %s", sub.Source, acquisFile) + return nil, fmt.Errorf("unknown data source %s in %s (position: %d)", sub.Source, acquisFile, idx) } src, err := DataSourceConfigure(sub) if err != nil { - return nil, errors.Wrapf(err, "while configuring datasource of type %s from %s", sub.Source, acquisFile) + return nil, errors.Wrapf(err, "while configuring datasource of type %s from %s (position: %d)", sub.Source, acquisFile, idx) } sources = append(sources, *src) + idx += 1 } } return sources, nil diff --git a/pkg/acquisition/configuration/configuration.go b/pkg/acquisition/configuration/configuration.go index 85a0a33c1..41d31ef25 100644 --- a/pkg/acquisition/configuration/configuration.go +++ b/pkg/acquisition/configuration/configuration.go @@ -5,12 +5,13 @@ import ( ) type DataSourceCommonCfg struct { - Mode string `yaml:"mode,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - LogLevel *log.Level `yaml:"log_level,omitempty"` - Source string `yaml:"source,omitempty"` - Name string `yaml:"name,omitempty"` - Config map[string]interface{} `yaml:",inline"` //to keep the datasource-specific configuration directives + Mode string `yaml:"mode,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + LogLevel *log.Level `yaml:"log_level,omitempty"` + Source string `yaml:"source,omitempty"` + Name string `yaml:"name,omitempty"` + UseTimeMachine bool `yaml:"use_time_machine,omitempty"` + Config map[string]interface{} `yaml:",inline"` //to keep the datasource-specific configuration directives } var TAIL_MODE = "tail" diff --git a/pkg/acquisition/modules/cloudwatch/cloudwatch.go b/pkg/acquisition/modules/cloudwatch/cloudwatch.go index 3bd9f6123..5997eccee 100644 --- a/pkg/acquisition/modules/cloudwatch/cloudwatch.go +++ b/pkg/acquisition/modules/cloudwatch/cloudwatch.go @@ -293,6 +293,12 @@ func (cw *CloudwatchSource) WatchLogGroupForStreams(out chan LogStreamTailConfig } cw.logger.Tracef("stream %s is elligible for monitoring", *event.LogStreamName) //the stream has been update recently, check if we should monitor it + var expectMode int + if !cw.Config.UseTimeMachine { + expectMode = leaky.LIVE + } else { + expectMode = leaky.TIMEMACHINE + } monitorStream := LogStreamTailConfig{ GroupName: cw.Config.GroupName, StreamName: *event.LogStreamName, @@ -300,7 +306,7 @@ func (cw *CloudwatchSource) WatchLogGroupForStreams(out chan LogStreamTailConfig PollStreamInterval: *cw.Config.PollStreamInterval, StreamReadTimeout: *cw.Config.StreamReadTimeout, PrependCloudwatchTimestamp: cw.Config.PrependCloudwatchTimestamp, - ExpectMode: leaky.LIVE, + ExpectMode: expectMode, Labels: cw.Config.Labels, } out <- monitorStream diff --git a/pkg/acquisition/modules/cloudwatch/cloudwatch_test.go b/pkg/acquisition/modules/cloudwatch/cloudwatch_test.go index 4a7c640e5..cb8cc8cea 100644 --- a/pkg/acquisition/modules/cloudwatch/cloudwatch_test.go +++ b/pkg/acquisition/modules/cloudwatch/cloudwatch_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "os" + "runtime" "strings" "testing" "time" @@ -37,6 +38,9 @@ func checkForLocalStackAvailability() error { } func TestMain(m *testing.M) { + if runtime.GOOS == "windows" { + os.Exit(0) + } if err := checkForLocalStackAvailability(); err != nil { log.Fatalf("local stack error : %s", err) } @@ -49,6 +53,9 @@ func TestMain(m *testing.M) { } func TestWatchLogGroupForStreams(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } var err error log.SetLevel(log.DebugLevel) tests := []struct { @@ -519,6 +526,9 @@ stream_name: test_stream`), } func TestConfiguration(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } var err error log.SetLevel(log.DebugLevel) tests := []struct { @@ -604,6 +614,9 @@ stream_name: test_stream`), } func TestConfigureByDSN(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } var err error log.SetLevel(log.DebugLevel) tests := []struct { @@ -657,6 +670,9 @@ func TestConfigureByDSN(t *testing.T) { } func TestOneShotAcquisition(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } var err error log.SetLevel(log.DebugLevel) tests := []struct { diff --git a/pkg/acquisition/modules/docker/docker.go b/pkg/acquisition/modules/docker/docker.go index 4ae9ed951..9619e8ac0 100644 --- a/pkg/acquisition/modules/docker/docker.go +++ b/pkg/acquisition/modules/docker/docker.go @@ -306,7 +306,7 @@ func (d *DockerSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) er l.Process = true l.Module = d.GetName() linesRead.With(prometheus.Labels{"source": containerConfig.Name}).Inc() - evt := types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + evt := types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE} out <- evt d.logger.Debugf("Sent line to parsing: %+v", evt.Line.Raw) } @@ -503,7 +503,12 @@ func (d *DockerSource) TailDocker(container *ContainerConfig, outChan chan types l.Src = container.Name l.Process = true l.Module = d.GetName() - evt := types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + var evt types.Event + if !d.Config.UseTimeMachine { + evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + } else { + evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE} + } linesRead.With(prometheus.Labels{"source": container.Name}).Inc() outChan <- evt d.logger.Debugf("Sent line to parsing: %+v", evt.Line.Raw) diff --git a/pkg/acquisition/modules/docker/docker_test.go b/pkg/acquisition/modules/docker/docker_test.go index 75ee234d0..2be558620 100644 --- a/pkg/acquisition/modules/docker/docker_test.go +++ b/pkg/acquisition/modules/docker/docker_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "runtime" "strings" "testing" "time" @@ -64,7 +65,12 @@ container_name: func TestConfigureDSN(t *testing.T) { log.Infof("Test 'TestConfigureDSN'") - + var dockerHost string + if runtime.GOOS == "windows" { + dockerHost = "npipe:////./pipe/docker_engine" + } else { + dockerHost = "unix:///var/run/podman/podman.sock" + } tests := []struct { name string dsn string @@ -92,7 +98,7 @@ func TestConfigureDSN(t *testing.T) { }, { name: "DSN ok with multiple parameters", - dsn: "docker://test_docker?since=42min&docker_host=unix:///var/run/podman/podman.sock", + dsn: fmt.Sprintf("docker://test_docker?since=42min&docker_host=%s", dockerHost), expectedErr: "", }, } diff --git a/pkg/acquisition/modules/file/file.go b/pkg/acquisition/modules/file/file.go index de6d18bbf..6128d3a16 100644 --- a/pkg/acquisition/modules/file/file.go +++ b/pkg/acquisition/modules/file/file.go @@ -77,7 +77,7 @@ func (f *FileSource) Configure(Config []byte, logger *log.Entry) error { f.logger.Tracef("Actual FileAcquisition Configuration %+v", f.config) for _, pattern := range f.config.Filenames { if f.config.ForceInotify { - directory := path.Dir(pattern) + directory := filepath.Dir(pattern) f.logger.Infof("Force add watch on %s", directory) if !f.watchedDirectories[directory] { err = f.watcher.Add(directory) @@ -98,7 +98,8 @@ func (f *FileSource) Configure(Config []byte, logger *log.Entry) error { } for _, file := range files { if files[0] != pattern && f.config.Mode == configuration.TAIL_MODE { //we have a glob pattern - directory := path.Dir(file) + directory := filepath.Dir(file) + f.logger.Debugf("Will add watch to directory: %s", directory) if !f.watchedDirectories[directory] { err = f.watcher.Add(directory) @@ -107,6 +108,8 @@ func (f *FileSource) Configure(Config []byte, logger *log.Entry) error { continue } f.watchedDirectories[directory] = true + } else { + f.logger.Debugf("Watch for directory %s already exists", directory) } } f.logger.Infof("Adding file %s to datasources", file) @@ -374,7 +377,7 @@ func (f *FileSource) tailFile(out chan types.Event, t *tomb.Tomb, tail *tail.Tai continue } linesRead.With(prometheus.Labels{"source": tail.Filename}).Inc() - l.Raw = line.Text + l.Raw = trimLine(line.Text) l.Labels = f.config.Labels l.Time = line.Time l.Src = tail.Filename @@ -382,7 +385,11 @@ func (f *FileSource) tailFile(out chan types.Event, t *tomb.Tomb, tail *tail.Tai l.Module = f.GetName() //we're tailing, it must be real time logs logger.Debugf("pushing %+v", l) - out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + if !f.config.UseTimeMachine { + out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + } else { + out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE} + } } } } diff --git a/pkg/acquisition/modules/file/file_test.go b/pkg/acquisition/modules/file/file_test.go index c11871bbc..45ebdb031 100644 --- a/pkg/acquisition/modules/file/file_test.go +++ b/pkg/acquisition/modules/file/file_test.go @@ -3,6 +3,7 @@ package fileacquisition import ( "fmt" "os" + "runtime" "testing" "time" @@ -44,6 +45,12 @@ func TestBadConfiguration(t *testing.T) { } func TestConfigureDSN(t *testing.T) { + var file string + if runtime.GOOS != "windows" { + file = "/etc/passwd" + } else { + file = "C:\\Windows\\System32\\drivers\\etc\\hosts" + } tests := []struct { dsn string expectedErr string @@ -57,11 +64,11 @@ func TestConfigureDSN(t *testing.T) { expectedErr: "empty file:// DSN", }, { - dsn: "file:///etc/passwd?log_level=warn", + dsn: fmt.Sprintf("file://%s?log_level=warn", file), expectedErr: "", }, { - dsn: "file:///etc/passwd?log_level=foobar", + dsn: fmt.Sprintf("file://%s?log_level=foobar", file), expectedErr: "unknown level foobar: not a valid logrus Level:", }, } @@ -76,6 +83,17 @@ func TestConfigureDSN(t *testing.T) { } func TestOneShot(t *testing.T) { + var permDeniedFile string + var permDeniedError string + if runtime.GOOS != "windows" { + permDeniedFile = "/etc/shadow" + permDeniedError = "failed opening /etc/shadow: open /etc/shadow: permission denied" + } else { + //Technically, this is not a permission denied error, but we just want to test what happens + //if we do not have access to the file + permDeniedFile = "C:\\Windows\\System32\\config\\SAM" + permDeniedError = "failed opening C:\\Windows\\System32\\config\\SAM: open C:\\Windows\\System32\\config\\SAM: The process cannot access the file because it is being used by another process." + } tests := []struct { config string expectedConfigErr string @@ -88,11 +106,11 @@ func TestOneShot(t *testing.T) { teardown func() }{ { - config: ` + config: fmt.Sprintf(` mode: cat -filename: /etc/shadow`, +filename: %s`, permDeniedFile), expectedConfigErr: "", - expectedErr: "failed opening /etc/shadow: open /etc/shadow: permission denied", + expectedErr: permDeniedError, expectedOutput: "", logLevel: log.WarnLevel, expectedLines: 0, @@ -162,12 +180,13 @@ filename: test_files/bad.gz`, mode: cat filename: test_files/test_delete.log`, setup: func() { - os.Create("test_files/test_delete.log") + f, _ := os.Create("test_files/test_delete.log") + f.Close() }, afterConfigure: func() { os.Remove("test_files/test_delete.log") }, - expectedErr: "could not stat file test_files/test_delete.log : stat test_files/test_delete.log: no such file or directory", + expectedErr: "could not stat file test_files/test_delete.log", }, } @@ -223,10 +242,25 @@ filename: test_files/test_delete.log`, } func TestLiveAcquisition(t *testing.T) { + var permDeniedFile string + var permDeniedError string + var testPattern string + if runtime.GOOS != "windows" { + permDeniedFile = "/etc/shadow" + permDeniedError = "unable to read /etc/shadow : open /etc/shadow: permission denied" + testPattern = "test_files/*.log" + } else { + //Technically, this is not a permission denied error, but we just want to test what happens + //if we do not have access to the file + permDeniedFile = "C:\\Windows\\System32\\config\\SAM" + permDeniedError = "unable to read C:\\Windows\\System32\\config\\SAM : open C:\\Windows\\System32\\config\\SAM: The process cannot access the file because it is being used by another process" + testPattern = "test_files\\\\*.log" // the \ must be escaped twice: once for the string, once for the yaml config + } tests := []struct { config string expectedErr string expectedOutput string + name string expectedLines int logLevel log.Level setup func() @@ -234,13 +268,14 @@ func TestLiveAcquisition(t *testing.T) { teardown func() }{ { - config: ` + config: fmt.Sprintf(` mode: tail -filename: /etc/shadow`, +filename: %s`, permDeniedFile), expectedErr: "", - expectedOutput: "unable to read /etc/shadow : open /etc/shadow: permission denied", + expectedOutput: permDeniedError, logLevel: log.InfoLevel, expectedLines: 0, + name: "PermissionDenied", }, { config: ` @@ -250,6 +285,7 @@ filename: /`, expectedOutput: "/ is a directory, ignoring it", logLevel: log.WarnLevel, expectedLines: 0, + name: "Directory", }, { config: ` @@ -259,45 +295,52 @@ filename: /do/not/exist`, expectedOutput: "No matching files for pattern /do/not/exist", logLevel: log.WarnLevel, expectedLines: 0, + name: "badPattern", }, { - config: ` + config: fmt.Sprintf(` mode: tail filenames: - - test_files/*.log -force_inotify: true`, + - %s +force_inotify: true`, testPattern), expectedErr: "", expectedOutput: "", expectedLines: 5, logLevel: log.DebugLevel, + name: "basicGlob", }, { - config: ` + config: fmt.Sprintf(` mode: tail filenames: - - test_files/*.log -force_inotify: true`, + - %s +force_inotify: true`, testPattern), expectedErr: "", expectedOutput: "", expectedLines: 0, logLevel: log.DebugLevel, + name: "GlobInotify", afterConfigure: func() { - os.Create("test_files/a.log") + f, _ := os.Create("test_files/a.log") + f.Close() + time.Sleep(1 * time.Second) os.Remove("test_files/a.log") }, }, { - config: ` + config: fmt.Sprintf(` mode: tail filenames: - - test_files/*.log -force_inotify: true`, + - %s +force_inotify: true`, testPattern), expectedErr: "", expectedOutput: "", expectedLines: 5, logLevel: log.DebugLevel, + name: "GlobInotifyChmod", afterConfigure: func() { - os.Create("test_files/a.log") + f, _ := os.Create("test_files/a.log") + f.Close() time.Sleep(1 * time.Second) os.Chmod("test_files/a.log", 0000) }, @@ -307,15 +350,16 @@ force_inotify: true`, }, }, { - config: ` + config: fmt.Sprintf(` mode: tail filenames: - - test_files/*.log -force_inotify: true`, + - %s +force_inotify: true`, testPattern), expectedErr: "", expectedOutput: "", expectedLines: 5, logLevel: log.DebugLevel, + name: "InotifyMkDir", afterConfigure: func() { os.Mkdir("test_files/pouet/", 0700) }, @@ -326,6 +370,7 @@ force_inotify: true`, } for _, ts := range tests { + t.Logf("test: %s", ts.name) logger, hook := test.NewNullLogger() logger.SetLevel(ts.logLevel) subLogger := logger.WithFields(log.Fields{ @@ -377,7 +422,7 @@ force_inotify: true`, //we sleep to make sure we detect the new file time.Sleep(1 * time.Second) os.Remove("test_files/stream.log") - assert.Equal(t, actualLines, ts.expectedLines) + assert.Equal(t, ts.expectedLines, actualLines) } if ts.expectedOutput != "" { diff --git a/pkg/acquisition/modules/file/tailline.go b/pkg/acquisition/modules/file/tailline.go new file mode 100644 index 000000000..ac377b663 --- /dev/null +++ b/pkg/acquisition/modules/file/tailline.go @@ -0,0 +1,7 @@ +// +build linux freebsd netbsd openbsd solaris !windows + +package fileacquisition + +func trimLine(text string) string { + return text +} diff --git a/pkg/acquisition/modules/file/tailline_windows.go b/pkg/acquisition/modules/file/tailline_windows.go new file mode 100644 index 000000000..0c853c6e9 --- /dev/null +++ b/pkg/acquisition/modules/file/tailline_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package fileacquisition + +import "strings" + +func trimLine(text string) string { + return strings.TrimRight(text, "\r") +} diff --git a/pkg/acquisition/modules/journalctl/journalctl.go b/pkg/acquisition/modules/journalctl/journalctl.go index 07fbfb4ae..2e7bfb4da 100644 --- a/pkg/acquisition/modules/journalctl/journalctl.go +++ b/pkg/acquisition/modules/journalctl/journalctl.go @@ -131,7 +131,12 @@ func (j *JournalCtlSource) runJournalCtl(out chan types.Event, t *tomb.Tomb) err l.Process = true l.Module = j.GetName() linesRead.With(prometheus.Labels{"source": j.src}).Inc() - evt := types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + var evt types.Event + if !j.config.UseTimeMachine { + evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + } else { + evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE} + } out <- evt case stderrLine := <-stderrChan: logger.Warnf("Got stderr message : %s", stderrLine) diff --git a/pkg/acquisition/modules/journalctl/journalctl_test.go b/pkg/acquisition/modules/journalctl/journalctl_test.go index 82fc861ea..a73aad9ab 100644 --- a/pkg/acquisition/modules/journalctl/journalctl_test.go +++ b/pkg/acquisition/modules/journalctl/journalctl_test.go @@ -3,6 +3,7 @@ package journalctlacquisition import ( "os" "os/exec" + "runtime" "testing" "time" @@ -15,6 +16,9 @@ import ( ) func TestBadConfiguration(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string expectedErr string @@ -50,6 +54,9 @@ journalctl_filter: } func TestConfigureDSN(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { dsn string expectedErr string @@ -94,6 +101,9 @@ func TestConfigureDSN(t *testing.T) { } func TestOneShot(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string expectedErr string @@ -182,6 +192,9 @@ journalctl_filter: } func TestStreaming(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string expectedErr string diff --git a/pkg/acquisition/modules/kinesis/kinesis.go b/pkg/acquisition/modules/kinesis/kinesis.go index 88dccc357..e7c4c7a3c 100644 --- a/pkg/acquisition/modules/kinesis/kinesis.go +++ b/pkg/acquisition/modules/kinesis/kinesis.go @@ -296,7 +296,12 @@ func (k *KinesisSource) ParseAndPushRecords(records []*kinesis.Record, out chan } else { l.Src = k.Config.StreamName } - evt := types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leakybucket.LIVE} + var evt types.Event + if !k.Config.UseTimeMachine { + evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leakybucket.LIVE} + } else { + evt = types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leakybucket.TIMEMACHINE} + } out <- evt } } diff --git a/pkg/acquisition/modules/kinesis/kinesis_test.go b/pkg/acquisition/modules/kinesis/kinesis_test.go index ae4e9ef8e..46435ac27 100644 --- a/pkg/acquisition/modules/kinesis/kinesis_test.go +++ b/pkg/acquisition/modules/kinesis/kinesis_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "os" + "runtime" "strings" "testing" "time" @@ -103,6 +104,9 @@ func TestMain(m *testing.M) { } func TestBadConfiguration(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string expectedErr string @@ -144,6 +148,9 @@ stream_arn: arn:aws:kinesis:eu-west-1:123456789012:stream/my-stream`, } func TestReadFromStream(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string count int @@ -187,6 +194,9 @@ stream_name: stream-1-shard`, } func TestReadFromMultipleShards(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string count int @@ -232,6 +242,9 @@ stream_name: stream-2-shards`, } func TestFromSubscription(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { config string count int diff --git a/pkg/acquisition/modules/syslog/syslog.go b/pkg/acquisition/modules/syslog/syslog.go index 7c84703e7..65bb76aff 100644 --- a/pkg/acquisition/modules/syslog/syslog.go +++ b/pkg/acquisition/modules/syslog/syslog.go @@ -238,7 +238,11 @@ func (s *SyslogSource) handleSyslogMsg(out chan types.Event, t *tomb.Tomb, c cha l.Time = ts l.Src = syslogLine.Client l.Process = true - out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + if !s.config.UseTimeMachine { + out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + } else { + out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE} + } } } } diff --git a/pkg/acquisition/modules/syslog/syslog_test.go b/pkg/acquisition/modules/syslog/syslog_test.go index dd3e29c83..49ecdce5c 100644 --- a/pkg/acquisition/modules/syslog/syslog_test.go +++ b/pkg/acquisition/modules/syslog/syslog_test.go @@ -3,6 +3,7 @@ package syslogacquisition import ( "fmt" "net" + "runtime" "testing" "time" @@ -77,10 +78,6 @@ func TestStreamingAcquisition(t *testing.T) { logs []string expectedLines int }{ - { - config: `source: syslog`, - expectedErr: "could not start syslog server: could not listen on port 514: listen udp 127.0.0.1:514: bind: permission denied", - }, { config: ` source: syslog @@ -109,6 +106,17 @@ listen_addr: 127.0.0.1`, `<13>May 18 12:37:56 mantis sshd`}, }, } + if runtime.GOOS != "windows" { + tests = append(tests, struct { + config string + expectedErr string + logs []string + expectedLines int + }{ + config: `source: syslog`, + expectedErr: "could not start syslog server: could not listen on port 514: listen udp 127.0.0.1:514: bind: permission denied", + }) + } for _, ts := range tests { subLogger := log.WithFields(log.Fields{ diff --git a/pkg/acquisition/modules/wineventlog/wineventlog.go b/pkg/acquisition/modules/wineventlog/wineventlog.go new file mode 100644 index 000000000..92bbd7be4 --- /dev/null +++ b/pkg/acquisition/modules/wineventlog/wineventlog.go @@ -0,0 +1,59 @@ +//go:build !windows + +package wineventlogacquisition + +import ( + "errors" + + "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "gopkg.in/tomb.v2" +) + +type WinEventLogSource struct{} + +func (w *WinEventLogSource) Configure(yamlConfig []byte, logger *log.Entry) error { + return nil +} + +func (w *WinEventLogSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry) error { + return nil +} + +func (w *WinEventLogSource) GetMode() string { + return "" +} + +func (w *WinEventLogSource) SupportedModes() []string { + return []string{configuration.TAIL_MODE, configuration.CAT_MODE} +} + +func (w *WinEventLogSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error { + return nil +} + +func (w *WinEventLogSource) GetMetrics() []prometheus.Collector { + return nil +} + +func (w *WinEventLogSource) GetAggregMetrics() []prometheus.Collector { + return nil +} + +func (w *WinEventLogSource) GetName() string { + return "wineventlog" +} + +func (w *WinEventLogSource) CanRun() error { + return errors.New("windows event log acquisition is only supported on Windows") +} + +func (w *WinEventLogSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error { + return nil +} + +func (w *WinEventLogSource) Dump() interface{} { + return w +} diff --git a/pkg/acquisition/modules/wineventlog/wineventlog_test.go b/pkg/acquisition/modules/wineventlog/wineventlog_test.go new file mode 100644 index 000000000..a24a63efc --- /dev/null +++ b/pkg/acquisition/modules/wineventlog/wineventlog_test.go @@ -0,0 +1,233 @@ +//go:build windows +// +build windows + +package wineventlogacquisition + +import ( + "runtime" + "testing" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" + "github.com/crowdsecurity/crowdsec/pkg/types" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "golang.org/x/sys/windows/svc/eventlog" + "gopkg.in/tomb.v2" +) + +func TestBadConfiguration(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Skipping test on non-windows OS") + } + tests := []struct { + config string + expectedErr string + }{ + { + config: `source: wineventlog +foobar: 42`, + expectedErr: "field foobar not found in type wineventlogacquisition.WinEventLogConfiguration", + }, + { + config: `source: wineventlog`, + expectedErr: "event_channel or xpath_query must be set", + }, + { + config: `source: wineventlog +event_channel: Security +event_level: blabla`, + expectedErr: "buildXpathQuery failed: invalid log level", + }, + { + config: `source: wineventlog +event_channel: Security +event_level: blabla`, + expectedErr: "buildXpathQuery failed: invalid log level", + }, + { + config: `source: wineventlog +event_channel: foo +xpath_query: test`, + expectedErr: "event_channel and xpath_query are mutually exclusive", + }, + } + + subLogger := log.WithFields(log.Fields{ + "type": "windowseventlog", + }) + for _, test := range tests { + f := WinEventLogSource{} + err := f.Configure([]byte(test.config), subLogger) + assert.Contains(t, err.Error(), test.expectedErr) + } +} + +func TestQueryBuilder(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Skipping test on non-windows OS") + } + tests := []struct { + config string + expectedQuery string + expectedErr string + }{ + { + config: `source: wineventlog +event_channel: Security +event_level: Information`, + expectedQuery: "", + expectedErr: "", + }, + { + config: `source: wineventlog +event_channel: Security +event_level: Error +event_ids: + - 42`, + expectedQuery: "", + expectedErr: "", + }, + { + config: `source: wineventlog +event_channel: Security +event_level: Error +event_ids: + - 42 + - 43`, + expectedQuery: "", + expectedErr: "", + }, + { + config: `source: wineventlog +event_channel: Security`, + expectedQuery: "", + expectedErr: "", + }, + { + config: `source: wineventlog +event_channel: Security +event_level: bla`, + expectedQuery: "", + expectedErr: "invalid log level", + }, + } + subLogger := log.WithFields(log.Fields{ + "type": "windowseventlog", + }) + for _, test := range tests { + f := WinEventLogSource{} + f.Configure([]byte(test.config), subLogger) + q, err := f.buildXpathQuery() + if test.expectedErr != "" { + if err == nil { + t.Fatalf("expected error '%s' but got none", test.expectedErr) + } + assert.Contains(t, err.Error(), test.expectedErr) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expectedQuery, q) + } + } +} + +func TestLiveAcquisition(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Skipping test on non-windows OS") + } + + tests := []struct { + config string + expectedLines []string + }{ + { + config: `source: wineventlog +xpath_query: | + + + + + `, + expectedLines: []string{ + "blabla", + "test", + "aaaa", + "bbbbb", + }, + }, + { + config: `source: wineventlog +xpath_query: | + asdfsdf`, + expectedLines: nil, + }, + { + config: `source: wineventlog +event_channel: Application +event_level: Information +event_ids: + - 42`, + expectedLines: []string{ + "testmessage", + }, + }, + { + config: `source: wineventlog +event_channel: Application +event_level: Information +event_ids: + - 43`, + expectedLines: nil, + }, + } + subLogger := log.WithFields(log.Fields{ + "type": "windowseventlog", + }) + + evthandler, err := eventlog.Open("Application") + + if err != nil { + t.Fatalf("failed to open event log: %s", err) + } + + for _, test := range tests { + to := &tomb.Tomb{} + c := make(chan types.Event) + f := WinEventLogSource{} + f.Configure([]byte(test.config), subLogger) + f.StreamingAcquisition(c, to) + time.Sleep(time.Second) + lines := test.expectedLines + go func() { + for _, line := range lines { + evthandler.Info(42, line) + } + }() + ticker := time.NewTicker(time.Second * 5) + linesRead := make([]string, 0) + READLOOP: + for { + select { + case <-ticker.C: + if test.expectedLines == nil { + break READLOOP + } + t.Fatalf("timeout") + case e := <-c: + + linesRead = append(linesRead, exprhelpers.XMLGetNodeValue(e.Line.Raw, "/Event/EventData[1]/Data")) + if len(linesRead) == len(lines) { + break READLOOP + } + } + } + if test.expectedLines == nil { + assert.Equal(t, 0, len(linesRead)) + } else { + assert.Equal(t, len(test.expectedLines), len(linesRead)) + assert.Equal(t, test.expectedLines, linesRead) + } + to.Kill(nil) + to.Wait() + } +} diff --git a/pkg/acquisition/modules/wineventlog/wineventlog_windows.go b/pkg/acquisition/modules/wineventlog/wineventlog_windows.go new file mode 100644 index 000000000..7e7bb5778 --- /dev/null +++ b/pkg/acquisition/modules/wineventlog/wineventlog_windows.go @@ -0,0 +1,320 @@ +package wineventlogacquisition + +import ( + "encoding/xml" + "errors" + "fmt" + "runtime" + "strings" + "syscall" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" + leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket" + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/google/winops/winlog" + "github.com/google/winops/winlog/wevtapi" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + "gopkg.in/tomb.v2" + "gopkg.in/yaml.v2" +) + +type WinEventLogConfiguration struct { + configuration.DataSourceCommonCfg `yaml:",inline"` + EventChannel string `yaml:"event_channel"` + EventLevel string `yaml:"event_level"` + EventIDs []int `yaml:"event_ids"` + XPathQuery string `yaml:"xpath_query"` + EventFile string `yaml:"event_file"` + PrettyName string `yaml:"pretty_name"` +} + +type WinEventLogSource struct { + config WinEventLogConfiguration + logger *log.Entry + evtConfig *winlog.SubscribeConfig + query string + name string +} + +type QueryList struct { + Select Select `xml:"Query>Select"` +} + +type Select struct { + Path string `xml:"Path,attr"` + Query string `xml:",chardata"` +} + +var linesRead = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "cs_winevtlogsource_hits_total", + Help: "Total event that were read.", + }, + []string{"source"}) + +func logLevelToInt(logLevel string) ([]string, error) { + switch strings.ToUpper(logLevel) { + case "CRITICAL": + return []string{"1"}, nil + case "ERROR": + return []string{"2"}, nil + case "WARNING": + return []string{"3"}, nil + case "INFORMATION": + return []string{"0", "4"}, nil + case "VERBOSE": + return []string{"5"}, nil + default: + return nil, errors.New("invalid log level") + } +} + +//This is lifted from winops/winlog, but we only want to render the basic XML string, we don't need the extra fluff +func (w *WinEventLogSource) getXMLEvents(config *winlog.SubscribeConfig, publisherCache map[string]windows.Handle, resultSet windows.Handle, maxEvents int) ([]string, error) { + var events = make([]windows.Handle, maxEvents) + var returned uint32 + + // Get handles to events from the result set. + err := wevtapi.EvtNext( + resultSet, // Handle to query or subscription result set. + uint32(len(events)), // The number of events to attempt to retrieve. + &events[0], // Pointer to the array of event handles. + 2000, // Timeout in milliseconds to wait. + 0, // Reserved. Must be zero. + &returned) // The number of handles in the array that are set by the API. + if err == windows.ERROR_NO_MORE_ITEMS { + return nil, err + } else if err != nil { + return nil, fmt.Errorf("wevtapi.EvtNext failed: %v", err) + } + + // Event handles must be closed after they are returned by EvtNext whether or not we use them. + defer func() { + for _, event := range events[:returned] { + winlog.Close(event) + } + }() + + // Render events. + var renderedEvents []string + for _, event := range events[:returned] { + // Render the basic XML representation of the event. + fragment, err := winlog.RenderFragment(event, wevtapi.EvtRenderEventXml) + if err != nil { + w.logger.Errorf("Failed to render event with RenderFragment, skipping: %v", err) + continue + } + w.logger.Tracef("Rendered event: %s", fragment) + renderedEvents = append(renderedEvents, fragment) + } + return renderedEvents, err +} + +func (w *WinEventLogSource) buildXpathQuery() (string, error) { + var query string + queryComponents := [][]string{} + if w.config.EventIDs != nil { + eventIds := []string{} + for _, id := range w.config.EventIDs { + eventIds = append(eventIds, fmt.Sprintf("EventID=%d", id)) + } + queryComponents = append(queryComponents, eventIds) + } + if w.config.EventLevel != "" { + levels, err := logLevelToInt(w.config.EventLevel) + logLevels := []string{} + if err != nil { + return "", err + } + for _, level := range levels { + logLevels = append(logLevels, fmt.Sprintf("Level=%s", level)) + } + queryComponents = append(queryComponents, logLevels) + } + if len(queryComponents) > 0 { + andList := []string{} + for _, component := range queryComponents { + andList = append(andList, fmt.Sprintf("(%s)", strings.Join(component, " or "))) + } + query = fmt.Sprintf("*[System[%s]]", strings.Join(andList, " and ")) + } else { + query = "*" + } + queryList := QueryList{Select: Select{Path: w.config.EventChannel, Query: query}} + xpathQuery, err := xml.Marshal(queryList) + if err != nil { + w.logger.Errorf("Marshal failed: %v", err) + return "", err + } + w.logger.Debugf("xpathQuery: %s", xpathQuery) + return string(xpathQuery), nil +} + +func (w *WinEventLogSource) getEvents(out chan types.Event, t *tomb.Tomb) error { + subscription, err := winlog.Subscribe(w.evtConfig) + if err != nil { + w.logger.Errorf("Failed to subscribe to event log: %s", err) + return err + } + defer winlog.Close(subscription) + publisherCache := make(map[string]windows.Handle) + defer func() { + for _, h := range publisherCache { + winlog.Close(h) + } + }() + for { + select { + case <-t.Dying(): + w.logger.Infof("wineventlog is dying") + return nil + default: + status, err := windows.WaitForSingleObject(w.evtConfig.SignalEvent, 1000) + if err != nil { + w.logger.Errorf("WaitForSingleObject failed: %s", err) + return err + } + if status == syscall.WAIT_OBJECT_0 { + renderedEvents, err := w.getXMLEvents(w.evtConfig, publisherCache, subscription, 500) + if err == windows.ERROR_NO_MORE_ITEMS { + windows.ResetEvent(w.evtConfig.SignalEvent) + } else if err != nil { + w.logger.Errorf("getXMLEvents failed: %v", err) + continue + } + for _, event := range renderedEvents { + linesRead.With(prometheus.Labels{"source": w.name}).Inc() + l := types.Line{} + l.Raw = event + l.Module = w.GetName() + l.Labels = w.config.Labels + l.Time = time.Now() + l.Src = w.name + l.Process = true + if !w.config.UseTimeMachine { + out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE} + } else { + out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE} + } + } + } + + } + } +} + +func (w *WinEventLogSource) generateConfig(query string) (*winlog.SubscribeConfig, error) { + var config winlog.SubscribeConfig + var err error + + // Create a subscription signaler. + config.SignalEvent, err = windows.CreateEvent( + nil, // Default security descriptor. + 1, // Manual reset. + 1, // Initial state is signaled. + nil) // Optional name. + if err != nil { + return &config, fmt.Errorf("windows.CreateEvent failed: %v", err) + } + config.Flags = wevtapi.EvtSubscribeToFutureEvents + config.Query, err = syscall.UTF16PtrFromString(query) + if err != nil { + return &config, fmt.Errorf("syscall.UTF16PtrFromString failed: %v", err) + } + + return &config, nil +} + +func (w *WinEventLogSource) Configure(yamlConfig []byte, logger *log.Entry) error { + + config := WinEventLogConfiguration{} + w.logger = logger + err := yaml.UnmarshalStrict(yamlConfig, &config) + + if err != nil { + return fmt.Errorf("unable to parse configuration: %v", err) + } + + if config.EventChannel != "" && config.XPathQuery != "" { + return fmt.Errorf("event_channel and xpath_query are mutually exclusive") + } + + if config.EventChannel == "" && config.XPathQuery == "" { + return fmt.Errorf("event_channel or xpath_query must be set") + } + + config.Mode = configuration.TAIL_MODE + w.config = config + + if config.XPathQuery != "" { + w.query = config.XPathQuery + } else { + w.query, err = w.buildXpathQuery() + if err != nil { + return fmt.Errorf("buildXpathQuery failed: %v", err) + } + } + + w.evtConfig, err = w.generateConfig(w.query) + if err != nil { + return err + } + + if config.PrettyName != "" { + w.name = config.PrettyName + } else { + w.name = w.query + } + + return nil +} + +func (w *WinEventLogSource) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry) error { + return nil +} + +func (w *WinEventLogSource) GetMode() string { + return w.config.Mode +} + +func (w *WinEventLogSource) SupportedModes() []string { + return []string{configuration.TAIL_MODE} +} + +func (w *WinEventLogSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) error { + return nil +} + +func (w *WinEventLogSource) GetMetrics() []prometheus.Collector { + return []prometheus.Collector{linesRead} +} + +func (w *WinEventLogSource) GetAggregMetrics() []prometheus.Collector { + return []prometheus.Collector{linesRead} +} + +func (w *WinEventLogSource) GetName() string { + return "wineventlog" +} + +func (w *WinEventLogSource) CanRun() error { + if runtime.GOOS != "windows" { + return errors.New("windows event log acquisition is only supported on Windows") + } + return nil +} + +func (w *WinEventLogSource) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error { + t.Go(func() error { + defer types.CatchPanic("crowdsec/acquis/wineventlog/streaming") + return w.getEvents(out, t) + }) + return nil +} + +func (w *WinEventLogSource) Dump() interface{} { + return w +} diff --git a/pkg/apiclient/client_test.go b/pkg/apiclient/client_test.go index 187f44365..45c061bdc 100644 --- a/pkg/apiclient/client_test.go +++ b/pkg/apiclient/client_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -142,7 +143,11 @@ func TestNewClientRegisterKO(t *testing.T) { URL: apiURL, VersionPrefix: "v1", }, &http.Client{}) - assert.Contains(t, fmt.Sprintf("%s", err), "dial tcp 127.0.0.1:4242: connect: connection refused") + if runtime.GOOS != "windows" { + assert.Contains(t, fmt.Sprintf("%s", err), "dial tcp 127.0.0.1:4242: connect: connection refused") + } else { + assert.Contains(t, fmt.Sprintf("%s", err), " No connection could be made because the target machine actively refused it.") + } } func TestNewClientRegisterOK(t *testing.T) { diff --git a/pkg/apiserver/alerts_test.go b/pkg/apiserver/alerts_test.go index a0d40a9fb..44437133a 100644 --- a/pkg/apiserver/alerts_test.go +++ b/pkg/apiserver/alerts_test.go @@ -9,6 +9,7 @@ import ( "sync" "testing" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/gin-gonic/gin" @@ -26,12 +27,12 @@ type LAPI struct { func SetupLAPITest(t *testing.T) LAPI { t.Helper() - router, loginResp, err := InitMachineTest() + router, loginResp, config, err := InitMachineTest() if err != nil { t.Fatal(err.Error()) } - APIKey, err := CreateTestBouncer() + APIKey, err := CreateTestBouncer(config.API.Server.DbConfig) if err != nil { t.Fatalf("%s", err.Error()) } @@ -59,25 +60,25 @@ func (l *LAPI) RecordResponse(verb string, url string, body *strings.Reader) *ht return w } -func InitMachineTest() (*gin.Engine, models.WatcherAuthResponse, error) { - router, err := NewAPITest() +func InitMachineTest() (*gin.Engine, models.WatcherAuthResponse, csconfig.Config, error) { + router, config, err := NewAPITest() if err != nil { - return nil, models.WatcherAuthResponse{}, fmt.Errorf("unable to run local API: %s", err) + return nil, models.WatcherAuthResponse{}, config, fmt.Errorf("unable to run local API: %s", err) } - loginResp, err := LoginToTestAPI(router) + loginResp, err := LoginToTestAPI(router, config) if err != nil { - return nil, models.WatcherAuthResponse{}, fmt.Errorf("%s", err.Error()) + return nil, models.WatcherAuthResponse{}, config, fmt.Errorf("%s", err.Error()) } - return router, loginResp, nil + return router, loginResp, config, nil } -func LoginToTestAPI(router *gin.Engine) (models.WatcherAuthResponse, error) { +func LoginToTestAPI(router *gin.Engine, config csconfig.Config) (models.WatcherAuthResponse, error) { body, err := CreateTestMachine(router) if err != nil { return models.WatcherAuthResponse{}, fmt.Errorf("%s", err.Error()) } - err = ValidateMachine("test") + err = ValidateMachine("test", config.API.Server.DbConfig) if err != nil { log.Fatalln(err.Error()) } @@ -141,14 +142,14 @@ func TestCreateAlert(t *testing.T) { func TestCreateAlertChannels(t *testing.T) { - apiServer, err := NewAPIServer() + apiServer, config, err := NewAPIServer() if err != nil { log.Fatalln(err.Error()) } apiServer.controller.PluginChannel = make(chan csplugin.ProfileAlert) apiServer.InitController() - loginResp, err := LoginToTestAPI(apiServer.router) + loginResp, err := LoginToTestAPI(apiServer.router, config) if err != nil { log.Fatalln(err.Error()) } @@ -427,7 +428,7 @@ func TestDeleteAlertTrustedIPS(t *testing.T) { if err != nil { log.Fatal(err.Error()) } - loginResp, err := LoginToTestAPI(router) + loginResp, err := LoginToTestAPI(router, cfg) if err != nil { log.Fatal(err.Error()) } diff --git a/pkg/apiserver/api_key_test.go b/pkg/apiserver/api_key_test.go index 0e91b79fd..3022c9584 100644 --- a/pkg/apiserver/api_key_test.go +++ b/pkg/apiserver/api_key_test.go @@ -11,12 +11,12 @@ import ( ) func TestAPIKey(t *testing.T) { - router, err := NewAPITest() + router, config, err := NewAPITest() if err != nil { log.Fatalf("unable to run local API: %s", err) } - APIKey, err := CreateTestBouncer() + APIKey, err := CreateTestBouncer(config.API.Server.DbConfig) if err != nil { log.Fatalf("%s", err.Error()) } diff --git a/pkg/apiserver/apic_test.go b/pkg/apiserver/apic_test.go index e85e19924..6b4841ee0 100644 --- a/pkg/apiserver/apic_test.go +++ b/pkg/apiserver/apic_test.go @@ -728,15 +728,15 @@ func TestAPICSendMetrics(t *testing.T) { }{ { name: "basic", - duration: time.Millisecond * 5, - metricsInterval: time.Millisecond, + duration: time.Millisecond * 30, + metricsInterval: time.Millisecond * 5, expectedCalls: 5, setUp: func() {}, }, { name: "with some metrics", - duration: time.Millisecond * 5, - metricsInterval: time.Millisecond, + duration: time.Millisecond * 30, + metricsInterval: time.Millisecond * 5, expectedCalls: 5, setUp: func() { api.dbClient.Ent.Machine.Create(). @@ -862,7 +862,8 @@ func TestAPICPull(t *testing.T) { panic(err) } }() - time.Sleep(time.Millisecond * 10) + //Slightly long because the CI runner for windows are slow, and this can lead to random failure + time.Sleep(time.Millisecond * 500) logrus.SetOutput(os.Stderr) assert.Contains(t, buf.String(), testCase.logContains) assertTotalDecisionCount(t, api.dbClient, testCase.expectedDecisionCount) diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 522482239..ece5ec91e 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" "time" @@ -41,9 +42,10 @@ func LoadTestConfig() csconfig.Config { flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } + tempDir, _ := os.MkdirTemp("", "crowdsec_tests") dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", - DbPath: "./ent", + DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } apiServerConfig := csconfig.LocalApiServerCfg{ @@ -72,9 +74,10 @@ func LoadTestConfigForwardedFor() csconfig.Config { flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } + tempDir, _ := os.MkdirTemp("", "crowdsec_tests") dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", - DbPath: "./ent", + DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } apiServerConfig := csconfig.LocalApiServerCfg{ @@ -99,58 +102,57 @@ func LoadTestConfigForwardedFor() csconfig.Config { return config } -func NewAPIServer() (*APIServer, error) { +func NewAPIServer() (*APIServer, csconfig.Config, error) { config := LoadTestConfig() os.Remove("./ent") apiServer, err := NewServer(config.API.Server) if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } log.Printf("Creating new API server") gin.SetMode(gin.TestMode) - return apiServer, nil + return apiServer, config, nil } -func NewAPITest() (*gin.Engine, error) { - apiServer, err := NewAPIServer() +func NewAPITest() (*gin.Engine, csconfig.Config, error) { + apiServer, config, err := NewAPIServer() if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } err = apiServer.InitController() if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } router, err := apiServer.Router() if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } - return router, nil + return router, config, nil } -func NewAPITestForwardedFor() (*gin.Engine, error) { +func NewAPITestForwardedFor() (*gin.Engine, csconfig.Config, error) { config := LoadTestConfigForwardedFor() os.Remove("./ent") apiServer, err := NewServer(config.API.Server) if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } err = apiServer.InitController() if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } log.Printf("Creating new API server") gin.SetMode(gin.TestMode) router, err := apiServer.Router() if err != nil { - return nil, fmt.Errorf("unable to run local API: %s", err) + return nil, config, fmt.Errorf("unable to run local API: %s", err) } - return router, nil + return router, config, nil } -func ValidateMachine(machineID string) error { - config := LoadTestConfig() - dbClient, err := database.NewClient(config.API.Server.DbConfig) +func ValidateMachine(machineID string, config *csconfig.DatabaseCfg) error { + dbClient, err := database.NewClient(config) if err != nil { return fmt.Errorf("unable to create new database client: %s", err) } @@ -160,9 +162,8 @@ func ValidateMachine(machineID string) error { return nil } -func GetMachineIP(machineID string) (string, error) { - config := LoadTestConfig() - dbClient, err := database.NewClient(config.API.Server.DbConfig) +func GetMachineIP(machineID string, config *csconfig.DatabaseCfg) (string, error) { + dbClient, err := database.NewClient(config) if err != nil { return "", fmt.Errorf("unable to create new database client: %s", err) } @@ -265,10 +266,8 @@ func CreateTestMachine(router *gin.Engine) (string, error) { return body, nil } -func CreateTestBouncer() (string, error) { - config := LoadTestConfig() - - dbClient, err := database.NewClient(config.API.Server.DbConfig) +func CreateTestBouncer(config *csconfig.DatabaseCfg) (string, error) { + dbClient, err := database.NewClient(config) if err != nil { log.Fatalf("unable to create new database client: %s", err) } @@ -304,7 +303,7 @@ func TestWithWrongFlushConfig(t *testing.T) { } func TestUnknownPath(t *testing.T) { - router, err := NewAPITest() + router, _, err := NewAPITest() if err != nil { log.Fatalf("unable to run local API: %s", err) } @@ -340,25 +339,23 @@ func TestLoggingDebugToFileConfig(t *testing.T) { flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } + tempDir, _ := os.MkdirTemp("", "crowdsec_tests") dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", - DbPath: "./ent", + DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } cfg := csconfig.LocalApiServerCfg{ ListenURI: "127.0.0.1:8080", LogMedia: "file", - LogDir: ".", + LogDir: tempDir, DbConfig: &dbconfig, } lvl := log.DebugLevel - expectedFile := "./crowdsec_api.log" + expectedFile := fmt.Sprintf("%s/crowdsec_api.log", tempDir) expectedLines := []string{"/test42"} cfg.LogLevel = &lvl - os.Remove("./crowdsec.log") - os.Remove(expectedFile) - // Configure logging if err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.CompressLogs); err != nil { t.Fatal(err.Error()) @@ -391,9 +388,6 @@ func TestLoggingDebugToFileConfig(t *testing.T) { } } - os.Remove("./crowdsec.log") - os.Remove(expectedFile) - } func TestLoggingErrorToFileConfig(t *testing.T) { @@ -403,24 +397,22 @@ func TestLoggingErrorToFileConfig(t *testing.T) { flushConfig := csconfig.FlushDBCfg{ MaxAge: &maxAge, } + tempDir, _ := os.MkdirTemp("", "crowdsec_tests") dbconfig := csconfig.DatabaseCfg{ Type: "sqlite", - DbPath: "./ent", + DbPath: filepath.Join(tempDir, "ent"), Flush: &flushConfig, } cfg := csconfig.LocalApiServerCfg{ ListenURI: "127.0.0.1:8080", LogMedia: "file", - LogDir: ".", + LogDir: tempDir, DbConfig: &dbconfig, } lvl := log.ErrorLevel - expectedFile := "./crowdsec_api.log" + expectedFile := fmt.Sprintf("%s/crowdsec_api.log", tempDir) cfg.LogLevel = &lvl - os.Remove("./crowdsec.log") - os.Remove(expectedFile) - // Configure logging if err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.CompressLogs); err != nil { t.Fatal(err.Error()) diff --git a/pkg/apiserver/jwt_test.go b/pkg/apiserver/jwt_test.go index f959da820..6d9ba27be 100644 --- a/pkg/apiserver/jwt_test.go +++ b/pkg/apiserver/jwt_test.go @@ -11,7 +11,7 @@ import ( ) func TestLogin(t *testing.T) { - router, err := NewAPITest() + router, config, err := NewAPITest() if err != nil { log.Fatalf("unable to run local API: %s", err) } @@ -58,7 +58,7 @@ func TestLogin(t *testing.T) { assert.Equal(t, "{\"code\":401,\"message\":\"input format error\"}", w.Body.String()) //Validate machine - err = ValidateMachine("test") + err = ValidateMachine("test", config.API.Server.DbConfig) if err != nil { log.Fatalln(err.Error()) } diff --git a/pkg/apiserver/machines_test.go b/pkg/apiserver/machines_test.go index 83622e11d..b9d63c172 100644 --- a/pkg/apiserver/machines_test.go +++ b/pkg/apiserver/machines_test.go @@ -12,7 +12,7 @@ import ( ) func TestCreateMachine(t *testing.T) { - router, err := NewAPITest() + router, _, err := NewAPITest() if err != nil { log.Fatalf("unable to run local API: %s", err) } @@ -53,7 +53,7 @@ func TestCreateMachine(t *testing.T) { } func TestCreateMachineWithForwardedFor(t *testing.T) { - router, err := NewAPITestForwardedFor() + router, config, err := NewAPITestForwardedFor() if err != nil { log.Fatalf("unable to run local API: %s", err) } @@ -74,7 +74,7 @@ func TestCreateMachineWithForwardedFor(t *testing.T) { assert.Equal(t, 201, w.Code) assert.Equal(t, "", w.Body.String()) - ip, err := GetMachineIP(*MachineTest.MachineID) + ip, err := GetMachineIP(*MachineTest.MachineID, config.API.Server.DbConfig) if err != nil { log.Fatalf("Could not get machine IP : %s", err) } @@ -82,7 +82,7 @@ func TestCreateMachineWithForwardedFor(t *testing.T) { } func TestCreateMachineWithForwardedForNoConfig(t *testing.T) { - router, err := NewAPITest() + router, config, err := NewAPITest() if err != nil { log.Fatalf("unable to run local API: %s", err) } @@ -103,7 +103,7 @@ func TestCreateMachineWithForwardedForNoConfig(t *testing.T) { assert.Equal(t, 201, w.Code) assert.Equal(t, "", w.Body.String()) - ip, err := GetMachineIP(*MachineTest.MachineID) + ip, err := GetMachineIP(*MachineTest.MachineID, config.API.Server.DbConfig) if err != nil { log.Fatalf("Could not get machine IP : %s", err) } @@ -113,7 +113,7 @@ func TestCreateMachineWithForwardedForNoConfig(t *testing.T) { } func TestCreateMachineWithoutForwardedFor(t *testing.T) { - router, err := NewAPITestForwardedFor() + router, config, err := NewAPITestForwardedFor() if err != nil { log.Fatalf("unable to run local API: %s", err) } @@ -133,7 +133,7 @@ func TestCreateMachineWithoutForwardedFor(t *testing.T) { assert.Equal(t, 201, w.Code) assert.Equal(t, "", w.Body.String()) - ip, err := GetMachineIP(*MachineTest.MachineID) + ip, err := GetMachineIP(*MachineTest.MachineID, config.API.Server.DbConfig) if err != nil { log.Fatalf("Could not get machine IP : %s", err) } @@ -143,7 +143,7 @@ func TestCreateMachineWithoutForwardedFor(t *testing.T) { } func TestCreateMachineAlreadyExist(t *testing.T) { - router, err := NewAPITest() + router, _, err := NewAPITest() if err != nil { log.Fatalf("unable to run local API: %s", err) } diff --git a/pkg/apiserver/testutils.go b/pkg/apiserver/testutils.go new file mode 100644 index 000000000..ba2f510df --- /dev/null +++ b/pkg/apiserver/testutils.go @@ -0,0 +1,9 @@ +//go:build !windows + +package apiserver + +import "os" + +func cleanFile(path string) { + os.Remove(path) +} diff --git a/pkg/apiserver/testutils_windows.go b/pkg/apiserver/testutils_windows.go new file mode 100644 index 000000000..f322ee200 --- /dev/null +++ b/pkg/apiserver/testutils_windows.go @@ -0,0 +1,7 @@ +package apiserver + +import "os" + +func cleanFile(path string) { + os.Remove(path) +} diff --git a/pkg/csconfig/config_test.go b/pkg/csconfig/config_test.go index 00f509ad8..c0e2b7f19 100644 --- a/pkg/csconfig/config_test.go +++ b/pkg/csconfig/config_test.go @@ -3,6 +3,7 @@ package csconfig import ( "fmt" "log" + "runtime" "strings" "testing" @@ -17,8 +18,14 @@ func TestNormalLoad(t *testing.T) { } _, err = NewConfig("./tests/xxx.yaml", false, false) - if fmt.Sprintf("%s", err) != "failed to read config file: open ./tests/xxx.yaml: no such file or directory" { - t.Fatalf("unexpected error %s", err) + if runtime.GOOS != "windows" { + if fmt.Sprintf("%s", err) != "failed to read config file: open ./tests/xxx.yaml: no such file or directory" { + t.Fatalf("unexpected error %s", err) + } + } else { + if fmt.Sprintf("%s", err) != "failed to read config file: open ./tests/xxx.yaml: The system cannot find the file specified." { + t.Fatalf("unexpected error %s", err) + } } _, err = NewConfig("./tests/simulation.yaml", false, false) diff --git a/pkg/csconfig/simulation_test.go b/pkg/csconfig/simulation_test.go index af3d5c40c..b02011f63 100644 --- a/pkg/csconfig/simulation_test.go +++ b/pkg/csconfig/simulation_test.go @@ -3,6 +3,7 @@ package csconfig import ( "fmt" "path/filepath" + "runtime" "strings" "testing" @@ -39,17 +40,6 @@ func TestSimulationLoading(t *testing.T) { }, expectedResult: &SimulationConfig{Simulation: new(bool)}, }, - { - name: "basic bad file name", - Input: &Config{ - ConfigPaths: &ConfigurationPaths{ - SimulationFilePath: "./tests/xxx.yaml", - DataDir: "./data", - }, - Crowdsec: &CrowdsecServiceCfg{}, - }, - err: fmt.Sprintf("while reading '%s': open %s: no such file or directory", testXXFullPath, testXXFullPath), - }, { name: "basic nil config", Input: &Config{ @@ -84,6 +74,42 @@ func TestSimulationLoading(t *testing.T) { }, } + if runtime.GOOS == "windows" { + tests = append(tests, struct { + name string + Input *Config + expectedResult *SimulationConfig + err string + }{ + name: "basic bad file name", + Input: &Config{ + ConfigPaths: &ConfigurationPaths{ + SimulationFilePath: "./tests/xxx.yaml", + DataDir: "./data", + }, + Crowdsec: &CrowdsecServiceCfg{}, + }, + err: fmt.Sprintf("while reading '%s': open %s: The system cannot find the file specified.", testXXFullPath, testXXFullPath), + }) + } else { + tests = append(tests, struct { + name string + Input *Config + expectedResult *SimulationConfig + err string + }{ + name: "basic bad file name", + Input: &Config{ + ConfigPaths: &ConfigurationPaths{ + SimulationFilePath: "./tests/xxx.yaml", + DataDir: "./data", + }, + Crowdsec: &CrowdsecServiceCfg{}, + }, + err: fmt.Sprintf("while reading '%s': open %s: no such file or directory", testXXFullPath, testXXFullPath), + }) + } + for idx, test := range tests { err := test.Input.LoadSimulation() if err == nil && test.err != "" { diff --git a/pkg/csplugin/broker.go b/pkg/csplugin/broker.go index 534631e34..2c9091be5 100644 --- a/pkg/csplugin/broker.go +++ b/pkg/csplugin/broker.go @@ -4,16 +4,10 @@ import ( "context" "fmt" "io" - "io/fs" - "math" "os" - "os/exec" - "os/user" "path/filepath" - "strconv" "strings" "sync" - "syscall" "text/template" "time" @@ -259,16 +253,9 @@ func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) ( return nil, err } log.Debugf("Executing plugin %s", binaryPath) - cmd := exec.Command(binaryPath) - if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" { - if !(pb.pluginProcConfig.User != "" && pb.pluginProcConfig.Group != "") { - return nil, errors.New("while getting process attributes: both plugin user and group must be set") - } - cmd.SysProcAttr, err = getProcessAttr(pb.pluginProcConfig.User, pb.pluginProcConfig.Group) - if err != nil { - return nil, errors.Wrap(err, "while getting process attributes") - } - cmd.SysProcAttr.Credential.NoSetGroups = true + cmd, err := pb.CreateCmd(binaryPath) + if err != nil { + return nil, err } pb.pluginMap[name] = &NotifierPlugin{} l := log.New() @@ -365,43 +352,6 @@ func setRequiredFields(pluginCfg *PluginConfig) { } -func pluginIsValid(path string) error { - var details fs.FileInfo - var err error - - // check if it exists - if details, err = os.Stat(path); err != nil { - return errors.Wrap(err, fmt.Sprintf("plugin at %s does not exist", path)) - } - - // check if it is owned by current user - currentUser, err := user.Current() - if err != nil { - return errors.Wrap(err, "while getting current user") - } - currentUID, err := getUID(currentUser.Username) - if err != nil { - return errors.Wrap(err, "while looking up the current uid") - } - stat := details.Sys().(*syscall.Stat_t) - if stat.Uid != currentUID { - return fmt.Errorf("plugin at %s is not owned by user '%s'", path, currentUser.Username) - } - - mode := details.Mode() - perm := uint32(mode) - if (perm & 00002) != 0 { - return fmt.Errorf("plugin at %s is world writable, world writable plugins are invalid", path) - } - if (perm & 00020) != 0 { - return fmt.Errorf("plugin at %s is group writable, group writable plugins are invalid", path) - } - if (mode & os.ModeSetgid) != 0 { - return fmt.Errorf("plugin at %s has setgid permission, which is not allowed", path) - } - return nil -} - // helper which gives paths to all files in the given directory non-recursively func listFilesAtPath(path string) ([]string, error) { filePaths := make([]string, 0) @@ -418,62 +368,6 @@ func listFilesAtPath(path string) ([]string, error) { return filePaths, nil } -func getPluginTypeAndSubtypeFromPath(path string) (string, string, error) { - pluginFileName := filepath.Base(path) - parts := strings.Split(pluginFileName, "-") - if len(parts) < 2 { - return "", "", fmt.Errorf("plugin name %s is invalid. Name should be like {type-name}", path) - } - return strings.Join(parts[:len(parts)-1], "-"), parts[len(parts)-1], nil -} - -func getUID(username string) (uint32, error) { - u, err := user.Lookup(username) - if err != nil { - return 0, err - } - uid, err := strconv.ParseInt(u.Uid, 10, 32) - if err != nil { - return 0, err - } - if uid < 0 || uid > math.MaxInt32 { - return 0, fmt.Errorf("out of bound uid") - } - return uint32(uid), nil -} - -func getGID(groupname string) (uint32, error) { - g, err := user.LookupGroup(groupname) - if err != nil { - return 0, err - } - gid, err := strconv.ParseInt(g.Gid, 10, 32) - if err != nil { - return 0, err - } - if gid < 0 || gid > math.MaxInt32 { - return 0, fmt.Errorf("out of bound gid") - } - return uint32(gid), nil -} - -func getProcessAttr(username string, groupname string) (*syscall.SysProcAttr, error) { - uid, err := getUID(username) - if err != nil { - return nil, err - } - gid, err := getGID(groupname) - if err != nil { - return nil, err - } - return &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Uid: uid, - Gid: gid, - }, - }, nil -} - func getUUID() (string, error) { uuidv4, err := uuid.NewRandom() if err != nil { diff --git a/pkg/csplugin/broker_test.go b/pkg/csplugin/broker_test.go index 6befc893b..b4363103c 100644 --- a/pkg/csplugin/broker_test.go +++ b/pkg/csplugin/broker_test.go @@ -1,3 +1,5 @@ +//go:build linux || freebsd || netbsd || openbsd || solaris || !windows + package csplugin import ( @@ -6,7 +8,9 @@ import ( "os" "os/exec" "path" + "path/filepath" "reflect" + "runtime" "testing" "time" @@ -109,8 +113,8 @@ func TestListFilesAtPath(t *testing.T) { path: testPath, }, want: []string{ - path.Join(testPath, "notification-gitter"), - path.Join(testPath, "slack"), + filepath.Join(testPath, "notification-gitter"), + filepath.Join(testPath, "slack"), }, }, { @@ -136,6 +140,9 @@ func TestListFilesAtPath(t *testing.T) { } func TestBrokerInit(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows") + } tests := []struct { name string @@ -570,8 +577,10 @@ func buildDummyPlugin() { } func setPluginPermTo(perm string) { - if err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run(); err != nil { - log.Fatal(errors.Wrapf(err, "chmod 744 %s", path.Join(testPath, "notification-dummy"))) + if runtime.GOOS != "windows" { + if err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run(); err != nil { + log.Fatal(errors.Wrapf(err, "chmod 744 %s", path.Join(testPath, "notification-dummy"))) + } } } @@ -580,14 +589,16 @@ func setUp() { if err != nil { log.Fatal(err) } - _, err = os.Create(path.Join(dir, "slack")) + f, err := os.Create(path.Join(dir, "slack")) if err != nil { log.Fatal(err) } - _, err = os.Create(path.Join(dir, "notification-gitter")) + f.Close() + f, err = os.Create(path.Join(dir, "notification-gitter")) if err != nil { log.Fatal(err) } + f.Close() err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666) if err != nil { log.Fatal(err) diff --git a/pkg/csplugin/broker_win_test.go b/pkg/csplugin/broker_win_test.go new file mode 100644 index 000000000..395daaffd --- /dev/null +++ b/pkg/csplugin/broker_win_test.go @@ -0,0 +1,257 @@ +//go:build windows + +package csplugin + +import ( + "log" + "os" + "os/exec" + "path" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/models" + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/stretchr/testify/assert" + "gopkg.in/tomb.v2" +) + +/* +Due to the complexity of file permission modification with go on windows, we only test the basic behaviour the broker, +not if it will actually reject plugins with invalid permissions +*/ + +var testPath string + +func TestGetPluginNameAndTypeFromPath(t *testing.T) { + setUp() + defer tearDown() + type args struct { + path string + } + tests := []struct { + name string + args args + want string + want1 string + wantErr bool + }{ + { + name: "valid plugin name, single dash", + args: args{ + path: path.Join(testPath, "notification-gitter"), + }, + want: "notification", + want1: "gitter", + wantErr: false, + }, + { + name: "invalid plugin name", + args: args{ + path: ".\\tests\\gitter.exe", + }, + want: "", + want1: "", + wantErr: true, + }, + { + name: "valid plugin name, multiple dash", + args: args{ + path: ".\\tests\\notification-instant-slack.exe", + }, + want: "notification-instant", + want1: "slack", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := getPluginTypeAndSubtypeFromPath(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("getPluginNameAndTypeFromPath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getPluginNameAndTypeFromPath() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("getPluginNameAndTypeFromPath() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestListFilesAtPath(t *testing.T) { + setUp() + defer tearDown() + type args struct { + path string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "valid directory", + args: args{ + path: testPath, + }, + want: []string{ + filepath.Join(testPath, "notification-gitter"), + filepath.Join(testPath, "slack"), + }, + }, + { + name: "invalid directory", + args: args{ + path: "./foo/bar/", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := listFilesAtPath(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("listFilesAtPath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("listFilesAtPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBrokerInit(t *testing.T) { + tests := []struct { + name string + action func() + errContains string + wantErr bool + procCfg csconfig.PluginCfg + }{ + { + name: "valid config", + wantErr: false, + }, + { + name: "no plugin dir", + wantErr: true, + errContains: "The system cannot find the file specified.", + action: tearDown, + }, + { + name: "no plugin binary", + wantErr: true, + errContains: "binary for plugin dummy_default not found", + action: func() { + err := os.Remove(path.Join(testPath, "notification-dummy.exe")) + if err != nil { + t.Fatal(err) + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + defer tearDown() + buildDummyPlugin() + if test.action != nil { + test.action() + } + pb := PluginBroker{} + profiles := csconfig.NewDefaultConfig().API.Server.Profiles + profiles = append(profiles, &csconfig.ProfileCfg{ + Notifications: []string{"dummy_default"}, + }) + err := pb.Init(&test.procCfg, profiles, &csconfig.ConfigurationPaths{ + PluginDir: testPath, + NotificationDir: "./tests/notifications", + }) + defer pb.Kill() + if test.wantErr { + assert.ErrorContains(t, err, test.errContains) + } else { + assert.NoError(t, err) + } + + }) + } +} + +func TestBrokerRun(t *testing.T) { + buildDummyPlugin() + defer tearDown() + procCfg := csconfig.PluginCfg{} + pb := PluginBroker{} + profiles := csconfig.NewDefaultConfig().API.Server.Profiles + profiles = append(profiles, &csconfig.ProfileCfg{ + Notifications: []string{"dummy_default"}, + }) + err := pb.Init(&procCfg, profiles, &csconfig.ConfigurationPaths{ + PluginDir: testPath, + NotificationDir: "./tests/notifications", + }) + assert.NoError(t, err) + tomb := tomb.Tomb{} + go pb.Run(&tomb) + defer pb.Kill() + + assert.NoFileExists(t, "./out") + defer os.Remove("./out") + + pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} + pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} + time.Sleep(time.Second * 4) + + assert.FileExists(t, ".\\out") + assert.Equal(t, types.GetLineCountForFile(".\\out"), 2) +} + +func buildDummyPlugin() { + dir, err := os.MkdirTemp(".\\tests", "cs_plugin_test") + if err != nil { + log.Fatal(err) + } + cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy.exe"), "../../plugins/notifications/dummy/") + if err := cmd.Run(); err != nil { + log.Fatal(err) + } + testPath = dir +} + +func setUp() { + dir, err := os.MkdirTemp("./", "cs_plugin_test") + if err != nil { + log.Fatal(err) + } + f, err := os.Create(path.Join(dir, "slack")) + if err != nil { + log.Fatal(err) + } + f.Close() + f, err = os.Create(path.Join(dir, "notification-gitter")) + if err != nil { + log.Fatal(err) + } + f.Close() + err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666) + if err != nil { + log.Fatal(err) + } + testPath = dir +} + +func tearDown() { + err := os.RemoveAll(testPath) + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/csplugin/utils.go b/pkg/csplugin/utils.go new file mode 100644 index 000000000..6149d68c4 --- /dev/null +++ b/pkg/csplugin/utils.go @@ -0,0 +1,150 @@ +//go:build linux || freebsd || netbsd || openbsd || solaris || !windows + +package csplugin + +import ( + "fmt" + "io/fs" + "math" + "os" + "os/exec" + "os/user" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/pkg/errors" +) + +func CheckCredential(uid int, gid int) *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + } +} + +func (pb *PluginBroker) CreateCmd(binaryPath string) (*exec.Cmd, error) { + var err error + cmd := exec.Command(binaryPath) + if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" { + if !(pb.pluginProcConfig.User != "" && pb.pluginProcConfig.Group != "") { + return nil, errors.New("while getting process attributes: both plugin user and group must be set") + } + cmd.SysProcAttr, err = getProcessAttr(pb.pluginProcConfig.User, pb.pluginProcConfig.Group) + if err != nil { + return nil, errors.Wrap(err, "while getting process attributes") + } + cmd.SysProcAttr.Credential.NoSetGroups = true + } + return cmd, err +} + +func getUID(username string) (uint32, error) { + u, err := user.Lookup(username) + if err != nil { + return 0, err + } + uid, err := strconv.ParseInt(u.Uid, 10, 32) + if err != nil { + return 0, err + } + if uid < 0 || uid > math.MaxInt32 { + return 0, fmt.Errorf("out of bound uid") + } + return uint32(uid), nil +} + +func getGID(groupname string) (uint32, error) { + g, err := user.LookupGroup(groupname) + if err != nil { + return 0, err + } + gid, err := strconv.ParseInt(g.Gid, 10, 32) + if err != nil { + return 0, err + } + if gid < 0 || gid > math.MaxInt32 { + return 0, fmt.Errorf("out of bound gid") + } + return uint32(gid), nil +} + +func getPluginTypeAndSubtypeFromPath(path string) (string, string, error) { + pluginFileName := filepath.Base(path) + parts := strings.Split(pluginFileName, "-") + if len(parts) < 2 { + return "", "", fmt.Errorf("plugin name %s is invalid. Name should be like {type-name}", path) + } + return strings.Join(parts[:len(parts)-1], "-"), parts[len(parts)-1], nil +} + +func getProcessAttr(username string, groupname string) (*syscall.SysProcAttr, error) { + u, err := user.Lookup(username) + if err != nil { + return nil, err + } + g, err := user.LookupGroup(groupname) + if err != nil { + return nil, err + } + uid, err := strconv.ParseInt(u.Uid, 10, 32) + if err != nil { + return nil, err + } + if uid < 0 && uid > math.MaxInt32 { + return nil, fmt.Errorf("out of bound uid") + } + gid, err := strconv.ParseInt(g.Gid, 10, 32) + if err != nil { + return nil, err + } + if gid < 0 && gid > math.MaxInt32 { + return nil, fmt.Errorf("out of bound gid") + } + return &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + }, nil +} + +func pluginIsValid(path string) error { + var details fs.FileInfo + var err error + + // check if it exists + if details, err = os.Stat(path); err != nil { + return errors.Wrap(err, fmt.Sprintf("plugin at %s does not exist", path)) + } + + // check if it is owned by current user + currentUser, err := user.Current() + if err != nil { + return errors.Wrap(err, "while getting current user") + } + currentUID, err := getUID(currentUser.Username) + if err != nil { + return errors.Wrap(err, "while looking up the current uid") + } + stat := details.Sys().(*syscall.Stat_t) + if stat.Uid != currentUID { + return fmt.Errorf("plugin at %s is not owned by user '%s'", path, currentUser.Username) + } + + mode := details.Mode() + perm := uint32(mode) + if (perm & 00002) != 0 { + return fmt.Errorf("plugin at %s is world writable, world writable plugins are invalid", path) + } + if (perm & 00020) != 0 { + return fmt.Errorf("plugin at %s is group writable, group writable plugins are invalid", path) + } + if (mode & os.ModeSetgid) != 0 { + return fmt.Errorf("plugin at %s has setgid permission, which is not allowed", path) + } + return nil +} diff --git a/pkg/csplugin/utils_windows.go b/pkg/csplugin/utils_windows.go new file mode 100644 index 000000000..874e30021 --- /dev/null +++ b/pkg/csplugin/utils_windows.go @@ -0,0 +1,242 @@ +//go:build windows + +package csplugin + +import ( + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" + "reflect" + "strings" + "syscall" + "unsafe" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +var ( + advapi32 = syscall.NewLazyDLL("advapi32.dll") + + procGetAce = advapi32.NewProc("GetAce") +) + +type AclSizeInformation struct { + AceCount uint32 + AclBytesInUse uint32 + AclBytesFree uint32 +} + +type Acl struct { + AclRevision uint8 + Sbz1 uint8 + AclSize uint16 + AceCount uint16 + Sbz2 uint16 +} + +type AccessAllowedAce struct { + AceType uint8 + AceFlags uint8 + AceSize uint16 + AccessMask uint32 + SidStart uint32 +} + +const ACCESS_ALLOWED_ACE_TYPE = 0 +const ACCESS_DENIED_ACE_TYPE = 1 + +func CheckPerms(path string) error { + log.Debugf("checking permissions of %s\n", path) + + systemSid, err := windows.CreateWellKnownSid(windows.WELL_KNOWN_SID_TYPE(windows.WinLocalSystemSid)) + if err != nil { + return errors.Wrap(err, "while creating SYSTEM well known sid") + } + + adminSid, err := windows.CreateWellKnownSid(windows.WELL_KNOWN_SID_TYPE(windows.WinBuiltinAdministratorsSid)) + if err != nil { + return errors.Wrap(err, "while creating built-in Administrators well known sid") + } + + currentUser, err := user.Current() + if err != nil { + return errors.Wrap(err, "while getting current user") + } + + currentUserSid, _, _, err := windows.LookupSID("", currentUser.Username) + + if err != nil { + return errors.Wrap(err, "while looking up current user sid") + } + + sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION) + if err != nil { + return errors.Wrap(err, "while getting owner security info") + } + if !sd.IsValid() { + return errors.New("security descriptor is invalid") + } + owner, _, err := sd.Owner() + if err != nil { + return errors.Wrap(err, "while getting owner") + } + if !owner.IsValid() { + return errors.New("owner is invalid") + } + + if !owner.Equals(systemSid) && !owner.Equals(currentUserSid) && !owner.Equals(adminSid) { + return fmt.Errorf("plugin at %s is not owned by SYSTEM, Administrators or by current user, but by %s", path, owner.String()) + } + + dacl, _, err := sd.DACL() + if err != nil { + return errors.Wrap(err, "while getting DACL") + } + + if dacl == nil { + return fmt.Errorf("no DACL found on plugin, meaning fully permissive access on plugin %s", path) + } + + if err != nil { + return errors.Wrap(err, "while looking up current user sid") + } + + rs := reflect.ValueOf(dacl).Elem() + + /* + For reference, the structure of the ACL type is: + type ACL struct { + aclRevision byte + sbz1 byte + aclSize uint16 + aceCount uint16 + sbz2 uint16 + } + As the field are not exported, we have to use reflection to access them, this should not be an issue as the structure won't (probably) change any time soon. + */ + aceCount := rs.Field(3).Uint() + + for i := uint64(0); i < aceCount; i++ { + ace := &AccessAllowedAce{} + ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace))) + if ret == 0 { + return errors.Wrap(windows.GetLastError(), "while getting ACE") + } + log.Debugf("ACE %d: %+v\n", i, ace) + + if ace.AceType == ACCESS_DENIED_ACE_TYPE { + continue + } + aceSid := (*windows.SID)(unsafe.Pointer(&ace.SidStart)) + + if aceSid.Equals(systemSid) || aceSid.Equals(adminSid) { + log.Debugf("Not checking permission for well-known SID %s", aceSid.String()) + continue + } + + if aceSid.Equals(currentUserSid) { + log.Debugf("Not checking permission for current user %s", currentUser.Username) + continue + } + + log.Debugf("Checking permission for SID %s", aceSid.String()) + denyMask := ^(windows.FILE_GENERIC_READ | windows.FILE_GENERIC_EXECUTE) + if ace.AccessMask&uint32(denyMask) != 0 { + return fmt.Errorf("only SYSTEM, Administrators or the user currently running crowdsec can have more than read/execute on plugin %s", path) + } + } + + return nil +} + +func getProcessAtr() (*syscall.SysProcAttr, error) { + var procToken, token windows.Token + + proc := windows.CurrentProcess() + defer windows.CloseHandle(proc) + + err := windows.OpenProcessToken(proc, windows.TOKEN_DUPLICATE|windows.TOKEN_ADJUST_DEFAULT| + windows.TOKEN_QUERY|windows.TOKEN_ASSIGN_PRIMARY|windows.TOKEN_ADJUST_GROUPS|windows.TOKEN_ADJUST_PRIVILEGES, &procToken) + if err != nil { + return nil, errors.Wrapf(err, "while opening process token") + } + defer procToken.Close() + + err = windows.DuplicateTokenEx(procToken, 0, nil, windows.SecurityImpersonation, + windows.TokenPrimary, &token) + if err != nil { + return nil, errors.Wrapf(err, "while duplicating token") + } + + //Remove all privileges from the token + + err = windows.AdjustTokenPrivileges(token, true, nil, 0, nil, nil) + + if err != nil { + return nil, errors.Wrapf(err, "while adjusting token privileges") + } + + //Run the plugin as a medium integrity level process + //For some reasons, low level integrity don't work, the plugin and crowdsec cannot communicate over the TCP socket + sid, err := windows.CreateWellKnownSid(windows.WELL_KNOWN_SID_TYPE(windows.WinMediumLabelSid)) + if err != nil { + return nil, err + } + + tml := &windows.Tokenmandatorylabel{} + tml.Label.Attributes = windows.SE_GROUP_INTEGRITY + tml.Label.Sid = sid + + err = windows.SetTokenInformation(token, windows.TokenIntegrityLevel, + (*byte)(unsafe.Pointer(tml)), tml.Size()) + if err != nil { + token.Close() + return nil, errors.Wrapf(err, "while setting token information") + } + + return &windows.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + Token: syscall.Token(token), + }, nil +} + +func (pb *PluginBroker) CreateCmd(binaryPath string) (*exec.Cmd, error) { + var err error + cmd := exec.Command(binaryPath) + cmd.SysProcAttr, err = getProcessAtr() + if err != nil { + return nil, errors.Wrap(err, "while getting process attributes") + } + return cmd, err +} + +func getPluginTypeAndSubtypeFromPath(path string) (string, string, error) { + pluginFileName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + + parts := strings.Split(pluginFileName, "-") + if len(parts) < 2 { + return "", "", fmt.Errorf("plugin name %s is invalid. Name should be like {type-name}", path) + } + return strings.Join(parts[:len(parts)-1], "-"), parts[len(parts)-1], nil +} + +func pluginIsValid(path string) error { + var err error + + // check if it exists + if _, err = os.Stat(path); err != nil { + return errors.Wrap(err, fmt.Sprintf("plugin at %s does not exist", path)) + } + + // check if it is owned by root + err = CheckPerms(path) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/csplugin/watcher_test.go b/pkg/csplugin/watcher_test.go index acc23f29a..ba3c93d69 100644 --- a/pkg/csplugin/watcher_test.go +++ b/pkg/csplugin/watcher_test.go @@ -3,6 +3,7 @@ package csplugin import ( "context" "log" + "runtime" "testing" "time" @@ -45,6 +46,9 @@ func listenChannelWithTimeout(ctx context.Context, channel chan string) error { } func TestPluginWatcherInterval(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows because timing is not reliable") + } pw := PluginWatcher{} alertsByPluginName := make(map[string][]*models.Alert) testTomb := tomb.Tomb{} @@ -74,6 +78,9 @@ func TestPluginWatcherInterval(t *testing.T) { } func TestPluginAlertCountWatcher(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping test on windows because timing is not reliable") + } pw := PluginWatcher{} alertsByPluginName := make(map[string][]*models.Alert) configs := map[string]PluginConfig{ diff --git a/pkg/cstest/hubtest_item.go b/pkg/cstest/hubtest_item.go index 8f4b3a3c5..f4692f6a8 100644 --- a/pkg/cstest/hubtest_item.go +++ b/pkg/cstest/hubtest_item.go @@ -193,10 +193,10 @@ func (t *HubTestItem) InstallHub() error { //return fmt.Errorf("parser '%s' doesn't exist in the hub and doesn't appear to be a custom one.", parser) } - customParserPathSplit := strings.Split(customParserPath, "/") - customParserName := customParserPathSplit[len(customParserPathSplit)-1] + customParserPathSplit, customParserName := filepath.Split(customParserPath) // because path is parsers///parser.yaml and we wan't the stage - customParserStage := customParserPathSplit[len(customParserPathSplit)-3] + splittedPath := strings.Split(customParserPathSplit, string(os.PathSeparator)) + customParserStage := splittedPath[len(splittedPath)-3] // check if stage exist hubStagePath := filepath.Join(t.HubPath, fmt.Sprintf("parsers/%s", customParserStage)) diff --git a/pkg/cwhub/cwhub_test.go b/pkg/cwhub/cwhub_test.go index 65ecaf83e..cd42a76da 100644 --- a/pkg/cwhub/cwhub_test.go +++ b/pkg/cwhub/cwhub_test.go @@ -230,11 +230,11 @@ func testTaintItem(cfg *csconfig.Hub, t *testing.T, item Item) { if err != nil { t.Fatalf("(taint) opening %s (%s) : %s", item.LocalPath, item.Name, err) } + defer f.Close() if _, err = f.WriteString("tainted"); err != nil { t.Fatalf("tainting %s : %s", item.Name, err) } - f.Close() //Local sync and check status if err, _ := LocalSync(cfg); err != nil { t.Fatalf("taint: failed to run localSync : %s", err) @@ -398,8 +398,9 @@ func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { func fileToStringX(path string) string { if f, err := os.Open(path); err == nil { + defer f.Close() if data, err := io.ReadAll(f); err == nil { - return string(data) + return strings.ReplaceAll(string(data), "\r\n", "\n") } else { panic(err) } diff --git a/pkg/cwhub/download.go b/pkg/cwhub/download.go index 11969c118..582d39d4a 100644 --- a/pkg/cwhub/download.go +++ b/pkg/cwhub/download.go @@ -233,6 +233,7 @@ func DownloadDataIfNeeded(hub *csconfig.Hub, target Item, force bool) error { if itemFile, err = os.Open(itemFilePath); err != nil { return errors.Wrapf(err, "while opening %s", itemFilePath) } + defer itemFile.Close() if err = downloadData(dataFolder, force, itemFile); err != nil { return errors.Wrapf(err, "while downloading data for %s", itemFilePath) } diff --git a/pkg/cwhub/loader.go b/pkg/cwhub/loader.go index 0f0f97367..36a3f2e65 100644 --- a/pkg/cwhub/loader.go +++ b/pkg/cwhub/loader.go @@ -47,9 +47,10 @@ func parser_visit(path string, f os.FileInfo, err error) error { return nil } - subs := strings.Split(path, "/") + subs := strings.Split(path, string(os.PathSeparator)) log.Tracef("path:%s, hubdir:%s, installdir:%s", path, hubdir, installdir) + log.Tracef("subs:%v", subs) /*we're in hub (~/.hub/hub/)*/ if strings.HasPrefix(path, hubdir) { log.Tracef("in hub dir") @@ -78,7 +79,7 @@ func parser_visit(path string, f os.FileInfo, err error) error { ftype = subs[len(subs)-3] fauthor = "" } else { - return fmt.Errorf("File '%s' is not from hub '%s' nor from the configuration directory '%s'", path, hubdir, installdir) + return fmt.Errorf("file '%s' is not from hub '%s' nor from the configuration directory '%s'", path, hubdir, installdir) } log.Tracef("stage:%s ftype:%s", stage, ftype) @@ -134,8 +135,7 @@ func parser_visit(path string, f os.FileInfo, err error) error { target.Local = true target.LocalPath = path target.UpToDate = true - x := strings.Split(path, "/") - target.FileName = x[len(x)-1] + _, target.FileName = filepath.Split(path) hubIdx[ftype][fname] = target return nil @@ -161,9 +161,10 @@ func parser_visit(path string, f os.FileInfo, err error) error { continue } //wrong file - if v.Name+".yaml" != fauthor+"/"+fname && v.Name+".yml" != fauthor+"/"+fname { + if CheckName(v.Name, fauthor, fname) { continue } + if path == hubdir+"/"+v.RemotePath { log.Tracef("marking %s as downloaded", v.Name) v.Downloaded = true @@ -171,9 +172,7 @@ func parser_visit(path string, f os.FileInfo, err error) error { } else { //wrong file /////.yaml - if !strings.HasSuffix(hubpath, v.RemotePath) { - //log.Printf("wrong file %s %s", hubpath, spew.Sdump(v)) - + if CheckSuffix(hubpath, v.RemotePath) { continue } } @@ -204,8 +203,7 @@ func parser_visit(path string, f os.FileInfo, err error) error { /*if we're walking the hub, present file doesn't means installed file*/ v.Installed = true v.LocalHash = sha - x := strings.Split(path, "/") - target.FileName = x[len(x)-1] + _, target.FileName = filepath.Split(path) } else { v.Downloaded = true v.LocalHash = sha @@ -229,8 +227,7 @@ func parser_visit(path string, f os.FileInfo, err error) error { v.LocalVersion = "?" v.Tainted = true v.LocalHash = sha - x := strings.Split(path, "/") - target.FileName = x[len(x)-1] + _, target.FileName = filepath.Split(path) } //update the entry if appropriate diff --git a/pkg/cwhub/path_separator_windows.go b/pkg/cwhub/path_separator_windows.go new file mode 100644 index 000000000..42f61aa16 --- /dev/null +++ b/pkg/cwhub/path_separator_windows.go @@ -0,0 +1,23 @@ +package cwhub + +import ( + "path/filepath" + "strings" +) + +func CheckSuffix(hubpath string, remotePath string) bool { + newPath := filepath.ToSlash(hubpath) + if !strings.HasSuffix(newPath, remotePath) { + return true + } else { + return false + } +} + +func CheckName(vname string, fauthor string, fname string) bool { + if vname+".yaml" != fauthor+"/"+fname && vname+".yml" != fauthor+"/"+fname { + return true + } else { + return false + } +} diff --git a/pkg/cwhub/pathseparator.go b/pkg/cwhub/pathseparator.go new file mode 100644 index 000000000..0340697ee --- /dev/null +++ b/pkg/cwhub/pathseparator.go @@ -0,0 +1,24 @@ +//go:build linux || freebsd || netbsd || openbsd || solaris || !windows +// +build linux freebsd netbsd openbsd solaris !windows + +package cwhub + +import "strings" + +const PathSeparator = "/" + +func CheckSuffix(hubpath string, remotePath string) bool { + if !strings.HasSuffix(hubpath, remotePath) { + return true + } else { + return false + } +} + +func CheckName(vname string, fauthor string, fname string) bool { + if vname+".yaml" != fauthor+"/"+fname && vname+".yml" != fauthor+"/"+fname { + return true + } else { + return false + } +} diff --git a/pkg/time/rate/rate_test.go b/pkg/time/rate/rate_test.go index 57b0fb1a8..6df94127a 100644 --- a/pkg/time/rate/rate_test.go +++ b/pkg/time/rate/rate_test.go @@ -182,6 +182,9 @@ func TestLongRunningQPS(t *testing.T) { t.Skip("low resolution time.Sleep invalidates test (golang.org/issue/14183)") return } + if runtime.GOOS == "windows" { + t.Skip("test is unreliable on windows") + } // The test runs for a few seconds executing many requests and then checks // that overall number of requests is reasonable. diff --git a/pkg/types/utils.go b/pkg/types/utils.go index 0315465cb..8159d2464 100644 --- a/pkg/types/utils.go +++ b/pkg/types/utils.go @@ -100,7 +100,7 @@ func Clone(a, b interface{}) error { } func WriteStackTrace(iErr interface{}) string { - tmpfile, err := ioutil.TempFile("/tmp/", "crowdsec-crash.*.txt") + tmpfile, err := ioutil.TempFile("", "crowdsec-crash.*.txt") if err != nil { log.Fatal(err) } diff --git a/platform/freebsd.mk b/platform/freebsd.mk index 299bdbb66..48ccdc553 100644 --- a/platform/freebsd.mk +++ b/platform/freebsd.mk @@ -3,3 +3,4 @@ Make=gmake +$(info building for FreeBSD) \ No newline at end of file diff --git a/platform/linux.mk b/platform/linux.mk index b956332bd..0c31e884a 100644 --- a/platform/linux.mk +++ b/platform/linux.mk @@ -2,3 +2,4 @@ MAKE=make +$(info Building for linux) \ No newline at end of file diff --git a/platform/unix_common.mk b/platform/unix_common.mk new file mode 100644 index 000000000..05fba2555 --- /dev/null +++ b/platform/unix_common.mk @@ -0,0 +1,18 @@ + +RM=rm -rf +CP=cp +CPR=cp -r +MKDIR=mkdir -p + +GO_MAJOR_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1) +GO_MINOR_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2) + +BUILD_GOVERSION="$(shell go version | cut -d " " -f3 | sed -E 's/[go]+//g')" + +#Current versioning information from env +BUILD_VERSION?="$(shell git describe --tags $$(git rev-list --tags --max-count=1))" +BUILD_CODENAME="alphaga" +BUILD_TIMESTAMP=$(shell date +%F"_"%T) +BUILD_TAG?="$(shell git rev-parse HEAD)" +DEFAULT_CONFIGDIR?=/etc/crowdsec +DEFAULT_DATADIR?=/var/lib/crowdsec/data \ No newline at end of file diff --git a/platform/windows.mk b/platform/windows.mk new file mode 100644 index 000000000..9cf6f7e8b --- /dev/null +++ b/platform/windows.mk @@ -0,0 +1,32 @@ +# Windows specific +# + +MAKE=make +GOOS=windows +PREFIX=$(shell $$env:TEMP) + +GO_MAJOR_VERSION ?= $(shell (go env GOVERSION).replace("go","").split(".")[0]) +GO_MINOR_VERSION ?= $(shell (go env GOVERSION).replace("go","").split(".")[1]) +MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1 +MINIMUM_SUPPORTED_GO_MINOR_VERSION = 17 +GO_VERSION_VALIDATION_ERR_MSG = Golang version ($(BUILD_GOVERSION)) is not supported, please use least $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION).$(MINIMUM_SUPPORTED_GO_MINOR_VERSION) +#Current versioning information from env +#BUILD_VERSION?=$(shell (Invoke-WebRequest -UseBasicParsing -Uri https://api.github.com/repos/crowdsecurity/crowdsec/releases/latest).Content | jq -r '.tag_name') +#hardcode it till i find a workaround +BUILD_VERSION?=$(shell git describe --tags $$(git rev-list --tags --max-count=1)) +BUILD_GOVERSION?=$(shell (go env GOVERSION).replace("go","")) +BUILD_CODENAME?=alphaga +BUILD_TIMESTAMP?=$(shell Get-Date -Format "yyyy-MM-dd_HH:mm:ss") +BUILD_TAG?=$(shell git rev-parse HEAD) +DEFAULT_CONFIGDIR?=C:\\ProgramData\\CrowdSec\\config +DEFAULT_DATADIR?=C:\\ProgramData\\CrowdSec\\data + +#please tell me there is a better way to completly ignore errors when trying to delete a file.... +RM=Remove-Item -ErrorAction Ignore -Recurse +CP=Copy-Item +CPR=Copy-Item -Recurse +MKDIR=New-Item -ItemType directory +WIN_IGNORE_ERR=; exit 0 + + +$(info Building for windows) diff --git a/plugins/notifications/dummy/Makefile b/plugins/notifications/dummy/Makefile index f45e4115e..0d423e842 100644 --- a/plugins/notifications/dummy/Makefile +++ b/plugins/notifications/dummy/Makefile @@ -1,13 +1,20 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get -BINARY_NAME=notification-dummy +BINARY_NAME=notification-dummy$(EXT) clean: - @$(RM) "$(BINARY_NAME)" + @$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR) build: clean @$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v diff --git a/plugins/notifications/email/Makefile b/plugins/notifications/email/Makefile index 697bea1f2..eb8e4e2ca 100644 --- a/plugins/notifications/email/Makefile +++ b/plugins/notifications/email/Makefile @@ -1,13 +1,20 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get -BINARY_NAME=notification-email +BINARY_NAME=notification-email$(EXT) clean: - @$(RM) "$(BINARY_NAME)" + @$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR) build: clean @$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v diff --git a/plugins/notifications/http/Makefile b/plugins/notifications/http/Makefile index ce1d8626a..3b6d4036a 100644 --- a/plugins/notifications/http/Makefile +++ b/plugins/notifications/http/Makefile @@ -1,16 +1,23 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get -BINARY_NAME=notification-http +BINARY_NAME=notification-http$(EXT) clean: - @$(RM) "$(BINARY_NAME)" + @$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR) build: clean - @$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v + $(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v static: clean $(GOBUILD) $(LD_OPTS_STATIC) -o $(BINARY_NAME) -v -a -tags netgo diff --git a/plugins/notifications/slack/Makefile b/plugins/notifications/slack/Makefile index e07fdd32a..1273121a8 100644 --- a/plugins/notifications/slack/Makefile +++ b/plugins/notifications/slack/Makefile @@ -1,16 +1,22 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get -BINARY_NAME=notification-slack +BINARY_NAME=notification-slack$(EXT) build: clean @$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v clean: - @$(RM) "$(BINARY_NAME)" + @$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR) static: clean diff --git a/plugins/notifications/splunk/Makefile b/plugins/notifications/splunk/Makefile index ae3c65cb9..eb0531e55 100644 --- a/plugins/notifications/splunk/Makefile +++ b/plugins/notifications/splunk/Makefile @@ -1,16 +1,24 @@ +ifeq ($(OS),Windows_NT) +SHELL := pwsh.exe +.SHELLFLAGS := -NoProfile -Command +EXT=.exe +endif + + + # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test GOGET=$(GOCMD) get -BINARY_NAME=notification-splunk +BINARY_NAME=notification-splunk$(EXT) build: clean - @$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v + $(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v clean: - @$(RM) "$(BINARY_NAME)" + @$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR) static: clean $(GOBUILD) $(LD_OPTS_STATIC) -o $(BINARY_NAME) -v -a -tags netgo diff --git a/scripts/check_go_version.ps1 b/scripts/check_go_version.ps1 new file mode 100644 index 000000000..ddc68ce9b --- /dev/null +++ b/scripts/check_go_version.ps1 @@ -0,0 +1,19 @@ +##This must be called with $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) $(MINIMUM_SUPPORTED_GO_MINOR_VERSION) in this order +$min_major=$args[0] +$min_minor=$args[1] +$goversion = (go env GOVERSION).replace("go","").split(".") +$goversion_major=$goversion[0] +$goversion_minor=$goversion[1] +$err_msg="Golang version $goversion_major.$goversion_minor is not supported, please use least $min_major.$min_minor" + +if ( $goversion_major -gt $min_major ) { + exit 0; +} +elseif ($goversion_major -lt $min_major) { + Write-Output $err_msg; + exit 1; +} +elseif ($goversion_minor -lt $min_minor) { + Write-Output $(GO_VERSION_VALIDATION_ERR_MSG); + exit 1; +} \ No newline at end of file diff --git a/scripts/test_env.ps1 b/scripts/test_env.ps1 new file mode 100644 index 000000000..3d8e18ac2 --- /dev/null +++ b/scripts/test_env.ps1 @@ -0,0 +1,90 @@ +#this is is straight up conversion of test_env.sh, not pretty but does the job + +param ( + [string]$base = ".\tests", + [switch]$help = $false +) + +function show_help() { + Write-Output ".\test_env.ps1 -d tests #creates test env in .\tests" +} + +function create_arbo() { + $null = New-Item -ItemType Directory $data_dir + $null = New-Item -ItemType Directory $log_dir + $null = New-Item -ItemType Directory $config_dir + $null = New-Item -ItemType Directory $parser_dir + $null = New-Item -ItemType Directory $parser_s00 + $null = New-Item -ItemType Directory $parser_s01 + $null = New-Item -ItemType Directory $parser_s02 + $null = New-Item -ItemType Directory $scenarios_dir + $null = New-Item -ItemType Directory $postoverflows_dir + $null = New-Item -ItemType Directory $cscli_dir + $null = New-Item -ItemType Directory $hub_dir + $null = New-Item -ItemType Directory $config_dir\$notif_dir + $null = New-Item -ItemType Directory $base\$plugins_dir +} + +function copy_file() { + $null = Copy-Item ".\config\profiles.yaml" $config_dir + $null = Copy-Item ".\config\simulation.yaml" $config_dir + $null = Copy-Item ".\cmd\crowdsec\crowdsec.exe" $base + $null = Copy-Item ".\cmd\crowdsec-cli\cscli.exe" $base + $null = Copy-Item -Recurse ".\config\patterns" $config_dir + $null = Copy-Item ".\config\acquis.yaml" $config_dir + $null = New-Item -ItemType File $config_dir\local_api_credentials.yaml + $null = New-Item -ItemType File $config_dir\online_api_credentials.yaml + #envsubst < "./config/dev.yaml" > $BASE/dev.yaml + Copy-Item .\config\dev.yaml $base\dev.yaml + $plugins | ForEach-Object { + Copy-Item $plugins_dir\$notif_dir\$_\notification-$_.exe $base\$plugins_dir\notification-$_.exe + Copy-Item $plugins_dir\$notif_dir\$_\$_.yaml $config_dir\$notif_dir\$_.yaml + } +} + +function setup() { + & $base\cscli.exe -c "$config_file" hub update + & $base\cscli.exe -c "$config_file" collections install crowdsecurity/linux crowdsecurity/windows +} + +function setup_api() { + & $base\cscli.exe -c "$config_file" machines add test -p testpassword -f $config_dir\local_api_credentials.yaml --force +} + +if ($help) { + show_help + exit 0; +} + +$null = New-Item -ItemType Directory $base + +$base=(Resolve-Path $base).Path +$data_dir="$base\data" +$log_dir="$base\logs\" +$config_dir="$base\config" +$config_file="$base\dev.yaml" +$cscli_dir="$config_dir\crowdsec-cli" +$parser_dir="$config_dir\parsers" +$parser_s00="$parser_dir\s00-raw" +$parser_s01="$parser_dir\s01-parse" +$parser_s02="$parser_dir\s02-enrich" +$scenarios_dir="$config_dir\scenarios" +$postoverflows_dir="$config_dir\postoverflows" +$hub_dir="$config_dir\hub" +$plugins=@("http", "slack", "splunk") +$plugins_dir="plugins" +$notif_dir="notifications" + + +Write-Output "Creating test arbo in $base" +create_arbo +Write-Output "Arbo created" +Write-Output "Copying files" +copy_file +Write-Output "Files copied" +Write-Output "Setting up configuration" +$cur_path=$pwd +Set-Location $base +setup_api +setup +Set-Location $cur_path \ No newline at end of file diff --git a/tests/bats.mk b/tests/bats.mk index d648e680b..03af05a14 100644 --- a/tests/bats.mk +++ b/tests/bats.mk @@ -87,7 +87,10 @@ bats-fixture: # Remove the local crowdsec installation and the fixture config + data bats-clean: - @$(RM) -r $(LOCAL_DIR) $(LOCAL_INIT_DIR) $(TEST_DIR)/dyn-bats/*.bats tests/.environment.sh + @$(RM) $(LOCAL_DIR) $(WIN_IGNORE_ERR) + @$(RM) $(LOCAL_INIT_DIR) $(WIN_IGNORE_ERR) + @$(RM) $(TEST_DIR)/dyn-bats/*.bats $(WIN_IGNORE_ERR) + @$(RM) tests/.environment.sh $(WIN_IGNORE_ERR) # Run the test suite bats-test: bats-environment bats-check-requirements diff --git a/windows/Chocolatey/crowdsec/ReadMe.md b/windows/Chocolatey/crowdsec/ReadMe.md new file mode 100644 index 000000000..431c518a1 --- /dev/null +++ b/windows/Chocolatey/crowdsec/ReadMe.md @@ -0,0 +1,133 @@ +## Summary +How do I create packages? See https://docs.chocolatey.org/en-us/create/create-packages + +If you are submitting packages to the community feed (https://community.chocolatey.org) +always try to ensure you have read, understood and adhere to the create +packages wiki link above. + +## Automatic Packaging Updates? +Consider making this package an automatic package, for the best +maintainability over time. Read up at https://docs.chocolatey.org/en-us/create/automatic-packages + +## Shim Generation +Any executables you include in the package or download (but don't call +install against using the built-in functions) will be automatically shimmed. + +This means those executables will automatically be included on the path. +Shim generation runs whether the package is self-contained or uses automation +scripts. + +By default, these are considered console applications. + +If the application is a GUI, you should create an empty file next to the exe +named 'name.exe.gui' e.g. 'bob.exe' would need a file named 'bob.exe.gui'. +See https://docs.chocolatey.org/en-us/create/create-packages#how-do-i-set-up-shims-for-applications-that-have-a-gui + +If you want to ignore the executable, create an empty file next to the exe +named 'name.exe.ignore' e.g. 'bob.exe' would need a file named +'bob.exe.ignore'. +See https://docs.chocolatey.org/en-us/create/create-packages#how-do-i-exclude-executables-from-getting-shims + +## Self-Contained? +If you have a self-contained package, you can remove the automation scripts +entirely and just include the executables, they will automatically get shimmed, +which puts them on the path. Ensure you have the legal right to distribute +the application though. See https://docs.chocolatey.org/en-us/information/legal. + +You should read up on the Shim Generation section to familiarize yourself +on what to do with GUI applications and/or ignoring shims. + +## Automation Scripts +You have a powerful use of Chocolatey, as you are using PowerShell. So you +can do just about anything you need. Choco has some very handy built-in +functions that you can use, these are sometimes called the helpers. + +### Built-In Functions +https://docs.chocolatey.org/en-us/create/functions + +A note about a couple: +* Get-BinRoot - this is a horribly named function that doesn't do what new folks think it does. It gets you the 'tools' root, which by default is set to 'c:\tools', not the chocolateyInstall bin folder - see https://docs.chocolatey.org/en-us/create/functions/get-toolslocation +* Install-BinFile - used for non-exe files - executables are automatically shimmed... - see https://docs.chocolatey.org/en-us/create/functions/install-binfile +* Uninstall-BinFile - used for non-exe files - executables are automatically shimmed - see https://docs.chocolatey.org/en-us/create/functions/uninstall-binfile + +### Getting package specific information +Use the package parameters pattern - see https://docs.chocolatey.org/en-us/guides/create/parse-packageparameters-argument + +### Need to mount an ISO? +https://docs.chocolatey.org/en-us/guides/create/mount-an-iso-in-chocolatey-package + +### Environment Variables +Chocolatey makes a number of environment variables available (You can access any of these with $env:TheVariableNameBelow): + + * TEMP/TMP - Overridden to the CacheLocation, but may be the same as the original TEMP folder + * ChocolateyInstall - Top level folder where Chocolatey is installed + * ChocolateyPackageName - The name of the package, equivalent to the `` field in the nuspec (0.9.9+) + * ChocolateyPackageTitle - The title of the package, equivalent to the `` field in the nuspec (0.10.1+) + * ChocolateyPackageVersion - The version of the package, equivalent to the `<version />` field in the nuspec (0.9.9+) + * ChocolateyPackageFolder - The top level location of the package folder - the folder where Chocolatey has downloaded and extracted the NuGet package, typically `C:\ProgramData\chocolatey\lib\packageName`. + +#### Advanced Environment Variables +The following are more advanced settings: + + * ChocolateyPackageParameters - Parameters to use with packaging, not the same as install arguments (which are passed directly to the native installer). Based on `--package-parameters`. (0.9.8.22+) + * CHOCOLATEY_VERSION - The version of Choco you normally see. Use if you are 'lighting' things up based on choco version. (0.9.9+) - Otherwise take a dependency on the specific version you need. + * ChocolateyForceX86 = If available and set to 'true', then user has requested 32bit version. (0.9.9+) - Automatically handled in built in Choco functions. + * OS_PLATFORM - Like Windows, OSX, Linux. (0.9.9+) + * OS_VERSION - The version of OS, like 6.1 something something for Windows. (0.9.9+) + * OS_NAME - The reported name of the OS. (0.9.9+) + * USER_NAME = The user name (0.10.6+) + * USER_DOMAIN = The user domain name (could also be local computer name) (0.10.6+) + * IS_PROCESSELEVATED = Is the process elevated? (0.9.9+) + * IS_SYSTEM = Is the user the system account? (0.10.6+) + * IS_REMOTEDESKTOP = Is the user in a terminal services session? (0.10.6+) + * ChocolateyToolsLocation - formerly 'ChocolateyBinRoot' ('ChocolateyBinRoot' will be removed with Chocolatey v2.0.0), this is where tools being installed outside of Chocolatey packaging will go. (0.9.10+) + +#### Set By Options and Configuration +Some environment variables are set based on options that are passed, configuration and/or features that are turned on: + + * ChocolateyEnvironmentDebug - Was `--debug` passed? If using the built-in PowerShell host, this is always true (but only logs debug messages to console if `--debug` was passed) (0.9.10+) + * ChocolateyEnvironmentVerbose - Was `--verbose` passed? If using the built-in PowerShell host, this is always true (but only logs verbose messages to console if `--verbose` was passed). (0.9.10+) + * ChocolateyExitOnRebootDetected - Are we exiting on a detected reboot? Set by ` --exit-when-reboot-detected` or the feature `exitOnRebootDetected` (0.11.0+) + * ChocolateyForce - Was `--force` passed? (0.9.10+) + * ChocolateyForceX86 - Was `-x86` passed? (CHECK) + * ChocolateyRequestTimeout - How long before a web request will time out. Set by config `webRequestTimeoutSeconds` (CHECK) + * ChocolateyResponseTimeout - How long to wait for a download to complete? Set by config `commandExecutionTimeoutSeconds` (CHECK) + * ChocolateyPowerShellHost - Are we using the built-in PowerShell host? Set by `--use-system-powershell` or the feature `powershellHost` (0.9.10+) + +#### Business Edition Variables + + * ChocolateyInstallArgumentsSensitive - Encrypted arguments passed from command line `--install-arguments-sensitive` that are not logged anywhere. (0.10.1+ and licensed editions 1.6.0+) + * ChocolateyPackageParametersSensitive - Package parameters passed from command line `--package-parameters-senstivite` that are not logged anywhere. (0.10.1+ and licensed editions 1.6.0+) + * ChocolateyLicensedVersion - What version is the licensed edition on? + * ChocolateyLicenseType - What edition / type of the licensed edition is installed? + * USER_CONTEXT - The original user context - different when self-service is used (Licensed v1.10.0+) + +#### Experimental Environment Variables +The following are experimental or use not recommended: + + * OS_IS64BIT = This may not return correctly - it may depend on the process the app is running under (0.9.9+) + * CHOCOLATEY_VERSION_PRODUCT = the version of Choco that may match CHOCOLATEY_VERSION but may be different (0.9.9+) - based on git describe + * IS_ADMIN = Is the user an administrator? But doesn't tell you if the process is elevated. (0.9.9+) + * IS_REMOTE = Is the user in a remote session? (0.10.6+) + +#### Not Useful Or Anti-Pattern If Used + + * ChocolateyInstallOverride = Not for use in package automation scripts. Based on `--override-arguments` being passed. (0.9.9+) + * ChocolateyInstallArguments = The installer arguments meant for the native installer. You should use chocolateyPackageParameters instead. Based on `--install-arguments` being passed. (0.9.9+) + * ChocolateyIgnoreChecksums - Was `--ignore-checksums` passed or the feature `checksumFiles` turned off? (0.9.9.9+) + * ChocolateyAllowEmptyChecksums - Was `--allow-empty-checksums` passed or the feature `allowEmptyChecksums` turned on? (0.10.0+) + * ChocolateyAllowEmptyChecksumsSecure - Was `--allow-empty-checksums-secure` passed or the feature `allowEmptyChecksumsSecure` turned on? (0.10.0+) + * ChocolateyChecksum32 - Was `--download-checksum` passed? (0.10.0+) + * ChocolateyChecksumType32 - Was `--download-checksum-type` passed? (0.10.0+) + * ChocolateyChecksum64 - Was `--download-checksum-x64` passed? (0.10.0)+ + * ChocolateyChecksumType64 - Was `--download-checksum-type-x64` passed? (0.10.0)+ + * ChocolateyPackageExitCode - The exit code of the script that just ran - usually set by `Set-PowerShellExitCode` (CHECK) + * ChocolateyLastPathUpdate - Set by Chocolatey as part of install, but not used for anything in particular in packaging. + * ChocolateyProxyLocation - The explicit proxy location as set in the configuration `proxy` (0.9.9.9+) + * ChocolateyDownloadCache - Use available download cache? Set by `--skip-download-cache`, `--use-download-cache`, or feature `downloadCache` (0.9.10+ and licensed editions 1.1.0+) + * ChocolateyProxyBypassList - Explicitly set locations to ignore in configuration `proxyBypassList` (0.10.4+) + * ChocolateyProxyBypassOnLocal - Should the proxy bypass on local connections? Set based on configuration `proxyBypassOnLocal` (0.10.4+) + * http_proxy - Set by original `http_proxy` passthrough, or same as `ChocolateyProxyLocation` if explicitly set. (0.10.4+) + * https_proxy - Set by original `https_proxy` passthrough, or same as `ChocolateyProxyLocation` if explicitly set. (0.10.4+) + * no_proxy- Set by original `no_proxy` passthrough, or same as `ChocolateyProxyBypassList` if explicitly set. (0.10.4+) + diff --git a/windows/Chocolatey/crowdsec/crowdsec.nuspec b/windows/Chocolatey/crowdsec/crowdsec.nuspec new file mode 100644 index 000000000..bcde3278b --- /dev/null +++ b/windows/Chocolatey/crowdsec/crowdsec.nuspec @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. --> +<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"> + <metadata> + <id>crowdsec</id> + <version>1.3.3</version> + <packageSourceUrl>https://github.com/crowdsecurity/crowdsec</packageSourceUrl> + <owners>CrowdSecurity</owners> + <!-- ============================== --> + + <!-- == SOFTWARE SPECIFIC SECTION == --> + <title>CrowdSec + CrowdSecurity + https://crowdsec.net/ + CrowdSec, 2022 + https://github.com/crowdsecurity/crowdsec/blob/main/LICENSE + true + https://github.com/crowdsecurity/crowdsec + https://docs.crowdsec.net + https://github.com/crowdsecurity/crowdsec/issues + crowdsec crowdsecurity security ips ids + CrowdSec IDS + + CrowdSec is a free, modern and collaborative behavior detection engine, coupled with a global IP reputation network. + It stacks on fail2ban's philosophy but is IPV6 compatible and 60x faster (Go vs Python), uses Grok patterns to parse logs and YAML scenario to identify behaviors. + CrowdSec is engineered for modern Cloud / Containers / VM based infrastructures (by decoupling detection and remediation). Once detected you can remedy threats with various bouncers (firewall block, nginx http 403, Captchas, etc.) while the aggressive IP can be sent to CrowdSec for curation before being shared among all users to further improve everyone's security. + + ### Package Specific + #### Package parameters + + - AgentOnly: If set, the local API will be disabled. You will need to register the agent in LAPI yourself and configure the service to start on boot. + + + + + + + + + + + + diff --git a/windows/Chocolatey/crowdsec/tools/LICENSE.txt b/windows/Chocolatey/crowdsec/tools/LICENSE.txt new file mode 100644 index 000000000..5c6536954 --- /dev/null +++ b/windows/Chocolatey/crowdsec/tools/LICENSE.txt @@ -0,0 +1,26 @@ + +From: https://github.com/crowdsecurity/crowdsec/blob/master/LICENSE + +LICENSE + +MIT License + +Copyright (c) 2022 crowdsec + +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. \ No newline at end of file diff --git a/windows/Chocolatey/crowdsec/tools/VERIFICATION.txt b/windows/Chocolatey/crowdsec/tools/VERIFICATION.txt new file mode 100644 index 000000000..eb16249b8 --- /dev/null +++ b/windows/Chocolatey/crowdsec/tools/VERIFICATION.txt @@ -0,0 +1,9 @@ + + +VERIFICATION +Verification is intended to assist the Chocolatey moderators and community +in verifying that this package's contents are trustworthy. + +This package is published by CrowdSecurity itself. The MSI is identical to the one published in the github releases for the project. +You can download the MSI from the latest release or pre-release here: https://github.com/crowdsecurity/crowdsec/releases +The MSI is also digitally signed. \ No newline at end of file diff --git a/windows/Chocolatey/crowdsec/tools/chocolateybeforemodify.ps1 b/windows/Chocolatey/crowdsec/tools/chocolateybeforemodify.ps1 new file mode 100644 index 000000000..bb71f3b1c --- /dev/null +++ b/windows/Chocolatey/crowdsec/tools/chocolateybeforemodify.ps1 @@ -0,0 +1 @@ +Stop-Service crowdsec \ No newline at end of file diff --git a/windows/Chocolatey/crowdsec/tools/chocolateyinstall.ps1 b/windows/Chocolatey/crowdsec/tools/chocolateyinstall.ps1 new file mode 100644 index 000000000..6c817a034 --- /dev/null +++ b/windows/Chocolatey/crowdsec/tools/chocolateyinstall.ps1 @@ -0,0 +1,29 @@ +$ErrorActionPreference = 'Stop'; +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +$fileLocation = Join-Path $toolsDir 'crowdsec.msi' + +$silentArgs = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`"" + + +$pp = Get-PackageParameters + +if ($pp['AgentOnly']) { + $silentArgs += " AGENT_ONLY=1" +} + + +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + unzipLocation = $toolsDir + fileType = 'msi' + file64 = $fileLocation + softwareName = 'Crowdsec' + silentArgs = $silentArgs + validExitCodes= @(0, 3010, 1641) +} + +Install-ChocolateyInstallPackage @packageArgs + +if ($pp['AgentOnly']) { + Write-Host "/AgentOnly was specified. LAPI is disabled, please register your agent manually and configure the service to start on boot." +} \ No newline at end of file diff --git a/windows/Chocolatey/crowdsec/tools/chocolateyuninstall.ps1 b/windows/Chocolatey/crowdsec/tools/chocolateyuninstall.ps1 new file mode 100644 index 000000000..ec6ec3dcf --- /dev/null +++ b/windows/Chocolatey/crowdsec/tools/chocolateyuninstall.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = 'Stop'; +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + softwareName = 'Crowdsec' + fileType = 'MSI' + silentArgs = "/qn /norestart" + validExitCodes= @(0, 3010, 1605, 1614, 1641) +} + +[array]$key = Get-UninstallRegistryKey -SoftwareName $packageArgs['softwareName'] + +if ($key.Count -eq 1) { + $key | % { + $packageArgs['file'] = "$($_.UninstallString)" + if ($packageArgs['fileType'] -eq 'MSI') { + $packageArgs['silentArgs'] = "$($_.PSChildName) $($packageArgs['silentArgs'])" + $packageArgs['file'] = '' + } else { + } + + Uninstall-ChocolateyPackage @packageArgs + } +} elseif ($key.Count -eq 0) { + Write-Warning "$packageName has already been uninstalled by other means." +} elseif ($key.Count -gt 1) { + Write-Warning "$($key.Count) matches found!" + Write-Warning "To prevent accidental data loss, no programs will be uninstalled." + Write-Warning "Please alert package maintainer the following keys were matched:" + $key | % {Write-Warning "- $($_.DisplayName)"} +} \ No newline at end of file diff --git a/windows/install_dev_windows.ps1 b/windows/install_dev_windows.ps1 new file mode 100644 index 000000000..d3eaad57d --- /dev/null +++ b/windows/install_dev_windows.ps1 @@ -0,0 +1,7 @@ +#install choco +Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +choco install -y golang +choco install -y jq +choco install -y git +choco install -y mingw +refreshenv diff --git a/windows/install_installer_windows.ps1 b/windows/install_installer_windows.ps1 new file mode 100644 index 000000000..a2ae1d555 --- /dev/null +++ b/windows/install_installer_windows.ps1 @@ -0,0 +1,2 @@ +choco install -y wixtoolset +$env:Path += ";C:\Program Files (x86)\WiX Toolset v3.11\bin" \ No newline at end of file diff --git a/windows/installer/WixUI_HK.wxs b/windows/installer/WixUI_HK.wxs new file mode 100644 index 000000000..38cccbd9d --- /dev/null +++ b/windows/installer/WixUI_HK.wxs @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 1 + "1"]]> + + 1 + + NOT Installed + Installed AND PATCH + + 1 + 1 + NOT WIXUI_DONTVALIDATEPATH + "1"]]> + WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" + + 1 + 1 + + Installed + + 1 + + 1 + 1 + 1 + + Updating Hub content + Installing Windows collection + Registering agent to local API + Registering to Crowdsec central API + + + + + + + + + + + + diff --git a/windows/installer/crowdsec_icon.ico b/windows/installer/crowdsec_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..93763e2bd672bb1db1393ea01c84c34d4e1463a2 GIT binary patch literal 4286 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 T1W14cNPq-LfCNY&G=cvEN}mi6 literal 0 HcmV?d00001 diff --git a/windows/installer/installer_dialog.bmp b/windows/installer/installer_dialog.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1e32ba7d9cbd8334302cbba45a7622731b6706b5 GIT binary patch literal 155914 zcmeIb4_s4Mw)cG==rEJa%{vt$#~K0CMoJESlXof*Cl{vRH0d#&D`~DU?(bP^uf6u#YtQ@#r7MJ^@|$}&t`M(Z{5Ou9fuH}zeTRMi z%l{t2MSPcIKlqToKpS1geYClWn=|b}uJ7VyuJVchms|Y9$GDZV=5YT! zBaK`5a0=?-iA99z!`I;;HaRztlXgl}%kLPk9HEiY)gB*=RD5!{_#)TbN`QtyV(8#x8?^HuJqB_-15g9+#B|vajR{a z+@_ZobLBst!`-vCmy7$4KCY{ND|dR|LGH)VQ#q?Hj{9Y*$eqCX`Q;ZlkJ-wV&i)w} z`R{$)H>W@4erL(xdOJ>X5A6M#`|3|ext~AuFt;+@!M&6`lZ#%_&VBvi$J}qM>74zc z?{kG<^lHKnxxPQ1;ChaCaElUu#Ql2a<6Q4wzu->4vxEEm_nW!n|6b4C^KV~svE_f} zKB?c%mHzl2xr_gGkuz5u=XSmP8?O7WUve+cdV+K74cx^;N4VD4R&xKJPR|wn!#wWG zgNL{?-}G_O-d66DclU5#g5|^OKI7WUf6G1p<9S^7?)N!Y`p>x^M#XT4{_S_1{eB&H z{-eKe_O-{jJ*BU4KdL#!>1)q%Z~n~1{jq5$_dn7e=YATa<39P|C|8-C$F+a?HTQSx zy1BF4c5$Z~H*tUY-A3-z+JECZPkh9EeE5%C{r_CZCD)zh!Yj{knZN%FSNu~K_t(Fi z;c6fM8CPbpabf!$7ku>hfAEtBn|6liV z?|1id*1xxN74u)4;xhy>Ed#l!R>2K`kezW&KxugGafLr~` zf8iEy?%=AowsSvw=6`X2`Lvt+O-?R%=EB$9KV|%!`_~^ixJ|1nIC1Gt?%3fI+&#a( z%%#nGihKL;m)xfR?B!meIbIyG`naMvg7z*Q9k z43?nnD{~224FrwZ%^WyrcPk(GX0QZpUl~i#Y9MILZsx#2yIc9dH-jZ;`^s2?Rs%s} zb~6VK+TF?rz8Ne*+gHXCv>FH+vzs|^(C$_~@XcTe+P*TDpw&RonBB~QgLb#_fo}#& z(Ds$F1g!>w#_VPe9JITY4}3FNg0`=WC1^DeG-fw*;Go^DeBhhG6106~EJ3S*pfS6d z0|)JHw8VDM*n>ldM?p8kV4X`Mc{TnDbWrRw3P3ixB z4ByCDf>r}Jprj0yfGfjQ;nS59=fs2#Dp#I|8ysmQUkB}O7Ko) zDyz+;d!f6s_>ZUDr^9C4hf&LVBVbBvmmv*-@_kdbl=<_RdYvP#V|lroJ5oZ)sTpYn3#w~AxTM#7A=bLfwrmOZc{Sm z!yMu!KE5Ms`u4b~;i)j-;i*azW0fi?1Z*Q_snja9S{WWQEi^SX6~32H`g-_djPje? zqiXKRs~Cm|X=_s3YBi^fp`wfl2k!_Kxl@&~v9Y1}QPa;b2e_$^@5q|IJ#K0kF8*dY zbxKTXs9GJX4xOTeU@L)|5vM;wW2Q|DSF)yjn4^sLL zTWq2&Ha0Od5wA8iD@uH>Qim#u_i)D;_2+kFP2V0jH4GD|?oep-2%9Q&>g0Q8I7OXK zAqaw2D+s*Z<#IYF+&3*2av!Uv#(kKBjQZo-qiXKRs~DDvDrrtq%=esjo_F&+uTby` ztyZfrIUMx2Lx<0J-sQYEG$|>GfQJuzpiz8rN7nT1aZ?fv6>{t{K@DNj3KgC>d4khL zREhGHa^*cf7X*b)CpsUS3cD>RXzOhbeZ?o!tHCDVt`jFrR@>C6Q((P69KuaWOicV0 zfTt=$2509EMBHz)I=YiCN2-1}%Sab1m8#e&4>(;8L1BVzrntU*vnCwdC;(g#V8lHZ zs-7M?H8B*rJ@KW)U;QdIF)>u;eS1AE5^pI=X}T(Q>b)+VTOg7|5#>7K?c>e1(&+@P z&gJ|8nsSJCsv5tEcPdIC0zGJ(+pCK1sHH1XDwroG4q_R|pLbLZ-(ELE)vr{lr=#JWcCU*U zOfb213I#)r_t(WOsQ?qN6BUA01a1Yda#)91ctK}*kwC@S!vV0*!BzdsM7ry(gRDT#@*LS){z z*W)6Iw|eRfqHRKN+oXZG!m$Dg=ZQBp2-yX}Q9%u1S_k`kJz z3W=5V`E3Opyx|Xj%*jI;^Dwr3eeV>WAYM4jDjcTRmcB_(3o z^dxl7MqX~K8oIMC1~5YJMF)&jx#C*hI)zq}-y6_Ypsy~NHKN{3$kC3QfAKJUEfLda zzc6y#+?`d$x7FQ%h7(OTj4q%xf$AU^3-r2<51QfCacml`@BO$**fsrTj*wXC~tO*8vPQuo$Kb`aNESdN1 zM4S0jbpk}2b)_W_%0RF@#8xn2++^T~L%tV8omDI9wtE-UmgQ>_1bx=l1efjM7iO~& zn2`zj?Nvp0)YSk4FVeuami4EI_&}UG!3wh*Lni}29P+(R5JcW8xR&L1?{3uUUA)F% zNH9M(XBK#mbb$Phs^Qz~X29D7`G!l4T5uDS@PW*&y6=8>NKHQ&x@a}I6x+o)3%fr$ z+U`nls>HOuamdEaKd{TT6@m1VZ)JG2HvK|z5D zy)^yYNz1x>Kl&n{$HxvsmU$AibdJZv8ypVv+__l>yIU|#nmae4FyCR-=e(Wr+u!yr ziQfFx@qEElP=M`o>~>uuVjQRV;+7*)r=;Q$yFs~~D(DWo7S#Lz-kP~N4sd0>G2$hf z6CPgJeck_}i>iBlLovV49a(KcGCmXMGSao7T(YLzy zezxUgMOnUG$7?ipH+*o40=vc_in<)VC|Xl`cjsHpa}%_J%j)@gZtwYZb^-}7 z-O}?FWhR}d6?6_V-+2X;dX7oqaJvh0`@Z?2%x=!o>)iRj&F%ecQ{Cd{qL+SBfzJ#Y zSp6^qt-L8#9ivim6dV_jseXsm@$GawgLw0boScFj@{nnD1qIu;z1H1#Ze88xLkG&{ znhj<_0gH}k8_ecOI<4ECpVHm;?)i#*vtDm?FU##cyQ*&S;>FRcK08ouhnmnjz#IKJ zosei_iGRR5kbsxlsed9CyGiGizaY-_q!(Q0mt0_^RBkMYCXlfSX7!@KX?|j&nh!2O!~h zSRLO^w{tabgdY;{9K%+%e>Q;-{ilovdJHzQMr5sji_`Jkh46sAnj3{JKH8ohs-KyB5^^ z0Q($0?wkU<$Icr>hbO0C!Lr_y>*`2k*Bv_G#b5~HS9r!dCr2Y_`MiaDKZ}MqAF3!T z5Ek|_+ys2_<{hxm$<7CF7=L(9M1Cr!5OWbS?_2u-NxUUr3OpF@g@Q_v+}U7T2x%q}*Lletzv{z^12TarDxo<@vlp zZ@|zwji@7FSz!lo1h)q=9)4@9ajKW~UO1*kY|k9J=wFLNl|`DJj0qc_bEE{@&- zrS0&zF+Q4O(4g%`2y)@V?yokHJkyt+{%Gks zGRU7=b@D)Y`T14RcuV5_RP?%&2g(G6J_p@4l5adE=*2mwC51!;(0t4B4#9gS`rB_w zyxoP5cAt44{*rvByReeHCxnwQq6II<;5N0$;0-UF;91svW)svozKve{RYk>DYwPIY zppB!~?KoO4phu_6A>1e!M;Bc8s#WB7xnT+3?cViA?&r<4IXcw2-6vOVI&}27x1iwQ znfDn^04Eg3d)XxH>qbT=ypNs(G{~7K)zwvITv(8 zq9xu>Enau%qciWXf-MjFv_mxcU<(S&Ry`gh79?dtl)7uGDmH+O%E7zcyZ*?7dFOWb zZGwDKgX}5$=rVY-{unUA2K%T{Z_wJ|htX<9(}ZQc7vMc5@dn}O)o0JVzl3-LHh$1! z2k#t%)j_;T!r`E`^532|J%EfacRiKhRlwD}?Xz>cp`+2E+605Etm2!^tT`sBtwSSy zrYwi?&LVHCr}oSaZ0aLiw`#|kci~NC+tU-U6oYC7W`n>y?j+$RtqHRVCrk;zU<=;u z-t|ZJ^JeB6qLI3Lx3D&Q-KIldlvjMQge)*}n&Gu>)u9TvK|$^}gBHWvjYpTR18xR< zab5JqKFBxYPq<;v!wF;7Ykj;Cy^4rNh1UKU(s@mfP)+B2+=m)3yX&b0uL30AU_s*= z0<81CP3UoxIIr7rwj%%JD%Pi_PXKn+(Fzk8-8yu>?K%0!&u<}Rk7ohpxh1`yQ2R|E z5b)?FXUlU6=F*s`R9Gmxpmipyrm3eTsbZ3(k&~eY#DjOccm0ti-lB;{LbSqtmA&V_ zmV}fH@&miiQj;t(#G7@i&$#V5h)gu-Nuie?J;dxe!u`}!oBDc5pVOm3&qm#<&&ttr zqyBn-p;PFbp|;R)7;S7ceCYA;yPit$DnR0m`M>xl=w@~It&NsEY5=$D3Ly27bbu&i29^$Aq0WM~A(_%}*LI^Q z+55tW9~lka?cViAmUs(1O*ar+3z&6I2mu+L=nH4hp-pDI>33c9=Ei*UBtb|Z-k!4K z2X>$Zr?2sU-O1i_7(1msnS5_P^o2vr(fQRmIoL4o5OYb zv3beT^T+cQ2|2EQZH_S(jHw~=d4_G8IyUAycKR)-hr8vJetizngF)r3xQqc1l2E3? zVWC}Exe6|H6cXIcFK+6Q5 zCRzI@DZxY_0aqvldu*aQGzMoLuG}q^e+%vtRSpC!0$s;j-Nz5Cr2$T|&x!j}br%uO zgdy(5bqDQt^C46$oKs0!w-tkt%1;E{Tq=JN@G4dsY;ur(2AzDd4R~9#!ODG#JXx zpvOkqpl>jn9)YjD#XB=KIP)s}e&59_c;zM41Y_PfdtY$2~X zHR7bl*PiXW@b!x;5AE8yGc&z7Jt{fbTU6k(V?GH_GyC(8e}eH9;C^xKADer>*|2j( zW>kE9Mn-04ag?RH)|;PywQ$p4CImMjzqZVKSSrE$;%eTye7heUgQAW$F z%ZB2O&WGJcH}z>?A{8@5`k#5792+Tx$~Dl-jf*>6}KWY%orIT zZ&{y*$-Z_T(+Ul|-2)2`M%vm_D25ojX;&9$c!77yyl=^~ zcNK4~1M=;%mmN5{iS@wIAFErr<9K0)nj1ZvpYEc!P!2m5|^p zqc}xs!=b0QeEQ`V6-Hxv9MpN7(U_SI@}0GIptCc?9yJN5wtTi z0g{+oO1-X}EQ7+HZ&ze(70ZsFIkO@iCfeTI%8p~$jC6WjTv!->lAk%~Egv<#`sIMppM)(8ff;k700M>Mlj=9T!Qha-1;?65d+t5#d8e zxVnf4mi)5v9%Dv&9C%+UO;3+R8y^)J83uzbDk^QS%Vot>a+97?K~Q89;#{z5g2Fyw z&UaK1NCPlLyy~r}h`Z#R;7vK}(G~-TL#G38OzhX@SDgHsTxuU5TalT5skces+Ks$x>HCc?FP{Vu8dTXCDHY?|hU03~oxm~zZnX3XMs&`8Lt%`5*(F15Hz z&6fx_;~iy;OOK4QY%KFq+7GN{ff;2czR=rhjJNF0t?XvJBT2gBCEleA-nMIzY7>mX zn`R%7vmRNu`DqU;m8nCnbaN{z;x0KS@z&5kngn7pnX|I8K-*D3L!C>u?Al4q_NC@_ z*xXbiBcqIwnQ`$JONY16fqg_I`9ZL%Jr7%0%5y6#>(e3FjCWYPF$}w*_vYb|>Vak& zl6a%_Cf<K3mTeKIX=r zwoo;eoRxWxGj9`&D3(wUQR2X@p+roR=4#6-zWDN3dL-k$`v`bPMnyqO#{+jHe@BZYTGQ9w#wYxs?zv42{(FU>G2jg+1hI{EhRxjE)a+}6bTvZ$h|4> ziQpX}^Bzy!%syvLD{Eev9I{kfbIp#j-EF0oC^*6|H6QGt9u{fzF!ZXy8{;M&EsfqH zj|21hvkVFDyz-gJ#Ji`-#~aOj7>I`%Ej20m0(jdUJPoiwAwZ!MZ}+!jZAvVR1Qj!= zjB5?tT_;0LgETxR4hJRU(eM}v;T3b|PAU+z`6*SU=y*3BZ_Y*YEb)$uOpgTeVq=u? z`&BKCd4)RBkf5LBF0B2vr7d@1W!V!@<0RmCEFz1c|I_1*jd@l~BSUYG@n+pIggzjn z6ILIVd5`b&2HqGHbHhZV0`lNZQ-%r(90q;i3tegH>Gkc+mF3BP-jLg3G|_09jmGL% z+Z&5KIzGXm_0%>#-(I=!o`Xk-H&lKk9t>FN=&l=UQtUbn_3nMVDRL1>9R)|I`r(M_ zvV4yxaqu>Y$e@b6SxBiS*ii^N0#jhMxtxthk~21zH|Lf=7?p{BcYHiR!jTe2iW+ zwcVTNal5QecSB?D!h3#OL%|NjnhI4W2Gy!k@JyiDVBK$%L>s(yP8(LZ!xaoJw3}Rl zQUj=JQ48~#d@jha2IqMW8PW=jjKT>c7S-0Ibu7GRVQx7LHQ2@QBIDlgSj7oC}Gl7z=aT8ve-9yi>!I5PQLe7qsySV36kJ+2QTc-Izs z9D)gJ3n2=T$1hCjM}ky?NZ4J4`AB-a?6!Z@q`6g$IbbSOI(vDtsRB~x~twW!c zlVAJW+n6nhdcUW<{5$}tOq0^(HN^A23 zgW%47bXjF3Tr+T`V%P*SP8SXvFDxDcPP}2G59FvDPW9$lDw9OihK5%Fu%uqeXQ5cmK?J&FW9%=oCs>asT; zeC@T|Bam+lr;$O&N_=E-dYHxQfLrEj-WZd!t0QFl++R`S@=CmuEzhj?7I_N`i#%Sh z)Ai0f&KVx0JkBXF=`fOF7S_N0t$QjL!WABtkr73_5$TeSRvC7ik$A^t#wAzH!%C8k z*{}U;Wmhr6BA9&pcw6E!jmE0`T-Wf{3%bzoS#p1DTtNFy0^S=oY&e$L)zjA1^=0t~ zA2fA!T`DiHtt}MvV!|YtZCdl(?fZUSxv<`t9B)B?49c7gbci;D8%A4k28A-l9rU<$ zR`+Y)%Izs-@oNZfk+wn$9~Kv8EU$IL#lnji8ifa)+HkYpsxHIh&%WktXYIUxjDbFw(__IiW_0c1QC#_K)}tXoKzULNKm(EAig2 zVdqXFzGu&=k9UFpu?;JVOUu13O^(@>W1c(7@pcOuaEeHd^s~;Oxd$MMQ8Qyye7do% z`O$;R7A#uUyzoe}g~g^a-ZXFm-eDGFOq+Xy?$YJR2U`!qNHa~&ztl*3^U`g zxZ=#>jOymfmiF@Y+REG-OBC~{67DF(s3JN74MCVCxy9?@wU{01mv4X<1RJGt8C>oz z1mk+Q#Czw44>s)FvuD@47e9RVLzL4O&waWR+P%XIe;cBev{t(lL8xuUj5x+SgCI&q zTNGM-l=O_s=1WbbO{IuPF0~YsGZlUq()=*y;}Eq7=ex1WTLdS%h=rpidkwBOK}Z|| zx7_~@-Xj3s8{Q*bzUR~*FA!{2p1t@krf@^RHC6}aoa8&)c8|9$IT^8Q{q{L%VqPE~ z0b_Ay+NI|1(qyBtqPcvdB??ZsI1GIgZ|b~Z{y`KBxcId8JeL-0j1J(fRoshpY_2K6 zyWP9~$i$nKJ$nxCIPK^C;fEKIp&|298EEHY@(@a4*)ufjQ<~Z>g}M-KsBVO;mNqwU z#8_EPbLGb3_%L$1GVzvp7sE4ymLSqtwZ2fRr-bPJ@~z-!B+0zT^C4usnXR_r)Zq&k z`-xv%y=TLUj`BiRq1Wp{##Fnvz0?TWd-m+viTV8GaDgR;3E>&&mm%V5H|C95_BQvV zfi)E--i$ZJ#TdamF21^Lk)S~jd;o7@hU{M(Pu$f1-nsKVKs*NH{L{}jJ?kTW`ogIV znMcaK`7JGtMUT%dDBFGM*ovKd4sSVh=>5Z|B<9p`7pG@fGD@2(|7=Vz?r84bh_+qA zEj8cJ=d7g%@632h&mvyy?&nR0y5LM2BBAEE*1+9$GVm^L>v&I6>8DPe`gF@_X0v^G zVb_M@BfHyc>g%6wtM!ys9KF2f)cYrQELrm6k|jHKtp0o#8hFSo8s>C}cYCrWGM&U5 zVNW!g;w#Vr_!H=<>z;1x@Y=E1xL>~Y$OuBbaS4Npe|PzH%xeHO-BI3F4|@&rjh@+_ zPcQmxbnvb|*wIvcEbT~J?ePQW552!-$;!=jD4Sn=@x`TE-v8sr?_stPIF@!-)>w=g zPc&B^NeAyMxTU~F((S}M&e)ajL8eweZ`kL^h;Ea4k2%!q91y&{dF>CvkqV8zVZ#SI zcU_c>wu`$qWTx#ta_oZ-j$!st#aBC4UObIFAC#O83(z5--pY$RR=@xL;s5;8e}37# z+i1+lfRvXS5%l8cO?n;gi9a>u;C-aMkYdoUwBHzDw+)hQ`tv&1D}Qx!HGtan6qZ4Z zo0^Ut`{3BIJ%=UUr!Vf>bF37+$@5Zw>A;0&pM91RKA;eAOdfsq+0!T&zWC_q<-TuL zWTw?)=)0KwFg|VW!wngyKqwkSGuF2hxoLf>e)}9r{*q+gH%D+}d!x3sE^m8PO;=Za zF@*fRwUWVxd@SI-_e+v{;=Kb>PYL)KZm6~$!U{BMr4LeW0DDj5FWy7(w-5sSgh;rok z*(HRVWSf$+G49~~?1#^utL$64ZrzfTea#>BoqZ1}ociM&e1;qIu`!D+JwCZ-(LmmM zjlc^s?{VSmKMBdV-7Xm19&dXW*zS^epI)+S!_H%tz2#l?_4QqsDn212j^UPM{HdpW z;@#JK7Ri0q9qR4zP-%HPeb2L^T>!n=xSm^|vwTtI4kC-LYEQX6Uy_o58;th9um$y)x za5eA5Ax2F67bm}tc?~e$6pZT1_tsu2MGKCRvYj*E zn(uSHA8kfHd}P8t19eViG5TpAo?iL>o)vLX#bF3l#n?Bs;P{mupK)Y;kq%m1nqR^) zJqVMBSb*+2*DHT@b2T9G)?;po#^r&{_GVgfCUR;ajIFf3Z4o~=Yi^Ds|2T3zT%_(d z>yNQl#`{_Du6u5A^x8iEI^p<^zB!HImKB(1f<74ZxSw}A94;6!xx!ng6?$$MX?22jzw!jO=)bt?jo?GEq3wyx^Z((0P7w)TU0+O2abeGk&w|LN407s0Y0 zxFp}FA=m#^w-_r~V6Aumvmiacbwn(xhwH#U%XKYQ^U+Hg3{GZE&-c&8(t^}&`!yy=R3i&&P@Df7NLf+O3Tcq10v zu-%|fFl)>SI)~d~-EK7`&@#YtvvRbeL+>hVe=~09u5+7VjS+Jq%XnWP-ix1FhhEth zEOQ_|3leWU4ChX;1Z*~hpTQ(fN@Zx61+lTwZXBls-MD-PTOX;&0FJ_}_jQQzvhhYiy>vLrK zLBvA}TR)ZtO;E771@E${G36j8f*X)%aSZNDKE%*9An~S&n`*?ZUh{~T3FkWX32;zM%L8GOhm8k`t+}B z5#w^2MpYzxZSi8#=jeV@`+YU<4==v!<4r^0VHCkOK)yBbynr|Jx2RBXDZVoFrOKa< zK@FgW*>E%it}zwl(AYGlX)AJa<{|^YqzBrLAY=te%_9+Zj6&|WJ*N(D*+IT?$oE;S zOSX#STUtkWFz>@VrNQqjyxslsO}ur2doofiMqGtkj&~RXu)`dJc$39U6f_z$E$FL3 z8V_D-#^=7{K@3AN<`KogJ(?Mp#-5CT*@660#?KjD{#2afO`0O=} zo<|-q!KGxp2j*EE^ShJ}=&u3Bn*sOZ#vgiRqX> zM1kj+^jY84+xO|`=c)Z>K~7hApZ@UKO`q;r5p_M@SZPovIKq>{G3P&GK;Aw7#mTQ@ zUIR>@lYC1B36D`|i7;q&l)_rf8%a;cjBd;>z^FE&k0~h=A{ek54HCCR)->-uw&8;> zzdZld>a{T4>*~-ALsZ+2)rWVzw*k5GGZ=3c!-mbtUSkW$(pj+F4gQ-H4=9z&h!89f zEiDj@_=hpSOZkBQ8USz7uY(KL5~bB$#iEvaBO=c*K?L(jqKssvOWE`gw?v^!=|?KN z^bOW{eMM% z5nRPF-bySy=)XAmbWehU!$?`qs zc&|C&FuX~;btWF>ngtn>G1-$6DYp0Yw00fYSe=|~q=9Yd`E-(RqY;rxZOuK!G?{P( z0@zq|3+a0-TXQvUW6wd!zt)c&y!okWHSrFYc;kw$xk~wiv8sU^@TTS)@{J^F#^SDp zwJG^!>({sS^mMe=9I1W<6M!v87A`EsT!W0R=1XaW*pf-3Vi*R+XVk#6g^GOg4FT`) zdiqgIZ%kTYhD{x+Oda5G8LOLgMF* zTvG$X@Yd>xw@LJRyP7E5Qe0SC2Y4gHs;I#lHChoF^4)IA$mq#iG|yYsnO(WC=E%nS zY5>Q7wAcvVm$i^b*>~6c;QxYj4!B;YvC53u}u%#}E{vAQ|=IM>v`aJ&fw3h$})cBL0%uHVLG z`3~e}pp{P8*a{o=zy=slqjfNNH^kkWlJds)VW>%_8q?G6^2Ncph<3&5kX zzdRja%AD*vVDFHrZM~AM!fCbwl^`UKfSaC`4&adL@^Kpcxx1P zXaciAFz5{eZ!aubQ|-e|*;OfF4=go_H<(8njYm?z8;$k=-X?^(%t*9_rs7(pGG=!t zA82q5po075Y=}3!t0?C&DjINaJ{kn_q+U4^@Vh3fZ87~G^uZS?|2<|ka0A|gy{NIu7;h}CS|sQVW{u82 zhQ_??;ATRCd<3s+wF0lV>K#Ref2QuY&sM`#4v=r+9rZ-J*T=j6>0&F8Iwz~K78xgv zgOAzW$p;!-12^DpcX;x$pSL7$EVCzMC190bEI`N#EOl0(jA^*lYp<8qFF_+N z4VCzLBQHMWJNcP;gLz8{Hr=j?NdS%oMP=SWAuS#B2E27*f`P}(QS`mBf(?vwif5Ee zbO|`gw*k|Mb-KA*w`OJOzE}Qda7Mv2N5<7 zeFNTCTG<=DL#AtT$YDf{mZOs3Qg=*ppW_92H&HBqF84STqM&ur+BIkvFv?Dv0zp-} z8L_dcs)!e6$@)Ba%*U9w&gHFoWk}vqMpfedJ%^wbY2l+myfxx4Y_StnNsDCOe-peB z5M$ygA_nmhMp}51xm*15jaLYGR8&*jLC)66ac)dKNiE(+V8R9P3(zxp|_@#@djPU_W<6JFyydI{E=mDa@FZDx^^{htw|K@Q<9X^ zW!`@yyotHT+X;mZ`DSGRZ_2DnBc&KI5ePK=YLo~}@}pBUivm3jXS@HUZR zrt#6d_8PX-C|$EJp(gXIQUC-6HEql_y&IM7U6_qTgDR$b&YbRpTP-_Z7J{h6FwQbWFab)8PWp4~K>CnrsUX z;{D$3hnCuJTK}6NA06v~dtnMv$4s)<81Me-WfmLzRo$85&|yrC<}*OAUu7cd(5fS$ zOaET-o3X9|@TPb)5QBV4DtREik-taGdUmysH#yKT1}+sJ@v!gz2y;Zye$#vf|?LT>}ztg;p?8^$%37{R~F-Mx(tyuNBit;X1vVH?AE0 z^7yc*rdp33%ZiJTZ!*}f@RpuBv&J*lHz^;lzXrgY*NIZW`rIR9JJi-1v$LopEexSc z*Yaj(jmJb4hbcj$qpTs<@FuI#(SLRFo3X9|@OI6(ck1NHlkc72bcsU|9NA|WCNFGz zg4yRJ-<0{D#2Q7SZLuKK)ox0_2pQu|o)=#s`6k}(vA#+9c>Og1-uK4ZY-;@2Y_Z>Q ziI`a8t9B?_%-&=P0NAg7E`B~kzH0OcajZewb0hM8&=?ukHHb&JDvG7Jw5k4yC+b@p z3nkt#*bv-81&5;c94(4oV~~0G;~Dce;2mpAj7?4DFug;SG-ryFgZ_e=}N;S6ZFCycDKk|bq><$ z$O*+FXAmr7$P&l=F69IIYXH0vRuVf!IbDT}8me%$da}zTXc1-%$wLPjIh%+#^Tu4o zb=W_6Mv>)HSySux+>j}&jx4g0JZ5J;JBGiT`y6FTY+UTI^ z*cd7Azc~4I%xeI=`KgE_N~O^6kdV;nw%EiOBJ4Iqr11)jVNs*XeiQa9w)C*LF`v*? z*!gl=DRlRPPj|Gn@9u7H&h6fPyyL-Scv-Q+MP^*n!aR@5?Gnuidb8Vu_;coILradx zIGFCHF~3Xsfc_c)Z(&Ag?3C2hR3#UpN{X4NoMU^yg@q;%Lr9G|T60V$zym# z8mtD-^6Qw_0Lix?h|Z~@NU9O4ObUrWZcB)HsLF+8t*V|=F3JJH1(vS>mQ3ugmy z1h#3%{4V7K`fGsNZx@nuOh{C#VlgEbHaciWa1k-gf}4Y4n+EL;F%Jy5-I0~$YIw$i z3=dG-Sha+(u%6$|2w#5wBJ7`2)t9CD5{w$vvN;kfD3)l;UZZLu?WOby|M>+n{%+#0>B zw(&tD;kNv4&D%+HX6W~C_2DMsC$dV`WTOE3`j(e1#sAh`i&Z8QM6}`Pmcc~VjQwrO zNA%TzkGBGSZ4-!3m;<2k;<-PA1C@UGrh(Tj6l=d{J#1l=#b? zWs4HG?cbUOk(Pw}q4|*S`6ssSlXy3jl(e*WrCHM2Yn_@LJ?nc@*s9Dsc#o&V8=f@8 zmcR^$j3c|HI#imNs|=@>9K0h^W0WdYOblm(wnk7Pa&-+?+;&GE*1#}6*s|=q?-(H1 zS^Issfg0pb;MaDaeB-69rK8%C)=^uCOTZP7iJ*{q2c5T+F$u)2q}A1I$smaJG-kTt znc!3r;ox#* zIwXQiniK1SPknf8j*M-HPc607x3!c&kIz4`fBwFPl7_8By1z)gSp!bITiWW8=HsA; zf*47g8}xTQmEcu?#G7D~TNC48Can&+)b5+6wyCFts@bAJz>S_6e$X+0$%Ur^%Z8bT z(dT?&-tw1?HEk`OCEHItbYlL#<>-99RpM{J8S@i{lI+gT2C#;aPCc`pBbFx+rHmkD zTmz;e1n+k5`Xl?~+vh#Qq#p93!B%%Y78|RMja4H19Z17gS0UR5dTX&RD>dF%B%Fs$ zZBih85Bwye-YV9%Kb`zwXA3y)duM+_$(oYo=zT4RfcJ~{LnpRM{jrAZPKkL-$IHn_ zj@P=g67)JuCX~m=g7;YZc}wC-V_=9iVO?w|hDq7lVAH2_F)^xCjHdwnRMBd>s`pL2 zHTXfc53AW=`gVC&T6ISY$?ZPpKA@J$zATA1sdZ+cmt_B8O5DwR;~53AIvp=z6xjfoF1*ZjQoR=3MQv5KWL ztJ;7tyQEU%vgs2`1mfd+Q0qKH&b{-;s2aK1abSXQiDfX3^@h3mRSUZuhP~ z@{sb)A{+#_I0IP^(K(xf?ssS`B4E@p-x0AM0j#^L^*PoTHt4Mm(X1`XKaz|f2SS+b zr%k|_X%5lezyHLO+qOBkmo&Wkqkn|f_w#P)*qB`7wd(~t;vHo94&Gxq9B&=mXl~JU z|93*sipPdxxD+F3(`MMw^+wi?fxKA@fK~uuDrV$MuYRS|hx-rIa#MkU4&LaRJ-Pp# zcf@y`&Z5H3s%PH#HPn6^S#2l{Z5@wPcg_}&?n#t+2M_n4d>+2`8|yrxBdoI{Q#FE> z6EReZk~rDvf;&}lwLcYmm`n&)F!3&{sJXwa$x_qCcz0I)emNXyCHorCio?^gZJU07 z*1k2ZRjsY99j#CQ^UKdyKV5UAszrJ@vfHxXu)JAYC^`_nD95)15BN3mE%mg}KvMu4 zrk&_q&I$KUp8WW|KXCF^0Rc|e@P>JZi79ToRYbC$(wQXQXp>uC{li-l?}p_i`%h&3 z)ZWlg^80_DIWzhD_(@Jnd!#rmIr*PE+N8&$p`|04axmLJa9=Cm{(d#NHc=FU z6(gW*I7JZf!n3SASM<3QoPs9Z&g-l*^4_#O-Nr2LHC0uw{sFxAm8@A_lCXc9tD&`e z=FFLCrPa?r|9o{d!Wr?u^pWJ`E}wumbYkBWw*ynlWcdyr@N48-;!W{tM4R+F3A%{| zvEh}6vjzD5PX$c~qL4H27EI3j-!8RC##%}1t5sEhV68Vg-}~R$zWkNsG;nSzO-rLC zFCwubN|+Ht<5;qY+Hdl@bQqh;irjWXmdtzHc}qOO@kRx_5paeAKdZB_H7yMm7_|8s z;I67-7W(q-`}aHl|3{vGzNx7x4J*`TVx>B~q(_D|)z>3v`Dx~KDamf=NJE#*ZP#bX z_W9ocZ?xX`d#jD4o=dXVka)Mg`W8&J!#(*H-%dOVpw6 zB;Hk3U9D)q(eK{oN~^9eO-7I^nrjMQjItDmS-P5c*CaQ0H`V-z`Ppb-6c&1|!<8Vn z-@yZZwY}ySTGkKm?~Gqh)P43juSw82y>HBXy@hzAmyLp7Yf3OsvUS^<9WogG$vgYnTr&)ShXyBxz0eSViQanf`6TI8K>yJEEycK$b)BAjKE3?)*81B}t zs<-y-f9Rnn^?$Hbmtr~*TaXO%4=h++IxH+LYGztnMOXKwjnrdDGf>h{WqBIO7e(1# z3*KWn2E3``#%s13ifYr0orEnLlKmFMyQ}IC?_hl7N%8lVrZlAIA}bAr?6tT|T77PB zb7@rRadgO-3$~%5!&th^VYkY>Z-3s1RVebNCHs79WSpb@u6m1j&v&g!Zc1ls9>me& zMGQKC$M-asBCT%Z8xU^VucV>Vn3m!ZM0tEHsJ!(Wvy{I9u5c1n^t$_J4oQFey5ZEI z4{poJK1T?W!r{rk&zoj!r@mDuwcXT&zuK^U|B3ngpZsZBHI|89fd%1Fc4lUVWn?60 zq_0?!Y01d=VJn(^^w>KwcG4N2)@Zl7^ufE`yZ*?~=Ni4}x6dfHg?RUuKzKu$(*&^m ziTR}##I0yq)09f)_CO4f%#vCzcp6*$Rz*BLADqht8ir|IN{gQsyw|4aoBFiv{hi{k-98VM1Orv*w^%l*h+{&YMjW zNKuOlU?;*?6#dn_9VSD92EoXsFH5{71N~K!;F5-) zGK1{|E$vAWta^ETEO@|4pQ9iX7p3V5j}67l z(Ad;zEdKa$CgrRz?h9BOQpz<7JA#PKRxE5#`hAKo?tF`yZv?0k#m*A&CWVf)O<9ob z6DRapTlc^8(!3Nl2+AT`XgiuVXen*a;}c}w!NbkkZw{F|*u-B>rAFF4C3O1~BxJM2 zs;4~Q4ZXv3At2&IZsDMP1VQi&RA zJnna*DF<*VGqzuk_rFr?Ib}6=*s)U08_6|WgUS_MN46Mxqjwr<X;lJ{)fdKP5+WdjwZe>nlyn@?sVnO)nw&Hf()q+e?_^FU==ggO|>?m(7l0ye0Xj z=oZAMejm-ZRU=wu-s8rblCG(elCVlIoA=F8h9;YODkVj~4sS%E8_)-LSV{}1*y z2$RO%m$g6frAd<-d~6x;25+smK{C*pi-rcr-Yl*;n2*_^Iyt^2c)KTgV%@iT8BdRAk83iLlO)n$N`gXXI>^ zc#}f6>Riag^gc-x$O!|Gcd3HrIvnRnI zx@F$CjyK_k=qr)hfwC*Y{Q~n0<7kpn2X7i)q15PRr{FGh=k2R!GsyhDQs68p5fip< z%|a4QYS@W3-ax9!NMj#i)HJ&VlS>|Kne7%$8Z3DbyxYC&k36&*@Bfr=Gs``6VxrCV zZ95Y3A&Dw}2JmLtb2V11J>$$TT6DznN-Laf40z}A)>iP|=ltn5Ju}uN+%ViEvGzRz zXzp989+_FS%{u?Cph?%CI6Y7LYhnJP_U#N^sHa39D1GwAe zDJ?CwVJ* zOP;q-b!ccR{4XZ)rPXsc%Vn8_E70VnUX@9`r5aP5GG^a3oMoQcaARV{?+yC0kwUmHBu(c%7hi zX+@Wn`ec@_cCyAPcZW1c=9g%x@n#J-@n*I;HQqJJue_S#Mn(|9r8jSr*Mq+Gyj7~y ziSW9`#M-9Xt!@#y@B4XEQbfqNAY%0yt7y;jb{W6_^m+i7;+o!K4j5mH?w4wEzW6_C zX?gnlMr6!&2ne@;abT0>`)5*pzZ;{=sFu~??CC19@f8Rd5 zZBwV(l+fucm=y!#KHd^;0*;(}d5txe+AHAyA&zID72q#>c=oO+7>V%{peRXXX$P#G-N5 zwi%6SKWb@MPlC)C_nT_)rAorRZ(sKRMrQn$+Cr>fsTBpY;EYY8#jt{Szjyng2Lm4T zH)B1JH>Ji7pNr7)>X$Oe=r+_kZ4QiYms4Z=+rD?`T+ETnC>@-Ynaoe2_ zsAH#wCdv9dc(`w<&)Ik=fcqbWkQgpW6`rb2MC9^!b(CPUUz?LUci44odXb3fDuuP> zHSzIjrO$L`BbKS5p`i^wDD<{r&96(-ij7^nYxCV%TMY^ky*p<_k`mF-6KBiv9X#NE zj>EiO0qx%au@KXh$c-Hy8k>5b6WLZwz-%(<(2n~&=?a=(3J(lct|@E#nZ;OIU4Mkq zq_Wb0-#s0_ezW=&BZAevc@BkMZ_x5cv+bH33SVBVGGdm@`*4W|j6y`T}J!pYiudlD)`1I5G{~+Eqr5Tf7?kFUrenY{~9ut*VhqS4R(44y|3}&3BMb4GYAgSNGk-#Mnfc zcknp-k30m(fIbQdQF551pi(7%*JZ+J8ML@0;y(F?BONX{haF>SFud~?<#`)@r3?>1 zQK21^k44c;?m6B;E7itgu`^k|{aAwb8W7weHlpdI5it=QR=b$&)G2@*0n5^m8Jjf@ zXPX(LT@JfLk1wz^C=`2^oM>_WP$`xeR5jy8kpmyIc9d{dGLjH?&f8 zczC!n5q)MSt#dJ;zzGB2sGdc_IAG6_{G%~c@LqG| z(Ta8de$)xabc9fd_+*TVBk%=Mj%BM%6yQerK+NVGDm4F$zQF=lI=2-~H#{u1SmhMo zn$!Kqqc_)FrToEI)xcHtJeofUH?`uSshBQ4?S5b07euMUqlzAy7Q9&&5G-wk&bVC% z*aBcrjZ(xI=pO8JAas(~Tdjp9Ry`E>YSR7oVZ$myk{ zxgx|};0vJQ-O%P3M6)}cKbQ>Gp)oNrH0xN6h9H)B!?)J&U6b|sSiv3QIHN#2^e!>z zQbm9wGQU82)l=_tQPM^@R9LeuiV*9^zk_e)%$YVti9xVgvu9&$U8M?-nZhJ|po|Z3 zo$^OxQv*Z88^x#8j6=wikicy!ay+R47J`gFe({T6yo9-iR33hK`t<3_P~?Z1MYv~6 z$U|k`W5Y6#ql|)VfV&_g2rEfs(S;B2p51u=KrUR~W>htBq)R-HE(Y zAT=-&45Qz6gvQ3aH0;z$K!x)D>0=aN`n^D!k zIM5sUDMocC@=k%&z{p{Y!ybWLxV+7%YG54bjrhsW z4)jKTic#H(yi*`GFmf2=DR?%iD~q2F8Kj$WJk7iJPUM{esezHh7>7Lqxo~-#QPsdW z&>Q(FMs+9hPJz_G$YG4b9)VoAyv?X;U>xX;{1l_Q6M3gVYGC9r#$k^@E?nMbR5dUT z^hSP)QQe8WQy?`kav0;VM<5q2Z!@YI7zcVIKgFo-MBXWo8W=f@ao8h}3zxSURSk>- zy^)_{RCglp6i5w>9L6~85y*wh+l;CP#)00*Pcf=Hk#`ED21X8J9QFw0!sTs7RRiNd zZ{(*K)t$&Y1yTbehcOO&1ajf>HlwP6aiBNyQ;h0P + + + + + + + + + + + + + + + + + + + + + AGENT_ONLY + + + + + + + + NOT AGENT_ONLY + + + + + + AGENT_ONLY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NOT AGENT_ONLY + + + + + + AGENT_ONLY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NOT Installed AND NOT REMOVE + NOT Installed AND NOT REMOVE + NOT Installed AND NOT REMOVE AND NOT AGENT_ONLY AND NOT WIX_UPGRADE_DETECTED + NOT Installed AND NOT REMOVE AND NOT AGENT_ONLY AND NOT WIX_UPGRADE_DETECTED + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/windows/windows.md b/windows/windows.md new file mode 100644 index 000000000..9033ce34e --- /dev/null +++ b/windows/windows.md @@ -0,0 +1,32 @@ +**POC Windows version of crowdsec** + +To test and develop on windows first execute the script that will install all required tools for windows [install dev on windows](/windows/install_dev_windows.ps1) +copy the script locally open a powershell window or launch powershell from command line +powershell +./install_dev_windows.ps1 +when all the required packages are installed +Clone the project and build manually the client and the cli +in cmd/crowdsec and cmd/crowdsec-cli with go build +you should now have a crowdsec.exe and crowdsec-cli.exe + +To make the installer and package first install the packages required executing the script + [install installer on windows](/windows/install_installer_windows.ps1) + +And finally to create the choco package and msi execute the script at root level + [make installer](/install_installer_windows.ps1) + ./make_installer.ps1 + +You should now have a CrowdSec.0.0.1.nupkg file +you can test it using +choco install CrowdSec.0.0.1.nupkg +it will install and configure crowdsec for windows. + +To test it navigate to C:\Program Files\CrowdSec and test the cli +.\crowdsec-cli.exe metrics + +Install something from the hub +.\crowdsec-cli.exe parsers install crowdsecurity/syslog-logs + +and restart the windows service +net start crowdsec +net stop crowdsec