Compare commits

...

124 commits

Author SHA1 Message Date
Laurence Jones 05b54687b6
feat: support stdout in cscli support dump (#2939)
* feat: support stdout in cscli support dump

* fix: skip log.info if stdout

* fix: handle errors by returning to runE instead
2024-04-26 15:56:15 +01:00
mmetc c4473839c4
Refact pkg/parser/node (#2953)
* extract method processFilter()

* extract method processWhitelist()

* lint (whitespace, errors)
2024-04-25 17:53:10 +02:00
mmetc d2c4bc55fc
plugins: use yaml.v3 (#2969)
* plugins: use yaml.v3

* lint
2024-04-25 17:34:49 +02:00
mmetc 2abc078e53
use go 1.22.2 (#2826) 2024-04-25 15:11:08 +02:00
blotus ceb4479ec4
add zfs magic for GetFSType (#2950) 2024-04-25 15:05:11 +02:00
mmetc 845d4542bb
cscli: use yaml.v3 (#2965)
* cscli: use yaml.v3

* lint
2024-04-25 14:41:02 +02:00
Thibault "bui" Koechlin f4ed7b3520
Truncate meta data (#2966)
* truncate meta-data if they are too big
2024-04-25 13:43:38 +02:00
mmetc 60431804d8
db config: don't exit setup if can't detect fs, improve detection for freebsd (#2963) 2024-04-25 11:11:57 +02:00
mmetc 0f942a95f1
pkg/cwhub - rename methods for clarity (#2961)
* pkg/cwhub - rename methods for clarity

* lint
2024-04-24 11:09:37 +02:00
mmetc 97e6588a45
cscli hub items: avoid global (#2960)
* cscli hub items: avoid global

* lint (whitespace, errors)

* lint
2024-04-24 10:05:55 +02:00
mmetc 725cae1fa8
CI: upload coverage with token (#2958) 2024-04-23 12:41:50 +02:00
mmetc c64332d30a
cscli config show: avoid globals, use yaml v3 (#2863)
* cscli config show: avoid globals, use yaml v3

* lint (whitespace/errors)
2024-04-23 12:28:38 +02:00
mmetc 718d1c54b2
pkg/database/decisiosn: remove filter parameter, which is always passed empty (#2954) 2024-04-23 11:15:27 +02:00
mmetc b48b728317
cscli support: include stack traces (#2935) 2024-04-22 23:54:51 +02:00
mmetc fb393f1c57
tests: bump yq, cfssl (#2952) 2024-04-22 17:19:00 +02:00
mmetc 630cbf0c70
update linter list and descriptions (#2951) 2024-04-22 17:18:11 +02:00
Laurence Jones 95f27677e4
enhance: add refactoring to governance (#2955) 2024-04-22 14:18:34 +01:00
blotus c6e40191dd
Revert "docker: pre-download all hub items and data, opt-in hub updat… (#2947) 2024-04-18 15:33:51 +02:00
AlteredCoder 0746e0c091
Rename bouncers to Remediation component in openAPI (#2936)
* Rename bouncers to Remediation component in openAPI
2024-04-11 11:23:19 +02:00
mmetc 2291a232cb
docker: pre-download hub items (debian image) (#2934) 2024-04-08 15:00:45 +02:00
mmetc 0e8a1c681b
docker: pre-download all hub items and data, opt-in hub update/upgrade (#2933)
* docker: pre-download all hub items and data, opt-in hub update/upgrade

* docker/bars: don't purge anything before pre-downloading hub

* Docker: README update
2024-04-08 14:53:12 +02:00
mmetc 990dd5e08e
use go 1.21.9; update dependencies (#2931) 2024-04-05 15:11:11 +02:00
mmetc 2682f801df
windows: fix data file update (remove before rename) (#2930) 2024-04-05 14:57:33 +02:00
Thibault "bui" Koechlin 912c4bca70
split & reorganize tests a bit. Add tests on existing zones (#2925) 2024-04-03 17:49:05 +02:00
mmetc 26bcd0912a
docker: distribute geoip db in slim image (#2920) 2024-04-03 13:34:35 +02:00
Thibault "bui" Koechlin 63bd31b471
Fix REQUEST_URI behavior + fix #2891 (#2917)
* fix our behavior to comply more with modsec, REQUEST_URI should be: path+query string

* fix #2891 as well

* add new transforms

* add transform tests
2024-03-29 17:57:54 +01:00
mmetc be97466809
CI: use golangci-lint 1.57 (#2916) 2024-03-26 09:30:32 +01:00
dependabot[bot] df13f43156
Bump github.com/docker/docker (#2913)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.7+incompatible to 24.0.9+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.7...v24.0.9)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 09:13:16 +01:00
dependabot[bot] 368d22ec30
Bump github.com/jackc/pgx/v4 from 4.14.1 to 4.18.2 (#2887)
Bumps [github.com/jackc/pgx/v4](https://github.com/jackc/pgx) from 4.14.1 to 4.18.2.
- [Changelog](https://github.com/jackc/pgx/blob/v4.18.2/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v4.14.1...v4.18.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 09:12:57 +01:00
Christian Kampka f6bb8412c5
Add patterns_dir configuration option (#2868)
* Add patterns_dir configuration option

* Update config.yaml

---------

Co-authored-by: mmetc <92726601+mmetc@users.noreply.github.com>
2024-03-25 16:20:16 +01:00
mmetc 2e1ddec107
cscli: Add user-agent to all hub requests (#2915)
* cscli: Add user-agent to all hub requests

* fix unit test and avoid httpmock

* fix windows test
2024-03-25 10:40:41 +01:00
blotus 52f86c2d10
add libinjection expr helpers (#2914) 2024-03-21 11:39:37 +01:00
mmetc 7779c7ff0c
hub update: reload crowdsec if only data files have changed (#2912) 2024-03-20 15:46:14 +01:00
Thibault "bui" Koechlin 75a50c0c9d
improve a bit cscli examples when it comes to list mgmt (#2911) 2024-03-20 14:02:29 +01:00
mmetc d9f2a22ee5
cscli metrics -> sort table order (#2908) 2024-03-20 13:27:28 +01:00
blotus c76325b91b
Update windows pipeline (#2909) 2024-03-19 17:42:08 +01:00
mmetc dd71f0a866
CI: bump lint version and update configuration (#2901)
* bump golangci-lint to 1.56

* lint (testifylint)

* update lint configuration

* windows test: remove stale code
2024-03-19 10:48:49 +01:00
Thibault "bui" Koechlin b63e64ee9f
Fix locking logic for HA + add list unsubscribe for PAPI (#2904)
* add list unsubscribe operation for papi

* fix the locking logic for HA
2024-03-19 10:29:16 +01:00
blotus 6de62a1468
warn if user is using inotify to tail a symlink (#2881) 2024-03-19 10:22:43 +01:00
mmetc b411782648
CI: use go 1.21.8 (#2906) 2024-03-19 10:03:54 +01:00
mmetc 2f49088163
file acquisition: don't bubble error when tailed file disappears (#2903)
* file acquisition: don't bubble error when tailed file disappears
* don't call t.Kill()
* lint (whitespace)
2024-03-18 11:25:45 +01:00
Manuel Sabban fd2bb8927c
Fix rpm build (#2894)
* fix rpm build
2024-03-15 14:36:34 +01:00
Laurence Jones e9b0f3c54e
wip: fix unix socket error (#2897) 2024-03-14 15:36:47 +00:00
mmetc a6b0e58380
CI: bump github actions (#2895) 2024-03-14 14:04:45 +01:00
mmetc caca4032d1
lapi: log error "can't sinchronize with console" only if papi is enabled (#2896) 2024-03-14 14:03:43 +01:00
blotus 7dd86e2b95
add cron as a suggested package (#2799) 2024-03-14 14:02:53 +01:00
dependabot[bot] 06bebdeac7
Bump google.golang.org/protobuf from 1.31.0 to 1.33.0 (#2893)
Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0.
2024-03-14 14:01:09 +01:00
blotus 742f5e8cda
[appsec] delete api key header before processing the request (#2890) 2024-03-14 14:00:39 +01:00
mmetc 6c042f18f0
LAPI: local api unix socket support (#2770) 2024-03-14 10:43:02 +01:00
Thibault "bui" Koechlin 2a7e8383c8
fix #2889 (#2892)
* fix #2889
2024-03-13 17:20:06 +01:00
Thibault "bui" Koechlin b1c09f7512
acquisition : take prometheus level into account (#2885)
* properly take into account the aggregation level of prometheus metrics in acquisition
2024-03-13 14:57:19 +01:00
Manuel Sabban bd785ede15
Fix armhf (#2886)
* armhf compile fix
2024-03-12 17:33:22 +01:00
Manuel Sabban 1a56a0e0b9
armhf fix for getfstype (#2884)
* armhf fix for getfstype
2024-03-12 14:33:10 +01:00
mmetc 49e0735b53
cscli tests + fix bouncer/machine prune (#2883)
* func tests: "cscli config feature-flags"
* func tests: "cscli bouncers list"
* func tests + fix: "cscli bouncers/machines prune"
* lint
2024-03-11 13:14:01 +01:00
blotus 6daaab1789
support both scope and scopes parameter in decisions filter (#2882) 2024-03-11 10:54:40 +01:00
blotus e8ff13bc17
appsec: get the original UA from headers (#2809) 2024-03-08 15:04:36 +01:00
mmetc a928b4d001
bump dependencies for geoip db / lookup (#2880) 2024-03-08 14:22:23 +01:00
blotus 44ec3b9e01
file acquis: add mutex to protect access to the internal tail map (#2878) 2024-03-08 13:56:59 +01:00
mmetc 6c5e8afde9
pkg/cwhub: download data assets to temporary files to avoid partial fetch (#2879) 2024-03-08 10:55:30 +01:00
mmetc 1eab943ec2
crowdsec: remove warning if prometheus port is taken during cold logs processing (#2857)
i.e. remove a "Warning: port is already in use" because it's probably LAPI
2024-03-07 14:36:28 +01:00
mmetc 8108e4156d
CI: "make generate" target; use ent 0.12.5 (#2871)
* CI: "make generate" target; pin tool versions
* use ent 0.12.5
* fix make help
* fix model generation target; re-run swagger
2024-03-07 14:25:25 +01:00
blotus 5731491b4e
Auto detect if reading logs or storing sqlite db on a network share (#2241) 2024-03-07 14:04:50 +01:00
mmetc 98560d0cf5
bin/crowdsec: avoid writing errors twice when log_media=stdout (#2876)
* bin/crowdsec: avoid writing errors twice when log_media=stdout
simpler, correct hook usage
* lint
2024-03-07 12:29:10 +01:00
mmetc e611d01c90
cscli: hide hashed api keys (#2874)
* cscli: hide hashed api keys
* lint
2024-03-06 14:27:05 +01:00
mmetc 5356ccc6cd
cron: spread server load when upgrading hub and data files (#2873) 2024-03-06 13:42:57 +01:00
mmetc d8877a71fc
lp metrics: collect datasources and console options (#2870) 2024-03-05 14:56:14 +01:00
mmetc e7ecea764e
pkg/csconfig: use yaml.v3; deprecate yaml.v2 for new code (#2867)
* pkg/csconfig: use yaml.v3; deprecate yaml.v2 for new code
* yaml.v3: handle empty files
* Lint whitespace, errors
2024-03-04 14:22:53 +01:00
mmetc 41b43733b0
fix: log stack trace while computing metrics (#2865) 2024-03-01 10:52:35 +01:00
mmetc 8e9e091656
systemd: check configuration before attempting reload (#2861) 2024-02-26 13:44:40 +01:00
mmetc a23fe06d68
remove dependencies on enescakir/emoji, gotest.tools (#2837)
* wrap emoji package in pkg/emoji
* remove dependency on enescakir/emoji
* remove dependency on gotest.tools
* lint (whitespace)
2024-02-23 16:05:01 +01:00
mmetc 4bf640c6e8
refact pkg/apiserver (auth helpers) (#2856) 2024-02-23 14:03:50 +01:00
mmetc e34af358d7
refact cscli (globals) (#2854)
* cscli capi: avoid globals, extract methods
* cscli config restore: avoid global
* cscli hubtest: avoid global
* lint (whitespace, wrapped errors)
2024-02-23 10:37:04 +01:00
Laurence Jones 0df8f54fbb
Add unix socket option to http plugin, we have to use this in conjunction with URL parameter as we dont know which path the user wants so if they would like to communicate over unix socket they need to use both, however, the hostname can be whatever they want. We could be a little smarter and actually parse the url, however, increasing code when a user can just define it correctly make no sense (#2764) 2024-02-22 11:18:29 +00:00
mmetc 8da490f593
refact pkg/apiclient (#2846)
* extract resperr.go
* extract method prepareRequest()
* reset token inside mutex
2024-02-22 11:42:33 +01:00
mmetc 3e3df5e4c6
refact "cscli config", remove flag "cscli restore --old-backup" (#2832)
* refact "cscli config show"
* refact "cscli config backup"
* refact "cscli confgi show-yaml"
* refact "cscli config restore"
* refact "cscli config feature-flags"
* cscli restore: remove 'old-backup' option
* lint (whitespace, wrapped errors)
2024-02-22 11:04:36 +01:00
Laurence Jones f3ea88f64c
Appsec unix socket (#2737)
* Appsec socket

* Patch detection of nil listenaddr

* Allow TLS unix socket

* Merge diff issue
2024-02-21 13:40:38 +00:00
mmetc e976614645
cscli metrics: rename buckets -> scenarios (#2848)
* cscli metrics: rename buckets -> scenarios
* update lint configuration
* lint
2024-02-15 14:34:12 +01:00
Thibault "bui" Koechlin 717fc97ca0
add SetMeta and SetParsed helpers (#2845)
* add SetMeta and SetParsed helpers
2024-02-14 13:38:40 +01:00
he2ss 97c441dab6
implement highAvailability feature (#2506)
* implement highAvailability feature
---------

Co-authored-by: Marco Mariani <marco@crowdsec.net>
2024-02-14 12:26:42 +01:00
mmetc 8de8bf0e06
pkg/hubtest: extract methods + consistent error handling (#2756)
* pkg/hubtest: extract methods + consistent error handling
* lint
* rename variables for further refactor
2024-02-14 11:53:12 +01:00
mmetc 2bbf0b4762
re-generate ent code (#2844) 2024-02-14 11:19:13 +01:00
mmetc 45571cea08
use go 1.21.7 (#2830) 2024-02-14 09:47:12 +01:00
mmetc d34fb7e8a8
log processor: share apiclient in output goroutines (#2836) 2024-02-13 14:22:19 +01:00
mmetc 4561eb787b
bats: color formatter in CI (#2838) 2024-02-12 20:15:16 +01:00
mmetc a6a4d460d7
refact "cscli console" (#2834) 2024-02-12 11:45:58 +01:00
mmetc eada3739e6
refact "cscli notifications" (#2833) 2024-02-12 11:40:59 +01:00
blotus bdecf38616
update codeql action to v3 (#2822) 2024-02-12 11:33:44 +01:00
mmetc 5c83695177
refact "cscli explain" (#2835) 2024-02-12 11:23:17 +01:00
mmetc 2853410576
refact "cscli alerts" (#2827) 2024-02-09 17:51:29 +01:00
mmetc 58a1d7164f
refact "cscli lapi" (#2825) 2024-02-09 17:39:50 +01:00
blotus 332af5dd8d
appsec: split return code for bouncer and user (#2821) 2024-02-09 14:39:34 +01:00
Laurence Jones fa56d35a48
[Loki] Set headers/basic auth if set for queryRange (#2815) 2024-02-09 14:37:49 +01:00
mmetc df159b0167
update calls to deprecated x509 methods (#2824) 2024-02-09 13:55:24 +01:00
mmetc af1df0696b
refact cscli metric processing (#2816)
* typos
* refact cscli metric processing
* lint
2024-02-07 11:10:25 +01:00
Thibault "bui" Koechlin 3208a40ef3
Dedicated whitelist metrics (#2813)
* add proper whitelist metrics : both its own table and an extension to acquis metrics to track discarded/whitelisted lines
2024-02-06 18:04:17 +01:00
mmetc 4e724f6c0a
refact "cscli" root cmd (#2811)
* refact "cscli" root cmd
* lint (naming, imports, whitespace)
2024-02-06 10:50:28 +01:00
mmetc fdc525164a
refact "cscli metrics" part 3 (#2807) 2024-02-06 10:07:05 +01:00
mmetc 81acad0d66
refact "cscli metrics" part 2 (#2806) 2024-02-02 10:40:55 +01:00
mmetc 5ff8a03195
refact "cscli metrics" par 1 (#2805) 2024-02-02 09:45:03 +01:00
mmetc 4160bb8102
refact "cscli decisions" (#2804)
* refact "cscli decisions"
* CI: relax mysql test timing
* lint
2024-02-01 22:36:21 +01:00
mmetc f5fbe4a200
refact "cscli dashboard" (#2803) 2024-02-01 17:27:15 +01:00
mmetc 45c669fb65
refact "cscli papi" (#2802) 2024-02-01 17:27:00 +01:00
mmetc 825c08aa9d
refact "cscli simulation" (#2801) 2024-02-01 17:26:46 +01:00
mmetc af14f1085f
refact "cscli <itemtype>" (#2782) 2024-02-01 17:26:06 +01:00
mmetc e6f5d157b8
refact "cscli hub" (#2800) 2024-02-01 17:25:29 +01:00
mmetc 785fce4dc7
refact "cscli alerts" (#2778) 2024-02-01 17:24:00 +01:00
mmetc 17db4cb970
refact "cscli machines" (#2777) 2024-02-01 17:22:52 +01:00
mmetc 4192af30d5
refact "cscli bouncers" (#2776) 2024-01-31 12:40:41 +01:00
mmetc 3921c3f480
CI: rename workflows, improve docker build and tests (#2798) 2024-01-31 12:07:27 +01:00
mmetc 6507e8f4cd
cscli: don't print use_wal warning (#2794) 2024-01-30 11:07:53 +01:00
mmetc 66544baa7f
CI: workflow improvements (#2792)
- update deprecated action dependencies
- remove go version matrix (track stable version)
- optimize docker builds
- comments, renamed workflow
2024-01-30 10:20:25 +01:00
mmetc 311dfdee1f
Decouple docker image from package release (#2791)
- entry point fixes for 1.6.0
 - correctly override BUILD_VERSION argument
 - manual release workflow
2024-01-29 22:05:26 +01:00
mmetc 91b0fce955
option to override hub url template. for testers only. (#2785) 2024-01-25 12:53:20 +01:00
mmetc 532e97e00f
disable docker flavor test (#2783) 2024-01-25 09:58:48 +01:00
mmetc d7116a4a6f
disable docker flavor test (#2781) 2024-01-25 00:03:56 +01:00
Laurence Jones 2fb6f209aa
Update docker_start.sh (#2780)
* Update docker_start.sh

* disable 'set -e' in docker entrypoint

---------

Co-authored-by: marco <marco@crowdsec.net>
2024-01-24 22:51:33 +00:00
Manuel Sabban 3f9e8e81e6
fix some bats tests (#2775) 2024-01-24 19:51:55 +01:00
mmetc 8c75efdb2a
lint: disallow naked returns (#2771) 2024-01-24 17:31:34 +01:00
mmetc f75cdeb239
lint: enalble linter "wastedassign" (#2772) 2024-01-24 17:31:11 +01:00
mmetc 4b8e6cd780
appsec: avoid nil dereference (#2773) 2024-01-23 09:32:41 +01:00
blotus 84606eb207
Appsec hooks fixes (#2769) 2024-01-22 13:33:20 +01:00
mmetc dc698ecea8
log "loading papi client" only if papi is enabled (#2762) 2024-01-22 13:25:36 +01:00
mmetc 455acf7c90
lapi/papi: when receiving alerts, log and discard invalid addr/range (#2708)
https://github.com/crowdsecurity/crowdsec/issues/2687
2024-01-22 12:24:26 +01:00
Thibault "bui" Koechlin 19d36c0fb2
Support console options in console enroll (#2760)
* make dev.yaml has a valid/default console path

* simplify and make more consistent help message about console opts

* allow enroll to specify options to enable

* allow 'all' shortcut for --enable
2024-01-19 15:49:00 +01:00
322 changed files with 16505 additions and 13141 deletions

View file

@ -42,7 +42,7 @@ issue:
3. Check [Releases](https://github.com/crowdsecurity/crowdsec/releases/latest) to make sure your agent is on the latest version. 3. Check [Releases](https://github.com/crowdsecurity/crowdsec/releases/latest) to make sure your agent is on the latest version.
- prefix: kind - prefix: kind
list: ['feature', 'bug', 'packaging', 'enhancement'] list: ['feature', 'bug', 'packaging', 'enhancement', 'refactoring']
multiple: false multiple: false
author_association: author_association:
author: true author: true
@ -54,6 +54,7 @@ issue:
@$AUTHOR: There are no 'kind' label on this issue. You need a 'kind' label to start the triage process. @$AUTHOR: There are no 'kind' label on this issue. You need a 'kind' label to start the triage process.
* `/kind feature` * `/kind feature`
* `/kind enhancement` * `/kind enhancement`
* `/kind refactoring`
* `/kind bug` * `/kind bug`
* `/kind packaging` * `/kind packaging`
@ -65,12 +66,13 @@ pull_request:
labels: labels:
- prefix: kind - prefix: kind
multiple: false multiple: false
list: [ 'feature', 'enhancement', 'fix', 'chore', 'dependencies'] list: [ 'feature', 'enhancement', 'fix', 'chore', 'dependencies', 'refactoring']
needs: needs:
comment: | comment: |
@$AUTHOR: There are no 'kind' label on this PR. You need a 'kind' label to generate the release automatically. @$AUTHOR: There are no 'kind' label on this PR. You need a 'kind' label to generate the release automatically.
* `/kind feature` * `/kind feature`
* `/kind enhancement` * `/kind enhancement`
* `/kind refactoring`
* `/kind fix` * `/kind fix`
* `/kind chore` * `/kind chore`
* `/kind dependencies` * `/kind dependencies`

View file

@ -1,4 +1,4 @@
name: Hub tests name: (sub) Bats / Hub
on: on:
workflow_call: workflow_call:
@ -8,16 +8,13 @@ on:
GIST_BADGES_ID: GIST_BADGES_ID:
required: true required: true
env:
PREFIX_TEST_NAMES_WITH_FILE: true
jobs: jobs:
build: build:
strategy: strategy:
matrix: matrix:
test-file: ["hub-1.bats", "hub-2.bats", "hub-3.bats"] test-file: ["hub-1.bats", "hub-2.bats", "hub-3.bats"]
name: "Build + tests" name: "Functional tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
@ -28,15 +25,15 @@ jobs:
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
- name: "Check out CrowdSec repository" - name: "Check out CrowdSec repository"
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- name: "Set up Go" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: "1.21.6" go-version: "1.22.2"
- name: "Install bats dependencies" - name: "Install bats dependencies"
env: env:
@ -50,13 +47,13 @@ jobs:
- name: "Run hub tests" - name: "Run hub tests"
run: | run: |
./test/bin/generate-hub-tests ./test/bin/generate-hub-tests
./test/run-tests test/dyn-bats/${{ matrix.test-file }} ./test/run-tests ./test/dyn-bats/${{ matrix.test-file }} --formatter $(pwd)/test/lib/color-formatter
- name: "Collect hub coverage" - name: "Collect hub coverage"
run: ./test/bin/collect-hub-coverage >> $GITHUB_ENV run: ./test/bin/collect-hub-coverage >> $GITHUB_ENV
- name: "Create Parsers badge" - name: "Create Parsers badge"
uses: schneegans/dynamic-badges-action@v1.6.0 uses: schneegans/dynamic-badges-action@v1.7.0
if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'crowdsecurity' }} if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'crowdsecurity' }}
with: with:
auth: ${{ secrets.GIST_BADGES_SECRET }} auth: ${{ secrets.GIST_BADGES_SECRET }}
@ -67,7 +64,7 @@ jobs:
color: ${{ env.SCENARIO_BADGE_COLOR }} color: ${{ env.SCENARIO_BADGE_COLOR }}
- name: "Create Scenarios badge" - name: "Create Scenarios badge"
uses: schneegans/dynamic-badges-action@v1.6.0 uses: schneegans/dynamic-badges-action@v1.7.0
if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'crowdsecurity' }} if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'crowdsecurity' }}
with: with:
auth: ${{ secrets.GIST_BADGES_SECRET }} auth: ${{ secrets.GIST_BADGES_SECRET }}

View file

@ -1,4 +1,4 @@
name: Functional tests (MySQL) name: (sub) Bats / MySQL
on: on:
workflow_call: workflow_call:
@ -7,16 +7,9 @@ on:
required: true required: true
type: string type: string
env:
PREFIX_TEST_NAMES_WITH_FILE: true
jobs: jobs:
build: build:
strategy: name: "Functional tests"
matrix:
go-version: ["1.21.6"]
name: "Build + tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
services: services:
@ -35,15 +28,15 @@ jobs:
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
- name: "Check out CrowdSec repository" - name: "Check out CrowdSec repository"
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- name: "Set up Go ${{ matrix.go-version }}" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: "1.22.2"
- name: "Install bats dependencies" - name: "Install bats dependencies"
env: env:
@ -62,7 +55,7 @@ jobs:
MYSQL_USER: root MYSQL_USER: root
- name: "Run tests" - name: "Run tests"
run: make bats-test run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
env: env:
DB_BACKEND: mysql DB_BACKEND: mysql
MYSQL_HOST: 127.0.0.1 MYSQL_HOST: 127.0.0.1

View file

@ -1,18 +1,11 @@
name: Functional tests (Postgres) name: (sub) Bats / Postgres
on: on:
workflow_call: workflow_call:
env:
PREFIX_TEST_NAMES_WITH_FILE: true
jobs: jobs:
build: build:
strategy: name: "Functional tests"
matrix:
go-version: ["1.21.6"]
name: "Build + tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
services: services:
@ -44,15 +37,15 @@ jobs:
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
- name: "Check out CrowdSec repository" - name: "Check out CrowdSec repository"
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- name: "Set up Go ${{ matrix.go-version }}" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: "1.22.2"
- name: "Install bats dependencies" - name: "Install bats dependencies"
env: env:
@ -71,7 +64,7 @@ jobs:
PGUSER: postgres PGUSER: postgres
- name: "Run tests (DB_BACKEND: pgx)" - name: "Run tests (DB_BACKEND: pgx)"
run: make bats-test run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
env: env:
DB_BACKEND: pgx DB_BACKEND: pgx
PGHOST: 127.0.0.1 PGHOST: 127.0.0.1

View file

@ -1,19 +1,14 @@
name: Functional tests (sqlite) name: (sub) Bats / sqlite + coverage
on: on:
workflow_call: workflow_call:
env: env:
PREFIX_TEST_NAMES_WITH_FILE: true
TEST_COVERAGE: true TEST_COVERAGE: true
jobs: jobs:
build: build:
strategy: name: "Functional tests"
matrix:
go-version: ["1.21.6"]
name: "Build + tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
@ -25,15 +20,15 @@ jobs:
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
- name: "Check out CrowdSec repository" - name: "Check out CrowdSec repository"
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- name: "Set up Go ${{ matrix.go-version }}" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: "1.22.2"
- name: "Install bats dependencies" - name: "Install bats dependencies"
env: env:
@ -46,7 +41,7 @@ jobs:
make clean bats-build bats-fixture BUILD_STATIC=1 make clean bats-build bats-fixture BUILD_STATIC=1
- name: "Run tests" - name: "Run tests"
run: make bats-test run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
- name: "Collect coverage data" - name: "Collect coverage data"
run: | run: |
@ -82,7 +77,8 @@ jobs:
if: ${{ always() }} if: ${{ always() }}
- name: Upload crowdsec coverage to codecov - name: Upload crowdsec coverage to codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
files: ./coverage-bats.out files: ./coverage-bats.out
flags: bats flags: bats
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Cleanup - name: Cleanup
run: | run: |

View file

@ -21,30 +21,26 @@ on:
jobs: jobs:
build: build:
strategy:
matrix:
go-version: ["1.21.6"]
name: Build name: Build
runs-on: windows-2019 runs-on: windows-2019
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: false submodules: false
- name: "Set up Go ${{ matrix.go-version }}" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: "1.22.2"
- name: Build - name: Build
run: make windows_installer BUILD_RE2_WASM=1 run: make windows_installer BUILD_RE2_WASM=1
- name: Upload MSI - name: Upload MSI
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
path: crowdsec*msi path: crowdsec*msi
name: crowdsec.msi name: crowdsec.msi

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Drafts your next Release notes as Pull Requests are merged into "master" # Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v6
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml

View file

@ -44,14 +44,20 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# required to pick up tags for BUILD_VERSION # required to pick up tags for BUILD_VERSION
fetch-depth: 0 fetch-depth: 0
- name: "Set up Go"
uses: actions/setup-go@v5
with:
go-version: "1.22.2"
cache-dependency-path: "**/go.sum"
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -62,7 +68,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild # - name: Autobuild
# uses: github/codeql-action/autobuild@v2 # uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -71,14 +77,8 @@ jobs:
# and modify them (or add more) to build your code if your project # and modify them (or add more) to build your code if your project
# uses a compiled language # uses a compiled language
- name: "Set up Go"
uses: actions/setup-go@v4
with:
go-version: "1.21.6"
cache-dependency-path: "**/go.sum"
- run: | - run: |
make clean build BUILD_RE2_WASM=1 make clean build BUILD_RE2_WASM=1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View file

@ -15,59 +15,42 @@ on:
- 'README.md' - 'README.md'
jobs: jobs:
test_docker_image: test_flavor:
strategy:
# we could test all the flavors in a single pytest job,
# but let's split them (and the image build) in multiple runners for performance
matrix:
# can be slim, full or debian (no debian slim).
flavor: ["slim", "debian"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
with: with:
config: .github/buildkit.toml config: .github/buildkit.toml
- name: "Build flavor: slim" - name: "Build image"
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile${{ matrix.flavor == 'debian' && '.debian' || '' }}
tags: crowdsecurity/crowdsec:test-slim tags: crowdsecurity/crowdsec:test${{ matrix.flavor == 'full' && '' || '-' }}${{ matrix.flavor == 'full' && '' || matrix.flavor }}
target: slim target: ${{ matrix.flavor == 'debian' && 'full' || matrix.flavor }}
platforms: linux/amd64
load: true
cache-from: type=gha
cache-to: type=gha,mode=min
- name: "Build flavor: full"
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
tags: crowdsecurity/crowdsec:test
target: full
platforms: linux/amd64
load: true
cache-from: type=gha
cache-to: type=gha,mode=min
- name: "Build flavor: full (debian)"
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.debian
tags: crowdsecurity/crowdsec:test-debian
target: full
platforms: linux/amd64 platforms: linux/amd64
load: true load: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=min cache-to: type=gha,mode=min
- name: "Setup Python" - name: "Setup Python"
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
@ -76,15 +59,15 @@ jobs:
cd docker/test cd docker/test
python -m pip install --upgrade pipenv wheel python -m pip install --upgrade pipenv wheel
- name: "Cache virtualenvs" #- name: "Cache virtualenvs"
id: cache-pipenv # id: cache-pipenv
uses: actions/cache@v3 # uses: actions/cache@v4
with: # with:
path: ~/.local/share/virtualenvs # path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }} # key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
- name: "Install dependencies" - name: "Install dependencies"
if: steps.cache-pipenv.outputs.cache-hit != 'true' #if: steps.cache-pipenv.outputs.cache-hit != 'true'
run: | run: |
cd docker/test cd docker/test
pipenv install --deploy pipenv install --deploy
@ -95,9 +78,10 @@ jobs:
- name: "Run tests" - name: "Run tests"
env: env:
CROWDSEC_TEST_VERSION: test CROWDSEC_TEST_VERSION: test
CROWDSEC_TEST_FLAVORS: slim,debian CROWDSEC_TEST_FLAVORS: ${{ matrix.flavor }}
CROWDSEC_TEST_NETWORK: net-test CROWDSEC_TEST_NETWORK: net-test
CROWDSEC_TEST_TIMEOUT: 90 CROWDSEC_TEST_TIMEOUT: 90
# running serially to reduce test flakiness
run: | run: |
cd docker/test cd docker/test
pipenv run pytest -n 2 --durations=0 --color=yes pipenv run pytest -n 1 --durations=0 --color=yes

View file

@ -20,25 +20,21 @@ env:
jobs: jobs:
build: build:
strategy:
matrix:
go-version: ["1.21.6"]
name: "Build + tests" name: "Build + tests"
runs-on: windows-2022 runs-on: windows-2022
steps: steps:
- name: Check out CrowdSec repository - name: Check out CrowdSec repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: false submodules: false
- name: "Set up Go ${{ matrix.go-version }}" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: "1.22.2"
- name: Build - name: Build
run: | run: |
@ -52,15 +48,16 @@ jobs:
cat out.txt | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter cat out.txt | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter
- name: Upload unit coverage to Codecov - name: Upload unit coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
files: coverage.out files: coverage.out
flags: unit-windows flags: unit-windows
token: ${{ secrets.CODECOV_TOKEN }}
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v4
with: with:
version: v1.55 version: v1.57
args: --issues-exit-code=1 --timeout 10m args: --issues-exit-code=1 --timeout 10m
only-new-issues: false only-new-issues: false
# the cache is already managed above, enabling it here # the cache is already managed above, enabling it here

View file

@ -118,15 +118,15 @@ jobs:
steps: steps:
- name: Check out CrowdSec repository - name: Check out CrowdSec repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: false submodules: false
- name: "Set up Go" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: "1.21.6" go-version: "1.22.2"
- name: Create localstack streams - name: Create localstack streams
run: | run: |
@ -149,15 +149,16 @@ jobs:
make go-acc | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter make go-acc | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter
- name: Upload unit coverage to Codecov - name: Upload unit coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
files: coverage.out files: coverage.out
flags: unit-linux flags: unit-linux
token: ${{ secrets.CODECOV_TOKEN }}
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v4
with: with:
version: v1.55 version: v1.57
args: --issues-exit-code=1 --timeout 10m args: --issues-exit-code=1 --timeout 10m
only-new-issues: false only-new-issues: false
# the cache is already managed above, enabling it here # the cache is already managed above, enabling it here

View file

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Semantic versioning, lock to different version: v2, v2.0 or a commit hash. # Semantic versioning, lock to different version: v2, v2.0 or a commit hash.
- uses: BirthdayResearch/oss-governance-bot@v3 - uses: BirthdayResearch/oss-governance-bot@v4
with: with:
# You can use a PAT to post a comment/label/status so that it shows up as a user instead of github-actions # You can use a PAT to post a comment/label/status so that it shows up as a user instead of github-actions
github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}' github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}'

View file

@ -0,0 +1,47 @@
name: (push-master) Publish latest Docker images
on:
push:
branches: [ master ]
paths:
- 'pkg/**'
- 'cmd/**'
- 'mk/**'
- 'docker/docker_start.sh'
- 'docker/config.yaml'
- '.github/workflows/publish-docker-master.yml'
- '.github/workflows/publish-docker.yml'
- 'Dockerfile'
- 'Dockerfile.debian'
- 'go.mod'
- 'go.sum'
- 'Makefile'
jobs:
dev-alpine:
uses: ./.github/workflows/publish-docker.yml
with:
platform: linux/amd64
crowdsec_version: ""
image_version: dev
latest: false
push: true
slim: false
debian: false
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
dev-debian:
uses: ./.github/workflows/publish-docker.yml
with:
platform: linux/amd64
crowdsec_version: ""
image_version: dev
latest: false
push: true
slim: false
debian: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -0,0 +1,48 @@
name: (manual) Publish Docker images
on:
workflow_dispatch:
inputs:
image_version:
description: Docker Image version (base tag, i.e. v1.6.0-2)
required: true
crowdsec_version:
description: Crowdsec version (BUILD_VERSION)
required: true
latest:
description: Overwrite latest (and slim) tags?
default: false
required: true
push:
description: Really push?
default: false
required: true
jobs:
alpine:
uses: ./.github/workflows/publish-docker.yml
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
with:
image_version: ${{ github.event.inputs.image_version }}
crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
latest: ${{ github.event.inputs.latest == 'true' }}
push: ${{ github.event.inputs.push == 'true' }}
slim: true
debian: false
platform: "linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6"
debian:
uses: ./.github/workflows/publish-docker.yml
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
with:
image_version: ${{ github.event.inputs.image_version }}
crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
latest: ${{ github.event.inputs.latest == 'true' }}
push: ${{ github.event.inputs.push == 'true' }}
slim: false
debian: true
platform: "linux/amd64,linux/386,linux/arm64"

125
.github/workflows/publish-docker.yml vendored Normal file
View file

@ -0,0 +1,125 @@
name: (sub) Publish Docker images
on:
workflow_call:
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
inputs:
platform:
required: true
type: string
image_version:
required: true
type: string
crowdsec_version:
required: true
type: string
latest:
required: true
type: boolean
push:
required: true
type: boolean
slim:
required: true
type: boolean
debian:
required: true
type: boolean
jobs:
push_to_registry:
name: Push Docker image to registries
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
config: .github/buildkit.toml
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare (slim)
if: ${{ inputs.slim }}
id: slim
run: |
DOCKERHUB_IMAGE=${{ secrets.DOCKER_USERNAME }}/crowdsec
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
VERSION=${{ inputs.image_version }}
DEBIAN=${{ inputs.debian && '-debian' || '' }}
TAGS="${DOCKERHUB_IMAGE}:${VERSION}-slim${DEBIAN},${GHCR_IMAGE}:${VERSION}-slim${DEBIAN}"
if [[ ${{ inputs.latest }} == true ]]; then
TAGS=$TAGS,${DOCKERHUB_IMAGE}:slim${DEBIAN},${GHCR_IMAGE}:slim${DEBIAN}
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Prepare (full)
id: full
run: |
DOCKERHUB_IMAGE=${{ secrets.DOCKER_USERNAME }}/crowdsec
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
VERSION=${{ inputs.image_version }}
DEBIAN=${{ inputs.debian && '-debian' || '' }}
TAGS="${DOCKERHUB_IMAGE}:${VERSION}${DEBIAN},${GHCR_IMAGE}:${VERSION}${DEBIAN}"
if [[ ${{ inputs.latest }} == true ]]; then
TAGS=$TAGS,${DOCKERHUB_IMAGE}:latest${DEBIAN},${GHCR_IMAGE}:latest${DEBIAN}
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Build and push image (slim)
if: ${{ inputs.slim }}
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile${{ inputs.debian && '.debian' || '' }}
push: ${{ inputs.push }}
tags: ${{ steps.slim.outputs.tags }}
target: slim
platforms: ${{ inputs.platform }}
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.slim.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
build-args: |
BUILD_VERSION=${{ inputs.crowdsec_version }}
- name: Build and push image (full)
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile${{ inputs.debian && '.debian' || '' }}
push: ${{ inputs.push }}
tags: ${{ steps.full.outputs.tags }}
target: full
platforms: ${{ inputs.platform }}
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.full.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
build-args: |
BUILD_VERSION=${{ inputs.crowdsec_version }}

View file

@ -1,5 +1,5 @@
# .github/workflows/build-docker-image.yml # .github/workflows/build-docker-image.yml
name: build name: Release
on: on:
release: release:
@ -12,24 +12,20 @@ permissions:
jobs: jobs:
build: build:
strategy:
matrix:
go-version: ["1.21.6"]
name: Build and upload binary package name: Build and upload binary package
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: false submodules: false
- name: "Set up Go ${{ matrix.go-version }}" - name: "Set up Go"
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: "1.22.2"
- name: Build the binaries - name: Build the binaries
run: | run: |

View file

@ -1,71 +0,0 @@
name: Publish Debian Docker image on Push to Master
on:
push:
branches: [ master ]
paths:
- 'pkg/**'
- 'cmd/**'
- 'plugins/**'
- 'docker/docker_start.sh'
- 'docker/config.yaml'
- '.github/workflows/publish_docker-image_on_master-debian.yml'
- 'Dockerfile.debian'
- 'go.mod'
- 'go.sum'
- 'Makefile'
jobs:
push_to_registry:
name: Push Debian Docker image to Docker Hub
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'crowdsecurity' }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=crowdsecurity/crowdsec
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
VERSION=dev-debian
TAGS="${DOCKER_IMAGE}:${VERSION},${GHCR_IMAGE}:${VERSION}"
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config: .github/buildkit.toml
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push full image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.debian
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}
platforms: linux/amd64
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=min

View file

@ -1,71 +0,0 @@
name: Publish Docker image on Push to Master
on:
push:
branches: [ master ]
paths:
- 'pkg/**'
- 'cmd/**'
- 'plugins/**'
- 'docker/docker_start.sh'
- 'docker/config.yaml'
- '.github/workflows/publish_docker-image_on_master.yml'
- 'Dockerfile'
- 'go.mod'
- 'go.sum'
- 'Makefile'
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'crowdsecurity' }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=crowdsecurity/crowdsec
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
VERSION=dev
TAGS="${DOCKER_IMAGE}:${VERSION},${GHCR_IMAGE}:${VERSION}"
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config: .github/buildkit.toml
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push full image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}
platforms: linux/amd64
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=min

View file

@ -1,61 +0,0 @@
name: Publish Docker Debian image
on:
release:
types:
- released
- prereleased
workflow_dispatch:
jobs:
push_to_registry:
name: Push Docker debian image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=crowdsecurity/crowdsec
VERSION=bullseye
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
elif [[ $GITHUB_REF == refs/heads/* ]]; then
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -E 's#/+#-#g')
elif [[ $GITHUB_REF == refs/pull/* ]]; then
VERSION=pr-${{ github.event.number }}
fi
TAGS="${DOCKER_IMAGE}:${VERSION}-debian"
if [[ "${{ github.event.action }}" == "released" ]]; then
TAGS=$TAGS,${DOCKER_IMAGE}:latest-debian
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config: .github/buildkit.toml
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.debian
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}
platforms: linux/amd64,linux/arm64,linux/386
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}

View file

@ -1,86 +0,0 @@
name: Publish Docker image
on:
release:
types:
- released
- prereleased
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=crowdsecurity/crowdsec
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
VERSION=edge
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
elif [[ $GITHUB_REF == refs/heads/* ]]; then
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -E 's#/+#-#g')
elif [[ $GITHUB_REF == refs/pull/* ]]; then
VERSION=pr-${{ github.event.number }}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${GHCR_IMAGE}:${VERSION}"
TAGS_SLIM="${DOCKER_IMAGE}:${VERSION}-slim,${GHCR_IMAGE}:${VERSION}-slim"
if [[ ${{ github.event.action }} == released ]]; then
TAGS=$TAGS,${DOCKER_IMAGE}:latest,${GHCR_IMAGE}:latest
TAGS_SLIM=$TAGS_SLIM,${DOCKER_IMAGE}:slim,${GHCR_IMAGE}:slim
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "tags_slim=${TAGS_SLIM}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config: .github/buildkit.toml
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push slim image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags_slim }}
target: slim
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/386
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Build and push full image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/386
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}

View file

@ -1,4 +1,4 @@
name: Update Docker Hub README name: (push-master) Update Docker Hub README
on: on:
push: push:
@ -13,7 +13,7 @@ jobs:
steps: steps:
- -
name: Check out the repo name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v4
if: ${{ github.repository_owner == 'crowdsecurity' }} if: ${{ github.repository_owner == 'crowdsecurity' }}
- -
name: Update docker hub README name: Update docker hub README

5
.gitignore vendored
View file

@ -6,7 +6,10 @@
*.dylib *.dylib
*~ *~
.pc .pc
# IDEs
.vscode .vscode
.idea
# If vendor is included, allow prebuilt (wasm?) libraries. # If vendor is included, allow prebuilt (wasm?) libraries.
!vendor/**/*.so !vendor/**/*.so
@ -34,7 +37,7 @@ test/coverage/*
*.swo *.swo
# Dependencies are not vendored by default, but a tarball is created by "make vendor" # Dependencies are not vendored by default, but a tarball is created by "make vendor"
# and provided in the release. Used by freebsd, gentoo, etc. # and provided in the release. Used by gentoo, etc.
vendor/ vendor/
vendor.tgz vendor.tgz

View file

@ -1,17 +1,9 @@
# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml # https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
run:
skip-dirs:
- pkg/time/rate
skip-files:
- pkg/database/ent/generate.go
- pkg/yamlpatch/merge.go
- pkg/yamlpatch/merge_test.go
linters-settings: linters-settings:
cyclop: cyclop:
# lower this after refactoring # lower this after refactoring
max-complexity: 66 max-complexity: 48
gci: gci:
sections: sections:
@ -20,43 +12,53 @@ linters-settings:
- prefix(github.com/crowdsecurity) - prefix(github.com/crowdsecurity)
- prefix(github.com/crowdsecurity/crowdsec) - prefix(github.com/crowdsecurity/crowdsec)
gomoddirectives:
replace-allow-list:
- golang.org/x/time/rate
gocognit: gocognit:
# lower this after refactoring # lower this after refactoring
min-complexity: 145 min-complexity: 145
gocyclo: gocyclo:
# lower this after refactoring # lower this after refactoring
min-complexity: 64 min-complexity: 48
funlen: funlen:
# Checks the number of lines in a function. # Checks the number of lines in a function.
# If lower than 0, disable the check. # If lower than 0, disable the check.
# Default: 60 # Default: 60
lines: -1 # lower this after refactoring
lines: 437
# Checks the number of statements in a function. # Checks the number of statements in a function.
# If lower than 0, disable the check. # If lower than 0, disable the check.
# Default: 40 # Default: 40
statements: -1 # lower this after refactoring
statements: 122
govet: govet:
check-shadowing: true enable-all: true
disable:
- reflectvaluecompare
- fieldalignment
lll: lll:
line-length: 140 # lower this after refactoring
line-length: 2607
maintidx: maintidx:
# raise this after refactoring # raise this after refactoring
under: 9 under: 11
misspell: misspell:
locale: US locale: US
nestif: nestif:
# lower this after refactoring # lower this after refactoring
min-complexity: 27 min-complexity: 28
nlreturn: nlreturn:
block-size: 4 block-size: 5
nolintlint: nolintlint:
allow-unused: false # report any unused nolint directives allow-unused: false # report any unused nolint directives
@ -68,10 +70,57 @@ linters-settings:
depguard: depguard:
rules: rules:
main: wrap:
deny: deny:
- pkg: "github.com/pkg/errors" - pkg: "github.com/pkg/errors"
desc: "errors.Wrap() is deprecated in favor of fmt.Errorf()" desc: "errors.Wrap() is deprecated in favor of fmt.Errorf()"
files:
- "!**/pkg/database/*.go"
- "!**/pkg/exprhelpers/*.go"
- "!**/pkg/acquisition/modules/appsec/appsec.go"
- "!**/pkg/acquisition/modules/loki/internal/lokiclient/loki_client.go"
- "!**/pkg/apiserver/controllers/v1/errors.go"
yaml:
files:
- "!**/pkg/acquisition/acquisition.go"
- "!**/pkg/acquisition/acquisition_test.go"
- "!**/pkg/acquisition/modules/appsec/appsec.go"
- "!**/pkg/acquisition/modules/cloudwatch/cloudwatch.go"
- "!**/pkg/acquisition/modules/docker/docker.go"
- "!**/pkg/acquisition/modules/file/file.go"
- "!**/pkg/acquisition/modules/journalctl/journalctl.go"
- "!**/pkg/acquisition/modules/kafka/kafka.go"
- "!**/pkg/acquisition/modules/kinesis/kinesis.go"
- "!**/pkg/acquisition/modules/kubernetesaudit/k8s_audit.go"
- "!**/pkg/acquisition/modules/loki/loki.go"
- "!**/pkg/acquisition/modules/loki/timestamp_test.go"
- "!**/pkg/acquisition/modules/s3/s3.go"
- "!**/pkg/acquisition/modules/syslog/syslog.go"
- "!**/pkg/acquisition/modules/wineventlog/wineventlog_windows.go"
- "!**/pkg/appsec/appsec.go"
- "!**/pkg/appsec/loader.go"
- "!**/pkg/csplugin/broker.go"
- "!**/pkg/csplugin/broker_test.go"
- "!**/pkg/dumps/bucket_dump.go"
- "!**/pkg/dumps/parser_dump.go"
- "!**/pkg/hubtest/coverage.go"
- "!**/pkg/hubtest/hubtest_item.go"
- "!**/pkg/hubtest/parser_assert.go"
- "!**/pkg/hubtest/scenario_assert.go"
- "!**/pkg/leakybucket/buckets_test.go"
- "!**/pkg/leakybucket/manager_load.go"
- "!**/pkg/metabase/metabase.go"
- "!**/pkg/parser/node.go"
- "!**/pkg/parser/node_test.go"
- "!**/pkg/parser/parsing_test.go"
- "!**/pkg/parser/stage.go"
deny:
- pkg: "gopkg.in/yaml.v2"
desc: "yaml.v2 is deprecated for new code in favor of yaml.v3"
wsl:
# Allow blocks to end with comments
allow-trailing-comment: true
linters: linters:
enable-all: true enable-all: true
@ -79,23 +128,30 @@ linters:
# #
# DEPRECATED by golangi-lint # DEPRECATED by golangi-lint
# #
- deadcode # The owner seems to have abandoned the linter. Replaced by unused. - deadcode
- exhaustivestruct # The owner seems to have abandoned the linter. Replaced by exhaustruct. - exhaustivestruct
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes - golint
- ifshort # Checks that your code uses short syntax for if-statements whenever possible - ifshort
- interfacer # Linter that suggests narrower interface types - interfacer
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted - maligned
- nosnakecase # nosnakecase is a linter that detects snake case of variable naming and function name. - nosnakecase
- scopelint # Scopelint checks for unpinned variables in go programs - scopelint
- structcheck # The owner seems to have abandoned the linter. Replaced by unused. - structcheck
- varcheck # The owner seems to have abandoned the linter. Replaced by unused. - varcheck
#
# Disabled until fixed for go 1.22
#
- copyloopvar # copyloopvar is a linter detects places where loop variables are copied
- intrange # intrange is a linter to find places where for loops could make use of an integer range.
# #
# Enabled # Enabled
# #
# - asasalint # check for pass []any as any in variadic func(...any) # - asasalint # check for pass []any as any in variadic func(...any)
# - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers # - asciicheck # checks that all code identifiers does not have non-ASCII symbols in the name
# - bidichk # Checks for dangerous unicode character sequences # - bidichk # Checks for dangerous unicode character sequences
# - bodyclose # checks whether HTTP response body is closed successfully # - bodyclose # checks whether HTTP response body is closed successfully
# - cyclop # checks function and package cyclomatic complexity # - cyclop # checks function and package cyclomatic complexity
@ -103,12 +159,15 @@ linters:
# - depguard # Go linter that checks if package imports are in a list of acceptable packages # - depguard # Go linter that checks if package imports are in a list of acceptable packages
# - dupword # checks for duplicate words in the source code # - dupword # checks for duplicate words in the source code
# - durationcheck # check for two durations multiplied together # - durationcheck # check for two durations multiplied together
# - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases # - errcheck # errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases
# - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. # - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
# - execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds
# - exportloopref # checks for pointers to enclosing loop variables # - exportloopref # checks for pointers to enclosing loop variables
# - funlen # Tool for detection of long functions # - funlen # Tool for detection of long functions
# - ginkgolinter # enforces standards of using ginkgo and gomega # - ginkgolinter # enforces standards of using ginkgo and gomega
# - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid.
# - gochecknoinits # Checks that no init functions are present in Go code # - gochecknoinits # Checks that no init functions are present in Go code
# - gochecksumtype # Run exhaustiveness checks on Go "sum types"
# - gocognit # Computes and checks the cognitive complexity of functions # - gocognit # Computes and checks the cognitive complexity of functions
# - gocritic # Provides diagnostics that check for bugs, performance and style issues. # - gocritic # Provides diagnostics that check for bugs, performance and style issues.
# - gocyclo # Computes and checks the cyclomatic complexity of functions # - gocyclo # Computes and checks the cyclomatic complexity of functions
@ -116,56 +175,63 @@ linters:
# - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. # - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
# - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. # - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
# - goprintffuncname # Checks that printf-like functions are named with `f` at the end # - goprintffuncname # Checks that printf-like functions are named with `f` at the end
# - gosimple # (megacheck): Linter for Go source code that specializes in simplifying a code # - gosimple # (megacheck): Linter for Go source code that specializes in simplifying code
# - govet # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string # - gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase
# - grouper # An analyzer to analyze expression groups. # - govet # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes.
# - grouper # Analyze expression groups.
# - importas # Enforces consistent import aliases # - importas # Enforces consistent import aliases
# - ineffassign # Detects when assignments to existing variables are not used # - ineffassign # Detects when assignments to existing variables are not used
# - interfacebloat # A linter that checks the number of methods inside an interface. # - interfacebloat # A linter that checks the number of methods inside an interface.
# - lll # Reports long lines
# - loggercheck # (logrlint): Checks key value pairs for common logger libraries (kitlog,klog,logr,zap).
# - logrlint # Check logr arguments. # - logrlint # Check logr arguments.
# - maintidx # maintidx measures the maintainability index of each function. # - maintidx # maintidx measures the maintainability index of each function.
# - makezero # Finds slice declarations with non-zero initial length # - makezero # Finds slice declarations with non-zero initial length
# - misspell # Finds commonly misspelled English words in comments # - mirror # reports wrong mirror patterns of bytes/strings usage
# - nakedret # Finds naked returns in functions greater than a specified function length # - misspell # Finds commonly misspelled English words
# - nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero).
# - nestif # Reports deeply nested if statements # - nestif # Reports deeply nested if statements
# - nilerr # Finds the code that returns nil even if it checks that the error is not nil. # - nilerr # Finds the code that returns nil even if it checks that the error is not nil.
# - nolintlint # Reports ill-formed or insufficient nolint directives # - nolintlint # Reports ill-formed or insufficient nolint directives
# - nonamedreturns # Reports all named returns # - nonamedreturns # Reports all named returns
# - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. # - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL.
# - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative.
# - predeclared # find code that shadows one of Go's predeclared identifiers # - predeclared # find code that shadows one of Go's predeclared identifiers
# - reassign # Checks that package variables are not reassigned # - reassign # Checks that package variables are not reassigned
# - rowserrcheck # checks whether Err of rows is checked successfully # - rowserrcheck # checks whether Rows.Err of rows is checked successfully
# - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. # - sloglint # ensure consistent code style when using log/slog
# - staticcheck # (megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks # - spancheck # Checks for mistakes with OpenTelemetry/Census spans.
# - testableexamples # linter checks if examples are testable (have an expected output) # - sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed.
# - staticcheck # (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint.
# - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 # - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17
# - testableexamples # linter checks if examples are testable (have an expected output)
# - testifylint # Checks usage of github.com/stretchr/testify.
# - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes # - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
# - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
# - unconvert # Remove unnecessary type conversions # - unconvert # Remove unnecessary type conversions
# - unused # (megacheck): Checks Go code for unused constants, variables, functions and types # - unused # (megacheck): Checks Go code for unused constants, variables, functions and types
# - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. # - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library.
# - wastedassign # wastedassign finds wasted assignment statements. # - wastedassign # Finds wasted assignment statements
# - zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg`
# #
# Recommended? (easy) # Recommended? (easy)
# #
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted.
- exhaustive # check exhaustiveness of enum switch statements - exhaustive # check exhaustiveness of enum switch statements
- gci # Gci control golang package import order and make it always deterministic. - gci # Gci control golang package import order and make it always deterministic.
- godot # Check if comments end in a period - godot # Check if comments end in a period
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. - goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode.
- gosec # (gas): Inspects source code for security problems - gosec # (gas): Inspects source code for security problems
- inamedparam # reports interfaces with unnamed method parameters - inamedparam # reports interfaces with unnamed method parameters
- lll # Reports long lines
- musttag # enforce field tags in (un)marshaled structs - musttag # enforce field tags in (un)marshaled structs
- promlinter # Check Prometheus metrics naming via promlint - promlinter # Check Prometheus metrics naming via promlint
- protogetter # Reports direct reads from proto message fields when getters should be used - protogetter # Reports direct reads from proto message fields when getters should be used
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
- tagalign # check that struct tags are well aligned - tagalign # check that struct tags are well aligned
- thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - thelper # thelper detects tests helpers which is not start with t.Helper() method.
- wrapcheck # Checks that errors returned from external packages are wrapped - wrapcheck # Checks that errors returned from external packages are wrapped
# #
@ -173,12 +239,12 @@ linters:
# #
- containedctx # containedctx is a linter that detects struct contained context.Context field - containedctx # containedctx is a linter that detects struct contained context.Context field
- contextcheck # check the function whether use a non-inherited context - contextcheck # check whether the function uses a non-inherited context
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.
- gomnd # An analyzer to detect magic numbers. - gomnd # An analyzer to detect magic numbers.
- ireturn # Accept Interfaces, Return Concrete Types - ireturn # Accept Interfaces, Return Concrete Types
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value.
- noctx # noctx finds sending http request without context.Context - noctx # Finds sending http request without context.Context
- unparam # Reports unused function parameters - unparam # Reports unused function parameters
# #
@ -187,8 +253,8 @@ linters:
- gofumpt # Gofumpt checks whether code was gofumpt-ed. - gofumpt # Gofumpt checks whether code was gofumpt-ed.
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- whitespace # Tool for detection of leading and trailing whitespace - whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
- wsl # Whitespace Linter - Forces you to use empty lines! - wsl # add or remove empty lines
# #
# Well intended, but not ready for this # Well intended, but not ready for this
@ -196,17 +262,16 @@ linters:
- dupl # Tool for code clone detection - dupl # Tool for code clone detection
- forcetypeassert # finds forced type assertions - forcetypeassert # finds forced type assertions
- godox # Tool for detection of FIXME, TODO and other comment keywords - godox # Tool for detection of FIXME, TODO and other comment keywords
- goerr113 # Golang linter to check the errors handling expressions - goerr113 # Go linter to check the errors handling expressions
- paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test - paralleltest # Detects missing usage of t.Parallel() method in your Go test
- testpackage # linter that makes you use a separate _test package - testpackage # linter that makes you use a separate _test package
# #
# Too strict / too many false positives (for now?) # Too strict / too many false positives (for now?)
# #
- execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds
- exhaustruct # Checks if all structure fields are initialized - exhaustruct # Checks if all structure fields are initialized
- forbidigo # Forbids identifiers - forbidigo # Forbids identifiers
- gochecknoglobals # check that no global variables exist - gochecknoglobals # Check that no global variables exist.
- goconst # Finds repeated strings that could be replaced by a constant - goconst # Finds repeated strings that could be replaced by a constant
- stylecheck # Stylecheck is a replacement for golint - stylecheck # Stylecheck is a replacement for golint
- tagliatelle # Checks the struct tags. - tagliatelle # Checks the struct tags.
@ -223,15 +288,21 @@ issues:
# “Look, thats why theres rules, understand? So that you think before you # “Look, thats why theres rules, understand? So that you think before you
# break em.” ― Terry Pratchett # break em.” ― Terry Pratchett
exclude-dirs:
- pkg/time/rate
exclude-files:
- pkg/yamlpatch/merge.go
- pkg/yamlpatch/merge_test.go
exclude-generated-strict: true
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclude-rules: exclude-rules:
# Won't fix: # Won't fix:
- path: go.mod
text: "replacement are not allowed: golang.org/x/time/rate"
# `err` is often shadowed, we may continue to do it # `err` is often shadowed, we may continue to do it
- linters: - linters:
- govet - govet
@ -263,6 +334,10 @@ issues:
- perfsprint - perfsprint
text: "fmt.Sprintf can be replaced .*" text: "fmt.Sprintf can be replaced .*"
- linters:
- perfsprint
text: "fmt.Errorf can be replaced with errors.New"
# #
# Will fix, easy but some neurons required # Will fix, easy but some neurons required
# #
@ -295,10 +370,6 @@ issues:
- nosprintfhostport - nosprintfhostport
text: "host:port in url should be constructed with net.JoinHostPort and not directly with fmt.Sprintf" text: "host:port in url should be constructed with net.JoinHostPort and not directly with fmt.Sprintf"
- linters:
- wastedassign
text: "assigned to .*, but reassigned without using the value"
# https://github.com/timakin/bodyclose # https://github.com/timakin/bodyclose
- linters: - linters:
- bodyclose - bodyclose
@ -309,22 +380,3 @@ issues:
- linters: - linters:
- nonamedreturns - nonamedreturns
text: "named return .* with type .* found" text: "named return .* with type .* found"
# https://github.com/alexkohler/nakedret#purpose
- linters:
- nakedret
text: "naked return in func .* with .* lines of code"
#
# Will fix, might be trickier
#
- linters:
- staticcheck
text: "x509.ParseCRL has been deprecated since Go 1.19: Use ParseRevocationList instead"
# https://github.com/pkg/errors/issues/245
- linters:
- depguard
text: "import 'github.com/pkg/errors' is not allowed .*"

View file

@ -1,8 +1,7 @@
# vim: set ft=dockerfile: # vim: set ft=dockerfile:
ARG GOVERSION=1.21.6 FROM golang:1.22.2-alpine3.18 AS build
ARG BUILD_VERSION
FROM golang:${GOVERSION}-alpine3.18 AS build ARG BUILD_VERSION
WORKDIR /go/src/crowdsec WORKDIR /go/src/crowdsec
@ -17,7 +16,7 @@ RUN apk add --no-cache git g++ gcc libc-dev make bash gettext binutils-gold core
cd re2-${RE2_VERSION} && \ cd re2-${RE2_VERSION} && \
make install && \ make install && \
echo "githubciXXXXXXXXXXXXXXXXXXXXXXXX" > /etc/machine-id && \ echo "githubciXXXXXXXXXXXXXXXXXXXXXXXX" > /etc/machine-id && \
go install github.com/mikefarah/yq/v4@v4.40.4 go install github.com/mikefarah/yq/v4@v4.43.1
COPY . . COPY . .
@ -40,31 +39,25 @@ RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/co
mkdir -p /staging/var/lib/crowdsec && \ mkdir -p /staging/var/lib/crowdsec && \
mkdir -p /var/lib/crowdsec/data mkdir -p /var/lib/crowdsec/data
COPY --from=build /go/bin/yq /usr/local/bin/yq COPY --from=build /go/bin/yq /usr/local/bin/crowdsec /usr/local/bin/cscli /usr/local/bin/
COPY --from=build /etc/crowdsec /staging/etc/crowdsec COPY --from=build /etc/crowdsec /staging/etc/crowdsec
COPY --from=build /usr/local/bin/crowdsec /usr/local/bin/crowdsec
COPY --from=build /usr/local/bin/cscli /usr/local/bin/cscli
COPY --from=build /go/src/crowdsec/docker/docker_start.sh / COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
COPY --from=build /var/lib/crowdsec /staging/var/lib/crowdsec
RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml
ENTRYPOINT /bin/bash /docker_start.sh ENTRYPOINT /bin/bash /docker_start.sh
FROM slim as plugins FROM slim as full
# Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp # Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
# The files are here for reference, as users will need to mount a new version to be actually able to use notifications # The files are here for reference, as users will need to mount a new version to be actually able to use notifications
COPY --from=build /go/src/crowdsec/cmd/notification-email/email.yaml /staging/etc/crowdsec/notifications/email.yaml COPY --from=build \
COPY --from=build /go/src/crowdsec/cmd/notification-http/http.yaml /staging/etc/crowdsec/notifications/http.yaml /go/src/crowdsec/cmd/notification-email/email.yaml \
COPY --from=build /go/src/crowdsec/cmd/notification-slack/slack.yaml /staging/etc/crowdsec/notifications/slack.yaml /go/src/crowdsec/cmd/notification-http/http.yaml \
COPY --from=build /go/src/crowdsec/cmd/notification-splunk/splunk.yaml /staging/etc/crowdsec/notifications/splunk.yaml /go/src/crowdsec/cmd/notification-slack/slack.yaml \
COPY --from=build /go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml /staging/etc/crowdsec/notifications/sentinel.yaml /go/src/crowdsec/cmd/notification-splunk/splunk.yaml \
/go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml \
/staging/etc/crowdsec/notifications/
COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
FROM slim as geoip
COPY --from=build /var/lib/crowdsec /staging/var/lib/crowdsec
FROM plugins as full
COPY --from=build /var/lib/crowdsec /staging/var/lib/crowdsec

View file

@ -1,8 +1,7 @@
# vim: set ft=dockerfile: # vim: set ft=dockerfile:
ARG GOVERSION=1.21.6 FROM golang:1.22.2-bookworm AS build
ARG BUILD_VERSION
FROM golang:${GOVERSION}-bookworm AS build ARG BUILD_VERSION
WORKDIR /go/src/crowdsec WORKDIR /go/src/crowdsec
@ -22,7 +21,7 @@ RUN apt-get update && \
make && \ make && \
make install && \ make install && \
echo "githubciXXXXXXXXXXXXXXXXXXXXXXXX" > /etc/machine-id && \ echo "githubciXXXXXXXXXXXXXXXXXXXXXXXX" > /etc/machine-id && \
go install github.com/mikefarah/yq/v4@v4.40.4 go install github.com/mikefarah/yq/v4@v4.43.1
COPY . . COPY . .
@ -56,10 +55,8 @@ RUN apt-get update && \
mkdir -p /staging/var/lib/crowdsec && \ mkdir -p /staging/var/lib/crowdsec && \
mkdir -p /var/lib/crowdsec/data mkdir -p /var/lib/crowdsec/data
COPY --from=build /go/bin/yq /usr/local/bin/yq COPY --from=build /go/bin/yq /usr/local/bin/crowdsec /usr/local/bin/cscli /usr/local/bin/
COPY --from=build /etc/crowdsec /staging/etc/crowdsec COPY --from=build /etc/crowdsec /staging/etc/crowdsec
COPY --from=build /usr/local/bin/crowdsec /usr/local/bin/crowdsec
COPY --from=build /usr/local/bin/cscli /usr/local/bin/cscli
COPY --from=build /go/src/crowdsec/docker/docker_start.sh / COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml && \ RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml && \
@ -71,11 +68,14 @@ FROM slim as plugins
# Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp # Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
# The files are here for reference, as users will need to mount a new version to be actually able to use notifications # The files are here for reference, as users will need to mount a new version to be actually able to use notifications
COPY --from=build /go/src/crowdsec/cmd/notification-email/email.yaml /staging/etc/crowdsec/notifications/email.yaml COPY --from=build \
COPY --from=build /go/src/crowdsec/cmd/notification-http/http.yaml /staging/etc/crowdsec/notifications/http.yaml /go/src/crowdsec/cmd/notification-email/email.yaml \
COPY --from=build /go/src/crowdsec/cmd/notification-slack/slack.yaml /staging/etc/crowdsec/notifications/slack.yaml /go/src/crowdsec/cmd/notification-http/http.yaml \
COPY --from=build /go/src/crowdsec/cmd/notification-splunk/splunk.yaml /staging/etc/crowdsec/notifications/splunk.yaml /go/src/crowdsec/cmd/notification-slack/slack.yaml \
COPY --from=build /go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml /staging/etc/crowdsec/notifications/sentinel.yaml /go/src/crowdsec/cmd/notification-splunk/splunk.yaml \
/go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml \
/staging/etc/crowdsec/notifications/
COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
FROM slim as geoip FROM slim as geoip

View file

@ -202,6 +202,10 @@ cscli: goversion ## Build cscli
crowdsec: goversion ## Build crowdsec crowdsec: goversion ## Build crowdsec
@$(MAKE) -C $(CROWDSEC_FOLDER) build $(MAKE_FLAGS) @$(MAKE) -C $(CROWDSEC_FOLDER) build $(MAKE_FLAGS)
.PHONY: generate
generate: ## Generate code for the database and APIs
$(GO) generate ./pkg/database/ent
$(GO) generate ./pkg/models
.PHONY: testclean .PHONY: testclean
testclean: bats-clean ## Remove test artifacts testclean: bats-clean ## Remove test artifacts

View file

@ -15,19 +15,13 @@ pool:
stages: stages:
- stage: Build - stage: Build
jobs: jobs:
- job: - job: Build
displayName: "Build" displayName: "Build"
steps: steps:
- task: DotNetCoreCLI@2
displayName: "Install SignClient"
inputs:
command: 'custom'
custom: 'tool'
arguments: 'install --global SignClient --version 1.3.155'
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go" displayName: "Install Go"
inputs: inputs:
version: '1.21.6' version: '1.22.2'
- pwsh: | - pwsh: |
choco install -y make choco install -y make
@ -39,24 +33,14 @@ stages:
#we are not calling make windows_installer because we want to sign the binaries before they are added to the MSI #we are not calling make windows_installer because we want to sign the binaries before they are added to the MSI
script: | script: |
make build BUILD_RE2_WASM=1 make build BUILD_RE2_WASM=1
- 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: | - pwsh: |
$build_version=$env:BUILD_SOURCEBRANCHNAME $build_version=$env:BUILD_SOURCEBRANCHNAME
#Override the version if it's set in the pipeline
if ( ${env:USERBUILDVERSION} -ne "")
{
$build_version = ${env:USERBUILDVERSION}
}
if ($build_version.StartsWith("v")) if ($build_version.StartsWith("v"))
{ {
$build_version = $build_version.Substring(1) $build_version = $build_version.Substring(1)
@ -69,35 +53,112 @@ stages:
displayName: GetCrowdsecVersion displayName: GetCrowdsecVersion
name: GetCrowdsecVersion name: GetCrowdsecVersion
- pwsh: | - pwsh: |
.\make_installer.ps1 -version '$(GetCrowdsecVersion.BuildVersion)' Get-ChildItem -Path .\cmd -Directory | ForEach-Object {
$dirName = $_.Name
Get-ChildItem -Path .\cmd\$dirName -File -Filter '*.exe' | ForEach-Object {
$fileName = $_.Name
$destDir = Join-Path $(Build.ArtifactStagingDirectory) cmd\$dirName
New-Item -ItemType Directory -Path $destDir -Force
Copy-Item -Path .\cmd\$dirName\$fileName -Destination $destDir
}
}
displayName: "Copy binaries to staging directory"
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'unsigned_binaries'
displayName: "Upload binaries artifact"
- stage: Sign
dependsOn: Build
variables:
- group: 'FOSS Build Variables'
- name: BuildVersion
value: $[ stageDependencies.Build.Build.outputs['GetCrowdsecVersion.BuildVersion'] ]
condition: succeeded()
jobs:
- job: Sign
displayName: "Sign"
steps:
- download: current
artifact: unsigned_binaries
displayName: "Download binaries artifact"
- task: CopyFiles@2
inputs:
SourceFolder: '$(Pipeline.Workspace)/unsigned_binaries'
TargetFolder: '$(Build.SourcesDirectory)'
displayName: "Copy binaries to workspace"
- task: DotNetCoreCLI@2
displayName: "Install SignTool tool"
inputs:
command: 'custom'
custom: 'tool'
arguments: install --global sign --version 0.9.0-beta.23127.3
- task: AzureKeyVault@2
displayName: "Get signing parameters"
inputs:
azureSubscription: "Azure subscription"
KeyVaultName: "$(KeyVaultName)"
SecretsFilter: "TenantId,ClientId,ClientSecret,Certificate,KeyVaultUrl"
- pwsh: |
sign code azure-key-vault `
"**/*.exe" `
--base-directory "$(Build.SourcesDirectory)/cmd/" `
--publisher-name "CrowdSec" `
--description "CrowdSec" `
--description-url "https://github.com/crowdsecurity/crowdsec" `
--azure-key-vault-tenant-id "$(TenantId)" `
--azure-key-vault-client-id "$(ClientId)" `
--azure-key-vault-client-secret "$(ClientSecret)" `
--azure-key-vault-certificate "$(Certificate)" `
--azure-key-vault-url "$(KeyVaultUrl)"
displayName: "Sign crowdsec binaries"
- pwsh: |
.\make_installer.ps1 -version '$(BuildVersion)'
displayName: "Build Crowdsec MSI" displayName: "Build Crowdsec MSI"
name: BuildMSI name: BuildMSI
- pwsh: | - pwsh: |
.\make_chocolatey.ps1 -version '$(GetCrowdsecVersion.BuildVersion)' .\make_chocolatey.ps1 -version '$(BuildVersion)'
displayName: "Build Chocolatey nupkg" displayName: "Build Chocolatey nupkg"
- pwsh: | - pwsh: |
SignClient.exe Sign --name "crowdsec-msi" ` sign code azure-key-vault `
--input "*.msi" --config (Join-Path -Path $(Agent.TempDirectory) -ChildPath "appsettings.json") ` "*.msi" `
--user $(CodeSigningUser) --secret '$(CodeSigningPassword)' --base-directory "$(Build.SourcesDirectory)" `
displayName: "Sign Crowdsec MSI" --publisher-name "CrowdSec" `
--description "CrowdSec" `
- task: PublishBuildArtifacts@1 --description-url "https://github.com/crowdsecurity/crowdsec" `
--azure-key-vault-tenant-id "$(TenantId)" `
--azure-key-vault-client-id "$(ClientId)" `
--azure-key-vault-client-secret "$(ClientSecret)" `
--azure-key-vault-certificate "$(Certificate)" `
--azure-key-vault-url "$(KeyVaultUrl)"
displayName: "Sign MSI package"
- pwsh: |
sign code azure-key-vault `
"*.nupkg" `
--base-directory "$(Build.SourcesDirectory)" `
--publisher-name "CrowdSec" `
--description "CrowdSec" `
--description-url "https://github.com/crowdsecurity/crowdsec" `
--azure-key-vault-tenant-id "$(TenantId)" `
--azure-key-vault-client-id "$(ClientId)" `
--azure-key-vault-client-secret "$(ClientSecret)" `
--azure-key-vault-certificate "$(Certificate)" `
--azure-key-vault-url "$(KeyVaultUrl)"
displayName: "Sign nuget package"
- task: PublishPipelineArtifact@1
inputs: inputs:
PathtoPublish: '$(Build.Repository.LocalPath)\\crowdsec_$(GetCrowdsecVersion.BuildVersion).msi' targetPath: '$(Build.SourcesDirectory)/crowdsec_$(BuildVersion).msi'
ArtifactName: 'crowdsec.msi' artifact: 'signed_msi_package'
publishLocation: 'Container' displayName: "Upload signed MSI artifact"
displayName: "Upload MSI artifact" - task: PublishPipelineArtifact@1
- task: PublishBuildArtifacts@1
inputs: inputs:
PathtoPublish: '$(Build.Repository.LocalPath)\\windows\\Chocolatey\\crowdsec\\crowdsec.$(GetCrowdsecVersion.BuildVersion).nupkg' targetPath: '$(Build.SourcesDirectory)/crowdsec.$(BuildVersion).nupkg'
ArtifactName: 'crowdsec.nupkg' artifact: 'signed_nuget_package'
publishLocation: 'Container' displayName: "Upload signed nuget artifact"
displayName: "Upload nupkg artifact"
- stage: Publish - stage: Publish
dependsOn: Build dependsOn: Sign
jobs: jobs:
- deployment: "Publish" - deployment: "Publish"
displayName: "Publish to GitHub" displayName: "Publish to GitHub"
@ -119,8 +180,7 @@ stages:
assetUploadMode: 'replace' assetUploadMode: 'replace'
addChangeLog: false addChangeLog: false
isPreRelease: true #we force prerelease because the pipeline is invoked on tag creation, which happens when we do a prerelease 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: | assets: |
$(Build.ArtifactStagingDirectory)\..\crowdsec.msi/*.msi $(Pipeline.Workspace)/signed_msi_package/*.msi
$(Build.ArtifactStagingDirectory)\..\crowdsec.nupkg/*.nupkg $(Pipeline.Workspace)/signed_nuget_package/*.nupkg
condition: ne(variables['GetLatestPrelease.LatestPreRelease'], '') condition: ne(variables['GetLatestPrelease.LatestPreRelease'], '')

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@ -11,13 +12,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
"time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
@ -30,83 +30,46 @@ import (
func DecisionsFromAlert(alert *models.Alert) string { func DecisionsFromAlert(alert *models.Alert) string {
ret := "" ret := ""
var decMap = make(map[string]int) decMap := make(map[string]int)
for _, decision := range alert.Decisions { for _, decision := range alert.Decisions {
k := *decision.Type k := *decision.Type
if *decision.Simulated { if *decision.Simulated {
k = fmt.Sprintf("(simul)%s", k) k = fmt.Sprintf("(simul)%s", k)
} }
v := decMap[k] v := decMap[k]
decMap[k] = v + 1 decMap[k] = v + 1
} }
for k, v := range decMap { for k, v := range decMap {
if len(ret) > 0 { if len(ret) > 0 {
ret += " " ret += " "
} }
ret += fmt.Sprintf("%s:%d", k, v) ret += fmt.Sprintf("%s:%d", k, v)
} }
return ret return ret
} }
func DateFromAlert(alert *models.Alert) string { func (cli *cliAlerts) alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
ts, err := time.Parse(time.RFC3339, alert.CreatedAt) switch cli.cfg().Cscli.Output {
if err != nil { case "raw":
log.Infof("while parsing %s with %s : %s", alert.CreatedAt, time.RFC3339, err)
return alert.CreatedAt
}
return ts.Format(time.RFC822)
}
func SourceFromAlert(alert *models.Alert) string {
//more than one item, just number and scope
if len(alert.Decisions) > 1 {
return fmt.Sprintf("%d %ss (%s)", len(alert.Decisions), *alert.Decisions[0].Scope, *alert.Decisions[0].Origin)
}
//fallback on single decision information
if len(alert.Decisions) == 1 {
return fmt.Sprintf("%s:%s", *alert.Decisions[0].Scope, *alert.Decisions[0].Value)
}
//try to compose a human friendly version
if *alert.Source.Value != "" && *alert.Source.Scope != "" {
scope := ""
scope = fmt.Sprintf("%s:%s", *alert.Source.Scope, *alert.Source.Value)
extra := ""
if alert.Source.Cn != "" {
extra = alert.Source.Cn
}
if alert.Source.AsNumber != "" {
extra += fmt.Sprintf("/%s", alert.Source.AsNumber)
}
if alert.Source.AsName != "" {
extra += fmt.Sprintf("/%s", alert.Source.AsName)
}
if extra != "" {
scope += " (" + extra + ")"
}
return scope
}
return ""
}
func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
if csConfig.Cscli.Output == "raw" {
csvwriter := csv.NewWriter(os.Stdout) csvwriter := csv.NewWriter(os.Stdout)
header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"} header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
if printMachine { if printMachine {
header = append(header, "machine") header = append(header, "machine")
} }
err := csvwriter.Write(header)
if err != nil { if err := csvwriter.Write(header); err != nil {
return err return err
} }
for _, alertItem := range *alerts { for _, alertItem := range *alerts {
row := []string{ row := []string{
fmt.Sprintf("%d", alertItem.ID), strconv.FormatInt(alertItem.ID, 10),
*alertItem.Source.Scope, *alertItem.Source.Scope,
*alertItem.Source.Value, *alertItem.Source.Value,
*alertItem.Scenario, *alertItem.Scenario,
@ -118,28 +81,32 @@ func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
if printMachine { if printMachine {
row = append(row, alertItem.MachineID) row = append(row, alertItem.MachineID)
} }
err := csvwriter.Write(row)
if err != nil { if err := csvwriter.Write(row); err != nil {
return err return err
} }
} }
csvwriter.Flush() csvwriter.Flush()
} else if csConfig.Cscli.Output == "json" { case "json":
if *alerts == nil { if *alerts == nil {
// avoid returning "null" in json // avoid returning "null" in json
// could be cleaner if we used slice of alerts directly // could be cleaner if we used slice of alerts directly
fmt.Println("[]") fmt.Println("[]")
return nil return nil
} }
x, _ := json.MarshalIndent(alerts, "", " ") x, _ := json.MarshalIndent(alerts, "", " ")
fmt.Printf("%s", string(x)) fmt.Print(string(x))
} else if csConfig.Cscli.Output == "human" { case "human":
if len(*alerts) == 0 { if len(*alerts) == 0 {
fmt.Println("No active alerts") fmt.Println("No active alerts")
return nil return nil
} }
alertsTable(color.Output, alerts, printMachine) alertsTable(color.Output, alerts, printMachine)
} }
return nil return nil
} }
@ -161,85 +128,95 @@ var alertTemplate = `
` `
func DisplayOneAlert(alert *models.Alert, withDetail bool) error { func (cli *cliAlerts) displayOneAlert(alert *models.Alert, withDetail bool) error {
if csConfig.Cscli.Output == "human" { tmpl, err := template.New("alert").Parse(alertTemplate)
tmpl, err := template.New("alert").Parse(alertTemplate) if err != nil {
if err != nil { return err
return err }
}
err = tmpl.Execute(os.Stdout, alert)
if err != nil {
return err
}
alertDecisionsTable(color.Output, alert) if err = tmpl.Execute(os.Stdout, alert); err != nil {
return err
}
if len(alert.Meta) > 0 { alertDecisionsTable(color.Output, alert)
fmt.Printf("\n - Context :\n")
sort.Slice(alert.Meta, func(i, j int) bool { if len(alert.Meta) > 0 {
return alert.Meta[i].Key < alert.Meta[j].Key fmt.Printf("\n - Context :\n")
}) sort.Slice(alert.Meta, func(i, j int) bool {
table := newTable(color.Output) return alert.Meta[i].Key < alert.Meta[j].Key
table.SetRowLines(false) })
table.SetHeaders("Key", "Value")
for _, meta := range alert.Meta { table := newTable(color.Output)
var valSlice []string table.SetRowLines(false)
if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil { table.SetHeaders("Key", "Value")
return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err)
} for _, meta := range alert.Meta {
for _, value := range valSlice { var valSlice []string
table.AddRow( if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
meta.Key, return fmt.Errorf("unknown context value type '%s': %w", meta.Value, err)
value, }
)
} for _, value := range valSlice {
table.AddRow(
meta.Key,
value,
)
} }
table.Render()
} }
if withDetail { table.Render()
fmt.Printf("\n - Events :\n") }
for _, event := range alert.Events {
alertEventTable(color.Output, event) if withDetail {
} fmt.Printf("\n - Events :\n")
for _, event := range alert.Events {
alertEventTable(color.Output, event)
} }
} }
return nil return nil
} }
type cliAlerts struct{} type cliAlerts struct {
client *apiclient.ApiClient
func NewCLIAlerts() *cliAlerts { cfg configGetter
return &cliAlerts{}
} }
func (cli cliAlerts) NewCommand() *cobra.Command { func NewCLIAlerts(getconfig configGetter) *cliAlerts {
return &cliAlerts{
cfg: getconfig,
}
}
func (cli *cliAlerts) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "alerts [action]", Use: "alerts [action]",
Short: "Manage alerts", Short: "Manage alerts",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
Aliases: []string{"alert"}, Aliases: []string{"alert"},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
var err error cfg := cli.cfg()
if err := csConfig.LoadAPIClient(); err != nil { if err := cfg.LoadAPIClient(); err != nil {
return fmt.Errorf("loading api client: %w", err) return fmt.Errorf("loading api client: %w", err)
} }
apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL) apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
if err != nil { if err != nil {
return fmt.Errorf("parsing api url %s: %w", apiURL, err) return fmt.Errorf("parsing api url %s: %w", apiURL, err)
} }
Client, err = apiclient.NewClient(&apiclient.Config{
MachineID: csConfig.API.Client.Credentials.Login, cli.client, err = apiclient.NewClient(&apiclient.Config{
Password: strfmt.Password(csConfig.API.Client.Credentials.Password), MachineID: cfg.API.Client.Credentials.Login,
Password: strfmt.Password(cfg.API.Client.Credentials.Password),
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiURL, URL: apiURL,
VersionPrefix: "v1", VersionPrefix: "v1",
}) })
if err != nil { if err != nil {
return fmt.Errorf("new api client: %w", err) return fmt.Errorf("new api client: %w", err)
} }
return nil return nil
}, },
} }
@ -252,8 +229,8 @@ func (cli cliAlerts) NewCommand() *cobra.Command {
return cmd return cmd
} }
func (cli cliAlerts) NewListCmd() *cobra.Command { func (cli *cliAlerts) NewListCmd() *cobra.Command {
var alertListFilter = apiclient.AlertsListOpts{ alertListFilter := apiclient.AlertsListOpts{
ScopeEquals: new(string), ScopeEquals: new(string),
ValueEquals: new(string), ValueEquals: new(string),
ScenarioEquals: new(string), ScenarioEquals: new(string),
@ -265,8 +242,10 @@ func (cli cliAlerts) NewListCmd() *cobra.Command {
IncludeCAPI: new(bool), IncludeCAPI: new(bool),
OriginEquals: new(string), OriginEquals: new(string),
} }
limit := new(int) limit := new(int)
contained := new(bool) contained := new(bool)
var printMachine bool var printMachine bool
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -275,12 +254,12 @@ func (cli cliAlerts) NewListCmd() *cobra.Command {
Example: `cscli alerts list Example: `cscli alerts list
cscli alerts list --ip 1.2.3.4 cscli alerts list --ip 1.2.3.4
cscli alerts list --range 1.2.3.0/24 cscli alerts list --range 1.2.3.0/24
cscli alerts list --origin lists
cscli alerts list -s crowdsecurity/ssh-bf cscli alerts list -s crowdsecurity/ssh-bf
cscli alerts list --type ban`, cscli alerts list --type ban`,
Long: `List alerts with optional filters`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
var err error
if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals, if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil { alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
printHelp(cmd) printHelp(cmd)
@ -346,49 +325,55 @@ cscli alerts list --type ban`,
alertListFilter.Contains = new(bool) alertListFilter.Contains = new(bool)
} }
alerts, _, err := Client.Alerts.List(context.Background(), alertListFilter) alerts, _, err := cli.client.Alerts.List(context.Background(), alertListFilter)
if err != nil { if err != nil {
return fmt.Errorf("unable to list alerts: %v", err) return fmt.Errorf("unable to list alerts: %w", err)
} }
err = AlertsToTable(alerts, printMachine) if err = cli.alertsToTable(alerts, printMachine); err != nil {
if err != nil { return fmt.Errorf("unable to list alerts: %w", err)
return fmt.Errorf("unable to list alerts: %v", err)
} }
return nil return nil
}, },
} }
cmd.Flags().SortFlags = false
cmd.Flags().BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API") flags := cmd.Flags()
cmd.Flags().StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)") flags.SortFlags = false
cmd.Flags().StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)") flags.BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
cmd.Flags().StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)") flags.StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
cmd.Flags().StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)") flags.StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
cmd.Flags().StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)") flags.StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
cmd.Flags().StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)") flags.StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
cmd.Flags().StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)") flags.StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
cmd.Flags().StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope") flags.StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
cmd.Flags().StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ","))) flags.StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range") flags.StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
cmd.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts") flags.StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
cmd.Flags().IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)") flags.BoolVar(contained, "contained", false, "query decisions contained by range")
flags.BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
flags.IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
return cmd return cmd
} }
func (cli cliAlerts) NewDeleteCmd() *cobra.Command { func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
var ActiveDecision *bool var (
var AlertDeleteAll bool ActiveDecision *bool
var delAlertByID string AlertDeleteAll bool
contained := new(bool) delAlertByID string
var alertDeleteFilter = apiclient.AlertsDeleteOpts{ )
alertDeleteFilter := apiclient.AlertsDeleteOpts{
ScopeEquals: new(string), ScopeEquals: new(string),
ValueEquals: new(string), ValueEquals: new(string),
ScenarioEquals: new(string), ScenarioEquals: new(string),
IPEquals: new(string), IPEquals: new(string),
RangeEquals: new(string), RangeEquals: new(string),
} }
contained := new(bool)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "delete [filters] [--all]", Use: "delete [filters] [--all]",
Short: `Delete alerts Short: `Delete alerts
@ -399,7 +384,7 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Aliases: []string{"remove"}, Aliases: []string{"remove"},
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, _ []string) error {
if AlertDeleteAll { if AlertDeleteAll {
return nil return nil
} }
@ -407,16 +392,16 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
*alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" && *alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" &&
*alertDeleteFilter.RangeEquals == "" && delAlertByID == "" { *alertDeleteFilter.RangeEquals == "" && delAlertByID == "" {
_ = cmd.Usage() _ = cmd.Usage()
return fmt.Errorf("at least one filter or --all must be specified") return errors.New("at least one filter or --all must be specified")
} }
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
var err error var err error
if !AlertDeleteAll { if !AlertDeleteAll {
if err := manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals, if err = manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil { alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
printHelp(cmd) printHelp(cmd)
return err return err
@ -452,14 +437,14 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
var alerts *models.DeleteAlertsResponse var alerts *models.DeleteAlertsResponse
if delAlertByID == "" { if delAlertByID == "" {
alerts, _, err = Client.Alerts.Delete(context.Background(), alertDeleteFilter) alerts, _, err = cli.client.Alerts.Delete(context.Background(), alertDeleteFilter)
if err != nil { if err != nil {
return fmt.Errorf("unable to delete alerts : %v", err) return fmt.Errorf("unable to delete alerts: %w", err)
} }
} else { } else {
alerts, _, err = Client.Alerts.DeleteOne(context.Background(), delAlertByID) alerts, _, err = cli.client.Alerts.DeleteOne(context.Background(), delAlertByID)
if err != nil { if err != nil {
return fmt.Errorf("unable to delete alert: %v", err) return fmt.Errorf("unable to delete alert: %w", err)
} }
} }
log.Infof("%s alert(s) deleted", alerts.NbDeleted) log.Infof("%s alert(s) deleted", alerts.NbDeleted)
@ -467,90 +452,99 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
return nil return nil
}, },
} }
cmd.Flags().SortFlags = false
cmd.Flags().StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)") flags := cmd.Flags()
cmd.Flags().StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope") flags.SortFlags = false
cmd.Flags().StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)") flags.StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
cmd.Flags().StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)") flags.StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
cmd.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)") flags.StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
cmd.Flags().StringVar(&delAlertByID, "id", "", "alert ID") flags.StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
cmd.Flags().BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts") flags.StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range") flags.StringVar(&delAlertByID, "id", "", "alert ID")
flags.BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
flags.BoolVar(contained, "contained", false, "query decisions contained by range")
return cmd return cmd
} }
func (cli cliAlerts) NewInspectCmd() *cobra.Command { func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
var details bool var details bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: `inspect "alert_id"`, Use: `inspect "alert_id"`,
Short: `Show info about an alert`, Short: `Show info about an alert`,
Example: `cscli alerts inspect 123`, Example: `cscli alerts inspect 123`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cfg := cli.cfg()
if len(args) == 0 { if len(args) == 0 {
printHelp(cmd) printHelp(cmd)
return fmt.Errorf("missing alert_id") return errors.New("missing alert_id")
} }
for _, alertID := range args { for _, alertID := range args {
id, err := strconv.Atoi(alertID) id, err := strconv.Atoi(alertID)
if err != nil { if err != nil {
return fmt.Errorf("bad alert id %s", alertID) return fmt.Errorf("bad alert id %s", alertID)
} }
alert, _, err := Client.Alerts.GetByID(context.Background(), id) alert, _, err := cli.client.Alerts.GetByID(context.Background(), id)
if err != nil { if err != nil {
return fmt.Errorf("can't find alert with id %s: %s", alertID, err) return fmt.Errorf("can't find alert with id %s: %w", alertID, err)
} }
switch csConfig.Cscli.Output { switch cfg.Cscli.Output {
case "human": case "human":
if err := DisplayOneAlert(alert, details); err != nil { if err := cli.displayOneAlert(alert, details); err != nil {
continue continue
} }
case "json": case "json":
data, err := json.MarshalIndent(alert, "", " ") data, err := json.MarshalIndent(alert, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err) return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
} }
fmt.Printf("%s\n", string(data)) fmt.Printf("%s\n", string(data))
case "raw": case "raw":
data, err := yaml.Marshal(alert) data, err := yaml.Marshal(alert)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err) return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
} }
fmt.Printf("%s\n", string(data)) fmt.Println(string(data))
} }
} }
return nil return nil
}, },
} }
cmd.Flags().SortFlags = false cmd.Flags().SortFlags = false
cmd.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events") cmd.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")
return cmd return cmd
} }
func (cli cliAlerts) NewFlushCmd() *cobra.Command { func (cli *cliAlerts) NewFlushCmd() *cobra.Command {
var maxItems int var (
var maxAge string maxItems int
maxAge string
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: `flush`, Use: `flush`,
Short: `Flush alerts Short: `Flush alerts
/!\ This command can be used only on the same machine than the local API`, /!\ This command can be used only on the same machine than the local API`,
Example: `cscli alerts flush --max-items 1000 --max-age 7d`, Example: `cscli alerts flush --max-items 1000 --max-age 7d`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
var err error cfg := cli.cfg()
if err := require.LAPI(csConfig); err != nil { if err := require.LAPI(cfg); err != nil {
return err return err
} }
dbClient, err = database.NewClient(csConfig.DbConfig) db, err := database.NewClient(cfg.DbConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create new database client: %s", err) return fmt.Errorf("unable to create new database client: %w", err)
} }
log.Info("Flushing alerts. !! This may take a long time !!") log.Info("Flushing alerts. !! This may take a long time !!")
err = dbClient.FlushAlerts(maxAge, maxItems) err = db.FlushAlerts(maxAge, maxItems)
if err != nil { if err != nil {
return fmt.Errorf("unable to flush alerts: %s", err) return fmt.Errorf("unable to flush alerts: %w", err)
} }
log.Info("Alerts flushed") log.Info("Alerts flushed")

View file

@ -3,8 +3,10 @@ package main
import ( import (
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "os"
"slices"
"strings" "strings"
"time" "time"
@ -12,7 +14,6 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"slices"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
@ -20,53 +21,33 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
) )
func getBouncers(out io.Writer, dbClient *database.Client) error { func askYesNo(message string, defaultAnswer bool) (bool, error) {
bouncers, err := dbClient.ListBouncers() var answer bool
if err != nil {
return fmt.Errorf("unable to list bouncers: %s", err) prompt := &survey.Confirm{
Message: message,
Default: defaultAnswer,
} }
switch csConfig.Cscli.Output { if err := survey.AskOne(prompt, &answer); err != nil {
case "human": return defaultAnswer, err
getBouncersTable(out, bouncers)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(bouncers); err != nil {
return fmt.Errorf("failed to unmarshal: %w", err)
}
return nil
case "raw":
csvwriter := csv.NewWriter(out)
err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"})
if err != nil {
return fmt.Errorf("failed to write raw header: %w", err)
}
for _, b := range bouncers {
var revoked string
if !b.Revoked {
revoked = "validated"
} else {
revoked = "pending"
}
err := csvwriter.Write([]string{b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType})
if err != nil {
return fmt.Errorf("failed to write raw: %w", err)
}
}
csvwriter.Flush()
} }
return nil return answer, nil
} }
type cliBouncers struct {} type cliBouncers struct {
db *database.Client
func NewCLIBouncers() *cliBouncers { cfg configGetter
return &cliBouncers{}
} }
func (cli cliBouncers) NewCommand() *cobra.Command { func NewCLIBouncers(cfg configGetter) *cliBouncers {
return &cliBouncers{
cfg: cfg,
}
}
func (cli *cliBouncers) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "bouncers [action]", Use: "bouncers [action]",
Short: "Manage bouncers [requires local API]", Short: "Manage bouncers [requires local API]",
@ -76,94 +57,130 @@ Note: This command requires database direct access, so is intended to be run on
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Aliases: []string{"bouncer"}, Aliases: []string{"bouncer"},
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
var err error var err error
if err = require.LAPI(csConfig); err != nil {
cfg := cli.cfg()
if err = require.LAPI(cfg); err != nil {
return err return err
} }
dbClient, err = database.NewClient(csConfig.DbConfig) cli.db, err = database.NewClient(cfg.DbConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create new database client: %s", err) return fmt.Errorf("can't connect to the database: %w", err)
} }
return nil return nil
}, },
} }
cmd.AddCommand(cli.NewListCmd()) cmd.AddCommand(cli.newListCmd())
cmd.AddCommand(cli.NewAddCmd()) cmd.AddCommand(cli.newAddCmd())
cmd.AddCommand(cli.NewDeleteCmd()) cmd.AddCommand(cli.newDeleteCmd())
cmd.AddCommand(cli.NewPruneCmd()) cmd.AddCommand(cli.newPruneCmd())
return cmd return cmd
} }
func (cli cliBouncers) NewListCmd() *cobra.Command { func (cli *cliBouncers) list() error {
out := color.Output
bouncers, err := cli.db.ListBouncers()
if err != nil {
return fmt.Errorf("unable to list bouncers: %w", err)
}
switch cli.cfg().Cscli.Output {
case "human":
getBouncersTable(out, bouncers)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(bouncers); err != nil {
return fmt.Errorf("failed to marshal: %w", err)
}
return nil
case "raw":
csvwriter := csv.NewWriter(out)
if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
return fmt.Errorf("failed to write raw header: %w", err)
}
for _, b := range bouncers {
valid := "validated"
if b.Revoked {
valid = "pending"
}
if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}); err != nil {
return fmt.Errorf("failed to write raw: %w", err)
}
}
csvwriter.Flush()
}
return nil
}
func (cli *cliBouncers) newListCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list all bouncers within the database", Short: "list all bouncers within the database",
Example: `cscli bouncers list`, Example: `cscli bouncers list`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, arg []string) error { RunE: func(_ *cobra.Command, _ []string) error {
err := getBouncers(color.Output, dbClient) return cli.list()
if err != nil {
return fmt.Errorf("unable to list bouncers: %s", err)
}
return nil
}, },
} }
return cmd return cmd
} }
func (cli cliBouncers) add(cmd *cobra.Command, args []string) error { func (cli *cliBouncers) add(bouncerName string, key string) error {
var err error
keyLength := 32 keyLength := 32
flags := cmd.Flags()
key, err := flags.GetString("key")
if err != nil {
return err
}
keyName := args[0]
var apiKey string
if keyName == "" {
return fmt.Errorf("please provide a name for the api key")
}
apiKey = key
if key == "" { if key == "" {
apiKey, err = middlewares.GenerateAPIKey(keyLength) key, err = middlewares.GenerateAPIKey(keyLength)
} if err != nil {
if err != nil { return fmt.Errorf("unable to generate api key: %w", err)
return fmt.Errorf("unable to generate api key: %s", err) }
}
_, err = dbClient.CreateBouncer(keyName, "", middlewares.HashSHA512(apiKey), types.ApiKeyAuthType)
if err != nil {
return fmt.Errorf("unable to create bouncer: %s", err)
} }
switch csConfig.Cscli.Output { _, err = cli.db.CreateBouncer(bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType)
if err != nil {
return fmt.Errorf("unable to create bouncer: %w", err)
}
switch cli.cfg().Cscli.Output {
case "human": case "human":
fmt.Printf("API key for '%s':\n\n", keyName) fmt.Printf("API key for '%s':\n\n", bouncerName)
fmt.Printf(" %s\n\n", apiKey) fmt.Printf(" %s\n\n", key)
fmt.Print("Please keep this key since you will not be able to retrieve it!\n") fmt.Print("Please keep this key since you will not be able to retrieve it!\n")
case "raw": case "raw":
fmt.Printf("%s", apiKey) fmt.Print(key)
case "json": case "json":
j, err := json.Marshal(apiKey) j, err := json.Marshal(key)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal api key") return errors.New("unable to marshal api key")
} }
fmt.Printf("%s", string(j))
fmt.Print(string(j))
} }
return nil return nil
} }
func (cli cliBouncers) NewAddCmd() *cobra.Command { func (cli *cliBouncers) newAddCmd() *cobra.Command {
var key string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "add MyBouncerName", Use: "add MyBouncerName",
Short: "add a single bouncer to the database", Short: "add a single bouncer to the database",
@ -171,127 +188,133 @@ func (cli cliBouncers) NewAddCmd() *cobra.Command {
cscli bouncers add MyBouncerName --key <random-key>`, cscli bouncers add MyBouncerName --key <random-key>`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.add, RunE: func(_ *cobra.Command, args []string) error {
return cli.add(args[0], key)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringP("length", "l", "", "length of the api key") flags.StringP("length", "l", "", "length of the api key")
flags.MarkDeprecated("length", "use --key instead") _ = flags.MarkDeprecated("length", "use --key instead")
flags.StringP("key", "k", "", "api key for the bouncer") flags.StringVarP(&key, "key", "k", "", "api key for the bouncer")
return cmd return cmd
} }
func (cli cliBouncers) delete(cmd *cobra.Command, args []string) error { func (cli *cliBouncers) deleteValid(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
for _, bouncerID := range args { bouncers, err := cli.db.ListBouncers()
err := dbClient.DeleteBouncer(bouncerID) if err != nil {
if err != nil { cobra.CompError("unable to list bouncers " + err.Error())
return fmt.Errorf("unable to delete bouncer '%s': %s", bouncerID, err) }
ret := []string{}
for _, bouncer := range bouncers {
if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
ret = append(ret, bouncer.Name)
} }
}
return ret, cobra.ShellCompDirectiveNoFileComp
}
func (cli *cliBouncers) delete(bouncers []string) error {
for _, bouncerID := range bouncers {
err := cli.db.DeleteBouncer(bouncerID)
if err != nil {
return fmt.Errorf("unable to delete bouncer '%s': %w", bouncerID, err)
}
log.Infof("bouncer '%s' deleted successfully", bouncerID) log.Infof("bouncer '%s' deleted successfully", bouncerID)
} }
return nil return nil
} }
func (cli cliBouncers) NewDeleteCmd() *cobra.Command { func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "delete MyBouncerName", Use: "delete MyBouncerName",
Short: "delete bouncer(s) from the database", Short: "delete bouncer(s) from the database",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Aliases: []string{"remove"}, Aliases: []string{"remove"},
DisableAutoGenTag: true, DisableAutoGenTag: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: cli.deleteValid,
var err error RunE: func(_ *cobra.Command, args []string) error {
dbClient, err = getDBClient() return cli.delete(args)
if err != nil {
cobra.CompError("unable to create new database client: " + err.Error())
return nil, cobra.ShellCompDirectiveNoFileComp
}
bouncers, err := dbClient.ListBouncers()
if err != nil {
cobra.CompError("unable to list bouncers " + err.Error())
}
ret := make([]string, 0)
for _, bouncer := range bouncers {
if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
ret = append(ret, bouncer.Name)
}
}
return ret, cobra.ShellCompDirectiveNoFileComp
}, },
RunE: cli.delete,
} }
return cmd return cmd
} }
func (cli cliBouncers) NewPruneCmd() *cobra.Command { func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
var parsedDuration time.Duration if duration < 2*time.Minute {
if yes, err := askYesNo(
"The duration you provided is less than 2 minutes. " +
"This may remove active bouncers. Continue?", false); err != nil {
return err
} else if !yes {
fmt.Println("User aborted prune. No changes were made.")
return nil
}
}
bouncers, err := cli.db.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(-duration))
if err != nil {
return fmt.Errorf("unable to query bouncers: %w", err)
}
if len(bouncers) == 0 {
fmt.Println("No bouncers to prune.")
return nil
}
getBouncersTable(color.Output, bouncers)
if !force {
if yes, err := askYesNo(
"You are about to PERMANENTLY remove the above bouncers from the database. " +
"These will NOT be recoverable. Continue?", false); err != nil {
return err
} else if !yes {
fmt.Println("User aborted prune. No changes were made.")
return nil
}
}
deleted, err := cli.db.BulkDeleteBouncers(bouncers)
if err != nil {
return fmt.Errorf("unable to prune bouncers: %w", err)
}
fmt.Fprintf(os.Stderr, "Successfully deleted %d bouncers\n", deleted)
return nil
}
func (cli *cliBouncers) newPruneCmd() *cobra.Command {
var (
duration time.Duration
force bool
)
const defaultDuration = 60 * time.Minute
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "prune", Use: "prune",
Short: "prune multiple bouncers from the database", Short: "prune multiple bouncers from the database",
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Example: `cscli bouncers prune -d 60m Example: `cscli bouncers prune -d 45m
cscli bouncers prune -d 60m --force`, cscli bouncers prune -d 45m --force`,
PreRunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
dur, _ := cmd.Flags().GetString("duration") return cli.prune(duration, force)
var err error
parsedDuration, err = time.ParseDuration(fmt.Sprintf("-%s", dur))
if err != nil {
return fmt.Errorf("unable to parse duration '%s': %s", dur, err)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
force, _ := cmd.Flags().GetBool("force")
if parsedDuration >= 0-2*time.Minute {
var answer bool
prompt := &survey.Confirm{
Message: "The duration you provided is less than or equal 2 minutes this may remove active bouncers continue ?",
Default: false,
}
if err := survey.AskOne(prompt, &answer); err != nil {
return fmt.Errorf("unable to ask about prune check: %s", err)
}
if !answer {
fmt.Println("user aborted prune no changes were made")
return nil
}
}
bouncers, err := dbClient.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(parsedDuration))
if err != nil {
return fmt.Errorf("unable to query bouncers: %s", err)
}
if len(bouncers) == 0 {
fmt.Println("no bouncers to prune")
return nil
}
getBouncersTable(color.Output, bouncers)
if !force {
var answer bool
prompt := &survey.Confirm{
Message: "You are about to PERMANENTLY remove the above bouncers from the database these will NOT be recoverable, continue ?",
Default: false,
}
if err := survey.AskOne(prompt, &answer); err != nil {
return fmt.Errorf("unable to ask about prune check: %s", err)
}
if !answer {
fmt.Println("user aborted prune no changes were made")
return nil
}
}
nbDeleted, err := dbClient.BulkDeleteBouncers(bouncers)
if err != nil {
return fmt.Errorf("unable to prune bouncers: %s", err)
}
fmt.Printf("successfully delete %d bouncers\n", nbDeleted)
return nil
}, },
} }
cmd.Flags().StringP("duration", "d", "60m", "duration of time since last pull")
cmd.Flags().Bool("force", false, "force prune without asking for confirmation") flags := cmd.Flags()
flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since last pull")
flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
return cmd return cmd
} }

View file

@ -5,9 +5,9 @@ import (
"time" "time"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
"github.com/enescakir/emoji"
"github.com/crowdsecurity/crowdsec/pkg/database/ent" "github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
) )
func getBouncersTable(out io.Writer, bouncers []*ent.Bouncer) { func getBouncersTable(out io.Writer, bouncers []*ent.Bouncer) {
@ -17,11 +17,9 @@ func getBouncersTable(out io.Writer, bouncers []*ent.Bouncer) {
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
for _, b := range bouncers { for _, b := range bouncers {
var revoked string revoked := emoji.CheckMark
if !b.Revoked { if b.Revoked {
revoked = emoji.CheckMark.String() revoked = emoji.Prohibited
} else {
revoked = emoji.Prohibited.String()
} }
t.AddRow(b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType) t.AddRow(b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType)

View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@ -9,7 +10,7 @@ import (
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
@ -26,24 +27,29 @@ const (
CAPIURLPrefix = "v3" CAPIURLPrefix = "v3"
) )
type cliCapi struct{} type cliCapi struct {
cfg configGetter
func NewCLICapi() *cliCapi {
return &cliCapi{}
} }
func (cli cliCapi) NewCommand() *cobra.Command { func NewCLICapi(cfg configGetter) *cliCapi {
var cmd = &cobra.Command{ return &cliCapi{
cfg: cfg,
}
}
func (cli *cliCapi) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "capi [action]", Use: "capi [action]",
Short: "Manage interaction with Central API (CAPI)", Short: "Manage interaction with Central API (CAPI)",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := require.LAPI(csConfig); err != nil { cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err return err
} }
if err := require.CAPI(csConfig); err != nil { if err := require.CAPI(cfg); err != nil {
return err return err
} }
@ -51,78 +57,91 @@ func (cli cliCapi) NewCommand() *cobra.Command {
}, },
} }
cmd.AddCommand(cli.NewRegisterCmd()) cmd.AddCommand(cli.newRegisterCmd())
cmd.AddCommand(cli.NewStatusCmd()) cmd.AddCommand(cli.newStatusCmd())
return cmd return cmd
} }
func (cli cliCapi) NewRegisterCmd() *cobra.Command { func (cli *cliCapi) register(capiUserPrefix string, outputFile string) error {
cfg := cli.cfg()
capiUser, err := generateID(capiUserPrefix)
if err != nil {
return fmt.Errorf("unable to generate machine id: %w", err)
}
password := strfmt.Password(generatePassword(passwordLength))
apiurl, err := url.Parse(types.CAPIBaseURL)
if err != nil {
return fmt.Errorf("unable to parse api url %s: %w", types.CAPIBaseURL, err)
}
_, err = apiclient.RegisterClient(&apiclient.Config{
MachineID: capiUser,
Password: password,
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiurl,
VersionPrefix: CAPIURLPrefix,
}, nil)
if err != nil {
return fmt.Errorf("api client register ('%s'): %w", types.CAPIBaseURL, err)
}
log.Infof("Successfully registered to Central API (CAPI)")
var dumpFile string
switch {
case outputFile != "":
dumpFile = outputFile
case cfg.API.Server.OnlineClient.CredentialsFilePath != "":
dumpFile = cfg.API.Server.OnlineClient.CredentialsFilePath
default:
dumpFile = ""
}
apiCfg := csconfig.ApiCredentialsCfg{
Login: capiUser,
Password: password.String(),
URL: types.CAPIBaseURL,
}
apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil {
return fmt.Errorf("unable to marshal api credentials: %w", err)
}
if dumpFile != "" {
err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
if err != nil {
return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err)
}
log.Infof("Central API credentials written to '%s'", dumpFile)
} else {
fmt.Println(string(apiConfigDump))
}
log.Warning(ReloadMessage())
return nil
}
func (cli *cliCapi) newRegisterCmd() *cobra.Command {
var ( var (
capiUserPrefix string capiUserPrefix string
outputFile string outputFile string
) )
var cmd = &cobra.Command{ cmd := &cobra.Command{
Use: "register", Use: "register",
Short: "Register to Central API (CAPI)", Short: "Register to Central API (CAPI)",
Args: cobra.MinimumNArgs(0), Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
var err error return cli.register(capiUserPrefix, outputFile)
capiUser, err := generateID(capiUserPrefix)
if err != nil {
return fmt.Errorf("unable to generate machine id: %s", err)
}
password := strfmt.Password(generatePassword(passwordLength))
apiurl, err := url.Parse(types.CAPIBaseURL)
if err != nil {
return fmt.Errorf("unable to parse api url %s: %w", types.CAPIBaseURL, err)
}
_, err = apiclient.RegisterClient(&apiclient.Config{
MachineID: capiUser,
Password: password,
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiurl,
VersionPrefix: CAPIURLPrefix,
}, nil)
if err != nil {
return fmt.Errorf("api client register ('%s'): %w", types.CAPIBaseURL, err)
}
log.Printf("Successfully registered to Central API (CAPI)")
var dumpFile string
if outputFile != "" {
dumpFile = outputFile
} else if csConfig.API.Server.OnlineClient.CredentialsFilePath != "" {
dumpFile = csConfig.API.Server.OnlineClient.CredentialsFilePath
} else {
dumpFile = ""
}
apiCfg := csconfig.ApiCredentialsCfg{
Login: capiUser,
Password: password.String(),
URL: types.CAPIBaseURL,
}
apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil {
return fmt.Errorf("unable to marshal api credentials: %w", err)
}
if dumpFile != "" {
err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
if err != nil {
return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err)
}
log.Printf("Central API credentials written to '%s'", dumpFile)
} else {
fmt.Println(string(apiConfigDump))
}
log.Warning(ReloadMessage())
return nil
}, },
} }
@ -136,59 +155,66 @@ func (cli cliCapi) NewRegisterCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliCapi) NewStatusCmd() *cobra.Command { func (cli *cliCapi) status() error {
cfg := cli.cfg()
if err := require.CAPIRegistered(cfg); err != nil {
return err
}
password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password)
apiurl, err := url.Parse(cfg.API.Server.OnlineClient.Credentials.URL)
if err != nil {
return fmt.Errorf("parsing api url ('%s'): %w", cfg.API.Server.OnlineClient.Credentials.URL, err)
}
hub, err := require.Hub(cfg, nil, nil)
if err != nil {
return err
}
scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
if err != nil {
return fmt.Errorf("failed to get scenarios: %w", err)
}
if len(scenarios) == 0 {
return errors.New("no scenarios installed, abort")
}
Client, err = apiclient.NewDefaultClient(apiurl, CAPIURLPrefix, fmt.Sprintf("crowdsec/%s", version.String()), nil)
if err != nil {
return fmt.Errorf("init default client: %w", err)
}
t := models.WatcherAuthRequest{
MachineID: &cfg.API.Server.OnlineClient.Credentials.Login,
Password: &password,
Scenarios: scenarios,
}
log.Infof("Loaded credentials from %s", cfg.API.Server.OnlineClient.CredentialsFilePath)
log.Infof("Trying to authenticate with username %s on %s", cfg.API.Server.OnlineClient.Credentials.Login, apiurl)
_, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
if err != nil {
return fmt.Errorf("failed to authenticate to Central API (CAPI): %w", err)
}
log.Info("You can successfully interact with Central API (CAPI)")
return nil
}
func (cli *cliCapi) newStatusCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "status", Use: "status",
Short: "Check status with the Central API (CAPI)", Short: "Check status with the Central API (CAPI)",
Args: cobra.MinimumNArgs(0), Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
if err := require.CAPIRegistered(csConfig); err != nil { return cli.status()
return err
}
password := strfmt.Password(csConfig.API.Server.OnlineClient.Credentials.Password)
apiurl, err := url.Parse(csConfig.API.Server.OnlineClient.Credentials.URL)
if err != nil {
return fmt.Errorf("parsing api url ('%s'): %w", csConfig.API.Server.OnlineClient.Credentials.URL, err)
}
hub, err := require.Hub(csConfig, nil, nil)
if err != nil {
return err
}
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
if err != nil {
return fmt.Errorf("failed to get scenarios: %w", err)
}
if len(scenarios) == 0 {
return fmt.Errorf("no scenarios installed, abort")
}
Client, err = apiclient.NewDefaultClient(apiurl, CAPIURLPrefix, fmt.Sprintf("crowdsec/%s", version.String()), nil)
if err != nil {
return fmt.Errorf("init default client: %w", err)
}
t := models.WatcherAuthRequest{
MachineID: &csConfig.API.Server.OnlineClient.Credentials.Login,
Password: &password,
Scenarios: scenarios,
}
log.Infof("Loaded credentials from %s", csConfig.API.Server.OnlineClient.CredentialsFilePath)
log.Infof("Trying to authenticate with username %s on %s", csConfig.API.Server.OnlineClient.Credentials.Login, apiurl)
_, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
if err != nil {
return fmt.Errorf("failed to authenticate to Central API (CAPI): %w", err)
}
log.Infof("You can successfully interact with Central API (CAPI)")
return nil
}, },
} }

View file

@ -4,19 +4,29 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func NewConfigCmd() *cobra.Command { type cliConfig struct {
cmdConfig := &cobra.Command{ cfg configGetter
}
func NewCLIConfig(cfg configGetter) *cliConfig {
return &cliConfig{
cfg: cfg,
}
}
func (cli *cliConfig) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config [command]", Use: "config [command]",
Short: "Allows to view current config", Short: "Allows to view current config",
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
} }
cmdConfig.AddCommand(NewConfigShowCmd()) cmd.AddCommand(cli.newShowCmd())
cmdConfig.AddCommand(NewConfigShowYAMLCmd()) cmd.AddCommand(cli.newShowYAMLCmd())
cmdConfig.AddCommand(NewConfigBackupCmd()) cmd.AddCommand(cli.newBackupCmd())
cmdConfig.AddCommand(NewConfigRestoreCmd()) cmd.AddCommand(cli.newRestoreCmd())
cmdConfig.AddCommand(NewConfigFeatureFlagsCmd()) cmd.AddCommand(cli.newFeatureFlagsCmd())
return cmdConfig return cmd
} }

View file

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -13,8 +14,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func backupHub(dirPath string) error { func (cli *cliConfig) backupHub(dirPath string) error {
hub, err := require.Hub(csConfig, nil, nil) hub, err := require.Hub(cli.cfg(), nil, nil)
if err != nil { if err != nil {
return err return err
} }
@ -32,7 +33,7 @@ func backupHub(dirPath string) error {
itemDirectory := fmt.Sprintf("%s/%s/", dirPath, itemType) itemDirectory := fmt.Sprintf("%s/%s/", dirPath, itemType)
if err = os.MkdirAll(itemDirectory, os.ModePerm); err != nil { if err = os.MkdirAll(itemDirectory, os.ModePerm); err != nil {
return fmt.Errorf("error while creating %s : %s", itemDirectory, err) return fmt.Errorf("error while creating %s: %w", itemDirectory, err)
} }
upstreamParsers := []string{} upstreamParsers := []string{}
@ -41,18 +42,18 @@ func backupHub(dirPath string) error {
clog = clog.WithFields(log.Fields{ clog = clog.WithFields(log.Fields{
"file": v.Name, "file": v.Name,
}) })
if !v.State.Installed { //only backup installed ones if !v.State.Installed { // only backup installed ones
clog.Debugf("[%s] : not installed", k) clog.Debugf("[%s]: not installed", k)
continue continue
} }
//for the local/tainted ones, we back up the full file // for the local/tainted ones, we back up the full file
if v.State.Tainted || v.State.IsLocal() || !v.State.UpToDate { if v.State.Tainted || v.State.IsLocal() || !v.State.UpToDate {
//we need to backup stages for parsers // we need to backup stages for parsers
if itemType == cwhub.PARSERS || itemType == cwhub.POSTOVERFLOWS { if itemType == cwhub.PARSERS || itemType == cwhub.POSTOVERFLOWS {
fstagedir := fmt.Sprintf("%s%s", itemDirectory, v.Stage) fstagedir := fmt.Sprintf("%s%s", itemDirectory, v.Stage)
if err = os.MkdirAll(fstagedir, os.ModePerm); err != nil { if err = os.MkdirAll(fstagedir, os.ModePerm); err != nil {
return fmt.Errorf("error while creating stage dir %s : %s", fstagedir, err) return fmt.Errorf("error while creating stage dir %s: %w", fstagedir, err)
} }
} }
@ -60,7 +61,7 @@ func backupHub(dirPath string) error {
tfile := fmt.Sprintf("%s%s/%s", itemDirectory, v.Stage, v.FileName) tfile := fmt.Sprintf("%s%s/%s", itemDirectory, v.Stage, v.FileName)
if err = CopyFile(v.State.LocalPath, tfile); err != nil { if err = CopyFile(v.State.LocalPath, tfile); err != nil {
return fmt.Errorf("failed copy %s %s to %s : %s", itemType, v.State.LocalPath, tfile, err) return fmt.Errorf("failed copy %s %s to %s: %w", itemType, v.State.LocalPath, tfile, err)
} }
clog.Infof("local/tainted saved %s to %s", v.State.LocalPath, tfile) clog.Infof("local/tainted saved %s to %s", v.State.LocalPath, tfile)
@ -68,21 +69,21 @@ func backupHub(dirPath string) error {
continue continue
} }
clog.Debugf("[%s] : from hub, just backup name (up-to-date:%t)", k, v.State.UpToDate) clog.Debugf("[%s]: from hub, just backup name (up-to-date:%t)", k, v.State.UpToDate)
clog.Infof("saving, version:%s, up-to-date:%t", v.Version, v.State.UpToDate) clog.Infof("saving, version:%s, up-to-date:%t", v.Version, v.State.UpToDate)
upstreamParsers = append(upstreamParsers, v.Name) upstreamParsers = append(upstreamParsers, v.Name)
} }
//write the upstream items // write the upstream items
upstreamParsersFname := fmt.Sprintf("%s/upstream-%s.json", itemDirectory, itemType) upstreamParsersFname := fmt.Sprintf("%s/upstream-%s.json", itemDirectory, itemType)
upstreamParsersContent, err := json.MarshalIndent(upstreamParsers, "", " ") upstreamParsersContent, err := json.MarshalIndent(upstreamParsers, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed marshaling upstream parsers : %s", err) return fmt.Errorf("failed marshaling upstream parsers: %w", err)
} }
err = os.WriteFile(upstreamParsersFname, upstreamParsersContent, 0o644) err = os.WriteFile(upstreamParsersFname, upstreamParsersContent, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("unable to write to %s %s : %s", itemType, upstreamParsersFname, err) return fmt.Errorf("unable to write to %s %s: %w", itemType, upstreamParsersFname, err)
} }
clog.Infof("Wrote %d entries for %s to %s", len(upstreamParsers), itemType, upstreamParsersFname) clog.Infof("Wrote %d entries for %s to %s", len(upstreamParsers), itemType, upstreamParsersFname)
@ -102,11 +103,13 @@ func backupHub(dirPath string) error {
- Tainted/local/out-of-date scenarios, parsers, postoverflows and collections - Tainted/local/out-of-date scenarios, parsers, postoverflows and collections
- Acquisition files (acquis.yaml, acquis.d/*.yaml) - Acquisition files (acquis.yaml, acquis.d/*.yaml)
*/ */
func backupConfigToDirectory(dirPath string) error { func (cli *cliConfig) backup(dirPath string) error {
var err error var err error
cfg := cli.cfg()
if dirPath == "" { if dirPath == "" {
return fmt.Errorf("directory path can't be empty") return errors.New("directory path can't be empty")
} }
log.Infof("Starting configuration backup") log.Infof("Starting configuration backup")
@ -121,10 +124,10 @@ func backupConfigToDirectory(dirPath string) error {
return fmt.Errorf("while creating %s: %w", dirPath, err) return fmt.Errorf("while creating %s: %w", dirPath, err)
} }
if csConfig.ConfigPaths.SimulationFilePath != "" { if cfg.ConfigPaths.SimulationFilePath != "" {
backupSimulation := filepath.Join(dirPath, "simulation.yaml") backupSimulation := filepath.Join(dirPath, "simulation.yaml")
if err = CopyFile(csConfig.ConfigPaths.SimulationFilePath, backupSimulation); err != nil { if err = CopyFile(cfg.ConfigPaths.SimulationFilePath, backupSimulation); err != nil {
return fmt.Errorf("failed copy %s to %s: %w", csConfig.ConfigPaths.SimulationFilePath, backupSimulation, err) return fmt.Errorf("failed copy %s to %s: %w", cfg.ConfigPaths.SimulationFilePath, backupSimulation, err)
} }
log.Infof("Saved simulation to %s", backupSimulation) log.Infof("Saved simulation to %s", backupSimulation)
@ -134,22 +137,22 @@ func backupConfigToDirectory(dirPath string) error {
- backup AcquisitionFilePath - backup AcquisitionFilePath
- backup the other files of acquisition directory - backup the other files of acquisition directory
*/ */
if csConfig.Crowdsec != nil && csConfig.Crowdsec.AcquisitionFilePath != "" { if cfg.Crowdsec != nil && cfg.Crowdsec.AcquisitionFilePath != "" {
backupAcquisition := filepath.Join(dirPath, "acquis.yaml") backupAcquisition := filepath.Join(dirPath, "acquis.yaml")
if err = CopyFile(csConfig.Crowdsec.AcquisitionFilePath, backupAcquisition); err != nil { if err = CopyFile(cfg.Crowdsec.AcquisitionFilePath, backupAcquisition); err != nil {
return fmt.Errorf("failed copy %s to %s: %s", csConfig.Crowdsec.AcquisitionFilePath, backupAcquisition, err) return fmt.Errorf("failed copy %s to %s: %w", cfg.Crowdsec.AcquisitionFilePath, backupAcquisition, err)
} }
} }
acquisBackupDir := filepath.Join(dirPath, "acquis") acquisBackupDir := filepath.Join(dirPath, "acquis")
if err = os.Mkdir(acquisBackupDir, 0o700); err != nil { if err = os.Mkdir(acquisBackupDir, 0o700); err != nil {
return fmt.Errorf("error while creating %s: %s", acquisBackupDir, err) return fmt.Errorf("error while creating %s: %w", acquisBackupDir, err)
} }
if csConfig.Crowdsec != nil && len(csConfig.Crowdsec.AcquisitionFiles) > 0 { if cfg.Crowdsec != nil && len(cfg.Crowdsec.AcquisitionFiles) > 0 {
for _, acquisFile := range csConfig.Crowdsec.AcquisitionFiles { for _, acquisFile := range cfg.Crowdsec.AcquisitionFiles {
/*if it was the default one, it was already backup'ed*/ /*if it was the default one, it was already backup'ed*/
if csConfig.Crowdsec.AcquisitionFilePath == acquisFile { if cfg.Crowdsec.AcquisitionFilePath == acquisFile {
continue continue
} }
@ -169,56 +172,48 @@ func backupConfigToDirectory(dirPath string) error {
if ConfigFilePath != "" { if ConfigFilePath != "" {
backupMain := fmt.Sprintf("%s/config.yaml", dirPath) backupMain := fmt.Sprintf("%s/config.yaml", dirPath)
if err = CopyFile(ConfigFilePath, backupMain); err != nil { if err = CopyFile(ConfigFilePath, backupMain); err != nil {
return fmt.Errorf("failed copy %s to %s: %s", ConfigFilePath, backupMain, err) return fmt.Errorf("failed copy %s to %s: %w", ConfigFilePath, backupMain, err)
} }
log.Infof("Saved default yaml to %s", backupMain) log.Infof("Saved default yaml to %s", backupMain)
} }
if csConfig.API != nil && csConfig.API.Server != nil && csConfig.API.Server.OnlineClient != nil && csConfig.API.Server.OnlineClient.CredentialsFilePath != "" { if cfg.API != nil && cfg.API.Server != nil && cfg.API.Server.OnlineClient != nil && cfg.API.Server.OnlineClient.CredentialsFilePath != "" {
backupCAPICreds := fmt.Sprintf("%s/online_api_credentials.yaml", dirPath) backupCAPICreds := fmt.Sprintf("%s/online_api_credentials.yaml", dirPath)
if err = CopyFile(csConfig.API.Server.OnlineClient.CredentialsFilePath, backupCAPICreds); err != nil { if err = CopyFile(cfg.API.Server.OnlineClient.CredentialsFilePath, backupCAPICreds); err != nil {
return fmt.Errorf("failed copy %s to %s: %s", csConfig.API.Server.OnlineClient.CredentialsFilePath, backupCAPICreds, err) return fmt.Errorf("failed copy %s to %s: %w", cfg.API.Server.OnlineClient.CredentialsFilePath, backupCAPICreds, err)
} }
log.Infof("Saved online API credentials to %s", backupCAPICreds) log.Infof("Saved online API credentials to %s", backupCAPICreds)
} }
if csConfig.API != nil && csConfig.API.Client != nil && csConfig.API.Client.CredentialsFilePath != "" { if cfg.API != nil && cfg.API.Client != nil && cfg.API.Client.CredentialsFilePath != "" {
backupLAPICreds := fmt.Sprintf("%s/local_api_credentials.yaml", dirPath) backupLAPICreds := fmt.Sprintf("%s/local_api_credentials.yaml", dirPath)
if err = CopyFile(csConfig.API.Client.CredentialsFilePath, backupLAPICreds); err != nil { if err = CopyFile(cfg.API.Client.CredentialsFilePath, backupLAPICreds); err != nil {
return fmt.Errorf("failed copy %s to %s: %s", csConfig.API.Client.CredentialsFilePath, backupLAPICreds, err) return fmt.Errorf("failed copy %s to %s: %w", cfg.API.Client.CredentialsFilePath, backupLAPICreds, err)
} }
log.Infof("Saved local API credentials to %s", backupLAPICreds) log.Infof("Saved local API credentials to %s", backupLAPICreds)
} }
if csConfig.API != nil && csConfig.API.Server != nil && csConfig.API.Server.ProfilesPath != "" { if cfg.API != nil && cfg.API.Server != nil && cfg.API.Server.ProfilesPath != "" {
backupProfiles := fmt.Sprintf("%s/profiles.yaml", dirPath) backupProfiles := fmt.Sprintf("%s/profiles.yaml", dirPath)
if err = CopyFile(csConfig.API.Server.ProfilesPath, backupProfiles); err != nil { if err = CopyFile(cfg.API.Server.ProfilesPath, backupProfiles); err != nil {
return fmt.Errorf("failed copy %s to %s: %s", csConfig.API.Server.ProfilesPath, backupProfiles, err) return fmt.Errorf("failed copy %s to %s: %w", cfg.API.Server.ProfilesPath, backupProfiles, err)
} }
log.Infof("Saved profiles to %s", backupProfiles) log.Infof("Saved profiles to %s", backupProfiles)
} }
if err = backupHub(dirPath); err != nil { if err = cli.backupHub(dirPath); err != nil {
return fmt.Errorf("failed to backup hub config: %s", err) return fmt.Errorf("failed to backup hub config: %w", err)
} }
return nil return nil
} }
func runConfigBackup(cmd *cobra.Command, args []string) error { func (cli *cliConfig) newBackupCmd() *cobra.Command {
if err := backupConfigToDirectory(args[0]); err != nil { cmd := &cobra.Command{
return fmt.Errorf("failed to backup config: %w", err)
}
return nil
}
func NewConfigBackupCmd() *cobra.Command {
cmdConfigBackup := &cobra.Command{
Use: `backup "directory"`, Use: `backup "directory"`,
Short: "Backup current config", Short: "Backup current config",
Long: `Backup the current crowdsec configuration including : Long: `Backup the current crowdsec configuration including :
@ -232,8 +227,14 @@ func NewConfigBackupCmd() *cobra.Command {
Example: `cscli config backup ./my-backup`, Example: `cscli config backup ./my-backup`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runConfigBackup, RunE: func(_ *cobra.Command, args []string) error {
if err := cli.backup(args[0]); err != nil {
return fmt.Errorf("failed to backup config: %w", err)
}
return nil
},
} }
return cmdConfigBackup return cmd
} }

View file

@ -11,14 +11,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/fflag" "github.com/crowdsecurity/crowdsec/pkg/fflag"
) )
func runConfigFeatureFlags(cmd *cobra.Command, args []string) error { func (cli *cliConfig) featureFlags(showRetired bool) error {
flags := cmd.Flags()
showRetired, err := flags.GetBool("retired")
if err != nil {
return err
}
green := color.New(color.FgGreen).SprintFunc() green := color.New(color.FgGreen).SprintFunc()
red := color.New(color.FgRed).SprintFunc() red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc()
@ -121,18 +114,22 @@ func runConfigFeatureFlags(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func NewConfigFeatureFlagsCmd() *cobra.Command { func (cli *cliConfig) newFeatureFlagsCmd() *cobra.Command {
cmdConfigFeatureFlags := &cobra.Command{ var showRetired bool
cmd := &cobra.Command{
Use: "feature-flags", Use: "feature-flags",
Short: "Displays feature flag status", Short: "Displays feature flag status",
Long: `Displays the supported feature flags and their current status.`, Long: `Displays the supported feature flags and their current status.`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runConfigFeatureFlags, RunE: func(_ *cobra.Command, _ []string) error {
return cli.featureFlags(showRetired)
},
} }
flags := cmdConfigFeatureFlags.Flags() flags := cmd.Flags()
flags.Bool("retired", false, "Show retired features") flags.BoolVar(&showRetired, "retired", false, "Show retired features")
return cmdConfigFeatureFlags return cmd
} }

View file

@ -3,26 +3,20 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
type OldAPICfg struct { func (cli *cliConfig) restoreHub(dirPath string) error {
MachineID string `json:"machine_id"` cfg := cli.cfg()
Password string `json:"password"`
}
func restoreHub(dirPath string) error { hub, err := require.Hub(cfg, require.RemoteHub(cfg), nil)
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), nil)
if err != nil { if err != nil {
return err return err
} }
@ -38,14 +32,14 @@ func restoreHub(dirPath string) error {
file, err := os.ReadFile(upstreamListFN) file, err := os.ReadFile(upstreamListFN)
if err != nil { if err != nil {
return fmt.Errorf("error while opening %s : %s", upstreamListFN, err) return fmt.Errorf("error while opening %s: %w", upstreamListFN, err)
} }
var upstreamList []string var upstreamList []string
err = json.Unmarshal(file, &upstreamList) err = json.Unmarshal(file, &upstreamList)
if err != nil { if err != nil {
return fmt.Errorf("error unmarshaling %s : %s", upstreamListFN, err) return fmt.Errorf("error unmarshaling %s: %w", upstreamListFN, err)
} }
for _, toinstall := range upstreamList { for _, toinstall := range upstreamList {
@ -55,8 +49,7 @@ func restoreHub(dirPath string) error {
continue continue
} }
err := item.Install(false, false) if err = item.Install(false, false); err != nil {
if err != nil {
log.Errorf("Error while installing %s : %s", toinstall, err) log.Errorf("Error while installing %s : %s", toinstall, err)
} }
} }
@ -64,42 +57,43 @@ func restoreHub(dirPath string) error {
/*restore the local and tainted items*/ /*restore the local and tainted items*/
files, err := os.ReadDir(itemDirectory) files, err := os.ReadDir(itemDirectory)
if err != nil { if err != nil {
return fmt.Errorf("failed enumerating files of %s : %s", itemDirectory, err) return fmt.Errorf("failed enumerating files of %s: %w", itemDirectory, err)
} }
for _, file := range files { for _, file := range files {
//this was the upstream data // this was the upstream data
if file.Name() == fmt.Sprintf("upstream-%s.json", itype) { if file.Name() == fmt.Sprintf("upstream-%s.json", itype) {
continue continue
} }
if itype == cwhub.PARSERS || itype == cwhub.POSTOVERFLOWS { if itype == cwhub.PARSERS || itype == cwhub.POSTOVERFLOWS {
//we expect a stage here // we expect a stage here
if !file.IsDir() { if !file.IsDir() {
continue continue
} }
stage := file.Name() stage := file.Name()
stagedir := fmt.Sprintf("%s/%s/%s/", csConfig.ConfigPaths.ConfigDir, itype, stage) stagedir := fmt.Sprintf("%s/%s/%s/", cfg.ConfigPaths.ConfigDir, itype, stage)
log.Debugf("Found stage %s in %s, target directory : %s", stage, itype, stagedir) log.Debugf("Found stage %s in %s, target directory : %s", stage, itype, stagedir)
if err = os.MkdirAll(stagedir, os.ModePerm); err != nil { if err = os.MkdirAll(stagedir, os.ModePerm); err != nil {
return fmt.Errorf("error while creating stage directory %s : %s", stagedir, err) return fmt.Errorf("error while creating stage directory %s: %w", stagedir, err)
} }
// find items // find items
ifiles, err := os.ReadDir(itemDirectory + "/" + stage + "/") ifiles, err := os.ReadDir(itemDirectory + "/" + stage + "/")
if err != nil { if err != nil {
return fmt.Errorf("failed enumerating files of %s : %s", itemDirectory+"/"+stage, err) return fmt.Errorf("failed enumerating files of %s: %w", itemDirectory+"/"+stage, err)
} }
//finally copy item
// finally copy item
for _, tfile := range ifiles { for _, tfile := range ifiles {
log.Infof("Going to restore local/tainted [%s]", tfile.Name()) log.Infof("Going to restore local/tainted [%s]", tfile.Name())
sourceFile := fmt.Sprintf("%s/%s/%s", itemDirectory, stage, tfile.Name()) sourceFile := fmt.Sprintf("%s/%s/%s", itemDirectory, stage, tfile.Name())
destinationFile := fmt.Sprintf("%s%s", stagedir, tfile.Name()) destinationFile := fmt.Sprintf("%s%s", stagedir, tfile.Name())
if err = CopyFile(sourceFile, destinationFile); err != nil { if err = CopyFile(sourceFile, destinationFile); err != nil {
return fmt.Errorf("failed copy %s %s to %s : %s", itype, sourceFile, destinationFile, err) return fmt.Errorf("failed copy %s %s to %s: %w", itype, sourceFile, destinationFile, err)
} }
log.Infof("restored %s to %s", sourceFile, destinationFile) log.Infof("restored %s to %s", sourceFile, destinationFile)
@ -107,10 +101,12 @@ func restoreHub(dirPath string) error {
} else { } else {
log.Infof("Going to restore local/tainted [%s]", file.Name()) log.Infof("Going to restore local/tainted [%s]", file.Name())
sourceFile := fmt.Sprintf("%s/%s", itemDirectory, file.Name()) sourceFile := fmt.Sprintf("%s/%s", itemDirectory, file.Name())
destinationFile := fmt.Sprintf("%s/%s/%s", csConfig.ConfigPaths.ConfigDir, itype, file.Name()) destinationFile := fmt.Sprintf("%s/%s/%s", cfg.ConfigPaths.ConfigDir, itype, file.Name())
if err = CopyFile(sourceFile, destinationFile); err != nil { if err = CopyFile(sourceFile, destinationFile); err != nil {
return fmt.Errorf("failed copy %s %s to %s : %s", itype, sourceFile, destinationFile, err) return fmt.Errorf("failed copy %s %s to %s: %w", itype, sourceFile, destinationFile, err)
} }
log.Infof("restored %s to %s", sourceFile, destinationFile) log.Infof("restored %s to %s", sourceFile, destinationFile)
} }
} }
@ -130,90 +126,64 @@ func restoreHub(dirPath string) error {
- Tainted/local/out-of-date scenarios, parsers, postoverflows and collections - Tainted/local/out-of-date scenarios, parsers, postoverflows and collections
- Acquisition files (acquis.yaml, acquis.d/*.yaml) - Acquisition files (acquis.yaml, acquis.d/*.yaml)
*/ */
func restoreConfigFromDirectory(dirPath string, oldBackup bool) error { func (cli *cliConfig) restore(dirPath string) error {
var err error var err error
if !oldBackup { cfg := cli.cfg()
backupMain := fmt.Sprintf("%s/config.yaml", dirPath)
if _, err = os.Stat(backupMain); err == nil { backupMain := fmt.Sprintf("%s/config.yaml", dirPath)
if csConfig.ConfigPaths != nil && csConfig.ConfigPaths.ConfigDir != "" { if _, err = os.Stat(backupMain); err == nil {
if err = CopyFile(backupMain, fmt.Sprintf("%s/config.yaml", csConfig.ConfigPaths.ConfigDir)); err != nil { if cfg.ConfigPaths != nil && cfg.ConfigPaths.ConfigDir != "" {
return fmt.Errorf("failed copy %s to %s : %s", backupMain, csConfig.ConfigPaths.ConfigDir, err) if err = CopyFile(backupMain, fmt.Sprintf("%s/config.yaml", cfg.ConfigPaths.ConfigDir)); err != nil {
} return fmt.Errorf("failed copy %s to %s: %w", backupMain, cfg.ConfigPaths.ConfigDir, err)
} }
} }
}
// Now we have config.yaml, we should regenerate config struct to have rights paths etc // Now we have config.yaml, we should regenerate config struct to have rights paths etc
ConfigFilePath = fmt.Sprintf("%s/config.yaml", csConfig.ConfigPaths.ConfigDir) ConfigFilePath = fmt.Sprintf("%s/config.yaml", cfg.ConfigPaths.ConfigDir)
initConfig() log.Debug("Reloading configuration")
backupCAPICreds := fmt.Sprintf("%s/online_api_credentials.yaml", dirPath) csConfig, _, err = loadConfigFor("config")
if _, err = os.Stat(backupCAPICreds); err == nil { if err != nil {
if err = CopyFile(backupCAPICreds, csConfig.API.Server.OnlineClient.CredentialsFilePath); err != nil { return fmt.Errorf("failed to reload configuration: %w", err)
return fmt.Errorf("failed copy %s to %s : %s", backupCAPICreds, csConfig.API.Server.OnlineClient.CredentialsFilePath, err) }
}
cfg = cli.cfg()
backupCAPICreds := fmt.Sprintf("%s/online_api_credentials.yaml", dirPath)
if _, err = os.Stat(backupCAPICreds); err == nil {
if err = CopyFile(backupCAPICreds, cfg.API.Server.OnlineClient.CredentialsFilePath); err != nil {
return fmt.Errorf("failed copy %s to %s: %w", backupCAPICreds, cfg.API.Server.OnlineClient.CredentialsFilePath, err)
} }
}
backupLAPICreds := fmt.Sprintf("%s/local_api_credentials.yaml", dirPath) backupLAPICreds := fmt.Sprintf("%s/local_api_credentials.yaml", dirPath)
if _, err = os.Stat(backupLAPICreds); err == nil { if _, err = os.Stat(backupLAPICreds); err == nil {
if err = CopyFile(backupLAPICreds, csConfig.API.Client.CredentialsFilePath); err != nil { if err = CopyFile(backupLAPICreds, cfg.API.Client.CredentialsFilePath); err != nil {
return fmt.Errorf("failed copy %s to %s : %s", backupLAPICreds, csConfig.API.Client.CredentialsFilePath, err) return fmt.Errorf("failed copy %s to %s: %w", backupLAPICreds, cfg.API.Client.CredentialsFilePath, err)
}
} }
}
backupProfiles := fmt.Sprintf("%s/profiles.yaml", dirPath) backupProfiles := fmt.Sprintf("%s/profiles.yaml", dirPath)
if _, err = os.Stat(backupProfiles); err == nil { if _, err = os.Stat(backupProfiles); err == nil {
if err = CopyFile(backupProfiles, csConfig.API.Server.ProfilesPath); err != nil { if err = CopyFile(backupProfiles, cfg.API.Server.ProfilesPath); err != nil {
return fmt.Errorf("failed copy %s to %s : %s", backupProfiles, csConfig.API.Server.ProfilesPath, err) return fmt.Errorf("failed copy %s to %s: %w", backupProfiles, cfg.API.Server.ProfilesPath, err)
}
}
} else {
var oldAPICfg OldAPICfg
backupOldAPICfg := fmt.Sprintf("%s/api_creds.json", dirPath)
jsonFile, err := os.Open(backupOldAPICfg)
if err != nil {
log.Warningf("failed to open %s : %s", backupOldAPICfg, err)
} else {
byteValue, _ := io.ReadAll(jsonFile)
err = json.Unmarshal(byteValue, &oldAPICfg)
if err != nil {
return fmt.Errorf("failed to load json file %s : %s", backupOldAPICfg, err)
}
apiCfg := csconfig.ApiCredentialsCfg{
Login: oldAPICfg.MachineID,
Password: oldAPICfg.Password,
URL: CAPIBaseURL,
}
apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil {
return fmt.Errorf("unable to dump api credentials: %s", err)
}
apiConfigDumpFile := fmt.Sprintf("%s/online_api_credentials.yaml", csConfig.ConfigPaths.ConfigDir)
if csConfig.API.Server.OnlineClient != nil && csConfig.API.Server.OnlineClient.CredentialsFilePath != "" {
apiConfigDumpFile = csConfig.API.Server.OnlineClient.CredentialsFilePath
}
err = os.WriteFile(apiConfigDumpFile, apiConfigDump, 0o600)
if err != nil {
return fmt.Errorf("write api credentials in '%s' failed: %s", apiConfigDumpFile, err)
}
log.Infof("Saved API credentials to %s", apiConfigDumpFile)
} }
} }
backupSimulation := fmt.Sprintf("%s/simulation.yaml", dirPath) backupSimulation := fmt.Sprintf("%s/simulation.yaml", dirPath)
if _, err = os.Stat(backupSimulation); err == nil { if _, err = os.Stat(backupSimulation); err == nil {
if err = CopyFile(backupSimulation, csConfig.ConfigPaths.SimulationFilePath); err != nil { if err = CopyFile(backupSimulation, cfg.ConfigPaths.SimulationFilePath); err != nil {
return fmt.Errorf("failed copy %s to %s : %s", backupSimulation, csConfig.ConfigPaths.SimulationFilePath, err) return fmt.Errorf("failed copy %s to %s: %w", backupSimulation, cfg.ConfigPaths.SimulationFilePath, err)
} }
} }
/*if there is a acquisition dir, restore its content*/ /*if there is a acquisition dir, restore its content*/
if csConfig.Crowdsec.AcquisitionDirPath != "" { if cfg.Crowdsec.AcquisitionDirPath != "" {
if err = os.MkdirAll(csConfig.Crowdsec.AcquisitionDirPath, 0o700); err != nil { if err = os.MkdirAll(cfg.Crowdsec.AcquisitionDirPath, 0o700); err != nil {
return fmt.Errorf("error while creating %s : %s", csConfig.Crowdsec.AcquisitionDirPath, err) return fmt.Errorf("error while creating %s: %w", cfg.Crowdsec.AcquisitionDirPath, err)
} }
} }
@ -222,16 +192,16 @@ func restoreConfigFromDirectory(dirPath string, oldBackup bool) error {
if _, err = os.Stat(backupAcquisition); err == nil { if _, err = os.Stat(backupAcquisition); err == nil {
log.Debugf("restoring backup'ed %s", backupAcquisition) log.Debugf("restoring backup'ed %s", backupAcquisition)
if err = CopyFile(backupAcquisition, csConfig.Crowdsec.AcquisitionFilePath); err != nil { if err = CopyFile(backupAcquisition, cfg.Crowdsec.AcquisitionFilePath); err != nil {
return fmt.Errorf("failed copy %s to %s : %s", backupAcquisition, csConfig.Crowdsec.AcquisitionFilePath, err) return fmt.Errorf("failed copy %s to %s: %w", backupAcquisition, cfg.Crowdsec.AcquisitionFilePath, err)
} }
} }
// if there is files in the acquis backup dir, restore them // if there are files in the acquis backup dir, restore them
acquisBackupDir := filepath.Join(dirPath, "acquis", "*.yaml") acquisBackupDir := filepath.Join(dirPath, "acquis", "*.yaml")
if acquisFiles, err := filepath.Glob(acquisBackupDir); err == nil { if acquisFiles, err := filepath.Glob(acquisBackupDir); err == nil {
for _, acquisFile := range acquisFiles { for _, acquisFile := range acquisFiles {
targetFname, err := filepath.Abs(csConfig.Crowdsec.AcquisitionDirPath + "/" + filepath.Base(acquisFile)) targetFname, err := filepath.Abs(cfg.Crowdsec.AcquisitionDirPath + "/" + filepath.Base(acquisFile))
if err != nil { if err != nil {
return fmt.Errorf("while saving %s to %s: %w", acquisFile, targetFname, err) return fmt.Errorf("while saving %s to %s: %w", acquisFile, targetFname, err)
} }
@ -239,17 +209,17 @@ func restoreConfigFromDirectory(dirPath string, oldBackup bool) error {
log.Debugf("restoring %s to %s", acquisFile, targetFname) log.Debugf("restoring %s to %s", acquisFile, targetFname)
if err = CopyFile(acquisFile, targetFname); err != nil { if err = CopyFile(acquisFile, targetFname); err != nil {
return fmt.Errorf("failed copy %s to %s : %s", acquisFile, targetFname, err) return fmt.Errorf("failed copy %s to %s: %w", acquisFile, targetFname, err)
} }
} }
} }
if csConfig.Crowdsec != nil && len(csConfig.Crowdsec.AcquisitionFiles) > 0 { if cfg.Crowdsec != nil && len(cfg.Crowdsec.AcquisitionFiles) > 0 {
for _, acquisFile := range csConfig.Crowdsec.AcquisitionFiles { for _, acquisFile := range cfg.Crowdsec.AcquisitionFiles {
log.Infof("backup filepath from dir -> %s", acquisFile) log.Infof("backup filepath from dir -> %s", acquisFile)
// if it was the default one, it has already been backed up // if it was the default one, it has already been backed up
if csConfig.Crowdsec.AcquisitionFilePath == acquisFile { if cfg.Crowdsec.AcquisitionFilePath == acquisFile {
log.Infof("skip this one") log.Infof("skip this one")
continue continue
} }
@ -260,37 +230,22 @@ func restoreConfigFromDirectory(dirPath string, oldBackup bool) error {
} }
if err = CopyFile(acquisFile, targetFname); err != nil { if err = CopyFile(acquisFile, targetFname); err != nil {
return fmt.Errorf("failed copy %s to %s : %s", acquisFile, targetFname, err) return fmt.Errorf("failed copy %s to %s: %w", acquisFile, targetFname, err)
} }
log.Infof("Saved acquis %s to %s", acquisFile, targetFname) log.Infof("Saved acquis %s to %s", acquisFile, targetFname)
} }
} }
if err = restoreHub(dirPath); err != nil { if err = cli.restoreHub(dirPath); err != nil {
return fmt.Errorf("failed to restore hub config : %s", err) return fmt.Errorf("failed to restore hub config: %w", err)
} }
return nil return nil
} }
func runConfigRestore(cmd *cobra.Command, args []string) error { func (cli *cliConfig) newRestoreCmd() *cobra.Command {
flags := cmd.Flags() cmd := &cobra.Command{
oldBackup, err := flags.GetBool("old-backup")
if err != nil {
return err
}
if err := restoreConfigFromDirectory(args[0], oldBackup); err != nil {
return fmt.Errorf("failed to restore config from %s: %w", args[0], err)
}
return nil
}
func NewConfigRestoreCmd() *cobra.Command {
cmdConfigRestore := &cobra.Command{
Use: `restore "directory"`, Use: `restore "directory"`,
Short: `Restore config in backup "directory"`, Short: `Restore config in backup "directory"`,
Long: `Restore the crowdsec configuration from specified backup "directory" including: Long: `Restore the crowdsec configuration from specified backup "directory" including:
@ -303,11 +258,16 @@ func NewConfigRestoreCmd() *cobra.Command {
- Backup of API credentials (local API and online API)`, - Backup of API credentials (local API and online API)`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runConfigRestore, RunE: func(_ *cobra.Command, args []string) error {
dirPath := args[0]
if err := cli.restore(dirPath); err != nil {
return fmt.Errorf("failed to restore config from %s: %w", dirPath, err)
}
return nil
},
} }
flags := cmdConfigRestore.Flags() return cmd
flags.BoolP("old-backup", "", false, "To use when you are upgrading crowdsec v0.X to v1.X and you need to restore backup from v0.X")
return cmdConfigRestore
} }

View file

@ -10,13 +10,15 @@ import (
"github.com/sanity-io/litter" "github.com/sanity-io/litter"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers" "github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
) )
func showConfigKey(key string) error { func (cli *cliConfig) showKey(key string) error {
cfg := cli.cfg()
type Env struct { type Env struct {
Config *csconfig.Config Config *csconfig.Config
} }
@ -30,15 +32,15 @@ func showConfigKey(key string) error {
return err return err
} }
output, err := expr.Run(program, Env{Config: csConfig}) output, err := expr.Run(program, Env{Config: cfg})
if err != nil { if err != nil {
return err return err
} }
switch csConfig.Cscli.Output { switch cfg.Cscli.Output {
case "human", "raw": case "human", "raw":
// Don't use litter for strings, it adds quotes // Don't use litter for strings, it adds quotes
// that we didn't have before // that would break compatibility with previous versions
switch output.(type) { switch output.(type) {
case string: case string:
fmt.Println(output) fmt.Println(output)
@ -51,13 +53,14 @@ func showConfigKey(key string) error {
return fmt.Errorf("failed to marshal configuration: %w", err) return fmt.Errorf("failed to marshal configuration: %w", err)
} }
fmt.Printf("%s\n", string(data)) fmt.Println(string(data))
} }
return nil return nil
} }
var configShowTemplate = `Global: func (cli *cliConfig) template() string {
return `Global:
{{- if .ConfigPaths }} {{- if .ConfigPaths }}
- Configuration Folder : {{.ConfigPaths.ConfigDir}} - Configuration Folder : {{.ConfigPaths.ConfigDir}}
@ -100,6 +103,7 @@ API Client:
{{- if .API.Server }} {{- if .API.Server }}
Local API Server{{if and .API.Server.Enable (not (ValueBool .API.Server.Enable))}} (disabled){{end}}: Local API Server{{if and .API.Server.Enable (not (ValueBool .API.Server.Enable))}} (disabled){{end}}:
- Listen URL : {{.API.Server.ListenURI}} - Listen URL : {{.API.Server.ListenURI}}
- Listen Socket : {{.API.Server.ListenSocket}}
- Profile File : {{.API.Server.ProfilesPath}} - Profile File : {{.API.Server.ProfilesPath}}
{{- if .API.Server.TLS }} {{- if .API.Server.TLS }}
@ -181,74 +185,74 @@ Central API:
{{- end }} {{- end }}
{{- end }} {{- end }}
` `
}
func runConfigShow(cmd *cobra.Command, args []string) error { func (cli *cliConfig) show() error {
flags := cmd.Flags() cfg := cli.cfg()
if err := csConfig.LoadAPIClient(); err != nil { switch cfg.Cscli.Output {
log.Errorf("failed to load API client configuration: %s", err)
// don't return, we can still show the configuration
}
key, err := flags.GetString("key")
if err != nil {
return err
}
if key != "" {
return showConfigKey(key)
}
switch csConfig.Cscli.Output {
case "human": case "human":
// The tests on .Enable look funny because the option has a true default which has // The tests on .Enable look funny because the option has a true default which has
// not been set yet (we don't really load the LAPI) and go templates don't dereference // not been set yet (we don't really load the LAPI) and go templates don't dereference
// pointers in boolean tests. Prefix notation is the cherry on top. // pointers in boolean tests. Prefix notation is the cherry on top.
funcs := template.FuncMap{ funcs := template.FuncMap{
// can't use generics here // can't use generics here
"ValueBool": func(b *bool) bool { return b!=nil && *b }, "ValueBool": func(b *bool) bool { return b != nil && *b },
} }
tmp, err := template.New("config").Funcs(funcs).Parse(configShowTemplate) tmp, err := template.New("config").Funcs(funcs).Parse(cli.template())
if err != nil { if err != nil {
return err return err
} }
err = tmp.Execute(os.Stdout, csConfig) err = tmp.Execute(os.Stdout, cfg)
if err != nil { if err != nil {
return err return err
} }
case "json": case "json":
data, err := json.MarshalIndent(csConfig, "", " ") data, err := json.MarshalIndent(cfg, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal configuration: %w", err) return fmt.Errorf("failed to marshal configuration: %w", err)
} }
fmt.Printf("%s\n", string(data)) fmt.Println(string(data))
case "raw": case "raw":
data, err := yaml.Marshal(csConfig) data, err := yaml.Marshal(cfg)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal configuration: %w", err) return fmt.Errorf("failed to marshal configuration: %w", err)
} }
fmt.Printf("%s\n", string(data)) fmt.Println(string(data))
} }
return nil return nil
} }
func NewConfigShowCmd() *cobra.Command { func (cli *cliConfig) newShowCmd() *cobra.Command {
cmdConfigShow := &cobra.Command{ var key string
cmd := &cobra.Command{
Use: "show", Use: "show",
Short: "Displays current config", Short: "Displays current config",
Long: `Displays the current cli configuration.`, Long: `Displays the current cli configuration.`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runConfigShow, RunE: func(_ *cobra.Command, _ []string) error {
if err := cli.cfg().LoadAPIClient(); err != nil {
log.Errorf("failed to load API client configuration: %s", err)
// don't return, we can still show the configuration
}
if key != "" {
return cli.showKey(key)
}
return cli.show()
},
} }
flags := cmdConfigShow.Flags() flags := cmd.Flags()
flags.StringP("key", "", "", "Display only this value (Config.API.Server.ListenURI)") flags.StringVarP(&key, "key", "", "", "Display only this value (Config.API.Server.ListenURI)")
return cmdConfigShow return cmd
} }

View file

@ -6,19 +6,21 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func runConfigShowYAML(cmd *cobra.Command, args []string) error { func (cli *cliConfig) showYAML() error {
fmt.Println(mergedConfig) fmt.Println(mergedConfig)
return nil return nil
} }
func NewConfigShowYAMLCmd() *cobra.Command { func (cli *cliConfig) newShowYAMLCmd() *cobra.Command {
cmdConfigShow := &cobra.Command{ cmd := &cobra.Command{
Use: "show-yaml", Use: "show-yaml",
Short: "Displays merged config.yaml + config.yaml.local", Short: "Displays merged config.yaml + config.yaml.local",
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runConfigShowYAML, RunE: func(_ *cobra.Command, _ []string) error {
return cli.showYAML()
},
} }
return cmdConfigShow return cmd
} }

View file

@ -4,9 +4,12 @@ import (
"context" "context"
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"strconv"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
@ -24,31 +27,53 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
) )
func NewConsoleCmd() *cobra.Command { type cliConsole struct {
var cmdConsole = &cobra.Command{ cfg configGetter
}
func NewCLIConsole(cfg configGetter) *cliConsole {
return &cliConsole{
cfg: cfg,
}
}
func (cli *cliConsole) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "console [action]", Use: "console [action]",
Short: "Manage interaction with Crowdsec console (https://app.crowdsec.net)", Short: "Manage interaction with Crowdsec console (https://app.crowdsec.net)",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := require.LAPI(csConfig); err != nil { cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err return err
} }
if err := require.CAPI(csConfig); err != nil { if err := require.CAPI(cfg); err != nil {
return err return err
} }
if err := require.CAPIRegistered(csConfig); err != nil { if err := require.CAPIRegistered(cfg); err != nil {
return err return err
} }
return nil return nil
}, },
} }
cmd.AddCommand(cli.newEnrollCmd())
cmd.AddCommand(cli.newEnableCmd())
cmd.AddCommand(cli.newDisableCmd())
cmd.AddCommand(cli.newStatusCmd())
return cmd
}
func (cli *cliConsole) newEnrollCmd() *cobra.Command {
name := "" name := ""
overwrite := false overwrite := false
tags := []string{} tags := []string{}
opts := []string{}
cmdEnroll := &cobra.Command{ cmd := &cobra.Command{
Use: "enroll [enroll-key]", Use: "enroll [enroll-key]",
Short: "Enroll this instance to https://app.crowdsec.net [requires local API]", Short: "Enroll this instance to https://app.crowdsec.net [requires local API]",
Long: ` Long: `
@ -56,68 +81,115 @@ Enroll this instance to https://app.crowdsec.net
You can get your enrollment key by creating an account on https://app.crowdsec.net. You can get your enrollment key by creating an account on https://app.crowdsec.net.
After running this command your will need to validate the enrollment in the webapp.`, After running this command your will need to validate the enrollment in the webapp.`,
Example: `cscli console enroll YOUR-ENROLL-KEY Example: fmt.Sprintf(`cscli console enroll YOUR-ENROLL-KEY
cscli console enroll --name [instance_name] YOUR-ENROLL-KEY cscli console enroll --name [instance_name] YOUR-ENROLL-KEY
cscli console enroll --name [instance_name] --tags [tag_1] --tags [tag_2] YOUR-ENROLL-KEY cscli console enroll --name [instance_name] --tags [tag_1] --tags [tag_2] YOUR-ENROLL-KEY
`, cscli console enroll --enable context,manual YOUR-ENROLL-KEY
valid options are : %s,all (see 'cscli console status' for details)`, strings.Join(csconfig.CONSOLE_CONFIGS, ",")),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
password := strfmt.Password(csConfig.API.Server.OnlineClient.Credentials.Password) cfg := cli.cfg()
apiURL, err := url.Parse(csConfig.API.Server.OnlineClient.Credentials.URL) password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password)
apiURL, err := url.Parse(cfg.API.Server.OnlineClient.Credentials.URL)
if err != nil { if err != nil {
return fmt.Errorf("could not parse CAPI URL: %s", err) return fmt.Errorf("could not parse CAPI URL: %w", err)
} }
hub, err := require.Hub(csConfig, nil, nil) hub, err := require.Hub(cfg, nil, nil)
if err != nil { if err != nil {
return err return err
} }
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS) scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
if err != nil { if err != nil {
return fmt.Errorf("failed to get installed scenarios: %s", err) return fmt.Errorf("failed to get installed scenarios: %w", err)
} }
if len(scenarios) == 0 { if len(scenarios) == 0 {
scenarios = make([]string, 0) scenarios = make([]string, 0)
} }
enableOpts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS}
if len(opts) != 0 {
for _, opt := range opts {
valid := false
if opt == "all" {
enableOpts = csconfig.CONSOLE_CONFIGS
break
}
for _, availableOpt := range csconfig.CONSOLE_CONFIGS {
if opt == availableOpt {
valid = true
enable := true
for _, enabledOpt := range enableOpts {
if opt == enabledOpt {
enable = false
continue
}
}
if enable {
enableOpts = append(enableOpts, opt)
}
break
}
}
if !valid {
return fmt.Errorf("option %s doesn't exist", opt)
}
}
}
c, _ := apiclient.NewClient(&apiclient.Config{ c, _ := apiclient.NewClient(&apiclient.Config{
MachineID: csConfig.API.Server.OnlineClient.Credentials.Login, MachineID: cli.cfg().API.Server.OnlineClient.Credentials.Login,
Password: password, Password: password,
Scenarios: scenarios, Scenarios: scenarios,
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiURL, URL: apiURL,
VersionPrefix: "v3", VersionPrefix: "v3",
}) })
resp, err := c.Auth.EnrollWatcher(context.Background(), args[0], name, tags, overwrite) resp, err := c.Auth.EnrollWatcher(context.Background(), args[0], name, tags, overwrite)
if err != nil { if err != nil {
return fmt.Errorf("could not enroll instance: %s", err) return fmt.Errorf("could not enroll instance: %w", err)
} }
if resp.Response.StatusCode == 200 && !overwrite { if resp.Response.StatusCode == 200 && !overwrite {
log.Warning("Instance already enrolled. You can use '--overwrite' to force enroll") log.Warning("Instance already enrolled. You can use '--overwrite' to force enroll")
return nil return nil
} }
if err := SetConsoleOpts([]string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS}, true); err != nil { if err := cli.setConsoleOpts(enableOpts, true); err != nil {
return err return err
} }
log.Info("Enabled tainted&manual alerts sharing, see 'cscli console status'.") for _, opt := range enableOpts {
log.Infof("Enabled %s : %s", opt, csconfig.CONSOLE_CONFIGS_HELP[opt])
}
log.Info("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.") log.Info("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.")
log.Info("Please restart crowdsec after accepting the enrollment.") log.Info("Please restart crowdsec after accepting the enrollment.")
return nil return nil
}, },
} }
cmdEnroll.Flags().StringVarP(&name, "name", "n", "", "Name to display in the console")
cmdEnroll.Flags().BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
cmdEnroll.Flags().StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
cmdConsole.AddCommand(cmdEnroll)
var enableAll, disableAll bool flags := cmd.Flags()
flags.StringVarP(&name, "name", "n", "", "Name to display in the console")
flags.BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
flags.StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
flags.StringSliceVarP(&opts, "enable", "e", opts, "Enable console options")
cmdEnable := &cobra.Command{ return cmd
}
func (cli *cliConsole) newEnableCmd() *cobra.Command {
var enableAll bool
cmd := &cobra.Command{
Use: "enable [option]", Use: "enable [option]",
Short: "Enable a console option", Short: "Enable a console option",
Example: "sudo cscli console enable tainted", Example: "sudo cscli console enable tainted",
@ -125,29 +197,36 @@ After running this command your will need to validate the enrollment in the weba
Enable given information push to the central API. Allows to empower the console`, Enable given information push to the central API. Allows to empower the console`,
ValidArgs: csconfig.CONSOLE_CONFIGS, ValidArgs: csconfig.CONSOLE_CONFIGS,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if enableAll { if enableAll {
if err := SetConsoleOpts(csconfig.CONSOLE_CONFIGS, true); err != nil { if err := cli.setConsoleOpts(csconfig.CONSOLE_CONFIGS, true); err != nil {
return err return err
} }
log.Infof("All features have been enabled successfully") log.Infof("All features have been enabled successfully")
} else { } else {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("you must specify at least one feature to enable") return errors.New("you must specify at least one feature to enable")
} }
if err := SetConsoleOpts(args, true); err != nil { if err := cli.setConsoleOpts(args, true); err != nil {
return err return err
} }
log.Infof("%v have been enabled", args) log.Infof("%v have been enabled", args)
} }
log.Infof(ReloadMessage()) log.Infof(ReloadMessage())
return nil return nil
}, },
} }
cmdEnable.Flags().BoolVarP(&enableAll, "all", "a", false, "Enable all console options") cmd.Flags().BoolVarP(&enableAll, "all", "a", false, "Enable all console options")
cmdConsole.AddCommand(cmdEnable)
cmdDisable := &cobra.Command{ return cmd
}
func (cli *cliConsole) newDisableCmd() *cobra.Command {
var disableAll bool
cmd := &cobra.Command{
Use: "disable [option]", Use: "disable [option]",
Short: "Disable a console option", Short: "Disable a console option",
Example: "sudo cscli console disable tainted", Example: "sudo cscli console disable tainted",
@ -155,47 +234,52 @@ Enable given information push to the central API. Allows to empower the console`
Disable given information push to the central API.`, Disable given information push to the central API.`,
ValidArgs: csconfig.CONSOLE_CONFIGS, ValidArgs: csconfig.CONSOLE_CONFIGS,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if disableAll { if disableAll {
if err := SetConsoleOpts(csconfig.CONSOLE_CONFIGS, false); err != nil { if err := cli.setConsoleOpts(csconfig.CONSOLE_CONFIGS, false); err != nil {
return err return err
} }
log.Infof("All features have been disabled") log.Infof("All features have been disabled")
} else { } else {
if err := SetConsoleOpts(args, false); err != nil { if err := cli.setConsoleOpts(args, false); err != nil {
return err return err
} }
log.Infof("%v have been disabled", args) log.Infof("%v have been disabled", args)
} }
log.Infof(ReloadMessage()) log.Infof(ReloadMessage())
return nil return nil
}, },
} }
cmdDisable.Flags().BoolVarP(&disableAll, "all", "a", false, "Disable all console options") cmd.Flags().BoolVarP(&disableAll, "all", "a", false, "Disable all console options")
cmdConsole.AddCommand(cmdDisable)
cmdConsoleStatus := &cobra.Command{ return cmd
}
func (cli *cliConsole) newStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status", Use: "status",
Short: "Shows status of the console options", Short: "Shows status of the console options",
Example: `sudo cscli console status`, Example: `sudo cscli console status`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
switch csConfig.Cscli.Output { cfg := cli.cfg()
consoleCfg := cfg.API.Server.ConsoleConfig
switch cfg.Cscli.Output {
case "human": case "human":
cmdConsoleStatusTable(color.Output, *csConfig) cmdConsoleStatusTable(color.Output, *consoleCfg)
case "json": case "json":
c := csConfig.API.Server.ConsoleConfig
out := map[string](*bool){ out := map[string](*bool){
csconfig.SEND_MANUAL_SCENARIOS: c.ShareManualDecisions, csconfig.SEND_MANUAL_SCENARIOS: consoleCfg.ShareManualDecisions,
csconfig.SEND_CUSTOM_SCENARIOS: c.ShareCustomScenarios, csconfig.SEND_CUSTOM_SCENARIOS: consoleCfg.ShareCustomScenarios,
csconfig.SEND_TAINTED_SCENARIOS: c.ShareTaintedScenarios, csconfig.SEND_TAINTED_SCENARIOS: consoleCfg.ShareTaintedScenarios,
csconfig.SEND_CONTEXT: c.ShareContext, csconfig.SEND_CONTEXT: consoleCfg.ShareContext,
csconfig.CONSOLE_MANAGEMENT: c.ConsoleManagement, csconfig.CONSOLE_MANAGEMENT: consoleCfg.ConsoleManagement,
} }
data, err := json.MarshalIndent(out, "", " ") data, err := json.MarshalIndent(out, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal configuration: %s", err) return fmt.Errorf("failed to marshal configuration: %w", err)
} }
fmt.Println(string(data)) fmt.Println(string(data))
case "raw": case "raw":
@ -206,11 +290,11 @@ Disable given information push to the central API.`,
} }
rows := [][]string{ rows := [][]string{
{csconfig.SEND_MANUAL_SCENARIOS, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareManualDecisions)}, {csconfig.SEND_MANUAL_SCENARIOS, strconv.FormatBool(*consoleCfg.ShareManualDecisions)},
{csconfig.SEND_CUSTOM_SCENARIOS, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios)}, {csconfig.SEND_CUSTOM_SCENARIOS, strconv.FormatBool(*consoleCfg.ShareCustomScenarios)},
{csconfig.SEND_TAINTED_SCENARIOS, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios)}, {csconfig.SEND_TAINTED_SCENARIOS, strconv.FormatBool(*consoleCfg.ShareTaintedScenarios)},
{csconfig.SEND_CONTEXT, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareContext)}, {csconfig.SEND_CONTEXT, strconv.FormatBool(*consoleCfg.ShareContext)},
{csconfig.CONSOLE_MANAGEMENT, fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ConsoleManagement)}, {csconfig.CONSOLE_MANAGEMENT, strconv.FormatBool(*consoleCfg.ConsoleManagement)},
} }
for _, row := range rows { for _, row := range rows {
err = csvwriter.Write(row) err = csvwriter.Write(row)
@ -220,132 +304,137 @@ Disable given information push to the central API.`,
} }
csvwriter.Flush() csvwriter.Flush()
} }
return nil return nil
}, },
} }
cmdConsole.AddCommand(cmdConsoleStatus)
return cmdConsole return cmd
} }
func dumpConsoleConfig(c *csconfig.LocalApiServerCfg) error { func (cli *cliConsole) dumpConfig() error {
out, err := yaml.Marshal(c.ConsoleConfig) serverCfg := cli.cfg().API.Server
out, err := yaml.Marshal(serverCfg.ConsoleConfig)
if err != nil { if err != nil {
return fmt.Errorf("while marshaling ConsoleConfig (for %s): %w", c.ConsoleConfigPath, err) return fmt.Errorf("while marshaling ConsoleConfig (for %s): %w", serverCfg.ConsoleConfigPath, err)
} }
if c.ConsoleConfigPath == "" { if serverCfg.ConsoleConfigPath == "" {
c.ConsoleConfigPath = csconfig.DefaultConsoleConfigFilePath serverCfg.ConsoleConfigPath = csconfig.DefaultConsoleConfigFilePath
log.Debugf("Empty console_path, defaulting to %s", c.ConsoleConfigPath) log.Debugf("Empty console_path, defaulting to %s", serverCfg.ConsoleConfigPath)
} }
if err := os.WriteFile(c.ConsoleConfigPath, out, 0o600); err != nil { if err := os.WriteFile(serverCfg.ConsoleConfigPath, out, 0o600); err != nil {
return fmt.Errorf("while dumping console config to %s: %w", c.ConsoleConfigPath, err) return fmt.Errorf("while dumping console config to %s: %w", serverCfg.ConsoleConfigPath, err)
} }
return nil return nil
} }
func SetConsoleOpts(args []string, wanted bool) error { func (cli *cliConsole) setConsoleOpts(args []string, wanted bool) error {
cfg := cli.cfg()
consoleCfg := cfg.API.Server.ConsoleConfig
for _, arg := range args { for _, arg := range args {
switch arg { switch arg {
case csconfig.CONSOLE_MANAGEMENT: case csconfig.CONSOLE_MANAGEMENT:
/*for each flag check if it's already set before setting it*/ /*for each flag check if it's already set before setting it*/
if csConfig.API.Server.ConsoleConfig.ConsoleManagement != nil { if consoleCfg.ConsoleManagement != nil {
if *csConfig.API.Server.ConsoleConfig.ConsoleManagement == wanted { if *consoleCfg.ConsoleManagement == wanted {
log.Debugf("%s already set to %t", csconfig.CONSOLE_MANAGEMENT, wanted) log.Debugf("%s already set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
} else { } else {
log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted) log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
*csConfig.API.Server.ConsoleConfig.ConsoleManagement = wanted *consoleCfg.ConsoleManagement = wanted
} }
} else { } else {
log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted) log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
csConfig.API.Server.ConsoleConfig.ConsoleManagement = ptr.Of(wanted) consoleCfg.ConsoleManagement = ptr.Of(wanted)
} }
if csConfig.API.Server.OnlineClient.Credentials != nil { if cfg.API.Server.OnlineClient.Credentials != nil {
changed := false changed := false
if wanted && csConfig.API.Server.OnlineClient.Credentials.PapiURL == "" { if wanted && cfg.API.Server.OnlineClient.Credentials.PapiURL == "" {
changed = true changed = true
csConfig.API.Server.OnlineClient.Credentials.PapiURL = types.PAPIBaseURL cfg.API.Server.OnlineClient.Credentials.PapiURL = types.PAPIBaseURL
} else if !wanted && csConfig.API.Server.OnlineClient.Credentials.PapiURL != "" { } else if !wanted && cfg.API.Server.OnlineClient.Credentials.PapiURL != "" {
changed = true changed = true
csConfig.API.Server.OnlineClient.Credentials.PapiURL = "" cfg.API.Server.OnlineClient.Credentials.PapiURL = ""
} }
if changed { if changed {
fileContent, err := yaml.Marshal(csConfig.API.Server.OnlineClient.Credentials) fileContent, err := yaml.Marshal(cfg.API.Server.OnlineClient.Credentials)
if err != nil { if err != nil {
return fmt.Errorf("cannot marshal credentials: %s", err) return fmt.Errorf("cannot marshal credentials: %w", err)
} }
log.Infof("Updating credentials file: %s", csConfig.API.Server.OnlineClient.CredentialsFilePath) log.Infof("Updating credentials file: %s", cfg.API.Server.OnlineClient.CredentialsFilePath)
err = os.WriteFile(csConfig.API.Server.OnlineClient.CredentialsFilePath, fileContent, 0o600) err = os.WriteFile(cfg.API.Server.OnlineClient.CredentialsFilePath, fileContent, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("cannot write credentials file: %s", err) return fmt.Errorf("cannot write credentials file: %w", err)
} }
} }
} }
case csconfig.SEND_CUSTOM_SCENARIOS: case csconfig.SEND_CUSTOM_SCENARIOS:
/*for each flag check if it's already set before setting it*/ /*for each flag check if it's already set before setting it*/
if csConfig.API.Server.ConsoleConfig.ShareCustomScenarios != nil { if consoleCfg.ShareCustomScenarios != nil {
if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios == wanted { if *consoleCfg.ShareCustomScenarios == wanted {
log.Debugf("%s already set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted) log.Debugf("%s already set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted) log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
*csConfig.API.Server.ConsoleConfig.ShareCustomScenarios = wanted *consoleCfg.ShareCustomScenarios = wanted
} }
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted) log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
csConfig.API.Server.ConsoleConfig.ShareCustomScenarios = ptr.Of(wanted) consoleCfg.ShareCustomScenarios = ptr.Of(wanted)
} }
case csconfig.SEND_TAINTED_SCENARIOS: case csconfig.SEND_TAINTED_SCENARIOS:
/*for each flag check if it's already set before setting it*/ /*for each flag check if it's already set before setting it*/
if csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios != nil { if consoleCfg.ShareTaintedScenarios != nil {
if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios == wanted { if *consoleCfg.ShareTaintedScenarios == wanted {
log.Debugf("%s already set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted) log.Debugf("%s already set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted) log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
*csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios = wanted *consoleCfg.ShareTaintedScenarios = wanted
} }
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted) log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios = ptr.Of(wanted) consoleCfg.ShareTaintedScenarios = ptr.Of(wanted)
} }
case csconfig.SEND_MANUAL_SCENARIOS: case csconfig.SEND_MANUAL_SCENARIOS:
/*for each flag check if it's already set before setting it*/ /*for each flag check if it's already set before setting it*/
if csConfig.API.Server.ConsoleConfig.ShareManualDecisions != nil { if consoleCfg.ShareManualDecisions != nil {
if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions == wanted { if *consoleCfg.ShareManualDecisions == wanted {
log.Debugf("%s already set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted) log.Debugf("%s already set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted) log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
*csConfig.API.Server.ConsoleConfig.ShareManualDecisions = wanted *consoleCfg.ShareManualDecisions = wanted
} }
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted) log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
csConfig.API.Server.ConsoleConfig.ShareManualDecisions = ptr.Of(wanted) consoleCfg.ShareManualDecisions = ptr.Of(wanted)
} }
case csconfig.SEND_CONTEXT: case csconfig.SEND_CONTEXT:
/*for each flag check if it's already set before setting it*/ /*for each flag check if it's already set before setting it*/
if csConfig.API.Server.ConsoleConfig.ShareContext != nil { if consoleCfg.ShareContext != nil {
if *csConfig.API.Server.ConsoleConfig.ShareContext == wanted { if *consoleCfg.ShareContext == wanted {
log.Debugf("%s already set to %t", csconfig.SEND_CONTEXT, wanted) log.Debugf("%s already set to %t", csconfig.SEND_CONTEXT, wanted)
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted) log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted)
*csConfig.API.Server.ConsoleConfig.ShareContext = wanted *consoleCfg.ShareContext = wanted
} }
} else { } else {
log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted) log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted)
csConfig.API.Server.ConsoleConfig.ShareContext = ptr.Of(wanted) consoleCfg.ShareContext = ptr.Of(wanted)
} }
default: default:
return fmt.Errorf("unknown flag %s", arg) return fmt.Errorf("unknown flag %s", arg)
} }
} }
if err := dumpConsoleConfig(csConfig.API.Server); err != nil { if err := cli.dumpConfig(); err != nil {
return fmt.Errorf("failed writing console config: %s", err) return fmt.Errorf("failed writing console config: %w", err)
} }
return nil return nil

View file

@ -4,12 +4,12 @@ import (
"io" "io"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
"github.com/enescakir/emoji"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
) )
func cmdConsoleStatusTable(out io.Writer, csConfig csconfig.Config) { func cmdConsoleStatusTable(out io.Writer, consoleCfg csconfig.ConsoleConfig) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
@ -17,45 +17,32 @@ func cmdConsoleStatusTable(out io.Writer, csConfig csconfig.Config) {
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
for _, option := range csconfig.CONSOLE_CONFIGS { for _, option := range csconfig.CONSOLE_CONFIGS {
activated := emoji.CrossMark
switch option { switch option {
case csconfig.SEND_CUSTOM_SCENARIOS: case csconfig.SEND_CUSTOM_SCENARIOS:
activated := string(emoji.CrossMark) if *consoleCfg.ShareCustomScenarios {
if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios { activated = emoji.CheckMarkButton
activated = string(emoji.CheckMarkButton)
} }
t.AddRow(option, activated, "Send alerts from custom scenarios to the console")
case csconfig.SEND_MANUAL_SCENARIOS: case csconfig.SEND_MANUAL_SCENARIOS:
activated := string(emoji.CrossMark) if *consoleCfg.ShareManualDecisions {
if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions { activated = emoji.CheckMarkButton
activated = string(emoji.CheckMarkButton)
} }
t.AddRow(option, activated, "Send manual decisions to the console")
case csconfig.SEND_TAINTED_SCENARIOS: case csconfig.SEND_TAINTED_SCENARIOS:
activated := string(emoji.CrossMark) if *consoleCfg.ShareTaintedScenarios {
if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios { activated = emoji.CheckMarkButton
activated = string(emoji.CheckMarkButton)
} }
t.AddRow(option, activated, "Send alerts from tainted scenarios to the console")
case csconfig.SEND_CONTEXT: case csconfig.SEND_CONTEXT:
activated := string(emoji.CrossMark) if *consoleCfg.ShareContext {
if *csConfig.API.Server.ConsoleConfig.ShareContext { activated = emoji.CheckMarkButton
activated = string(emoji.CheckMarkButton)
} }
t.AddRow(option, activated, "Send context with alerts to the console")
case csconfig.CONSOLE_MANAGEMENT: case csconfig.CONSOLE_MANAGEMENT:
activated := string(emoji.CrossMark) if *consoleCfg.ConsoleManagement {
if *csConfig.API.Server.ConsoleConfig.ConsoleManagement { activated = emoji.CheckMarkButton
activated = string(emoji.CheckMarkButton)
} }
t.AddRow(option, activated, "Receive decisions from console")
} }
t.AddRow(option, activated, csconfig.CONSOLE_CONFIGS_HELP[option])
} }
t.Render() t.Render()

View file

@ -9,7 +9,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
/*help to copy the file, ioutil doesn't offer the feature*/ /*help to copy the file, ioutil doesn't offer the feature*/
func copyFileContents(src, dst string) (err error) { func copyFileContents(src, dst string) (err error) {
@ -41,7 +40,7 @@ func copyFileContents(src, dst string) (err error) {
} }
/*copy the file, ioutile doesn't offer the feature*/ /*copy the file, ioutile doesn't offer the feature*/
func CopyFile(sourceSymLink, destinationFile string) (err error) { func CopyFile(sourceSymLink, destinationFile string) error {
sourceFile, err := filepath.EvalSymlinks(sourceSymLink) sourceFile, err := filepath.EvalSymlinks(sourceSymLink)
if err != nil { if err != nil {
log.Infof("Not a symlink : %s", err) log.Infof("Not a symlink : %s", err)
@ -51,7 +50,7 @@ func CopyFile(sourceSymLink, destinationFile string) (err error) {
sourceFileStat, err := os.Stat(sourceFile) sourceFileStat, err := os.Stat(sourceFile)
if err != nil { if err != nil {
return return err
} }
if !sourceFileStat.Mode().IsRegular() { if !sourceFileStat.Mode().IsRegular() {
@ -63,14 +62,15 @@ func CopyFile(sourceSymLink, destinationFile string) (err error) {
destinationFileStat, err := os.Stat(destinationFile) destinationFileStat, err := os.Stat(destinationFile)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return return err
} }
} else { } else {
if !(destinationFileStat.Mode().IsRegular()) { if !(destinationFileStat.Mode().IsRegular()) {
return fmt.Errorf("copyFile: non-regular destination file %s (%q)", destinationFileStat.Name(), destinationFileStat.Mode().String()) return fmt.Errorf("copyFile: non-regular destination file %s (%q)", destinationFileStat.Name(), destinationFileStat.Mode().String())
} }
if os.SameFile(sourceFileStat, destinationFileStat) { if os.SameFile(sourceFileStat, destinationFileStat) {
return return err
} }
} }
@ -78,6 +78,5 @@ func CopyFile(sourceSymLink, destinationFile string) (err error) {
err = copyFileContents(sourceFile, destinationFile) err = copyFileContents(sourceFile, destinationFile)
} }
return return err
} }

View file

@ -19,15 +19,14 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/metabase"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/metabase"
) )
var ( var (
metabaseUser = "crowdsec@crowdsec.net" metabaseUser = "crowdsec@crowdsec.net"
metabasePassword string metabasePassword string
metabaseDbPath string metabaseDBPath string
metabaseConfigPath string metabaseConfigPath string
metabaseConfigFolder = "metabase/" metabaseConfigFolder = "metabase/"
metabaseConfigFile = "metabase.yaml" metabaseConfigFile = "metabase.yaml"
@ -43,14 +42,17 @@ var (
// information needed to set up a random password on user's behalf // information needed to set up a random password on user's behalf
) )
type cliDashboard struct{} type cliDashboard struct {
cfg configGetter
func NewCLIDashboard() *cliDashboard {
return &cliDashboard{}
} }
func (cli cliDashboard) NewCommand() *cobra.Command { func NewCLIDashboard(cfg configGetter) *cliDashboard {
/* ---- UPDATE COMMAND */ return &cliDashboard{
cfg: cfg,
}
}
func (cli *cliDashboard) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dashboard [command]", Use: "dashboard [command]",
Short: "Manage your metabase dashboard container [requires local API]", Short: "Manage your metabase dashboard container [requires local API]",
@ -65,8 +67,9 @@ cscli dashboard start
cscli dashboard stop cscli dashboard stop
cscli dashboard remove cscli dashboard remove
`, `,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := require.LAPI(csConfig); err != nil { cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err return err
} }
@ -74,13 +77,13 @@ cscli dashboard remove
return err return err
} }
metabaseConfigFolderPath := filepath.Join(csConfig.ConfigPaths.ConfigDir, metabaseConfigFolder) metabaseConfigFolderPath := filepath.Join(cfg.ConfigPaths.ConfigDir, metabaseConfigFolder)
metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile) metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil { if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
return err return err
} }
if err := require.DB(csConfig); err != nil { if err := require.DB(cfg); err != nil {
return err return err
} }
@ -95,20 +98,21 @@ cscli dashboard remove
metabaseContainerID = oldContainerID metabaseContainerID = oldContainerID
} }
} }
return nil return nil
}, },
} }
cmd.AddCommand(cli.NewSetupCmd()) cmd.AddCommand(cli.newSetupCmd())
cmd.AddCommand(cli.NewStartCmd()) cmd.AddCommand(cli.newStartCmd())
cmd.AddCommand(cli.NewStopCmd()) cmd.AddCommand(cli.newStopCmd())
cmd.AddCommand(cli.NewShowPasswordCmd()) cmd.AddCommand(cli.newShowPasswordCmd())
cmd.AddCommand(cli.NewRemoveCmd()) cmd.AddCommand(cli.newRemoveCmd())
return cmd return cmd
} }
func (cli cliDashboard) NewSetupCmd() *cobra.Command { func (cli *cliDashboard) newSetupCmd() *cobra.Command {
var force bool var force bool
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -122,9 +126,9 @@ cscli dashboard setup
cscli dashboard setup --listen 0.0.0.0 cscli dashboard setup --listen 0.0.0.0
cscli dashboard setup -l 0.0.0.0 -p 443 --password <password> cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
if metabaseDbPath == "" { if metabaseDBPath == "" {
metabaseDbPath = csConfig.ConfigPaths.DataDir metabaseDBPath = cli.cfg().ConfigPaths.DataDir
} }
if metabasePassword == "" { if metabasePassword == "" {
@ -145,10 +149,10 @@ cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
if err != nil { if err != nil {
return err return err
} }
if err = chownDatabase(dockerGroup.Gid); err != nil { if err = cli.chownDatabase(dockerGroup.Gid); err != nil {
return err return err
} }
mb, err := metabase.SetupMetabase(csConfig.API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDbPath, dockerGroup.Gid, metabaseContainerID, metabaseImage) mb, err := metabase.SetupMetabase(cli.cfg().API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDBPath, dockerGroup.Gid, metabaseContainerID, metabaseImage)
if err != nil { if err != nil {
return err return err
} }
@ -161,29 +165,32 @@ cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
fmt.Printf("\tURL : '%s'\n", mb.Config.ListenURL) fmt.Printf("\tURL : '%s'\n", mb.Config.ListenURL)
fmt.Printf("\tusername : '%s'\n", mb.Config.Username) fmt.Printf("\tusername : '%s'\n", mb.Config.Username)
fmt.Printf("\tpassword : '%s'\n", mb.Config.Password) fmt.Printf("\tpassword : '%s'\n", mb.Config.Password)
return nil return nil
}, },
} }
cmd.Flags().BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
cmd.Flags().StringVarP(&metabaseDbPath, "dir", "d", "", "Shared directory with metabase container") flags := cmd.Flags()
cmd.Flags().StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container") flags.BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
cmd.Flags().StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use") flags.StringVarP(&metabaseDBPath, "dir", "d", "", "Shared directory with metabase container")
cmd.Flags().StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container") flags.StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes") flags.StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
//cmd.Flags().StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user") flags.StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
cmd.Flags().StringVar(&metabasePassword, "password", "", "metabase password") flags.BoolVarP(&forceYes, "yes", "y", false, "force yes")
// flags.StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
flags.StringVar(&metabasePassword, "password", "", "metabase password")
return cmd return cmd
} }
func (cli cliDashboard) NewStartCmd() *cobra.Command { func (cli *cliDashboard) newStartCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "start", Use: "start",
Short: "Start the metabase container.", Short: "Start the metabase container.",
Long: `Stats the metabase container using docker.`, Long: `Stats the metabase container using docker.`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID) mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
if err != nil { if err != nil {
return err return err
@ -197,22 +204,24 @@ func (cli cliDashboard) NewStartCmd() *cobra.Command {
} }
log.Infof("Started metabase") log.Infof("Started metabase")
log.Infof("url : http://%s:%s", mb.Config.ListenAddr, mb.Config.ListenPort) log.Infof("url : http://%s:%s", mb.Config.ListenAddr, mb.Config.ListenPort)
return nil return nil
}, },
} }
cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes") cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes")
return cmd return cmd
} }
func (cli cliDashboard) NewStopCmd() *cobra.Command { func (cli *cliDashboard) newStopCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "stop", Use: "stop",
Short: "Stops the metabase container.", Short: "Stops the metabase container.",
Long: `Stops the metabase container using docker.`, Long: `Stops the metabase container using docker.`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
if err := metabase.StopContainer(metabaseContainerID); err != nil { if err := metabase.StopContainer(metabaseContainerID); err != nil {
return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err) return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
} }
@ -223,17 +232,18 @@ func (cli cliDashboard) NewStopCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliDashboard) NewShowPasswordCmd() *cobra.Command { func (cli *cliDashboard) newShowPasswordCmd() *cobra.Command {
cmd := &cobra.Command{Use: "show-password", cmd := &cobra.Command{Use: "show-password",
Short: "displays password of metabase.", Short: "displays password of metabase.",
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
m := metabase.Metabase{} m := metabase.Metabase{}
if err := m.LoadConfig(metabaseConfigPath); err != nil { if err := m.LoadConfig(metabaseConfigPath); err != nil {
return err return err
} }
log.Printf("'%s'", m.Config.Password) log.Printf("'%s'", m.Config.Password)
return nil return nil
}, },
} }
@ -241,7 +251,7 @@ func (cli cliDashboard) NewShowPasswordCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliDashboard) NewRemoveCmd() *cobra.Command { func (cli *cliDashboard) newRemoveCmd() *cobra.Command {
var force bool var force bool
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -254,7 +264,7 @@ func (cli cliDashboard) NewRemoveCmd() *cobra.Command {
cscli dashboard remove cscli dashboard remove
cscli dashboard remove --force cscli dashboard remove --force
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
if !forceYes { if !forceYes {
var answer bool var answer bool
prompt := &survey.Confirm{ prompt := &survey.Confirm{
@ -291,8 +301,8 @@ cscli dashboard remove --force
} }
log.Infof("container %s stopped & removed", metabaseContainerID) log.Infof("container %s stopped & removed", metabaseContainerID)
} }
log.Debugf("Removing metabase db %s", csConfig.ConfigPaths.DataDir) log.Debugf("Removing metabase db %s", cli.cfg().ConfigPaths.DataDir)
if err := metabase.RemoveDatabase(csConfig.ConfigPaths.DataDir); err != nil { if err := metabase.RemoveDatabase(cli.cfg().ConfigPaths.DataDir); err != nil {
log.Warnf("failed to remove metabase internal db : %s", err) log.Warnf("failed to remove metabase internal db : %s", err)
} }
if force { if force {
@ -306,11 +316,14 @@ cscli dashboard remove --force
} }
} }
} }
return nil return nil
}, },
} }
cmd.Flags().BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes") flags := cmd.Flags()
flags.BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
flags.BoolVarP(&forceYes, "yes", "y", false, "force yes")
return cmd return cmd
} }
@ -431,22 +444,24 @@ func checkGroups(forceYes *bool) (*user.Group, error) {
return user.LookupGroup(crowdsecGroup) return user.LookupGroup(crowdsecGroup)
} }
func chownDatabase(gid string) error { func (cli *cliDashboard) chownDatabase(gid string) error {
cfg := cli.cfg()
intID, err := strconv.Atoi(gid) intID, err := strconv.Atoi(gid)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert group ID to int: %s", err) return fmt.Errorf("unable to convert group ID to int: %s", err)
} }
if stat, err := os.Stat(csConfig.DbConfig.DbPath); !os.IsNotExist(err) { if stat, err := os.Stat(cfg.DbConfig.DbPath); !os.IsNotExist(err) {
info := stat.Sys() info := stat.Sys()
if err := os.Chown(csConfig.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil { if err := os.Chown(cfg.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
return fmt.Errorf("unable to chown sqlite db file '%s': %s", csConfig.DbConfig.DbPath, err) return fmt.Errorf("unable to chown sqlite db file '%s': %s", cfg.DbConfig.DbPath, err)
} }
} }
if csConfig.DbConfig.Type == "sqlite" && csConfig.DbConfig.UseWal != nil && *csConfig.DbConfig.UseWal { if cfg.DbConfig.Type == "sqlite" && cfg.DbConfig.UseWal != nil && *cfg.DbConfig.UseWal {
for _, ext := range []string{"-wal", "-shm"} { for _, ext := range []string{"-wal", "-shm"} {
file := csConfig.DbConfig.DbPath + ext file := cfg.DbConfig.DbPath + ext
if stat, err := os.Stat(file); !os.IsNotExist(err) { if stat, err := os.Stat(file); !os.IsNotExist(err) {
info := stat.Sys() info := stat.Sys()
if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil { if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil {

View file

@ -9,17 +9,21 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type cliDashboard struct{} type cliDashboard struct{
cfg configGetter
}
func NewCLIDashboard() *cliDashboard { func NewCLIDashboard(cfg configGetter) *cliDashboard {
return &cliDashboard{} return &cliDashboard{
cfg: cfg,
}
} }
func (cli cliDashboard) NewCommand() *cobra.Command { func (cli cliDashboard) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dashboard", Use: "dashboard",
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { Run: func(_ *cobra.Command, _ []string) {
log.Infof("Dashboard command is disabled on %s", runtime.GOOS) log.Infof("Dashboard command is disabled on %s", runtime.GOOS)
}, },
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@ -25,7 +26,7 @@ import (
var Client *apiclient.ApiClient var Client *apiclient.ApiClient
func DecisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error { func (cli *cliDecisions) decisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
/*here we cheat a bit : to make it more readable for the user, we dedup some entries*/ /*here we cheat a bit : to make it more readable for the user, we dedup some entries*/
spamLimit := make(map[string]bool) spamLimit := make(map[string]bool)
skipped := 0 skipped := 0
@ -49,7 +50,8 @@ func DecisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error
alertItem.Decisions = newDecisions alertItem.Decisions = newDecisions
} }
if csConfig.Cscli.Output == "raw" { switch cli.cfg().Cscli.Output {
case "raw":
csvwriter := csv.NewWriter(os.Stdout) csvwriter := csv.NewWriter(os.Stdout)
header := []string{"id", "source", "ip", "reason", "action", "country", "as", "events_count", "expiration", "simulated", "alert_id"} header := []string{"id", "source", "ip", "reason", "action", "country", "as", "events_count", "expiration", "simulated", "alert_id"}
@ -89,21 +91,24 @@ func DecisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error
} }
csvwriter.Flush() csvwriter.Flush()
} else if csConfig.Cscli.Output == "json" { case "json":
if *alerts == nil { if *alerts == nil {
// avoid returning "null" in `json" // avoid returning "null" in `json"
// could be cleaner if we used slice of alerts directly // could be cleaner if we used slice of alerts directly
fmt.Println("[]") fmt.Println("[]")
return nil return nil
} }
x, _ := json.MarshalIndent(alerts, "", " ") x, _ := json.MarshalIndent(alerts, "", " ")
fmt.Printf("%s", string(x)) fmt.Printf("%s", string(x))
} else if csConfig.Cscli.Output == "human" { case "human":
if len(*alerts) == 0 { if len(*alerts) == 0 {
fmt.Println("No active decisions") fmt.Println("No active decisions")
return nil return nil
} }
decisionsTable(color.Output, alerts, printMachine)
cli.decisionsTable(color.Output, alerts, printMachine)
if skipped > 0 { if skipped > 0 {
fmt.Printf("%d duplicated entries skipped\n", skipped) fmt.Printf("%d duplicated entries skipped\n", skipped)
} }
@ -112,14 +117,17 @@ func DecisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error
return nil return nil
} }
type cliDecisions struct {
type cliDecisions struct {} cfg configGetter
func NewCLIDecisions() *cliDecisions {
return &cliDecisions{}
} }
func (cli cliDecisions) NewCommand() *cobra.Command { func NewCLIDecisions(cfg configGetter) *cliDecisions {
return &cliDecisions{
cfg: cfg,
}
}
func (cli *cliDecisions) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "decisions [action]", Use: "decisions [action]",
Short: "Manage decisions", Short: "Manage decisions",
@ -130,16 +138,17 @@ func (cli cliDecisions) NewCommand() *cobra.Command {
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := csConfig.LoadAPIClient(); err != nil { cfg := cli.cfg()
if err := cfg.LoadAPIClient(); err != nil {
return fmt.Errorf("loading api client: %w", err) return fmt.Errorf("loading api client: %w", err)
} }
password := strfmt.Password(csConfig.API.Client.Credentials.Password) password := strfmt.Password(cfg.API.Client.Credentials.Password)
apiurl, err := url.Parse(csConfig.API.Client.Credentials.URL) apiurl, err := url.Parse(cfg.API.Client.Credentials.URL)
if err != nil { if err != nil {
return fmt.Errorf("parsing api url %s: %w", csConfig.API.Client.Credentials.URL, err) return fmt.Errorf("parsing api url %s: %w", cfg.API.Client.Credentials.URL, err)
} }
Client, err = apiclient.NewClient(&apiclient.Config{ Client, err = apiclient.NewClient(&apiclient.Config{
MachineID: csConfig.API.Client.Credentials.Login, MachineID: cfg.API.Client.Credentials.Login,
Password: password, Password: password,
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiurl, URL: apiurl,
@ -148,19 +157,20 @@ func (cli cliDecisions) NewCommand() *cobra.Command {
if err != nil { if err != nil {
return fmt.Errorf("creating api client: %w", err) return fmt.Errorf("creating api client: %w", err)
} }
return nil return nil
}, },
} }
cmd.AddCommand(cli.NewListCmd()) cmd.AddCommand(cli.newListCmd())
cmd.AddCommand(cli.NewAddCmd()) cmd.AddCommand(cli.newAddCmd())
cmd.AddCommand(cli.NewDeleteCmd()) cmd.AddCommand(cli.newDeleteCmd())
cmd.AddCommand(cli.NewImportCmd()) cmd.AddCommand(cli.newImportCmd())
return cmd return cmd
} }
func (cli cliDecisions) NewListCmd() *cobra.Command { func (cli *cliDecisions) newListCmd() *cobra.Command {
var filter = apiclient.AlertsListOpts{ var filter = apiclient.AlertsListOpts{
ValueEquals: new(string), ValueEquals: new(string),
ScopeEquals: new(string), ScopeEquals: new(string),
@ -186,7 +196,7 @@ func (cli cliDecisions) NewListCmd() *cobra.Command {
Example: `cscli decisions list -i 1.2.3.4 Example: `cscli decisions list -i 1.2.3.4
cscli decisions list -r 1.2.3.0/24 cscli decisions list -r 1.2.3.0/24
cscli decisions list -s crowdsecurity/ssh-bf cscli decisions list -s crowdsecurity/ssh-bf
cscli decisions list -t ban cscli decisions list --origin lists --scenario list_name
`, `,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
@ -262,7 +272,7 @@ cscli decisions list -t ban
return fmt.Errorf("unable to retrieve decisions: %w", err) return fmt.Errorf("unable to retrieve decisions: %w", err)
} }
err = DecisionsToTable(alerts, printMachine) err = cli.decisionsToTable(alerts, printMachine)
if err != nil { if err != nil {
return fmt.Errorf("unable to print decisions: %w", err) return fmt.Errorf("unable to print decisions: %w", err)
} }
@ -289,7 +299,7 @@ cscli decisions list -t ban
return cmd return cmd
} }
func (cli cliDecisions) NewAddCmd() *cobra.Command { func (cli *cliDecisions) newAddCmd() *cobra.Command {
var ( var (
addIP string addIP string
addRange string addRange string
@ -325,7 +335,7 @@ cscli decisions add --scope username --value foobar
createdAt := time.Now().UTC().Format(time.RFC3339) createdAt := time.Now().UTC().Format(time.RFC3339)
/*take care of shorthand options*/ /*take care of shorthand options*/
if err := manageCliDecisionAlerts(&addIP, &addRange, &addScope, &addValue); err != nil { if err = manageCliDecisionAlerts(&addIP, &addRange, &addScope, &addValue); err != nil {
return err return err
} }
@ -337,11 +347,11 @@ cscli decisions add --scope username --value foobar
addScope = types.Range addScope = types.Range
} else if addValue == "" { } else if addValue == "" {
printHelp(cmd) printHelp(cmd)
return fmt.Errorf("missing arguments, a value is required (--ip, --range or --scope and --value)") return errors.New("missing arguments, a value is required (--ip, --range or --scope and --value)")
} }
if addReason == "" { if addReason == "" {
addReason = fmt.Sprintf("manual '%s' from '%s'", addType, csConfig.API.Client.Credentials.Login) addReason = fmt.Sprintf("manual '%s' from '%s'", addType, cli.cfg().API.Client.Credentials.Login)
} }
decision := models.Decision{ decision := models.Decision{
Duration: &addDuration, Duration: &addDuration,
@ -362,7 +372,7 @@ cscli decisions add --scope username --value foobar
Scenario: &addReason, Scenario: &addReason,
ScenarioVersion: &empty, ScenarioVersion: &empty,
Simulated: &simulated, Simulated: &simulated,
//setting empty scope/value broke plugins, and it didn't seem to be needed anymore w/ latest papi changes // setting empty scope/value broke plugins, and it didn't seem to be needed anymore w/ latest papi changes
Source: &models.Source{ Source: &models.Source{
AsName: empty, AsName: empty,
AsNumber: empty, AsNumber: empty,
@ -384,6 +394,7 @@ cscli decisions add --scope username --value foobar
} }
log.Info("Decision successfully added") log.Info("Decision successfully added")
return nil return nil
}, },
} }
@ -400,8 +411,8 @@ cscli decisions add --scope username --value foobar
return cmd return cmd
} }
func (cli cliDecisions) NewDeleteCmd() *cobra.Command { func (cli *cliDecisions) newDeleteCmd() *cobra.Command {
var delFilter = apiclient.DecisionsDeleteOpts{ delFilter := apiclient.DecisionsDeleteOpts{
ScopeEquals: new(string), ScopeEquals: new(string),
ValueEquals: new(string), ValueEquals: new(string),
TypeEquals: new(string), TypeEquals: new(string),
@ -426,6 +437,7 @@ func (cli cliDecisions) NewDeleteCmd() *cobra.Command {
cscli decisions delete -i 1.2.3.4 cscli decisions delete -i 1.2.3.4
cscli decisions delete --id 42 cscli decisions delete --id 42
cscli decisions delete --type captcha cscli decisions delete --type captcha
cscli decisions delete --origin lists --scenario list_name
`, `,
/*TBD : refaire le Long/Example*/ /*TBD : refaire le Long/Example*/
PreRunE: func(cmd *cobra.Command, _ []string) error { PreRunE: func(cmd *cobra.Command, _ []string) error {
@ -437,7 +449,7 @@ cscli decisions delete --type captcha
*delFilter.RangeEquals == "" && *delFilter.ScenarioEquals == "" && *delFilter.RangeEquals == "" && *delFilter.ScenarioEquals == "" &&
*delFilter.OriginEquals == "" && delDecisionID == "" { *delFilter.OriginEquals == "" && delDecisionID == "" {
cmd.Usage() cmd.Usage()
return fmt.Errorf("at least one filter or --all must be specified") return errors.New("at least one filter or --all must be specified")
} }
return nil return nil
@ -490,6 +502,7 @@ cscli decisions delete --type captcha
} }
} }
log.Infof("%s decision(s) deleted", decisions.NbDeleted) log.Infof("%s decision(s) deleted", decisions.NbDeleted)
return nil return nil
}, },
} }

View file

@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -67,7 +68,7 @@ func parseDecisionList(content []byte, format string) ([]decisionRaw, error) {
} }
func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error { func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error {
flags := cmd.Flags() flags := cmd.Flags()
input, err := flags.GetString("input") input, err := flags.GetString("input")
@ -81,7 +82,7 @@ func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error {
} }
if defaultDuration == "" { if defaultDuration == "" {
return fmt.Errorf("--duration cannot be empty") return errors.New("--duration cannot be empty")
} }
defaultScope, err := flags.GetString("scope") defaultScope, err := flags.GetString("scope")
@ -90,7 +91,7 @@ func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error {
} }
if defaultScope == "" { if defaultScope == "" {
return fmt.Errorf("--scope cannot be empty") return errors.New("--scope cannot be empty")
} }
defaultReason, err := flags.GetString("reason") defaultReason, err := flags.GetString("reason")
@ -99,7 +100,7 @@ func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error {
} }
if defaultReason == "" { if defaultReason == "" {
return fmt.Errorf("--reason cannot be empty") return errors.New("--reason cannot be empty")
} }
defaultType, err := flags.GetString("type") defaultType, err := flags.GetString("type")
@ -108,7 +109,7 @@ func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error {
} }
if defaultType == "" { if defaultType == "" {
return fmt.Errorf("--type cannot be empty") return errors.New("--type cannot be empty")
} }
batchSize, err := flags.GetInt("batch") batchSize, err := flags.GetInt("batch")
@ -136,7 +137,7 @@ func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error {
} }
if format == "" { if format == "" {
return fmt.Errorf("unable to guess format from file extension, please provide a format with --format flag") return errors.New("unable to guess format from file extension, please provide a format with --format flag")
} }
if input == "-" { if input == "-" {
@ -235,14 +236,14 @@ func (cli cliDecisions) runImport(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli *cliDecisions) newImportCmd() *cobra.Command {
func (cli cliDecisions) NewImportCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "import [options]", Use: "import [options]",
Short: "Import decisions from a file or pipe", Short: "Import decisions from a file or pipe",
Long: "expected format:\n" + Long: "expected format:\n" +
"csv : any of duration,reason,scope,type,value, with a header line\n" + "csv : any of duration,reason,scope,type,value, with a header line\n" +
"json :" + "`{" + `"duration" : "24h", "reason" : "my_scenario", "scope" : "ip", "type" : "ban", "value" : "x.y.z.z"` + "}`", "json :" + "`{" + `"duration" : "24h", "reason" : "my_scenario", "scope" : "ip", "type" : "ban", "value" : "x.y.z.z"` + "}`",
Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Example: `decisions.csv: Example: `decisions.csv:
duration,scope,value duration,scope,value

View file

@ -8,13 +8,15 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
) )
func decisionsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachine bool) { func (cli *cliDecisions) decisionsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachine bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
header := []string{"ID", "Source", "Scope:Value", "Reason", "Action", "Country", "AS", "Events", "expiration", "Alert ID"} header := []string{"ID", "Source", "Scope:Value", "Reason", "Action", "Country", "AS", "Events", "expiration", "Alert ID"}
if printMachine { if printMachine {
header = append(header, "Machine") header = append(header, "Machine")
} }
t.SetHeaders(header...) t.SetHeaders(header...)
for _, alertItem := range *alerts { for _, alertItem := range *alerts {
@ -22,6 +24,7 @@ func decisionsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachin
if *alertItem.Simulated { if *alertItem.Simulated {
*decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type) *decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type)
} }
row := []string{ row := []string{
strconv.Itoa(int(decisionItem.ID)), strconv.Itoa(int(decisionItem.ID)),
*decisionItem.Origin, *decisionItem.Origin,
@ -42,5 +45,6 @@ func decisionsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachin
t.AddRow(row...) t.AddRow(row...)
} }
} }
t.Render() t.Render()
} }

View file

@ -39,8 +39,10 @@ id: %s
title: %s title: %s
--- ---
` `
name := filepath.Base(filename) name := filepath.Base(filename)
base := strings.TrimSuffix(name, filepath.Ext(name)) base := strings.TrimSuffix(name, filepath.Ext(name))
return fmt.Sprintf(header, base, strings.ReplaceAll(base, "_", " ")) return fmt.Sprintf(header, base, strings.ReplaceAll(base, "_", " "))
} }

View file

@ -16,33 +16,53 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/hubtest" "github.com/crowdsecurity/crowdsec/pkg/hubtest"
) )
func GetLineCountForFile(filepath string) (int, error) { func getLineCountForFile(filepath string) (int, error) {
f, err := os.Open(filepath) f, err := os.Open(filepath)
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer f.Close() defer f.Close()
lc := 0 lc := 0
fs := bufio.NewReader(f) fs := bufio.NewReader(f)
for { for {
input, err := fs.ReadBytes('\n') input, err := fs.ReadBytes('\n')
if len(input) > 1 { if len(input) > 1 {
lc++ lc++
} }
if err != nil && err == io.EOF { if err != nil && err == io.EOF {
break break
} }
} }
return lc, nil return lc, nil
} }
type cliExplain struct{} type cliExplain struct {
cfg configGetter
func NewCLIExplain() *cliExplain { flags struct {
return &cliExplain{} logFile string
dsn string
logLine string
logType string
details bool
skipOk bool
onlySuccessfulParsers bool
noClean bool
crowdsec string
labels string
}
} }
func (cli cliExplain) NewCommand() *cobra.Command { func NewCLIExplain(cfg configGetter) *cliExplain {
return &cliExplain{
cfg: cfg,
}
}
func (cli *cliExplain) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "explain", Use: "explain",
Short: "Explain log pipeline", Short: "Explain log pipeline",
@ -57,118 +77,50 @@ tail -n 5 myfile.log | cscli explain --type nginx -f -
`, `,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.run, RunE: func(_ *cobra.Command, _ []string) error {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return cli.run()
flags := cmd.Flags() },
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
logFile, err := flags.GetString("file")
if err != nil {
return err
}
dsn, err := flags.GetString("dsn")
if err != nil {
return err
}
logLine, err := flags.GetString("log")
if err != nil {
return err
}
logType, err := flags.GetString("type")
if err != nil {
return err
}
if logLine == "" && logFile == "" && dsn == "" {
printHelp(cmd)
fmt.Println()
return fmt.Errorf("please provide --log, --file or --dsn flag")
}
if logType == "" {
printHelp(cmd)
fmt.Println()
return fmt.Errorf("please provide --type flag")
}
fileInfo, _ := os.Stdin.Stat() fileInfo, _ := os.Stdin.Stat()
if logFile == "-" && ((fileInfo.Mode() & os.ModeCharDevice) == os.ModeCharDevice) { if cli.flags.logFile == "-" && ((fileInfo.Mode() & os.ModeCharDevice) == os.ModeCharDevice) {
return fmt.Errorf("the option -f - is intended to work with pipes") return errors.New("the option -f - is intended to work with pipes")
} }
return nil return nil
}, },
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringP("file", "f", "", "Log file to test") flags.StringVarP(&cli.flags.logFile, "file", "f", "", "Log file to test")
flags.StringP("dsn", "d", "", "DSN to test") flags.StringVarP(&cli.flags.dsn, "dsn", "d", "", "DSN to test")
flags.StringP("log", "l", "", "Log line to test") flags.StringVarP(&cli.flags.logLine, "log", "l", "", "Log line to test")
flags.StringP("type", "t", "", "Type of the acquisition to test") flags.StringVarP(&cli.flags.logType, "type", "t", "", "Type of the acquisition to test")
flags.String("labels", "", "Additional labels to add to the acquisition format (key:value,key2:value2)") flags.StringVar(&cli.flags.labels, "labels", "", "Additional labels to add to the acquisition format (key:value,key2:value2)")
flags.BoolP("verbose", "v", false, "Display individual changes") flags.BoolVarP(&cli.flags.details, "verbose", "v", false, "Display individual changes")
flags.Bool("failures", false, "Only show failed lines") flags.BoolVar(&cli.flags.skipOk, "failures", false, "Only show failed lines")
flags.Bool("only-successful-parsers", false, "Only show successful parsers") flags.BoolVar(&cli.flags.onlySuccessfulParsers, "only-successful-parsers", false, "Only show successful parsers")
flags.String("crowdsec", "crowdsec", "Path to crowdsec") flags.StringVar(&cli.flags.crowdsec, "crowdsec", "crowdsec", "Path to crowdsec")
flags.Bool("no-clean", false, "Don't clean runtime environment after tests") flags.BoolVar(&cli.flags.noClean, "no-clean", false, "Don't clean runtime environment after tests")
cmd.MarkFlagRequired("type")
cmd.MarkFlagsOneRequired("log", "file", "dsn")
return cmd return cmd
} }
func (cli cliExplain) run(cmd *cobra.Command, args []string) error { func (cli *cliExplain) run() error {
flags := cmd.Flags() logFile := cli.flags.logFile
logLine := cli.flags.logLine
logType := cli.flags.logType
dsn := cli.flags.dsn
labels := cli.flags.labels
crowdsec := cli.flags.crowdsec
logFile, err := flags.GetString("file") opts := dumps.DumpOpts{
if err != nil { Details: cli.flags.details,
return err SkipOk: cli.flags.skipOk,
} ShowNotOkParsers: !cli.flags.onlySuccessfulParsers,
dsn, err := flags.GetString("dsn")
if err != nil {
return err
}
logLine, err := flags.GetString("log")
if err != nil {
return err
}
logType, err := flags.GetString("type")
if err != nil {
return err
}
opts := dumps.DumpOpts{}
opts.Details, err = flags.GetBool("verbose")
if err != nil {
return err
}
no_clean, err := flags.GetBool("no-clean")
if err != nil {
return err
}
opts.SkipOk, err = flags.GetBool("failures")
if err != nil {
return err
}
opts.ShowNotOkParsers, err = flags.GetBool("only-successful-parsers")
opts.ShowNotOkParsers = !opts.ShowNotOkParsers
if err != nil {
return err
}
crowdsec, err := flags.GetString("crowdsec")
if err != nil {
return err
}
labels, err := flags.GetString("labels")
if err != nil {
return err
} }
var f *os.File var f *os.File
@ -176,22 +128,25 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
// using empty string fallback to /tmp // using empty string fallback to /tmp
dir, err := os.MkdirTemp("", "cscli_explain") dir, err := os.MkdirTemp("", "cscli_explain")
if err != nil { if err != nil {
return fmt.Errorf("couldn't create a temporary directory to store cscli explain result: %s", err) return fmt.Errorf("couldn't create a temporary directory to store cscli explain result: %w", err)
} }
defer func() { defer func() {
if no_clean { if cli.flags.noClean {
return return
} }
if _, err := os.Stat(dir); !os.IsNotExist(err) { if _, err := os.Stat(dir); !os.IsNotExist(err) {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
log.Errorf("unable to delete temporary directory '%s': %s", dir, err) log.Errorf("unable to delete temporary directory '%s': %s", dir, err)
} }
} }
}() }()
tmpFile := ""
// we create a temporary log file if a log line/stdin has been provided // we create a temporary log file if a log line/stdin has been provided
if logLine != "" || logFile == "-" { if logLine != "" || logFile == "-" {
tmpFile = filepath.Join(dir, "cscli_test_tmp.log") tmpFile := filepath.Join(dir, "cscli_test_tmp.log")
f, err = os.Create(tmpFile) f, err = os.Create(tmpFile)
if err != nil { if err != nil {
return err return err
@ -205,22 +160,27 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
} else if logFile == "-" { } else if logFile == "-" {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
errCount := 0 errCount := 0
for { for {
input, err := reader.ReadBytes('\n') input, err := reader.ReadBytes('\n')
if err != nil && errors.Is(err, io.EOF) { if err != nil && errors.Is(err, io.EOF) {
break break
} }
if len(input) > 1 { if len(input) > 1 {
_, err = f.Write(input) _, err = f.Write(input)
} }
if err != nil || len(input) <= 1 { if err != nil || len(input) <= 1 {
errCount++ errCount++
} }
} }
if errCount > 0 { if errCount > 0 {
log.Warnf("Failed to write %d lines to %s", errCount, tmpFile) log.Warnf("Failed to write %d lines to %s", errCount, tmpFile)
} }
} }
f.Close() f.Close()
// this is the file that was going to be read by crowdsec anyway // this is the file that was going to be read by crowdsec anyway
logFile = tmpFile logFile = tmpFile
@ -231,34 +191,43 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return fmt.Errorf("unable to get absolute path of '%s', exiting", logFile) return fmt.Errorf("unable to get absolute path of '%s', exiting", logFile)
} }
dsn = fmt.Sprintf("file://%s", absolutePath) dsn = fmt.Sprintf("file://%s", absolutePath)
lineCount, err := GetLineCountForFile(absolutePath)
lineCount, err := getLineCountForFile(absolutePath)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("file %s has %d lines", absolutePath, lineCount) log.Debugf("file %s has %d lines", absolutePath, lineCount)
if lineCount == 0 { if lineCount == 0 {
return fmt.Errorf("the log file is empty: %s", absolutePath) return fmt.Errorf("the log file is empty: %s", absolutePath)
} }
if lineCount > 100 { if lineCount > 100 {
log.Warnf("%s contains %d lines. This may take a lot of resources.", absolutePath, lineCount) log.Warnf("%s contains %d lines. This may take a lot of resources.", absolutePath, lineCount)
} }
} }
if dsn == "" { if dsn == "" {
return fmt.Errorf("no acquisition (--file or --dsn) provided, can't run cscli test") return errors.New("no acquisition (--file or --dsn) provided, can't run cscli test")
} }
cmdArgs := []string{"-c", ConfigFilePath, "-type", logType, "-dsn", dsn, "-dump-data", dir, "-no-api"} cmdArgs := []string{"-c", ConfigFilePath, "-type", logType, "-dsn", dsn, "-dump-data", dir, "-no-api"}
if labels != "" { if labels != "" {
log.Debugf("adding labels %s", labels) log.Debugf("adding labels %s", labels)
cmdArgs = append(cmdArgs, "-label", labels) cmdArgs = append(cmdArgs, "-label", labels)
} }
crowdsecCmd := exec.Command(crowdsec, cmdArgs...) crowdsecCmd := exec.Command(crowdsec, cmdArgs...)
output, err := crowdsecCmd.CombinedOutput() output, err := crowdsecCmd.CombinedOutput()
if err != nil { if err != nil {
fmt.Println(string(output)) fmt.Println(string(output))
return fmt.Errorf("fail to run crowdsec for test: %v", err)
return fmt.Errorf("fail to run crowdsec for test: %w", err)
} }
parserDumpFile := filepath.Join(dir, hubtest.ParserResultFileName) parserDumpFile := filepath.Join(dir, hubtest.ParserResultFileName)
@ -266,12 +235,12 @@ func (cli cliExplain) run(cmd *cobra.Command, args []string) error {
parserDump, err := dumps.LoadParserDump(parserDumpFile) parserDump, err := dumps.LoadParserDump(parserDumpFile)
if err != nil { if err != nil {
return fmt.Errorf("unable to load parser dump result: %s", err) return fmt.Errorf("unable to load parser dump result: %w", err)
} }
bucketStateDump, err := dumps.LoadBucketPourDump(bucketStateDumpFile) bucketStateDump, err := dumps.LoadBucketPourDump(bucketStateDumpFile)
if err != nil { if err != nil {
return fmt.Errorf("unable to load bucket dump result: %s", err) return fmt.Errorf("unable to load bucket dump result: %w", err)
} }
dumps.DumpTree(*parserDump, *bucketStateDump, opts) dumps.DumpTree(*parserDump, *bucketStateDump, opts)

29
cmd/crowdsec-cli/flag.go Normal file
View file

@ -0,0 +1,29 @@
package main
// Custom types for flag validation and conversion.
import (
"errors"
)
type MachinePassword string
func (p *MachinePassword) String() string {
return string(*p)
}
func (p *MachinePassword) Set(v string) error {
// a password can't be more than 72 characters
// due to bcrypt limitations
if len(v) > 72 {
return errors.New("password too long (max 72 characters)")
}
*p = MachinePassword(v)
return nil
}
func (p *MachinePassword) Type() string {
return "string"
}

View file

@ -13,13 +13,17 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
type cliHub struct{} type cliHub struct{
cfg configGetter
func NewCLIHub() *cliHub {
return &cliHub{}
} }
func (cli cliHub) NewCommand() *cobra.Command { func NewCLIHub(cfg configGetter) *cliHub {
return &cliHub{
cfg: cfg,
}
}
func (cli *cliHub) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "hub [action]", Use: "hub [action]",
Short: "Manage hub index", Short: "Manage hub index",
@ -34,23 +38,16 @@ cscli hub upgrade`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
} }
cmd.AddCommand(cli.NewListCmd()) cmd.AddCommand(cli.newListCmd())
cmd.AddCommand(cli.NewUpdateCmd()) cmd.AddCommand(cli.newUpdateCmd())
cmd.AddCommand(cli.NewUpgradeCmd()) cmd.AddCommand(cli.newUpgradeCmd())
cmd.AddCommand(cli.NewTypesCmd()) cmd.AddCommand(cli.newTypesCmd())
return cmd return cmd
} }
func (cli cliHub) list(cmd *cobra.Command, args []string) error { func (cli *cliHub) list(all bool) error {
flags := cmd.Flags() hub, err := require.Hub(cli.cfg(), nil, log.StandardLogger())
all, err := flags.GetBool("all")
if err != nil {
return err
}
hub, err := require.Hub(csConfig, nil, log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
@ -80,24 +77,28 @@ func (cli cliHub) list(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliHub) NewListCmd() *cobra.Command { func (cli *cliHub) newListCmd() *cobra.Command {
var all bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list [-a]", Use: "list [-a]",
Short: "List all installed configurations", Short: "List all installed configurations",
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.list, RunE: func(_ *cobra.Command, _ []string) error {
return cli.list(all)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolP("all", "a", false, "List disabled items as well") flags.BoolVarP(&all, "all", "a", false, "List disabled items as well")
return cmd return cmd
} }
func (cli cliHub) update(cmd *cobra.Command, args []string) error { func (cli *cliHub) update() error {
local := csConfig.Hub local := cli.cfg().Hub
remote := require.RemoteHub(csConfig) remote := require.RemoteHub(cli.cfg())
// don't use require.Hub because if there is no index file, it would fail // don't use require.Hub because if there is no index file, it would fail
hub, err := cwhub.NewHub(local, remote, true, log.StandardLogger()) hub, err := cwhub.NewHub(local, remote, true, log.StandardLogger())
@ -112,7 +113,7 @@ func (cli cliHub) update(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliHub) NewUpdateCmd() *cobra.Command { func (cli *cliHub) newUpdateCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update", Use: "update",
Short: "Download the latest index (catalog of available configurations)", Short: "Download the latest index (catalog of available configurations)",
@ -121,27 +122,22 @@ Fetches the .index.json file from the hub, containing the list of available conf
`, `,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.update, RunE: func(_ *cobra.Command, _ []string) error {
return cli.update()
},
} }
return cmd return cmd
} }
func (cli cliHub) upgrade(cmd *cobra.Command, args []string) error { func (cli *cliHub) upgrade(force bool) error {
flags := cmd.Flags() hub, err := require.Hub(cli.cfg(), require.RemoteHub(cli.cfg()), log.StandardLogger())
force, err := flags.GetBool("force")
if err != nil {
return err
}
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
for _, itemType := range cwhub.ItemTypes { for _, itemType := range cwhub.ItemTypes {
items, err := hub.GetInstalledItems(itemType) items, err := hub.GetInstalledItemsByType(itemType)
if err != nil { if err != nil {
return err return err
} }
@ -167,7 +163,9 @@ func (cli cliHub) upgrade(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliHub) NewUpgradeCmd() *cobra.Command { func (cli *cliHub) newUpgradeCmd() *cobra.Command {
var force bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "upgrade", Use: "upgrade",
Short: "Upgrade all configurations to their latest version", Short: "Upgrade all configurations to their latest version",
@ -176,17 +174,19 @@ Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if
`, `,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.upgrade, RunE: func(_ *cobra.Command, _ []string) error {
return cli.upgrade(force)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files") flags.BoolVar(&force, "force", false, "Force upgrade: overwrite tainted and outdated files")
return cmd return cmd
} }
func (cli cliHub) types(cmd *cobra.Command, args []string) error { func (cli *cliHub) types() error {
switch csConfig.Cscli.Output { switch cli.cfg().Cscli.Output {
case "human": case "human":
s, err := yaml.Marshal(cwhub.ItemTypes) s, err := yaml.Marshal(cwhub.ItemTypes)
if err != nil { if err != nil {
@ -210,7 +210,7 @@ func (cli cliHub) types(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliHub) NewTypesCmd() *cobra.Command { func (cli *cliHub) newTypesCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "types", Use: "types",
Short: "List supported item types", Short: "List supported item types",
@ -219,7 +219,9 @@ List the types of supported hub items.
`, `,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.types, RunE: func(_ *cobra.Command, _ []string) error {
return cli.types()
},
} }
return cmd return cmd

View file

@ -13,8 +13,9 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func NewCLIAppsecConfig() *cliItem { func NewCLIAppsecConfig(cfg configGetter) *cliItem {
return &cliItem{ return &cliItem{
cfg: cfg,
name: cwhub.APPSEC_CONFIGS, name: cwhub.APPSEC_CONFIGS,
singular: "appsec-config", singular: "appsec-config",
oneOrMore: "appsec-config(s)", oneOrMore: "appsec-config(s)",
@ -46,7 +47,7 @@ cscli appsec-configs list crowdsecurity/vpatch`,
} }
} }
func NewCLIAppsecRule() *cliItem { func NewCLIAppsecRule(cfg configGetter) *cliItem {
inspectDetail := func(item *cwhub.Item) error { inspectDetail := func(item *cwhub.Item) error {
// Only show the converted rules in human mode // Only show the converted rules in human mode
if csConfig.Cscli.Output != "human" { if csConfig.Cscli.Output != "human" {
@ -57,11 +58,11 @@ func NewCLIAppsecRule() *cliItem {
yamlContent, err := os.ReadFile(item.State.LocalPath) yamlContent, err := os.ReadFile(item.State.LocalPath)
if err != nil { if err != nil {
return fmt.Errorf("unable to read file %s : %s", item.State.LocalPath, err) return fmt.Errorf("unable to read file %s: %w", item.State.LocalPath, err)
} }
if err := yaml.Unmarshal(yamlContent, &appsecRule); err != nil { if err := yaml.Unmarshal(yamlContent, &appsecRule); err != nil {
return fmt.Errorf("unable to unmarshal yaml file %s : %s", item.State.LocalPath, err) return fmt.Errorf("unable to unmarshal yaml file %s: %w", item.State.LocalPath, err)
} }
for _, ruleType := range appsec_rule.SupportedTypes() { for _, ruleType := range appsec_rule.SupportedTypes() {
@ -70,7 +71,7 @@ func NewCLIAppsecRule() *cliItem {
for _, rule := range appsecRule.Rules { for _, rule := range appsecRule.Rules {
convertedRule, _, err := rule.Convert(ruleType, appsecRule.Name) convertedRule, _, err := rule.Convert(ruleType, appsecRule.Name)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert rule %s : %s", rule.Name, err) return fmt.Errorf("unable to convert rule %s: %w", rule.Name, err)
} }
fmt.Println(convertedRule) fmt.Println(convertedRule)
@ -88,6 +89,7 @@ func NewCLIAppsecRule() *cliItem {
} }
return &cliItem{ return &cliItem{
cfg: cfg,
name: "appsec-rules", name: "appsec-rules",
singular: "appsec-rule", singular: "appsec-rule",
oneOrMore: "appsec-rule(s)", oneOrMore: "appsec-rule(s)",

View file

@ -4,8 +4,9 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func NewCLICollection() *cliItem { func NewCLICollection(cfg configGetter) *cliItem {
return &cliItem{ return &cliItem{
cfg: cfg,
name: cwhub.COLLECTIONS, name: cwhub.COLLECTIONS,
singular: "collection", singular: "collection",
oneOrMore: "collection(s)", oneOrMore: "collection(s)",

View file

@ -4,8 +4,9 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func NewCLIContext() *cliItem { func NewCLIContext(cfg configGetter) *cliItem {
return &cliItem{ return &cliItem{
cfg: cfg,
name: cwhub.CONTEXTS, name: cwhub.CONTEXTS,
singular: "context", singular: "context",
oneOrMore: "context(s)", oneOrMore: "context(s)",

View file

@ -4,8 +4,9 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func NewCLIParser() *cliItem { func NewCLIParser(cfg configGetter) *cliItem {
return &cliItem{ return &cliItem{
cfg: cfg,
name: cwhub.PARSERS, name: cwhub.PARSERS,
singular: "parser", singular: "parser",
oneOrMore: "parser(s)", oneOrMore: "parser(s)",

View file

@ -4,8 +4,9 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func NewCLIPostOverflow() *cliItem { func NewCLIPostOverflow(cfg configGetter) *cliItem {
return &cliItem{ return &cliItem{
cfg: cfg,
name: cwhub.POSTOVERFLOWS, name: cwhub.POSTOVERFLOWS,
singular: "postoverflow", singular: "postoverflow",
oneOrMore: "postoverflow(s)", oneOrMore: "postoverflow(s)",

View file

@ -4,8 +4,9 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
func NewCLIScenario() *cliItem { func NewCLIScenario(cfg configGetter) *cliItem {
return &cliItem{ return &cliItem{
cfg: cfg,
name: cwhub.SCENARIOS, name: cwhub.SCENARIOS,
singular: "scenario", singular: "scenario",
oneOrMore: "scenario(s)", oneOrMore: "scenario(s)",

View file

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"os" "os"
@ -10,31 +11,39 @@ import (
"text/template" "text/template"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/enescakir/emoji"
"github.com/fatih/color" "github.com/fatih/color"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/pkg/dumps" "github.com/crowdsecurity/crowdsec/pkg/dumps"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
"github.com/crowdsecurity/crowdsec/pkg/hubtest" "github.com/crowdsecurity/crowdsec/pkg/hubtest"
) )
var HubTest hubtest.HubTest var (
var HubAppsecTests hubtest.HubTest HubTest hubtest.HubTest
var hubPtr *hubtest.HubTest HubAppsecTests hubtest.HubTest
var isAppsecTest bool hubPtr *hubtest.HubTest
isAppsecTest bool
)
type cliHubTest struct{} type cliHubTest struct {
cfg configGetter
func NewCLIHubTest() *cliHubTest {
return &cliHubTest{}
} }
func (cli cliHubTest) NewCommand() *cobra.Command { func NewCLIHubTest(cfg configGetter) *cliHubTest {
var hubPath string return &cliHubTest{
var crowdsecPath string cfg: cfg,
var cscliPath string }
}
func (cli *cliHubTest) NewCommand() *cobra.Command {
var (
hubPath string
crowdsecPath string
cscliPath string
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "hubtest", Use: "hubtest",
@ -53,11 +62,13 @@ func (cli cliHubTest) NewCommand() *cobra.Command {
if err != nil { if err != nil {
return fmt.Errorf("unable to load appsec specific hubtest: %+v", err) return fmt.Errorf("unable to load appsec specific hubtest: %+v", err)
} }
/*commands will use the hubPtr, will point to the default hubTest object, or the one dedicated to appsec tests*/
// commands will use the hubPtr, will point to the default hubTest object, or the one dedicated to appsec tests
hubPtr = &HubTest hubPtr = &HubTest
if isAppsecTest { if isAppsecTest {
hubPtr = &HubAppsecTests hubPtr = &HubAppsecTests
} }
return nil return nil
}, },
} }
@ -79,13 +90,16 @@ func (cli cliHubTest) NewCommand() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewCreateCmd() *cobra.Command { func (cli *cliHubTest) NewCreateCmd() *cobra.Command {
var (
ignoreParsers bool
labels map[string]string
logType string
)
parsers := []string{} parsers := []string{}
postoverflows := []string{} postoverflows := []string{}
scenarios := []string{} scenarios := []string{}
var ignoreParsers bool
var labels map[string]string
var logType string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create", Use: "create",
@ -107,7 +121,7 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
} }
if logType == "" { if logType == "" {
return fmt.Errorf("please provide a type (--type) for the test") return errors.New("please provide a type (--type) for the test")
} }
if err := os.MkdirAll(testPath, os.ModePerm); err != nil { if err := os.MkdirAll(testPath, os.ModePerm); err != nil {
@ -118,17 +132,18 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
configFileData := &hubtest.HubTestItemConfig{} configFileData := &hubtest.HubTestItemConfig{}
if logType == "appsec" { if logType == "appsec" {
//create empty nuclei template file // create empty nuclei template file
nucleiFileName := fmt.Sprintf("%s.yaml", testName) nucleiFileName := fmt.Sprintf("%s.yaml", testName)
nucleiFilePath := filepath.Join(testPath, nucleiFileName) nucleiFilePath := filepath.Join(testPath, nucleiFileName)
nucleiFile, err := os.OpenFile(nucleiFilePath, os.O_RDWR|os.O_CREATE, 0755)
nucleiFile, err := os.OpenFile(nucleiFilePath, os.O_RDWR|os.O_CREATE, 0o755)
if err != nil { if err != nil {
return err return err
} }
ntpl := template.Must(template.New("nuclei").Parse(hubtest.TemplateNucleiFile)) ntpl := template.Must(template.New("nuclei").Parse(hubtest.TemplateNucleiFile))
if ntpl == nil { if ntpl == nil {
return fmt.Errorf("unable to parse nuclei template") return errors.New("unable to parse nuclei template")
} }
ntpl.ExecuteTemplate(nucleiFile, "nuclei", struct{ TestName string }{TestName: testName}) ntpl.ExecuteTemplate(nucleiFile, "nuclei", struct{ TestName string }{TestName: testName})
nucleiFile.Close() nucleiFile.Close()
@ -188,24 +203,24 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath) fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath)
fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath) fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath)
fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath) fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath)
} }
fd, err := os.Create(configFilePath) fd, err := os.Create(configFilePath)
if err != nil { if err != nil {
return fmt.Errorf("open: %s", err) return fmt.Errorf("open: %w", err)
} }
data, err := yaml.Marshal(configFileData) data, err := yaml.Marshal(configFileData)
if err != nil { if err != nil {
return fmt.Errorf("marshal: %s", err) return fmt.Errorf("marshal: %w", err)
} }
_, err = fd.Write(data) _, err = fd.Write(data)
if err != nil { if err != nil {
return fmt.Errorf("write: %s", err) return fmt.Errorf("write: %w", err)
} }
if err := fd.Close(); err != nil { if err := fd.Close(); err != nil {
return fmt.Errorf("close: %s", err) return fmt.Errorf("close: %w", err)
} }
return nil return nil
}, },
} }
@ -219,20 +234,25 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
return cmd return cmd
} }
func (cli cliHubTest) NewRunCmd() *cobra.Command { func (cli *cliHubTest) NewRunCmd() *cobra.Command {
var noClean bool var (
var runAll bool noClean bool
var forceClean bool runAll bool
var NucleiTargetHost string forceClean bool
var AppSecHost string NucleiTargetHost string
var cmd = &cobra.Command{ AppSecHost string
)
cmd := &cobra.Command{
Use: "run", Use: "run",
Short: "run [test_name]", Short: "run [test_name]",
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cfg := cli.cfg()
if !runAll && len(args) == 0 { if !runAll && len(args) == 0 {
printHelp(cmd) printHelp(cmd)
return fmt.Errorf("please provide test to run or --all flag") return errors.New("please provide test to run or --all flag")
} }
hubPtr.NucleiTargetHost = NucleiTargetHost hubPtr.NucleiTargetHost = NucleiTargetHost
hubPtr.AppSecHost = AppSecHost hubPtr.AppSecHost = AppSecHost
@ -244,7 +264,7 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
for _, testName := range args { for _, testName := range args {
_, err := hubPtr.LoadTestItem(testName) _, err := hubPtr.LoadTestItem(testName)
if err != nil { if err != nil {
return fmt.Errorf("unable to load test '%s': %s", testName, err) return fmt.Errorf("unable to load test '%s': %w", testName, err)
} }
} }
} }
@ -252,7 +272,7 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
// set timezone to avoid DST issues // set timezone to avoid DST issues
os.Setenv("TZ", "UTC") os.Setenv("TZ", "UTC")
for _, test := range hubPtr.Tests { for _, test := range hubPtr.Tests {
if csConfig.Cscli.Output == "human" { if cfg.Cscli.Output == "human" {
log.Infof("Running test '%s'", test.Name) log.Infof("Running test '%s'", test.Name)
} }
err := test.Run() err := test.Run()
@ -264,6 +284,8 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
return nil return nil
}, },
PersistentPostRunE: func(_ *cobra.Command, _ []string) error { PersistentPostRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
success := true success := true
testResult := make(map[string]bool) testResult := make(map[string]bool)
for _, test := range hubPtr.Tests { for _, test := range hubPtr.Tests {
@ -280,7 +302,7 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
} }
if !noClean { if !noClean {
if err := test.Clean(); err != nil { if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err) return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
} }
} }
fmt.Printf("\nPlease fill your assert file(s) for test '%s', exiting\n", test.Name) fmt.Printf("\nPlease fill your assert file(s) for test '%s', exiting\n", test.Name)
@ -288,18 +310,18 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
} }
testResult[test.Name] = test.Success testResult[test.Name] = test.Success
if test.Success { if test.Success {
if csConfig.Cscli.Output == "human" { if cfg.Cscli.Output == "human" {
log.Infof("Test '%s' passed successfully (%d assertions)\n", test.Name, test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert) log.Infof("Test '%s' passed successfully (%d assertions)\n", test.Name, test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert)
} }
if !noClean { if !noClean {
if err := test.Clean(); err != nil { if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err) return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
} }
} }
} else { } else {
success = false success = false
cleanTestEnv := false cleanTestEnv := false
if csConfig.Cscli.Output == "human" { if cfg.Cscli.Output == "human" {
if len(test.ParserAssert.Fails) > 0 { if len(test.ParserAssert.Fails) > 0 {
fmt.Println() fmt.Println()
log.Errorf("Parser test '%s' failed (%d errors)\n", test.Name, len(test.ParserAssert.Fails)) log.Errorf("Parser test '%s' failed (%d errors)\n", test.Name, len(test.ParserAssert.Fails))
@ -330,20 +352,20 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
Default: true, Default: true,
} }
if err := survey.AskOne(prompt, &cleanTestEnv); err != nil { if err := survey.AskOne(prompt, &cleanTestEnv); err != nil {
return fmt.Errorf("unable to ask to remove runtime folder: %s", err) return fmt.Errorf("unable to ask to remove runtime folder: %w", err)
} }
} }
} }
if cleanTestEnv || forceClean { if cleanTestEnv || forceClean {
if err := test.Clean(); err != nil { if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err) return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
} }
} }
} }
} }
switch csConfig.Cscli.Output { switch cfg.Cscli.Output {
case "human": case "human":
hubTestResultTable(color.Output, testResult) hubTestResultTable(color.Output, testResult)
case "json": case "json":
@ -359,11 +381,11 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
} }
jsonStr, err := json.Marshal(jsonResult) jsonStr, err := json.Marshal(jsonResult)
if err != nil { if err != nil {
return fmt.Errorf("unable to json test result: %s", err) return fmt.Errorf("unable to json test result: %w", err)
} }
fmt.Println(string(jsonStr)) fmt.Println(string(jsonStr))
default: default:
return fmt.Errorf("only human/json output modes are supported") return errors.New("only human/json output modes are supported")
} }
if !success { if !success {
@ -383,8 +405,8 @@ func (cli cliHubTest) NewRunCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewCleanCmd() *cobra.Command { func (cli *cliHubTest) NewCleanCmd() *cobra.Command {
var cmd = &cobra.Command{ cmd := &cobra.Command{
Use: "clean", Use: "clean",
Short: "clean [test_name]", Short: "clean [test_name]",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
@ -393,10 +415,10 @@ func (cli cliHubTest) NewCleanCmd() *cobra.Command {
for _, testName := range args { for _, testName := range args {
test, err := hubPtr.LoadTestItem(testName) test, err := hubPtr.LoadTestItem(testName)
if err != nil { if err != nil {
return fmt.Errorf("unable to load test '%s': %s", testName, err) return fmt.Errorf("unable to load test '%s': %w", testName, err)
} }
if err := test.Clean(); err != nil { if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err) return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
} }
} }
@ -407,7 +429,7 @@ func (cli cliHubTest) NewCleanCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewInfoCmd() *cobra.Command { func (cli *cliHubTest) NewInfoCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "info", Use: "info",
Short: "info [test_name]", Short: "info [test_name]",
@ -417,7 +439,7 @@ func (cli cliHubTest) NewInfoCmd() *cobra.Command {
for _, testName := range args { for _, testName := range args {
test, err := hubPtr.LoadTestItem(testName) test, err := hubPtr.LoadTestItem(testName)
if err != nil { if err != nil {
return fmt.Errorf("unable to load test '%s': %s", testName, err) return fmt.Errorf("unable to load test '%s': %w", testName, err)
} }
fmt.Println() fmt.Println()
fmt.Printf(" Test name : %s\n", test.Name) fmt.Printf(" Test name : %s\n", test.Name)
@ -440,17 +462,19 @@ func (cli cliHubTest) NewInfoCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewListCmd() *cobra.Command { func (cli *cliHubTest) NewListCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list", Short: "list",
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
if err := hubPtr.LoadAllTests(); err != nil { if err := hubPtr.LoadAllTests(); err != nil {
return fmt.Errorf("unable to load all tests: %s", err) return fmt.Errorf("unable to load all tests: %w", err)
} }
switch csConfig.Cscli.Output { switch cfg.Cscli.Output {
case "human": case "human":
hubTestListTable(color.Output, hubPtr.Tests) hubTestListTable(color.Output, hubPtr.Tests)
case "json": case "json":
@ -460,7 +484,7 @@ func (cli cliHubTest) NewListCmd() *cobra.Command {
} }
fmt.Println(string(j)) fmt.Println(string(j))
default: default:
return fmt.Errorf("only human/json output modes are supported") return errors.New("only human/json output modes are supported")
} }
return nil return nil
@ -470,18 +494,22 @@ func (cli cliHubTest) NewListCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewCoverageCmd() *cobra.Command { func (cli *cliHubTest) NewCoverageCmd() *cobra.Command {
var showParserCov bool var (
var showScenarioCov bool showParserCov bool
var showOnlyPercent bool showScenarioCov bool
var showAppsecCov bool showOnlyPercent bool
showAppsecCov bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "coverage", Use: "coverage",
Short: "coverage", Short: "coverage",
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
//for this one we explicitly don't do for appsec cfg := cli.cfg()
// for this one we explicitly don't do for appsec
if err := HubTest.LoadAllTests(); err != nil { if err := HubTest.LoadAllTests(); err != nil {
return fmt.Errorf("unable to load all tests: %+v", err) return fmt.Errorf("unable to load all tests: %+v", err)
} }
@ -499,7 +527,7 @@ func (cli cliHubTest) NewCoverageCmd() *cobra.Command {
if showParserCov || showAll { if showParserCov || showAll {
parserCoverage, err = HubTest.GetParsersCoverage() parserCoverage, err = HubTest.GetParsersCoverage()
if err != nil { if err != nil {
return fmt.Errorf("while getting parser coverage: %s", err) return fmt.Errorf("while getting parser coverage: %w", err)
} }
parserTested := 0 parserTested := 0
for _, test := range parserCoverage { for _, test := range parserCoverage {
@ -513,7 +541,7 @@ func (cli cliHubTest) NewCoverageCmd() *cobra.Command {
if showScenarioCov || showAll { if showScenarioCov || showAll {
scenarioCoverage, err = HubTest.GetScenariosCoverage() scenarioCoverage, err = HubTest.GetScenariosCoverage()
if err != nil { if err != nil {
return fmt.Errorf("while getting scenario coverage: %s", err) return fmt.Errorf("while getting scenario coverage: %w", err)
} }
scenarioTested := 0 scenarioTested := 0
@ -529,7 +557,7 @@ func (cli cliHubTest) NewCoverageCmd() *cobra.Command {
if showAppsecCov || showAll { if showAppsecCov || showAll {
appsecRuleCoverage, err = HubTest.GetAppsecCoverage() appsecRuleCoverage, err = HubTest.GetAppsecCoverage()
if err != nil { if err != nil {
return fmt.Errorf("while getting scenario coverage: %s", err) return fmt.Errorf("while getting scenario coverage: %w", err)
} }
appsecRuleTested := 0 appsecRuleTested := 0
@ -542,19 +570,20 @@ func (cli cliHubTest) NewCoverageCmd() *cobra.Command {
} }
if showOnlyPercent { if showOnlyPercent {
if showAll { switch {
case showAll:
fmt.Printf("parsers=%d%%\nscenarios=%d%%\nappsec_rules=%d%%", parserCoveragePercent, scenarioCoveragePercent, appsecRuleCoveragePercent) fmt.Printf("parsers=%d%%\nscenarios=%d%%\nappsec_rules=%d%%", parserCoveragePercent, scenarioCoveragePercent, appsecRuleCoveragePercent)
} else if showParserCov { case showParserCov:
fmt.Printf("parsers=%d%%", parserCoveragePercent) fmt.Printf("parsers=%d%%", parserCoveragePercent)
} else if showScenarioCov { case showScenarioCov:
fmt.Printf("scenarios=%d%%", scenarioCoveragePercent) fmt.Printf("scenarios=%d%%", scenarioCoveragePercent)
} else if showAppsecCov { case showAppsecCov:
fmt.Printf("appsec_rules=%d%%", appsecRuleCoveragePercent) fmt.Printf("appsec_rules=%d%%", appsecRuleCoveragePercent)
} }
os.Exit(0) os.Exit(0)
} }
switch csConfig.Cscli.Output { switch cfg.Cscli.Output {
case "human": case "human":
if showParserCov || showAll { if showParserCov || showAll {
hubTestParserCoverageTable(color.Output, parserCoverage) hubTestParserCoverageTable(color.Output, parserCoverage)
@ -595,7 +624,7 @@ func (cli cliHubTest) NewCoverageCmd() *cobra.Command {
} }
fmt.Printf("%s", dump) fmt.Printf("%s", dump)
default: default:
return fmt.Errorf("only human/json output modes are supported") return errors.New("only human/json output modes are supported")
} }
return nil return nil
@ -610,7 +639,7 @@ func (cli cliHubTest) NewCoverageCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewEvalCmd() *cobra.Command { func (cli *cliHubTest) NewEvalCmd() *cobra.Command {
var evalExpression string var evalExpression string
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -647,7 +676,7 @@ func (cli cliHubTest) NewEvalCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliHubTest) NewExplainCmd() *cobra.Command { func (cli *cliHubTest) NewExplainCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "explain", Use: "explain",
Short: "explain [test_name]", Short: "explain [test_name]",
@ -666,7 +695,7 @@ func (cli cliHubTest) NewExplainCmd() *cobra.Command {
} }
if err = test.ParserAssert.LoadTest(test.ParserResultFile); err != nil { if err = test.ParserAssert.LoadTest(test.ParserResultFile); err != nil {
return fmt.Errorf("unable to load parser result after run: %s", err) return fmt.Errorf("unable to load parser result after run: %w", err)
} }
} }
@ -677,7 +706,7 @@ func (cli cliHubTest) NewExplainCmd() *cobra.Command {
} }
if err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile); err != nil { if err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile); err != nil {
return fmt.Errorf("unable to load scenario result after run: %s", err) return fmt.Errorf("unable to load scenario result after run: %w", err)
} }
} }
opts := dumps.DumpOpts{} opts := dumps.DumpOpts{}

View file

@ -5,8 +5,8 @@ import (
"io" "io"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
"github.com/enescakir/emoji"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
"github.com/crowdsecurity/crowdsec/pkg/hubtest" "github.com/crowdsecurity/crowdsec/pkg/hubtest"
) )
@ -17,9 +17,9 @@ func hubTestResultTable(out io.Writer, testResult map[string]bool) {
t.SetAlignment(table.AlignLeft) t.SetAlignment(table.AlignLeft)
for testName, success := range testResult { for testName, success := range testResult {
status := emoji.CheckMarkButton.String() status := emoji.CheckMarkButton
if !success { if !success {
status = emoji.CrossMark.String() status = emoji.CrossMark
} }
t.AddRow(testName, status) t.AddRow(testName, status)
@ -50,11 +50,12 @@ func hubTestParserCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
parserTested := 0 parserTested := 0
for _, test := range coverage { for _, test := range coverage {
status := emoji.RedCircle.String() status := emoji.RedCircle
if test.TestsCount > 0 { if test.TestsCount > 0 {
status = emoji.GreenCircle.String() status = emoji.GreenCircle
parserTested++ parserTested++
} }
t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn)))
} }
@ -70,11 +71,12 @@ func hubTestAppsecRuleCoverageTable(out io.Writer, coverage []hubtest.Coverage)
parserTested := 0 parserTested := 0
for _, test := range coverage { for _, test := range coverage {
status := emoji.RedCircle.String() status := emoji.RedCircle
if test.TestsCount > 0 { if test.TestsCount > 0 {
status = emoji.GreenCircle.String() status = emoji.GreenCircle
parserTested++ parserTested++
} }
t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn)))
} }
@ -90,11 +92,12 @@ func hubTestScenarioCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
parserTested := 0 parserTested := 0
for _, test := range coverage { for _, test := range coverage {
status := emoji.RedCircle.String() status := emoji.RedCircle
if test.TestsCount > 0 { if test.TestsCount > 0 {
status = emoji.GreenCircle.String() status = emoji.GreenCircle
parserTested++ parserTested++
} }
t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn)))
} }

View file

@ -37,6 +37,7 @@ func ShowMetrics(hubItem *cwhub.Item) error {
appsecMetricsTable(color.Output, hubItem.Name, metrics) appsecMetricsTable(color.Output, hubItem.Name, metrics)
default: // no metrics for this item type default: // no metrics for this item type
} }
return nil return nil
} }
@ -49,21 +50,27 @@ func GetParserMetric(url string, itemName string) map[string]map[string]int {
if !strings.HasPrefix(fam.Name, "cs_") { if !strings.HasPrefix(fam.Name, "cs_") {
continue continue
} }
log.Tracef("round %d", idx) log.Tracef("round %d", idx)
for _, m := range fam.Metrics { for _, m := range fam.Metrics {
metric, ok := m.(prom2json.Metric) metric, ok := m.(prom2json.Metric)
if !ok { if !ok {
log.Debugf("failed to convert metric to prom2json.Metric") log.Debugf("failed to convert metric to prom2json.Metric")
continue continue
} }
name, ok := metric.Labels["name"] name, ok := metric.Labels["name"]
if !ok { if !ok {
log.Debugf("no name in Metric %v", metric.Labels) log.Debugf("no name in Metric %v", metric.Labels)
} }
if name != itemName { if name != itemName {
continue continue
} }
source, ok := metric.Labels["source"] source, ok := metric.Labels["source"]
if !ok { if !ok {
log.Debugf("no source in Metric %v", metric.Labels) log.Debugf("no source in Metric %v", metric.Labels)
} else { } else {
@ -71,12 +78,15 @@ func GetParserMetric(url string, itemName string) map[string]map[string]int {
source = srctype + ":" + source source = srctype + ":" + source
} }
} }
value := m.(prom2json.Metric).Value value := m.(prom2json.Metric).Value
fval, err := strconv.ParseFloat(value, 32) fval, err := strconv.ParseFloat(value, 32)
if err != nil { if err != nil {
log.Errorf("Unexpected int value %s : %s", value, err) log.Errorf("Unexpected int value %s : %s", value, err)
continue continue
} }
ival := int(fval) ival := int(fval)
switch fam.Name { switch fam.Name {
@ -119,6 +129,7 @@ func GetParserMetric(url string, itemName string) map[string]map[string]int {
} }
} }
} }
return stats return stats
} }
@ -136,26 +147,34 @@ func GetScenarioMetric(url string, itemName string) map[string]int {
if !strings.HasPrefix(fam.Name, "cs_") { if !strings.HasPrefix(fam.Name, "cs_") {
continue continue
} }
log.Tracef("round %d", idx) log.Tracef("round %d", idx)
for _, m := range fam.Metrics { for _, m := range fam.Metrics {
metric, ok := m.(prom2json.Metric) metric, ok := m.(prom2json.Metric)
if !ok { if !ok {
log.Debugf("failed to convert metric to prom2json.Metric") log.Debugf("failed to convert metric to prom2json.Metric")
continue continue
} }
name, ok := metric.Labels["name"] name, ok := metric.Labels["name"]
if !ok { if !ok {
log.Debugf("no name in Metric %v", metric.Labels) log.Debugf("no name in Metric %v", metric.Labels)
} }
if name != itemName { if name != itemName {
continue continue
} }
value := m.(prom2json.Metric).Value value := m.(prom2json.Metric).Value
fval, err := strconv.ParseFloat(value, 32) fval, err := strconv.ParseFloat(value, 32)
if err != nil { if err != nil {
log.Errorf("Unexpected int value %s : %s", value, err) log.Errorf("Unexpected int value %s : %s", value, err)
continue continue
} }
ival := int(fval) ival := int(fval)
switch fam.Name { switch fam.Name {
@ -174,6 +193,7 @@ func GetScenarioMetric(url string, itemName string) map[string]int {
} }
} }
} }
return stats return stats
} }
@ -188,17 +208,22 @@ func GetAppsecRuleMetric(url string, itemName string) map[string]int {
if !strings.HasPrefix(fam.Name, "cs_") { if !strings.HasPrefix(fam.Name, "cs_") {
continue continue
} }
log.Tracef("round %d", idx) log.Tracef("round %d", idx)
for _, m := range fam.Metrics { for _, m := range fam.Metrics {
metric, ok := m.(prom2json.Metric) metric, ok := m.(prom2json.Metric)
if !ok { if !ok {
log.Debugf("failed to convert metric to prom2json.Metric") log.Debugf("failed to convert metric to prom2json.Metric")
continue continue
} }
name, ok := metric.Labels["rule_name"] name, ok := metric.Labels["rule_name"]
if !ok { if !ok {
log.Debugf("no rule_name in Metric %v", metric.Labels) log.Debugf("no rule_name in Metric %v", metric.Labels)
} }
if name != itemName { if name != itemName {
continue continue
} }
@ -209,11 +234,13 @@ func GetAppsecRuleMetric(url string, itemName string) map[string]int {
} }
value := m.(prom2json.Metric).Value value := m.(prom2json.Metric).Value
fval, err := strconv.ParseFloat(value, 32) fval, err := strconv.ParseFloat(value, 32)
if err != nil { if err != nil {
log.Errorf("Unexpected int value %s : %s", value, err) log.Errorf("Unexpected int value %s : %s", value, err)
continue continue
} }
ival := int(fval) ival := int(fval)
switch fam.Name { switch fam.Name {
@ -231,6 +258,7 @@ func GetAppsecRuleMetric(url string, itemName string) map[string]int {
} }
} }
} }
return stats return stats
} }
@ -247,6 +275,7 @@ func GetPrometheusMetric(url string) []*prom2json.Family {
go func() { go func() {
defer trace.CatchPanic("crowdsec/GetPrometheusMetric") defer trace.CatchPanic("crowdsec/GetPrometheusMetric")
err := prom2json.FetchMetricFamilies(url, mfChan, transport) err := prom2json.FetchMetricFamilies(url, mfChan, transport)
if err != nil { if err != nil {
log.Fatalf("failed to fetch prometheus metrics : %v", err) log.Fatalf("failed to fetch prometheus metrics : %v", err)
@ -257,6 +286,7 @@ func GetPrometheusMetric(url string) []*prom2json.Family {
for mf := range mfChan { for mf := range mfChan {
result = append(result, prom2json.NewFamily(mf)) result = append(result, prom2json.NewFamily(mf))
} }
log.Debugf("Finished reading prometheus output, %d entries", len(result)) log.Debugf("Finished reading prometheus output, %d entries", len(result))
return result return result

View file

@ -61,7 +61,7 @@ func compInstalledItems(itemType string, args []string, toComplete string) ([]st
return nil, cobra.ShellCompDirectiveDefault return nil, cobra.ShellCompDirectiveDefault
} }
items, err := hub.GetInstalledItemNames(itemType) items, err := hub.GetInstalledNamesByType(itemType)
if err != nil { if err != nil {
cobra.CompDebugln(fmt.Sprintf("list installed %s err: %s", itemType, err), true) cobra.CompDebugln(fmt.Sprintf("list installed %s err: %s", itemType, err), true)
return nil, cobra.ShellCompDirectiveDefault return nil, cobra.ShellCompDirectiveDefault

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -28,6 +29,7 @@ type cliHelp struct {
} }
type cliItem struct { type cliItem struct {
cfg configGetter
name string // plural, as used in the hub index name string // plural, as used in the hub index
singular string singular string
oneOrMore string // parenthetical pluralizaion: "parser(s)" oneOrMore string // parenthetical pluralizaion: "parser(s)"
@ -51,34 +53,19 @@ func (cli cliItem) NewCommand() *cobra.Command {
DisableAutoGenTag: true, DisableAutoGenTag: true,
} }
cmd.AddCommand(cli.NewInstallCmd()) cmd.AddCommand(cli.newInstallCmd())
cmd.AddCommand(cli.NewRemoveCmd()) cmd.AddCommand(cli.newRemoveCmd())
cmd.AddCommand(cli.NewUpgradeCmd()) cmd.AddCommand(cli.newUpgradeCmd())
cmd.AddCommand(cli.NewInspectCmd()) cmd.AddCommand(cli.newInspectCmd())
cmd.AddCommand(cli.NewListCmd()) cmd.AddCommand(cli.newListCmd())
return cmd return cmd
} }
func (cli cliItem) Install(cmd *cobra.Command, args []string) error { func (cli cliItem) install(args []string, downloadOnly bool, force bool, ignoreError bool) error {
flags := cmd.Flags() cfg := cli.cfg()
downloadOnly, err := flags.GetBool("download-only") hub, err := require.Hub(cfg, require.RemoteHub(cfg), log.StandardLogger())
if err != nil {
return err
}
force, err := flags.GetBool("force")
if err != nil {
return err
}
ignoreError, err := flags.GetBool("ignore")
if err != nil {
return err
}
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
@ -88,7 +75,7 @@ func (cli cliItem) Install(cmd *cobra.Command, args []string) error {
if item == nil { if item == nil {
msg := suggestNearestMessage(hub, cli.name, name) msg := suggestNearestMessage(hub, cli.name, name)
if !ignoreError { if !ignoreError {
return fmt.Errorf(msg) return errors.New(msg)
} }
log.Errorf(msg) log.Errorf(msg)
@ -110,7 +97,13 @@ func (cli cliItem) Install(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliItem) NewInstallCmd() *cobra.Command { func (cli cliItem) newInstallCmd() *cobra.Command {
var (
downloadOnly bool
force bool
ignoreError bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: coalesce.String(cli.installHelp.use, "install [item]..."), Use: coalesce.String(cli.installHelp.use, "install [item]..."),
Short: coalesce.String(cli.installHelp.short, fmt.Sprintf("Install given %s", cli.oneOrMore)), Short: coalesce.String(cli.installHelp.short, fmt.Sprintf("Install given %s", cli.oneOrMore)),
@ -118,16 +111,18 @@ func (cli cliItem) NewInstallCmd() *cobra.Command {
Example: cli.installHelp.example, Example: cli.installHelp.example,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compAllItems(cli.name, args, toComplete) return compAllItems(cli.name, args, toComplete)
}, },
RunE: cli.Install, RunE: func(_ *cobra.Command, args []string) error {
return cli.install(args, downloadOnly, force, ignoreError)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolP("download-only", "d", false, "Only download packages, don't enable") flags.BoolVarP(&downloadOnly, "download-only", "d", false, "Only download packages, don't enable")
flags.Bool("force", false, "Force install: overwrite tainted and outdated files") flags.BoolVar(&force, "force", false, "Force install: overwrite tainted and outdated files")
flags.Bool("ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", cli.name)) flags.BoolVar(&ignoreError, "ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", cli.name))
return cmd return cmd
} }
@ -145,33 +140,16 @@ func istalledParentNames(item *cwhub.Item) []string {
return ret return ret
} }
func (cli cliItem) Remove(cmd *cobra.Command, args []string) error { func (cli cliItem) remove(args []string, purge bool, force bool, all bool) error {
flags := cmd.Flags() hub, err := require.Hub(cli.cfg(), nil, log.StandardLogger())
purge, err := flags.GetBool("purge")
if err != nil {
return err
}
force, err := flags.GetBool("force")
if err != nil {
return err
}
all, err := flags.GetBool("all")
if err != nil {
return err
}
hub, err := require.Hub(csConfig, nil, log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
if all { if all {
getter := hub.GetInstalledItems getter := hub.GetInstalledItemsByType
if purge { if purge {
getter = hub.GetAllItems getter = hub.GetItemsByType
} }
items, err := getter(cli.name) items, err := getter(cli.name)
@ -189,6 +167,7 @@ func (cli cliItem) Remove(cmd *cobra.Command, args []string) error {
if didRemove { if didRemove {
log.Infof("Removed %s", item.Name) log.Infof("Removed %s", item.Name)
removed++ removed++
} }
} }
@ -230,6 +209,7 @@ func (cli cliItem) Remove(cmd *cobra.Command, args []string) error {
if didRemove { if didRemove {
log.Infof("Removed %s", item.Name) log.Infof("Removed %s", item.Name)
removed++ removed++
} }
} }
@ -243,7 +223,13 @@ func (cli cliItem) Remove(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliItem) NewRemoveCmd() *cobra.Command { func (cli cliItem) newRemoveCmd() *cobra.Command {
var (
purge bool
force bool
all bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: coalesce.String(cli.removeHelp.use, "remove [item]..."), Use: coalesce.String(cli.removeHelp.use, "remove [item]..."),
Short: coalesce.String(cli.removeHelp.short, fmt.Sprintf("Remove given %s", cli.oneOrMore)), Short: coalesce.String(cli.removeHelp.short, fmt.Sprintf("Remove given %s", cli.oneOrMore)),
@ -251,40 +237,32 @@ func (cli cliItem) NewRemoveCmd() *cobra.Command {
Example: cli.removeHelp.example, Example: cli.removeHelp.example,
Aliases: []string{"delete"}, Aliases: []string{"delete"},
DisableAutoGenTag: true, DisableAutoGenTag: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstalledItems(cli.name, args, toComplete) return compInstalledItems(cli.name, args, toComplete)
}, },
RunE: cli.Remove, RunE: func(_ *cobra.Command, args []string) error {
return cli.remove(args, purge, force, all)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.Bool("purge", false, "Delete source file too") flags.BoolVar(&purge, "purge", false, "Delete source file too")
flags.Bool("force", false, "Force remove: remove tainted and outdated files") flags.BoolVar(&force, "force", false, "Force remove: remove tainted and outdated files")
flags.Bool("all", false, fmt.Sprintf("Remove all the %s", cli.name)) flags.BoolVar(&all, "all", false, fmt.Sprintf("Remove all the %s", cli.name))
return cmd return cmd
} }
func (cli cliItem) Upgrade(cmd *cobra.Command, args []string) error { func (cli cliItem) upgrade(args []string, force bool, all bool) error {
flags := cmd.Flags() cfg := cli.cfg()
force, err := flags.GetBool("force") hub, err := require.Hub(cfg, require.RemoteHub(cfg), log.StandardLogger())
if err != nil {
return err
}
all, err := flags.GetBool("all")
if err != nil {
return err
}
hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
if all { if all {
items, err := hub.GetInstalledItems(cli.name) items, err := hub.GetInstalledItemsByType(cli.name)
if err != nil { if err != nil {
return err return err
} }
@ -330,6 +308,7 @@ func (cli cliItem) Upgrade(cmd *cobra.Command, args []string) error {
if didUpdate { if didUpdate {
log.Infof("Updated %s", item.Name) log.Infof("Updated %s", item.Name)
updated++ updated++
} }
} }
@ -341,60 +320,51 @@ func (cli cliItem) Upgrade(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliItem) NewUpgradeCmd() *cobra.Command { func (cli cliItem) newUpgradeCmd() *cobra.Command {
var (
all bool
force bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: coalesce.String(cli.upgradeHelp.use, "upgrade [item]..."), Use: coalesce.String(cli.upgradeHelp.use, "upgrade [item]..."),
Short: coalesce.String(cli.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", cli.oneOrMore)), Short: coalesce.String(cli.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", cli.oneOrMore)),
Long: coalesce.String(cli.upgradeHelp.long, fmt.Sprintf("Fetch and upgrade one or more %s from the hub", cli.name)), Long: coalesce.String(cli.upgradeHelp.long, fmt.Sprintf("Fetch and upgrade one or more %s from the hub", cli.name)),
Example: cli.upgradeHelp.example, Example: cli.upgradeHelp.example,
DisableAutoGenTag: true, DisableAutoGenTag: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstalledItems(cli.name, args, toComplete) return compInstalledItems(cli.name, args, toComplete)
}, },
RunE: cli.Upgrade, RunE: func(_ *cobra.Command, args []string) error {
return cli.upgrade(args, force, all)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolP("all", "a", false, fmt.Sprintf("Upgrade all the %s", cli.name)) flags.BoolVarP(&all, "all", "a", false, fmt.Sprintf("Upgrade all the %s", cli.name))
flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files") flags.BoolVar(&force, "force", false, "Force upgrade: overwrite tainted and outdated files")
return cmd return cmd
} }
func (cli cliItem) Inspect(cmd *cobra.Command, args []string) error { func (cli cliItem) inspect(args []string, url string, diff bool, rev bool, noMetrics bool) error {
flags := cmd.Flags() cfg := cli.cfg()
url, err := flags.GetString("url") if rev && !diff {
if err != nil { return errors.New("--rev can only be used with --diff")
return err
} }
if url != "" { if url != "" {
csConfig.Cscli.PrometheusUrl = url cfg.Cscli.PrometheusUrl = url
}
diff, err := flags.GetBool("diff")
if err != nil {
return err
}
rev, err := flags.GetBool("rev")
if err != nil {
return err
}
noMetrics, err := flags.GetBool("no-metrics")
if err != nil {
return err
} }
remote := (*cwhub.RemoteHubCfg)(nil) remote := (*cwhub.RemoteHubCfg)(nil)
if diff { if diff {
remote = require.RemoteHub(csConfig) remote = require.RemoteHub(cfg)
} }
hub, err := require.Hub(csConfig, remote, log.StandardLogger()) hub, err := require.Hub(cfg, remote, log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
@ -411,7 +381,7 @@ func (cli cliItem) Inspect(cmd *cobra.Command, args []string) error {
continue continue
} }
if err = InspectItem(item, !noMetrics); err != nil { if err = inspectItem(item, !noMetrics); err != nil {
return err return err
} }
@ -425,7 +395,14 @@ func (cli cliItem) Inspect(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliItem) NewInspectCmd() *cobra.Command { func (cli cliItem) newInspectCmd() *cobra.Command {
var (
url string
diff bool
rev bool
noMetrics bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: coalesce.String(cli.inspectHelp.use, "inspect [item]..."), Use: coalesce.String(cli.inspectHelp.use, "inspect [item]..."),
Short: coalesce.String(cli.inspectHelp.short, fmt.Sprintf("Inspect given %s", cli.oneOrMore)), Short: coalesce.String(cli.inspectHelp.short, fmt.Sprintf("Inspect given %s", cli.oneOrMore)),
@ -433,49 +410,25 @@ func (cli cliItem) NewInspectCmd() *cobra.Command {
Example: cli.inspectHelp.example, Example: cli.inspectHelp.example,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstalledItems(cli.name, args, toComplete) return compInstalledItems(cli.name, args, toComplete)
}, },
PreRunE: func(cmd *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, args []string) error {
flags := cmd.Flags() return cli.inspect(args, url, diff, rev, noMetrics)
diff, err := flags.GetBool("diff")
if err != nil {
return err
}
rev, err := flags.GetBool("rev")
if err != nil {
return err
}
if rev && !diff {
return fmt.Errorf("--rev can only be used with --diff")
}
return nil
}, },
RunE: cli.Inspect,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringP("url", "u", "", "Prometheus url") flags.StringVarP(&url, "url", "u", "", "Prometheus url")
flags.Bool("diff", false, "Show diff with latest version (for tainted items)") flags.BoolVar(&diff, "diff", false, "Show diff with latest version (for tainted items)")
flags.Bool("rev", false, "Reverse diff output") flags.BoolVar(&rev, "rev", false, "Reverse diff output")
flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)") flags.BoolVar(&noMetrics, "no-metrics", false, "Don't show metrics (when cscli.output=human)")
return cmd return cmd
} }
func (cli cliItem) List(cmd *cobra.Command, args []string) error { func (cli cliItem) list(args []string, all bool) error {
flags := cmd.Flags() hub, err := require.Hub(cli.cfg(), nil, log.StandardLogger())
all, err := flags.GetBool("all")
if err != nil {
return err
}
hub, err := require.Hub(csConfig, nil, log.StandardLogger())
if err != nil { if err != nil {
return err return err
} }
@ -494,18 +447,22 @@ func (cli cliItem) List(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliItem) NewListCmd() *cobra.Command { func (cli cliItem) newListCmd() *cobra.Command {
var all bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: coalesce.String(cli.listHelp.use, "list [item... | -a]"), Use: coalesce.String(cli.listHelp.use, "list [item... | -a]"),
Short: coalesce.String(cli.listHelp.short, fmt.Sprintf("List %s", cli.oneOrMore)), Short: coalesce.String(cli.listHelp.short, fmt.Sprintf("List %s", cli.oneOrMore)),
Long: coalesce.String(cli.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", cli.name)), Long: coalesce.String(cli.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", cli.name)),
Example: cli.listHelp.example, Example: cli.listHelp.example,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: cli.List, RunE: func(_ *cobra.Command, args []string) error {
return cli.list(args, all)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolP("all", "a", false, "List disabled items as well") flags.BoolVarP(&all, "all", "a", false, "List disabled items as well")
return cmd return cmd
} }
@ -580,6 +537,7 @@ func (cli cliItem) whyTainted(hub *cwhub.Hub, item *cwhub.Item, reverse bool) st
// hack: avoid message "item is tainted by itself" // hack: avoid message "item is tainted by itself"
continue continue
} }
ret = append(ret, fmt.Sprintf("# %s is tainted by %s", sub.FQName(), taintList)) ret = append(ret, fmt.Sprintf("# %s is tainted by %s", sub.FQName(), taintList))
} }
} }

View file

@ -17,7 +17,7 @@ import (
// selectItems returns a slice of items of a given type, selected by name and sorted by case-insensitive name // selectItems returns a slice of items of a given type, selected by name and sorted by case-insensitive name
func selectItems(hub *cwhub.Hub, itemType string, args []string, installedOnly bool) ([]*cwhub.Item, error) { func selectItems(hub *cwhub.Hub, itemType string, args []string, installedOnly bool) ([]*cwhub.Item, error) {
itemNames := hub.GetItemNames(itemType) itemNames := hub.GetNamesByType(itemType)
notExist := []string{} notExist := []string{}
@ -116,7 +116,7 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item
} }
if err := csvwriter.Write(header); err != nil { if err := csvwriter.Write(header); err != nil {
return fmt.Errorf("failed to write header: %s", err) return fmt.Errorf("failed to write header: %w", err)
} }
for _, itemType := range itemTypes { for _, itemType := range itemTypes {
@ -132,32 +132,30 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item
} }
if err := csvwriter.Write(row); err != nil { if err := csvwriter.Write(row); err != nil {
return fmt.Errorf("failed to write raw output: %s", err) return fmt.Errorf("failed to write raw output: %w", err)
} }
} }
} }
csvwriter.Flush() csvwriter.Flush()
default:
return fmt.Errorf("unknown output format '%s'", csConfig.Cscli.Output)
} }
return nil return nil
} }
func InspectItem(item *cwhub.Item, showMetrics bool) error { func inspectItem(item *cwhub.Item, showMetrics bool) error {
switch csConfig.Cscli.Output { switch csConfig.Cscli.Output {
case "human", "raw": case "human", "raw":
enc := yaml.NewEncoder(os.Stdout) enc := yaml.NewEncoder(os.Stdout)
enc.SetIndent(2) enc.SetIndent(2)
if err := enc.Encode(item); err != nil { if err := enc.Encode(item); err != nil {
return fmt.Errorf("unable to encode item: %s", err) return fmt.Errorf("unable to encode item: %w", err)
} }
case "json": case "json":
b, err := json.MarshalIndent(*item, "", " ") b, err := json.MarshalIndent(*item, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal item: %s", err) return fmt.Errorf("unable to marshal item: %w", err)
} }
fmt.Print(string(b)) fmt.Print(string(b))

View file

@ -6,14 +6,14 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"slices"
"sort" "sort"
"strings" "strings"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"slices"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
@ -29,39 +29,55 @@ import (
const LAPIURLPrefix = "v1" const LAPIURLPrefix = "v1"
func runLapiStatus(cmd *cobra.Command, args []string) error { type cliLapi struct {
password := strfmt.Password(csConfig.API.Client.Credentials.Password) cfg configGetter
apiurl, err := url.Parse(csConfig.API.Client.Credentials.URL) }
login := csConfig.API.Client.Credentials.Login
func NewCLILapi(cfg configGetter) *cliLapi {
return &cliLapi{
cfg: cfg,
}
}
func (cli *cliLapi) status() error {
cfg := cli.cfg()
password := strfmt.Password(cfg.API.Client.Credentials.Password)
login := cfg.API.Client.Credentials.Login
origURL := cfg.API.Client.Credentials.URL
apiURL, err := url.Parse(origURL)
if err != nil { if err != nil {
return fmt.Errorf("parsing api url: %w", err) return fmt.Errorf("parsing api url: %w", err)
} }
hub, err := require.Hub(csConfig, nil, nil) hub, err := require.Hub(cfg, nil, nil)
if err != nil { if err != nil {
return err return err
} }
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS) scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
if err != nil { if err != nil {
return fmt.Errorf("failed to get scenarios: %w", err) return fmt.Errorf("failed to get scenarios: %w", err)
} }
Client, err = apiclient.NewDefaultClient(apiurl, Client, err = apiclient.NewDefaultClient(apiURL,
LAPIURLPrefix, LAPIURLPrefix,
fmt.Sprintf("crowdsec/%s", version.String()), fmt.Sprintf("crowdsec/%s", version.String()),
nil) nil)
if err != nil { if err != nil {
return fmt.Errorf("init default client: %w", err) return fmt.Errorf("init default client: %w", err)
} }
t := models.WatcherAuthRequest{ t := models.WatcherAuthRequest{
MachineID: &login, MachineID: &login,
Password: &password, Password: &password,
Scenarios: scenarios, Scenarios: scenarios,
} }
log.Infof("Loaded credentials from %s", csConfig.API.Client.CredentialsFilePath) log.Infof("Loaded credentials from %s", cfg.API.Client.CredentialsFilePath)
log.Infof("Trying to authenticate with username %s on %s", login, apiurl) // use the original string because apiURL would print 'http://unix/'
log.Infof("Trying to authenticate with username %s on %s", login, origURL)
_, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t) _, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
if err != nil { if err != nil {
@ -69,26 +85,15 @@ func runLapiStatus(cmd *cobra.Command, args []string) error {
} }
log.Infof("You can successfully interact with Local API (LAPI)") log.Infof("You can successfully interact with Local API (LAPI)")
return nil return nil
} }
func runLapiRegister(cmd *cobra.Command, args []string) error { func (cli *cliLapi) register(apiURL string, outputFile string, machine string) error {
flags := cmd.Flags() var err error
apiURL, err := flags.GetString("url") lapiUser := machine
if err != nil { cfg := cli.cfg()
return err
}
outputFile, err := flags.GetString("file")
if err != nil {
return err
}
lapiUser, err := flags.GetString("machine")
if err != nil {
return err
}
if lapiUser == "" { if lapiUser == "" {
lapiUser, err = generateID("") lapiUser, err = generateID("")
@ -96,25 +101,14 @@ func runLapiRegister(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unable to generate machine id: %w", err) return fmt.Errorf("unable to generate machine id: %w", err)
} }
} }
password := strfmt.Password(generatePassword(passwordLength)) password := strfmt.Password(generatePassword(passwordLength))
if apiURL == "" {
if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil || csConfig.API.Client.Credentials.URL == "" { apiurl, err := prepareAPIURL(cfg.API.Client, apiURL)
return fmt.Errorf("no Local API URL. Please provide it in your configuration or with the -u parameter")
}
apiURL = csConfig.API.Client.Credentials.URL
}
/*URL needs to end with /, but user doesn't care*/
if !strings.HasSuffix(apiURL, "/") {
apiURL += "/"
}
/*URL needs to start with http://, but user doesn't care*/
if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") {
apiURL = "http://" + apiURL
}
apiurl, err := url.Parse(apiURL)
if err != nil { if err != nil {
return fmt.Errorf("parsing api url: %w", err) return fmt.Errorf("parsing api url: %w", err)
} }
_, err = apiclient.RegisterClient(&apiclient.Config{ _, err = apiclient.RegisterClient(&apiclient.Config{
MachineID: lapiUser, MachineID: lapiUser,
Password: password, Password: password,
@ -122,7 +116,6 @@ func runLapiRegister(cmd *cobra.Command, args []string) error {
URL: apiurl, URL: apiurl,
VersionPrefix: LAPIURLPrefix, VersionPrefix: LAPIURLPrefix,
}, nil) }, nil)
if err != nil { if err != nil {
return fmt.Errorf("api client register: %w", err) return fmt.Errorf("api client register: %w", err)
} }
@ -130,138 +123,165 @@ func runLapiRegister(cmd *cobra.Command, args []string) error {
log.Printf("Successfully registered to Local API (LAPI)") log.Printf("Successfully registered to Local API (LAPI)")
var dumpFile string var dumpFile string
if outputFile != "" { if outputFile != "" {
dumpFile = outputFile dumpFile = outputFile
} else if csConfig.API.Client.CredentialsFilePath != "" { } else if cfg.API.Client.CredentialsFilePath != "" {
dumpFile = csConfig.API.Client.CredentialsFilePath dumpFile = cfg.API.Client.CredentialsFilePath
} else { } else {
dumpFile = "" dumpFile = ""
} }
apiCfg := csconfig.ApiCredentialsCfg{ apiCfg := csconfig.ApiCredentialsCfg{
Login: lapiUser, Login: lapiUser,
Password: password.String(), Password: password.String(),
URL: apiURL, URL: apiURL,
} }
apiConfigDump, err := yaml.Marshal(apiCfg) apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal api credentials: %w", err) return fmt.Errorf("unable to marshal api credentials: %w", err)
} }
if dumpFile != "" { if dumpFile != "" {
err = os.WriteFile(dumpFile, apiConfigDump, 0o600) err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err) return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err)
} }
log.Printf("Local API credentials written to '%s'", dumpFile) log.Printf("Local API credentials written to '%s'", dumpFile)
} else { } else {
fmt.Printf("%s\n", string(apiConfigDump)) fmt.Printf("%s\n", string(apiConfigDump))
} }
log.Warning(ReloadMessage()) log.Warning(ReloadMessage())
return nil return nil
} }
func NewLapiStatusCmd() *cobra.Command { // prepareAPIURL checks/fixes a LAPI connection url (http, https or socket) and returns an URL struct
func prepareAPIURL(clientCfg *csconfig.LocalApiClientCfg, apiURL string) (*url.URL, error) {
if apiURL == "" {
if clientCfg == nil || clientCfg.Credentials == nil || clientCfg.Credentials.URL == "" {
return nil, errors.New("no Local API URL. Please provide it in your configuration or with the -u parameter")
}
apiURL = clientCfg.Credentials.URL
}
// URL needs to end with /, but user doesn't care
if !strings.HasSuffix(apiURL, "/") {
apiURL += "/"
}
// URL needs to start with http://, but user doesn't care
if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") && !strings.HasPrefix(apiURL, "/") {
apiURL = "http://" + apiURL
}
return url.Parse(apiURL)
}
func (cli *cliLapi) newStatusCmd() *cobra.Command {
cmdLapiStatus := &cobra.Command{ cmdLapiStatus := &cobra.Command{
Use: "status", Use: "status",
Short: "Check authentication to Local API (LAPI)", Short: "Check authentication to Local API (LAPI)",
Args: cobra.MinimumNArgs(0), Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runLapiStatus, RunE: func(_ *cobra.Command, _ []string) error {
return cli.status()
},
} }
return cmdLapiStatus return cmdLapiStatus
} }
func NewLapiRegisterCmd() *cobra.Command { func (cli *cliLapi) newRegisterCmd() *cobra.Command {
cmdLapiRegister := &cobra.Command{ var (
apiURL string
outputFile string
machine string
)
cmd := &cobra.Command{
Use: "register", Use: "register",
Short: "Register a machine to Local API (LAPI)", Short: "Register a machine to Local API (LAPI)",
Long: `Register your machine to the Local API (LAPI). Long: `Register your machine to the Local API (LAPI).
Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`, Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`,
Args: cobra.MinimumNArgs(0), Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runLapiRegister, RunE: func(_ *cobra.Command, _ []string) error {
return cli.register(apiURL, outputFile, machine)
},
} }
flags := cmdLapiRegister.Flags() flags := cmd.Flags()
flags.StringP("url", "u", "", "URL of the API (ie. http://127.0.0.1)") flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)")
flags.StringP("file", "f", "", "output file destination") flags.StringVarP(&outputFile, "file", "f", "", "output file destination")
flags.String("machine", "", "Name of the machine to register with") flags.StringVar(&machine, "machine", "", "Name of the machine to register with")
return cmdLapiRegister return cmd
} }
func NewLapiCmd() *cobra.Command { func (cli *cliLapi) NewCommand() *cobra.Command {
cmdLapi := &cobra.Command{ cmd := &cobra.Command{
Use: "lapi [action]", Use: "lapi [action]",
Short: "Manage interaction with Local API (LAPI)", Short: "Manage interaction with Local API (LAPI)",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := csConfig.LoadAPIClient(); err != nil { if err := cli.cfg().LoadAPIClient(); err != nil {
return fmt.Errorf("loading api client: %w", err) return fmt.Errorf("loading api client: %w", err)
} }
return nil return nil
}, },
} }
cmdLapi.AddCommand(NewLapiRegisterCmd()) cmd.AddCommand(cli.newRegisterCmd())
cmdLapi.AddCommand(NewLapiStatusCmd()) cmd.AddCommand(cli.newStatusCmd())
cmdLapi.AddCommand(NewLapiContextCmd()) cmd.AddCommand(cli.newContextCmd())
return cmdLapi return cmd
} }
func AddContext(key string, values []string) error { func (cli *cliLapi) addContext(key string, values []string) error {
cfg := cli.cfg()
if err := alertcontext.ValidateContextExpr(key, values); err != nil { if err := alertcontext.ValidateContextExpr(key, values); err != nil {
return fmt.Errorf("invalid context configuration :%s", err) return fmt.Errorf("invalid context configuration: %w", err)
} }
if _, ok := csConfig.Crowdsec.ContextToSend[key]; !ok {
csConfig.Crowdsec.ContextToSend[key] = make([]string, 0) if _, ok := cfg.Crowdsec.ContextToSend[key]; !ok {
cfg.Crowdsec.ContextToSend[key] = make([]string, 0)
log.Infof("key '%s' added", key) log.Infof("key '%s' added", key)
} }
data := csConfig.Crowdsec.ContextToSend[key]
data := cfg.Crowdsec.ContextToSend[key]
for _, val := range values { for _, val := range values {
if !slices.Contains(data, val) { if !slices.Contains(data, val) {
log.Infof("value '%s' added to key '%s'", val, key) log.Infof("value '%s' added to key '%s'", val, key)
data = append(data, val) data = append(data, val)
} }
csConfig.Crowdsec.ContextToSend[key] = data
cfg.Crowdsec.ContextToSend[key] = data
} }
if err := csConfig.Crowdsec.DumpContextConfigFile(); err != nil {
if err := cfg.Crowdsec.DumpContextConfigFile(); err != nil {
return err return err
} }
return nil return nil
} }
func NewLapiContextCmd() *cobra.Command { func (cli *cliLapi) newContextAddCmd() *cobra.Command {
cmdContext := &cobra.Command{ var (
Use: "context [command]", keyToAdd string
Short: "Manage context to send with alerts", valuesToAdd []string
DisableAutoGenTag: true, )
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := csConfig.LoadCrowdsec(); err != nil {
fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", csConfig.Crowdsec.ConsoleContextPath)
if err.Error() != fileNotFoundMessage {
return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err)
}
}
if csConfig.DisableAgent {
return errors.New("agent is disabled and lapi context can only be used on the agent")
}
return nil cmd := &cobra.Command{
},
Run: func(cmd *cobra.Command, args []string) {
printHelp(cmd)
},
}
var keyToAdd string
var valuesToAdd []string
cmdContextAdd := &cobra.Command{
Use: "add", Use: "add",
Short: "Add context to send with alerts. You must specify the output key with the expr value you want", Short: "Add context to send with alerts. You must specify the output key with the expr value you want",
Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip
@ -269,18 +289,18 @@ cscli lapi context add --key file_source --value evt.Line.Src
cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
hub, err := require.Hub(csConfig, nil, nil) hub, err := require.Hub(cli.cfg(), nil, nil)
if err != nil { if err != nil {
return err return err
} }
if err = alertcontext.LoadConsoleContext(csConfig, hub); err != nil { if err = alertcontext.LoadConsoleContext(cli.cfg(), hub); err != nil {
return fmt.Errorf("while loading context: %w", err) return fmt.Errorf("while loading context: %w", err)
} }
if keyToAdd != "" { if keyToAdd != "" {
if err := AddContext(keyToAdd, valuesToAdd); err != nil { if err := cli.addContext(keyToAdd, valuesToAdd); err != nil {
return err return err
} }
return nil return nil
@ -290,7 +310,7 @@ cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
keySlice := strings.Split(v, ".") keySlice := strings.Split(v, ".")
key := keySlice[len(keySlice)-1] key := keySlice[len(keySlice)-1]
value := []string{v} value := []string{v}
if err := AddContext(key, value); err != nil { if err := cli.addContext(key, value); err != nil {
return err return err
} }
} }
@ -298,31 +318,37 @@ cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
return nil return nil
}, },
} }
cmdContextAdd.Flags().StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send")
cmdContextAdd.Flags().StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key")
cmdContextAdd.MarkFlagRequired("value")
cmdContext.AddCommand(cmdContextAdd)
cmdContextStatus := &cobra.Command{ flags := cmd.Flags()
flags.StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send")
flags.StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key")
cmd.MarkFlagRequired("value")
return cmd
}
func (cli *cliLapi) newContextStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status", Use: "status",
Short: "List context to send with alerts", Short: "List context to send with alerts",
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
hub, err := require.Hub(csConfig, nil, nil) cfg := cli.cfg()
hub, err := require.Hub(cfg, nil, nil)
if err != nil { if err != nil {
return err return err
} }
if err = alertcontext.LoadConsoleContext(csConfig, hub); err != nil { if err = alertcontext.LoadConsoleContext(cfg, hub); err != nil {
return fmt.Errorf("while loading context: %w", err) return fmt.Errorf("while loading context: %w", err)
} }
if len(csConfig.Crowdsec.ContextToSend) == 0 { if len(cfg.Crowdsec.ContextToSend) == 0 {
fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.") fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.")
return nil return nil
} }
dump, err := yaml.Marshal(csConfig.Crowdsec.ContextToSend) dump, err := yaml.Marshal(cfg.Crowdsec.ContextToSend)
if err != nil { if err != nil {
return fmt.Errorf("unable to show context status: %w", err) return fmt.Errorf("unable to show context status: %w", err)
} }
@ -332,10 +358,14 @@ cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
return nil return nil
}, },
} }
cmdContext.AddCommand(cmdContextStatus)
return cmd
}
func (cli *cliLapi) newContextDetectCmd() *cobra.Command {
var detectAll bool var detectAll bool
cmdContextDetect := &cobra.Command{
cmd := &cobra.Command{
Use: "detect", Use: "detect",
Short: "Detect available fields from the installed parsers", Short: "Detect available fields from the installed parsers",
Example: `cscli lapi context detect --all Example: `cscli lapi context detect --all
@ -343,6 +373,7 @@ cscli lapi context detect crowdsecurity/sshd-logs
`, `,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cfg := cli.cfg()
if !detectAll && len(args) == 0 { if !detectAll && len(args) == 0 {
log.Infof("Please provide parsers to detect or --all flag.") log.Infof("Please provide parsers to detect or --all flag.")
printHelp(cmd) printHelp(cmd)
@ -355,13 +386,13 @@ cscli lapi context detect crowdsecurity/sshd-logs
return fmt.Errorf("failed to init expr helpers: %w", err) return fmt.Errorf("failed to init expr helpers: %w", err)
} }
hub, err := require.Hub(csConfig, nil, nil) hub, err := require.Hub(cfg, nil, nil)
if err != nil { if err != nil {
return err return err
} }
csParsers := parser.NewParsers(hub) csParsers := parser.NewParsers(hub)
if csParsers, err = parser.LoadParsers(csConfig, csParsers); err != nil { if csParsers, err = parser.LoadParsers(cfg, csParsers); err != nil {
return fmt.Errorf("unable to load parsers: %w", err) return fmt.Errorf("unable to load parsers: %w", err)
} }
@ -418,47 +449,85 @@ cscli lapi context detect crowdsecurity/sshd-logs
return nil return nil
}, },
} }
cmdContextDetect.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser") cmd.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser")
cmdContext.AddCommand(cmdContextDetect)
cmdContextDelete := &cobra.Command{ return cmd
}
func (cli *cliLapi) newContextDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete", Use: "delete",
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
filePath := csConfig.Crowdsec.ConsoleContextPath filePath := cli.cfg().Crowdsec.ConsoleContextPath
if filePath == "" { if filePath == "" {
filePath = "the context file" filePath = "the context file"
} }
fmt.Printf("Command \"delete\" is deprecated, please manually edit %s.", filePath) fmt.Printf("Command 'delete' is deprecated, please manually edit %s.", filePath)
return nil return nil
}, },
} }
cmdContext.AddCommand(cmdContextDelete)
return cmdContext return cmd
} }
func detectStaticField(GrokStatics []parser.ExtraField) []string { func (cli *cliLapi) newContextCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "context [command]",
Short: "Manage context to send with alerts",
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
if err := cfg.LoadCrowdsec(); err != nil {
fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", cfg.Crowdsec.ConsoleContextPath)
if err.Error() != fileNotFoundMessage {
return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err)
}
}
if cfg.DisableAgent {
return errors.New("agent is disabled and lapi context can only be used on the agent")
}
return nil
},
Run: func(cmd *cobra.Command, _ []string) {
printHelp(cmd)
},
}
cmd.AddCommand(cli.newContextAddCmd())
cmd.AddCommand(cli.newContextStatusCmd())
cmd.AddCommand(cli.newContextDetectCmd())
cmd.AddCommand(cli.newContextDeleteCmd())
return cmd
}
func detectStaticField(grokStatics []parser.ExtraField) []string {
ret := make([]string, 0) ret := make([]string, 0)
for _, static := range GrokStatics { for _, static := range grokStatics {
if static.Parsed != "" { if static.Parsed != "" {
fieldName := fmt.Sprintf("evt.Parsed.%s", static.Parsed) fieldName := fmt.Sprintf("evt.Parsed.%s", static.Parsed)
if !slices.Contains(ret, fieldName) { if !slices.Contains(ret, fieldName) {
ret = append(ret, fieldName) ret = append(ret, fieldName)
} }
} }
if static.Meta != "" { if static.Meta != "" {
fieldName := fmt.Sprintf("evt.Meta.%s", static.Meta) fieldName := fmt.Sprintf("evt.Meta.%s", static.Meta)
if !slices.Contains(ret, fieldName) { if !slices.Contains(ret, fieldName) {
ret = append(ret, fieldName) ret = append(ret, fieldName)
} }
} }
if static.TargetByName != "" { if static.TargetByName != "" {
fieldName := static.TargetByName fieldName := static.TargetByName
if !strings.HasPrefix(fieldName, "evt.") { if !strings.HasPrefix(fieldName, "evt.") {
fieldName = "evt." + fieldName fieldName = "evt." + fieldName
} }
if !slices.Contains(ret, fieldName) { if !slices.Contains(ret, fieldName) {
ret = append(ret, fieldName) ret = append(ret, fieldName)
} }
@ -515,7 +584,7 @@ func detectNode(node parser.Node, parserCTX parser.UnixParserCtx) []string {
} }
func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string {
var ret = make([]string, 0) ret := make([]string, 0)
for _, subnode := range node.LeavesNodes { for _, subnode := range node.LeavesNodes {
if subnode.Grok.RunTimeRegexp != nil { if subnode.Grok.RunTimeRegexp != nil {
@ -526,6 +595,7 @@ func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string {
} }
} }
} }
if subnode.Grok.RegexpName != "" { if subnode.Grok.RegexpName != "" {
grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName) grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName)
if err == nil { if err == nil {

View file

@ -0,0 +1,49 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
)
func TestPrepareAPIURL_NoProtocol(t *testing.T) {
url, err := prepareAPIURL(nil, "localhost:81")
require.NoError(t, err)
assert.Equal(t, "http://localhost:81/", url.String())
}
func TestPrepareAPIURL_Http(t *testing.T) {
url, err := prepareAPIURL(nil, "http://localhost:81")
require.NoError(t, err)
assert.Equal(t, "http://localhost:81/", url.String())
}
func TestPrepareAPIURL_Https(t *testing.T) {
url, err := prepareAPIURL(nil, "https://localhost:81")
require.NoError(t, err)
assert.Equal(t, "https://localhost:81/", url.String())
}
func TestPrepareAPIURL_UnixSocket(t *testing.T) {
url, err := prepareAPIURL(nil, "/path/socket")
require.NoError(t, err)
assert.Equal(t, "/path/socket/", url.String())
}
func TestPrepareAPIURL_Empty(t *testing.T) {
_, err := prepareAPIURL(nil, "")
require.Error(t, err)
}
func TestPrepareAPIURL_Empty_ConfigOverride(t *testing.T) {
url, err := prepareAPIURL(&csconfig.LocalApiClientCfg{
Credentials: &csconfig.ApiCredentialsCfg{
URL: "localhost:80",
},
}, "")
require.NoError(t, err)
assert.Equal(t, "http://localhost:80/", url.String())
}

View file

@ -4,10 +4,11 @@ import (
saferand "crypto/rand" saferand "crypto/rand"
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"math/big" "math/big"
"os" "os"
"slices"
"strings" "strings"
"time" "time"
@ -18,7 +19,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"slices"
"github.com/crowdsecurity/machineid" "github.com/crowdsecurity/machineid"
@ -46,6 +46,7 @@ func generatePassword(length int) string {
if err != nil { if err != nil {
log.Fatalf("failed getting data from prng for password generation : %s", err) log.Fatalf("failed getting data from prng for password generation : %s", err)
} }
buf[i] = charset[rInt.Int64()] buf[i] = charset[rInt.Int64()]
} }
@ -60,12 +61,14 @@ func generateIDPrefix() (string, error) {
if err == nil { if err == nil {
return prefix, nil return prefix, nil
} }
log.Debugf("failed to get machine-id with usual files: %s", err) log.Debugf("failed to get machine-id with usual files: %s", err)
bID, err := uuid.NewRandom() bID, err := uuid.NewRandom()
if err == nil { if err == nil {
return bID.String(), nil return bID.String(), nil
} }
return "", fmt.Errorf("generating machine id: %w", err) return "", fmt.Errorf("generating machine id: %w", err)
} }
@ -76,11 +79,14 @@ func generateID(prefix string) (string, error) {
if prefix == "" { if prefix == "" {
prefix, err = generateIDPrefix() prefix, err = generateIDPrefix()
} }
if err != nil { if err != nil {
return "", err return "", err
} }
prefix = strings.ReplaceAll(prefix, "-", "")[:32] prefix = strings.ReplaceAll(prefix, "-", "")[:32]
suffix := generatePassword(16) suffix := generatePassword(16)
return prefix + suffix, nil return prefix + suffix, nil
} }
@ -101,53 +107,18 @@ func getLastHeartbeat(m *ent.Machine) (string, bool) {
return hb, true return hb, true
} }
func getAgents(out io.Writer, dbClient *database.Client) error { type cliMachines struct {
machines, err := dbClient.ListMachines() db *database.Client
if err != nil { cfg configGetter
return fmt.Errorf("unable to list machines: %s", err)
}
switch csConfig.Cscli.Output {
case "human":
getAgentsTable(out, machines)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(machines); err != nil {
return fmt.Errorf("failed to marshal")
}
return nil
case "raw":
csvwriter := csv.NewWriter(out)
err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"})
if err != nil {
return fmt.Errorf("failed to write header: %s", err)
}
for _, m := range machines {
validated := "false"
if m.IsValidated {
validated = "true"
}
hb, _ := getLastHeartbeat(m)
err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb})
if err != nil {
return fmt.Errorf("failed to write raw output: %w", err)
}
}
csvwriter.Flush()
default:
return fmt.Errorf("unknown output '%s'", csConfig.Cscli.Output)
}
return nil
} }
type cliMachines struct{} func NewCLIMachines(cfg configGetter) *cliMachines {
return &cliMachines{
func NewCLIMachines() *cliMachines { cfg: cfg,
return &cliMachines{} }
} }
func (cli cliMachines) NewCommand() *cobra.Command { func (cli *cliMachines) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "machines [action]", Use: "machines [action]",
Short: "Manage local API machines [requires local API]", Short: "Manage local API machines [requires local API]",
@ -159,27 +130,75 @@ Note: This command requires database direct access, so is intended to be run on
Aliases: []string{"machine"}, Aliases: []string{"machine"},
PersistentPreRunE: func(_ *cobra.Command, _ []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
var err error var err error
if err = require.LAPI(csConfig); err != nil { if err = require.LAPI(cli.cfg()); err != nil {
return err return err
} }
dbClient, err = database.NewClient(csConfig.DbConfig) cli.db, err = database.NewClient(cli.cfg().DbConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create new database client: %s", err) return fmt.Errorf("unable to create new database client: %w", err)
} }
return nil return nil
}, },
} }
cmd.AddCommand(cli.NewListCmd()) cmd.AddCommand(cli.newListCmd())
cmd.AddCommand(cli.NewAddCmd()) cmd.AddCommand(cli.newAddCmd())
cmd.AddCommand(cli.NewDeleteCmd()) cmd.AddCommand(cli.newDeleteCmd())
cmd.AddCommand(cli.NewValidateCmd()) cmd.AddCommand(cli.newValidateCmd())
cmd.AddCommand(cli.NewPruneCmd()) cmd.AddCommand(cli.newPruneCmd())
return cmd return cmd
} }
func (cli cliMachines) NewListCmd() *cobra.Command { func (cli *cliMachines) list() error {
out := color.Output
machines, err := cli.db.ListMachines()
if err != nil {
return fmt.Errorf("unable to list machines: %w", err)
}
switch cli.cfg().Cscli.Output {
case "human":
getAgentsTable(out, machines)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(machines); err != nil {
return errors.New("failed to marshal")
}
return nil
case "raw":
csvwriter := csv.NewWriter(out)
err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"})
if err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
for _, m := range machines {
validated := "false"
if m.IsValidated {
validated = "true"
}
hb, _ := getLastHeartbeat(m)
if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb}); err != nil {
return fmt.Errorf("failed to write raw output: %w", err)
}
}
csvwriter.Flush()
}
return nil
}
func (cli *cliMachines) newListCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list all machines in the database", Short: "list all machines in the database",
@ -188,163 +207,179 @@ func (cli cliMachines) NewListCmd() *cobra.Command {
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
err := getAgents(color.Output, dbClient) return cli.list()
if err != nil {
return fmt.Errorf("unable to list machines: %s", err)
}
return nil
}, },
} }
return cmd return cmd
} }
func (cli cliMachines) NewAddCmd() *cobra.Command { func (cli *cliMachines) newAddCmd() *cobra.Command {
var (
password MachinePassword
dumpFile string
apiURL string
interactive bool
autoAdd bool
force bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "add", Use: "add",
Short: "add a single machine to the database", Short: "add a single machine to the database",
DisableAutoGenTag: true, DisableAutoGenTag: true,
Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`, Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`,
Example: ` Example: `cscli machines add --auto
cscli machines add --auto
cscli machines add MyTestMachine --auto cscli machines add MyTestMachine --auto
cscli machines add MyTestMachine --password MyPassword cscli machines add MyTestMachine --password MyPassword
`, cscli machines add -f- --auto > /tmp/mycreds.yaml`,
RunE: cli.add, RunE: func(_ *cobra.Command, args []string) error {
return cli.add(args, string(password), dumpFile, apiURL, interactive, autoAdd, force)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringP("password", "p", "", "machine password to login to the API") flags.VarP(&password, "password", "p", "machine password to login to the API")
flags.StringP("file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")") flags.StringVarP(&dumpFile, "file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")")
flags.StringP("url", "u", "", "URL of the local API") flags.StringVarP(&apiURL, "url", "u", "", "URL of the local API")
flags.BoolP("interactive", "i", false, "interfactive mode to enter the password") flags.BoolVarP(&interactive, "interactive", "i", false, "interfactive mode to enter the password")
flags.BoolP("auto", "a", false, "automatically generate password (and username if not provided)") flags.BoolVarP(&autoAdd, "auto", "a", false, "automatically generate password (and username if not provided)")
flags.Bool("force", false, "will force add the machine if it already exist") flags.BoolVar(&force, "force", false, "will force add the machine if it already exist")
return cmd return cmd
} }
func (cli cliMachines) add(cmd *cobra.Command, args []string) error { func (cli *cliMachines) add(args []string, machinePassword string, dumpFile string, apiURL string, interactive bool, autoAdd bool, force bool) error {
flags := cmd.Flags() var (
err error
machinePassword, err := flags.GetString("password") machineID string
if err != nil { )
return err
}
dumpFile, err := flags.GetString("file")
if err != nil {
return err
}
apiURL, err := flags.GetString("url")
if err != nil {
return err
}
interactive, err := flags.GetBool("interactive")
if err != nil {
return err
}
autoAdd, err := flags.GetBool("auto")
if err != nil {
return err
}
force, err := flags.GetBool("force")
if err != nil {
return err
}
var machineID string
// create machineID if not specified by user // create machineID if not specified by user
if len(args) == 0 { if len(args) == 0 {
if !autoAdd { if !autoAdd {
printHelp(cmd) return errors.New("please specify a machine name to add, or use --auto")
return nil
} }
machineID, err = generateID("") machineID, err = generateID("")
if err != nil { if err != nil {
return fmt.Errorf("unable to generate machine id: %s", err) return fmt.Errorf("unable to generate machine id: %w", err)
} }
} else { } else {
machineID = args[0] machineID = args[0]
} }
clientCfg := cli.cfg().API.Client
serverCfg := cli.cfg().API.Server
/*check if file already exists*/ /*check if file already exists*/
if dumpFile == "" && csConfig.API.Client != nil && csConfig.API.Client.CredentialsFilePath != "" { if dumpFile == "" && clientCfg != nil && clientCfg.CredentialsFilePath != "" {
credFile := csConfig.API.Client.CredentialsFilePath credFile := clientCfg.CredentialsFilePath
// use the default only if the file does not exist // use the default only if the file does not exist
_, err = os.Stat(credFile) _, err = os.Stat(credFile)
switch { switch {
case os.IsNotExist(err) || force: case os.IsNotExist(err) || force:
dumpFile = csConfig.API.Client.CredentialsFilePath dumpFile = credFile
case err != nil: case err != nil:
return fmt.Errorf("unable to stat '%s': %s", credFile, err) return fmt.Errorf("unable to stat '%s': %w", credFile, err)
default: default:
return fmt.Errorf(`credentials file '%s' already exists: please remove it, use "--force" or specify a different file with "-f" ("-f -" for standard output)`, credFile) return fmt.Errorf(`credentials file '%s' already exists: please remove it, use "--force" or specify a different file with "-f" ("-f -" for standard output)`, credFile)
} }
} }
if dumpFile == "" { if dumpFile == "" {
return fmt.Errorf(`please specify a file to dump credentials to, with -f ("-f -" for standard output)`) return errors.New(`please specify a file to dump credentials to, with -f ("-f -" for standard output)`)
} }
// create a password if it's not specified by user // create a password if it's not specified by user
if machinePassword == "" && !interactive { if machinePassword == "" && !interactive {
if !autoAdd { if !autoAdd {
return fmt.Errorf("please specify a password with --password or use --auto") return errors.New("please specify a password with --password or use --auto")
} }
machinePassword = generatePassword(passwordLength) machinePassword = generatePassword(passwordLength)
} else if machinePassword == "" && interactive { } else if machinePassword == "" && interactive {
qs := &survey.Password{ qs := &survey.Password{
Message: "Please provide a password for the machine", Message: "Please provide a password for the machine:",
} }
survey.AskOne(qs, &machinePassword) survey.AskOne(qs, &machinePassword)
} }
password := strfmt.Password(machinePassword) password := strfmt.Password(machinePassword)
_, err = dbClient.CreateMachine(&machineID, &password, "", true, force, types.PasswordAuthType)
_, err = cli.db.CreateMachine(&machineID, &password, "", true, force, types.PasswordAuthType)
if err != nil { if err != nil {
return fmt.Errorf("unable to create machine: %s", err) return fmt.Errorf("unable to create machine: %w", err)
} }
fmt.Printf("Machine '%s' successfully added to the local API.\n", machineID)
fmt.Fprintf(os.Stderr, "Machine '%s' successfully added to the local API.\n", machineID)
if apiURL == "" { if apiURL == "" {
if csConfig.API.Client != nil && csConfig.API.Client.Credentials != nil && csConfig.API.Client.Credentials.URL != "" { if clientCfg != nil && clientCfg.Credentials != nil && clientCfg.Credentials.URL != "" {
apiURL = csConfig.API.Client.Credentials.URL apiURL = clientCfg.Credentials.URL
} else if csConfig.API.Server != nil && csConfig.API.Server.ListenURI != "" { } else if serverCfg.ClientURL() != "" {
apiURL = "http://" + csConfig.API.Server.ListenURI apiURL = serverCfg.ClientURL()
} else { } else {
return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter") return errors.New("unable to dump an api URL. Please provide it in your configuration or with the -u parameter")
} }
} }
apiCfg := csconfig.ApiCredentialsCfg{ apiCfg := csconfig.ApiCredentialsCfg{
Login: machineID, Login: machineID,
Password: password.String(), Password: password.String(),
URL: apiURL, URL: apiURL,
} }
apiConfigDump, err := yaml.Marshal(apiCfg) apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal api credentials: %s", err) return fmt.Errorf("unable to marshal api credentials: %w", err)
} }
if dumpFile != "" && dumpFile != "-" { if dumpFile != "" && dumpFile != "-" {
err = os.WriteFile(dumpFile, apiConfigDump, 0o600) if err = os.WriteFile(dumpFile, apiConfigDump, 0o600); err != nil {
if err != nil { return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err)
return fmt.Errorf("write api credentials in '%s' failed: %s", dumpFile, err)
} }
fmt.Printf("API credentials written to '%s'.\n", dumpFile)
fmt.Fprintf(os.Stderr, "API credentials written to '%s'.\n", dumpFile)
} else { } else {
fmt.Printf("%s\n", string(apiConfigDump)) fmt.Print(string(apiConfigDump))
} }
return nil return nil
} }
func (cli cliMachines) NewDeleteCmd() *cobra.Command { func (cli *cliMachines) deleteValid(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
machines, err := cli.db.ListMachines()
if err != nil {
cobra.CompError("unable to list machines " + err.Error())
}
ret := []string{}
for _, machine := range machines {
if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) {
ret = append(ret, machine.MachineId)
}
}
return ret, cobra.ShellCompDirectiveNoFileComp
}
func (cli *cliMachines) delete(machines []string) error {
for _, machineID := range machines {
if err := cli.db.DeleteWatcher(machineID); err != nil {
log.Errorf("unable to delete machine '%s': %s", machineID, err)
return nil
}
log.Infof("machine '%s' deleted successfully", machineID)
}
return nil
}
func (cli *cliMachines) newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "delete [machine_name]...", Use: "delete [machine_name]...",
Short: "delete machine(s) by name", Short: "delete machine(s) by name",
@ -352,40 +387,75 @@ func (cli cliMachines) NewDeleteCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Aliases: []string{"remove"}, Aliases: []string{"remove"},
DisableAutoGenTag: true, DisableAutoGenTag: true,
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: cli.deleteValid,
machines, err := dbClient.ListMachines() RunE: func(_ *cobra.Command, args []string) error {
if err != nil { return cli.delete(args)
cobra.CompError("unable to list machines " + err.Error())
}
ret := make([]string, 0)
for _, machine := range machines {
if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) {
ret = append(ret, machine.MachineId)
}
}
return ret, cobra.ShellCompDirectiveNoFileComp
}, },
RunE: cli.delete,
} }
return cmd return cmd
} }
func (cli cliMachines) delete(_ *cobra.Command, args []string) error { func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force bool) error {
for _, machineID := range args { if duration < 2*time.Minute && !notValidOnly {
err := dbClient.DeleteWatcher(machineID) if yes, err := askYesNo(
if err != nil { "The duration you provided is less than 2 minutes. " +
log.Errorf("unable to delete machine '%s': %s", machineID, err) "This can break installations if the machines are only temporarily disconnected. Continue?", false); err != nil {
return err
} else if !yes {
fmt.Println("User aborted prune. No changes were made.")
return nil return nil
} }
log.Infof("machine '%s' deleted successfully", machineID)
} }
machines := []*ent.Machine{}
if pending, err := cli.db.QueryPendingMachine(); err == nil {
machines = append(machines, pending...)
}
if !notValidOnly {
if pending, err := cli.db.QueryLastValidatedHeartbeatLT(time.Now().UTC().Add(-duration)); err == nil {
machines = append(machines, pending...)
}
}
if len(machines) == 0 {
fmt.Println("No machines to prune.")
return nil
}
getAgentsTable(color.Output, machines)
if !force {
if yes, err := askYesNo(
"You are about to PERMANENTLY remove the above machines from the database. " +
"These will NOT be recoverable. Continue?", false); err != nil {
return err
} else if !yes {
fmt.Println("User aborted prune. No changes were made.")
return nil
}
}
deleted, err := cli.db.BulkDeleteWatchers(machines)
if err != nil {
return fmt.Errorf("unable to prune machines: %w", err)
}
fmt.Fprintf(os.Stderr, "successfully delete %d machines\n", deleted)
return nil return nil
} }
func (cli cliMachines) NewPruneCmd() *cobra.Command { func (cli *cliMachines) newPruneCmd() *cobra.Command {
var parsedDuration time.Duration var (
duration time.Duration
notValidOnly bool
force bool
)
const defaultDuration = 10 * time.Minute
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "prune", Use: "prune",
Short: "prune multiple machines from the database", Short: "prune multiple machines from the database",
@ -395,76 +465,30 @@ cscli machines prune --duration 1h
cscli machines prune --not-validated-only --force`, cscli machines prune --not-validated-only --force`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
dur, _ := cmd.Flags().GetString("duration") return cli.prune(duration, notValidOnly, force)
var err error
parsedDuration, err = time.ParseDuration(fmt.Sprintf("-%s", dur))
if err != nil {
return fmt.Errorf("unable to parse duration '%s': %s", dur, err)
}
return nil
},
RunE: func(cmd *cobra.Command, _ []string) error {
notValidOnly, _ := cmd.Flags().GetBool("not-validated-only")
force, _ := cmd.Flags().GetBool("force")
if parsedDuration >= 0-60*time.Second && !notValidOnly {
var answer bool
prompt := &survey.Confirm{
Message: "The duration you provided is less than or equal 60 seconds this can break installations do you want to continue ?",
Default: false,
}
if err := survey.AskOne(prompt, &answer); err != nil {
return fmt.Errorf("unable to ask about prune check: %s", err)
}
if !answer {
fmt.Println("user aborted prune no changes were made")
return nil
}
}
machines := make([]*ent.Machine, 0)
if pending, err := dbClient.QueryPendingMachine(); err == nil {
machines = append(machines, pending...)
}
if !notValidOnly {
if pending, err := dbClient.QueryLastValidatedHeartbeatLT(time.Now().UTC().Add(parsedDuration)); err == nil {
machines = append(machines, pending...)
}
}
if len(machines) == 0 {
fmt.Println("no machines to prune")
return nil
}
getAgentsTable(color.Output, machines)
if !force {
var answer bool
prompt := &survey.Confirm{
Message: "You are about to PERMANENTLY remove the above machines from the database these will NOT be recoverable, continue ?",
Default: false,
}
if err := survey.AskOne(prompt, &answer); err != nil {
return fmt.Errorf("unable to ask about prune check: %s", err)
}
if !answer {
fmt.Println("user aborted prune no changes were made")
return nil
}
}
nbDeleted, err := dbClient.BulkDeleteWatchers(machines)
if err != nil {
return fmt.Errorf("unable to prune machines: %s", err)
}
fmt.Printf("successfully delete %d machines\n", nbDeleted)
return nil
}, },
} }
cmd.Flags().StringP("duration", "d", "10m", "duration of time since validated machine last heartbeat")
cmd.Flags().Bool("not-validated-only", false, "only prune machines that are not validated") flags := cmd.Flags()
cmd.Flags().Bool("force", false, "force prune without asking for confirmation") flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat")
flags.BoolVar(&notValidOnly, "not-validated-only", false, "only prune machines that are not validated")
flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
return cmd return cmd
} }
func (cli cliMachines) NewValidateCmd() *cobra.Command { func (cli *cliMachines) validate(machineID string) error {
if err := cli.db.ValidateMachine(machineID); err != nil {
return fmt.Errorf("unable to validate machine '%s': %w", machineID, err)
}
log.Infof("machine '%s' validated successfully", machineID)
return nil
}
func (cli *cliMachines) newValidateCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "validate", Use: "validate",
Short: "validate a machine to access the local API", Short: "validate a machine to access the local API",
@ -473,13 +497,7 @@ func (cli cliMachines) NewValidateCmd() *cobra.Command {
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
machineID := args[0] return cli.validate(args[0])
if err := dbClient.ValidateMachine(machineID); err != nil {
return fmt.Errorf("unable to validate machine '%s': %s", machineID, err)
}
log.Infof("machine '%s' validated successfully", machineID)
return nil
}, },
} }

View file

@ -5,9 +5,9 @@ import (
"time" "time"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
"github.com/enescakir/emoji"
"github.com/crowdsecurity/crowdsec/pkg/database/ent" "github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
) )
func getAgentsTable(out io.Writer, machines []*ent.Machine) { func getAgentsTable(out io.Writer, machines []*ent.Machine) {
@ -17,17 +17,16 @@ func getAgentsTable(out io.Writer, machines []*ent.Machine) {
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
for _, m := range machines { for _, m := range machines {
var validated string validated := emoji.Prohibited
if m.IsValidated { if m.IsValidated {
validated = emoji.CheckMark.String() validated = emoji.CheckMark
} else {
validated = emoji.Prohibited.String()
} }
hb, active := getLastHeartbeat(m) hb, active := getLastHeartbeat(m)
if !active { if !active {
hb = emoji.Warning.String() + " " + hb hb = emoji.Warning + " " + hb
} }
t.AddRow(m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb) t.AddRow(m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb)
} }

View file

@ -1,7 +1,9 @@
package main package main
import ( import (
"fmt"
"os" "os"
"path/filepath"
"slices" "slices"
"time" "time"
@ -10,48 +12,102 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/fflag" "github.com/crowdsecurity/crowdsec/pkg/fflag"
) )
var trace_lvl, dbg_lvl, nfo_lvl, wrn_lvl, err_lvl bool var (
ConfigFilePath string
csConfig *csconfig.Config
dbClient *database.Client
)
var ConfigFilePath string type configGetter func() *csconfig.Config
var csConfig *csconfig.Config
var dbClient *database.Client
var OutputFormat string
var OutputColor string
var mergedConfig string var mergedConfig string
// flagBranch overrides the value in csConfig.Cscli.HubBranch type cliRoot struct {
var flagBranch = "" logTrace bool
logDebug bool
logInfo bool
logWarn bool
logErr bool
outputColor string
outputFormat string
// flagBranch overrides the value in csConfig.Cscli.HubBranch
flagBranch string
}
func initConfig() { func newCliRoot() *cliRoot {
var err error return &cliRoot{}
}
if trace_lvl { // cfg() is a helper function to get the configuration loaded from config.yaml,
log.SetLevel(log.TraceLevel) // we pass it to subcommands because the file is not read until the Execute() call
} else if dbg_lvl { func (cli *cliRoot) cfg() *csconfig.Config {
log.SetLevel(log.DebugLevel) return csConfig
} else if nfo_lvl { }
log.SetLevel(log.InfoLevel)
} else if wrn_lvl { // wantedLogLevel returns the log level requested in the command line flags.
log.SetLevel(log.WarnLevel) func (cli *cliRoot) wantedLogLevel() log.Level {
} else if err_lvl { switch {
log.SetLevel(log.ErrorLevel) case cli.logTrace:
return log.TraceLevel
case cli.logDebug:
return log.DebugLevel
case cli.logInfo:
return log.InfoLevel
case cli.logWarn:
return log.WarnLevel
case cli.logErr:
return log.ErrorLevel
default:
return log.InfoLevel
}
}
// loadConfigFor loads the configuration file for the given sub-command.
// If the sub-command does not need it, it returns a default configuration.
func loadConfigFor(command string) (*csconfig.Config, string, error) {
noNeedConfig := []string{
"doc",
"help",
"completion",
"version",
"hubtest",
} }
if !slices.Contains(NoNeedConfig, os.Args[1]) { if !slices.Contains(noNeedConfig, command) {
log.Debugf("Using %s as configuration file", ConfigFilePath) log.Debugf("Using %s as configuration file", ConfigFilePath)
csConfig, mergedConfig, err = csconfig.NewConfig(ConfigFilePath, false, false, true)
config, merged, err := csconfig.NewConfig(ConfigFilePath, false, false, true)
if err != nil { if err != nil {
log.Fatal(err) return nil, "", err
} }
} else {
csConfig = csconfig.NewDefaultConfig() // set up directory for trace files
if err := trace.Init(filepath.Join(config.ConfigPaths.DataDir, "trace")); err != nil {
return nil, "", fmt.Errorf("while setting up trace directory: %w", err)
}
return config, merged, nil
}
return csconfig.NewDefaultConfig(), "", nil
}
// initialize is called before the subcommand is executed.
func (cli *cliRoot) initialize() {
var err error
log.SetLevel(cli.wantedLogLevel())
csConfig, mergedConfig, err = loadConfigFor(os.Args[1])
if err != nil {
log.Fatal(err)
} }
// recap of the enabled feature flags, because logging // recap of the enabled feature flags, because logging
@ -60,20 +116,22 @@ func initConfig() {
log.Debugf("Enabled feature flags: %s", fflist) log.Debugf("Enabled feature flags: %s", fflist)
} }
if flagBranch != "" { if cli.flagBranch != "" {
csConfig.Cscli.HubBranch = flagBranch csConfig.Cscli.HubBranch = cli.flagBranch
} }
if OutputFormat != "" { if cli.outputFormat != "" {
csConfig.Cscli.Output = OutputFormat csConfig.Cscli.Output = cli.outputFormat
if OutputFormat != "json" && OutputFormat != "raw" && OutputFormat != "human" {
log.Fatalf("output format %s unknown", OutputFormat)
}
} }
if csConfig.Cscli.Output == "" { if csConfig.Cscli.Output == "" {
csConfig.Cscli.Output = "human" csConfig.Cscli.Output = "human"
} }
if csConfig.Cscli.Output != "human" && csConfig.Cscli.Output != "json" && csConfig.Cscli.Output != "raw" {
log.Fatalf("output format '%s' not supported: must be one of human, json, raw", csConfig.Cscli.Output)
}
if csConfig.Cscli.Output == "json" { if csConfig.Cscli.Output == "json" {
log.SetFormatter(&log.JSONFormatter{}) log.SetFormatter(&log.JSONFormatter{})
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
@ -81,11 +139,11 @@ func initConfig() {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
} }
if OutputColor != "" { if cli.outputColor != "" {
csConfig.Cscli.Color = OutputColor csConfig.Cscli.Color = cli.outputColor
if OutputColor != "yes" && OutputColor != "no" && OutputColor != "auto" { if cli.outputColor != "yes" && cli.outputColor != "no" && cli.outputColor != "auto" {
log.Fatalf("output color %s unknown", OutputColor) log.Fatalf("output color %s unknown", cli.outputColor)
} }
} }
} }
@ -98,15 +156,25 @@ var validArgs = []string{
"postoverflows", "scenarios", "simulation", "support", "version", "postoverflows", "scenarios", "simulation", "support", "version",
} }
var NoNeedConfig = []string{ func (cli *cliRoot) colorize(cmd *cobra.Command) {
"doc", cc.Init(&cc.Config{
"help", RootCmd: cmd,
"completion", Headings: cc.Yellow,
"version", Commands: cc.Green + cc.Bold,
"hubtest", CmdShortDescr: cc.Cyan,
Example: cc.Italic,
ExecName: cc.Bold,
Aliases: cc.Bold + cc.Italic,
FlagsDataType: cc.White,
Flags: cc.Green,
FlagsDescr: cc.Cyan,
NoExtraNewlines: true,
NoBottomNewline: true,
})
cmd.SetOut(color.Output)
} }
func main() { func (cli *cliRoot) NewCommand() *cobra.Command {
// set the formatter asap and worry about level later // set the formatter asap and worry about level later
logFormatter := &log.TextFormatter{TimestampFormat: time.RFC3339, FullTimestamp: true} logFormatter := &log.TextFormatter{TimestampFormat: time.RFC3339, FullTimestamp: true}
log.SetFormatter(logFormatter) log.SetFormatter(logFormatter)
@ -131,31 +199,25 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
/*TBD examples*/ /*TBD examples*/
} }
cc.Init(&cc.Config{ cli.colorize(cmd)
RootCmd: cmd,
Headings: cc.Yellow,
Commands: cc.Green + cc.Bold,
CmdShortDescr: cc.Cyan,
Example: cc.Italic,
ExecName: cc.Bold,
Aliases: cc.Bold + cc.Italic,
FlagsDataType: cc.White,
Flags: cc.Green,
FlagsDescr: cc.Cyan,
})
cmd.SetOut(color.Output)
cmd.PersistentFlags().StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file") /*don't sort flags so we can enforce order*/
cmd.PersistentFlags().StringVarP(&OutputFormat, "output", "o", "", "Output format: human, json, raw") cmd.Flags().SortFlags = false
cmd.PersistentFlags().StringVarP(&OutputColor, "color", "", "auto", "Output color: yes, no, auto")
cmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug")
cmd.PersistentFlags().BoolVar(&nfo_lvl, "info", false, "Set logging to info")
cmd.PersistentFlags().BoolVar(&wrn_lvl, "warning", false, "Set logging to warning")
cmd.PersistentFlags().BoolVar(&err_lvl, "error", false, "Set logging to error")
cmd.PersistentFlags().BoolVar(&trace_lvl, "trace", false, "Set logging to trace")
cmd.PersistentFlags().StringVar(&flagBranch, "branch", "", "Override hub branch on github") pflags := cmd.PersistentFlags()
if err := cmd.PersistentFlags().MarkHidden("branch"); err != nil { pflags.SortFlags = false
pflags.StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file")
pflags.StringVarP(&cli.outputFormat, "output", "o", "", "Output format: human, json, raw")
pflags.StringVarP(&cli.outputColor, "color", "", "auto", "Output color: yes, no, auto")
pflags.BoolVar(&cli.logDebug, "debug", false, "Set logging to debug")
pflags.BoolVar(&cli.logInfo, "info", false, "Set logging to info")
pflags.BoolVar(&cli.logWarn, "warning", false, "Set logging to warning")
pflags.BoolVar(&cli.logErr, "error", false, "Set logging to error")
pflags.BoolVar(&cli.logTrace, "trace", false, "Set logging to trace")
pflags.StringVar(&cli.flagBranch, "branch", "", "Override hub branch on github")
if err := pflags.MarkHidden("branch"); err != nil {
log.Fatalf("failed to hide flag: %s", err) log.Fatalf("failed to hide flag: %s", err)
} }
@ -175,49 +237,46 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
} }
if len(os.Args) > 1 { if len(os.Args) > 1 {
cobra.OnInitialize(initConfig) cobra.OnInitialize(cli.initialize)
} }
/*don't sort flags so we can enforce order*/
cmd.Flags().SortFlags = false
cmd.PersistentFlags().SortFlags = false
cmd.AddCommand(NewCLIDoc().NewCommand(cmd)) cmd.AddCommand(NewCLIDoc().NewCommand(cmd))
cmd.AddCommand(NewCLIVersion().NewCommand()) cmd.AddCommand(NewCLIVersion().NewCommand())
cmd.AddCommand(NewConfigCmd()) cmd.AddCommand(NewCLIConfig(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIHub().NewCommand()) cmd.AddCommand(NewCLIHub(cli.cfg).NewCommand())
cmd.AddCommand(NewMetricsCmd()) cmd.AddCommand(NewCLIMetrics(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIDashboard().NewCommand()) cmd.AddCommand(NewCLIDashboard(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIDecisions().NewCommand()) cmd.AddCommand(NewCLIDecisions(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIAlerts().NewCommand()) cmd.AddCommand(NewCLIAlerts(cli.cfg).NewCommand())
cmd.AddCommand(NewCLISimulation().NewCommand()) cmd.AddCommand(NewCLISimulation(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIBouncers().NewCommand()) cmd.AddCommand(NewCLIBouncers(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIMachines().NewCommand()) cmd.AddCommand(NewCLIMachines(cli.cfg).NewCommand())
cmd.AddCommand(NewCLICapi().NewCommand()) cmd.AddCommand(NewCLICapi(cli.cfg).NewCommand())
cmd.AddCommand(NewLapiCmd()) cmd.AddCommand(NewCLILapi(cli.cfg).NewCommand())
cmd.AddCommand(NewCompletionCmd()) cmd.AddCommand(NewCompletionCmd())
cmd.AddCommand(NewConsoleCmd()) cmd.AddCommand(NewCLIConsole(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIExplain().NewCommand()) cmd.AddCommand(NewCLIExplain(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIHubTest().NewCommand()) cmd.AddCommand(NewCLIHubTest(cli.cfg).NewCommand())
cmd.AddCommand(NewCLINotifications().NewCommand()) cmd.AddCommand(NewCLINotifications(cli.cfg).NewCommand())
cmd.AddCommand(NewCLISupport().NewCommand()) cmd.AddCommand(NewCLISupport().NewCommand())
cmd.AddCommand(NewCLIPapi().NewCommand()) cmd.AddCommand(NewCLIPapi(cli.cfg).NewCommand())
cmd.AddCommand(NewCLICollection().NewCommand()) cmd.AddCommand(NewCLICollection(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIParser().NewCommand()) cmd.AddCommand(NewCLIParser(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIScenario().NewCommand()) cmd.AddCommand(NewCLIScenario(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIPostOverflow().NewCommand()) cmd.AddCommand(NewCLIPostOverflow(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIContext().NewCommand()) cmd.AddCommand(NewCLIContext(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIAppsecConfig().NewCommand()) cmd.AddCommand(NewCLIAppsecConfig(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIAppsecRule().NewCommand()) cmd.AddCommand(NewCLIAppsecRule(cli.cfg).NewCommand())
if fflag.CscliSetup.IsEnabled() { if fflag.CscliSetup.IsEnabled() {
cmd.AddCommand(NewSetupCmd()) cmd.AddCommand(NewSetupCmd())
} }
if fflag.PapiClient.IsEnabled() { return cmd
cmd.AddCommand(NewCLIPapi().NewCommand()) }
}
func main() {
cmd := newCliRoot().NewCommand()
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View file

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -16,11 +17,63 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/go-cs-lib/maptools"
"github.com/crowdsecurity/go-cs-lib/trace" "github.com/crowdsecurity/go-cs-lib/trace"
) )
// FormatPrometheusMetrics is a complete rip from prom2json type (
func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error { statAcquis map[string]map[string]int
statParser map[string]map[string]int
statBucket map[string]map[string]int
statWhitelist map[string]map[string]map[string]int
statLapi map[string]map[string]int
statLapiMachine map[string]map[string]map[string]int
statLapiBouncer map[string]map[string]map[string]int
statLapiDecision map[string]struct {
NonEmpty int
Empty int
}
statDecision map[string]map[string]map[string]int
statAppsecEngine map[string]map[string]int
statAppsecRule map[string]map[string]map[string]int
statAlert map[string]int
statStash map[string]struct {
Type string
Count int
}
)
var (
ErrMissingConfig = errors.New("prometheus section missing, can't show metrics")
ErrMetricsDisabled = errors.New("prometheus is not enabled, can't show metrics")
)
type metricSection interface {
Table(out io.Writer, noUnit bool, showEmpty bool)
Description() (string, string)
}
type metricStore map[string]metricSection
func NewMetricStore() metricStore {
return metricStore{
"acquisition": statAcquis{},
"scenarios": statBucket{},
"parsers": statParser{},
"lapi": statLapi{},
"lapi-machine": statLapiMachine{},
"lapi-bouncer": statLapiBouncer{},
"lapi-decisions": statLapiDecision{},
"decisions": statDecision{},
"alerts": statAlert{},
"stash": statStash{},
"appsec-engine": statAppsecEngine{},
"appsec-rule": statAppsecRule{},
"whitelists": statWhitelist{},
}
}
func (ms metricStore) Fetch(url string) error {
mfChan := make(chan *dto.MetricFamily, 1024) mfChan := make(chan *dto.MetricFamily, 1024)
errChan := make(chan error, 1) errChan := make(chan error, 1)
@ -33,9 +86,10 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
transport.ResponseHeaderTimeout = time.Minute transport.ResponseHeaderTimeout = time.Minute
go func() { go func() {
defer trace.CatchPanic("crowdsec/ShowPrometheus") defer trace.CatchPanic("crowdsec/ShowPrometheus")
err := prom2json.FetchMetricFamilies(url, mfChan, transport) err := prom2json.FetchMetricFamilies(url, mfChan, transport)
if err != nil { if err != nil {
errChan <- fmt.Errorf("failed to fetch prometheus metrics: %w", err) errChan <- fmt.Errorf("failed to fetch metrics: %w", err)
return return
} }
errChan <- nil errChan <- nil
@ -50,42 +104,42 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
return err return err
} }
log.Debugf("Finished reading prometheus output, %d entries", len(result)) log.Debugf("Finished reading metrics output, %d entries", len(result))
/*walk*/ /*walk*/
lapi_decisions_stats := map[string]struct {
NonEmpty int mAcquis := ms["acquisition"].(statAcquis)
Empty int mParser := ms["parsers"].(statParser)
}{} mBucket := ms["scenarios"].(statBucket)
acquis_stats := map[string]map[string]int{} mLapi := ms["lapi"].(statLapi)
parsers_stats := map[string]map[string]int{} mLapiMachine := ms["lapi-machine"].(statLapiMachine)
buckets_stats := map[string]map[string]int{} mLapiBouncer := ms["lapi-bouncer"].(statLapiBouncer)
lapi_stats := map[string]map[string]int{} mLapiDecision := ms["lapi-decisions"].(statLapiDecision)
lapi_machine_stats := map[string]map[string]map[string]int{} mDecision := ms["decisions"].(statDecision)
lapi_bouncer_stats := map[string]map[string]map[string]int{} mAppsecEngine := ms["appsec-engine"].(statAppsecEngine)
decisions_stats := map[string]map[string]map[string]int{} mAppsecRule := ms["appsec-rule"].(statAppsecRule)
appsec_engine_stats := map[string]map[string]int{} mAlert := ms["alerts"].(statAlert)
appsec_rule_stats := map[string]map[string]map[string]int{} mStash := ms["stash"].(statStash)
alerts_stats := map[string]int{} mWhitelist := ms["whitelists"].(statWhitelist)
stash_stats := map[string]struct {
Type string
Count int
}{}
for idx, fam := range result { for idx, fam := range result {
if !strings.HasPrefix(fam.Name, "cs_") { if !strings.HasPrefix(fam.Name, "cs_") {
continue continue
} }
log.Tracef("round %d", idx) log.Tracef("round %d", idx)
for _, m := range fam.Metrics { for _, m := range fam.Metrics {
metric, ok := m.(prom2json.Metric) metric, ok := m.(prom2json.Metric)
if !ok { if !ok {
log.Debugf("failed to convert metric to prom2json.Metric") log.Debugf("failed to convert metric to prom2json.Metric")
continue continue
} }
name, ok := metric.Labels["name"] name, ok := metric.Labels["name"]
if !ok { if !ok {
log.Debugf("no name in Metric %v", metric.Labels) log.Debugf("no name in Metric %v", metric.Labels)
} }
source, ok := metric.Labels["source"] source, ok := metric.Labels["source"]
if !ok { if !ok {
log.Debugf("no source in Metric %v for %s", metric.Labels, fam.Name) log.Debugf("no source in Metric %v for %s", metric.Labels, fam.Name)
@ -106,148 +160,89 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
origin := metric.Labels["origin"] origin := metric.Labels["origin"]
action := metric.Labels["action"] action := metric.Labels["action"]
appsecEngine := metric.Labels["appsec_engine"]
appsecRule := metric.Labels["rule_name"]
mtype := metric.Labels["type"] mtype := metric.Labels["type"]
fval, err := strconv.ParseFloat(value, 32) fval, err := strconv.ParseFloat(value, 32)
if err != nil { if err != nil {
log.Errorf("Unexpected int value %s : %s", value, err) log.Errorf("Unexpected int value %s : %s", value, err)
} }
ival := int(fval) ival := int(fval)
switch fam.Name { switch fam.Name {
/*buckets*/ //
// buckets
//
case "cs_bucket_created_total": case "cs_bucket_created_total":
if _, ok := buckets_stats[name]; !ok { mBucket.Process(name, "instantiation", ival)
buckets_stats[name] = make(map[string]int)
}
buckets_stats[name]["instantiation"] += ival
case "cs_buckets": case "cs_buckets":
if _, ok := buckets_stats[name]; !ok { mBucket.Process(name, "curr_count", ival)
buckets_stats[name] = make(map[string]int)
}
buckets_stats[name]["curr_count"] += ival
case "cs_bucket_overflowed_total": case "cs_bucket_overflowed_total":
if _, ok := buckets_stats[name]; !ok { mBucket.Process(name, "overflow", ival)
buckets_stats[name] = make(map[string]int)
}
buckets_stats[name]["overflow"] += ival
case "cs_bucket_poured_total": case "cs_bucket_poured_total":
if _, ok := buckets_stats[name]; !ok { mBucket.Process(name, "pour", ival)
buckets_stats[name] = make(map[string]int) mAcquis.Process(source, "pour", ival)
}
if _, ok := acquis_stats[source]; !ok {
acquis_stats[source] = make(map[string]int)
}
buckets_stats[name]["pour"] += ival
acquis_stats[source]["pour"] += ival
case "cs_bucket_underflowed_total": case "cs_bucket_underflowed_total":
if _, ok := buckets_stats[name]; !ok { mBucket.Process(name, "underflow", ival)
buckets_stats[name] = make(map[string]int) //
} // parsers
buckets_stats[name]["underflow"] += ival //
/*acquis*/
case "cs_parser_hits_total": case "cs_parser_hits_total":
if _, ok := acquis_stats[source]; !ok { mAcquis.Process(source, "reads", ival)
acquis_stats[source] = make(map[string]int)
}
acquis_stats[source]["reads"] += ival
case "cs_parser_hits_ok_total": case "cs_parser_hits_ok_total":
if _, ok := acquis_stats[source]; !ok { mAcquis.Process(source, "parsed", ival)
acquis_stats[source] = make(map[string]int)
}
acquis_stats[source]["parsed"] += ival
case "cs_parser_hits_ko_total": case "cs_parser_hits_ko_total":
if _, ok := acquis_stats[source]; !ok { mAcquis.Process(source, "unparsed", ival)
acquis_stats[source] = make(map[string]int)
}
acquis_stats[source]["unparsed"] += ival
case "cs_node_hits_total": case "cs_node_hits_total":
if _, ok := parsers_stats[name]; !ok { mParser.Process(name, "hits", ival)
parsers_stats[name] = make(map[string]int)
}
parsers_stats[name]["hits"] += ival
case "cs_node_hits_ok_total": case "cs_node_hits_ok_total":
if _, ok := parsers_stats[name]; !ok { mParser.Process(name, "parsed", ival)
parsers_stats[name] = make(map[string]int)
}
parsers_stats[name]["parsed"] += ival
case "cs_node_hits_ko_total": case "cs_node_hits_ko_total":
if _, ok := parsers_stats[name]; !ok { mParser.Process(name, "unparsed", ival)
parsers_stats[name] = make(map[string]int) //
} // whitelists
parsers_stats[name]["unparsed"] += ival //
case "cs_node_wl_hits_total":
mWhitelist.Process(name, reason, "hits", ival)
case "cs_node_wl_hits_ok_total":
mWhitelist.Process(name, reason, "whitelisted", ival)
// track as well whitelisted lines at acquis level
mAcquis.Process(source, "whitelisted", ival)
//
// lapi
//
case "cs_lapi_route_requests_total": case "cs_lapi_route_requests_total":
if _, ok := lapi_stats[route]; !ok { mLapi.Process(route, method, ival)
lapi_stats[route] = make(map[string]int)
}
lapi_stats[route][method] += ival
case "cs_lapi_machine_requests_total": case "cs_lapi_machine_requests_total":
if _, ok := lapi_machine_stats[machine]; !ok { mLapiMachine.Process(machine, route, method, ival)
lapi_machine_stats[machine] = make(map[string]map[string]int)
}
if _, ok := lapi_machine_stats[machine][route]; !ok {
lapi_machine_stats[machine][route] = make(map[string]int)
}
lapi_machine_stats[machine][route][method] += ival
case "cs_lapi_bouncer_requests_total": case "cs_lapi_bouncer_requests_total":
if _, ok := lapi_bouncer_stats[bouncer]; !ok { mLapiBouncer.Process(bouncer, route, method, ival)
lapi_bouncer_stats[bouncer] = make(map[string]map[string]int)
}
if _, ok := lapi_bouncer_stats[bouncer][route]; !ok {
lapi_bouncer_stats[bouncer][route] = make(map[string]int)
}
lapi_bouncer_stats[bouncer][route][method] += ival
case "cs_lapi_decisions_ko_total", "cs_lapi_decisions_ok_total": case "cs_lapi_decisions_ko_total", "cs_lapi_decisions_ok_total":
if _, ok := lapi_decisions_stats[bouncer]; !ok { mLapiDecision.Process(bouncer, fam.Name, ival)
lapi_decisions_stats[bouncer] = struct { //
NonEmpty int // decisions
Empty int //
}{}
}
x := lapi_decisions_stats[bouncer]
if fam.Name == "cs_lapi_decisions_ko_total" {
x.Empty += ival
} else if fam.Name == "cs_lapi_decisions_ok_total" {
x.NonEmpty += ival
}
lapi_decisions_stats[bouncer] = x
case "cs_active_decisions": case "cs_active_decisions":
if _, ok := decisions_stats[reason]; !ok { mDecision.Process(reason, origin, action, ival)
decisions_stats[reason] = make(map[string]map[string]int)
}
if _, ok := decisions_stats[reason][origin]; !ok {
decisions_stats[reason][origin] = make(map[string]int)
}
decisions_stats[reason][origin][action] += ival
case "cs_alerts": case "cs_alerts":
/*if _, ok := alerts_stats[scenario]; !ok { mAlert.Process(reason, ival)
alerts_stats[scenario] = make(map[string]int) //
}*/ // stash
alerts_stats[reason] += ival //
case "cs_cache_size": case "cs_cache_size":
stash_stats[name] = struct { mStash.Process(name, mtype, ival)
Type string //
Count int // appsec
}{Type: mtype, Count: ival} //
case "cs_appsec_reqs_total": case "cs_appsec_reqs_total":
if _, ok := appsec_engine_stats[metric.Labels["appsec_engine"]]; !ok { mAppsecEngine.Process(appsecEngine, "processed", ival)
appsec_engine_stats[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
}
appsec_engine_stats[metric.Labels["appsec_engine"]]["processed"] = ival
case "cs_appsec_block_total": case "cs_appsec_block_total":
if _, ok := appsec_engine_stats[metric.Labels["appsec_engine"]]; !ok { mAppsecEngine.Process(appsecEngine, "blocked", ival)
appsec_engine_stats[metric.Labels["appsec_engine"]] = make(map[string]int, 0)
}
appsec_engine_stats[metric.Labels["appsec_engine"]]["blocked"] = ival
case "cs_appsec_rule_hits": case "cs_appsec_rule_hits":
appsecEngine := metric.Labels["appsec_engine"] mAppsecRule.Process(appsecEngine, appsecRule, "triggered", ival)
ruleID := metric.Labels["rule_name"]
if _, ok := appsec_rule_stats[appsecEngine]; !ok {
appsec_rule_stats[appsecEngine] = make(map[string]map[string]int, 0)
}
if _, ok := appsec_rule_stats[appsecEngine][ruleID]; !ok {
appsec_rule_stats[appsecEngine][ruleID] = make(map[string]int, 0)
}
appsec_rule_stats[appsecEngine][ruleID]["triggered"] = ival
default: default:
log.Debugf("unknown: %+v", fam.Name) log.Debugf("unknown: %+v", fam.Name)
continue continue
@ -255,46 +250,50 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
} }
} }
if formatType == "human" { return nil
acquisStatsTable(out, acquis_stats) }
bucketStatsTable(out, buckets_stats)
parserStatsTable(out, parsers_stats) type cliMetrics struct {
lapiStatsTable(out, lapi_stats) cfg configGetter
lapiMachineStatsTable(out, lapi_machine_stats) }
lapiBouncerStatsTable(out, lapi_bouncer_stats)
lapiDecisionStatsTable(out, lapi_decisions_stats) func NewCLIMetrics(cfg configGetter) *cliMetrics {
decisionStatsTable(out, decisions_stats) return &cliMetrics{
alertStatsTable(out, alerts_stats) cfg: cfg,
stashStatsTable(out, stash_stats) }
appsecMetricsToTable(out, appsec_engine_stats) }
appsecRulesToTable(out, appsec_rule_stats)
return nil func (ms metricStore) Format(out io.Writer, sections []string, formatType string, noUnit bool) error {
// copy only the sections we want
want := map[string]metricSection{}
// if explicitly asking for sections, we want to show empty tables
showEmpty := len(sections) > 0
// if no sections are specified, we want all of them
if len(sections) == 0 {
sections = maptools.SortedKeys(ms)
} }
stats := make(map[string]any) for _, section := range sections {
want[section] = ms[section]
stats["acquisition"] = acquis_stats }
stats["buckets"] = buckets_stats
stats["parsers"] = parsers_stats
stats["lapi"] = lapi_stats
stats["lapi_machine"] = lapi_machine_stats
stats["lapi_bouncer"] = lapi_bouncer_stats
stats["lapi_decisions"] = lapi_decisions_stats
stats["decisions"] = decisions_stats
stats["alerts"] = alerts_stats
stats["stash"] = stash_stats
switch formatType { switch formatType {
case "human":
for _, section := range maptools.SortedKeys(want) {
want[section].Table(out, noUnit, showEmpty)
}
case "json": case "json":
x, err := json.MarshalIndent(stats, "", " ") x, err := json.MarshalIndent(want, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshal metrics : %v", err) return fmt.Errorf("failed to marshal metrics: %w", err)
} }
out.Write(x) out.Write(x)
case "raw": case "raw":
x, err := yaml.Marshal(stats) x, err := yaml.Marshal(want)
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshal metrics : %v", err) return fmt.Errorf("failed to marshal metrics: %w", err)
} }
out.Write(x) out.Write(x)
default: default:
@ -304,52 +303,195 @@ func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error
return nil return nil
} }
var noUnit bool func (cli *cliMetrics) show(sections []string, url string, noUnit bool) error {
cfg := cli.cfg()
func runMetrics(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
url, err := flags.GetString("url")
if err != nil {
return err
}
if url != "" { if url != "" {
csConfig.Cscli.PrometheusUrl = url cfg.Cscli.PrometheusUrl = url
} }
noUnit, err = flags.GetBool("no-unit") if cfg.Prometheus == nil {
if err != nil { return ErrMissingConfig
}
if !cfg.Prometheus.Enabled {
return ErrMetricsDisabled
}
ms := NewMetricStore()
if err := ms.Fetch(cfg.Cscli.PrometheusUrl); err != nil {
return err return err
} }
if csConfig.Prometheus == nil { // any section that we don't have in the store is an error
return fmt.Errorf("prometheus section missing, can't show metrics") for _, section := range sections {
if _, ok := ms[section]; !ok {
return fmt.Errorf("unknown metrics type: %s", section)
}
} }
if !csConfig.Prometheus.Enabled { if err := ms.Format(color.Output, sections, cfg.Cscli.Output, noUnit); err != nil {
return fmt.Errorf("prometheus is not enabled, can't show metrics")
}
if err = FormatPrometheusMetrics(color.Output, csConfig.Cscli.PrometheusUrl, csConfig.Cscli.Output); err != nil {
return err return err
} }
return nil return nil
} }
func NewMetricsCmd() *cobra.Command { func (cli *cliMetrics) NewCommand() *cobra.Command {
cmdMetrics := &cobra.Command{ var (
Use: "metrics", url string
Short: "Display crowdsec prometheus metrics.", noUnit bool
Long: `Fetch metrics from the prometheus server and display them in a human-friendly way`, )
cmd := &cobra.Command{
Use: "metrics",
Short: "Display crowdsec prometheus metrics.",
Long: `Fetch metrics from a Local API server and display them`,
Example: `# Show all Metrics, skip empty tables (same as "cecli metrics show")
cscli metrics
# Show only some metrics, connect to a different url
cscli metrics --url http://lapi.local:6060/metrics show acquisition parsers
# List available metric types
cscli metrics list`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: runMetrics, RunE: func(_ *cobra.Command, _ []string) error {
return cli.show(nil, url, noUnit)
},
} }
flags := cmdMetrics.PersistentFlags() flags := cmd.Flags()
flags.StringP("url", "u", "", "Prometheus url (http://<ip>:<port>/metrics)") flags.StringVarP(&url, "url", "u", "", "Prometheus url (http://<ip>:<port>/metrics)")
flags.Bool("no-unit", false, "Show the real number instead of formatted with units") flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
return cmdMetrics cmd.AddCommand(cli.newShowCmd())
cmd.AddCommand(cli.newListCmd())
return cmd
}
// expandAlias returns a list of sections. The input can be a list of sections or alias.
func (cli *cliMetrics) expandAlias(args []string) []string {
ret := []string{}
for _, section := range args {
switch section {
case "engine":
ret = append(ret, "acquisition", "parsers", "scenarios", "stash", "whitelists")
case "lapi":
ret = append(ret, "alerts", "decisions", "lapi", "lapi-bouncer", "lapi-decisions", "lapi-machine")
case "appsec":
ret = append(ret, "appsec-engine", "appsec-rule")
default:
ret = append(ret, section)
}
}
return ret
}
func (cli *cliMetrics) newShowCmd() *cobra.Command {
var (
url string
noUnit bool
)
cmd := &cobra.Command{
Use: "show [type]...",
Short: "Display all or part of the available metrics.",
Long: `Fetch metrics from a Local API server and display them, optionally filtering on specific types.`,
Example: `# Show all Metrics, skip empty tables
cscli metrics show
# Use an alias: "engine", "lapi" or "appsec" to show a group of metrics
cscli metrics show engine
# Show some specific metrics, show empty tables, connect to a different url
cscli metrics show acquisition parsers scenarios stash --url http://lapi.local:6060/metrics
# To list available metric types, use "cscli metrics list"
cscli metrics list; cscli metrics list -o json
# Show metrics in json format
cscli metrics show acquisition parsers scenarios stash -o json`,
// Positional args are optional
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
args = cli.expandAlias(args)
return cli.show(args, url, noUnit)
},
}
flags := cmd.Flags()
flags.StringVarP(&url, "url", "u", "", "Metrics url (http://<ip>:<port>/metrics)")
flags.BoolVar(&noUnit, "no-unit", false, "Show the real number instead of formatted with units")
return cmd
}
func (cli *cliMetrics) list() error {
type metricType struct {
Type string `json:"type" yaml:"type"`
Title string `json:"title" yaml:"title"`
Description string `json:"description" yaml:"description"`
}
var allMetrics []metricType
ms := NewMetricStore()
for _, section := range maptools.SortedKeys(ms) {
title, description := ms[section].Description()
allMetrics = append(allMetrics, metricType{
Type: section,
Title: title,
Description: description,
})
}
switch cli.cfg().Cscli.Output {
case "human":
t := newTable(color.Output)
t.SetRowLines(true)
t.SetHeaders("Type", "Title", "Description")
for _, metric := range allMetrics {
t.AddRow(metric.Type, metric.Title, metric.Description)
}
t.Render()
case "json":
x, err := json.MarshalIndent(allMetrics, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal metric types: %w", err)
}
fmt.Println(string(x))
case "raw":
x, err := yaml.Marshal(allMetrics)
if err != nil {
return fmt.Errorf("failed to marshal metric types: %w", err)
}
fmt.Println(string(x))
}
return nil
}
func (cli *cliMetrics) newListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List available types of metrics.",
Long: `List available types of metrics.`,
Args: cobra.ExactArgs(0),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.list()
},
}
return cmd
} }

View file

@ -1,25 +1,33 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"sort" "sort"
"strconv"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/maptools"
) )
// ErrNilTable means a nil pointer was passed instead of a table instance. This is a programming error.
var ErrNilTable = errors.New("nil table")
func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int { func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int {
// stats: machine -> route -> method -> count // stats: machine -> route -> method -> count
// sort keys to keep consistent order when printing // sort keys to keep consistent order when printing
machineKeys := []string{} machineKeys := []string{}
for k := range stats { for k := range stats {
machineKeys = append(machineKeys, k) machineKeys = append(machineKeys, k)
} }
sort.Strings(machineKeys) sort.Strings(machineKeys)
numRows := 0 numRows := 0
for _, machine := range machineKeys { for _, machine := range machineKeys {
// oneRow: route -> method -> count // oneRow: route -> method -> count
machineRow := stats[machine] machineRow := stats[machine]
@ -31,41 +39,79 @@ func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]i
methodName, methodName,
} }
if count != 0 { if count != 0 {
row = append(row, fmt.Sprintf("%d", count)) row = append(row, strconv.Itoa(count))
} else { } else {
row = append(row, "-") row = append(row, "-")
} }
t.AddRow(row...) t.AddRow(row...)
numRows++ numRows++
} }
} }
} }
return numRows return numRows
} }
func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []string) (int, error) { func wlMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int, noUnit bool) (int, error) {
if t == nil { if t == nil {
return 0, fmt.Errorf("nil table") return 0, ErrNilTable
} }
// sort keys to keep consistent order when printing
sortedKeys := []string{}
for k := range stats {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
numRows := 0 numRows := 0
for _, alabel := range sortedKeys {
for _, name := range maptools.SortedKeys(stats) {
for _, reason := range maptools.SortedKeys(stats[name]) {
row := []string{
name,
reason,
"-",
"-",
}
for _, action := range maptools.SortedKeys(stats[name][reason]) {
value := stats[name][reason][action]
switch action {
case "whitelisted":
row[3] = strconv.Itoa(value)
case "hits":
row[2] = strconv.Itoa(value)
default:
log.Debugf("unexpected counter '%s' for whitelists = %d", action, value)
}
}
t.AddRow(row...)
numRows++
}
}
return numRows, nil
}
func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []string, noUnit bool) (int, error) {
if t == nil {
return 0, ErrNilTable
}
numRows := 0
for _, alabel := range maptools.SortedKeys(stats) {
astats, ok := stats[alabel] astats, ok := stats[alabel]
if !ok { if !ok {
continue continue
} }
row := []string{ row := []string{
alabel, alabel,
} }
for _, sl := range keys { for _, sl := range keys {
if v, ok := astats[sl]; ok && v != 0 { if v, ok := astats[sl]; ok && v != 0 {
numberToShow := fmt.Sprintf("%d", v) numberToShow := strconv.Itoa(v)
if !noUnit { if !noUnit {
numberToShow = formatNumber(v) numberToShow = formatNumber(v)
} }
@ -75,76 +121,192 @@ func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []stri
row = append(row, "-") row = append(row, "-")
} }
} }
t.AddRow(row...) t.AddRow(row...)
numRows++ numRows++
} }
return numRows, nil return numRows, nil
} }
func bucketStatsTable(out io.Writer, stats map[string]map[string]int) { func (s statBucket) Description() (string, string) {
return "Scenario Metrics",
`Measure events in different scenarios. Current count is the number of buckets during metrics collection. ` +
`Overflows are past event-producing buckets, while Expired are the ones that didnt receive enough events to Overflow.`
}
func (s statBucket) Process(bucket, metric string, val int) {
if _, ok := s[bucket]; !ok {
s[bucket] = make(map[string]int)
}
s[bucket][metric] += val
}
func (s statBucket) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Bucket", "Current Count", "Overflows", "Instantiated", "Poured", "Expired") t.SetHeaders("Scenario", "Current Count", "Overflows", "Instantiated", "Poured", "Expired")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
keys := []string{"curr_count", "overflow", "instantiation", "pour", "underflow"} keys := []string{"curr_count", "overflow", "instantiation", "pour", "underflow"}
if numRows, err := metricsToTable(t, stats, keys); err != nil { if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
log.Warningf("while collecting bucket stats: %s", err) log.Warningf("while collecting scenario stats: %s", err)
} else if numRows > 0 { } else if numRows > 0 || showEmpty {
renderTableTitle(out, "\nBucket Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func acquisStatsTable(out io.Writer, stats map[string]map[string]int) { func (s statAcquis) Description() (string, string) {
return "Acquisition Metrics",
`Measures the lines read, parsed, and unparsed per datasource. ` +
`Zero read lines indicate a misconfigured or inactive datasource. ` +
`Zero parsed lines mean the parser(s) failed. ` +
`Non-zero parsed lines are fine as crowdsec selects relevant lines.`
}
func (s statAcquis) Process(source, metric string, val int) {
if _, ok := s[source]; !ok {
s[source] = make(map[string]int)
}
s[source][metric] += val
}
func (s statAcquis) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket") t.SetHeaders("Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket", "Lines whitelisted")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
keys := []string{"reads", "parsed", "unparsed", "pour"} keys := []string{"reads", "parsed", "unparsed", "pour", "whitelisted"}
if numRows, err := metricsToTable(t, stats, keys); err != nil { if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
log.Warningf("while collecting acquis stats: %s", err) log.Warningf("while collecting acquis stats: %s", err)
} else if numRows > 0 { } else if numRows > 0 || showEmpty {
renderTableTitle(out, "\nAcquisition Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func appsecMetricsToTable(out io.Writer, metrics map[string]map[string]int) { func (s statAppsecEngine) Description() (string, string) {
return "Appsec Metrics",
`Measures the number of parsed and blocked requests by the AppSec Component.`
}
func (s statAppsecEngine) Process(appsecEngine, metric string, val int) {
if _, ok := s[appsecEngine]; !ok {
s[appsecEngine] = make(map[string]int)
}
s[appsecEngine][metric] += val
}
func (s statAppsecEngine) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Appsec Engine", "Processed", "Blocked") t.SetHeaders("Appsec Engine", "Processed", "Blocked")
t.SetAlignment(table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft)
keys := []string{"processed", "blocked"} keys := []string{"processed", "blocked"}
if numRows, err := metricsToTable(t, metrics, keys); err != nil {
if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
log.Warningf("while collecting appsec stats: %s", err) log.Warningf("while collecting appsec stats: %s", err)
} else if numRows > 0 { } else if numRows > 0 || showEmpty {
renderTableTitle(out, "\nAppsec Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func appsecRulesToTable(out io.Writer, metrics map[string]map[string]map[string]int) { func (s statAppsecRule) Description() (string, string) {
for appsecEngine, appsecEngineRulesStats := range metrics { return "Appsec Rule Metrics",
`Provides “per AppSec Component” information about the number of matches for loaded AppSec Rules.`
}
func (s statAppsecRule) Process(appsecEngine, appsecRule string, metric string, val int) {
if _, ok := s[appsecEngine]; !ok {
s[appsecEngine] = make(map[string]map[string]int)
}
if _, ok := s[appsecEngine][appsecRule]; !ok {
s[appsecEngine][appsecRule] = make(map[string]int)
}
s[appsecEngine][appsecRule][metric] += val
}
func (s statAppsecRule) Table(out io.Writer, noUnit bool, showEmpty bool) {
for appsecEngine, appsecEngineRulesStats := range s {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Rule ID", "Triggered") t.SetHeaders("Rule ID", "Triggered")
t.SetAlignment(table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft)
keys := []string{"triggered"} keys := []string{"triggered"}
if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys); err != nil {
if numRows, err := metricsToTable(t, appsecEngineRulesStats, keys, noUnit); err != nil {
log.Warningf("while collecting appsec rules stats: %s", err) log.Warningf("while collecting appsec rules stats: %s", err)
} else if numRows > 0 { } else if numRows > 0 || showEmpty {
renderTableTitle(out, fmt.Sprintf("\nAppsec '%s' Rules Metrics:", appsecEngine)) renderTableTitle(out, fmt.Sprintf("\nAppsec '%s' Rules Metrics:", appsecEngine))
t.Render() t.Render()
} }
} }
} }
func parserStatsTable(out io.Writer, stats map[string]map[string]int) { func (s statWhitelist) Description() (string, string) {
return "Whitelist Metrics",
`Tracks the number of events processed and possibly whitelisted by each parser whitelist.`
}
func (s statWhitelist) Process(whitelist, reason, metric string, val int) {
if _, ok := s[whitelist]; !ok {
s[whitelist] = make(map[string]map[string]int)
}
if _, ok := s[whitelist][reason]; !ok {
s[whitelist][reason] = make(map[string]int)
}
s[whitelist][reason][metric] += val
}
func (s statWhitelist) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out)
t.SetRowLines(false)
t.SetHeaders("Whitelist", "Reason", "Hits", "Whitelisted")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
if numRows, err := wlMetricsToTable(t, s, noUnit); err != nil {
log.Warningf("while collecting parsers stats: %s", err)
} else if numRows > 0 || showEmpty {
title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render()
}
}
func (s statParser) Description() (string, string) {
return "Parser Metrics",
`Tracks the number of events processed by each parser and indicates success of failure. ` +
`Zero parsed lines means the parer(s) failed. ` +
`Non-zero unparsed lines are fine as crowdsec select relevant lines.`
}
func (s statParser) Process(parser, metric string, val int) {
if _, ok := s[parser]; !ok {
s[parser] = make(map[string]int)
}
s[parser][metric] += val
}
func (s statParser) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed") t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed")
@ -152,187 +314,302 @@ func parserStatsTable(out io.Writer, stats map[string]map[string]int) {
keys := []string{"hits", "parsed", "unparsed"} keys := []string{"hits", "parsed", "unparsed"}
if numRows, err := metricsToTable(t, stats, keys); err != nil { if numRows, err := metricsToTable(t, s, keys, noUnit); err != nil {
log.Warningf("while collecting parsers stats: %s", err) log.Warningf("while collecting parsers stats: %s", err)
} else if numRows > 0 { } else if numRows > 0 || showEmpty {
renderTableTitle(out, "\nParser Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func stashStatsTable(out io.Writer, stats map[string]struct { func (s statStash) Description() (string, string) {
Type string return "Parser Stash Metrics",
Count int `Tracks the status of stashes that might be created by various parsers and scenarios.`
}) { }
func (s statStash) Process(name, mtype string, val int) {
s[name] = struct {
Type string
Count int
}{
Type: mtype,
Count: val,
}
}
func (s statStash) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Name", "Type", "Items") t.SetHeaders("Name", "Type", "Items")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
// unfortunately, we can't reuse metricsToTable as the structure is too different :/ // unfortunately, we can't reuse metricsToTable as the structure is too different :/
sortedKeys := []string{}
for k := range stats {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
numRows := 0 numRows := 0
for _, alabel := range sortedKeys {
astats := stats[alabel] for _, alabel := range maptools.SortedKeys(s) {
astats := s[alabel]
row := []string{ row := []string{
alabel, alabel,
astats.Type, astats.Type,
fmt.Sprintf("%d", astats.Count), strconv.Itoa(astats.Count),
} }
t.AddRow(row...) t.AddRow(row...)
numRows++ numRows++
} }
if numRows > 0 {
renderTableTitle(out, "\nParser Stash Metrics:") if numRows > 0 || showEmpty {
title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func lapiStatsTable(out io.Writer, stats map[string]map[string]int) { func (s statLapi) Description() (string, string) {
return "Local API Metrics",
`Monitors the requests made to local API routes.`
}
func (s statLapi) Process(route, method string, val int) {
if _, ok := s[route]; !ok {
s[route] = make(map[string]int)
}
s[route][method] += val
}
func (s statLapi) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Route", "Method", "Hits") t.SetHeaders("Route", "Method", "Hits")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
// unfortunately, we can't reuse metricsToTable as the structure is too different :/ // unfortunately, we can't reuse metricsToTable as the structure is too different :/
sortedKeys := []string{}
for k := range stats {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
numRows := 0 numRows := 0
for _, alabel := range sortedKeys {
astats := stats[alabel] for _, alabel := range maptools.SortedKeys(s) {
astats := s[alabel]
subKeys := []string{} subKeys := []string{}
for skey := range astats { for skey := range astats {
subKeys = append(subKeys, skey) subKeys = append(subKeys, skey)
} }
sort.Strings(subKeys) sort.Strings(subKeys)
for _, sl := range subKeys { for _, sl := range subKeys {
row := []string{ row := []string{
alabel, alabel,
sl, sl,
fmt.Sprintf("%d", astats[sl]), strconv.Itoa(astats[sl]),
} }
t.AddRow(row...) t.AddRow(row...)
numRows++ numRows++
} }
} }
if numRows > 0 { if numRows > 0 || showEmpty {
renderTableTitle(out, "\nLocal API Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func lapiMachineStatsTable(out io.Writer, stats map[string]map[string]map[string]int) { func (s statLapiMachine) Description() (string, string) {
return "Local API Machines Metrics",
`Tracks the number of calls to the local API from each registered machine.`
}
func (s statLapiMachine) Process(machine, route, method string, val int) {
if _, ok := s[machine]; !ok {
s[machine] = make(map[string]map[string]int)
}
if _, ok := s[machine][route]; !ok {
s[machine][route] = make(map[string]int)
}
s[machine][route][method] += val
}
func (s statLapiMachine) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Machine", "Route", "Method", "Hits") t.SetHeaders("Machine", "Route", "Method", "Hits")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
numRows := lapiMetricsToTable(t, stats) numRows := lapiMetricsToTable(t, s)
if numRows > 0 { if numRows > 0 || showEmpty {
renderTableTitle(out, "\nLocal API Machines Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func lapiBouncerStatsTable(out io.Writer, stats map[string]map[string]map[string]int) { func (s statLapiBouncer) Description() (string, string) {
return "Local API Bouncers Metrics",
`Tracks total hits to remediation component related API routes.`
}
func (s statLapiBouncer) Process(bouncer, route, method string, val int) {
if _, ok := s[bouncer]; !ok {
s[bouncer] = make(map[string]map[string]int)
}
if _, ok := s[bouncer][route]; !ok {
s[bouncer][route] = make(map[string]int)
}
s[bouncer][route][method] += val
}
func (s statLapiBouncer) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Bouncer", "Route", "Method", "Hits") t.SetHeaders("Bouncer", "Route", "Method", "Hits")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
numRows := lapiMetricsToTable(t, stats) numRows := lapiMetricsToTable(t, s)
if numRows > 0 { if numRows > 0 || showEmpty {
renderTableTitle(out, "\nLocal API Bouncers Metrics:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func lapiDecisionStatsTable(out io.Writer, stats map[string]struct { func (s statLapiDecision) Description() (string, string) {
NonEmpty int return "Local API Bouncers Decisions",
Empty int `Tracks the number of empty/non-empty answers from LAPI to bouncers that are working in "live" mode.`
}, }
) {
func (s statLapiDecision) Process(bouncer, fam string, val int) {
if _, ok := s[bouncer]; !ok {
s[bouncer] = struct {
NonEmpty int
Empty int
}{}
}
x := s[bouncer]
switch fam {
case "cs_lapi_decisions_ko_total":
x.Empty += val
case "cs_lapi_decisions_ok_total":
x.NonEmpty += val
}
s[bouncer] = x
}
func (s statLapiDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Bouncer", "Empty answers", "Non-empty answers") t.SetHeaders("Bouncer", "Empty answers", "Non-empty answers")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
numRows := 0 numRows := 0
for bouncer, hits := range stats {
for bouncer, hits := range s {
t.AddRow( t.AddRow(
bouncer, bouncer,
fmt.Sprintf("%d", hits.Empty), strconv.Itoa(hits.Empty),
fmt.Sprintf("%d", hits.NonEmpty), strconv.Itoa(hits.NonEmpty),
) )
numRows++ numRows++
} }
if numRows > 0 { if numRows > 0 || showEmpty {
renderTableTitle(out, "\nLocal API Bouncers Decisions:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func decisionStatsTable(out io.Writer, stats map[string]map[string]map[string]int) { func (s statDecision) Description() (string, string) {
return "Local API Decisions",
`Provides information about all currently active decisions. ` +
`Includes both local (crowdsec) and global decisions (CAPI), and lists subscriptions (lists).`
}
func (s statDecision) Process(reason, origin, action string, val int) {
if _, ok := s[reason]; !ok {
s[reason] = make(map[string]map[string]int)
}
if _, ok := s[reason][origin]; !ok {
s[reason][origin] = make(map[string]int)
}
s[reason][origin][action] += val
}
func (s statDecision) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Reason", "Origin", "Action", "Count") t.SetHeaders("Reason", "Origin", "Action", "Count")
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
numRows := 0 numRows := 0
for reason, origins := range stats {
for reason, origins := range s {
for origin, actions := range origins { for origin, actions := range origins {
for action, hits := range actions { for action, hits := range actions {
t.AddRow( t.AddRow(
reason, reason,
origin, origin,
action, action,
fmt.Sprintf("%d", hits), strconv.Itoa(hits),
) )
numRows++ numRows++
} }
} }
} }
if numRows > 0 { if numRows > 0 || showEmpty {
renderTableTitle(out, "\nLocal API Decisions:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }
func alertStatsTable(out io.Writer, stats map[string]int) { func (s statAlert) Description() (string, string) {
return "Local API Alerts",
`Tracks the total number of past and present alerts for the installed scenarios.`
}
func (s statAlert) Process(reason string, val int) {
s[reason] += val
}
func (s statAlert) Table(out io.Writer, noUnit bool, showEmpty bool) {
t := newTable(out) t := newTable(out)
t.SetRowLines(false) t.SetRowLines(false)
t.SetHeaders("Reason", "Count") t.SetHeaders("Reason", "Count")
t.SetAlignment(table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft)
numRows := 0 numRows := 0
for scenario, hits := range stats {
for scenario, hits := range s {
t.AddRow( t.AddRow(
scenario, scenario,
fmt.Sprintf("%d", hits), strconv.Itoa(hits),
) )
numRows++ numRows++
} }
if numRows > 0 { if numRows > 0 || showEmpty {
renderTableTitle(out, "\nLocal API Alerts:") title, _ := s.Description()
renderTableTitle(out, "\n"+title+":")
t.Render() t.Render()
} }
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net/url" "net/url"
@ -23,14 +24,13 @@ import (
"github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/csprofiles" "github.com/crowdsecurity/crowdsec/pkg/csprofiles"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
) )
type NotificationsCfg struct { type NotificationsCfg struct {
@ -39,13 +39,17 @@ type NotificationsCfg struct {
ids []uint ids []uint
} }
type cliNotifications struct{} type cliNotifications struct {
cfg configGetter
func NewCLINotifications() *cliNotifications {
return &cliNotifications{}
} }
func (cli cliNotifications) NewCommand() *cobra.Command { func NewCLINotifications(cfg configGetter) *cliNotifications {
return &cliNotifications{
cfg: cfg,
}
}
func (cli *cliNotifications) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "notifications [action]", Use: "notifications [action]",
Short: "Helper for notification plugin configuration", Short: "Helper for notification plugin configuration",
@ -53,14 +57,15 @@ func (cli cliNotifications) NewCommand() *cobra.Command {
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Aliases: []string{"notifications", "notification"}, Aliases: []string{"notifications", "notification"},
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := require.LAPI(csConfig); err != nil { cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err return err
} }
if err := csConfig.LoadAPIClient(); err != nil { if err := cfg.LoadAPIClient(); err != nil {
return fmt.Errorf("loading api client: %w", err) return fmt.Errorf("loading api client: %w", err)
} }
if err := require.Notifications(csConfig); err != nil { if err := require.Notifications(cfg); err != nil {
return err return err
} }
@ -76,67 +81,79 @@ func (cli cliNotifications) NewCommand() *cobra.Command {
return cmd return cmd
} }
func getPluginConfigs() (map[string]csplugin.PluginConfig, error) { func (cli *cliNotifications) getPluginConfigs() (map[string]csplugin.PluginConfig, error) {
cfg := cli.cfg()
pcfgs := map[string]csplugin.PluginConfig{} pcfgs := map[string]csplugin.PluginConfig{}
wf := func(path string, info fs.FileInfo, err error) error { wf := func(path string, info fs.FileInfo, err error) error {
if info == nil { if info == nil {
return fmt.Errorf("error while traversing directory %s: %w", path, err) return fmt.Errorf("error while traversing directory %s: %w", path, err)
} }
name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
name := filepath.Join(cfg.ConfigPaths.NotificationDir, info.Name()) // Avoid calling info.Name() twice
if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) { if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
ts, err := csplugin.ParsePluginConfigFile(name) ts, err := csplugin.ParsePluginConfigFile(name)
if err != nil { if err != nil {
return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err) return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err)
} }
for _, t := range ts { for _, t := range ts {
csplugin.SetRequiredFields(&t) csplugin.SetRequiredFields(&t)
pcfgs[t.Name] = t pcfgs[t.Name] = t
} }
} }
return nil return nil
} }
if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil { if err := filepath.Walk(cfg.ConfigPaths.NotificationDir, wf); err != nil {
return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err) return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err)
} }
return pcfgs, nil return pcfgs, nil
} }
func getProfilesConfigs() (map[string]NotificationsCfg, error) { func (cli *cliNotifications) getProfilesConfigs() (map[string]NotificationsCfg, error) {
cfg := cli.cfg()
// A bit of a tricky stuf now: reconcile profiles and notification plugins // A bit of a tricky stuf now: reconcile profiles and notification plugins
pcfgs, err := getPluginConfigs() pcfgs, err := cli.getPluginConfigs()
if err != nil { if err != nil {
return nil, err return nil, err
} }
ncfgs := map[string]NotificationsCfg{} ncfgs := map[string]NotificationsCfg{}
for _, pc := range pcfgs { for _, pc := range pcfgs {
ncfgs[pc.Name] = NotificationsCfg{ ncfgs[pc.Name] = NotificationsCfg{
Config: pc, Config: pc,
} }
} }
profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
if err != nil { if err != nil {
return nil, fmt.Errorf("while extracting profiles from configuration: %w", err) return nil, fmt.Errorf("while extracting profiles from configuration: %w", err)
} }
for profileID, profile := range profiles { for profileID, profile := range profiles {
for _, notif := range profile.Cfg.Notifications { for _, notif := range profile.Cfg.Notifications {
pc, ok := pcfgs[notif] pc, ok := pcfgs[notif]
if !ok { if !ok {
return nil, fmt.Errorf("notification plugin '%s' does not exist", notif) return nil, fmt.Errorf("notification plugin '%s' does not exist", notif)
} }
tmp, ok := ncfgs[pc.Name] tmp, ok := ncfgs[pc.Name]
if !ok { if !ok {
return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name) return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name)
} }
tmp.Profiles = append(tmp.Profiles, profile.Cfg) tmp.Profiles = append(tmp.Profiles, profile.Cfg)
tmp.ids = append(tmp.ids, uint(profileID)) tmp.ids = append(tmp.ids, uint(profileID))
ncfgs[pc.Name] = tmp ncfgs[pc.Name] = tmp
} }
} }
return ncfgs, nil return ncfgs, nil
} }
func (cli cliNotifications) NewListCmd() *cobra.Command { func (cli *cliNotifications) NewListCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list active notifications plugins", Short: "list active notifications plugins",
@ -144,21 +161,22 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
Example: `cscli notifications list`, Example: `cscli notifications list`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, arg []string) error { RunE: func(_ *cobra.Command, _ []string) error {
ncfgs, err := getProfilesConfigs() cfg := cli.cfg()
ncfgs, err := cli.getProfilesConfigs()
if err != nil { if err != nil {
return fmt.Errorf("can't build profiles configuration: %w", err) return fmt.Errorf("can't build profiles configuration: %w", err)
} }
if csConfig.Cscli.Output == "human" { if cfg.Cscli.Output == "human" {
notificationListTable(color.Output, ncfgs) notificationListTable(color.Output, ncfgs)
} else if csConfig.Cscli.Output == "json" { } else if cfg.Cscli.Output == "json" {
x, err := json.MarshalIndent(ncfgs, "", " ") x, err := json.MarshalIndent(ncfgs, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal notification configuration: %w", err) return fmt.Errorf("failed to marshal notification configuration: %w", err)
} }
fmt.Printf("%s", string(x)) fmt.Printf("%s", string(x))
} else if csConfig.Cscli.Output == "raw" { } else if cfg.Cscli.Output == "raw" {
csvwriter := csv.NewWriter(os.Stdout) csvwriter := csv.NewWriter(os.Stdout)
err := csvwriter.Write([]string{"Name", "Type", "Profile name"}) err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
if err != nil { if err != nil {
@ -176,6 +194,7 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
} }
csvwriter.Flush() csvwriter.Flush()
} }
return nil return nil
}, },
} }
@ -183,7 +202,7 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliNotifications) NewInspectCmd() *cobra.Command { func (cli *cliNotifications) NewInspectCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "inspect", Use: "inspect",
Short: "Inspect active notifications plugin configuration", Short: "Inspect active notifications plugin configuration",
@ -191,36 +210,32 @@ func (cli cliNotifications) NewInspectCmd() *cobra.Command {
Example: `cscli notifications inspect <plugin_name>`, Example: `cscli notifications inspect <plugin_name>`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if args[0] == "" { cfg := cli.cfg()
return fmt.Errorf("please provide a plugin name to inspect") ncfgs, err := cli.getProfilesConfigs()
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ncfgs, err := getProfilesConfigs()
if err != nil { if err != nil {
return fmt.Errorf("can't build profiles configuration: %w", err) return fmt.Errorf("can't build profiles configuration: %w", err)
} }
cfg, ok := ncfgs[args[0]] ncfg, ok := ncfgs[args[0]]
if !ok { if !ok {
return fmt.Errorf("plugin '%s' does not exist or is not active", args[0]) return fmt.Errorf("plugin '%s' does not exist or is not active", args[0])
} }
if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" { if cfg.Cscli.Output == "human" || cfg.Cscli.Output == "raw" {
fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type) fmt.Printf(" - %15s: %15s\n", "Type", ncfg.Config.Type)
fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name) fmt.Printf(" - %15s: %15s\n", "Name", ncfg.Config.Name)
fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut) fmt.Printf(" - %15s: %15s\n", "Timeout", ncfg.Config.TimeOut)
fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format) fmt.Printf(" - %15s: %15s\n", "Format", ncfg.Config.Format)
for k, v := range cfg.Config.Config { for k, v := range ncfg.Config.Config {
fmt.Printf(" - %15s: %15v\n", k, v) fmt.Printf(" - %15s: %15v\n", k, v)
} }
} else if csConfig.Cscli.Output == "json" { } else if cfg.Cscli.Output == "json" {
x, err := json.MarshalIndent(cfg, "", " ") x, err := json.MarshalIndent(cfg, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal notification configuration: %w", err) return fmt.Errorf("failed to marshal notification configuration: %w", err)
} }
fmt.Printf("%s", string(x)) fmt.Printf("%s", string(x))
} }
return nil return nil
}, },
} }
@ -228,12 +243,13 @@ func (cli cliNotifications) NewInspectCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliNotifications) NewTestCmd() *cobra.Command { func (cli *cliNotifications) NewTestCmd() *cobra.Command {
var ( var (
pluginBroker csplugin.PluginBroker pluginBroker csplugin.PluginBroker
pluginTomb tomb.Tomb pluginTomb tomb.Tomb
alertOverride string alertOverride string
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test [plugin name]", Use: "test [plugin name]",
Short: "send a generic test alert to notification plugin", Short: "send a generic test alert to notification plugin",
@ -241,25 +257,26 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
Example: `cscli notifications test [plugin_name]`, Example: `cscli notifications test [plugin_name]`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
pconfigs, err := getPluginConfigs() cfg := cli.cfg()
pconfigs, err := cli.getPluginConfigs()
if err != nil { if err != nil {
return fmt.Errorf("can't build profiles configuration: %w", err) return fmt.Errorf("can't build profiles configuration: %w", err)
} }
cfg, ok := pconfigs[args[0]] pcfg, ok := pconfigs[args[0]]
if !ok { if !ok {
return fmt.Errorf("plugin name: '%s' does not exist", args[0]) return fmt.Errorf("plugin name: '%s' does not exist", args[0])
} }
//Create a single profile with plugin name as notification name // Create a single profile with plugin name as notification name
return pluginBroker.Init(csConfig.PluginConfig, []*csconfig.ProfileCfg{ return pluginBroker.Init(cfg.PluginConfig, []*csconfig.ProfileCfg{
{ {
Notifications: []string{ Notifications: []string{
cfg.Name, pcfg.Name,
}, },
}, },
}, csConfig.ConfigPaths) }, cfg.ConfigPaths)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
pluginTomb.Go(func() error { pluginTomb.Go(func() error {
pluginBroker.Run(&pluginTomb) pluginBroker.Run(&pluginTomb)
return nil return nil
@ -298,13 +315,16 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil { if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil {
return fmt.Errorf("failed to unmarshal alert override: %w", err) return fmt.Errorf("failed to unmarshal alert override: %w", err)
} }
pluginBroker.PluginChannel <- csplugin.ProfileAlert{ pluginBroker.PluginChannel <- csplugin.ProfileAlert{
ProfileID: uint(0), ProfileID: uint(0),
Alert: alert, Alert: alert,
} }
//time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
pluginTomb.Kill(fmt.Errorf("terminating")) // time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
pluginTomb.Kill(errors.New("terminating"))
pluginTomb.Wait() pluginTomb.Wait()
return nil return nil
}, },
} }
@ -313,9 +333,11 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliNotifications) NewReinjectCmd() *cobra.Command { func (cli *cliNotifications) NewReinjectCmd() *cobra.Command {
var alertOverride string var (
var alert *models.Alert alertOverride string
alert *models.Alert
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "reinject", Use: "reinject",
@ -328,25 +350,30 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
`, `,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
var err error var err error
alert, err = FetchAlertFromArgString(args[0]) alert, err = cli.fetchAlertFromArgString(args[0])
if err != nil { if err != nil {
return err return err
} }
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
var ( var (
pluginBroker csplugin.PluginBroker pluginBroker csplugin.PluginBroker
pluginTomb tomb.Tomb pluginTomb tomb.Tomb
) )
cfg := cli.cfg()
if alertOverride != "" { if alertOverride != "" {
if err := json.Unmarshal([]byte(alertOverride), alert); err != nil { if err := json.Unmarshal([]byte(alertOverride), alert); err != nil {
return fmt.Errorf("can't unmarshal data in the alert flag: %w", err) return fmt.Errorf("can't unmarshal data in the alert flag: %w", err)
} }
} }
err := pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths)
err := pluginBroker.Init(cfg.PluginConfig, cfg.API.Server.Profiles, cfg.ConfigPaths)
if err != nil { if err != nil {
return fmt.Errorf("can't initialize plugins: %w", err) return fmt.Errorf("can't initialize plugins: %w", err)
} }
@ -356,7 +383,7 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
return nil return nil
}) })
profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles) profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
if err != nil { if err != nil {
return fmt.Errorf("cannot extract profiles from configuration: %w", err) return fmt.Errorf("cannot extract profiles from configuration: %w", err)
} }
@ -382,17 +409,18 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
default: default:
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
log.Info("sleeping\n") log.Info("sleeping\n")
} }
} }
if profile.Cfg.OnSuccess == "break" { if profile.Cfg.OnSuccess == "break" {
log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name) log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
break break
} }
} }
//time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent // time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
pluginTomb.Kill(fmt.Errorf("terminating")) pluginTomb.Kill(errors.New("terminating"))
pluginTomb.Wait() pluginTomb.Wait()
return nil return nil
}, },
} }
@ -401,18 +429,22 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
return cmd return cmd
} }
func FetchAlertFromArgString(toParse string) (*models.Alert, error) { func (cli *cliNotifications) fetchAlertFromArgString(toParse string) (*models.Alert, error) {
cfg := cli.cfg()
id, err := strconv.Atoi(toParse) id, err := strconv.Atoi(toParse)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad alert id %s", toParse) return nil, fmt.Errorf("bad alert id %s", toParse)
} }
apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing the URL of the API: %w", err) return nil, fmt.Errorf("error parsing the URL of the API: %w", err)
} }
client, err := apiclient.NewClient(&apiclient.Config{ client, err := apiclient.NewClient(&apiclient.Config{
MachineID: csConfig.API.Client.Credentials.Login, MachineID: cfg.API.Client.Credentials.Login,
Password: strfmt.Password(csConfig.API.Client.Credentials.Password), Password: strfmt.Password(cfg.API.Client.Credentials.Password),
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiURL, URL: apiURL,
VersionPrefix: "v1", VersionPrefix: "v1",
@ -420,9 +452,11 @@ func FetchAlertFromArgString(toParse string) (*models.Alert, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating the client for the API: %w", err) return nil, fmt.Errorf("error creating the client for the API: %w", err)
} }
alert, _, err := client.Alerts.GetByID(context.Background(), id) alert, _, err := client.Alerts.GetByID(context.Background(), id)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't find alert with id %d: %w", id, err) return nil, fmt.Errorf("can't find alert with id %d: %w", id, err)
} }
return alert, nil return alert, nil
} }

View file

@ -6,7 +6,8 @@ import (
"strings" "strings"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
"github.com/enescakir/emoji"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
) )
func notificationListTable(out io.Writer, ncfgs map[string]NotificationsCfg) { func notificationListTable(out io.Writer, ncfgs map[string]NotificationsCfg) {
@ -14,24 +15,31 @@ func notificationListTable(out io.Writer, ncfgs map[string]NotificationsCfg) {
t.SetHeaders("Active", "Name", "Type", "Profile name") t.SetHeaders("Active", "Name", "Type", "Profile name")
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
keys := make([]string, 0, len(ncfgs)) keys := make([]string, 0, len(ncfgs))
for k := range ncfgs { for k := range ncfgs {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Slice(keys, func(i, j int) bool { sort.Slice(keys, func(i, j int) bool {
return len(ncfgs[keys[i]].Profiles) > len(ncfgs[keys[j]].Profiles) return len(ncfgs[keys[i]].Profiles) > len(ncfgs[keys[j]].Profiles)
}) })
for _, k := range keys { for _, k := range keys {
b := ncfgs[k] b := ncfgs[k]
profilesList := []string{} profilesList := []string{}
for _, p := range b.Profiles { for _, p := range b.Profiles {
profilesList = append(profilesList, p.Name) profilesList = append(profilesList, p.Name)
} }
active := emoji.CheckMark.String()
active := emoji.CheckMark
if len(profilesList) == 0 { if len(profilesList) == 0 {
active = emoji.Prohibited.String() active = emoji.Prohibited
} }
t.AddRow(active, b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")) t.AddRow(active, b.Config.Name, b.Config.Type, strings.Join(profilesList, ", "))
} }
t.Render() t.Render()
} }

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -9,34 +10,39 @@ import (
"github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiserver" "github.com/crowdsecurity/crowdsec/pkg/apiserver"
"github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
) )
type cliPapi struct {} type cliPapi struct {
cfg configGetter
func NewCLIPapi() *cliPapi {
return &cliPapi{}
} }
func (cli cliPapi) NewCommand() *cobra.Command { func NewCLIPapi(cfg configGetter) *cliPapi {
var cmd = &cobra.Command{ return &cliPapi{
cfg: cfg,
}
}
func (cli *cliPapi) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "papi [action]", Use: "papi [action]",
Short: "Manage interaction with Polling API (PAPI)", Short: "Manage interaction with Polling API (PAPI)",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := require.LAPI(csConfig); err != nil { cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err return err
} }
if err := require.CAPI(csConfig); err != nil { if err := require.CAPI(cfg); err != nil {
return err return err
} }
if err := require.PAPI(csConfig); err != nil { if err := require.PAPI(cfg); err != nil {
return err return err
} }
return nil return nil
}, },
} }
@ -47,35 +53,33 @@ func (cli cliPapi) NewCommand() *cobra.Command {
return cmd return cmd
} }
func (cli cliPapi) NewStatusCmd() *cobra.Command { func (cli *cliPapi) NewStatusCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "status", Use: "status",
Short: "Get status of the Polling API", Short: "Get status of the Polling API",
Args: cobra.MinimumNArgs(0), Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { RunE: func(_ *cobra.Command, _ []string) error {
var err error var err error
dbClient, err = database.NewClient(csConfig.DbConfig) cfg := cli.cfg()
dbClient, err = database.NewClient(cfg.DbConfig)
if err != nil { if err != nil {
log.Fatalf("unable to initialize database client : %s", err) return fmt.Errorf("unable to initialize database client: %w", err)
} }
apic, err := apiserver.NewAPIC(csConfig.API.Server.OnlineClient, dbClient, csConfig.API.Server.ConsoleConfig, csConfig.API.Server.CapiWhitelists) apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, dbClient, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
if err != nil { if err != nil {
log.Fatalf("unable to initialize API client : %s", err) return fmt.Errorf("unable to initialize API client: %w", err)
} }
papi, err := apiserver.NewPAPI(apic, dbClient, csConfig.API.Server.ConsoleConfig, log.GetLevel()) papi, err := apiserver.NewPAPI(apic, dbClient, cfg.API.Server.ConsoleConfig, log.GetLevel())
if err != nil { if err != nil {
log.Fatalf("unable to initialize PAPI client : %s", err) return fmt.Errorf("unable to initialize PAPI client: %w", err)
} }
perms, err := papi.GetPermissions() perms, err := papi.GetPermissions()
if err != nil { if err != nil {
log.Fatalf("unable to get PAPI permissions: %s", err) return fmt.Errorf("unable to get PAPI permissions: %w", err)
} }
var lastTimestampStr *string var lastTimestampStr *string
lastTimestampStr, err = dbClient.GetConfigItem(apiserver.PapiPullKey) lastTimestampStr, err = dbClient.GetConfigItem(apiserver.PapiPullKey)
@ -90,45 +94,47 @@ func (cli cliPapi) NewStatusCmd() *cobra.Command {
for _, sub := range perms.Categories { for _, sub := range perms.Categories {
log.Infof(" - %s", sub) log.Infof(" - %s", sub)
} }
return nil
}, },
} }
return cmd return cmd
} }
func (cli cliPapi) NewSyncCmd() *cobra.Command { func (cli *cliPapi) NewSyncCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "sync", Use: "sync",
Short: "Sync with the Polling API, pulling all non-expired orders for the instance", Short: "Sync with the Polling API, pulling all non-expired orders for the instance",
Args: cobra.MinimumNArgs(0), Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { RunE: func(_ *cobra.Command, _ []string) error {
var err error var err error
cfg := cli.cfg()
t := tomb.Tomb{} t := tomb.Tomb{}
dbClient, err = database.NewClient(csConfig.DbConfig)
dbClient, err = database.NewClient(cfg.DbConfig)
if err != nil { if err != nil {
log.Fatalf("unable to initialize database client : %s", err) return fmt.Errorf("unable to initialize database client: %w", err)
} }
apic, err := apiserver.NewAPIC(csConfig.API.Server.OnlineClient, dbClient, csConfig.API.Server.ConsoleConfig, csConfig.API.Server.CapiWhitelists) apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, dbClient, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
if err != nil { if err != nil {
log.Fatalf("unable to initialize API client : %s", err) return fmt.Errorf("unable to initialize API client: %w", err)
} }
t.Go(apic.Push) t.Go(apic.Push)
papi, err := apiserver.NewPAPI(apic, dbClient, csConfig.API.Server.ConsoleConfig, log.GetLevel()) papi, err := apiserver.NewPAPI(apic, dbClient, cfg.API.Server.ConsoleConfig, log.GetLevel())
if err != nil { if err != nil {
log.Fatalf("unable to initialize PAPI client : %s", err) return fmt.Errorf("unable to initialize PAPI client: %w", err)
} }
t.Go(papi.SyncDecisions) t.Go(papi.SyncDecisions)
err = papi.PullOnce(time.Time{}, true) err = papi.PullOnce(time.Time{}, true)
if err != nil { if err != nil {
log.Fatalf("unable to sync decisions: %s", err) return fmt.Errorf("unable to sync decisions: %w", err)
} }
log.Infof("Sending acknowledgements to CAPI") log.Infof("Sending acknowledgements to CAPI")
@ -138,6 +144,7 @@ func (cli cliPapi) NewSyncCmd() *cobra.Command {
t.Wait() t.Wait()
time.Sleep(5 * time.Second) //FIXME: the push done by apic.Push is run inside a sub goroutine, sleep to make sure it's done time.Sleep(5 * time.Second) //FIXME: the push done by apic.Push is run inside a sub goroutine, sleep to make sure it's done
return nil
}, },
} }

View file

@ -56,3 +56,7 @@ func HubBranch(cfg *csconfig.Config) string {
return branch return branch
} }
func HubURLTemplate(cfg *csconfig.Config) string {
return cfg.Cscli.HubURLTemplate
}

View file

@ -1,6 +1,7 @@
package require package require
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
@ -11,12 +12,12 @@ import (
) )
func LAPI(c *csconfig.Config) error { func LAPI(c *csconfig.Config) error {
if err := c.LoadAPIServer(); err != nil { if err := c.LoadAPIServer(true); err != nil {
return fmt.Errorf("failed to load Local API: %w", err) return fmt.Errorf("failed to load Local API: %w", err)
} }
if c.DisableAPI { if c.DisableAPI {
return fmt.Errorf("local API is disabled -- this command must be run on the local API machine") return errors.New("local API is disabled -- this command must be run on the local API machine")
} }
return nil return nil
@ -32,7 +33,7 @@ func CAPI(c *csconfig.Config) error {
func PAPI(c *csconfig.Config) error { func PAPI(c *csconfig.Config) error {
if c.API.Server.OnlineClient.Credentials.PapiURL == "" { if c.API.Server.OnlineClient.Credentials.PapiURL == "" {
return fmt.Errorf("no PAPI URL in configuration") return errors.New("no PAPI URL in configuration")
} }
return nil return nil
@ -40,14 +41,14 @@ func PAPI(c *csconfig.Config) error {
func CAPIRegistered(c *csconfig.Config) error { func CAPIRegistered(c *csconfig.Config) error {
if c.API.Server.OnlineClient.Credentials == nil { if c.API.Server.OnlineClient.Credentials == nil {
return fmt.Errorf("the Central API (CAPI) must be configured with 'cscli capi register'") return errors.New("the Central API (CAPI) must be configured with 'cscli capi register'")
} }
return nil return nil
} }
func DB(c *csconfig.Config) error { func DB(c *csconfig.Config) error {
if err := c.LoadDBConfig(); err != nil { if err := c.LoadDBConfig(true); err != nil {
return fmt.Errorf("this command requires direct database access (must be run on the local API machine): %w", err) return fmt.Errorf("this command requires direct database access (must be run on the local API machine): %w", err)
} }
@ -56,7 +57,7 @@ func DB(c *csconfig.Config) error {
func Notifications(c *csconfig.Config) error { func Notifications(c *csconfig.Config) error {
if c.ConfigPaths.NotificationDir == "" { if c.ConfigPaths.NotificationDir == "" {
return fmt.Errorf("config_paths.notification_dir is not set in crowdsec config") return errors.New("config_paths.notification_dir is not set in crowdsec config")
} }
return nil return nil
@ -66,10 +67,10 @@ func Notifications(c *csconfig.Config) error {
func RemoteHub(c *csconfig.Config) *cwhub.RemoteHubCfg { func RemoteHub(c *csconfig.Config) *cwhub.RemoteHubCfg {
// set branch in config, and log if necessary // set branch in config, and log if necessary
branch := HubBranch(c) branch := HubBranch(c)
urlTemplate := HubURLTemplate(c)
remote := &cwhub.RemoteHubCfg{ remote := &cwhub.RemoteHubCfg{
Branch: branch, Branch: branch,
URLTemplate: "https://hub-cdn.crowdsec.net/%s/%s", URLTemplate: urlTemplate,
// URLTemplate: "http://localhost:8000/crowdsecurity/%s/hub/%s",
IndexPath: ".index.json", IndexPath: ".index.json",
} }
@ -82,7 +83,7 @@ func Hub(c *csconfig.Config, remote *cwhub.RemoteHubCfg, logger *logrus.Logger)
local := c.Hub local := c.Hub
if local == nil { if local == nil {
return nil, fmt.Errorf("you must configure cli before interacting with hub") return nil, errors.New("you must configure cli before interacting with hub")
} }
if logger == nil { if logger == nil {

View file

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -118,9 +119,11 @@ func runSetupDetect(cmd *cobra.Command, args []string) error {
switch detectConfigFile { switch detectConfigFile {
case "-": case "-":
log.Tracef("Reading detection rules from stdin") log.Tracef("Reading detection rules from stdin")
detectReader = os.Stdin detectReader = os.Stdin
default: default:
log.Tracef("Reading detection rules: %s", detectConfigFile) log.Tracef("Reading detection rules: %s", detectConfigFile)
detectReader, err = os.Open(detectConfigFile) detectReader, err = os.Open(detectConfigFile)
if err != nil { if err != nil {
return err return err
@ -171,6 +174,7 @@ func runSetupDetect(cmd *cobra.Command, args []string) error {
_, err := exec.LookPath("systemctl") _, err := exec.LookPath("systemctl")
if err != nil { if err != nil {
log.Debug("systemctl not available: snubbing systemd") log.Debug("systemctl not available: snubbing systemd")
snubSystemd = true snubSystemd = true
} }
} }
@ -182,6 +186,7 @@ func runSetupDetect(cmd *cobra.Command, args []string) error {
if forcedOSFamily == "" && forcedOSID != "" { if forcedOSFamily == "" && forcedOSID != "" {
log.Debug("force-os-id is set: force-os-family defaults to 'linux'") log.Debug("force-os-id is set: force-os-family defaults to 'linux'")
forcedOSFamily = "linux" forcedOSFamily = "linux"
} }
@ -219,6 +224,7 @@ func runSetupDetect(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(setup) fmt.Println(setup)
return nil return nil
@ -318,6 +324,7 @@ func runSetupInstallHub(cmd *cobra.Command, args []string) error {
func runSetupValidate(cmd *cobra.Command, args []string) error { func runSetupValidate(cmd *cobra.Command, args []string) error {
fromFile := args[0] fromFile := args[0]
input, err := os.ReadFile(fromFile) input, err := os.ReadFile(fromFile)
if err != nil { if err != nil {
return fmt.Errorf("while reading stdin: %w", err) return fmt.Errorf("while reading stdin: %w", err)
@ -325,7 +332,7 @@ func runSetupValidate(cmd *cobra.Command, args []string) error {
if err = setup.Validate(input); err != nil { if err = setup.Validate(input); err != nil {
fmt.Printf("%v\n", err) fmt.Printf("%v\n", err)
return fmt.Errorf("invalid setup file") return errors.New("invalid setup file")
} }
return nil return nil

View file

@ -1,25 +1,30 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"slices"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"slices"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
type cliSimulation struct{} type cliSimulation struct {
cfg configGetter
func NewCLISimulation() *cliSimulation {
return &cliSimulation{}
} }
func (cli cliSimulation) NewCommand() *cobra.Command { func NewCLISimulation(cfg configGetter) *cliSimulation {
return &cliSimulation{
cfg: cfg,
}
}
func (cli *cliSimulation) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "simulation [command]", Use: "simulation [command]",
Short: "Manage simulation status of scenarios", Short: "Manage simulation status of scenarios",
@ -27,16 +32,17 @@ func (cli cliSimulation) NewCommand() *cobra.Command {
cscli simulation enable crowdsecurity/ssh-bf cscli simulation enable crowdsecurity/ssh-bf
cscli simulation disable crowdsecurity/ssh-bf`, cscli simulation disable crowdsecurity/ssh-bf`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := csConfig.LoadSimulation(); err != nil { if err := cli.cfg().LoadSimulation(); err != nil {
log.Fatal(err) return err
} }
if csConfig.Cscli.SimulationConfig == nil { if cli.cfg().Cscli.SimulationConfig == nil {
return fmt.Errorf("no simulation configured") return errors.New("no simulation configured")
} }
return nil return nil
}, },
PersistentPostRun: func(cmd *cobra.Command, args []string) { PersistentPostRun: func(cmd *cobra.Command, _ []string) {
if cmd.Name() != "status" { if cmd.Name() != "status" {
log.Infof(ReloadMessage()) log.Infof(ReloadMessage())
} }
@ -52,7 +58,7 @@ cscli simulation disable crowdsecurity/ssh-bf`,
return cmd return cmd
} }
func (cli cliSimulation) NewEnableCmd() *cobra.Command { func (cli *cliSimulation) NewEnableCmd() *cobra.Command {
var forceGlobalSimulation bool var forceGlobalSimulation bool
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -60,15 +66,15 @@ func (cli cliSimulation) NewEnableCmd() *cobra.Command {
Short: "Enable the simulation, globally or on specified scenarios", Short: "Enable the simulation, globally or on specified scenarios",
Example: `cscli simulation enable`, Example: `cscli simulation enable`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
hub, err := require.Hub(csConfig, nil, nil) hub, err := require.Hub(cli.cfg(), nil, nil)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
if len(args) > 0 { if len(args) > 0 {
for _, scenario := range args { for _, scenario := range args {
var item = hub.GetItem(cwhub.SCENARIOS, scenario) item := hub.GetItem(cwhub.SCENARIOS, scenario)
if item == nil { if item == nil {
log.Errorf("'%s' doesn't exist or is not a scenario", scenario) log.Errorf("'%s' doesn't exist or is not a scenario", scenario)
continue continue
@ -76,37 +82,35 @@ func (cli cliSimulation) NewEnableCmd() *cobra.Command {
if !item.State.Installed { if !item.State.Installed {
log.Warningf("'%s' isn't enabled", scenario) log.Warningf("'%s' isn't enabled", scenario)
} }
isExcluded := slices.Contains(csConfig.Cscli.SimulationConfig.Exclusions, scenario) isExcluded := slices.Contains(cli.cfg().Cscli.SimulationConfig.Exclusions, scenario)
if *csConfig.Cscli.SimulationConfig.Simulation && !isExcluded { if *cli.cfg().Cscli.SimulationConfig.Simulation && !isExcluded {
log.Warning("global simulation is already enabled") log.Warning("global simulation is already enabled")
continue continue
} }
if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded { if !*cli.cfg().Cscli.SimulationConfig.Simulation && isExcluded {
log.Warningf("simulation for '%s' already enabled", scenario) log.Warningf("simulation for '%s' already enabled", scenario)
continue continue
} }
if *csConfig.Cscli.SimulationConfig.Simulation && isExcluded { if *cli.cfg().Cscli.SimulationConfig.Simulation && isExcluded {
if err := removeFromExclusion(scenario); err != nil { cli.removeFromExclusion(scenario)
log.Fatal(err)
}
log.Printf("simulation enabled for '%s'", scenario) log.Printf("simulation enabled for '%s'", scenario)
continue continue
} }
if err := addToExclusion(scenario); err != nil { cli.addToExclusion(scenario)
log.Fatal(err)
}
log.Printf("simulation mode for '%s' enabled", scenario) log.Printf("simulation mode for '%s' enabled", scenario)
} }
if err := dumpSimulationFile(); err != nil { if err := cli.dumpSimulationFile(); err != nil {
log.Fatalf("simulation enable: %s", err) return fmt.Errorf("simulation enable: %w", err)
} }
} else if forceGlobalSimulation { } else if forceGlobalSimulation {
if err := enableGlobalSimulation(); err != nil { if err := cli.enableGlobalSimulation(); err != nil {
log.Fatalf("unable to enable global simulation mode : %s", err) return fmt.Errorf("unable to enable global simulation mode: %w", err)
} }
} else { } else {
printHelp(cmd) printHelp(cmd)
} }
return nil
}, },
} }
cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Enable global simulation (reverse mode)") cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Enable global simulation (reverse mode)")
@ -114,7 +118,7 @@ func (cli cliSimulation) NewEnableCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliSimulation) NewDisableCmd() *cobra.Command { func (cli *cliSimulation) NewDisableCmd() *cobra.Command {
var forceGlobalSimulation bool var forceGlobalSimulation bool
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -122,18 +126,16 @@ func (cli cliSimulation) NewDisableCmd() *cobra.Command {
Short: "Disable the simulation mode. Disable only specified scenarios", Short: "Disable the simulation mode. Disable only specified scenarios",
Example: `cscli simulation disable`, Example: `cscli simulation disable`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 { if len(args) > 0 {
for _, scenario := range args { for _, scenario := range args {
isExcluded := slices.Contains(csConfig.Cscli.SimulationConfig.Exclusions, scenario) isExcluded := slices.Contains(cli.cfg().Cscli.SimulationConfig.Exclusions, scenario)
if !*csConfig.Cscli.SimulationConfig.Simulation && !isExcluded { if !*cli.cfg().Cscli.SimulationConfig.Simulation && !isExcluded {
log.Warningf("%s isn't in simulation mode", scenario) log.Warningf("%s isn't in simulation mode", scenario)
continue continue
} }
if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded { if !*cli.cfg().Cscli.SimulationConfig.Simulation && isExcluded {
if err := removeFromExclusion(scenario); err != nil { cli.removeFromExclusion(scenario)
log.Fatal(err)
}
log.Printf("simulation mode for '%s' disabled", scenario) log.Printf("simulation mode for '%s' disabled", scenario)
continue continue
} }
@ -141,21 +143,21 @@ func (cli cliSimulation) NewDisableCmd() *cobra.Command {
log.Warningf("simulation mode is enabled but is already disable for '%s'", scenario) log.Warningf("simulation mode is enabled but is already disable for '%s'", scenario)
continue continue
} }
if err := addToExclusion(scenario); err != nil { cli.addToExclusion(scenario)
log.Fatal(err)
}
log.Printf("simulation mode for '%s' disabled", scenario) log.Printf("simulation mode for '%s' disabled", scenario)
} }
if err := dumpSimulationFile(); err != nil { if err := cli.dumpSimulationFile(); err != nil {
log.Fatalf("simulation disable: %s", err) return fmt.Errorf("simulation disable: %w", err)
} }
} else if forceGlobalSimulation { } else if forceGlobalSimulation {
if err := disableGlobalSimulation(); err != nil { if err := cli.disableGlobalSimulation(); err != nil {
log.Fatalf("unable to disable global simulation mode : %s", err) return fmt.Errorf("unable to disable global simulation mode: %w", err)
} }
} else { } else {
printHelp(cmd) printHelp(cmd)
} }
return nil
}, },
} }
cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Disable global simulation (reverse mode)") cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Disable global simulation (reverse mode)")
@ -163,16 +165,14 @@ func (cli cliSimulation) NewDisableCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliSimulation) NewStatusCmd() *cobra.Command { func (cli *cliSimulation) NewStatusCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "status", Use: "status",
Short: "Show simulation mode status", Short: "Show simulation mode status",
Example: `cscli simulation status`, Example: `cscli simulation status`,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { Run: func(_ *cobra.Command, _ []string) {
if err := simulationStatus(); err != nil { cli.status()
log.Fatal(err)
}
}, },
PersistentPostRun: func(cmd *cobra.Command, args []string) { PersistentPostRun: func(cmd *cobra.Command, args []string) {
}, },
@ -181,29 +181,29 @@ func (cli cliSimulation) NewStatusCmd() *cobra.Command {
return cmd return cmd
} }
func addToExclusion(name string) error { func (cli *cliSimulation) addToExclusion(name string) {
csConfig.Cscli.SimulationConfig.Exclusions = append(csConfig.Cscli.SimulationConfig.Exclusions, name) cfg := cli.cfg()
return nil cfg.Cscli.SimulationConfig.Exclusions = append(cfg.Cscli.SimulationConfig.Exclusions, name)
} }
func removeFromExclusion(name string) error { func (cli *cliSimulation) removeFromExclusion(name string) {
index := slices.Index(csConfig.Cscli.SimulationConfig.Exclusions, name) cfg := cli.cfg()
index := slices.Index(cfg.Cscli.SimulationConfig.Exclusions, name)
// Remove element from the slice // Remove element from the slice
csConfig.Cscli.SimulationConfig.Exclusions[index] = csConfig.Cscli.SimulationConfig.Exclusions[len(csConfig.Cscli.SimulationConfig.Exclusions)-1] cfg.Cscli.SimulationConfig.Exclusions[index] = cfg.Cscli.SimulationConfig.Exclusions[len(cfg.Cscli.SimulationConfig.Exclusions)-1]
csConfig.Cscli.SimulationConfig.Exclusions[len(csConfig.Cscli.SimulationConfig.Exclusions)-1] = "" cfg.Cscli.SimulationConfig.Exclusions[len(cfg.Cscli.SimulationConfig.Exclusions)-1] = ""
csConfig.Cscli.SimulationConfig.Exclusions = csConfig.Cscli.SimulationConfig.Exclusions[:len(csConfig.Cscli.SimulationConfig.Exclusions)-1] cfg.Cscli.SimulationConfig.Exclusions = cfg.Cscli.SimulationConfig.Exclusions[:len(cfg.Cscli.SimulationConfig.Exclusions)-1]
return nil
} }
func enableGlobalSimulation() error { func (cli *cliSimulation) enableGlobalSimulation() error {
csConfig.Cscli.SimulationConfig.Simulation = new(bool) cfg := cli.cfg()
*csConfig.Cscli.SimulationConfig.Simulation = true cfg.Cscli.SimulationConfig.Simulation = new(bool)
csConfig.Cscli.SimulationConfig.Exclusions = []string{} *cfg.Cscli.SimulationConfig.Simulation = true
cfg.Cscli.SimulationConfig.Exclusions = []string{}
if err := dumpSimulationFile(); err != nil { if err := cli.dumpSimulationFile(); err != nil {
log.Fatalf("unable to dump simulation file: %s", err) return fmt.Errorf("unable to dump simulation file: %w", err)
} }
log.Printf("global simulation: enabled") log.Printf("global simulation: enabled")
@ -211,59 +211,72 @@ func enableGlobalSimulation() error {
return nil return nil
} }
func dumpSimulationFile() error { func (cli *cliSimulation) dumpSimulationFile() error {
newConfigSim, err := yaml.Marshal(csConfig.Cscli.SimulationConfig) cfg := cli.cfg()
newConfigSim, err := yaml.Marshal(cfg.Cscli.SimulationConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal simulation configuration: %s", err) return fmt.Errorf("unable to marshal simulation configuration: %w", err)
} }
err = os.WriteFile(csConfig.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
err = os.WriteFile(cfg.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("write simulation config in '%s' failed: %s", csConfig.ConfigPaths.SimulationFilePath, err) return fmt.Errorf("write simulation config in '%s' failed: %w", cfg.ConfigPaths.SimulationFilePath, err)
} }
log.Debugf("updated simulation file %s", csConfig.ConfigPaths.SimulationFilePath)
log.Debugf("updated simulation file %s", cfg.ConfigPaths.SimulationFilePath)
return nil return nil
} }
func disableGlobalSimulation() error { func (cli *cliSimulation) disableGlobalSimulation() error {
csConfig.Cscli.SimulationConfig.Simulation = new(bool) cfg := cli.cfg()
*csConfig.Cscli.SimulationConfig.Simulation = false cfg.Cscli.SimulationConfig.Simulation = new(bool)
*cfg.Cscli.SimulationConfig.Simulation = false
csConfig.Cscli.SimulationConfig.Exclusions = []string{} cfg.Cscli.SimulationConfig.Exclusions = []string{}
newConfigSim, err := yaml.Marshal(csConfig.Cscli.SimulationConfig)
newConfigSim, err := yaml.Marshal(cfg.Cscli.SimulationConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal new simulation configuration: %s", err) return fmt.Errorf("unable to marshal new simulation configuration: %w", err)
} }
err = os.WriteFile(csConfig.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
err = os.WriteFile(cfg.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("unable to write new simulation config in '%s' : %s", csConfig.ConfigPaths.SimulationFilePath, err) return fmt.Errorf("unable to write new simulation config in '%s': %w", cfg.ConfigPaths.SimulationFilePath, err)
} }
log.Printf("global simulation: disabled") log.Printf("global simulation: disabled")
return nil return nil
} }
func simulationStatus() error { func (cli *cliSimulation) status() {
if csConfig.Cscli.SimulationConfig == nil { cfg := cli.cfg()
if cfg.Cscli.SimulationConfig == nil {
log.Printf("global simulation: disabled (configuration file is missing)") log.Printf("global simulation: disabled (configuration file is missing)")
return nil return
} }
if *csConfig.Cscli.SimulationConfig.Simulation {
if *cfg.Cscli.SimulationConfig.Simulation {
log.Println("global simulation: enabled") log.Println("global simulation: enabled")
if len(csConfig.Cscli.SimulationConfig.Exclusions) > 0 {
if len(cfg.Cscli.SimulationConfig.Exclusions) > 0 {
log.Println("Scenarios not in simulation mode :") log.Println("Scenarios not in simulation mode :")
for _, scenario := range csConfig.Cscli.SimulationConfig.Exclusions {
for _, scenario := range cfg.Cscli.SimulationConfig.Exclusions {
log.Printf(" - %s", scenario) log.Printf(" - %s", scenario)
} }
} }
} else { } else {
log.Println("global simulation: disabled") log.Println("global simulation: disabled")
if len(csConfig.Cscli.SimulationConfig.Exclusions) > 0 {
if len(cfg.Cscli.SimulationConfig.Exclusions) > 0 {
log.Println("Scenarios in simulation mode :") log.Println("Scenarios in simulation mode :")
for _, scenario := range csConfig.Cscli.SimulationConfig.Exclusions {
for _, scenario := range cfg.Cscli.SimulationConfig.Exclusions {
log.Printf(" - %s", scenario) log.Printf(" - %s", scenario)
} }
} }
} }
return nil
} }

View file

@ -4,6 +4,7 @@ import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -12,12 +13,14 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
"github.com/blackfireio/osinfo" "github.com/blackfireio/osinfo"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
@ -47,6 +50,7 @@ const (
SUPPORT_CAPI_STATUS_PATH = "capi_status.txt" SUPPORT_CAPI_STATUS_PATH = "capi_status.txt"
SUPPORT_ACQUISITION_CONFIG_BASE_PATH = "config/acquis/" SUPPORT_ACQUISITION_CONFIG_BASE_PATH = "config/acquis/"
SUPPORT_CROWDSEC_PROFILE_PATH = "config/profiles.yaml" SUPPORT_CROWDSEC_PROFILE_PATH = "config/profiles.yaml"
SUPPORT_CRASH_PATH = "crash/"
) )
// from https://github.com/acarl005/stripansi // from https://github.com/acarl005/stripansi
@ -62,32 +66,38 @@ func collectMetrics() ([]byte, []byte, error) {
if csConfig.Cscli.PrometheusUrl == "" { if csConfig.Cscli.PrometheusUrl == "" {
log.Warn("No Prometheus URL configured, metrics will not be collected") log.Warn("No Prometheus URL configured, metrics will not be collected")
return nil, nil, fmt.Errorf("prometheus_uri is not set") return nil, nil, errors.New("prometheus_uri is not set")
} }
humanMetrics := bytes.NewBuffer(nil) humanMetrics := bytes.NewBuffer(nil)
err := FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl, "human")
if err != nil { ms := NewMetricStore()
return nil, nil, fmt.Errorf("could not fetch promtheus metrics: %s", err)
if err := ms.Fetch(csConfig.Cscli.PrometheusUrl); err != nil {
return nil, nil, fmt.Errorf("could not fetch prometheus metrics: %w", err)
}
if err := ms.Format(humanMetrics, nil, "human", false); err != nil {
return nil, nil, err
} }
req, err := http.NewRequest(http.MethodGet, csConfig.Cscli.PrometheusUrl, nil) req, err := http.NewRequest(http.MethodGet, csConfig.Cscli.PrometheusUrl, nil)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not create requests to prometheus endpoint: %s", err) return nil, nil, fmt.Errorf("could not create requests to prometheus endpoint: %w", err)
} }
client := &http.Client{}
resp, err := client.Do(req)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not get metrics from prometheus endpoint: %s", err) return nil, nil, fmt.Errorf("could not get metrics from prometheus endpoint: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not read metrics from prometheus endpoint: %s", err) return nil, nil, fmt.Errorf("could not read metrics from prometheus endpoint: %w", err)
} }
return humanMetrics.Bytes(), body, nil return humanMetrics.Bytes(), body, nil
@ -100,31 +110,33 @@ func collectVersion() []byte {
func collectFeatures() []byte { func collectFeatures() []byte {
log.Info("Collecting feature flags") log.Info("Collecting feature flags")
enabledFeatures := fflag.Crowdsec.GetEnabledFeatures() enabledFeatures := fflag.Crowdsec.GetEnabledFeatures()
w := bytes.NewBuffer(nil) w := bytes.NewBuffer(nil)
for _, k := range enabledFeatures { for _, k := range enabledFeatures {
fmt.Fprintf(w, "%s\n", k) fmt.Fprintf(w, "%s\n", k)
} }
return w.Bytes() return w.Bytes()
} }
func collectOSInfo() ([]byte, error) { func collectOSInfo() ([]byte, error) {
log.Info("Collecting OS info") log.Info("Collecting OS info")
info, err := osinfo.GetOSInfo()
info, err := osinfo.GetOSInfo()
if err != nil { if err != nil {
return nil, err return nil, err
} }
w := bytes.NewBuffer(nil) w := bytes.NewBuffer(nil)
w.WriteString(fmt.Sprintf("Architecture: %s\n", info.Architecture)) fmt.Fprintf(w, "Architecture: %s\n", info.Architecture)
w.WriteString(fmt.Sprintf("Family: %s\n", info.Family)) fmt.Fprintf(w, "Family: %s\n", info.Family)
w.WriteString(fmt.Sprintf("ID: %s\n", info.ID)) fmt.Fprintf(w, "ID: %s\n", info.ID)
w.WriteString(fmt.Sprintf("Name: %s\n", info.Name)) fmt.Fprintf(w, "Name: %s\n", info.Name)
w.WriteString(fmt.Sprintf("Codename: %s\n", info.Codename)) fmt.Fprintf(w, "Codename: %s\n", info.Codename)
w.WriteString(fmt.Sprintf("Version: %s\n", info.Version)) fmt.Fprintf(w, "Version: %s\n", info.Version)
w.WriteString(fmt.Sprintf("Build: %s\n", info.Build)) fmt.Fprintf(w, "Build: %s\n", info.Build)
return w.Bytes(), nil return w.Bytes(), nil
} }
@ -133,6 +145,7 @@ func collectHubItems(hub *cwhub.Hub, itemType string) []byte {
var err error var err error
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
log.Infof("Collecting %s list", itemType) log.Infof("Collecting %s list", itemType)
items := make(map[string][]*cwhub.Item) items := make(map[string][]*cwhub.Item)
@ -144,24 +157,33 @@ func collectHubItems(hub *cwhub.Hub, itemType string) []byte {
if err := listItems(out, []string{itemType}, items, false); err != nil { if err := listItems(out, []string{itemType}, items, false); err != nil {
log.Warnf("could not collect %s list: %s", itemType, err) log.Warnf("could not collect %s list: %s", itemType, err)
} }
return out.Bytes() return out.Bytes()
} }
func collectBouncers(dbClient *database.Client) ([]byte, error) { func collectBouncers(dbClient *database.Client) ([]byte, error) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
err := getBouncers(out, dbClient)
bouncers, err := dbClient.ListBouncers()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to list bouncers: %w", err)
} }
getBouncersTable(out, bouncers)
return out.Bytes(), nil return out.Bytes(), nil
} }
func collectAgents(dbClient *database.Client) ([]byte, error) { func collectAgents(dbClient *database.Client) ([]byte, error) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
err := getAgents(out, dbClient)
machines, err := dbClient.ListMachines()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to list machines: %w", err)
} }
getAgentsTable(out, machines)
return out.Bytes(), nil return out.Bytes(), nil
} }
@ -169,13 +191,15 @@ func collectAPIStatus(login string, password string, endpoint string, prefix str
if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil { if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil {
return []byte("No agent credentials found, are we LAPI ?") return []byte("No agent credentials found, are we LAPI ?")
} }
pwd := strfmt.Password(password)
apiurl, err := url.Parse(endpoint)
pwd := strfmt.Password(password)
apiurl, err := url.Parse(endpoint)
if err != nil { if err != nil {
return []byte(fmt.Sprintf("cannot parse API URL: %s", err)) return []byte(fmt.Sprintf("cannot parse API URL: %s", err))
} }
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
if err != nil { if err != nil {
return []byte(fmt.Sprintf("could not collect scenarios: %s", err)) return []byte(fmt.Sprintf("could not collect scenarios: %s", err))
} }
@ -187,6 +211,7 @@ func collectAPIStatus(login string, password string, endpoint string, prefix str
if err != nil { if err != nil {
return []byte(fmt.Sprintf("could not init client: %s", err)) return []byte(fmt.Sprintf("could not init client: %s", err))
} }
t := models.WatcherAuthRequest{ t := models.WatcherAuthRequest{
MachineID: &login, MachineID: &login,
Password: &pwd, Password: &pwd,
@ -203,6 +228,7 @@ func collectAPIStatus(login string, password string, endpoint string, prefix str
func collectCrowdsecConfig() []byte { func collectCrowdsecConfig() []byte {
log.Info("Collecting crowdsec config") log.Info("Collecting crowdsec config")
config, err := os.ReadFile(*csConfig.FilePath) config, err := os.ReadFile(*csConfig.FilePath)
if err != nil { if err != nil {
return []byte(fmt.Sprintf("could not read config file: %s", err)) return []byte(fmt.Sprintf("could not read config file: %s", err))
@ -215,15 +241,18 @@ func collectCrowdsecConfig() []byte {
func collectCrowdsecProfile() []byte { func collectCrowdsecProfile() []byte {
log.Info("Collecting crowdsec profile") log.Info("Collecting crowdsec profile")
config, err := os.ReadFile(csConfig.API.Server.ProfilesPath) config, err := os.ReadFile(csConfig.API.Server.ProfilesPath)
if err != nil { if err != nil {
return []byte(fmt.Sprintf("could not read profile file: %s", err)) return []byte(fmt.Sprintf("could not read profile file: %s", err))
} }
return config return config
} }
func collectAcquisitionConfig() map[string][]byte { func collectAcquisitionConfig() map[string][]byte {
log.Info("Collecting acquisition config") log.Info("Collecting acquisition config")
ret := make(map[string][]byte) ret := make(map[string][]byte)
for _, filename := range csConfig.Crowdsec.AcquisitionFiles { for _, filename := range csConfig.Crowdsec.AcquisitionFiles {
@ -238,6 +267,11 @@ func collectAcquisitionConfig() map[string][]byte {
return ret return ret
} }
func collectCrash() ([]string, error) {
log.Info("Collecting crash dumps")
return trace.List()
}
type cliSupport struct{} type cliSupport struct{}
func NewCLISupport() *cliSupport { func NewCLISupport() *cliSupport {
@ -285,7 +319,7 @@ cscli support dump -f /tmp/crowdsec-support.zip
`, `,
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) { RunE: func(_ *cobra.Command, _ []string) error {
var err error var err error
var skipHub, skipDB, skipCAPI, skipLAPI, skipAgent bool var skipHub, skipDB, skipCAPI, skipLAPI, skipAgent bool
infos := map[string][]byte{ infos := map[string][]byte{
@ -305,13 +339,13 @@ cscli support dump -f /tmp/crowdsec-support.zip
infos[SUPPORT_AGENTS_PATH] = []byte(err.Error()) infos[SUPPORT_AGENTS_PATH] = []byte(err.Error())
} }
if err := csConfig.LoadAPIServer(); err != nil { if err = csConfig.LoadAPIServer(true); err != nil {
log.Warnf("could not load LAPI, skipping CAPI check") log.Warnf("could not load LAPI, skipping CAPI check")
skipLAPI = true skipLAPI = true
infos[SUPPORT_CAPI_STATUS_PATH] = []byte(err.Error()) infos[SUPPORT_CAPI_STATUS_PATH] = []byte(err.Error())
} }
if err := csConfig.LoadCrowdsec(); err != nil { if err = csConfig.LoadCrowdsec(); err != nil {
log.Warnf("could not load agent config, skipping crowdsec config check") log.Warnf("could not load agent config, skipping crowdsec config check")
skipAgent = true skipAgent = true
} }
@ -397,7 +431,6 @@ cscli support dump -f /tmp/crowdsec-support.zip
} }
if !skipAgent { if !skipAgent {
acquis := collectAcquisitionConfig() acquis := collectAcquisitionConfig()
for filename, content := range acquis { for filename, content := range acquis {
@ -406,11 +439,31 @@ cscli support dump -f /tmp/crowdsec-support.zip
} }
} }
crash, err := collectCrash()
if err != nil {
log.Errorf("could not collect crash dumps: %s", err)
}
for _, filename := range crash {
content, err := os.ReadFile(filename)
if err != nil {
log.Errorf("could not read crash dump %s: %s", filename, err)
}
infos[SUPPORT_CRASH_PATH+filepath.Base(filename)] = content
}
w := bytes.NewBuffer(nil) w := bytes.NewBuffer(nil)
zipWriter := zip.NewWriter(w) zipWriter := zip.NewWriter(w)
for filename, data := range infos { for filename, data := range infos {
fw, err := zipWriter.Create(filename) header := &zip.FileHeader{
Name: filename,
Method: zip.Deflate,
// TODO: retain mtime where possible (esp. trace)
Modified: time.Now(),
}
fw, err := zipWriter.CreateHeader(header)
if err != nil { if err != nil {
log.Errorf("Could not add zip entry for %s: %s", filename, err) log.Errorf("Could not add zip entry for %s: %s", filename, err)
continue continue
@ -420,15 +473,19 @@ cscli support dump -f /tmp/crowdsec-support.zip
err = zipWriter.Close() err = zipWriter.Close()
if err != nil { if err != nil {
log.Fatalf("could not finalize zip file: %s", err) return fmt.Errorf("could not finalize zip file: %s", err)
} }
if outFile == "-" {
_, err = os.Stdout.Write(w.Bytes())
return err
}
err = os.WriteFile(outFile, w.Bytes(), 0o600) err = os.WriteFile(outFile, w.Bytes(), 0o600)
if err != nil { if err != nil {
log.Fatalf("could not write zip file to %s: %s", outFile, err) return fmt.Errorf("could not write zip file to %s: %s", outFile, err)
} }
log.Infof("Written zip file to %s", outFile) log.Infof("Written zip file to %s", outFile)
return nil
}, },
} }

View file

@ -8,7 +8,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
) )
@ -26,6 +25,7 @@ func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *
return fmt.Errorf("%s isn't a valid range", *ipRange) return fmt.Errorf("%s isn't a valid range", *ipRange)
} }
} }
if *ip != "" { if *ip != "" {
ipRepr := net.ParseIP(*ip) ipRepr := net.ParseIP(*ip)
if ipRepr == nil { if ipRepr == nil {
@ -33,7 +33,7 @@ func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *
} }
} }
//avoid confusion on scope (ip vs Ip and range vs Range) // avoid confusion on scope (ip vs Ip and range vs Range)
switch strings.ToLower(*scope) { switch strings.ToLower(*scope) {
case "ip": case "ip":
*scope = types.Ip *scope = types.Ip
@ -44,18 +44,8 @@ func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *
case "as": case "as":
*scope = types.AS *scope = types.AS
} }
return nil
}
func getDBClient() (*database.Client, error) { return nil
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
return nil, err
}
ret, err := database.NewClient(csConfig.DbConfig)
if err != nil {
return nil, err
}
return ret, nil
} }
func removeFromSlice(val string, slice []string) []string { func removeFromSlice(val string, slice []string) []string {

View file

@ -6,9 +6,9 @@ import (
"strconv" "strconv"
"github.com/aquasecurity/table" "github.com/aquasecurity/table"
"github.com/enescakir/emoji"
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
) )
func listHubItemTable(out io.Writer, title string, items []*cwhub.Item) { func listHubItemTable(out io.Writer, title string, items []*cwhub.Item) {
@ -21,6 +21,7 @@ func listHubItemTable(out io.Writer, title string, items []*cwhub.Item) {
status := fmt.Sprintf("%v %s", item.State.Emoji(), item.State.Text()) status := fmt.Sprintf("%v %s", item.State.Emoji(), item.State.Text())
t.AddRow(item.Name, status, item.State.LocalVersion, item.State.LocalPath) t.AddRow(item.Name, status, item.State.LocalVersion, item.State.LocalPath)
} }
renderTableTitle(out, title) renderTableTitle(out, title)
t.Render() t.Render()
} }
@ -42,6 +43,7 @@ func scenarioMetricsTable(out io.Writer, itemName string, metrics map[string]int
if metrics["instantiation"] == 0 { if metrics["instantiation"] == 0 {
return return
} }
t := newTable(out) t := newTable(out)
t.SetHeaders("Current Count", "Overflows", "Instantiated", "Poured", "Expired") t.SetHeaders("Current Count", "Overflows", "Instantiated", "Poured", "Expired")
@ -72,6 +74,7 @@ func parserMetricsTable(out io.Writer, itemName string, metrics map[string]map[s
strconv.Itoa(stats["parsed"]), strconv.Itoa(stats["parsed"]),
strconv.Itoa(stats["unparsed"]), strconv.Itoa(stats["unparsed"]),
) )
showTable = true showTable = true
} }
} }

View file

@ -1,11 +1,11 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"runtime" "runtime"
"time" "time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/trace" "github.com/crowdsecurity/go-cs-lib/trace"
@ -56,7 +56,8 @@ func initAPIServer(cConfig *csconfig.Config) (*apiserver.APIServer, error) {
return apiServer, nil return apiServer, nil
} }
func serveAPIServer(apiServer *apiserver.APIServer, apiReady chan bool) { func serveAPIServer(apiServer *apiserver.APIServer) {
apiReady := make(chan bool, 1)
apiTomb.Go(func() error { apiTomb.Go(func() error {
defer trace.CatchPanic("crowdsec/serveAPIServer") defer trace.CatchPanic("crowdsec/serveAPIServer")
go func() { go func() {
@ -80,6 +81,7 @@ func serveAPIServer(apiServer *apiserver.APIServer, apiReady chan bool) {
} }
return nil return nil
}) })
<-apiReady
} }
func hasPlugins(profiles []*csconfig.ProfileCfg) bool { func hasPlugins(profiles []*csconfig.ProfileCfg) bool {

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -8,13 +9,14 @@ import (
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/go-cs-lib/trace" "github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/crowdsec/pkg/acquisition" "github.com/crowdsecurity/crowdsec/pkg/acquisition"
"github.com/crowdsecurity/crowdsec/pkg/appsec" "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
"github.com/crowdsecurity/crowdsec/pkg/alertcontext" "github.com/crowdsecurity/crowdsec/pkg/alertcontext"
"github.com/crowdsecurity/crowdsec/pkg/appsec"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket" leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
@ -22,130 +24,160 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
) )
func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, error) { // initCrowdsec prepares the log processor service
func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, []acquisition.DataSource, error) {
var err error var err error
if err = alertcontext.LoadConsoleContext(cConfig, hub); err != nil { if err = alertcontext.LoadConsoleContext(cConfig, hub); err != nil {
return nil, fmt.Errorf("while loading context: %w", err) return nil, nil, fmt.Errorf("while loading context: %w", err)
} }
// Start loading configs // Start loading configs
csParsers := parser.NewParsers(hub) csParsers := parser.NewParsers(hub)
if csParsers, err = parser.LoadParsers(cConfig, csParsers); err != nil { if csParsers, err = parser.LoadParsers(cConfig, csParsers); err != nil {
return nil, fmt.Errorf("while loading parsers: %w", err) return nil, nil, fmt.Errorf("while loading parsers: %w", err)
} }
if err := LoadBuckets(cConfig, hub); err != nil { if err := LoadBuckets(cConfig, hub); err != nil {
return nil, fmt.Errorf("while loading scenarios: %w", err) return nil, nil, fmt.Errorf("while loading scenarios: %w", err)
} }
if err := appsec.LoadAppsecRules(hub); err != nil { if err := appsec.LoadAppsecRules(hub); err != nil {
return nil, fmt.Errorf("while loading appsec rules: %w", err) return nil, nil, fmt.Errorf("while loading appsec rules: %w", err)
} }
if err := LoadAcquisition(cConfig); err != nil { datasources, err := LoadAcquisition(cConfig)
return nil, fmt.Errorf("while loading acquisition config: %w", err) if err != nil {
return nil, nil, fmt.Errorf("while loading acquisition config: %w", err)
} }
return csParsers, nil return csParsers, datasources, nil
} }
func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.Hub) error { // runCrowdsec starts the log processor service
func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.Hub, datasources []acquisition.DataSource) error {
inputEventChan = make(chan types.Event) inputEventChan = make(chan types.Event)
inputLineChan = make(chan types.Event) inputLineChan = make(chan types.Event)
//start go-routines for parsing, buckets pour and outputs. // start go-routines for parsing, buckets pour and outputs.
parserWg := &sync.WaitGroup{} parserWg := &sync.WaitGroup{}
parsersTomb.Go(func() error { parsersTomb.Go(func() error {
parserWg.Add(1) parserWg.Add(1)
for i := 0; i < cConfig.Crowdsec.ParserRoutinesCount; i++ { for i := 0; i < cConfig.Crowdsec.ParserRoutinesCount; i++ {
parsersTomb.Go(func() error { parsersTomb.Go(func() error {
defer trace.CatchPanic("crowdsec/runParse") defer trace.CatchPanic("crowdsec/runParse")
if err := runParse(inputLineChan, inputEventChan, *parsers.Ctx, parsers.Nodes); err != nil { //this error will never happen as parser.Parse is not able to return errors
if err := runParse(inputLineChan, inputEventChan, *parsers.Ctx, parsers.Nodes); err != nil {
// this error will never happen as parser.Parse is not able to return errors
log.Fatalf("starting parse error : %s", err) log.Fatalf("starting parse error : %s", err)
return err return err
} }
return nil return nil
}) })
} }
parserWg.Done() parserWg.Done()
return nil return nil
}) })
parserWg.Wait() parserWg.Wait()
bucketWg := &sync.WaitGroup{} bucketWg := &sync.WaitGroup{}
bucketsTomb.Go(func() error { bucketsTomb.Go(func() error {
bucketWg.Add(1) bucketWg.Add(1)
/*restore previous state as well if present*/ /*restore previous state as well if present*/
if cConfig.Crowdsec.BucketStateFile != "" { if cConfig.Crowdsec.BucketStateFile != "" {
log.Warningf("Restoring buckets state from %s", cConfig.Crowdsec.BucketStateFile) log.Warningf("Restoring buckets state from %s", cConfig.Crowdsec.BucketStateFile)
if err := leaky.LoadBucketsState(cConfig.Crowdsec.BucketStateFile, buckets, holders); err != nil { if err := leaky.LoadBucketsState(cConfig.Crowdsec.BucketStateFile, buckets, holders); err != nil {
return fmt.Errorf("unable to restore buckets : %s", err) return fmt.Errorf("unable to restore buckets: %w", err)
} }
} }
for i := 0; i < cConfig.Crowdsec.BucketsRoutinesCount; i++ { for i := 0; i < cConfig.Crowdsec.BucketsRoutinesCount; i++ {
bucketsTomb.Go(func() error { bucketsTomb.Go(func() error {
defer trace.CatchPanic("crowdsec/runPour") defer trace.CatchPanic("crowdsec/runPour")
if err := runPour(inputEventChan, holders, buckets, cConfig); err != nil { if err := runPour(inputEventChan, holders, buckets, cConfig); err != nil {
log.Fatalf("starting pour error : %s", err) log.Fatalf("starting pour error : %s", err)
return err return err
} }
return nil return nil
}) })
} }
bucketWg.Done() bucketWg.Done()
return nil return nil
}) })
bucketWg.Wait() bucketWg.Wait()
apiClient, err := AuthenticatedLAPIClient(*cConfig.API.Client.Credentials, hub)
if err != nil {
return err
}
log.Debugf("Starting HeartBeat service")
apiClient.HeartBeat.StartHeartBeat(context.Background(), &outputsTomb)
outputWg := &sync.WaitGroup{} outputWg := &sync.WaitGroup{}
outputsTomb.Go(func() error { outputsTomb.Go(func() error {
outputWg.Add(1) outputWg.Add(1)
for i := 0; i < cConfig.Crowdsec.OutputRoutinesCount; i++ { for i := 0; i < cConfig.Crowdsec.OutputRoutinesCount; i++ {
outputsTomb.Go(func() error { outputsTomb.Go(func() error {
defer trace.CatchPanic("crowdsec/runOutput") defer trace.CatchPanic("crowdsec/runOutput")
if err := runOutput(inputEventChan, outputEventChan, buckets, *parsers.Povfwctx, parsers.Povfwnodes, *cConfig.API.Client.Credentials, hub); err != nil {
if err := runOutput(inputEventChan, outputEventChan, buckets, *parsers.Povfwctx, parsers.Povfwnodes, apiClient); err != nil {
log.Fatalf("starting outputs error : %s", err) log.Fatalf("starting outputs error : %s", err)
return err return err
} }
return nil return nil
}) })
} }
outputWg.Done() outputWg.Done()
return nil return nil
}) })
outputWg.Wait() outputWg.Wait()
if cConfig.Prometheus != nil && cConfig.Prometheus.Enabled { if cConfig.Prometheus != nil && cConfig.Prometheus.Enabled {
aggregated := false aggregated := false
if cConfig.Prometheus.Level == "aggregated" { if cConfig.Prometheus.Level == configuration.CFG_METRICS_AGGREGATE {
aggregated = true aggregated = true
} }
if err := acquisition.GetMetrics(dataSources, aggregated); err != nil { if err := acquisition.GetMetrics(dataSources, aggregated); err != nil {
return fmt.Errorf("while fetching prometheus metrics for datasources: %w", err) return fmt.Errorf("while fetching prometheus metrics for datasources: %w", err)
} }
} }
log.Info("Starting processing data") log.Info("Starting processing data")
if err := acquisition.StartAcquisition(dataSources, inputLineChan, &acquisTomb); err != nil { if err := acquisition.StartAcquisition(dataSources, inputLineChan, &acquisTomb); err != nil {
log.Fatalf("starting acquisition error : %s", err) return fmt.Errorf("starting acquisition error: %w", err)
return err
} }
return nil return nil
} }
func serveCrowdsec(parsers *parser.Parsers, cConfig *csconfig.Config, hub *cwhub.Hub, agentReady chan bool) { // serveCrowdsec wraps the log processor service
func serveCrowdsec(parsers *parser.Parsers, cConfig *csconfig.Config, hub *cwhub.Hub, datasources []acquisition.DataSource, agentReady chan bool) {
crowdsecTomb.Go(func() error { crowdsecTomb.Go(func() error {
defer trace.CatchPanic("crowdsec/serveCrowdsec") defer trace.CatchPanic("crowdsec/serveCrowdsec")
go func() { go func() {
defer trace.CatchPanic("crowdsec/runCrowdsec") defer trace.CatchPanic("crowdsec/runCrowdsec")
// this logs every time, even at config reload // this logs every time, even at config reload
log.Debugf("running agent after %s ms", time.Since(crowdsecT0)) log.Debugf("running agent after %s ms", time.Since(crowdsecT0))
agentReady <- true agentReady <- true
if err := runCrowdsec(cConfig, parsers, hub); err != nil {
if err := runCrowdsec(cConfig, parsers, hub, datasources); err != nil {
log.Fatalf("unable to start crowdsec routines: %s", err) log.Fatalf("unable to start crowdsec routines: %s", err)
} }
}() }()
@ -156,74 +188,88 @@ func serveCrowdsec(parsers *parser.Parsers, cConfig *csconfig.Config, hub *cwhub
*/ */
waitOnTomb() waitOnTomb()
log.Debugf("Shutting down crowdsec routines") log.Debugf("Shutting down crowdsec routines")
if err := ShutdownCrowdsecRoutines(); err != nil { if err := ShutdownCrowdsecRoutines(); err != nil {
log.Fatalf("unable to shutdown crowdsec routines: %s", err) log.Fatalf("unable to shutdown crowdsec routines: %s", err)
} }
log.Debugf("everything is dead, return crowdsecTomb") log.Debugf("everything is dead, return crowdsecTomb")
if dumpStates { if dumpStates {
dumpParserState() dumpParserState()
dumpOverflowState() dumpOverflowState()
dumpBucketsPour() dumpBucketsPour()
os.Exit(0) os.Exit(0)
} }
return nil return nil
}) })
} }
func dumpBucketsPour() { func dumpBucketsPour() {
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "bucketpour-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "bucketpour-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
if err != nil { if err != nil {
log.Fatalf("open: %s", err) log.Fatalf("open: %s", err)
} }
out, err := yaml.Marshal(leaky.BucketPourCache) out, err := yaml.Marshal(leaky.BucketPourCache)
if err != nil { if err != nil {
log.Fatalf("marshal: %s", err) log.Fatalf("marshal: %s", err)
} }
b, err := fd.Write(out) b, err := fd.Write(out)
if err != nil { if err != nil {
log.Fatalf("write: %s", err) log.Fatalf("write: %s", err)
} }
log.Tracef("wrote %d bytes", b) log.Tracef("wrote %d bytes", b)
if err := fd.Close(); err != nil { if err := fd.Close(); err != nil {
log.Fatalf(" close: %s", err) log.Fatalf(" close: %s", err)
} }
} }
func dumpParserState() { func dumpParserState() {
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "parser-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "parser-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil { if err != nil {
log.Fatalf("open: %s", err) log.Fatalf("open: %s", err)
} }
out, err := yaml.Marshal(parser.StageParseCache) out, err := yaml.Marshal(parser.StageParseCache)
if err != nil { if err != nil {
log.Fatalf("marshal: %s", err) log.Fatalf("marshal: %s", err)
} }
b, err := fd.Write(out) b, err := fd.Write(out)
if err != nil { if err != nil {
log.Fatalf("write: %s", err) log.Fatalf("write: %s", err)
} }
log.Tracef("wrote %d bytes", b) log.Tracef("wrote %d bytes", b)
if err := fd.Close(); err != nil { if err := fd.Close(); err != nil {
log.Fatalf(" close: %s", err) log.Fatalf(" close: %s", err)
} }
} }
func dumpOverflowState() { func dumpOverflowState() {
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "bucket-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
fd, err := os.OpenFile(filepath.Join(parser.DumpFolder, "bucket-dump.yaml"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil { if err != nil {
log.Fatalf("open: %s", err) log.Fatalf("open: %s", err)
} }
out, err := yaml.Marshal(bucketOverflows) out, err := yaml.Marshal(bucketOverflows)
if err != nil { if err != nil {
log.Fatalf("marshal: %s", err) log.Fatalf("marshal: %s", err)
} }
b, err := fd.Write(out) b, err := fd.Write(out)
if err != nil { if err != nil {
log.Fatalf("write: %s", err) log.Fatalf("write: %s", err)
} }
log.Tracef("wrote %d bytes", b) log.Tracef("wrote %d bytes", b)
if err := fd.Close(); err != nil { if err := fd.Close(); err != nil {
log.Fatalf(" close: %s", err) log.Fatalf(" close: %s", err)
} }
@ -235,7 +281,7 @@ func waitOnTomb() {
case <-acquisTomb.Dead(): case <-acquisTomb.Dead():
/*if it's acquisition dying it means that we were in "cat" mode. /*if it's acquisition dying it means that we were in "cat" mode.
while shutting down, we need to give time for all buckets to process in flight data*/ while shutting down, we need to give time for all buckets to process in flight data*/
log.Warning("Acquisition is finished, shutting down") log.Info("Acquisition is finished, shutting down")
/* /*
While it might make sense to want to shut-down parser/buckets/etc. as soon as acquisition is finished, While it might make sense to want to shut-down parser/buckets/etc. as soon as acquisition is finished,
we might have some pending buckets: buckets that overflowed, but whose LeakRoutine are still alive because they we might have some pending buckets: buckets that overflowed, but whose LeakRoutine are still alive because they

28
cmd/crowdsec/fatalhook.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"io"
log "github.com/sirupsen/logrus"
)
// FatalHook is used to log fatal messages to stderr when the rest goes to a file
type FatalHook struct {
Writer io.Writer
LogLevels []log.Level
}
func (hook *FatalHook) Fire(entry *log.Entry) error {
line, err := entry.String()
if err != nil {
return err
}
_, err = hook.Writer.Write([]byte(line))
return err
}
func (hook *FatalHook) Levels() []log.Level {
return hook.LogLevels
}

View file

@ -1,43 +0,0 @@
package main
import (
"io"
"os"
log "github.com/sirupsen/logrus"
)
type ConditionalHook struct {
Writer io.Writer
LogLevels []log.Level
Enabled bool
}
func (hook *ConditionalHook) Fire(entry *log.Entry) error {
if hook.Enabled {
line, err := entry.String()
if err != nil {
return err
}
_, err = hook.Writer.Write([]byte(line))
return err
}
return nil
}
func (hook *ConditionalHook) Levels() []log.Level {
return hook.LogLevels
}
// The primal logging hook is set up before parsing config.yaml.
// Once config.yaml is parsed, the primal hook is disabled if the
// configured logger is writing to stderr. Otherwise it's used to
// report fatal errors and panics to stderr in addition to the log file.
var primalHook = &ConditionalHook{
Writer: os.Stderr,
LogLevels: []log.Level{log.FatalLevel, log.PanicLevel},
Enabled: true,
}

View file

@ -0,0 +1,92 @@
package main
import (
"context"
"fmt"
"net/url"
"time"
"github.com/go-openapi/strfmt"
"github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/models"
)
func AuthenticatedLAPIClient(credentials csconfig.ApiCredentialsCfg, hub *cwhub.Hub) (*apiclient.ApiClient, error) {
scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
if err != nil {
return nil, fmt.Errorf("loading list of installed hub scenarios: %w", err)
}
appsecRules, err := hub.GetInstalledNamesByType(cwhub.APPSEC_RULES)
if err != nil {
return nil, fmt.Errorf("loading list of installed hub appsec rules: %w", err)
}
installedScenariosAndAppsecRules := make([]string, 0, len(scenarios)+len(appsecRules))
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, scenarios...)
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, appsecRules...)
apiURL, err := url.Parse(credentials.URL)
if err != nil {
return nil, fmt.Errorf("parsing api url ('%s'): %w", credentials.URL, err)
}
papiURL, err := url.Parse(credentials.PapiURL)
if err != nil {
return nil, fmt.Errorf("parsing polling api url ('%s'): %w", credentials.PapiURL, err)
}
password := strfmt.Password(credentials.Password)
client, err := apiclient.NewClient(&apiclient.Config{
MachineID: credentials.Login,
Password: password,
Scenarios: installedScenariosAndAppsecRules,
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiURL,
PapiURL: papiURL,
VersionPrefix: "v1",
UpdateScenario: func() ([]string, error) {
scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
if err != nil {
return nil, err
}
appsecRules, err := hub.GetInstalledNamesByType(cwhub.APPSEC_RULES)
if err != nil {
return nil, err
}
ret := make([]string, 0, len(scenarios)+len(appsecRules))
ret = append(ret, scenarios...)
ret = append(ret, appsecRules...)
return ret, nil
},
})
if err != nil {
return nil, fmt.Errorf("new client api: %w", err)
}
authResp, _, err := client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
MachineID: &credentials.Login,
Password: &password,
Scenarios: installedScenariosAndAppsecRules,
})
if err != nil {
return nil, fmt.Errorf("authenticate watcher (%s): %w", credentials.Login, err)
}
var expiration time.Time
if err := expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
return nil, fmt.Errorf("unable to parse jwt expiration: %w", err)
}
client.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
client.GetClient().Transport.(*apiclient.JWTTransport).Expiration = expiration
return client, nil
}

View file

@ -1,19 +1,22 @@
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"path/filepath"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/tomb.v2" "gopkg.in/tomb.v2"
"github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/crowdsec/pkg/acquisition" "github.com/crowdsecurity/crowdsec/pkg/acquisition"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/csplugin"
@ -72,7 +75,11 @@ type Flags struct {
DisableCAPI bool DisableCAPI bool
Transform string Transform string
OrderEvent bool OrderEvent bool
CpuProfile string CPUProfile string
}
func (f *Flags) haveTimeMachine() bool {
return f.OneShotDSN != ""
} }
type labelsMap map[string]string type labelsMap map[string]string
@ -92,10 +99,10 @@ func LoadBuckets(cConfig *csconfig.Config, hub *cwhub.Hub) error {
buckets = leakybucket.NewBuckets() buckets = leakybucket.NewBuckets()
log.Infof("Loading %d scenario files", len(files)) log.Infof("Loading %d scenario files", len(files))
holders, outputEventChan, err = leakybucket.LoadBuckets(cConfig.Crowdsec, hub, files, &bucketsTomb, buckets, flags.OrderEvent)
holders, outputEventChan, err = leakybucket.LoadBuckets(cConfig.Crowdsec, hub, files, &bucketsTomb, buckets, flags.OrderEvent)
if err != nil { if err != nil {
return fmt.Errorf("scenario loading failed: %v", err) return fmt.Errorf("scenario loading failed: %w", err)
} }
if cConfig.Prometheus != nil && cConfig.Prometheus.Enabled { if cConfig.Prometheus != nil && cConfig.Prometheus.Enabled {
@ -107,7 +114,7 @@ func LoadBuckets(cConfig *csconfig.Config, hub *cwhub.Hub) error {
return nil return nil
} }
func LoadAcquisition(cConfig *csconfig.Config) error { func LoadAcquisition(cConfig *csconfig.Config) ([]acquisition.DataSource, error) {
var err error var err error
if flags.SingleFileType != "" && flags.OneShotDSN != "" { if flags.SingleFileType != "" && flags.OneShotDSN != "" {
@ -116,20 +123,20 @@ func LoadAcquisition(cConfig *csconfig.Config) error {
dataSources, err = acquisition.LoadAcquisitionFromDSN(flags.OneShotDSN, flags.Labels, flags.Transform) dataSources, err = acquisition.LoadAcquisitionFromDSN(flags.OneShotDSN, flags.Labels, flags.Transform)
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to configure datasource for %s", flags.OneShotDSN) return nil, fmt.Errorf("failed to configure datasource for %s: %w", flags.OneShotDSN, err)
} }
} else { } else {
dataSources, err = acquisition.LoadAcquisitionFromFile(cConfig.Crowdsec) dataSources, err = acquisition.LoadAcquisitionFromFile(cConfig.Crowdsec, cConfig.Prometheus)
if err != nil { if err != nil {
return err return nil, err
} }
} }
if len(dataSources) == 0 { if len(dataSources) == 0 {
return fmt.Errorf("no datasource enabled") return nil, errors.New("no datasource enabled")
} }
return nil return dataSources, nil
} }
var ( var (
@ -181,7 +188,7 @@ func (f *Flags) Parse() {
} }
flag.StringVar(&dumpFolder, "dump-data", "", "dump parsers/buckets raw outputs") flag.StringVar(&dumpFolder, "dump-data", "", "dump parsers/buckets raw outputs")
flag.StringVar(&f.CpuProfile, "cpu-profile", "", "write cpu profile to file") flag.StringVar(&f.CPUProfile, "cpu-profile", "", "write cpu profile to file")
flag.Parse() flag.Parse()
} }
@ -226,6 +233,10 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
return nil, fmt.Errorf("while loading configuration file: %w", err) return nil, fmt.Errorf("while loading configuration file: %w", err)
} }
if err := trace.Init(filepath.Join(cConfig.ConfigPaths.DataDir, "trace")); err != nil {
return nil, fmt.Errorf("while setting up trace directory: %w", err)
}
cConfig.Common.LogLevel = newLogLevel(cConfig.Common.LogLevel, flags) cConfig.Common.LogLevel = newLogLevel(cConfig.Common.LogLevel, flags)
if dumpFolder != "" { if dumpFolder != "" {
@ -249,7 +260,12 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
return nil, err return nil, err
} }
primalHook.Enabled = (cConfig.Common.LogMedia != "stdout") if cConfig.Common.LogMedia != "stdout" {
log.AddHook(&FatalHook{
Writer: os.Stderr,
LogLevels: []log.Level{log.FatalLevel, log.PanicLevel},
})
}
if err := csconfig.LoadFeatureFlagsFile(configFile, log.StandardLogger()); err != nil { if err := csconfig.LoadFeatureFlagsFile(configFile, log.StandardLogger()); err != nil {
return nil, err return nil, err
@ -262,7 +278,7 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
} }
if !cConfig.DisableAPI { if !cConfig.DisableAPI {
if err := cConfig.LoadAPIServer(); err != nil { if err := cConfig.LoadAPIServer(false); err != nil {
return nil, err return nil, err
} }
} }
@ -272,7 +288,7 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
} }
if cConfig.DisableAPI && cConfig.DisableAgent { if cConfig.DisableAPI && cConfig.DisableAgent {
return nil, errors.New("You must run at least the API Server or crowdsec") return nil, errors.New("you must run at least the API Server or crowdsec")
} }
if flags.OneShotDSN != "" && flags.SingleFileType == "" { if flags.OneShotDSN != "" && flags.SingleFileType == "" {
@ -323,7 +339,9 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
var crowdsecT0 time.Time var crowdsecT0 time.Time
func main() { func main() {
log.AddHook(primalHook) // The initial log level is INFO, even if the user provided an -error or -warning flag
// because we need feature flags before parsing cli flags
log.SetFormatter(&log.TextFormatter{TimestampFormat: time.RFC3339, FullTimestamp: true})
if err := fflag.RegisterAllFeatures(); err != nil { if err := fflag.RegisterAllFeatures(); err != nil {
log.Fatalf("failed to register features: %s", err) log.Fatalf("failed to register features: %s", err)
@ -355,16 +373,19 @@ func main() {
os.Exit(0) os.Exit(0)
} }
if flags.CpuProfile != "" { if flags.CPUProfile != "" {
f, err := os.Create(flags.CpuProfile) f, err := os.Create(flags.CPUProfile)
if err != nil { if err != nil {
log.Fatalf("could not create CPU profile: %s", err) log.Fatalf("could not create CPU profile: %s", err)
} }
log.Infof("CPU profile will be written to %s", flags.CpuProfile)
log.Infof("CPU profile will be written to %s", flags.CPUProfile)
if err := pprof.StartCPUProfile(f); err != nil { if err := pprof.StartCPUProfile(f); err != nil {
f.Close() f.Close()
log.Fatalf("could not start CPU profile: %s", err) log.Fatalf("could not start CPU profile: %s", err)
} }
defer f.Close() defer f.Close()
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }

View file

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@ -12,6 +11,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/trace" "github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers/v1" v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers/v1"
"github.com/crowdsecurity/crowdsec/pkg/cache" "github.com/crowdsecurity/crowdsec/pkg/cache"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -21,7 +21,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/parser" "github.com/crowdsecurity/crowdsec/pkg/parser"
) )
/*prometheus*/ // Prometheus
var globalParserHits = prometheus.NewCounterVec( var globalParserHits = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "cs_parser_hits_total", Name: "cs_parser_hits_total",
@ -29,6 +30,7 @@ var globalParserHits = prometheus.NewCounterVec(
}, },
[]string{"source", "type"}, []string{"source", "type"},
) )
var globalParserHitsOk = prometheus.NewCounterVec( var globalParserHitsOk = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "cs_parser_hits_ok_total", Name: "cs_parser_hits_ok_total",
@ -36,6 +38,7 @@ var globalParserHitsOk = prometheus.NewCounterVec(
}, },
[]string{"source", "type"}, []string{"source", "type"},
) )
var globalParserHitsKo = prometheus.NewCounterVec( var globalParserHitsKo = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "cs_parser_hits_ko_total", Name: "cs_parser_hits_ko_total",
@ -102,25 +105,29 @@ var globalPourHistogram = prometheus.NewHistogramVec(
func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.HandlerFunc { func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//update cache metrics (stash) // catch panics here because they are not handled by servePrometheus
defer trace.CatchPanic("crowdsec/computeDynamicMetrics")
// update cache metrics (stash)
cache.UpdateCacheMetrics() cache.UpdateCacheMetrics()
//update cache metrics (regexp) // update cache metrics (regexp)
exprhelpers.UpdateRegexpCacheMetrics() exprhelpers.UpdateRegexpCacheMetrics()
//decision metrics are only relevant for LAPI // decision metrics are only relevant for LAPI
if dbClient == nil { if dbClient == nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
decisionsFilters := make(map[string][]string, 0) decisions, err := dbClient.QueryDecisionCountByScenario()
decisions, err := dbClient.QueryDecisionCountByScenario(decisionsFilters)
if err != nil { if err != nil {
log.Errorf("Error querying decisions for metrics: %v", err) log.Errorf("Error querying decisions for metrics: %v", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
globalActiveDecisions.Reset() globalActiveDecisions.Reset()
for _, d := range decisions { for _, d := range decisions {
globalActiveDecisions.With(prometheus.Labels{"reason": d.Scenario, "origin": d.Origin, "action": d.Type}).Set(float64(d.Count)) globalActiveDecisions.With(prometheus.Labels{"reason": d.Scenario, "origin": d.Origin, "action": d.Type}).Set(float64(d.Count))
} }
@ -132,10 +139,10 @@ func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.Ha
} }
alerts, err := dbClient.AlertsCountPerScenario(alertsFilter) alerts, err := dbClient.AlertsCountPerScenario(alertsFilter)
if err != nil { if err != nil {
log.Errorf("Error querying alerts for metrics: %v", err) log.Errorf("Error querying alerts for metrics: %v", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -154,14 +161,14 @@ func registerPrometheus(config *csconfig.PrometheusCfg) {
// Registering prometheus // Registering prometheus
// If in aggregated mode, do not register events associated with a source, to keep the cardinality low // If in aggregated mode, do not register events associated with a source, to keep the cardinality low
if config.Level == "aggregated" { if config.Level == configuration.CFG_METRICS_AGGREGATE {
log.Infof("Loading aggregated prometheus collectors") log.Infof("Loading aggregated prometheus collectors")
prometheus.MustRegister(globalParserHits, globalParserHitsOk, globalParserHitsKo, prometheus.MustRegister(globalParserHits, globalParserHitsOk, globalParserHitsKo,
globalCsInfo, globalParsingHistogram, globalPourHistogram, globalCsInfo, globalParsingHistogram, globalPourHistogram,
leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow, leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow,
v1.LapiRouteHits, v1.LapiRouteHits,
leaky.BucketsCurrentCount, leaky.BucketsCurrentCount,
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics, cache.CacheMetrics, exprhelpers.RegexpCacheMetrics, parser.NodesWlHitsOk, parser.NodesWlHits,
) )
} else { } else {
log.Infof("Loading prometheus collectors") log.Infof("Loading prometheus collectors")
@ -170,14 +177,15 @@ func registerPrometheus(config *csconfig.PrometheusCfg) {
globalCsInfo, globalParsingHistogram, globalPourHistogram, globalCsInfo, globalParsingHistogram, globalPourHistogram,
v1.LapiRouteHits, v1.LapiMachineHits, v1.LapiBouncerHits, v1.LapiNilDecisions, v1.LapiNonNilDecisions, v1.LapiResponseTime, v1.LapiRouteHits, v1.LapiMachineHits, v1.LapiBouncerHits, v1.LapiNilDecisions, v1.LapiNonNilDecisions, v1.LapiResponseTime,
leaky.BucketsPour, leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow, leaky.BucketsCurrentCount, leaky.BucketsPour, leaky.BucketsUnderflow, leaky.BucketsCanceled, leaky.BucketsInstantiation, leaky.BucketsOverflow, leaky.BucketsCurrentCount,
globalActiveDecisions, globalAlerts, globalActiveDecisions, globalAlerts, parser.NodesWlHitsOk, parser.NodesWlHits,
cache.CacheMetrics, exprhelpers.RegexpCacheMetrics, cache.CacheMetrics, exprhelpers.RegexpCacheMetrics,
) )
} }
} }
func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client, apiReady chan bool, agentReady chan bool) { func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client, agentReady chan bool) {
<-agentReady
if !config.Enabled { if !config.Enabled {
return return
} }
@ -185,10 +193,11 @@ func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client,
defer trace.CatchPanic("crowdsec/servePrometheus") defer trace.CatchPanic("crowdsec/servePrometheus")
http.Handle("/metrics", computeDynamicMetrics(promhttp.Handler(), dbClient)) http.Handle("/metrics", computeDynamicMetrics(promhttp.Handler(), dbClient))
<-apiReady
<-agentReady
log.Debugf("serving metrics after %s ms", time.Since(crowdsecT0))
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", config.ListenAddr, config.ListenPort), nil); err != nil { if err := http.ListenAndServe(fmt.Sprintf("%s:%d", config.ListenAddr, config.ListenPort), nil); err != nil {
log.Warningf("prometheus: %s", err) // in time machine, we most likely have the LAPI using the port
if !flags.haveTimeMachine() {
log.Warningf("prometheus: %s", err)
}
} }
} }

View file

@ -3,18 +3,12 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"sync" "sync"
"time" "time"
"github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket" leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/parser" "github.com/crowdsecurity/crowdsec/pkg/parser"
@ -22,7 +16,6 @@ import (
) )
func dedupAlerts(alerts []types.RuntimeAlert) ([]*models.Alert, error) { func dedupAlerts(alerts []types.RuntimeAlert) ([]*models.Alert, error) {
var dedupCache []*models.Alert var dedupCache []*models.Alert
for idx, alert := range alerts { for idx, alert := range alerts {
@ -32,16 +25,21 @@ func dedupAlerts(alerts []types.RuntimeAlert) ([]*models.Alert, error) {
dedupCache = append(dedupCache, alert.Alert) dedupCache = append(dedupCache, alert.Alert)
continue continue
} }
for k, src := range alert.Sources { for k, src := range alert.Sources {
refsrc := *alert.Alert //copy refsrc := *alert.Alert // copy
log.Tracef("source[%s]", k) log.Tracef("source[%s]", k)
refsrc.Source = &src refsrc.Source = &src
dedupCache = append(dedupCache, &refsrc) dedupCache = append(dedupCache, &refsrc)
} }
} }
if len(dedupCache) != len(alerts) { if len(dedupCache) != len(alerts) {
log.Tracef("went from %d to %d alerts", len(alerts), len(dedupCache)) log.Tracef("went from %d to %d alerts", len(alerts), len(dedupCache))
} }
return dedupCache, nil return dedupCache, nil
} }
@ -52,93 +50,25 @@ func PushAlerts(alerts []types.RuntimeAlert, client *apiclient.ApiClient) error
if err != nil { if err != nil {
return fmt.Errorf("failed to transform alerts for api: %w", err) return fmt.Errorf("failed to transform alerts for api: %w", err)
} }
_, _, err = client.Alerts.Add(ctx, alertsToPush) _, _, err = client.Alerts.Add(ctx, alertsToPush)
if err != nil { if err != nil {
return fmt.Errorf("failed sending alert to LAPI: %w", err) return fmt.Errorf("failed sending alert to LAPI: %w", err)
} }
return nil return nil
} }
var bucketOverflows []types.Event var bucketOverflows []types.Event
func runOutput(input chan types.Event, overflow chan types.Event, buckets *leaky.Buckets, func runOutput(input chan types.Event, overflow chan types.Event, buckets *leaky.Buckets, postOverflowCTX parser.UnixParserCtx,
postOverflowCTX parser.UnixParserCtx, postOverflowNodes []parser.Node, postOverflowNodes []parser.Node, client *apiclient.ApiClient) error {
apiConfig csconfig.ApiCredentialsCfg, hub *cwhub.Hub) error { var (
cache []types.RuntimeAlert
cacheMutex sync.Mutex
)
var err error
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
var cache []types.RuntimeAlert
var cacheMutex sync.Mutex
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
if err != nil {
return fmt.Errorf("loading list of installed hub scenarios: %w", err)
}
appsecRules, err := hub.GetInstalledItemNames(cwhub.APPSEC_RULES)
if err != nil {
return fmt.Errorf("loading list of installed hub appsec rules: %w", err)
}
installedScenariosAndAppsecRules := make([]string, 0, len(scenarios)+len(appsecRules))
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, scenarios...)
installedScenariosAndAppsecRules = append(installedScenariosAndAppsecRules, appsecRules...)
apiURL, err := url.Parse(apiConfig.URL)
if err != nil {
return fmt.Errorf("parsing api url ('%s'): %w", apiConfig.URL, err)
}
papiURL, err := url.Parse(apiConfig.PapiURL)
if err != nil {
return fmt.Errorf("parsing polling api url ('%s'): %w", apiConfig.PapiURL, err)
}
password := strfmt.Password(apiConfig.Password)
Client, err := apiclient.NewClient(&apiclient.Config{
MachineID: apiConfig.Login,
Password: password,
Scenarios: installedScenariosAndAppsecRules,
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiURL,
PapiURL: papiURL,
VersionPrefix: "v1",
UpdateScenario: func() ([]string, error) {
scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
if err != nil {
return nil, err
}
appsecRules, err := hub.GetInstalledItemNames(cwhub.APPSEC_RULES)
if err != nil {
return nil, err
}
ret := make([]string, 0, len(scenarios)+len(appsecRules))
ret = append(ret, scenarios...)
ret = append(ret, appsecRules...)
return ret, nil
},
})
if err != nil {
return fmt.Errorf("new client api: %w", err)
}
authResp, _, err := Client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
MachineID: &apiConfig.Login,
Password: &password,
Scenarios: installedScenariosAndAppsecRules,
})
if err != nil {
return fmt.Errorf("authenticate watcher (%s): %w", apiConfig.Login, err)
}
if err := Client.GetClient().Transport.(*apiclient.JWTTransport).Expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
return fmt.Errorf("unable to parse jwt expiration: %w", err)
}
Client.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
//start the heartbeat service
log.Debugf("Starting HeartBeat service")
Client.HeartBeat.StartHeartBeat(context.Background(), &outputsTomb)
LOOP: LOOP:
for { for {
select { select {
@ -149,9 +79,9 @@ LOOP:
newcache := make([]types.RuntimeAlert, 0) newcache := make([]types.RuntimeAlert, 0)
cache = newcache cache = newcache
cacheMutex.Unlock() cacheMutex.Unlock()
if err := PushAlerts(cachecopy, Client); err != nil { if err := PushAlerts(cachecopy, client); err != nil {
log.Errorf("while pushing to api : %s", err) log.Errorf("while pushing to api : %s", err)
//just push back the events to the queue // just push back the events to the queue
cacheMutex.Lock() cacheMutex.Lock()
cache = append(cache, cachecopy...) cache = append(cache, cachecopy...)
cacheMutex.Unlock() cacheMutex.Unlock()
@ -162,10 +92,11 @@ LOOP:
cacheMutex.Lock() cacheMutex.Lock()
cachecopy := cache cachecopy := cache
cacheMutex.Unlock() cacheMutex.Unlock()
if err := PushAlerts(cachecopy, Client); err != nil { if err := PushAlerts(cachecopy, client); err != nil {
log.Errorf("while pushing leftovers to api : %s", err) log.Errorf("while pushing leftovers to api : %s", err)
} }
} }
break LOOP break LOOP
case event := <-overflow: case event := <-overflow:
/*if alert is empty and mapKey is present, the overflow is just to cleanup bucket*/ /*if alert is empty and mapKey is present, the overflow is just to cleanup bucket*/
@ -176,11 +107,11 @@ LOOP:
/* process post overflow parser nodes */ /* process post overflow parser nodes */
event, err := parser.Parse(postOverflowCTX, event, postOverflowNodes) event, err := parser.Parse(postOverflowCTX, event, postOverflowNodes)
if err != nil { if err != nil {
return fmt.Errorf("postoverflow failed : %s", err) return fmt.Errorf("postoverflow failed: %w", err)
} }
log.Printf("%s", *event.Overflow.Alert.Message) log.Printf("%s", *event.Overflow.Alert.Message)
//if the Alert is nil, it's to signal bucket is ready for GC, don't track this // if the Alert is nil, it's to signal bucket is ready for GC, don't track this
//dump after postoveflow processing to avoid missing whitelist info // dump after postoveflow processing to avoid missing whitelist info
if dumpStates && event.Overflow.Alert != nil { if dumpStates && event.Overflow.Alert != nil {
if bucketOverflows == nil { if bucketOverflows == nil {
bucketOverflows = make([]types.Event, 0) bucketOverflows = make([]types.Event, 0)
@ -206,6 +137,6 @@ LOOP:
} }
ticker.Stop() ticker.Stop()
return nil
return nil
} }

View file

@ -11,7 +11,6 @@ import (
) )
func runParse(input chan types.Event, output chan types.Event, parserCTX parser.UnixParserCtx, nodes []parser.Node) error { func runParse(input chan types.Event, output chan types.Event, parserCTX parser.UnixParserCtx, nodes []parser.Node) error {
LOOP: LOOP:
for { for {
select { select {
@ -56,5 +55,6 @@ LOOP:
output <- parsed output <- parsed
} }
} }
return nil return nil
} }

View file

@ -4,27 +4,30 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket" leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
) )
func runPour(input chan types.Event, holders []leaky.BucketFactory, buckets *leaky.Buckets, cConfig *csconfig.Config) error { func runPour(input chan types.Event, holders []leaky.BucketFactory, buckets *leaky.Buckets, cConfig *csconfig.Config) error {
count := 0 count := 0
for { for {
//bucket is now ready // bucket is now ready
select { select {
case <-bucketsTomb.Dying(): case <-bucketsTomb.Dying():
log.Infof("Bucket routine exiting") log.Infof("Bucket routine exiting")
return nil return nil
case parsed := <-input: case parsed := <-input:
startTime := time.Now() startTime := time.Now()
count++ count++
if count%5000 == 0 { if count%5000 == 0 {
log.Infof("%d existing buckets", leaky.LeakyRoutineCount) log.Infof("%d existing buckets", leaky.LeakyRoutineCount)
//when in forensics mode, garbage collect buckets // when in forensics mode, garbage collect buckets
if cConfig.Crowdsec.BucketsGCEnabled { if cConfig.Crowdsec.BucketsGCEnabled {
if parsed.MarshaledTime != "" { if parsed.MarshaledTime != "" {
z := &time.Time{} z := &time.Time{}
@ -32,26 +35,30 @@ func runPour(input chan types.Event, holders []leaky.BucketFactory, buckets *lea
log.Warningf("Failed to unmarshal time from event '%s' : %s", parsed.MarshaledTime, err) log.Warningf("Failed to unmarshal time from event '%s' : %s", parsed.MarshaledTime, err)
} else { } else {
log.Warning("Starting buckets garbage collection ...") log.Warning("Starting buckets garbage collection ...")
if err = leaky.GarbageCollectBuckets(*z, buckets); err != nil { if err = leaky.GarbageCollectBuckets(*z, buckets); err != nil {
return fmt.Errorf("failed to start bucket GC : %s", err) return fmt.Errorf("failed to start bucket GC : %w", err)
} }
} }
} }
} }
} }
//here we can bucketify with parsed // here we can bucketify with parsed
poured, err := leaky.PourItemToHolders(parsed, holders, buckets) poured, err := leaky.PourItemToHolders(parsed, holders, buckets)
if err != nil { if err != nil {
log.Errorf("bucketify failed for: %v", parsed) log.Errorf("bucketify failed for: %v", parsed)
continue continue
} }
elapsed := time.Since(startTime) elapsed := time.Since(startTime)
globalPourHistogram.With(prometheus.Labels{"type": parsed.Line.Module, "source": parsed.Line.Src}).Observe(elapsed.Seconds()) globalPourHistogram.With(prometheus.Labels{"type": parsed.Line.Module, "source": parsed.Line.Src}).Observe(elapsed.Seconds())
if poured { if poured {
globalBucketPourOk.Inc() globalBucketPourOk.Inc()
} else { } else {
globalBucketPourKo.Inc() globalBucketPourKo.Inc()
} }
if len(parsed.MarshaledTime) != 0 { if len(parsed.MarshaledTime) != 0 {
if err := lastProcessedItem.UnmarshalText([]byte(parsed.MarshaledTime)); err != nil { if err := lastProcessedItem.UnmarshalText([]byte(parsed.MarshaledTime)); err != nil {
log.Warningf("failed to unmarshal time from event : %s", err) log.Warningf("failed to unmarshal time from event : %s", err)

View file

@ -23,8 +23,8 @@ func StartRunSvc() error {
defer trace.CatchPanic("crowdsec/StartRunSvc") defer trace.CatchPanic("crowdsec/StartRunSvc")
//Always try to stop CPU profiling to avoid passing flags around // Always try to stop CPU profiling to avoid passing flags around
//It's a noop if profiling is not enabled // It's a noop if profiling is not enabled
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
if cConfig, err = LoadConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI, false); err != nil { if cConfig, err = LoadConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI, false); err != nil {
@ -33,7 +33,6 @@ func StartRunSvc() error {
log.Infof("Crowdsec %s", version.String()) log.Infof("Crowdsec %s", version.String())
apiReady := make(chan bool, 1)
agentReady := make(chan bool, 1) agentReady := make(chan bool, 1)
// Enable profiling early // Enable profiling early
@ -46,14 +45,19 @@ func StartRunSvc() error {
dbClient, err = database.NewClient(cConfig.DbConfig) dbClient, err = database.NewClient(cConfig.DbConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create database client: %s", err) return fmt.Errorf("unable to create database client: %w", err)
} }
} }
registerPrometheus(cConfig.Prometheus) registerPrometheus(cConfig.Prometheus)
go servePrometheus(cConfig.Prometheus, dbClient, apiReady, agentReady) go servePrometheus(cConfig.Prometheus, dbClient, agentReady)
} else {
// avoid leaking the channel
go func() {
<-agentReady
}()
} }
return Serve(cConfig, apiReady, agentReady) return Serve(cConfig, agentReady)
} }

View file

@ -20,8 +20,8 @@ func StartRunSvc() error {
defer trace.CatchPanic("crowdsec/StartRunSvc") defer trace.CatchPanic("crowdsec/StartRunSvc")
//Always try to stop CPU profiling to avoid passing flags around // Always try to stop CPU profiling to avoid passing flags around
//It's a noop if profiling is not enabled // It's a noop if profiling is not enabled
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
isRunninginService, err := svc.IsWindowsService() isRunninginService, err := svc.IsWindowsService()
@ -73,7 +73,6 @@ func WindowsRun() error {
log.Infof("Crowdsec %s", version.String()) log.Infof("Crowdsec %s", version.String())
apiReady := make(chan bool, 1)
agentReady := make(chan bool, 1) agentReady := make(chan bool, 1)
// Enable profiling early // Enable profiling early
@ -85,11 +84,11 @@ func WindowsRun() error {
dbClient, err = database.NewClient(cConfig.DbConfig) dbClient, err = database.NewClient(cConfig.DbConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create database client: %s", err) return fmt.Errorf("unable to create database client: %w", err)
} }
} }
registerPrometheus(cConfig.Prometheus) registerPrometheus(cConfig.Prometheus)
go servePrometheus(cConfig.Prometheus, dbClient, apiReady, agentReady) go servePrometheus(cConfig.Prometheus, dbClient, agentReady)
} }
return Serve(cConfig, apiReady, agentReady) return Serve(cConfig, agentReady)
} }

View file

@ -42,7 +42,9 @@ func debugHandler(sig os.Signal, cConfig *csconfig.Config) error {
if err := leaky.ShutdownAllBuckets(buckets); err != nil { if err := leaky.ShutdownAllBuckets(buckets); err != nil {
log.Warningf("Failed to shut down routines : %s", err) log.Warningf("Failed to shut down routines : %s", err)
} }
log.Printf("Shutdown is finished, buckets are in %s", tmpFile) log.Printf("Shutdown is finished, buckets are in %s", tmpFile)
return nil return nil
} }
@ -66,15 +68,16 @@ func reloadHandler(sig os.Signal) (*csconfig.Config, error) {
if !cConfig.DisableAPI { if !cConfig.DisableAPI {
if flags.DisableCAPI { if flags.DisableCAPI {
log.Warningf("Communication with CrowdSec Central API disabled from args") log.Warningf("Communication with CrowdSec Central API disabled from args")
cConfig.API.Server.OnlineClient = nil cConfig.API.Server.OnlineClient = nil
} }
apiServer, err := initAPIServer(cConfig) apiServer, err := initAPIServer(cConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to init api server: %w", err) return nil, fmt.Errorf("unable to init api server: %w", err)
} }
apiReady := make(chan bool, 1) serveAPIServer(apiServer)
serveAPIServer(apiServer, apiReady)
} }
if !cConfig.DisableAgent { if !cConfig.DisableAgent {
@ -83,7 +86,7 @@ func reloadHandler(sig os.Signal) (*csconfig.Config, error) {
return nil, fmt.Errorf("while loading hub index: %w", err) return nil, fmt.Errorf("while loading hub index: %w", err)
} }
csParsers, err := initCrowdsec(cConfig, hub) csParsers, datasources, err := initCrowdsec(cConfig, hub)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to init crowdsec: %w", err) return nil, fmt.Errorf("unable to init crowdsec: %w", err)
} }
@ -100,7 +103,7 @@ func reloadHandler(sig os.Signal) (*csconfig.Config, error) {
} }
agentReady := make(chan bool, 1) agentReady := make(chan bool, 1)
serveCrowdsec(csParsers, cConfig, hub, agentReady) serveCrowdsec(csParsers, cConfig, hub, datasources, agentReady)
} }
log.Printf("Reload is finished") log.Printf("Reload is finished")
@ -110,6 +113,7 @@ func reloadHandler(sig os.Signal) (*csconfig.Config, error) {
log.Warningf("Failed to delete temp file (%s) : %s", tmpFile, err) log.Warningf("Failed to delete temp file (%s) : %s", tmpFile, err)
} }
} }
return cConfig, nil return cConfig, nil
} }
@ -117,10 +121,12 @@ func ShutdownCrowdsecRoutines() error {
var reterr error var reterr error
log.Debugf("Shutting down crowdsec sub-routines") log.Debugf("Shutting down crowdsec sub-routines")
if len(dataSources) > 0 { if len(dataSources) > 0 {
acquisTomb.Kill(nil) acquisTomb.Kill(nil)
log.Debugf("waiting for acquisition to finish") log.Debugf("waiting for acquisition to finish")
drainChan(inputLineChan) drainChan(inputLineChan)
if err := acquisTomb.Wait(); err != nil { if err := acquisTomb.Wait(); err != nil {
log.Warningf("Acquisition returned error : %s", err) log.Warningf("Acquisition returned error : %s", err)
reterr = err reterr = err
@ -130,6 +136,7 @@ func ShutdownCrowdsecRoutines() error {
log.Debugf("acquisition is finished, wait for parser/bucket/ouputs.") log.Debugf("acquisition is finished, wait for parser/bucket/ouputs.")
parsersTomb.Kill(nil) parsersTomb.Kill(nil)
drainChan(inputEventChan) drainChan(inputEventChan)
if err := parsersTomb.Wait(); err != nil { if err := parsersTomb.Wait(); err != nil {
log.Warningf("Parsers returned error : %s", err) log.Warningf("Parsers returned error : %s", err)
reterr = err reterr = err
@ -160,6 +167,7 @@ func ShutdownCrowdsecRoutines() error {
log.Warningf("Outputs returned error : %s", err) log.Warningf("Outputs returned error : %s", err)
reterr = err reterr = err
} }
log.Debugf("outputs are done") log.Debugf("outputs are done")
case <-time.After(3 * time.Second): case <-time.After(3 * time.Second):
// this can happen if outputs are stuck in a http retry loop // this can happen if outputs are stuck in a http retry loop
@ -181,6 +189,7 @@ func shutdownAPI() error {
} }
log.Debugf("done") log.Debugf("done")
return nil return nil
} }
@ -193,6 +202,7 @@ func shutdownCrowdsec() error {
} }
log.Debugf("done") log.Debugf("done")
return nil return nil
} }
@ -220,7 +230,7 @@ func drainChan(c chan types.Event) {
for { for {
select { select {
case _, ok := <-c: case _, ok := <-c:
if !ok { //closed if !ok { // closed
return return
} }
default: default:
@ -246,8 +256,8 @@ func HandleSignals(cConfig *csconfig.Config) error {
exitChan := make(chan error) exitChan := make(chan error)
//Always try to stop CPU profiling to avoid passing flags around // Always try to stop CPU profiling to avoid passing flags around
//It's a noop if profiling is not enabled // It's a noop if profiling is not enabled
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
go func() { go func() {
@ -292,10 +302,11 @@ func HandleSignals(cConfig *csconfig.Config) error {
if err == nil { if err == nil {
log.Warning("Crowdsec service shutting down") log.Warning("Crowdsec service shutting down")
} }
return err return err
} }
func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) error { func Serve(cConfig *csconfig.Config, agentReady chan bool) error {
acquisTomb = tomb.Tomb{} acquisTomb = tomb.Tomb{}
parsersTomb = tomb.Tomb{} parsersTomb = tomb.Tomb{}
bucketsTomb = tomb.Tomb{} bucketsTomb = tomb.Tomb{}
@ -325,6 +336,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
if cConfig.API.CTI != nil && *cConfig.API.CTI.Enabled { if cConfig.API.CTI != nil && *cConfig.API.CTI.Enabled {
log.Infof("Crowdsec CTI helper enabled") log.Infof("Crowdsec CTI helper enabled")
if err := exprhelpers.InitCrowdsecCTI(cConfig.API.CTI.Key, cConfig.API.CTI.CacheTimeout, cConfig.API.CTI.CacheSize, cConfig.API.CTI.LogLevel); err != nil { if err := exprhelpers.InitCrowdsecCTI(cConfig.API.CTI.Key, cConfig.API.CTI.CacheTimeout, cConfig.API.CTI.CacheSize, cConfig.API.CTI.LogLevel); err != nil {
return fmt.Errorf("failed to init crowdsec cti: %w", err) return fmt.Errorf("failed to init crowdsec cti: %w", err)
} }
@ -337,6 +349,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
if flags.DisableCAPI { if flags.DisableCAPI {
log.Warningf("Communication with CrowdSec Central API disabled from args") log.Warningf("Communication with CrowdSec Central API disabled from args")
cConfig.API.Server.OnlineClient = nil cConfig.API.Server.OnlineClient = nil
} }
@ -346,10 +359,8 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
} }
if !flags.TestMode { if !flags.TestMode {
serveAPIServer(apiServer, apiReady) serveAPIServer(apiServer)
} }
} else {
apiReady <- true
} }
if !cConfig.DisableAgent { if !cConfig.DisableAgent {
@ -358,14 +369,16 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
return fmt.Errorf("while loading hub index: %w", err) return fmt.Errorf("while loading hub index: %w", err)
} }
csParsers, err := initCrowdsec(cConfig, hub) csParsers, datasources, err := initCrowdsec(cConfig, hub)
if err != nil { if err != nil {
return fmt.Errorf("crowdsec init: %w", err) return fmt.Errorf("crowdsec init: %w", err)
} }
// if it's just linting, we're done // if it's just linting, we're done
if !flags.TestMode { if !flags.TestMode {
serveCrowdsec(csParsers, cConfig, hub, agentReady) serveCrowdsec(csParsers, cConfig, hub, datasources, agentReady)
} else {
agentReady <- true
} }
} else { } else {
agentReady <- true agentReady <- true
@ -378,7 +391,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
} }
if cConfig.Common != nil && cConfig.Common.Daemonize { if cConfig.Common != nil && cConfig.Common.Daemonize {
csdaemon.NotifySystemd(log.StandardLogger()) csdaemon.Notify(csdaemon.Ready, log.StandardLogger())
// wait for signals // wait for signals
return HandleSignals(cConfig) return HandleSignals(cConfig)
} }
@ -395,6 +408,7 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
for _, ch := range waitChans { for _, ch := range waitChans {
<-ch <-ch
switch ch { switch ch {
case apiTomb.Dead(): case apiTomb.Dead():
log.Infof("api shutdown") log.Infof("api shutdown")
@ -402,5 +416,6 @@ func Serve(cConfig *csconfig.Config, apiReady chan bool, agentReady chan bool) e
log.Infof("crowdsec shutdown") log.Infof("crowdsec shutdown")
} }
} }
return nil return nil
} }

View file

@ -23,7 +23,7 @@ type crowdsec_winservice struct {
config *csconfig.Config config *csconfig.Config
} }
func (m *crowdsec_winservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { func (m *crowdsec_winservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending} changes <- svc.Status{State: svc.StartPending}
tick := time.Tick(500 * time.Millisecond) tick := time.Tick(500 * time.Millisecond)
@ -59,7 +59,8 @@ func (m *crowdsec_winservice) Execute(args []string, r <-chan svc.ChangeRequest,
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return
return false, 0
} }
func runService(name string) error { func runService(name string) error {

View file

@ -5,10 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
) )
type PluginConfig struct { type PluginConfig struct {
@ -32,6 +33,7 @@ func (s *DummyPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
if _, ok := s.PluginConfigByName[notification.Name]; !ok { if _, ok := s.PluginConfigByName[notification.Name]; !ok {
return nil, fmt.Errorf("invalid plugin config name %s", notification.Name) return nil, fmt.Errorf("invalid plugin config name %s", notification.Name)
} }
cfg := s.PluginConfigByName[notification.Name] cfg := s.PluginConfigByName[notification.Name]
if cfg.LogLevel != nil && *cfg.LogLevel != "" { if cfg.LogLevel != nil && *cfg.LogLevel != "" {
@ -42,19 +44,22 @@ func (s *DummyPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
logger.Debug(notification.Text) logger.Debug(notification.Text)
if cfg.OutputFile != nil && *cfg.OutputFile != "" { if cfg.OutputFile != nil && *cfg.OutputFile != "" {
f, err := os.OpenFile(*cfg.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) f, err := os.OpenFile(*cfg.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("Cannot open notification file: %s", err)) logger.Error(fmt.Sprintf("Cannot open notification file: %s", err))
} }
if _, err := f.WriteString(notification.Text + "\n"); err != nil { if _, err := f.WriteString(notification.Text + "\n"); err != nil {
f.Close() f.Close()
logger.Error(fmt.Sprintf("Cannot write notification to file: %s", err)) logger.Error(fmt.Sprintf("Cannot write notification to file: %s", err))
} }
err = f.Close() err = f.Close()
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("Cannot close notification file: %s", err)) logger.Error(fmt.Sprintf("Cannot close notification file: %s", err))
} }
} }
fmt.Println(notification.Text) fmt.Println(notification.Text)
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
@ -64,11 +69,12 @@ func (s *DummyPlugin) Configure(ctx context.Context, config *protobufs.Config) (
d := PluginConfig{} d := PluginConfig{}
err := yaml.Unmarshal(config.Config, &d) err := yaml.Unmarshal(config.Config, &d)
s.PluginConfigByName[d.Name] = d s.PluginConfigByName[d.Name] = d
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
} }
func main() { func main() {
var handshake = plugin.HandshakeConfig{ handshake := plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "CROWDSEC_PLUGIN_KEY", MagicCookieKey: "CROWDSEC_PLUGIN_KEY",
MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"),

View file

@ -2,15 +2,17 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"time" "time"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
mail "github.com/xhit/go-simple-mail/v2" mail "github.com/xhit/go-simple-mail/v2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
) )
var baseLogger hclog.Logger = hclog.New(&hclog.LoggerOptions{ var baseLogger hclog.Logger = hclog.New(&hclog.LoggerOptions{
@ -72,19 +74,20 @@ func (n *EmailPlugin) Configure(ctx context.Context, config *protobufs.Config) (
} }
if d.Name == "" { if d.Name == "" {
return nil, fmt.Errorf("name is required") return nil, errors.New("name is required")
} }
if d.SMTPHost == "" { if d.SMTPHost == "" {
return nil, fmt.Errorf("SMTP host is not set") return nil, errors.New("SMTP host is not set")
} }
if d.ReceiverEmails == nil || len(d.ReceiverEmails) == 0 { if d.ReceiverEmails == nil || len(d.ReceiverEmails) == 0 {
return nil, fmt.Errorf("receiver emails are not set") return nil, errors.New("receiver emails are not set")
} }
n.ConfigByName[d.Name] = d n.ConfigByName[d.Name] = d
baseLogger.Debug(fmt.Sprintf("Email plugin '%s' use SMTP host '%s:%d'", d.Name, d.SMTPHost, d.SMTPPort)) baseLogger.Debug(fmt.Sprintf("Email plugin '%s' use SMTP host '%s:%d'", d.Name, d.SMTPHost, d.SMTPPort))
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
} }
@ -92,6 +95,7 @@ func (n *EmailPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
if _, ok := n.ConfigByName[notification.Name]; !ok { if _, ok := n.ConfigByName[notification.Name]; !ok {
return nil, fmt.Errorf("invalid plugin config name %s", notification.Name) return nil, fmt.Errorf("invalid plugin config name %s", notification.Name)
} }
cfg := n.ConfigByName[notification.Name] cfg := n.ConfigByName[notification.Name]
logger := baseLogger.Named(cfg.Name) logger := baseLogger.Named(cfg.Name)
@ -117,6 +121,7 @@ func (n *EmailPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
server.ConnectTimeout, err = time.ParseDuration(cfg.ConnectTimeout) server.ConnectTimeout, err = time.ParseDuration(cfg.ConnectTimeout)
if err != nil { if err != nil {
logger.Warn(fmt.Sprintf("invalid connect timeout '%s', using default '10s'", cfg.ConnectTimeout)) logger.Warn(fmt.Sprintf("invalid connect timeout '%s', using default '10s'", cfg.ConnectTimeout))
server.ConnectTimeout = 10 * time.Second server.ConnectTimeout = 10 * time.Second
} }
} }
@ -125,15 +130,18 @@ func (n *EmailPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
server.SendTimeout, err = time.ParseDuration(cfg.SendTimeout) server.SendTimeout, err = time.ParseDuration(cfg.SendTimeout)
if err != nil { if err != nil {
logger.Warn(fmt.Sprintf("invalid send timeout '%s', using default '10s'", cfg.SendTimeout)) logger.Warn(fmt.Sprintf("invalid send timeout '%s', using default '10s'", cfg.SendTimeout))
server.SendTimeout = 10 * time.Second server.SendTimeout = 10 * time.Second
} }
} }
logger.Debug("making smtp connection") logger.Debug("making smtp connection")
smtpClient, err := server.Connect() smtpClient, err := server.Connect()
if err != nil { if err != nil {
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
} }
logger.Debug("smtp connection done") logger.Debug("smtp connection done")
email := mail.NewMSG() email := mail.NewMSG()
@ -146,12 +154,14 @@ func (n *EmailPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
if err != nil { if err != nil {
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
} }
logger.Info(fmt.Sprintf("sent email to %v", cfg.ReceiverEmails)) logger.Info(fmt.Sprintf("sent email to %v", cfg.ReceiverEmails))
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
} }
func main() { func main() {
var handshake = plugin.HandshakeConfig{ handshake := plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "CROWDSEC_PLUGIN_KEY", MagicCookieKey: "CROWDSEC_PLUGIN_KEY",
MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"),

View file

@ -7,18 +7,22 @@ import (
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"os" "os"
"strings"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
) )
type PluginConfig struct { type PluginConfig struct {
Name string `yaml:"name"` Name string `yaml:"name"`
URL string `yaml:"url"` URL string `yaml:"url"`
UnixSocket string `yaml:"unix_socket"`
Headers map[string]string `yaml:"headers"` Headers map[string]string `yaml:"headers"`
SkipTLSVerification bool `yaml:"skip_tls_verification"` SkipTLSVerification bool `yaml:"skip_tls_verification"`
Method string `yaml:"method"` Method string `yaml:"method"`
@ -66,42 +70,52 @@ func getCertPool(caPath string) (*x509.CertPool, error) {
return cp, nil return cp, nil
} }
func getTLSClient(tlsVerify bool, caPath, certPath, keyPath string) (*http.Client, error) { func getTLSClient(c *PluginConfig) error {
var client *http.Client caCertPool, err := getCertPool(c.CAPath)
caCertPool, err := getCertPool(caPath)
if err != nil { if err != nil {
return nil, err return err
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
RootCAs: caCertPool, RootCAs: caCertPool,
InsecureSkipVerify: tlsVerify, InsecureSkipVerify: c.SkipTLSVerification,
} }
if certPath != "" && keyPath != "" { if c.CertPath != "" && c.KeyPath != "" {
logger.Info(fmt.Sprintf("Using client certificate '%s' and key '%s'", certPath, keyPath)) logger.Info(fmt.Sprintf("Using client certificate '%s' and key '%s'", c.CertPath, c.KeyPath))
cert, err := tls.LoadX509KeyPair(certPath, keyPath) cert, err := tls.LoadX509KeyPair(c.CertPath, c.KeyPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load client certificate '%s' and key '%s': %w", certPath, keyPath, err) return fmt.Errorf("unable to load client certificate '%s' and key '%s': %w", c.CertPath, c.KeyPath, err)
} }
tlsConfig.Certificates = []tls.Certificate{cert} tlsConfig.Certificates = []tls.Certificate{cert}
} }
client = &http.Client{ transport := &http.Transport{
Transport: &http.Transport{ TLSClientConfig: tlsConfig,
TLSClientConfig: tlsConfig,
},
} }
return client, err
if c.UnixSocket != "" {
logger.Info(fmt.Sprintf("Using socket '%s'", c.UnixSocket))
transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", strings.TrimSuffix(c.UnixSocket, "/"))
}
}
c.Client = &http.Client{
Transport: transport,
}
return nil
} }
func (s *HTTPPlugin) Notify(ctx context.Context, notification *protobufs.Notification) (*protobufs.Empty, error) { func (s *HTTPPlugin) Notify(ctx context.Context, notification *protobufs.Notification) (*protobufs.Empty, error) {
if _, ok := s.PluginConfigByName[notification.Name]; !ok { if _, ok := s.PluginConfigByName[notification.Name]; !ok {
return nil, fmt.Errorf("invalid plugin config name %s", notification.Name) return nil, fmt.Errorf("invalid plugin config name %s", notification.Name)
} }
cfg := s.PluginConfigByName[notification.Name] cfg := s.PluginConfigByName[notification.Name]
if cfg.LogLevel != nil && *cfg.LogLevel != "" { if cfg.LogLevel != nil && *cfg.LogLevel != "" {
@ -114,11 +128,14 @@ func (s *HTTPPlugin) Notify(ctx context.Context, notification *protobufs.Notific
if err != nil { if err != nil {
return nil, err return nil, err
} }
for headerName, headerValue := range cfg.Headers { for headerName, headerValue := range cfg.Headers {
logger.Debug(fmt.Sprintf("adding header %s: %s", headerName, headerValue)) logger.Debug(fmt.Sprintf("adding header %s: %s", headerName, headerValue))
request.Header.Add(headerName, headerValue) request.Header.Add(headerName, headerValue)
} }
logger.Debug(fmt.Sprintf("making HTTP %s call to %s with body %s", cfg.Method, cfg.URL, notification.Text)) logger.Debug(fmt.Sprintf("making HTTP %s call to %s with body %s", cfg.Method, cfg.URL, notification.Text))
resp, err := cfg.Client.Do(request.WithContext(ctx)) resp, err := cfg.Client.Do(request.WithContext(ctx))
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("Failed to make HTTP request : %s", err)) logger.Error(fmt.Sprintf("Failed to make HTTP request : %s", err))
@ -128,13 +145,15 @@ func (s *HTTPPlugin) Notify(ctx context.Context, notification *protobufs.Notific
respData, err := io.ReadAll(resp.Body) respData, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read response body got error %s", err) return nil, fmt.Errorf("failed to read response body got error %w", err)
} }
logger.Debug(fmt.Sprintf("got response %s", string(respData))) logger.Debug(fmt.Sprintf("got response %s", string(respData)))
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
logger.Warn(fmt.Sprintf("HTTP server returned non 200 status code: %d", resp.StatusCode)) logger.Warn(fmt.Sprintf("HTTP server returned non 200 status code: %d", resp.StatusCode))
logger.Debug(fmt.Sprintf("HTTP server returned body: %s", string(respData)))
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
} }
@ -143,21 +162,25 @@ func (s *HTTPPlugin) Notify(ctx context.Context, notification *protobufs.Notific
func (s *HTTPPlugin) Configure(ctx context.Context, config *protobufs.Config) (*protobufs.Empty, error) { func (s *HTTPPlugin) Configure(ctx context.Context, config *protobufs.Config) (*protobufs.Empty, error) {
d := PluginConfig{} d := PluginConfig{}
err := yaml.Unmarshal(config.Config, &d) err := yaml.Unmarshal(config.Config, &d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
d.Client, err = getTLSClient(d.SkipTLSVerification, d.CAPath, d.CertPath, d.KeyPath)
err = getTLSClient(&d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.PluginConfigByName[d.Name] = d s.PluginConfigByName[d.Name] = d
logger.Debug(fmt.Sprintf("HTTP plugin '%s' use URL '%s'", d.Name, d.URL)) logger.Debug(fmt.Sprintf("HTTP plugin '%s' use URL '%s'", d.Name, d.URL))
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
} }
func main() { func main() {
var handshake = plugin.HandshakeConfig{ handshake := plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "CROWDSEC_PLUGIN_KEY", MagicCookieKey: "CROWDSEC_PLUGIN_KEY",
MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"),

View file

@ -5,12 +5,12 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
"github.com/slack-go/slack" "github.com/slack-go/slack"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
) )
type PluginConfig struct { type PluginConfig struct {
@ -33,13 +33,16 @@ func (n *Notify) Notify(ctx context.Context, notification *protobufs.Notificatio
if _, ok := n.ConfigByName[notification.Name]; !ok { if _, ok := n.ConfigByName[notification.Name]; !ok {
return nil, fmt.Errorf("invalid plugin config name %s", notification.Name) return nil, fmt.Errorf("invalid plugin config name %s", notification.Name)
} }
cfg := n.ConfigByName[notification.Name] cfg := n.ConfigByName[notification.Name]
if cfg.LogLevel != nil && *cfg.LogLevel != "" { if cfg.LogLevel != nil && *cfg.LogLevel != "" {
logger.SetLevel(hclog.LevelFromString(*cfg.LogLevel)) logger.SetLevel(hclog.LevelFromString(*cfg.LogLevel))
} }
logger.Info(fmt.Sprintf("found notify signal for %s config", notification.Name)) logger.Info(fmt.Sprintf("found notify signal for %s config", notification.Name))
logger.Debug(fmt.Sprintf("posting to %s webhook, message %s", cfg.Webhook, notification.Text)) logger.Debug(fmt.Sprintf("posting to %s webhook, message %s", cfg.Webhook, notification.Text))
err := slack.PostWebhookContext(ctx, n.ConfigByName[notification.Name].Webhook, &slack.WebhookMessage{ err := slack.PostWebhookContext(ctx, n.ConfigByName[notification.Name].Webhook, &slack.WebhookMessage{
Text: notification.Text, Text: notification.Text,
}) })
@ -52,16 +55,19 @@ func (n *Notify) Notify(ctx context.Context, notification *protobufs.Notificatio
func (n *Notify) Configure(ctx context.Context, config *protobufs.Config) (*protobufs.Empty, error) { func (n *Notify) Configure(ctx context.Context, config *protobufs.Config) (*protobufs.Empty, error) {
d := PluginConfig{} d := PluginConfig{}
if err := yaml.Unmarshal(config.Config, &d); err != nil { if err := yaml.Unmarshal(config.Config, &d); err != nil {
return nil, err return nil, err
} }
n.ConfigByName[d.Name] = d n.ConfigByName[d.Name] = d
logger.Debug(fmt.Sprintf("Slack plugin '%s' use URL '%s'", d.Name, d.Webhook)) logger.Debug(fmt.Sprintf("Slack plugin '%s' use URL '%s'", d.Name, d.Webhook))
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
} }
func main() { func main() {
var handshake = plugin.HandshakeConfig{ handshake := plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "CROWDSEC_PLUGIN_KEY", MagicCookieKey: "CROWDSEC_PLUGIN_KEY",
MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"),

View file

@ -10,11 +10,11 @@ import (
"os" "os"
"strings" "strings"
"github.com/crowdsecurity/crowdsec/pkg/protobufs"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
"gopkg.in/yaml.v3"
"gopkg.in/yaml.v2" "github.com/crowdsecurity/crowdsec/pkg/protobufs"
) )
var logger hclog.Logger = hclog.New(&hclog.LoggerOptions{ var logger hclog.Logger = hclog.New(&hclog.LoggerOptions{
@ -44,6 +44,7 @@ func (s *Splunk) Notify(ctx context.Context, notification *protobufs.Notificatio
if _, ok := s.PluginConfigByName[notification.Name]; !ok { if _, ok := s.PluginConfigByName[notification.Name]; !ok {
return &protobufs.Empty{}, fmt.Errorf("splunk invalid config name %s", notification.Name) return &protobufs.Empty{}, fmt.Errorf("splunk invalid config name %s", notification.Name)
} }
cfg := s.PluginConfigByName[notification.Name] cfg := s.PluginConfigByName[notification.Name]
if cfg.LogLevel != nil && *cfg.LogLevel != "" { if cfg.LogLevel != nil && *cfg.LogLevel != "" {
@ -53,6 +54,7 @@ func (s *Splunk) Notify(ctx context.Context, notification *protobufs.Notificatio
logger.Info(fmt.Sprintf("received notify signal for %s config", notification.Name)) logger.Info(fmt.Sprintf("received notify signal for %s config", notification.Name))
p := Payload{Event: notification.Text} p := Payload{Event: notification.Text}
data, err := json.Marshal(p) data, err := json.Marshal(p)
if err != nil { if err != nil {
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
@ -65,6 +67,7 @@ func (s *Splunk) Notify(ctx context.Context, notification *protobufs.Notificatio
req.Header.Add("Authorization", fmt.Sprintf("Splunk %s", cfg.Token)) req.Header.Add("Authorization", fmt.Sprintf("Splunk %s", cfg.Token))
logger.Debug(fmt.Sprintf("posting event %s to %s", string(data), req.URL)) logger.Debug(fmt.Sprintf("posting event %s to %s", string(data), req.URL))
resp, err := s.Client.Do(req.WithContext(ctx)) resp, err := s.Client.Do(req.WithContext(ctx))
if err != nil { if err != nil {
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
@ -73,15 +76,19 @@ func (s *Splunk) Notify(ctx context.Context, notification *protobufs.Notificatio
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
content, err := io.ReadAll(resp.Body) content, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return &protobufs.Empty{}, fmt.Errorf("got non 200 response and failed to read error %s", err) return &protobufs.Empty{}, fmt.Errorf("got non 200 response and failed to read error %w", err)
} }
return &protobufs.Empty{}, fmt.Errorf("got non 200 response %s", string(content)) return &protobufs.Empty{}, fmt.Errorf("got non 200 response %s", string(content))
} }
respData, err := io.ReadAll(resp.Body) respData, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return &protobufs.Empty{}, fmt.Errorf("failed to read response body got error %s", err) return &protobufs.Empty{}, fmt.Errorf("failed to read response body got error %w", err)
} }
logger.Debug(fmt.Sprintf("got response %s", string(respData))) logger.Debug(fmt.Sprintf("got response %s", string(respData)))
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
} }
@ -90,11 +97,12 @@ func (s *Splunk) Configure(ctx context.Context, config *protobufs.Config) (*prot
err := yaml.Unmarshal(config.Config, &d) err := yaml.Unmarshal(config.Config, &d)
s.PluginConfigByName[d.Name] = d s.PluginConfigByName[d.Name] = d
logger.Debug(fmt.Sprintf("Splunk plugin '%s' use URL '%s'", d.Name, d.URL)) logger.Debug(fmt.Sprintf("Splunk plugin '%s' use URL '%s'", d.Name, d.URL))
return &protobufs.Empty{}, err return &protobufs.Empty{}, err
} }
func main() { func main() {
var handshake = plugin.HandshakeConfig{ handshake := plugin.HandshakeConfig{
ProtocolVersion: 1, ProtocolVersion: 1,
MagicCookieKey: "CROWDSEC_PLUGIN_KEY", MagicCookieKey: "CROWDSEC_PLUGIN_KEY",
MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"),

View file

@ -2,12 +2,13 @@
test -x /usr/bin/cscli || exit 0 test -x /usr/bin/cscli || exit 0
# splay hub upgrade and crowdsec reload
sleep "$(seq 1 300 | shuf -n 1)"
/usr/bin/cscli --error hub update /usr/bin/cscli --error hub update
upgraded=$(/usr/bin/cscli --error hub upgrade) upgraded=$(/usr/bin/cscli --error hub upgrade)
if [ -n "$upgraded" ]; then if [ -n "$upgraded" ]; then
# splay initial metrics push
sleep $(seq 1 90 | shuf -n 1)
systemctl reload crowdsec systemctl reload crowdsec
fi fi

View file

@ -8,6 +8,7 @@ Environment=LC_ALL=C LANG=C
ExecStartPre=/usr/local/bin/crowdsec -c /etc/crowdsec/config.yaml -t -error ExecStartPre=/usr/local/bin/crowdsec -c /etc/crowdsec/config.yaml -t -error
ExecStart=/usr/local/bin/crowdsec -c /etc/crowdsec/config.yaml ExecStart=/usr/local/bin/crowdsec -c /etc/crowdsec/config.yaml
#ExecStartPost=/bin/sleep 0.1 #ExecStartPost=/bin/sleep 0.1
ExecReload=/usr/local/bin/crowdsec -c /etc/crowdsec/config.yaml -t -error
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID
Restart=always Restart=always
RestartSec=60 RestartSec=60

Some files were not shown because too many files have changed in this diff Show more