Windows Support (#1159)

This commit is contained in:
blotus 2022-05-17 18:14:59 +08:00 committed by GitHub
parent a49b023a28
commit 0449ec1868
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 3401 additions and 437 deletions

View file

@ -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 ./...

View file

@ -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

View file

@ -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

11
.gitignore vendored
View file

@ -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

184
Makefile
View file

@ -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

103
azure-pipelines.yml Normal file
View file

@ -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'], '')

View file

@ -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)

View file

@ -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)
*/
}
},
}

View file

@ -0,0 +1,5 @@
// +build linux freebsd netbsd openbsd solaris !windows
package main
const DefaultConfigFile = "/etc/crowdsec/config.yaml"

View file

@ -0,0 +1,6 @@
//go:build windows
// +build windows
package main
const DefaultConfigFile = "C:\\ProgramData\\CrowdSec\\config\\config.yaml"

View file

@ -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
}

View file

@ -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)

View file

@ -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 == "" {

View file

@ -0,0 +1,5 @@
// +build linux freebsd netbsd openbsd solaris !windows
package main
const DefaultConfigFile = "/etc/crowdsec/config.yaml"

View file

@ -0,0 +1,3 @@
package main
const DefaultConfigFile = "C:\\ProgramData\\CrowdSec\\config\\config.yaml"

View file

@ -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()
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

8
config/acquis_win.yaml Normal file
View file

@ -0,0 +1,8 @@
source: wineventlog
event_channel: Security
event_ids:
- 4625
- 4623
event_level: information
labels:
type: eventlog

49
config/config_win.yaml Normal file
View file

@ -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

View file

@ -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

12
go.mod
View file

@ -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

18
go.sum
View file

@ -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=

18
make_chocolatey.ps1 Normal file
View file

@ -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

20
make_installer.ps1 Normal file
View file

@ -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

View file

@ -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

View file

@ -10,6 +10,7 @@ type DataSourceCommonCfg struct {
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
}

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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: "",
},
}

View file

@ -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)
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}
}
}
}
}

View file

@ -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 != "" {

View file

@ -0,0 +1,7 @@
// +build linux freebsd netbsd openbsd solaris !windows
package fileacquisition
func trimLine(text string) string {
return text
}

View file

@ -0,0 +1,9 @@
// +build windows
package fileacquisition
import "strings"
func trimLine(text string) string {
return strings.TrimRight(text, "\r")
}

View file

@ -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)

View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

@ -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
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}
}
}
}
}

View file

@ -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{

View file

@ -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
}

View file

@ -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: "<QueryList><Query><Select Path=\"Security\">*[System[(Level=0 or Level=4)]]</Select></Query></QueryList>",
expectedErr: "",
},
{
config: `source: wineventlog
event_channel: Security
event_level: Error
event_ids:
- 42`,
expectedQuery: "<QueryList><Query><Select Path=\"Security\">*[System[(EventID=42) and (Level=2)]]</Select></Query></QueryList>",
expectedErr: "",
},
{
config: `source: wineventlog
event_channel: Security
event_level: Error
event_ids:
- 42
- 43`,
expectedQuery: "<QueryList><Query><Select Path=\"Security\">*[System[(EventID=42 or EventID=43) and (Level=2)]]</Select></Query></QueryList>",
expectedErr: "",
},
{
config: `source: wineventlog
event_channel: Security`,
expectedQuery: "<QueryList><Query><Select Path=\"Security\">*</Select></Query></QueryList>",
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: |
<QueryList>
<Query Id="0" Path="Application">
<Select Path="Application">*[System[(Level=4 or Level=0) and (EventID=42)]]</Select>
</Query>
</QueryList>`,
expectedLines: []string{
"blabla",
"test",
"aaaa",
"bbbbb",
},
},
{
config: `source: wineventlog
xpath_query: |
<sdf>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()
}
}

View file

@ -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
}

View file

@ -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{})
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) {

View file

@ -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())
}

View file

@ -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())
}

View file

@ -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)

View file

@ -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())

View file

@ -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())
}

View file

@ -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)
}

View file

@ -0,0 +1,9 @@
//go:build !windows
package apiserver
import "os"
func cleanFile(path string) {
os.Remove(path)
}

View file

@ -0,0 +1,7 @@
package apiserver
import "os"
func cleanFile(path string) {
os.Remove(path)
}

View file

@ -3,6 +3,7 @@ package csconfig
import (
"fmt"
"log"
"runtime"
"strings"
"testing"
@ -17,9 +18,15 @@ func TestNormalLoad(t *testing.T) {
}
_, err = NewConfig("./tests/xxx.yaml", false, false)
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)
if !strings.HasPrefix(fmt.Sprintf("%s", err), "yaml: unmarshal errors:") {

View file

@ -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 != "" {

View file

@ -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)
cmd, err := pb.CreateCmd(binaryPath)
if err != nil {
return nil, errors.Wrap(err, "while getting process attributes")
}
cmd.SysProcAttr.Credential.NoSetGroups = true
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 {

View file

@ -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,24 +577,28 @@ func buildDummyPlugin() {
}
func setPluginPermTo(perm string) {
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")))
}
}
}
func setUp() {
dir, err := os.MkdirTemp("./", "cs_plugin_test")
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)

View file

@ -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)
}
}

150
pkg/csplugin/utils.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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{

View file

@ -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/<stage>/<author>/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))

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
//<type>/<stage>/<author>/<name>.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

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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.

View file

@ -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)
}

View file

@ -3,3 +3,4 @@
Make=gmake
$(info building for FreeBSD)

View file

@ -2,3 +2,4 @@
MAKE=make
$(info Building for linux)

18
platform/unix_common.mk Normal file
View file

@ -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

32
platform/windows.mk Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;
}

90
scripts/test_env.ps1 Normal file
View file

@ -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

View file

@ -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

View file

@ -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 `<id />` field in the nuspec (0.9.9+)
* ChocolateyPackageTitle - The title of the package, equivalent to the `<title />` 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+)

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not remove this test for UTF-8: if “Ω” doesnt 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</title>
<authors>CrowdSecurity</authors>
<projectUrl>https://crowdsec.net/</projectUrl>
<copyright>CrowdSec, 2022</copyright>
<licenseUrl>https://github.com/crowdsecurity/crowdsec/blob/main/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/crowdsecurity/crowdsec</projectSourceUrl>
<docsUrl>https://docs.crowdsec.net</docsUrl>
<bugTrackerUrl>https://github.com/crowdsecurity/crowdsec/issues</bugTrackerUrl>
<tags>crowdsec crowdsecurity security ips ids</tags>
<summary>CrowdSec IDS</summary>
<description>
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.
</description>
<!-- =============================== -->
<dependencies>
<dependency id="chocolatey-core.extension" version="1.1.0" />
</dependencies>
</metadata>
<files>
<file src="tools\**" target="tools" />
</files>
</package>

View file

@ -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.

View file

@ -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.

View file

@ -0,0 +1 @@
Stop-Service crowdsec

View file

@ -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."
}

View file

@ -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)"}
}

View file

@ -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

View file

@ -0,0 +1,2 @@
choco install -y wixtoolset
$env:Path += ";C:\Program Files (x86)\WiX Toolset v3.11\bin"

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<UI Id="WixUI_HK">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<Property Id="WixUI_Mode" Value="InstallDir" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">NOT Installed</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed</Publish>
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
<ProgressText Action="HubUpdate">Updating Hub content</ProgressText>
<ProgressText Action="InstallWinCollection">Installing Windows collection</ProgressText>
<ProgressText Action="RegisterMachine">Registering agent to local API</ProgressText>
<ProgressText Action="RegisterCAPI">Registering to Crowdsec central API</ProgressText>
</UI>
<UIRef Id="WixUI_Common" />
<WixVariable Id="WixUIDialogBmp" Value="..\windows\installer\installer_dialog.bmp" />
<WixVariable Id="WixUIBannerBmp" Value="..\windows\installer\crowdsec_msi_top_banner.bmp" />
<Icon Id="icon.ico" SourceFile="..\windows\installer\crowdsec_icon.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
</Fragment>
</Wix>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View file

@ -0,0 +1,165 @@
<?xml version="1.0"?>
<?if $(sys.BUILDARCH)="x86"?>
<?define Program_Files="ProgramFilesFolder"?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Program_Files="ProgramFiles64Folder"?>
<?else ?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif ?>
<?define ProductName="CrowdSec"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" UpgradeCode="8eab6970-25e3-4b7d-882f-5b7efa311afc" Name="$(var.ProductName)" Version="$(var.Version)" Manufacturer="CrowdSecurity" Language="1033">
<Package InstallerVersion="200" Compressed="yes" Comments="Crowdsec Installer Package" InstallScope="perMachine" />
<Media Id="1" Cabinet="product.cab" EmbedCab="yes" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." Schedule="afterInstallExecute" />
<SetProperty After="AppSearch" Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="LAPI is disabled and no agents credentials were generated.Please register your agent in your LAPI instance, update C:\ProgramData\CrowdSec\config\local_api_credentials.yaml, and configure the CrowdSec service to start on boot." >
AGENT_ONLY
</SetProperty>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.Program_Files)">
<Directory Id="INSTALLDIR" Name="$(var.ProductName)">
<Component Id="Crowdsec" Guid="200299fc-e728-4749-a283-cfbd20c02a59">
<File Id="crowdsec.exe" Source="cmd\crowdsec\crowdsec.exe" />
<Condition>NOT AGENT_ONLY</Condition>
<ServiceInstall Id="CrowdsecService" Name="Crowdsec" DisplayName="Crowdsec" Description="Crowdsec IPS/IDS" Start="auto" Type="ownProcess" ErrorControl="normal" Account="LocalSystem" Vital="yes" Interactive="no" />
<ServiceControl Id="CrowdsecService" Name="Crowdsec" Start="install" Stop="both" Remove="uninstall" Wait="yes" />
</Component>
<Component Id="CrowdsecNoStart" Guid="a9fac892-a7ea-4a3b-9a00-4f4bf2205de9">
<File Id="crowdsec2.exe" Source="cmd\crowdsec\crowdsec.exe" />
<Condition>AGENT_ONLY</Condition>
<ServiceInstall Id="CrowdsecService2" Name="Crowdsec" DisplayName="Crowdsec" Description="Crowdsec IPS/IDS" Start="disabled" Type="ownProcess" ErrorControl="normal" Account="LocalSystem" Vital="yes" Interactive="no" />
<ServiceControl Id="CrowdsecService2" Name="Crowdsec" Stop="both" Remove="uninstall" Wait="yes" />
</Component>
<Component Id="Cscli" Guid="b3da82cb-d111-4205-b0ee-5499b34dd57b">
<File Id="cscli.exe" Source="cmd\crowdsec-cli\cscli.exe" />
<Environment Id="UpdatePath" Name="PATH" Value="[INSTALLDIR]" Part="last" Action="set" System="yes" />
</Component>
</Directory>
</Directory>
<Directory Id="CommonAppDataFolder">
<Directory Id="CrowdSecCommonDir" Name="CrowdSec">
<Directory Id="ConfigDir" Name="config">
<Component Id="AcquisConfig" Guid="08bfc6fd-4811-48ed-b63e-035acb1f69d8">
<File Id="acquis.yaml" Source="config\acquis_win.yaml" Name="acquis.yaml" />
</Component>
<Component Id="LocalCreds" Guid="fea92471-ba4b-4067-a92a-19af0d581b60">
<File Id="local_api_credentials.yaml" Source="config\local_api_credentials.yaml" />
</Component>
<Component Id="OnlineCreds" Guid="a652a6cb-d464-40b1-8f50-78dce0135d20">
<File Id="online_api_credentials.yaml" Source="config\online_api_credentials.yaml" />
</Component>
<Component Id="ProfilesConfig" Guid="8d6fca04-b3be-4a52-a9df-278139d0498e">
<File Id="profiles.yaml" Source="config\profiles.yaml" />
</Component>
<Component Id="SimulationConfig" Guid="a27346e6-af4a-4ee6-aea9-d783b036cd21">
<File Id="simulation.yaml" Source="config\simulation.yaml" />
</Component>
<Component Id="ConsoleConfig" Guid="8393e488-18d5-4578-9e4c-99b54f7b2bb6">
<File Id="console.yaml" Source="config\console.yaml" />
</Component>
<Component Id="Csconfig_lapi" Guid="a99bd70c-61af-43ca-8394-6dc789cec566">
<Condition>
NOT AGENT_ONLY
</Condition>
<File Id="config.yaml" Source="config\config_win.yaml" Name="config.yaml"/>
</Component>
<Component Id="Csconfig_no_lapi" Guid="494d2e56-9db0-4d31-bde4-826f28a5683c">
<Condition>
AGENT_ONLY
</Condition>
<File Id="config_no_lapi.yaml" Source="config\config_win_no_lapi.yaml" Name="config.yaml"/>
</Component>
<Directory Id="NotifConfigDir" Name="notifications">
<Component Id="NotifConfig" Guid="4d04a852-e876-408f-95a7-a7effa7762c4">
<File Id="slack.yaml" Source="plugins\notifications\slack\slack.yaml" Name="slack.yaml" />
<File Id="http.yaml" Source="plugins\notifications\http\http.yaml" Name="http.yaml" />
<File Id="email.yaml" Source="plugins\notifications\email\email.yaml" Name="email.yaml" />
<File Id="splunk.yaml" Source="plugins\notifications\splunk\splunk.yaml" Name="splunk.yaml" />
</Component>
</Directory>
<Directory Id="PatternsDir" Name="patterns" />
</Directory>
<Directory Id="logCrowdsec" Name="log">
<Component Id="CreateLog" Guid="bfb37d14-10c4-40fb-bafa-2a29f95e4a53">
<CreateFolder />
</Component>
</Directory>
<Directory Id="hubCrowdsec" Name="hub">
<Component Id="CreateHub" Guid="ac528dd2-49f7-4448-a9e7-91c66061404b">
<CreateFolder />
</Component>
</Directory>
<Directory Id="CrowdsecDataDir" Name="data">
<Component Id="CreateCrowdsecDataDir" Guid="de529565-a499-4327-948d-2a318f8e822a">
<CreateFolder />
</Component>
</Directory>
<Directory Id="CrowdsecPluginsDir" Name="plugins">
<Component Id="CreateCrowdsecPluginsDir" Guid="bb7c8f19-8457-44b9-a538-aed494ec575d">
<File Id="notification_slack.exe" Source="plugins\notifications\slack\notification-slack.exe" />
<File Id="notification_email.exe" Source="plugins\notifications\email\notification-email.exe" />
<File Id="notification_http.exe" Source="plugins\notifications\http\notification-http.exe" />
<File Id="notification_splunk.exe" Source="plugins\notifications\splunk\notification-splunk.exe" />
</Component>
</Directory>
</Directory>
</Directory>
</Directory>
<SetProperty Id="HubUpdate" Value="&quot;[INSTALLDIR]\cscli.exe&quot; hub update" Sequence="execute" Before="HubUpdate" />
<CustomAction Id="HubUpdate" BinaryKey="WixCA" DllEntry="WixQuietExec" Execute="deferred" Return="check" Impersonate="no" />
<SetProperty Id="InstallWinCollection" Value="&quot;[INSTALLDIR]\cscli.exe&quot; collections install crowdsecurity/windows" Sequence="execute" Before="InstallWinCollection" />
<CustomAction Id="InstallWinCollection" BinaryKey="WixCA" DllEntry="WixQuietExec" Execute="deferred" Return="check" Impersonate="no" />
<SetProperty Id="RegisterMachine" Value="&quot;[INSTALLDIR]\cscli.exe&quot; machines add -a" Sequence="execute" Before="RegisterMachine" />
<CustomAction Id="RegisterMachine" BinaryKey="WixCA" DllEntry="WixQuietExec" Execute="deferred" Return="check" Impersonate="no" />
<SetProperty Id="RegisterCAPI" Value="&quot;[INSTALLDIR]\cscli.exe&quot; capi register" Sequence="execute" Before="RegisterMachine" />
<CustomAction Id="RegisterCAPI" BinaryKey="WixCA" DllEntry="WixQuietExec" Execute="deferred" Return="check" Impersonate="no" />
<InstallExecuteSequence>
<WriteEnvironmentStrings />
<Custom Action="HubUpdate" After="InstallFiles">NOT Installed AND NOT REMOVE</Custom>
<Custom Action="InstallWinCollection" After="HubUpdate">NOT Installed AND NOT REMOVE</Custom>
<Custom Action="RegisterMachine" After="InstallWinCollection">NOT Installed AND NOT REMOVE AND NOT AGENT_ONLY AND NOT WIX_UPGRADE_DETECTED</Custom>
<Custom Action="RegisterCAPI" After="RegisterMachine">NOT Installed AND NOT REMOVE AND NOT AGENT_ONLY AND NOT WIX_UPGRADE_DETECTED</Custom>
</InstallExecuteSequence>
<Feature Id="DefaultFeature" Level="1">
<ComponentRef Id="Crowdsec" />
<ComponentRef Id="CrowdsecNoStart" />
<ComponentRef Id="Cscli" />
<ComponentRef Id="AcquisConfig"/>
<ComponentRef Id="LocalCreds"/>
<ComponentRef Id="OnlineCreds"/>
<ComponentRef Id="ProfilesConfig"/>
<ComponentRef Id="SimulationConfig"/>
<ComponentRef Id="ConsoleConfig"/>
<ComponentRef Id="CreateLog" />
<ComponentRef Id="CreateHub" />
<ComponentRef Id="NotifConfig" />
<ComponentRef Id="CreateCrowdsecPluginsDir"/>
<ComponentRef Id="CreateCrowdsecDataDir" />
<ComponentRef Id="Csconfig_lapi" />
<ComponentRef Id="Csconfig_no_lapi" />
<ComponentGroupRef Id="CrowdsecPatterns" />
</Feature>
<UI>
<UIRef Id="WixUI_HK" />
</UI>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
<Property Id="MsiLogging" Value="voicewarmupx!" />
<!-- this should help to propagate env var changes -->
<CustomActionRef Id="WixBroadcastEnvironmentChange" />
</Product>
</Wix>

32
windows/windows.md Normal file
View file

@ -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