├── .gitignore ├── .github ├── contributing_header_slack.png ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── gofmt.yml │ ├── test.yml │ └── release.yml ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── examples ├── elasticsearch │ ├── elasticsearch.yml │ ├── README.md │ └── mapping.json ├── elastalert │ ├── systemd.service │ ├── run_uptime.yaml │ ├── upstart.conf │ ├── elastalert.yaml │ └── README.md ├── go-audit │ ├── systemd.service │ ├── upstart.conf │ ├── README.md │ └── go-audit.yaml ├── streamstash │ ├── systemd.service │ ├── upstart.conf │ ├── README.md │ └── streamstash.js ├── README.md ├── rsyslog │ ├── 50-default.conf │ ├── rsyslog.conf │ ├── 01-go-audit.conf │ └── README.md └── kibana │ └── README.md ├── CODEOWNERS ├── contrib ├── logrotate.go-audit.conf ├── line-parser │ ├── package.json │ ├── README.md │ └── line-parser ├── systemd.go-audit.service ├── upstart.go-audit.conf ├── rh-sysv.go-audit.init └── go-audit.rpmbuild.spec ├── make_deb.sh ├── writer.go ├── extras.go ├── Makefile ├── LICENSE ├── extras_helpers.go ├── extras_cgroups.go ├── CHANGELOG.md ├── extras_containers_test.go ├── BATTLE_TESTING.md ├── CODE_OF_CONDUCT.md ├── go.mod ├── marshaller_test.go ├── client.go ├── parser_test.go ├── extras_containers_capsule8.go ├── marshaller.go ├── client_test.go ├── parser.go ├── README.md ├── go-audit.yaml.example ├── extras_containers.go ├── audit.go ├── audit_test.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | go-audit 2 | go-audit.yaml 3 | !examples/** 4 | *.pprof 5 | *.test 6 | *.deb 7 | *.out 8 | node_modules 9 | -------------------------------------------------------------------------------- /.github/contributing_header_slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/go-audit/HEAD/.github/contributing_header_slack.png -------------------------------------------------------------------------------- /examples/elasticsearch/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | # /etc/elasticsearch/elasticsearch.yml 2 | network.host: [ _site_, _local_ ] 3 | node.name: ${HOSTNAME} 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. 2 | #ECCN:Open Source 3 | #GUSINFO:Open Source,Open Source Workflow 4 | -------------------------------------------------------------------------------- /contrib/logrotate.go-audit.conf: -------------------------------------------------------------------------------- 1 | /var/log/go-audit/go-audit.log { 2 | compress 3 | delaycompress 4 | missingok 5 | notifempty 6 | daily 7 | rotate 7 8 | sharedscripts 9 | postrotate 10 | /usr/bin/killall -USR1 go-audit 2>/dev/null || true 11 | endscript 12 | } 13 | -------------------------------------------------------------------------------- /examples/elastalert/systemd.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/elastalert.service 2 | [Unit] 3 | Description = elastalert 4 | After=network.target 5 | 6 | [Service] 7 | Type = simple 8 | ExecStart = /usr/local/bin/elastalert --config /etc/elastalert.yaml 9 | 10 | [Install] 11 | WantedBy = multi-user.target 12 | -------------------------------------------------------------------------------- /contrib/line-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "line-parser", 3 | "description": "Parses go-audit lines from stdin, parses, and outputs on stdout", 4 | "license": "MIT", 5 | "version": "1.0.0", 6 | "dependencies": { 7 | "streamstash": "https://github.com/nbrownus/streamstash#2.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contrib/systemd.go-audit.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/go-audit.service 2 | [Unit] 3 | Description = go-audit 4 | After=network.target auditd.service 5 | Conflicts = auditd.service 6 | 7 | [Service] 8 | Type = simple 9 | ExecStart = /usr/local/bin/go-audit -config /etc/go-audit.yaml 10 | 11 | [Install] 12 | WantedBy = multi-user.target 13 | -------------------------------------------------------------------------------- /examples/go-audit/systemd.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/go-audit.service 2 | [Unit] 3 | Description = go-audit 4 | After=network.target auditd.service 5 | Conflicts = auditd.service 6 | 7 | [Service] 8 | Type = simple 9 | ExecStart = /usr/local/bin/go-audit -config /etc/go-audit.yaml 10 | 11 | [Install] 12 | WantedBy = multi-user.target 13 | -------------------------------------------------------------------------------- /examples/streamstash/systemd.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/streamstash.service 2 | [Unit] 3 | Description = streamstash 4 | After=network.target 5 | 6 | [Service] 7 | Type = simple 8 | # You may have to tweak the path to streamstash here 9 | ExecStart = /usr/local/lib/node_modules/streamstash/bin/streamstash /etc/streamstash.js 10 | User=nobody 11 | Group=nogroup 12 | 13 | [Install] 14 | WantedBy = multi-user.target 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | golang-x-dependencies: 14 | patterns: 15 | - "golang.org/x/*" 16 | protobuf-dependencies: 17 | patterns: 18 | - "github.com/golang/protobuf" 19 | - "google.golang.org/protobuf" 20 | -------------------------------------------------------------------------------- /examples/elastalert/run_uptime.yaml: -------------------------------------------------------------------------------- 1 | # /opt/elastalert_rules/run_uptime.yaml 2 | name: go-audit run uptime 3 | index: streamstash-%Y.%m.%d 4 | use_strftime_index: true 5 | type: any 6 | filter: 7 | - query: 8 | query_string: 9 | query: go-audit.execve.command:uptime 10 | # write alerts to /tmp/alerts for debugging purposes 11 | alert: 12 | - command 13 | pipe_match_json: true 14 | command: ["/usr/bin/tee", "-a", "/tmp/alerts"] 15 | # Enable email alerts: 16 | #alert: 17 | #- email 18 | #email: YOUR-EMAIL@SOMEWHERE.COM 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | Describe the goal of this PR. Mention any related Issue numbers. 4 | 5 | ### Requirements (place an `x` in each `[ ]`) 6 | 7 | * [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackhq/go-audit/blob/master/.github/CONTRIBUTING.md) and have done my best effort to follow them. 8 | * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). 9 | * [ ] I've written tests to cover the new code and functionality included in this PR. 10 | -------------------------------------------------------------------------------- /contrib/upstart.go-audit.conf: -------------------------------------------------------------------------------- 1 | description "go-audit server" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [!2345] 5 | 6 | respawn 7 | respawn limit 10 5 8 | 9 | script 10 | # Catch any output from stdout/stderr and forward to syslog 11 | rm -f "/tmp/go-audit.log" 12 | mkfifo "/tmp/go-audit.log" 13 | (setsid logger -t"go-audit" <"/tmp/go-audit.log" &) 14 | exec >"/tmp/go-audit.log" 2>"/tmp/go-audit.log" 15 | rm "/tmp/go-audit.log" 16 | 17 | # There can be only one auditd 18 | /etc/init.d/auditd stop 19 | 20 | exec /usr/local/bin/go-audit 21 | end script 22 | -------------------------------------------------------------------------------- /examples/elastalert/upstart.conf: -------------------------------------------------------------------------------- 1 | # /etc/init/elastalert.conf 2 | description "elastalert" 3 | 4 | start on runlevel [2345] 5 | stop on runlevel [!2345] 6 | 7 | respawn 8 | respawn limit 10 5 9 | 10 | setuid nobody 11 | setgid nogroup 12 | 13 | chdir /opt/elastalert 14 | 15 | script 16 | set -e 17 | rm -f "/tmp/elastalert.log" 18 | mkfifo "/tmp/elastalert.log" 19 | (setsid logger -t"elastalert" <"/tmp/elastalert.log" &) 20 | exec >"/tmp/elastalert.log" 2>"/tmp/elastalert.log" 21 | rm "/tmp/elastalert.log" 22 | 23 | exec /usr/local/bin/elastalert --config /etc/elastalert.yaml 24 | end script 25 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## examples ## 2 | 3 | The following folders contain configs for each program, together they should give you a strong 4 | starting point for running all of this in production. 5 | 6 | These configs are targeted for everything running on a single Ubuntu 14.04 or 16.04 host. They _should_ 7 | work for other distributions but may require some modification. 8 | 9 | Set everything up in the following order: 10 | 11 | 1. [`elasticsearch`](./elasticsearch) 12 | 1. [`streamstash`](./streamstash) 13 | 1. [`rsyslog`](./rsyslog) 14 | 1. [`go-audit`](./go-audit) 15 | 1. [`kibana`](./kibana) 16 | 1. [`elastalert`](./elastalert) 17 | -------------------------------------------------------------------------------- /examples/streamstash/upstart.conf: -------------------------------------------------------------------------------- 1 | # /etc/init/streamstash.conf 2 | description "streamstash" 3 | 4 | start on runlevel [2345] 5 | stop on runlevel [!2345] 6 | 7 | respawn 8 | respawn limit 10 5 9 | 10 | kill timeout 32 11 | 12 | setuid nobody 13 | setgid nogroup 14 | 15 | script 16 | set -e 17 | rm -f "/tmp/streamstash.log" 18 | mkfifo "/tmp/streamstash.log" 19 | (setsid logger -t"streamstash" <"/tmp/streamstash.log" &) 20 | exec >"/tmp/streamstash.log" 2>"/tmp/streamstash.log" 21 | rm "/tmp/streamstash.log" 22 | 23 | exec /usr/lib/node_modules/streamstash/bin/streamstash /etc/streamstash.js 24 | end script 25 | -------------------------------------------------------------------------------- /examples/go-audit/upstart.conf: -------------------------------------------------------------------------------- 1 | # /etc/init/go-audit.conf 2 | 3 | description "go-audit" 4 | 5 | start on runlevel [2345] 6 | stop on runlevel [!2345] 7 | 8 | respawn 9 | respawn limit 10 5 10 | 11 | script 12 | # redirect stdout and stderr to syslog 13 | set -e 14 | rm -f "/tmp/go-audit.log" 15 | mkfifo "/tmp/go-audit.log" 16 | (setsid logger -t"go-audit" <"/tmp/go-audit.log" &) 17 | exec >"/tmp/go-audit.log" 2>"/tmp/go-audit.log" 18 | rm "/tmp/go-audit.log" 19 | 20 | # There can be only one auditd 21 | /etc/init.d/auditd stop || true 22 | 23 | exec /usr/local/bin/go-audit -config=/etc/go-audit.yaml 24 | end script 25 | -------------------------------------------------------------------------------- /examples/rsyslog/50-default.conf: -------------------------------------------------------------------------------- 1 | # /etc/rsyslog.d/50-default.conf 2 | 3 | # These rules output to 4 | # /var/log/syslog 5 | # /var/log/kern.log 6 | # /var/log/auth.log 7 | 8 | if prifilt("auth,authpriv.*") then { 9 | # Log all auth events to auth.log, fsync after each event 10 | action(name="auth-log" type="omfile" file="/var/log/auth.log" sync="on") 11 | 12 | } else { 13 | # Log everything but auth events to syslog, don't fsync 14 | action(name="syslog" type="omfile" file="/var/log/syslog" sync="off") 15 | 16 | if prifilt("kern.*") then { 17 | # Log all kernel events to kern.log, don't fsync 18 | action(name="kern_log" type="omfile" file="/var/log/kern.log" sync="off") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contrib/line-parser/README.md: -------------------------------------------------------------------------------- 1 | ## `line-parser` 2 | 3 | This program uses [`streamstash`](https://github.com/nbrownus/streamstash) to decode `go-audit` output 4 | 5 | It takes log lines from stdin and outputs the decoded json on stdout 6 | 7 | ### Install 8 | 9 | Make sure you have [nodejs](https://nodejs.org/en/download/) installed, the latest LTS version is advised. 10 | 11 | Then either run `npm install` within this directory or `npm install -g https://github.com/nbrownus/streamstash#2.0` 12 | to install `streamstash` globally 13 | 14 | ### Usage 15 | 16 | If you already have `go-audit` logging to a local file then your best bet is to run the following command 17 | 18 | ``` 19 | tail -f /path/to/file.log | ./line-parser 20 | ``` 21 | -------------------------------------------------------------------------------- /.github/workflows/gofmt.yml: -------------------------------------------------------------------------------- 1 | name: gofmt 2 | on: 3 | push: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/gofmt.yml' 7 | - '**.go' 8 | jobs: 9 | 10 | gofmt: 11 | name: Run gofmt 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set up Go Stable 16 | uses: actions/setup-go@v6 17 | with: 18 | go-version: stable 19 | check-latest: true 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v5 24 | 25 | - name: gofmt 26 | run: | 27 | if [ "$(find . -iname '*.go' | xargs gofmt -l)" ] 28 | then 29 | find . -iname '*.go' | xargs gofmt -d 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: 3 | push: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/test.yml' 7 | - '**Makefile' 8 | - '**.go' 9 | - 'go.mod' 10 | - 'go.sum' 11 | jobs: 12 | 13 | test-linux: 14 | name: Build all and test 15 | runs-on: ubuntu-latest 16 | steps: 17 | 18 | - name: Set up Go Stable 19 | uses: actions/setup-go@v6 20 | with: 21 | go-version: stable 22 | check-latest: true 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v5 27 | 28 | - name: Build 29 | run: make 30 | 31 | - name: Test 32 | run: make test 33 | -------------------------------------------------------------------------------- /make_deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # depends on `fpm`, install via `gem` 4 | 5 | VERSION="0.16.0" 6 | BUILD="slack1" 7 | CONTACT="Jane Doe " 8 | PACKAGE_NAME="go-audit" 9 | 10 | DIRNAME="$(cd "$(dirname "$0")" && pwd)" 11 | OLDESTPWD="$PWD" 12 | 13 | go build 14 | rm -f "$PWD/rootfs" 15 | mkdir -p "$PWD/rootfs/usr/local/bin" 16 | mv "$PWD/go-audit" "$PWD/rootfs/usr/local/bin/" 17 | 18 | fakeroot fpm -C "$PWD/rootfs" \ 19 | --license "MIT" \ 20 | --url "https://github.com/slackhq/go-audit" \ 21 | --vendor "" \ 22 | --description "go-audit is an alternative to the auditd daemon that ships with many distros." \ 23 | -d "auditd" \ 24 | -m "${CONTACT}" \ 25 | -n "${PACKAGE_NAME}" -v "$VERSION-$BUILD" \ 26 | -p "$OLDESTPWD/${PACKAGE_NAME}_${VERSION}-${BUILD}_amd64.deb" \ 27 | -s "dir" -t "deb" \ 28 | "usr" 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * [ ] I've read and understood the [Contributing guidelines](https://github.com/slackhq/go-audit/blob/master/.github/CONTRIBUTING.md) and have done my best effort to follow them. 2 | * [ ] I've read and agree to the [Code of Conduct](https://github.com/slackhq/go-audit/blob/master/CODE_OF_CONDUCT.md). 3 | * [ ] I've searched for any related issues and avoided creating a duplicate issue. 4 | 5 | #### Description 6 | 7 | > e.g. Description of the bug or feature 8 | 9 | #### Reproducible in: 10 | 11 | `go-audit` version: 12 | OS version(s): 13 | 14 | #### Steps to reproduce: 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | ### Expected result: 21 | 22 | > e.g. What you expected to happen 23 | 24 | ### Actual result: 25 | 26 | > e.g. What actually happened 27 | 28 | ### Attachments: 29 | 30 | > e.g. Logs, screenshots, screencast, sample project, funny gif, etc. 31 | 32 | 33 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "time" 7 | ) 8 | 9 | type AuditWriter struct { 10 | e *json.Encoder 11 | w io.Writer 12 | attempts int 13 | } 14 | 15 | func NewAuditWriter(w io.Writer, attempts int) *AuditWriter { 16 | return &AuditWriter{ 17 | e: json.NewEncoder(w), 18 | w: w, 19 | attempts: attempts, 20 | } 21 | } 22 | 23 | func (a *AuditWriter) Write(msg *AuditMessageGroup) (err error) { 24 | for i := 0; i < a.attempts; i++ { 25 | err = a.e.Encode(msg) 26 | if err == nil { 27 | break 28 | } 29 | 30 | if i != a.attempts { 31 | // We have to reset the encoder because write errors are kept internally and can not be retried 32 | a.e = json.NewEncoder(a.w) 33 | el.Println("Failed to write message, retrying in 1 second. Error:", err) 34 | time.Sleep(time.Second * 1) 35 | } 36 | } 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /contrib/line-parser/line-parser: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var StreamStash = require('streamstash') 4 | 5 | var logger = new StreamStash.Logger({ level: 0 }), 6 | streamStash = new StreamStash({ logger: logger }) 7 | 8 | process.stdin.on('end', function () { 9 | process.exit(0) 10 | }) 11 | 12 | streamStash.addInputPlugin(new StreamStash.inputs.StdInInput( 13 | { 14 | streamStash: streamStash, 15 | EventContainer: StreamStash.EventContainer, 16 | logger: logger 17 | } 18 | )) 19 | 20 | streamStash.addOutputPlugin(new StreamStash.outputs.StdOutOutput( 21 | { 22 | streamStash: streamStash, 23 | logger: logger 24 | } 25 | )) 26 | 27 | streamStash.addFilter(function (event) { 28 | StreamStash.parsers.goAuditParser(event) 29 | 30 | delete event.data['event_source'] 31 | delete event.data['_type'] 32 | 33 | event.next() 34 | }) 35 | 36 | streamStash.start() 37 | -------------------------------------------------------------------------------- /extras.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/spf13/viper" 4 | 5 | var extraParserConstructors = []func(config *viper.Viper) (ExtraParser, error){} 6 | 7 | type ExtraParser interface { 8 | Parse(am *AuditMessage) 9 | } 10 | 11 | type ExtraParsers []ExtraParser 12 | 13 | func RegisterExtraParser(constructor func(config *viper.Viper) (ExtraParser, error)) { 14 | extraParserConstructors = append(extraParserConstructors, constructor) 15 | } 16 | 17 | func createExtraParsers(config *viper.Viper) ExtraParsers { 18 | var extraParsers ExtraParsers 19 | for _, constructor := range extraParserConstructors { 20 | cp, err := constructor(config) 21 | if err != nil { 22 | el.Fatalf("Failed to create ExtraParser: %v", err) 23 | } 24 | if cp != nil { 25 | extraParsers = append(extraParsers, cp) 26 | } 27 | } 28 | return extraParsers 29 | } 30 | 31 | func (ps ExtraParsers) Parse(am *AuditMessage) { 32 | for _, p := range ps { 33 | p.Parse(am) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/go-audit/README.md: -------------------------------------------------------------------------------- 1 | ## go-audit ## 2 | 3 | The files here will get `go-audit` logging to `rsyslog` and has a decent default ruleset. 4 | 5 | An upstart config and systemd unit are provided as well 6 | 7 | ### Things to install 8 | 9 | - `auditd` - the one that comes with your distro is fine, we just need `auditctl` for now 10 | - ie: `sudo apt install auditd` 11 | - [`golang`](https://golang.org/dl/) - so you can compile `go-audit` 12 | 13 | On Ubuntu: 14 | 15 | ``` 16 | sudo apt install auditd golang 17 | ``` 18 | 19 | To install `go-audit` 20 | 21 | ``` 22 | make 23 | sudo cp go-audit /usr/local/bin 24 | ``` 25 | 26 | Place the files: 27 | 28 | - [`go-audit.yaml`](./go-audit.yaml) 29 | - [`systemd.service`](./systemd.service) - if running `systemd` 30 | - [`upstart.conf`](./upstart.conf) - if running `upstart` 31 | 32 | Start or restart `go-audit` 33 | 34 | - 14.04 - `sudo start go-audit` 35 | - 16.04 - `sudo systemctl start go-audit.service` 36 | 37 | Logs will be in `elasticsearch` 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_NUMBER ?= dev+$(shell date -u '+%Y%m%d%H%M%S') 2 | GO111MODULE = on 3 | export GO111MODULE 4 | 5 | LDFLAGS = -X main.Build=$(BUILD_NUMBER) 6 | 7 | ALL = linux-amd64 linux-arm64 8 | 9 | bin: 10 | go build -ldflags "$(LDFLAGS)" 11 | 12 | test: 13 | go test -v 14 | 15 | test-cov-html: 16 | go test -coverprofile=coverage.out 17 | go tool cover -html=coverage.out 18 | 19 | bench: 20 | go test -bench=. 21 | 22 | bench-cpu: 23 | go test -bench=. -benchtime=5s -cpuprofile=cpu.pprof 24 | go tool pprof go-audit.test cpu.pprof 25 | 26 | bench-cpu-long: 27 | go test -bench=. -benchtime=60s -cpuprofile=cpu.pprof 28 | go tool pprof go-audit.test cpu.pprof 29 | 30 | release: $(ALL:%=build/go-audit-%.tar.gz) 31 | 32 | build/%/go-audit: .FORCE 33 | GOOS=$(firstword $(subst -, , $*)) \ 34 | GOARCH=$(word 2, $(subst -, ,$*)) \ 35 | go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 36 | 37 | build/go-audit-%.tar.gz: build/%/go-audit 38 | tar -zcv -C build/$* -f $@ go-audit 39 | 40 | .FORCE: 41 | .PHONY: test test-cov-html bench bench-cpu bench-cpu-long bin release 42 | .DEFAULT_GOAL := bin 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Slack Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /examples/kibana/README.md: -------------------------------------------------------------------------------- 1 | ## kibana ## 2 | 3 | There isn't really any file based configuration required to make `kibana` work. 4 | 5 | Download and install the version compatible with your elasticsearch version: 6 | - [4.x](https://www.elastic.co/downloads/past-releases/kibana-4-6-2) (if running elasticsearch 2.x) 7 | 8 | On Ubuntu: 9 | 10 | ``` 11 | wget https://download.elastic.co/kibana/kibana/kibana-4.6.2-amd64.deb 12 | sudo dpkg -i kibana-4.6.2-amd64.deb 13 | ``` 14 | 15 | Start or restart `kibana` 16 | 17 | - 14.04 - `sudo /etc/init.d/kibana start` 18 | - 16.04 - `sudo systemctl start kibana.service` 19 | 20 | You will need to have installed and setup `rsyslog`, `go-audit`, and `streamstash` before you can complete the 21 | install 22 | 23 | When you visit `kibana` for the first time in a web browser, usually via `http://someip:5601`, it will have you 24 | do a one time setup. 25 | 26 | You will want to set: 27 | 28 | - `Index name or pattern` = `streamstash-*` 29 | - `Time-field name` = `@timestamp` 30 | 31 | You can now hit `create` and then `Discover`, you should start to see data! 32 | 33 | Logs will be sent to syslog, usually end up at `/var/log/syslog` 34 | -------------------------------------------------------------------------------- /examples/streamstash/README.md: -------------------------------------------------------------------------------- 1 | ## streamstash ## 2 | 3 | The following config will get `streamstash` handling events for the local machine. `go-audit`, `sshd`, and `sudo` logs 4 | will be parsed. 5 | 6 | An upstart config and systemd unit are provided as well 7 | 8 | 9 | ### Things to install 10 | 11 | - [`nodejs`](https://nodejs.org/en/download/) - latest v4.x LTS is advised, should work on v6.x LTS 12 | 13 | On Ubuntu: 14 | 15 | ``` 16 | sudo apt install nodejs-legacy npm git 17 | ``` 18 | 19 | On Ubuntu 14.04: 20 | 21 | ``` 22 | # 14.04 ships with a very old version of node and npm so you'll need to update npm 23 | sudo npm install -g npm 24 | ``` 25 | 26 | To install `streamstash` 27 | 28 | ``` 29 | sudo npm install -g https://github.com/nbrownus/streamstash#2.0 30 | ``` 31 | 32 | Place the files: 33 | 34 | - [`streamstash.js`](./streamstash.js) 35 | - [`systemd.service`](./systemd.service) - if running `systemd` 36 | - [`upstart.conf`](./upstart.conf) - if running `upstart` 37 | 38 | Start or restart `streamstash` 39 | 40 | - 14.04 - `sudo start streamstash` 41 | - 16.04 - `sudo systemctl start streamstash.service` 42 | 43 | Logs will be sent to syslog, usually end up at `/var/log/syslog` 44 | -------------------------------------------------------------------------------- /extras_helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func getPid(data string) (pid, ppid int) { 9 | start := 0 10 | end := 0 11 | var err error 12 | 13 | for { 14 | if start = strings.Index(data, "pid="); start < 0 { 15 | return 16 | } 17 | 18 | // Progress the start point beyon the = sign 19 | start += 4 20 | if end = strings.IndexByte(data[start:], spaceChar); end < 0 { 21 | // There was no ending space, maybe the pid is at the end of the line 22 | end = len(data) - start 23 | 24 | // If the end of the line is greater than 7 characters away (overflows 22 bit uint) then it can't be a pid 25 | // > On 64-bit systems, pid_max can be set to any value up to 2^22 (PID_MAX_LIMIT, approximately 4 million). 26 | if end > 7 { 27 | return 28 | } 29 | } 30 | 31 | id := data[start : start+end] 32 | if start > 4 && data[start-5] == 'p' { 33 | ppid, err = strconv.Atoi(id) 34 | } else { 35 | pid, err = strconv.Atoi(id) 36 | } 37 | if err != nil { 38 | el.Printf("Failed to parse pid: %s: %v\n", id, err) 39 | } 40 | if pid != 0 && ppid != 0 { 41 | return 42 | } 43 | 44 | data = data[start+end:] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /extras_cgroups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | func init() { 8 | RegisterExtraParser(func(config *viper.Viper) (ExtraParser, error) { 9 | if config.GetBool("extras.cgroups.enabled") { 10 | l.Printf("cgroup parser enabled") 11 | return &CgroupParser{}, nil 12 | } 13 | return nil, nil 14 | }) 15 | } 16 | 17 | type CgroupParser struct { 18 | } 19 | 20 | func (p *CgroupParser) Parse(am *AuditMessage) { 21 | switch am.Type { 22 | case 1300, 1302, 1309, 1326: // AUDIT_SYSCALL, AUDIT_PATH, AUDIT_EXECVE, AUDIT_SECCOMP 23 | pid, _ := getPid(am.Data) 24 | cgroup := p.getCgroupRootForPid(pid) 25 | if cgroup != "" { 26 | am.Extras = &AuditExtras{CgroupRoot: cgroup} 27 | } 28 | } 29 | } 30 | 31 | func (p *CgroupParser) getCgroupRootForPid(pid int) string { 32 | if pid == 0 { 33 | return "" 34 | } 35 | 36 | var v1PidPath string 37 | cgroups, err := taskControlGroups(pid, pid) 38 | if err != nil { 39 | return "" 40 | } 41 | 42 | for _, cgroup := range cgroups { 43 | if cgroup.ID == 0 { 44 | // v2 path 45 | return cgroup.Path 46 | } else if len(cgroup.Controllers) > 0 && cgroup.Controllers[0] == "pids" { 47 | // fall back to cgroup v1 pid path if we don't have cgroups v2 48 | v1PidPath = cgroup.Path 49 | } 50 | } 51 | 52 | return v1PidPath 53 | } 54 | -------------------------------------------------------------------------------- /examples/elasticsearch/README.md: -------------------------------------------------------------------------------- 1 | ## elasticsearch ## 2 | 3 | Very bare bones approach to getting elasticsearch running 4 | 5 | ## Things to install ## 6 | 7 | - `java` 8 | - [`elasticsearch`](https://www.elastic.co/downloads/past-releases/elasticsearch-2-4-1) - Avoid using 5.x until [elastalert supports it](https://github.com/Yelp/elastalert/issues/510) 9 | - [`kopf`](https://github.com/lmenezes/elasticsearch-kopf) - makes ops a lot easier 10 | 11 | On Ubuntu 16.04: 12 | 13 | ``` 14 | sudo apt install openjdk-8-jre-headless 15 | ``` 16 | 17 | On Ubuntu 14.04: 18 | 19 | ``` 20 | sudo apt install openjdk-7-jre-headless 21 | ``` 22 | 23 | On Ubuntu: 24 | 25 | ``` 26 | wget https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.4.1/elasticsearch-2.4.1.deb 27 | sudo dpkg -i elasticsearch-2.4.1.deb 28 | ``` 29 | 30 | Place the files 31 | 32 | - [`elasticsearch.yml`](./elasticsearch.yml) 33 | 34 | Start or restart `elasticsearch` 35 | 36 | - 14.04 - `sudo /etc/init.d/elasticsearch start` 37 | - 16.04 - `sudo systemctl start elasticsearch.service` 38 | 39 | Once the service is running apply the [`mapping.json`](./mapping.json) template to prepare for `streamstash` logs 40 | 41 | ``` 42 | curl -d @mapping.json http://localhost:9200/_template/streamstash 43 | ``` 44 | 45 | Logs are usually at `/var/log/elasticsearch/elasticsearch.log` 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.2.0] - 2023-04-07 11 | 12 | ### Added 13 | 14 | - Add `containerd` support in the same way we support grabbing container 15 | metadata from docker. (#106) 16 | 17 | - Build releases for arm64. (#107) 18 | 19 | ## [1.1.1] - 2022-01-24 20 | 21 | ### Fixed 22 | 23 | - Fix `cgroup` support on non-amd64 architectures. (#96) 24 | 25 | ## [1.1.0] - 2022-01-24 26 | 27 | ### Added 28 | 29 | - Added `cgroup` optional parser. When enabled, this will annotate audit 30 | events with the cgroup v2 root path. If running on an older system with only 31 | cgroup v1, this falls back to the pid cgroup path. (#95) 32 | 33 | ## [1.0.0] - 2020-06-18 34 | 35 | ### Added 36 | 37 | - You can now run `go-audit -version` to check the build version. 38 | 39 | [Unreleased]: https://github.com/slackhq/go-audit/compare/v1.2.0...HEAD 40 | [1.2.0]: https://github.com/slackhq/go-audit/releases/tag/v1.2.0 41 | [1.1.1]: https://github.com/slackhq/go-audit/releases/tag/v1.1.1 42 | [1.1.0]: https://github.com/slackhq/go-audit/releases/tag/v1.1.0 43 | [1.0.0]: https://github.com/slackhq/go-audit/releases/tag/v1.0.0 44 | -------------------------------------------------------------------------------- /examples/elasticsearch/mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "streamstash-*", 3 | "mappings": { 4 | "_default_": { 5 | "dynamic_templates": [ 6 | { 7 | "message_field": { 8 | "mapping": { 9 | "index": "analyzed", 10 | "omit_norms": true, 11 | "type": "string" 12 | }, 13 | "match_mapping_type": "string", 14 | "match": "message" 15 | } 16 | }, 17 | { 18 | "string_fields": { 19 | "mapping": { 20 | "index": "analyzed", 21 | "omit_norms": true, 22 | "type": "string", 23 | "fields": { 24 | "raw": { 25 | "index": "not_analyzed", 26 | "type": "string" 27 | } 28 | } 29 | }, 30 | "match_mapping_type": "string", 31 | "match": "*" 32 | } 33 | } 34 | ], 35 | "_all": { 36 | "omit_norms": true, 37 | "enabled": true 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/rsyslog/rsyslog.conf: -------------------------------------------------------------------------------- 1 | # /etc/rsyslog.conf 2 | 3 | # modules 4 | 5 | module(load="imuxsock" sysSock.rateLimit.Interval="0") 6 | module(load="imklog" permitNonKernelFacility="on") 7 | module(load="imptcp") 8 | 9 | module(load="omprog") 10 | module(load="omrelp") 11 | 12 | module( 13 | load="builtin:omfile" 14 | template="RSYSLOG_TraditionalFileFormat" 15 | fileOwner="syslog" 16 | fileGroup="adm" 17 | fileCreateMode="0644" 18 | dirCreateMode="0755" 19 | ) 20 | 21 | # global directives 22 | 23 | global ( 24 | parser.escapeControlCharactersCStyle="on" 25 | parser.escapeControlCharactersOnReceive="on" 26 | action.reportSuspensionContinuation="on" 27 | maxMessageSize="1024k" 28 | WorkDirectory="/var/spool/rsyslog" 29 | ) 30 | 31 | main_queue( 32 | queue.dequeueBatchSize="100000" 33 | queue.fileName="main" 34 | queue.size="100000" 35 | queue.type="LinkedList" 36 | queue.saveOnShutdown="on" 37 | queue.maxFileSize="1g" 38 | ) 39 | 40 | $RepeatedMsgReduction off 41 | $Umask 0022 42 | $PrivDropToUser syslog 43 | $PrivDropToGroup syslog 44 | 45 | # record process stats using 46 | # https://github.com/slackhq/go-rsyslog-pstats for more info 47 | #ruleset(name="pstats"){ 48 | # action(type="omprog" name="pstats" binary="/usr/local/bin/go-rsyslog-pstats --port 8125") 49 | #} 50 | #module(load="impstats" interval="10" severity="7" format="json" ruleset="pstats") 51 | 52 | # Include all config files in /etc/rsyslog.d/ 53 | $IncludeConfig /etc/rsyslog.d/*.conf 54 | -------------------------------------------------------------------------------- /examples/go-audit/go-audit.yaml: -------------------------------------------------------------------------------- 1 | # /etc/go-audit.yaml 2 | 3 | canary: true 4 | 5 | # use /var/run/go-audit.sock to write events 6 | output: 7 | syslog: 8 | attempts: 15 9 | enabled: true 10 | network: unix 11 | address: /var/run/go-audit.sock 12 | priority: 132 13 | tag: go-audit 14 | 15 | # log an event when we believe a message has been lost 16 | message_tracking: 17 | enabled: true 18 | log_out_of_order: false 19 | max_out_of_order: 500 20 | 21 | rules: 22 | - -b 1024 23 | # required if you set canary: true 24 | - -w /proc/net/netlink -p war -k netlink-file 25 | # watch interesting network events 26 | - -a exit,always -S connect 27 | - -a exit,always -S listen 28 | # watch execve for everything that has an auid set (ignores things like cron) 29 | - -a exit,always -F arch=b64 -F auid!=-1 -S execve -k user_commands 30 | - -a exit,always -F arch=b32 -F auid!=-1 -S execve -k user_commands 31 | # failure to access file because of perms 32 | - -a always,exit -F arch=b32 -S open -S openat -F exit=-EACCES -k access 33 | - -a always,exit -F arch=b64 -S open -S openat -F exit=-EACCES -k access 34 | - -a always,exit -F arch=b32 -S open -S openat -F exit=-EPERM -k access 35 | - -a always,exit -F arch=b64 -S open -S openat -F exit=-EPERM -k access 36 | 37 | filters: 38 | # reduce the number of connect syscall events being logged 39 | - syscall: 42 40 | message_type: 1306 41 | # 0200....7F - ipv4 on any port to 127.x.x.x 42 | # 01 - local/unix domain sockets 43 | regex: saddr=(0200....7F|01) 44 | -------------------------------------------------------------------------------- /examples/rsyslog/01-go-audit.conf: -------------------------------------------------------------------------------- 1 | # /etc/rsyslog.d/01-go-audit.conf 2 | 3 | # Give us higher resolution timestamps 4 | template( 5 | name="LongTagForwardFormat" 6 | type="string" 7 | string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg%" 8 | ) 9 | 10 | # rulesets make it easier to target this output 11 | ruleset( 12 | name="go-audit-output" 13 | queue.discardmark="1000" 14 | queue.discardseverity="0" 15 | queue.size="1000" 16 | queue.type="LinkedList" 17 | ){ 18 | 19 | # send everything to streamstash via relp 20 | action( 21 | type="omrelp" 22 | name="streamstash-relp" 23 | target="127.0.0.1" 24 | port="5514" 25 | template="LongTagForwardFormat" 26 | action.resumeRetryCount="-1" 27 | windowSize="1000" 28 | queue.discardmark="50000" 29 | queue.discardseverity="0" 30 | queue.size="50000" 31 | queue.type="LinkedList" 32 | ) 33 | } 34 | 35 | # Expose a sock stream socket for log lines > 128kb 36 | input(type="imptcp" path="/var/run/go-audit.sock" unlink="on" name="go-audit-input" ruleset="go-audit-output") 37 | 38 | # Capture audit system log lines and stop them from getting to disk, no further processing will happen to these events 39 | if $programname == "go-audit" then { 40 | call go-audit-output 41 | stop 42 | } 43 | 44 | # Tee off interesting auth facility log lines, matching lines will continue on to other outputs 45 | if $programname == "sshd" or $programname == "sudo" then { 46 | call go-audit-output 47 | } 48 | -------------------------------------------------------------------------------- /examples/rsyslog/README.md: -------------------------------------------------------------------------------- 1 | ## rsyslog ## 2 | 3 | The files here will configure `rsyslog` to do the normal system logging that you are probably used to 4 | as well as prepare for ingesting `go-audit` events and outputting them to `streamstash` 5 | 6 | ### Things to install 7 | 8 | The following packages (and their dependencies) are required for the config to work properly. You can find the 9 | latest versions [here](http://www.rsyslog.com/downloads/download-v8-stable/) 10 | 11 | Version 8.20 is the minimum version for all this to work properly 12 | 13 | - `rsyslog` 14 | - `rsyslog-imptcp` 15 | - `rsyslog-relp` 16 | - [`go-rsyslog-pstats`](https://github.com/slackhq/go-rsyslog-pstats) - (optional) takes process stats from rsyslog and 17 | sends them to `statsite` or `statsd`, helpful for debugging issues 18 | 19 | On Ubuntu: 20 | 21 | ``` 22 | sudo add-apt-repository ppa:adiscon/v8-stable 23 | sudo apt update 24 | sudo apt install rsyslog rsyslog-imptcp rsyslog-relp 25 | ``` 26 | 27 | Place the files: 28 | 29 | - [`rsyslog.conf`](./rsyslog.conf) 30 | - [`01-go-audit.conf`](./01-go-audit.conf) 31 | - [`50-default.conf`](./50-default.conf) 32 | 33 | Start or restart `rsyslog` 34 | 35 | - 14.04 - `sudo restart rsyslog` 36 | - 16.04 - `sudo systemctl start rsyslog.service` 37 | 38 | ### Debugging ### 39 | 40 | If you are having issues with your config you can get more information by running `rsyslog` directly 41 | 42 | ``` 43 | sudo rsyslogd -n 44 | ``` 45 | 46 | or with lots of debug info 47 | 48 | ``` 49 | sudo rsyslogd -nd 50 | ``` 51 | 52 | You may have to background the process to quit 53 | 54 | -------------------------------------------------------------------------------- /examples/elastalert/elastalert.yaml: -------------------------------------------------------------------------------- 1 | # /etc/elastalert.yaml 2 | rules_folder: /opt/elastalert_rules 3 | 4 | # How often ElastAlert will query Elasticsearch 5 | # The unit can be anything from weeks to seconds 6 | run_every: 7 | seconds: 5 8 | 9 | # ElastAlert will buffer results from the most recent 10 | # period of time, in case some log sources are not in real time 11 | buffer_time: 12 | minutes: 45 13 | 14 | # The Elasticsearch hostname for metadata writeback 15 | # Note that every rule can have its own Elasticsearch host 16 | es_host: 127.0.0.1 17 | 18 | # The Elasticsearch port 19 | es_port: 9200 20 | 21 | # Optional URL prefix for Elasticsearch 22 | #es_url_prefix: elasticsearch 23 | 24 | # Connect with TLS to Elasticsearch 25 | #use_ssl: True 26 | 27 | # Verify TLS certificates 28 | #verify_certs: True 29 | 30 | # GET request with body is the default option for Elasticsearch. 31 | # If it fails for some reason, you can pass 'GET', 'POST' or 'source'. 32 | # See http://elasticsearch-py.readthedocs.io/en/master/connection.html?highlight=send_get_body_as#transport 33 | # for details 34 | #es_send_get_body_as: GET 35 | 36 | # Option basic-auth username and password for Elasticsearch 37 | #es_username: someusername 38 | #es_password: somepassword 39 | 40 | # The index on es_host which is used for metadata storage 41 | # This can be a unmapped index, but it is recommended that you run 42 | # elastalert-create-index to set a mapping 43 | writeback_index: elastalert_status 44 | 45 | # If an alert fails for some reason, ElastAlert will retry 46 | # sending the alert until this time period has elapsed 47 | alert_time_limit: 48 | days: 2 49 | -------------------------------------------------------------------------------- /examples/elastalert/README.md: -------------------------------------------------------------------------------- 1 | ## elastalert ## 2 | 3 | ### Things to install 4 | 5 | - `python` 6 | - `python-dev` 7 | - `pip` 8 | - `gcc` 9 | 10 | On Ubuntu: 11 | 12 | ``` 13 | sudo apt install python python-dev python-pip gcc 14 | ``` 15 | 16 | [elastalert docs](http://elastalert.readthedocs.io/en/latest/running_elastalert.html#downloading-and-configuring) has a 17 | good guide to getting setup. 18 | 19 | A TLDR version: 20 | 21 | ``` 22 | sudo pip install --upgrade setuptools pip 23 | cd /opt 24 | sudo git clone https://github.com/Yelp/elastalert.git 25 | cd elastalert 26 | sudo python setup.py install 27 | sudo pip install -r requirements.txt 28 | # just answer the defaults for this one 29 | elastalert-create-index --host localhost --port 9200 --no-ssl --no-auth 30 | ``` 31 | 32 | Place the files: 33 | 34 | - [`elastalert.yaml`](./elastalert.yaml) 35 | - [`run_uptime.yaml`](./run_uptime.yaml) 36 | - [`systemd.service`](./systemd.service) - if running `systemd` 37 | - [`upstart.conf`](./upstart.conf) - if running `upstart` 38 | 39 | Logs will be sent to syslog, usually end up at `/var/log/syslog` 40 | 41 | Once all that is done you can test the `run_uptime.yaml` rule with (you may want to run `uptime` first) 42 | 43 | ``` 44 | uptime 45 | elastalert-test-rule --config /etc/elastalert.yaml /opt/elastalert_rules/run_uptime.yaml 46 | ``` 47 | 48 | You should see a big json blob of you running `uptime`! 49 | 50 | Start or restart `elastalert` 51 | 52 | - 14.04 - `sudo start elastalert` 53 | - 16.04 - `sudo systemctl start elastalert.service` 54 | 55 | Logs will be sent to syslog, usually end up at `/var/log/syslog` 56 | 57 | Alerts will be sent to `/tmp/alerts` 58 | -------------------------------------------------------------------------------- /extras_containers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetPid(t *testing.T) { 10 | data := "arch=c0000000 syscall=59 success=yes exit=0 a0=1600000 a1=1600000 a2=1600000 a3=500 items=2 ppid=30296 pid=31475 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=4294967295 comm=\"date\" exe=\"/bin/date\" key=(null)" 11 | 12 | pid, ppid := getPid(data) 13 | assert.Equal(t, pid, 31475) 14 | assert.Equal(t, ppid, 30296) 15 | 16 | data = "pid=31475 foo=bar ppid=30296" 17 | 18 | pid, ppid = getPid(data) 19 | assert.Equal(t, pid, 31475) 20 | assert.Equal(t, ppid, 30296) 21 | 22 | data = "pid=31475 foo=bar" 23 | 24 | pid, ppid = getPid(data) 25 | assert.Equal(t, pid, 31475) 26 | assert.Equal(t, ppid, 0) 27 | } 28 | 29 | func TestContainerID(t *testing.T) { 30 | assert.Equal(t, 31 | "5c69ff1a4edf85228df5153f36cacbdee440ad6fd585704e77f50f54d3e58249", 32 | containerID("/docker/5c69ff1a4edf85228df5153f36cacbdee440ad6fd585704e77f50f54d3e58249"), 33 | ) 34 | assert.Equal(t, 35 | "2ce19d7466dbb3eb7b7493be01b6d3353c990e6722258e94fac8016baeefd6c8", 36 | containerID("/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podd12649dc_490b_4e0c_b6fe_9e2fbf4a058c.slice/cri-containerd-2ce19d7466dbb3eb7b7493be01b6d3353c990e6722258e94fac8016baeefd6c8.scope"), 37 | ) 38 | assert.Equal(t, 39 | "d84f26ddf627f6fde170d83478b4ae9d5baaaa645e484b755a46844f3da785c6", 40 | containerID("/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podec04832a_309d_4044_9f92_7fac604a2384.slice/docker-d84f26ddf627f6fde170d83478b4ae9d5baaaa645e484b755a46844f3da785c6.scope"), 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /contrib/rh-sysv.go-audit.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # go-audit 4 | # 5 | # chkconfig: - 15 85 6 | # description: go-audit is an alternative to the auditd daemon that \ 7 | # ships with many distros. 8 | # processname: go-audit 9 | # config: /etc/go-audit.yaml 10 | # pidfile: /var/run/go-audit.pid 11 | 12 | # Source function library. 13 | . /etc/rc.d/init.d/functions 14 | 15 | exec="/usr/local/bin/go-audit" 16 | prog=$(basename $exec) 17 | pidfile=/var/run/${prog}.pid 18 | 19 | CONFIG_FILE="/etc/go-audit.yaml" 20 | LOG_DIRECTORY="/var/log/go-audit" 21 | 22 | [ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog 23 | 24 | start() { 25 | [ -f $pidfile ] && checkpid $(cat $pidfile) && echo "$prog already running" && return 26 | 27 | echo -n $"Starting $prog: " 28 | nohup $exec --config $CONFIG_FILE >>${LOG_DIRECTORY}/go-audit.out 2>>${LOG_DIRECTORY}/go-audit.err $pidfile 31 | 32 | sleep 0.1 # Sleep briefly to see if the process ends immediately 33 | checkpid $(cat $pidfile) 34 | status=$? 35 | if [ $status -eq 0 ]; then success; else failure; fi 36 | echo 37 | return $status 38 | } 39 | 40 | stop() { 41 | echo -n $"Stopping $prog: " 42 | killproc -p $pidfile $prog 43 | retval=$? 44 | echo 45 | [ $retval -eq 0 ] && rm -f $pidfile 46 | return $retval 47 | } 48 | 49 | restart() { 50 | stop 51 | start 52 | } 53 | 54 | rh_status() { 55 | status -p $pidfile $prog 56 | } 57 | 58 | case "$1" in 59 | start|stop|restart) 60 | $1 61 | ;; 62 | status) 63 | rh_status 64 | ;; 65 | *) 66 | echo $"Usage: $0 {start|stop|status|restart}" 67 | exit 2 68 | esac 69 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing at Slack 2 | 3 | ![Header Image](contributing_header_slack.png) 4 | 5 | ## Before Contributing 6 | 7 | Before contributing, please read our [Code of Conduct](../CODE_OF_CONDUCT.md). We take it very seriously, and expect that you will as well. 8 | 9 | ## New Issues 10 | 11 | Before opening a new issue, please consider: 12 | 13 | - Reading [the documentation](./README.md) and [the changelog](./CHANGELOG.md) first. 14 | - Searching for any related issues and avoid creating duplicated issues. 15 | - Adding details, diagnoses, screenshots or any type of useful information in existing issues, even if they are marked as closed. The team will still review it. 16 | - Trying out the examples [provided in this repository](https://github.com/slackhq/go-audit#getting-set-up). 17 | - Taking the time to think of a solution and [**open a pull request**](#new-pull-requests) for either improving the documentation, fixing a bug or suggesting a feature. 18 | - Finally, [**open an issue**](https://github.com/slackhq/go-audit/issues/new) to report a bug, ask for help or suggest a feature. The more information you give, the better people can help you. 19 | 20 | 21 | ## New Pull Requests 22 | 23 | We love pull requests and we are generally very receptive to contributions. Things to keep in mind: 24 | 25 | - [Fork the repository](https://github.com/slackhq/go-audit) and make sure to work on a branch up to date with origin master. 26 | - Do your thing! 27 | - Be mindful about doing atomic commits, adding documentation to your changes, not refactoring too much. 28 | - Add tests covering the new code or functionality you are adding. 29 | - Add a descriptive title and add any useful information for the reviewer. If your contribution is a user facing thing, please attach a screenshot and/or screencast (gif preferrably). 30 | - Read and agree to our [Contributor License Agreement (CLA)](https://docs.google.com/a/slack-corp.com/forms/d/1q_w8rlJG_x_xJOoSUMNl7R35rkpA7N6pUkKhfHHMD9c/viewform). _We cannot accept your PR without your agreement to our CLA_. 31 | - Create your pull request (yay!). If it is in relation to an existing issue, please mention it on the title or description. 32 | 33 | [Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) 34 | -------------------------------------------------------------------------------- /BATTLE_TESTING.md: -------------------------------------------------------------------------------- 1 | # Battle Testing 2 | 3 | Here are some tests that can be run to see how things perform under weird or heavy conditions 4 | 5 | ### Receive buffer 6 | 7 | The idea of this test is to see where your system will begin having trouble receiving messages from `kauditd`. 8 | It may be possible to tweak the netlink socket receive buffer and avoid this problem. We have not seen any message 9 | loss as a result of this scenario to date. 10 | 11 | - Make a file that a user can't read 12 | 13 | `sudo touch /tmp/nope && sudo chmod 0600 /tmp/nope` 14 | 15 | - Set your `go-audit.yaml` to the following: 16 | 17 | ``` 18 | rules: 19 | - -a always,exit -F arch=b32 -S open -S openat -F exit=-EACCES -k access 20 | - -a always,exit -F arch=b64 -S open -S openat -F exit=-EACCES -k access 21 | ``` 22 | 23 | - Spawn a bunch of background processes that can't read the file (run the following line many times) 24 | 25 | `while [ true ]; do cat /tmp/nope > /dev/null 2>&1; done &` 26 | 27 | - Run go audit and observe, it may take a while but you should eventually see the following message 28 | 29 | ``` 30 | Error during message receive: no buffer space available 31 | ``` 32 | 33 | - Experiment with the `socket_buffer.receive` value in your `go-audit` config. 34 | 35 | ### Message loss 36 | 37 | This tests purpose is to make sure you are recording detected message loss. How quickly message loss is 38 | discovered is currently based on how many audit events occur every second and the value of 39 | `message_tracking.max_out_of_order` 40 | 41 | - Set your `go-audit.yaml` to the following: 42 | 43 | ``` 44 | message_tracking: 45 | enabled: true 46 | log_out_of_order: false 47 | max_out_of_order: 2 48 | rules: 49 | - -a exit,always -F arch=b64 -S execve 50 | - -a exit,always -F arch=b32 -S execve 51 | ``` 52 | 53 | - Run two instances of `go-audit`. Start one about 2 seconds after the other for best results. 54 | 55 | - Spawn a background process that executes a command in a subsecond interval. 56 | 57 | `while [ true ]; do uptime > /dev/null; sleep 0.5; done &` 58 | 59 | - Observe the output of each process, eventually you should see a log line similar to: 60 | 61 | ``` 62 | Likely missed sequence 100, current 102, worst message delay 0 63 | ``` 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Slack open source code of conduct 2 | 3 | 4 | ## Introduction 5 | Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand. 6 | 7 | Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic. 8 | 9 | This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members. 10 | 11 | ## Expected Behavior 12 | * Be welcoming. 13 | * Be kind. 14 | * Look out for each other. 15 | 16 | ## Unacceptable Behavior 17 | * Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature. 18 | * Unwelcome, suggestive, derogatory or inappropriate nicknames or terms. 19 | * Disrespect towards others. (Jokes, innuendo, dismissive attitudes.) 20 | * Intimidation or harassment (online or in-person). Please read the [Citizen Code of Conduct](http://citizencodeofconduct.org/) for how we interpret harassment. 21 | * Disrespect towards differences of opinion. 22 | * Inappropriate attention or contact. Be aware of how your actions affect others. If it makes someone uncomfortable, stop. 23 | * Not understanding the differences between constructive criticism and disparagement. 24 | * Sustained disruptions. 25 | * Violence, threats of violence or violent language. 26 | 27 | ## Enforcement 28 | Understand that speech and actions have consequences, and unacceptable behavior will not be tolerated. 29 | 30 | If you are the subject of, or witness to any violations of this Code of Conduct, please contact us by submitting a form [here](https://docs.google.com/a/slack-corp.com/forms/d/1NVqj2S2Q49XVIOT5N3L6Tx1oihvk9CpMa_UX8T_6ESo/viewform), or email conduct@slack.com. 31 | 32 | If violations occur, organizers will take any action they deem appropriate for the infraction, up to and including expulsion. 33 | 34 | _Thanks to the [Django Code of Conduct](https://www.djangoproject.com/conduct/), [The Citizen Code of Conduct](http://citizencodeofconduct.org/), [The Rust Code of Conduct](https://www.rust-lang.org/conduct.html) and [The Ada Initiative](http://adainitiative.org/2014/02/18/howto-design-a-code-of-conduct-for-your-community/)._ 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v[0-9]+.[0-9]+.[0-9]*' 5 | 6 | name: Create release and upload binaries 7 | 8 | jobs: 9 | build-linux: 10 | name: Create and Upload Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go Stable 14 | uses: actions/setup-go@v6 15 | with: 16 | go-version: stable 17 | check-latest: true 18 | id: go 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v5 22 | 23 | - name: Build 24 | run: | 25 | make BUILD_NUMBER="${GITHUB_REF#refs/tags/v}" release 26 | 27 | - name: Create sha256sum 28 | run: | 29 | for dir in build 30 | do 31 | ( 32 | cd $dir 33 | for v in *.tar.gz 34 | do 35 | sha256sum $v 36 | tar zxf $v --to-command='sh -c "sha256sum | sed s=-$='$v'/$TAR_FILENAME="' 37 | done 38 | ) 39 | done | sort -k 2 >SHASUM256.txt 40 | 41 | - name: Create Release 42 | id: create_release 43 | uses: actions/create-release@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | tag_name: ${{ github.ref }} 48 | release_name: Release ${{ github.ref }} 49 | draft: false 50 | prerelease: false 51 | 52 | ## 53 | ## Upload assets (I wish we could just upload the whole folder at once... 54 | ## 55 | 56 | - name: Upload SHASUM256.txt 57 | uses: actions/upload-release-asset@v1.0.2 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | with: 61 | upload_url: ${{ steps.create_release.outputs.upload_url }} 62 | asset_path: ./SHASUM256.txt 63 | asset_name: SHASUM256.txt 64 | asset_content_type: text/plain 65 | 66 | - name: Upload linux-amd64 67 | uses: actions/upload-release-asset@v1.0.2 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | upload_url: ${{ steps.create_release.outputs.upload_url }} 72 | asset_path: ./build/go-audit-linux-amd64.tar.gz 73 | asset_name: go-audit-linux-amd64.tar.gz 74 | asset_content_type: application/gzip 75 | 76 | - name: Upload linux-arm64 77 | uses: actions/upload-release-asset@v1.0.2 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | with: 81 | upload_url: ${{ steps.create_release.outputs.upload_url }} 82 | asset_path: ./build/go-audit-linux-arm64.tar.gz 83 | asset_name: go-audit-linux-arm64.tar.gz 84 | asset_content_type: application/gzip 85 | -------------------------------------------------------------------------------- /examples/streamstash/streamstash.js: -------------------------------------------------------------------------------- 1 | // /etc/streamstash.js 2 | 3 | // Pause inputs if we have this many items in memory 4 | streamStash.highWatermark = 30000 5 | 6 | // Emit telemtry to statsite or statsd 7 | telemetry('localhost', 8125) 8 | 9 | // Listen for relp connections on localhost:5514 10 | addInputPlugin('relp', { host: '127.0.0.1', port: 5514 }) 11 | 12 | // Send logs to elasticsearch on localhost:9200 13 | addOutputPlugin( 14 | 'elasticsearch', 15 | { 16 | typeField: '_type', 17 | timestampField: '@timestamp', 18 | hostname: '127.0.0.1', 19 | port: '9200', 20 | batchSize: 500, 21 | indexPrefix: 'streamstash' 22 | } 23 | ) 24 | 25 | // If you are having issues and want to make sure events are flowing you can uncomment the following line 26 | // and run streamstash in a terminal 27 | //addOutputPlugin('stdout') 28 | 29 | addFilter(function (event) { 30 | // Only work with events that has a syslog object, which is probably everything 31 | if (event.data.hasOwnProperty('syslog') === false) { 32 | return event.next() 33 | } 34 | 35 | // Strip the pid from the service 36 | if (matches = /(.*)\[([0-9]*)\]$/.exec(event.data.syslog.service)) { 37 | event.data.syslog.service = matches[1] 38 | event.data.syslog.service_pid = matches[2] 39 | } 40 | 41 | // Parse events from specific services 42 | switch (event.data.syslog.service) { 43 | case 'sshd': 44 | StreamStash.parsers.sshdParser(event) 45 | break 46 | 47 | case 'sudo': 48 | StreamStash.parsers.sudoParser(event) 49 | break 50 | 51 | case 'go-audit': 52 | // If you get sick of seeing the unparsed 1305 messages in kibana, uncomment this line 53 | // if (event.data.message.indexOf('"type":1305') >= 0) { 54 | // return event.cancel() 55 | // } 56 | 57 | StreamStash.parsers.goAuditParser(event) 58 | break 59 | 60 | default: 61 | // Puts the json document in a field named after the parsed syslog service 62 | // This is an attempt to eliminate mapping conflicts in elasticsearch 63 | StreamStash.parsers.jsonParser(event, '_type', false, event.data.syslog.service) 64 | } 65 | 66 | // Rename syslog specific things and drop useless to us fields 67 | if (event.data.syslog.hasOwnProperty('facilityName')) { 68 | event.data.syslog['facility'] = event.data.syslog.facilityName 69 | delete event.data.syslog['facilityName'] 70 | } 71 | 72 | if (event.data.syslog.hasOwnProperty('severityName')) { 73 | event.data.syslog['severity'] = event.data.syslog.severityName 74 | delete event.data.syslog['severityName'] 75 | } 76 | 77 | if (event.data.syslog.hasOwnProperty('service')) { 78 | event.data['_type'] = event.data.syslog.service 79 | } 80 | 81 | delete event.data.syslog['priority'] 82 | 83 | // Use the timestamp from the parsed data, if any 84 | if (event.data.syslog.hasOwnProperty('timestamp')) { 85 | event.data['@timestamp'] = event.data.syslog.timestamp 86 | } 87 | 88 | // Worst case use the timestamp from when the input received the event 89 | if (event.data.hasOwnProperty('@timestamp') === false && event.data.event_source.hasOwnProperty('timestamp')) { 90 | event.data['@timestamp'] = event.data.event_source.timestamp 91 | } 92 | 93 | event.next() 94 | }) 95 | -------------------------------------------------------------------------------- /contrib/go-audit.rpmbuild.spec: -------------------------------------------------------------------------------- 1 | # Update this commit hash to the desired release hash to package 2 | %global commit 56a1a65a67038849223779fa53611809a832ac0d 3 | %global shortcommit %(c=%{commit}; echo ${c:0:7}) 4 | %global debug_package %{nil} 5 | 6 | %if 0%{?fedora} >= 15 || 0%{?rhel} >= 7 7 | %define use_systemd 1 8 | %else 9 | %define use_systemd 0 10 | %endif 11 | 12 | Name: go-audit 13 | Version: 0 14 | Release: 1.git%{shortcommit}%{?dist} 15 | Summary: go-audit is an alternative to the auditd daemon that ships with many distros. 16 | 17 | License: MIT 18 | URL: https://github.com/slackhq/go-audit 19 | Source0: https://github.com/slackhq/go-audit/archive/%{commit}/go-audit-%{shortcommit}.tar.gz 20 | 21 | # Golang 1.7 or higher is currently a requirement to build. However, there is not currently a package for golang 1.7 on 22 | # CentOS 7. Instead, the official release can be installed manually, however please ensure that it is in your PATH. 23 | #BuildRequires: golang >= 1.7 24 | 25 | Requires: /sbin/auditctl 26 | 27 | %if %{use_systemd} 28 | BuildRequires: systemd 29 | Requires(post): systemd 30 | Requires(preun): systemd 31 | Requires(postun): systemd 32 | %else 33 | Requires(post): chkconfig 34 | Requires(preun): chkconfig, initscripts 35 | %endif 36 | 37 | %description 38 | go-audit is an alternative to the auditd daemon that ships with many distros. 39 | 40 | %prep 41 | %setup -q -n go-audit-%{commit} 42 | 43 | %build 44 | mkdir -p ./_build/src/github.com/slackhq 45 | ln -s $(pwd) ./_build/src/github.com/slackhq/go-audit 46 | export GOPATH=$(pwd)/_build 47 | export PATH=$PATH:$(pwd)/_build/bin 48 | 49 | go get -u github.com/kardianos/govendor 50 | pushd _build/src/github.com/slackhq/go-audit 51 | make 52 | popd 53 | 54 | %install 55 | install -d %{buildroot}/usr/local/bin 56 | install -d %{buildroot}%{_sysconfdir} 57 | install -d %{buildroot}%{_sysconfdir}/logrotate.d 58 | install -m 0750 -d %{buildroot}%{_localstatedir}/log/go-audit 59 | install -m 0755 ./go-audit %{buildroot}/usr/local/bin/go-audit 60 | install -m 0644 ./go-audit.yaml.example %{buildroot}%{_sysconfdir}/go-audit.yaml.example 61 | install -m 0644 ./contrib/logrotate.go-audit.conf %{buildroot}%{_sysconfdir}/logrotate.d/go-audit 62 | 63 | %if %{use_systemd} 64 | install -d %{buildroot}%{_unitdir} 65 | install -m 0644 ./contrib/systemd.go-audit.service %{buildroot}%{_unitdir}/go-audit.service 66 | %else 67 | install -d %{buildroot}%{_initddir} 68 | install -m 0755 ./contrib/rh-sysv.go-audit.init %{buildroot}%{_initddir}/go-audit 69 | %endif 70 | 71 | %post 72 | %if %{use_systemd} 73 | %systemd_post go-audit.service 74 | %else 75 | if [ $1 -eq 1 ] 76 | then 77 | /sbin/chkconfig --add go-audit 78 | fi 79 | %endif 80 | 81 | %preun 82 | %if %{use_systemd} 83 | %systemd_preun go-audit.service 84 | %else 85 | if [ $1 -eq 0 ] 86 | then 87 | /sbin/service go-audit stop >/dev/null 2>&1 88 | /sbin/chkconfig --del go-audit 89 | fi 90 | %endif 91 | 92 | %postun 93 | %if %{use_systemd} 94 | %systemd_postun go-audit.service 95 | %endif 96 | 97 | %files 98 | %defattr(-,root,root,-) 99 | %doc README.md 100 | %dir %{_localstatedir}/log/go-audit 101 | /usr/local/bin/go-audit 102 | %{_sysconfdir}/go-audit.yaml.example 103 | %{_sysconfdir}/logrotate.d/go-audit 104 | %if %{use_systemd} 105 | %{_unitdir}/go-audit.service 106 | %else 107 | %{_initddir}/go-audit 108 | %endif 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/slackhq/go-audit 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/containerd/containerd v1.7.29 9 | github.com/docker/docker v28.5.2+incompatible 10 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da 11 | github.com/spf13/viper v1.21.0 12 | github.com/stretchr/testify v1.11.1 13 | gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0 14 | ) 15 | 16 | require ( 17 | cyphar.com/go-pathrs v0.2.1 // indirect 18 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect 19 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/Microsoft/hcsshim v0.11.7 // indirect 22 | github.com/containerd/cgroups v1.1.0 // indirect 23 | github.com/containerd/containerd/api v1.8.0 // indirect 24 | github.com/containerd/continuity v0.4.4 // indirect 25 | github.com/containerd/errdefs v0.3.0 // indirect 26 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 27 | github.com/containerd/fifo v1.1.0 // indirect 28 | github.com/containerd/log v0.1.0 // indirect 29 | github.com/containerd/platforms v0.2.1 // indirect 30 | github.com/containerd/ttrpc v1.2.7 // indirect 31 | github.com/containerd/typeurl/v2 v2.2.0 // indirect 32 | github.com/cyphar/filepath-securejoin v0.6.0 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/distribution/reference v0.6.0 // indirect 35 | github.com/docker/go-connections v0.4.0 // indirect 36 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect 37 | github.com/docker/go-units v0.5.0 // indirect 38 | github.com/felixge/httpsnoop v1.0.4 // indirect 39 | github.com/fsnotify/fsnotify v1.9.0 // indirect 40 | github.com/go-logr/logr v1.4.2 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 43 | github.com/gogo/protobuf v1.3.2 // indirect 44 | github.com/google/go-cmp v0.7.0 // indirect 45 | github.com/google/uuid v1.6.0 // indirect 46 | github.com/klauspost/compress v1.16.7 // indirect 47 | github.com/moby/docker-image-spec v1.3.1 // indirect 48 | github.com/moby/locker v1.0.1 // indirect 49 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 50 | github.com/moby/sys/mountinfo v0.6.2 // indirect 51 | github.com/moby/sys/sequential v0.6.0 // indirect 52 | github.com/moby/sys/signal v0.7.0 // indirect 53 | github.com/moby/sys/user v0.3.0 // indirect 54 | github.com/moby/sys/userns v0.1.0 // indirect 55 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect 56 | github.com/morikuni/aec v1.0.0 // indirect 57 | github.com/opencontainers/go-digest v1.0.0 // indirect 58 | github.com/opencontainers/image-spec v1.1.0 // indirect 59 | github.com/opencontainers/runtime-spec v1.1.0 // indirect 60 | github.com/opencontainers/selinux v1.13.0 // indirect 61 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 62 | github.com/pkg/errors v0.9.1 // indirect 63 | github.com/pmezard/go-difflib v1.0.0 // indirect 64 | github.com/sagikazarmark/locafero v0.11.0 // indirect 65 | github.com/sirupsen/logrus v1.9.3 // indirect 66 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 67 | github.com/spf13/afero v1.15.0 // indirect 68 | github.com/spf13/cast v1.10.0 // indirect 69 | github.com/spf13/pflag v1.0.10 // indirect 70 | github.com/subosito/gotenv v1.6.0 // indirect 71 | go.opencensus.io v0.24.0 // indirect 72 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 73 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 74 | go.opentelemetry.io/otel v1.35.0 // indirect 75 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 76 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 77 | go.yaml.in/yaml/v3 v3.0.4 // indirect 78 | golang.org/x/net v0.42.0 // indirect 79 | golang.org/x/sync v0.16.0 // indirect 80 | golang.org/x/sys v0.34.0 // indirect 81 | golang.org/x/text v0.28.0 // indirect 82 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect 83 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect 84 | google.golang.org/grpc v1.67.3 // indirect 85 | google.golang.org/protobuf v1.36.1 // indirect 86 | gopkg.in/yaml.v3 v3.0.1 // indirect 87 | gotest.tools/v3 v3.4.0 // indirect 88 | ) 89 | -------------------------------------------------------------------------------- /marshaller_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "syscall" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMarshallerConstants(t *testing.T) { 14 | assert.Equal(t, 1320, EVENT_EOE) 15 | } 16 | 17 | func TestAuditMarshaller_Consume(t *testing.T) { 18 | w := &bytes.Buffer{} 19 | m := NewAuditMarshaller(NewAuditWriter(w, 1), uint16(1100), uint16(1399), false, false, 0, []AuditFilter{}, nil) 20 | 21 | // Flush group on 1320 22 | m.Consume(&syscall.NetlinkMessage{ 23 | Header: syscall.NlMsghdr{ 24 | Len: uint32(44), 25 | Type: uint16(1300), 26 | Flags: uint16(0), 27 | Seq: uint32(0), 28 | Pid: uint32(0), 29 | }, 30 | Data: []byte("audit(10000001:1): hi there"), 31 | }) 32 | 33 | m.Consume(&syscall.NetlinkMessage{ 34 | Header: syscall.NlMsghdr{ 35 | Len: uint32(44), 36 | Type: uint16(1301), 37 | Flags: uint16(0), 38 | Seq: uint32(0), 39 | Pid: uint32(0), 40 | }, 41 | Data: []byte("audit(10000001:1): hi there"), 42 | }) 43 | 44 | m.Consume(new1320("1")) 45 | 46 | assert.Equal( 47 | t, 48 | "{\"sequence\":1,\"timestamp\":\"10000001\",\"messages\":[{\"type\":1300,\"data\":\"hi there\"},{\"type\":1301,\"data\":\"hi there\"}],\"uid_map\":{}}\n", 49 | w.String(), 50 | ) 51 | assert.Equal(t, 0, len(m.msgs)) 52 | 53 | // Ignore below 1100 54 | w.Reset() 55 | m.Consume(&syscall.NetlinkMessage{ 56 | Header: syscall.NlMsghdr{ 57 | Len: uint32(44), 58 | Type: uint16(1099), 59 | Flags: uint16(0), 60 | Seq: uint32(0), 61 | Pid: uint32(0), 62 | }, 63 | Data: []byte("audit(10000001:2): hi there"), 64 | }) 65 | 66 | assert.Equal(t, 0, len(m.msgs)) 67 | 68 | // Ignore above 1399 69 | w.Reset() 70 | m.Consume(&syscall.NetlinkMessage{ 71 | Header: syscall.NlMsghdr{ 72 | Len: uint32(44), 73 | Type: uint16(1400), 74 | Flags: uint16(0), 75 | Seq: uint32(0), 76 | Pid: uint32(0), 77 | }, 78 | Data: []byte("audit(10000001:3): hi there"), 79 | }) 80 | 81 | assert.Equal(t, 0, len(m.msgs)) 82 | 83 | // Ignore sequences of 0 84 | w.Reset() 85 | m.Consume(&syscall.NetlinkMessage{ 86 | Header: syscall.NlMsghdr{ 87 | Len: uint32(44), 88 | Type: uint16(1400), 89 | Flags: uint16(0), 90 | Seq: uint32(0), 91 | Pid: uint32(0), 92 | }, 93 | Data: []byte("audit(10000001:0): hi there"), 94 | }) 95 | 96 | assert.Equal(t, 0, len(m.msgs)) 97 | 98 | // Should flush old msgs after 2 seconds 99 | w.Reset() 100 | m.Consume(&syscall.NetlinkMessage{ 101 | Header: syscall.NlMsghdr{ 102 | Len: uint32(44), 103 | Type: uint16(1300), 104 | Flags: uint16(0), 105 | Seq: uint32(0), 106 | Pid: uint32(0), 107 | }, 108 | Data: []byte("audit(10000001:4): hi there"), 109 | }) 110 | 111 | start := time.Now() 112 | for len(m.msgs) != 0 { 113 | m.Consume(new1320("0")) 114 | } 115 | 116 | assert.Equal(t, "{\"sequence\":4,\"timestamp\":\"10000001\",\"messages\":[{\"type\":1300,\"data\":\"hi there\"}],\"uid_map\":{}}\n", w.String()) 117 | expected := start.Add(time.Second * 2) 118 | assert.True(t, expected.Equal(time.Now()) || expected.Before(time.Now()), "Should have taken at least 2 seconds to flush") 119 | assert.Equal(t, 0, len(m.msgs)) 120 | } 121 | 122 | func TestAuditMarshaller_completeMessage(t *testing.T) { 123 | //TODO: cant test because completeMessage calls exit 124 | t.Skip() 125 | return 126 | // lb, elb := hookLogger() 127 | // m := NewAuditMarshaller(NewAuditWriter(&FailWriter{}, 1), uint16(1300), uint16(1399), false, false, 0, []AuditFilter{}) 128 | 129 | // m.Consume(&syscall.NetlinkMessage{ 130 | // Header: syscall.NlMsghdr{ 131 | // Len: uint32(44), 132 | // Type: uint16(1300), 133 | // Flags: uint16(0), 134 | // Seq: uint32(0), 135 | // Pid: uint32(0), 136 | // }, 137 | // Data: []byte("audit(10000001:4): hi there"), 138 | // }) 139 | 140 | // m.completeMessage(4) 141 | // assert.Equal(t, "!", lb.String()) 142 | // assert.Equal(t, "!", elb.String()) 143 | } 144 | 145 | func new1320(seq string) *syscall.NetlinkMessage { 146 | return &syscall.NetlinkMessage{ 147 | Header: syscall.NlMsghdr{ 148 | Len: uint32(44), 149 | Type: uint16(1320), 150 | Flags: uint16(0), 151 | Seq: uint32(0), 152 | Pid: uint32(0), 153 | }, 154 | Data: []byte("audit(10000001:" + seq + "): "), 155 | } 156 | } 157 | 158 | type FailWriter struct{} 159 | 160 | func (f *FailWriter) Write(p []byte) (n int, err error) { 161 | return 0, errors.New("derp") 162 | } 163 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "sync/atomic" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | // Endianness is an alias for what we assume is the current machine endianness 14 | var Endianness = binary.LittleEndian 15 | 16 | const ( 17 | // MAX_AUDIT_MESSAGE_LENGTH see https://github.com/torvalds/linux/blob/v5.6/include/uapi/linux/audit.h#L441 18 | MAX_AUDIT_MESSAGE_LENGTH = 8970 19 | ) 20 | 21 | // TODO: this should live in a marshaller 22 | type AuditStatusPayload struct { 23 | Mask uint32 24 | Enabled uint32 25 | Failure uint32 26 | Pid uint32 27 | RateLimit uint32 28 | BacklogLimit uint32 29 | Lost uint32 30 | Backlog uint32 31 | Version uint32 32 | BacklogWaitTime uint32 33 | } 34 | 35 | // NetlinkPacket is an alias to give the header a similar name here 36 | type NetlinkPacket syscall.NlMsghdr 37 | 38 | type NetlinkClient struct { 39 | fd int 40 | address syscall.Sockaddr 41 | seq uint32 42 | buf []byte 43 | } 44 | 45 | // NewNetlinkClient creates a new NetLinkClient and optionally tries to modify the netlink recv buffer 46 | func NewNetlinkClient(recvSize int) (*NetlinkClient, error) { 47 | fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_AUDIT) 48 | if err != nil { 49 | return nil, fmt.Errorf("Could not create a socket: %s", err) 50 | } 51 | 52 | n := &NetlinkClient{ 53 | fd: fd, 54 | address: &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK, Groups: 0, Pid: 0}, 55 | buf: make([]byte, MAX_AUDIT_MESSAGE_LENGTH), 56 | } 57 | 58 | if err = syscall.Bind(fd, n.address); err != nil { 59 | syscall.Close(fd) 60 | return nil, fmt.Errorf("Could not bind to netlink socket: %s", err) 61 | } 62 | 63 | // Set the buffer size if we were asked 64 | if recvSize > 0 { 65 | if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, recvSize); err != nil { 66 | el.Println("Failed to set receive buffer size") 67 | } 68 | } 69 | 70 | // Print the current receive buffer size 71 | if v, err := syscall.GetsockoptInt(n.fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF); err == nil { 72 | l.Println("Socket receive buffer size:", v) 73 | } 74 | 75 | go func() { 76 | for { 77 | n.KeepConnection() 78 | time.Sleep(time.Second * 5) 79 | } 80 | }() 81 | 82 | return n, nil 83 | } 84 | 85 | // Send will send a packet and payload to the netlink socket without waiting for a response 86 | func (n *NetlinkClient) Send(np *NetlinkPacket, a *AuditStatusPayload) error { 87 | //We need to get the length first. This is a bit wasteful, but requests are rare so yolo.. 88 | buf := new(bytes.Buffer) 89 | var length int 90 | 91 | np.Seq = atomic.AddUint32(&n.seq, 1) 92 | 93 | for { 94 | buf.Reset() 95 | binary.Write(buf, Endianness, np) 96 | binary.Write(buf, Endianness, a) 97 | if np.Len == 0 { 98 | length = len(buf.Bytes()) 99 | np.Len = uint32(length) 100 | } else { 101 | break 102 | } 103 | } 104 | 105 | if err := syscall.Sendto(n.fd, buf.Bytes(), 0, n.address); err != nil { 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // Receive will receive a packet from a netlink socket 113 | func (n *NetlinkClient) Receive() (*syscall.NetlinkMessage, error) { 114 | nlen, _, err := syscall.Recvfrom(n.fd, n.buf, 0) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if nlen < 1 { 120 | return nil, errors.New("Got a 0 length packet") 121 | } 122 | 123 | msg := &syscall.NetlinkMessage{ 124 | Header: syscall.NlMsghdr{ 125 | Len: Endianness.Uint32(n.buf[0:4]), 126 | Type: Endianness.Uint16(n.buf[4:6]), 127 | Flags: Endianness.Uint16(n.buf[6:8]), 128 | Seq: Endianness.Uint32(n.buf[8:12]), 129 | Pid: Endianness.Uint32(n.buf[12:16]), 130 | }, 131 | Data: n.buf[syscall.SizeofNlMsghdr:nlen], 132 | } 133 | 134 | return msg, nil 135 | } 136 | 137 | // KeepConnection re-establishes our connection to the netlink socket 138 | func (n *NetlinkClient) KeepConnection() { 139 | payload := &AuditStatusPayload{ 140 | Mask: 4, 141 | Enabled: 1, 142 | Pid: uint32(syscall.Getpid()), 143 | //TODO: Failure: http://lxr.free-electrons.com/source/include/uapi/linux/audit.h#L338 144 | } 145 | 146 | packet := &NetlinkPacket{ 147 | Type: uint16(1001), 148 | Flags: syscall.NLM_F_REQUEST | syscall.NLM_F_ACK, 149 | Pid: uint32(syscall.Getpid()), 150 | } 151 | 152 | err := n.Send(packet, payload) 153 | if err != nil { 154 | el.Println("Error occurred while trying to keep the connection:", err) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "syscall" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestAuditConstants(t *testing.T) { 11 | assert.Equal(t, 7, HEADER_MIN_LENGTH) 12 | assert.Equal(t, 6, HEADER_START_POS) 13 | assert.Equal(t, time.Second*2, COMPLETE_AFTER) 14 | assert.Equal(t, []byte{")"[0]}, headerEndChar) 15 | } 16 | 17 | func TestNewAuditMessage(t *testing.T) { 18 | msg := &syscall.NetlinkMessage{ 19 | Header: syscall.NlMsghdr{ 20 | Len: uint32(44), 21 | Type: uint16(1309), 22 | Flags: uint16(0), 23 | Seq: uint32(0), 24 | Pid: uint32(0), 25 | }, 26 | Data: []byte("audit(10000001:99): hi there"), 27 | } 28 | 29 | am := NewAuditMessage(msg) 30 | assert.Equal(t, uint16(1309), am.Type) 31 | assert.Equal(t, 99, am.Seq) 32 | assert.Equal(t, "10000001", am.AuditTime) 33 | assert.Equal(t, "hi there", am.Data) 34 | } 35 | 36 | func TestAuditMessageGroup_AddMessage(t *testing.T) { 37 | uidMap = make(map[string]string, 0) 38 | uidMap["0"] = "hi" 39 | uidMap["1"] = "nope" 40 | 41 | amg := &AuditMessageGroup{ 42 | Seq: 1, 43 | AuditTime: "ok", 44 | CompleteAfter: time.Now().Add(COMPLETE_AFTER), 45 | UidMap: make(map[string]string, 2), 46 | } 47 | 48 | m := &AuditMessage{ 49 | Data: "uid=0 things notuid=nopethisisnot", 50 | } 51 | 52 | amg.AddMessage(m) 53 | assert.Equal(t, 1, len(amg.Msgs), "Expected 1 message") 54 | assert.Equal(t, m, amg.Msgs[0], "First message was wrong") 55 | assert.Equal(t, 1, len(amg.UidMap), "Incorrect uid mapping count") 56 | assert.Equal(t, "hi", amg.UidMap["0"]) 57 | 58 | // Make sure we don't parse uids for message types that don't have them 59 | m = &AuditMessage{ 60 | Type: uint16(1309), 61 | Data: "uid=1", 62 | } 63 | amg.AddMessage(m) 64 | assert.Equal(t, 2, len(amg.Msgs), "Expected 2 messages") 65 | assert.Equal(t, m, amg.Msgs[1], "2nd message was wrong") 66 | assert.Equal(t, 1, len(amg.UidMap), "Incorrect uid mapping count") 67 | 68 | m = &AuditMessage{ 69 | Type: uint16(1307), 70 | Data: "uid=1", 71 | } 72 | amg.AddMessage(m) 73 | assert.Equal(t, 3, len(amg.Msgs), "Expected 2 messages") 74 | assert.Equal(t, m, amg.Msgs[2], "3rd message was wrong") 75 | assert.Equal(t, 1, len(amg.UidMap), "Incorrect uid mapping count") 76 | } 77 | 78 | func TestNewAuditMessageGroup(t *testing.T) { 79 | uidMap = make(map[string]string, 0) 80 | m := &AuditMessage{ 81 | Type: uint16(1300), 82 | Seq: 1019, 83 | AuditTime: "9919", 84 | Data: "Stuff is here", 85 | } 86 | 87 | amg := NewAuditMessageGroup(m) 88 | assert.Equal(t, 1019, amg.Seq) 89 | assert.Equal(t, "9919", amg.AuditTime) 90 | assert.True(t, amg.CompleteAfter.After(time.Now()), "Complete after time should be greater than right now") 91 | assert.Equal(t, 6, cap(amg.Msgs), "Msgs capacity should be 6") 92 | assert.Equal(t, 1, len(amg.Msgs), "Msgs should only have 1 message") 93 | assert.Equal(t, 0, len(amg.UidMap), "No uids in the original message") 94 | assert.Equal(t, m, amg.Msgs[0], "First message should be the original") 95 | } 96 | 97 | func Test_getUsername(t *testing.T) { 98 | uidMap = make(map[string]string, 0) 99 | assert.Equal(t, "root", getUsername("0"), "0 should be root you animal") 100 | assert.Equal(t, "UNKNOWN_USER", getUsername("-1"), "Expected UNKNOWN_USER") 101 | 102 | val, ok := uidMap["0"] 103 | if !ok { 104 | t.Fatal("Expected the uid mapping to be cached") 105 | } 106 | assert.Equal(t, "root", val) 107 | 108 | val, ok = uidMap["-1"] 109 | if !ok { 110 | t.Fatal("Expected the uid mapping to be cached") 111 | } 112 | assert.Equal(t, "UNKNOWN_USER", val) 113 | } 114 | 115 | func TestAuditMessageGroup_mapUids(t *testing.T) { 116 | uidMap = make(map[string]string, 0) 117 | uidMap["0"] = "hi" 118 | uidMap["1"] = "there" 119 | uidMap["2"] = "fun" 120 | uidMap["3"] = "test" 121 | uidMap["99999"] = "derp" 122 | 123 | amg := &AuditMessageGroup{ 124 | Seq: 1, 125 | AuditTime: "ok", 126 | CompleteAfter: time.Now().Add(COMPLETE_AFTER), 127 | UidMap: make(map[string]string, 2), 128 | } 129 | 130 | m := &AuditMessage{ 131 | Data: "uid=0 1uid=1 2uid=2 3uid=3 not here 4uid=99999", 132 | } 133 | amg.mapUids(m) 134 | 135 | assert.Equal(t, 5, len(amg.UidMap), "Uid map is too big") 136 | assert.Equal(t, "hi", amg.UidMap["0"]) 137 | assert.Equal(t, "there", amg.UidMap["1"]) 138 | assert.Equal(t, "fun", amg.UidMap["2"]) 139 | assert.Equal(t, "test", amg.UidMap["3"]) 140 | assert.Equal(t, "derp", amg.UidMap["99999"]) 141 | } 142 | 143 | func Benchmark_getUsername(b *testing.B) { 144 | for i := 0; i < b.N; i++ { 145 | _ = getUsername("0") 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /extras_containers_capsule8.go: -------------------------------------------------------------------------------- 1 | //go:build !nocontainers 2 | // +build !nocontainers 3 | 4 | // NOTE: This code was originally sourced from: 5 | // 6 | // https://github.com/capsule8/capsule8 7 | // 8 | // But this repository was removed, so we have vendored just these functions 9 | // that we need, with the original license intact. 10 | 11 | // Copyright 2018 Capsule8, Inc. 12 | // 13 | // Licensed under the Apache License, Version 2.0 (the "License"); 14 | // you may not use this file except in compliance with the License. 15 | // You may obtain a copy of the License at 16 | // 17 | // http://www.apache.org/licenses/LICENSE-2.0 18 | // 19 | // Unless required by applicable law or agreed to in writing, software 20 | // distributed under the License is distributed on an "AS IS" BASIS, 21 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | // See the License for the specific language governing permissions and 23 | // limitations under the License. 24 | 25 | package main 26 | 27 | import ( 28 | "bufio" 29 | "bytes" 30 | "crypto/sha256" 31 | "fmt" 32 | "os" 33 | "strconv" 34 | "strings" 35 | ) 36 | 37 | // controlGroup describes the cgroup membership of a process 38 | type controlGroup struct { 39 | // Unique hierarchy ID 40 | ID int 41 | 42 | // Cgroup controllers (subsystems) bound to the hierarchy 43 | Controllers []string 44 | 45 | // Path is the pathname of the control group to which the process 46 | // belongs. It is relative to the mountpoint of the hierarchy. 47 | Path string 48 | } 49 | 50 | // processContainerID returns the container ID running the specified process. 51 | // If the process is not running inside of a container, the return will be the 52 | // empty string. 53 | func processContainerID(pid int) (string, error) { 54 | cgroups, err := taskControlGroups(pid, pid) 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | for _, cg := range cgroups { 60 | if id := containerID(cg.Path); id != "" { 61 | return id, nil 62 | } 63 | } 64 | 65 | return "", nil 66 | } 67 | 68 | // TaskControlGroups returns the cgroup membership of the specified task. 69 | func taskControlGroups(tgid, pid int) ([]controlGroup, error) { 70 | filename := fmt.Sprintf("/proc/%d/task/%d/cgroup", tgid, pid) 71 | data, err := os.ReadFile(filename) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | var cgroups []controlGroup 77 | 78 | scanner := bufio.NewScanner(bytes.NewReader(data)) 79 | for scanner.Scan() { 80 | t := scanner.Text() 81 | parts := strings.Split(t, ":") 82 | var ID int 83 | ID, err = strconv.Atoi(parts[0]) 84 | if err != nil { 85 | // glog.Warningf("Couldn't parse cgroup line: %s", t) 86 | continue 87 | } 88 | 89 | c := controlGroup{ 90 | ID: ID, 91 | Controllers: strings.Split(parts[1], ","), 92 | Path: parts[2], 93 | } 94 | 95 | cgroups = append(cgroups, c) 96 | } 97 | 98 | return cgroups, nil 99 | } 100 | 101 | // Historical note: 102 | // procfs.FileSystem.ProcessContainerID was initially written to use a regex 103 | // to determine whether a cgroup path was for a container: 104 | // 105 | // Docker cgroup paths may look like either of: 106 | // - /docker/[CONTAINER_ID] 107 | // - /kubepods/[...]/[CONTAINER_ID] 108 | // - /system.slice/docker-[CONTAINER_ID].scope 109 | // 110 | // const cgroupContainerPattern = "^(/docker/|/kubepods/.*/|/system.slice/docker-)([[:xdigit:]]{64})(.scope|$)" 111 | // 112 | // I've elected to not continue using this method, because it is inherently 113 | // fragile. We can see here that Docker has already changed its format at least 114 | // once. It also fails to work for anything other than Docker. Other container 115 | // environments are not accounted for. More frustratingly, LXC, for example, 116 | // even allows runtime customization of cgroup paths. 117 | // 118 | // What does not appear to be so fragile is that container IDs always have a 119 | // sha256 hash in them. So we're going to look for sha256 strings. 120 | 121 | func isHexDigit(r rune) bool { 122 | if r >= '0' && r <= '9' { 123 | return true 124 | } 125 | if r >= 'A' && r <= 'F' { 126 | return true 127 | } 128 | if r >= 'a' && r <= 'f' { 129 | return true 130 | } 131 | return false 132 | } 133 | 134 | // sha256.Size is sha256 size in bytes. Hexadecimal representation doubles that 135 | const sha256HexSize = sha256.Size * 2 136 | 137 | func isSHA256(s string) bool { 138 | if len(s) != sha256HexSize { 139 | return false 140 | } 141 | for _, c := range s { 142 | if !isHexDigit(c) { 143 | return false 144 | } 145 | } 146 | return true 147 | } 148 | 149 | // ContainerID returns the ContainerID extracted from the given string. The 150 | // string may simply be a container ID or it may be a full cgroup controller 151 | // path with a container ID embedded in it. If the given string contains no 152 | // discernable container ID, the return will be "". 153 | func containerID(s string) string { 154 | paths := strings.Split(s, "/") 155 | for _, p := range paths { 156 | if isSHA256(p) { 157 | return p 158 | } 159 | if len(p) > sha256HexSize { 160 | // Does it start with a sha256? 161 | x := p[:sha256HexSize] 162 | if !isHexDigit(rune(p[sha256HexSize])) && isSHA256(x) { 163 | return x 164 | } 165 | // Does it end with a sha256? 166 | p = strings.TrimSuffix(p, ".scope") 167 | x = p[len(p)-sha256HexSize:] 168 | if !isHexDigit(rune(p[len(p)-sha256HexSize-1])) && isSHA256(x) { 169 | return x 170 | } 171 | } 172 | } 173 | return "" 174 | } 175 | -------------------------------------------------------------------------------- /marshaller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "syscall" 7 | "time" 8 | ) 9 | 10 | const ( 11 | EVENT_EOE = 1320 // End of multi packet event 12 | ) 13 | 14 | type AuditMarshaller struct { 15 | msgs map[int]*AuditMessageGroup 16 | writer *AuditWriter 17 | lastSeq int 18 | missed map[int]bool 19 | worstLag int 20 | eventMin uint16 21 | eventMax uint16 22 | trackMessages bool 23 | logOutOfOrder bool 24 | maxOutOfOrder int 25 | attempts int 26 | filters map[string]map[uint16][]*regexp.Regexp // { syscall: { mtype: [regexp, ...] } } 27 | extraParsers ExtraParsers 28 | } 29 | 30 | type AuditFilter struct { 31 | messageType uint16 32 | regex *regexp.Regexp 33 | syscall string 34 | } 35 | 36 | // Create a new marshaller 37 | func NewAuditMarshaller(w *AuditWriter, eventMin uint16, eventMax uint16, trackMessages, logOOO bool, maxOOO int, filters []AuditFilter, extraParsers ExtraParsers) *AuditMarshaller { 38 | am := AuditMarshaller{ 39 | writer: w, 40 | msgs: make(map[int]*AuditMessageGroup, 5), // It is not typical to have more than 2 message groups at any given time 41 | missed: make(map[int]bool, 10), 42 | eventMin: eventMin, 43 | eventMax: eventMax, 44 | trackMessages: trackMessages, 45 | logOutOfOrder: logOOO, 46 | maxOutOfOrder: maxOOO, 47 | filters: make(map[string]map[uint16][]*regexp.Regexp), 48 | extraParsers: extraParsers, 49 | } 50 | 51 | for _, filter := range filters { 52 | if _, ok := am.filters[filter.syscall]; !ok { 53 | am.filters[filter.syscall] = make(map[uint16][]*regexp.Regexp) 54 | } 55 | 56 | if _, ok := am.filters[filter.syscall][filter.messageType]; !ok { 57 | am.filters[filter.syscall][filter.messageType] = []*regexp.Regexp{} 58 | } 59 | 60 | am.filters[filter.syscall][filter.messageType] = append(am.filters[filter.syscall][filter.messageType], filter.regex) 61 | } 62 | 63 | return &am 64 | } 65 | 66 | // Ingests a netlink message and likely prepares it to be logged 67 | func (a *AuditMarshaller) Consume(nlMsg *syscall.NetlinkMessage) { 68 | aMsg := NewAuditMessage(nlMsg) 69 | 70 | if aMsg.Seq == 0 { 71 | // We got an invalid audit message, return the current message and reset 72 | a.flushOld() 73 | return 74 | } 75 | 76 | if a.trackMessages { 77 | a.detectMissing(aMsg.Seq) 78 | } 79 | 80 | if nlMsg.Header.Type < a.eventMin || nlMsg.Header.Type > a.eventMax { 81 | // Drop all audit messages that aren't things we care about or end a multi packet event 82 | a.flushOld() 83 | return 84 | } else if nlMsg.Header.Type == EVENT_EOE { 85 | // This is end of event msg, flush the msg with that sequence and discard this one 86 | a.completeMessage(aMsg.Seq) 87 | return 88 | } 89 | 90 | if val, ok := a.msgs[aMsg.Seq]; ok { 91 | // Use the original AuditMessageGroup if we have one 92 | val.AddMessage(aMsg) 93 | } else { 94 | // Create a new AuditMessageGroup 95 | a.msgs[aMsg.Seq] = NewAuditMessageGroup(aMsg) 96 | } 97 | a.extraParsers.Parse(aMsg) 98 | 99 | a.flushOld() 100 | } 101 | 102 | // Outputs any messages that are old enough 103 | // This is because there is no indication of multi message events coming from kaudit 104 | func (a *AuditMarshaller) flushOld() { 105 | now := time.Now() 106 | for seq, msg := range a.msgs { 107 | if msg.CompleteAfter.Before(now) || now.Equal(msg.CompleteAfter) { 108 | a.completeMessage(seq) 109 | } 110 | } 111 | } 112 | 113 | // Write a complete message group to the configured output in json format 114 | func (a *AuditMarshaller) completeMessage(seq int) { 115 | var msg *AuditMessageGroup 116 | var ok bool 117 | 118 | if msg, ok = a.msgs[seq]; !ok { 119 | //TODO: attempted to complete a missing message, log? 120 | return 121 | } 122 | 123 | if a.dropMessage(msg) { 124 | delete(a.msgs, seq) 125 | return 126 | } 127 | 128 | if err := a.writer.Write(msg); err != nil { 129 | el.Println("Failed to write message. Error:", err) 130 | os.Exit(1) 131 | } 132 | 133 | delete(a.msgs, seq) 134 | } 135 | 136 | func (a *AuditMarshaller) dropMessage(msg *AuditMessageGroup) bool { 137 | filters, ok := a.filters[msg.Syscall] 138 | if !ok { 139 | return false 140 | } 141 | 142 | for _, msg := range msg.Msgs { 143 | if fg, ok := filters[msg.Type]; ok { 144 | for _, filter := range fg { 145 | if filter.MatchString(msg.Data) { 146 | return true 147 | } 148 | } 149 | } 150 | } 151 | 152 | return false 153 | } 154 | 155 | // Track sequence numbers and log if we suspect we missed a message 156 | func (a *AuditMarshaller) detectMissing(seq int) { 157 | if seq > a.lastSeq+1 && a.lastSeq != 0 { 158 | // We likely leap frogged over a msg, wait until the next sequence to make sure 159 | for i := a.lastSeq + 1; i < seq; i++ { 160 | a.missed[i] = true 161 | } 162 | } 163 | 164 | for missedSeq := range a.missed { 165 | if missedSeq == seq { 166 | lag := a.lastSeq - missedSeq 167 | if lag > a.worstLag { 168 | a.worstLag = lag 169 | } 170 | 171 | if a.logOutOfOrder { 172 | el.Println("Got sequence", missedSeq, "after", lag, "messages. Worst lag so far", a.worstLag, "messages") 173 | } 174 | delete(a.missed, missedSeq) 175 | } else if seq-missedSeq > a.maxOutOfOrder { 176 | el.Printf("Likely missed sequence %d, current %d, worst message delay %d\n", missedSeq, seq, a.worstLag) 177 | delete(a.missed, missedSeq) 178 | } 179 | } 180 | 181 | if seq > a.lastSeq { 182 | // Keep track of the largest sequence 183 | a.lastSeq = seq 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "github.com/stretchr/testify/assert" 7 | "os" 8 | "syscall" 9 | "testing" 10 | ) 11 | 12 | func TestNetlinkClient_KeepConnection(t *testing.T) { 13 | n := makeNelinkClient(t) 14 | defer syscall.Close(n.fd) 15 | 16 | n.KeepConnection() 17 | msg, err := n.Receive() 18 | if err != nil { 19 | t.Fatal("Did not expect an error", err) 20 | } 21 | 22 | expectedData := []byte{4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 23 | binary.LittleEndian.PutUint32(expectedData[12:16], uint32(os.Getpid())) 24 | 25 | assert.Equal(t, uint16(1001), msg.Header.Type, "Header.Type mismatch") 26 | assert.Equal(t, uint16(5), msg.Header.Flags, "Header.Flags mismatch") 27 | assert.Equal(t, uint32(1), msg.Header.Seq, "Header.Seq mismatch") 28 | assert.Equal(t, uint32(56), msg.Header.Len, "Packet size is wrong - this test is brittle though") 29 | assert.EqualValues(t, msg.Data[:40], expectedData, "data was wrong") 30 | 31 | // Make sure we get errors printed 32 | lb, elb := hookLogger() 33 | defer resetLogger() 34 | syscall.Close(n.fd) 35 | n.KeepConnection() 36 | assert.Equal(t, "", lb.String(), "Got some log lines we did not expect") 37 | assert.Equal(t, "Error occurred while trying to keep the connection: bad file descriptor\n", elb.String(), "Figured we would have an error") 38 | } 39 | 40 | func TestNetlinkClient_SendReceive(t *testing.T) { 41 | var err error 42 | var msg *syscall.NetlinkMessage 43 | 44 | // Build our client 45 | n := makeNelinkClient(t) 46 | defer syscall.Close(n.fd) 47 | 48 | // Make sure we can encode/decode properly 49 | payload := &AuditStatusPayload{ 50 | Mask: 4, 51 | Enabled: 1, 52 | Pid: uint32(1006), 53 | } 54 | 55 | packet := &NetlinkPacket{ 56 | Type: uint16(1001), 57 | Flags: syscall.NLM_F_REQUEST | syscall.NLM_F_ACK, 58 | Pid: uint32(1006), 59 | } 60 | 61 | msg = sendReceive(t, n, packet, payload) 62 | 63 | assert.Equal(t, uint32(1006), msg.Header.Pid, "Header.Pid mismatch") 64 | assert.Equal(t, packet.Type, msg.Header.Type, "Header.Type mismatch") 65 | assert.Equal(t, packet.Flags, msg.Header.Flags, "Header.Flags mismatch") 66 | assert.Equal(t, uint32(1), msg.Header.Seq, "Header.Seq mismatch") 67 | assert.Equal(t, uint32(56), msg.Header.Len, "Packet size is wrong - this test is brittle though") 68 | assert.EqualValues(t, msg.Data[:40], []byte{4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 238, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, "data was wrong") 69 | 70 | // Make sure sequences numbers increment on our side 71 | msg = sendReceive(t, n, packet, payload) 72 | assert.Equal(t, uint32(2), msg.Header.Seq, "Header.Seq did not increment") 73 | 74 | // Make sure 0 length packets result in an error 75 | syscall.Sendto(n.fd, []byte{}, 0, n.address) 76 | _, err = n.Receive() 77 | assert.Equal(t, "Got a 0 length packet", err.Error(), "Error was incorrect") 78 | 79 | // Make sure we get errors from sendto back 80 | syscall.Close(n.fd) 81 | err = n.Send(packet, payload) 82 | assert.Equal(t, "bad file descriptor", err.Error(), "Error was incorrect") 83 | 84 | // Make sure we get errors from recvfrom back 85 | n.fd = 0 86 | _, err = n.Receive() 87 | assert.Equal(t, "socket operation on non-socket", err.Error(), "Error was incorrect") 88 | } 89 | 90 | func TestNewNetlinkClient(t *testing.T) { 91 | lb, elb := hookLogger() 92 | defer resetLogger() 93 | 94 | n, err := NewNetlinkClient(1024) 95 | 96 | assert.Nil(t, err) 97 | if n == nil { 98 | t.Fatal("Expected a netlink client but had an error instead!") 99 | } else { 100 | assert.True(t, (n.fd > 0), "No file descriptor") 101 | assert.True(t, (n.address != nil), "Address was nil") 102 | assert.Equal(t, uint32(0), n.seq, "Seq should start at 0") 103 | assert.True(t, MAX_AUDIT_MESSAGE_LENGTH >= len(n.buf), "Client buffer is too small") 104 | 105 | assert.Equal(t, "Socket receive buffer size: ", lb.String()[:28], "Expected some nice log lines") 106 | assert.Equal(t, "", elb.String(), "Did not expect any error messages") 107 | } 108 | } 109 | 110 | // Helper to make a client listening on a unix socket 111 | func makeNelinkClient(t *testing.T) *NetlinkClient { 112 | os.Remove("go-audit.test.sock") 113 | fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_RAW, 0) 114 | if err != nil { 115 | t.Fatal("Could not create a socket:", err) 116 | } 117 | 118 | n := &NetlinkClient{ 119 | fd: fd, 120 | address: &syscall.SockaddrUnix{Name: "go-audit.test.sock"}, 121 | buf: make([]byte, MAX_AUDIT_MESSAGE_LENGTH), 122 | } 123 | 124 | if err = syscall.Bind(fd, n.address); err != nil { 125 | syscall.Close(fd) 126 | t.Fatal("Could not bind to netlink socket:", err) 127 | } 128 | 129 | return n 130 | } 131 | 132 | // Helper to send and then receive a message with the netlink client 133 | func sendReceive(t *testing.T, n *NetlinkClient, packet *NetlinkPacket, payload *AuditStatusPayload) *syscall.NetlinkMessage { 134 | err := n.Send(packet, payload) 135 | if err != nil { 136 | t.Fatal("Failed to send:", err) 137 | } 138 | 139 | msg, err := n.Receive() 140 | if err != nil { 141 | t.Fatal("Failed to receive:", err) 142 | } 143 | 144 | return msg 145 | } 146 | 147 | // Resets global loggers 148 | func resetLogger() { 149 | l.SetOutput(os.Stdout) 150 | el.SetOutput(os.Stderr) 151 | } 152 | 153 | // Hooks the global loggers writers so you can assert their contents 154 | func hookLogger() (lb *bytes.Buffer, elb *bytes.Buffer) { 155 | lb = &bytes.Buffer{} 156 | l.SetOutput(lb) 157 | 158 | elb = &bytes.Buffer{} 159 | el.SetOutput(elb) 160 | return 161 | } 162 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os/user" 6 | "strconv" 7 | "strings" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | var uidMap = map[string]string{} 13 | var headerEndChar = []byte{")"[0]} 14 | var headerSepChar = byte(':') 15 | var spaceChar = byte(' ') 16 | 17 | const ( 18 | HEADER_MIN_LENGTH = 7 // Minimum length of an audit header 19 | HEADER_START_POS = 6 // Position in the audit header that the data starts 20 | COMPLETE_AFTER = time.Second * 2 // Log a message after this time or EOE 21 | ) 22 | 23 | type AuditMessage struct { 24 | Type uint16 `json:"type"` 25 | Data string `json:"data"` 26 | Seq int `json:"-"` 27 | AuditTime string `json:"-"` 28 | 29 | Containers map[string]string `json:"containers,omitempty"` 30 | Extras *AuditExtras `json:"extras,omitempty"` 31 | } 32 | 33 | type AuditExtras struct { 34 | CgroupRoot string `json:"cgroup_root,omitempty"` 35 | } 36 | 37 | type AuditMessageGroup struct { 38 | Seq int `json:"sequence"` 39 | AuditTime string `json:"timestamp"` 40 | CompleteAfter time.Time `json:"-"` 41 | Msgs []*AuditMessage `json:"messages"` 42 | UidMap map[string]string `json:"uid_map"` 43 | Syscall string `json:"-"` 44 | } 45 | 46 | // Creates a new message group from the details parsed from the message 47 | func NewAuditMessageGroup(am *AuditMessage) *AuditMessageGroup { 48 | //TODO: allocating 6 msgs per group is lame and we _should_ know ahead of time roughly how many we need 49 | amg := &AuditMessageGroup{ 50 | Seq: am.Seq, 51 | AuditTime: am.AuditTime, 52 | CompleteAfter: time.Now().Add(COMPLETE_AFTER), 53 | UidMap: make(map[string]string, 2), // Usually only 2 individual uids per execve 54 | Msgs: make([]*AuditMessage, 0, 6), 55 | } 56 | 57 | amg.AddMessage(am) 58 | return amg 59 | } 60 | 61 | // Creates a new go-audit message from a netlink message 62 | func NewAuditMessage(nlm *syscall.NetlinkMessage) *AuditMessage { 63 | aTime, seq := parseAuditHeader(nlm) 64 | return &AuditMessage{ 65 | Type: nlm.Header.Type, 66 | Data: string(nlm.Data), 67 | Seq: seq, 68 | AuditTime: aTime, 69 | } 70 | } 71 | 72 | // Gets the timestamp and audit sequence id from a netlink message 73 | func parseAuditHeader(msg *syscall.NetlinkMessage) (time string, seq int) { 74 | headerStop := bytes.Index(msg.Data, headerEndChar) 75 | // If the position the header appears to stop is less than the minimum length of a header, bail out 76 | if headerStop < HEADER_MIN_LENGTH { 77 | return 78 | } 79 | 80 | header := string(msg.Data[:headerStop]) 81 | if header[:HEADER_START_POS] == "audit(" { 82 | //TODO: out of range check, possibly fully binary? 83 | sep := strings.IndexByte(header, headerSepChar) 84 | time = header[HEADER_START_POS:sep] 85 | seq, _ = strconv.Atoi(header[sep+1:]) 86 | 87 | // Remove the header from data 88 | msg.Data = msg.Data[headerStop+3:] 89 | } 90 | 91 | return time, seq 92 | } 93 | 94 | // Add a new message to the current message group 95 | func (amg *AuditMessageGroup) AddMessage(am *AuditMessage) { 96 | amg.Msgs = append(amg.Msgs, am) 97 | //TODO: need to find more message types that won't contain uids, also make these constants 98 | switch am.Type { 99 | case 1309, 1307, 1306: 100 | // Don't map uids here 101 | case 1300: 102 | amg.findSyscall(am) 103 | amg.mapUids(am) 104 | default: 105 | amg.mapUids(am) 106 | } 107 | } 108 | 109 | // Find all `uid=` occurrences in a message and adds the username to the UidMap object 110 | func (amg *AuditMessageGroup) mapUids(am *AuditMessage) { 111 | data := am.Data 112 | start := 0 113 | end := 0 114 | 115 | for { 116 | if start = strings.Index(data, "uid="); start < 0 { 117 | break 118 | } 119 | 120 | // Progress the start point beyon the = sign 121 | start += 4 122 | if end = strings.IndexByte(data[start:], spaceChar); end < 0 { 123 | // There was no ending space, maybe the uid is at the end of the line 124 | end = len(data) - start 125 | 126 | // If the end of the line is greater than 5 characters away (overflows a 16 bit uint) then it can't be a uid 127 | if end > 5 { 128 | break 129 | } 130 | } 131 | 132 | uid := data[start : start+end] 133 | 134 | // Don't bother re-adding if the existing group already has the mapping 135 | if _, ok := amg.UidMap[uid]; !ok { 136 | amg.UidMap[uid] = getUsername(data[start : start+end]) 137 | } 138 | 139 | // Find the next uid= if we have space for one 140 | next := start + end + 1 141 | if next >= len(data) { 142 | break 143 | } 144 | 145 | data = data[next:] 146 | } 147 | 148 | } 149 | 150 | func (amg *AuditMessageGroup) findSyscall(am *AuditMessage) { 151 | data := am.Data 152 | start := 0 153 | end := 0 154 | 155 | if start = strings.Index(data, "syscall="); start < 0 { 156 | return 157 | } 158 | 159 | // Progress the start point beyond the = sign 160 | start += 8 161 | if end = strings.IndexByte(data[start:], spaceChar); end < 0 { 162 | // There was no ending space, maybe the syscall id is at the end of the line 163 | end = len(data) - start 164 | 165 | // If the end of the line is greater than 5 characters away (overflows a 16 bit uint) then it can't be a syscall id 166 | if end > 5 { 167 | return 168 | } 169 | } 170 | 171 | amg.Syscall = data[start : start+end] 172 | } 173 | 174 | // Gets a username for a user id 175 | func getUsername(uid string) string { 176 | uname := "UNKNOWN_USER" 177 | 178 | // Make sure we have a uid element to work with. 179 | // Give a default value in case we don't find something. 180 | if lUser, ok := uidMap[uid]; ok { 181 | uname = lUser 182 | } else { 183 | lUser, err := user.LookupId(uid) 184 | if err == nil { 185 | uname = lUser.Username 186 | } 187 | uidMap[uid] = uname 188 | } 189 | 190 | return uname 191 | } 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-audit 2 | 3 | [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](http://opensource.org/licenses/MIT) 4 | 5 | ## About 6 | 7 | go-audit is an alternative to the auditd daemon that ships with many distros. 8 | After having created an [auditd audisp](https://people.redhat.com/sgrubb/audit/) plugin to convert audit logs to json, 9 | I became interested in creating a replacement for the existing daemon. 10 | 11 | ##### Goals 12 | 13 | * Safe : Written in a modern language that is type safe and performant 14 | * Fast : Never ever ever ever block if we can avoid it 15 | * Outputs json : Yay 16 | * Pluggable pipelines : Can write to syslog, local file, Graylog2 or stdout. Additional outputs are easily written. 17 | * Connects to the linux kernel via netlink (info [here](https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/kernel/audit.c?id=refs/tags/v3.14.56) and [here](https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/include/uapi/linux/audit.h?h=linux-3.14.y)) 18 | 19 | ## Usage 20 | 21 | ##### Installation 22 | 23 | 1. Install [golang](https://golang.org/doc/install), version 1.14 or greater is required 24 | 2. Clone the repo 25 | 26 | ``` 27 | git clone (this repo) 28 | cd go-audit 29 | ``` 30 | 31 | 3. Build the binary 32 | 33 | ``` 34 | make 35 | ``` 36 | 37 | 4. Copy the binary `go-audit` to wherever you'd like 38 | 39 | ##### Testing 40 | 41 | - `make test` - run the unit test suite 42 | - `make test-cov-html` - run the unit tests and open up the code coverage results 43 | - `make bench` - run the benchmark test suite 44 | - `make bench-cpu` - run the benchmark test suite with cpu profiling 45 | - `make bench-cpulong` - run the benchmark test suite with cpu profiling and try to get some gc collection 46 | 47 | ##### Running as a service 48 | 49 | Check the [contrib](contrib) folder, it contains examples for how to run `go-audit` as a proper service on your machine. 50 | 51 | ##### Example Config 52 | 53 | See [go-audit.yaml.example](go-audit.yaml.example) 54 | 55 | ## FAQ 56 | 57 | #### I am seeing `Error during message receive: no buffer space available` in the logs 58 | 59 | This is because `go-audit` is not receiving data as quickly as your system is generating it. You can increase 60 | the receive buffer system wide and maybe it will help. Best to try and reduce the amount of data `go-audit` has 61 | to handle. 62 | 63 | If reducing audit velocity is not an option you can try increasing `socket_buffer.receive` in your config. 64 | See [Example Config](#example-config) for more information 65 | 66 | ``` 67 | socket_buffer: 68 | receive: 69 | ``` 70 | 71 | #### Sometime files don't have a `name`, only `inode`, what gives? 72 | 73 | The kernel doesn't always know the filename for file access. Figuring out the filename from an inode is expensive and 74 | error prone. 75 | 76 | You can map back to a filename, possibly not *the* filename, that triggured the audit line though. 77 | 78 | ``` 79 | sudo debugfs -R "ncheck " /dev/ 80 | ``` 81 | 82 | #### I don't like math and want you to tell me the syslog priority to use 83 | 84 | Use the default, or consult this handy table. 85 | 86 | Wikipedia has a pretty good [page](https://en.wikipedia.org/wiki/Syslog) on this 87 | 88 | | | emerg (0)| alert (1) | crit (2) | err (3) | warn (4) | notice (5) | info (6) | debug (7) | 89 | |-------------------|----------|-----------|-----------|---------|----------|------------|-----------|-----------| 90 | | **kernel (0)** | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 91 | | **user (1)** | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 92 | | **mail (2)** | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 93 | | **daemon (3)** | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 94 | | **auth (4)** | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 95 | | **syslog (5)** | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 96 | | **lpr (6)** | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 97 | | **news (7)** | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 98 | | **uucp (8)** | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 99 | | **clock (9)** | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 100 | | **authpriv (10)** | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 101 | | **ftp (11)** | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 102 | | **ntp (12)** | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 103 | | **logaudit (13)** | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 104 | | **logalert (14)** | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 105 | | **cron (15)** | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 106 | | **local0 (16)** | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 107 | | **local1 (17)** | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 108 | | **local2 (18)** | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 109 | | **local3 (19)** | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 110 | | **local4 (20)** | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 111 | | **local5 (21)** | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 112 | | **local6 (22)** | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 113 | | **local7 (23)** | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 114 | 115 | #### I am seeing duplicate entries in syslog! 116 | 117 | This is likely because you are running `journald` which is also reading audit events. To disable it you need to disable the functionality in `journald`. 118 | 119 | ```sh 120 | sudo systemctl mask systemd-journald-audit.socket 121 | ``` 122 | 123 | ## Thanks! 124 | 125 | To Hardik Juneja, Arun Sori, Aalekh Nigam Aalekhn for the inspiration via https://github.com/mozilla/audit-go 126 | -------------------------------------------------------------------------------- /go-audit.yaml.example: -------------------------------------------------------------------------------- 1 | # Configure socket buffers, leave unset to use the system defaults 2 | # Values will be doubled by the kernel 3 | # It is recommended you do not set any of these values unless you really need to 4 | socket_buffer: 5 | # Default is net.core.rmem_default (/proc/sys/net/core/rmem_default) 6 | # Maximum max is net.core.rmem_max (/proc/sys/net/core/rmem_max) 7 | receive: 16384 8 | 9 | events: 10 | # Minimum event type to capture, default 1300 11 | min: 1300 12 | # Maximum event type to capture, default 1399 13 | max: 1399 14 | 15 | # Configure message sequence tracking 16 | message_tracking: 17 | # Track messages and identify if we missed any, default true 18 | enabled: true 19 | 20 | # Log out of orderness, these messages typically signify an overloading system, default false 21 | log_out_of_order: false 22 | 23 | # Maximum out of orderness before a missed sequence is presumed dropped, default 500 24 | max_out_of_order: 500 25 | 26 | # Configure where to output audit events 27 | # Only 1 output can be active at a given time 28 | output: 29 | # Writes to stdout 30 | # All program status logging will be moved to stderr 31 | stdout: 32 | enabled: true 33 | 34 | # Total number of attempts to write a line before considering giving up 35 | # If a write fails go-audit will sleep for 1 second before retrying 36 | # Default is 3 37 | attempts: 2 38 | 39 | # Writes logs to syslog 40 | syslog: 41 | enabled: false 42 | attempts: 5 43 | 44 | # Configure the type of socket this should be, default is unixgram 45 | # This maps to `network` in golangs net.Dial: https://golang.org/pkg/net/#Dial 46 | network: unixgram 47 | 48 | # Set the remote address to connect to, this can be a path or an ip address 49 | # This maps to `address` in golangs net.Dial: https://golang.org/pkg/net/#Dial 50 | address: /dev/log 51 | 52 | # Sets the facility and severity for all events. See the table below for help 53 | # The default is 132 which maps to local0 | warn 54 | priority: 129 # local0 | emerg 55 | 56 | # Typically the name of the program generating the message. The PID is of the process is appended for you: [1233] 57 | # Default value is "go-audit" 58 | tag: "audit-thing" 59 | 60 | # Appends logs to a file 61 | file: 62 | enabled: false 63 | attempts: 2 64 | 65 | # Path of the file to write lines to 66 | # The actual file will be created if it is missing but make sure the parent directory exists 67 | path: /var/log/go-audit/go-audit.log 68 | 69 | # Octal file mode for the log file, make sure to always have a leading 0 70 | mode: 0600 71 | 72 | # User and group that should own the log file 73 | user: root 74 | group: root 75 | 76 | # Writes logs to Graylog2 server using GELF standard: http://docs.graylog.org/en/stable/pages/gelf.html 77 | gelf: 78 | enabled: false 79 | attempts: 3 80 | 81 | # Configure the type of socket this should be, this can only be "udp" or "tcp". 82 | # Default value is "udp". 83 | network: udp 84 | 85 | # Set the remote address to connect to, this can be an IP address or a hostname and port. 86 | # This setting is mandatory and has no default value. 87 | address: localhost:12201 88 | 89 | # Defines the compression settings when using GELF over UDP network 90 | compression: 91 | # Sets the level of compression 92 | # This maps to `compress/flate` consts: https://godoc.org/compress/flate#pkg-constants 93 | # Default value is: 1, which means "BestSpeed" 94 | level: 1 95 | 96 | # Configure the compression type the writer should use when sending messages to server 97 | # This maps to `CompressionType` into gelf library: https://godoc.org/gopkg.in/Graylog2/go-gelf.v2/gelf#CompressType 98 | # Default values is: 0, which means "Gzip" 99 | type: 0 100 | 101 | # Configure logging, only stdout and stderr are used. 102 | log: 103 | # Gives you a bit of control over log line prefixes. Default is 0 - nothing. 104 | # To get the `filename:lineno` you would set this to 16 105 | # 106 | # Ldate = 1 // the date in the local time zone: 2009/01/23 107 | # Ltime = 2 // the time in the local time zone: 01:23:23 108 | # Lmicroseconds = 4 // microsecond resolution: 01:23:23.123123. assumes Ltime. 109 | # Llongfile = 8 // full file name and line number: /a/b/c/d.go:23 110 | # Lshortfile = 16 // final file name element and line number: d.go:23. overrides Llongfile 111 | # LUTC = 32 // if Ldate or Ltime is set, use UTC rather than the local time zone 112 | # 113 | # See also: https://golang.org/pkg/log/#pkg-constants 114 | flags: 0 115 | 116 | rules: 117 | # Watch all 64 bit program executions 118 | - -a exit,always -F arch=b64 -S execve 119 | # Watch all 32 bit program executions 120 | - -a exit,always -F arch=b32 -S execve 121 | # Enable kernel auditing (required if not done via the "audit" kernel boot parameter) 122 | # You can also use this to lock the rules. Locking requires a reboot to modify the ruleset. 123 | # This should be the last rule in the chain. 124 | - -e 1 125 | 126 | # If kaudit filtering isn't powerful enough you can use the following filter mechanism 127 | filters: 128 | # Each filter consists of exactly 3 parts 129 | - syscall: 49 # The syscall id of the message group (a single log line from go-audit), to test against the regex 130 | message_type: 1306 # The message type identifier containing the data to test against the regex 131 | regex: saddr=(10..|0A..) # The regex to test against the message specific message types data 132 | 133 | extras: 134 | # Fetch extra fields for containers: 135 | # - containers.id 136 | # - containers.image (requires docker) 137 | # - containers.name (from kubernetes, if docker enabled) 138 | # - containers.pod_uid (from kubernetes, if docker enabled) 139 | # - containers.pod_name (from kubernetes, if available) 140 | # - containers.pod_namespace (from kubernetes, if available) 141 | # 142 | # The values listed below are the defaults, you can specify only the ones 143 | # you need to change 144 | containers: 145 | enabled: false 146 | 147 | # if enabled, make requests to the local containerd daemon for extra container details 148 | containerd: false 149 | containerd_sock: /run/containerd/containerd.sock 150 | containerd_namespace: k8s.io 151 | 152 | # if enabled, make requests to the local docker daemon for extra container details 153 | docker: false 154 | docker_api_version: 1.24 155 | 156 | # number of pid -> container_id mappings to cache (0 means disable cache) 157 | pid_cache: 0 158 | # number of container_id -> docker_details to cache (0 means disable cache) 159 | docker_cache: 0 160 | # number of container_id -> containerd_details to cache (0 means disable cache) 161 | containerd_cache: 0 162 | -------------------------------------------------------------------------------- /extras_containers.go: -------------------------------------------------------------------------------- 1 | //go:build !nocontainers 2 | // +build !nocontainers 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/containerd/containerd" 10 | "github.com/containerd/containerd/containers" 11 | dockertypes "github.com/docker/docker/api/types" 12 | dockerclient "github.com/docker/docker/client" 13 | "github.com/golang/groupcache/lru" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | func init() { 18 | RegisterExtraParser(func(config *viper.Viper) (ExtraParser, error) { 19 | if config.GetBool("extras.containers.enabled") { 20 | cp, err := NewContainerParser(config.Sub("extras.containers")) 21 | if err == nil { 22 | l.Printf("ContainerParser enabled (docker=%v containerd=%v pid_cache=%d docker_cache=%d containerd_cache=%d)\n", 23 | cp.docker != nil, 24 | cp.containerd != nil, 25 | cacheSize(cp.pidCache), 26 | cacheSize(cp.dockerCache), 27 | cacheSize(cp.containerdCache), 28 | ) 29 | } 30 | return cp, err 31 | } 32 | return nil, nil 33 | }) 34 | } 35 | 36 | type ContainerParser struct { 37 | docker *dockerclient.Client 38 | containerd *containerd.Client 39 | 40 | // map[int]string 41 | // (pid -> containerID) 42 | pidCache Cache 43 | // map[string]dockertypes.ContainerJSON 44 | // (containerID -> dockerResponse) 45 | dockerCache Cache 46 | // map[string]*containers.Container 47 | // (containerID -> containerdResponse) 48 | containerdCache Cache 49 | } 50 | 51 | type Cache interface { 52 | Add(lru.Key, interface{}) 53 | Get(lru.Key) (interface{}, bool) 54 | } 55 | 56 | type NoCache struct{} 57 | 58 | func (NoCache) Add(lru.Key, interface{}) {} 59 | func (NoCache) Get(lru.Key) (interface{}, bool) { return nil, false } 60 | 61 | // NewCache returns an lru.Cache if size is >0, NoCache otherwise 62 | func NewCache(size int) Cache { 63 | if size > 0 { 64 | return lru.New(size) 65 | } 66 | return NoCache{} 67 | } 68 | 69 | func cacheSize(c Cache) int { 70 | switch x := c.(type) { 71 | case *lru.Cache: 72 | return x.MaxEntries 73 | } 74 | return 0 75 | } 76 | 77 | func NewContainerParser(config *viper.Viper) (*ContainerParser, error) { 78 | var docker *dockerclient.Client 79 | if config.GetBool("docker") { 80 | version := config.GetString("docker_api_version") 81 | if version == "" { 82 | // > Docker does not recommend running versions prior to 1.12, which 83 | // > means you are encouraged to use an API version of 1.24 or higher. 84 | // https://docs.docker.com/develop/sdk/#api-version-matrix 85 | version = "1.24" 86 | } 87 | var err error 88 | docker, err = dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithVersion(version)) 89 | if err != nil { 90 | return nil, err 91 | } 92 | } 93 | 94 | var containerdClient *containerd.Client 95 | if config.GetBool("containerd") { 96 | var opts []containerd.ClientOpt 97 | sockAddr := config.GetString("containerd_sock") 98 | if sockAddr == "" { 99 | sockAddr = "/run/containerd/containerd.sock" 100 | } 101 | namespace := config.GetString("containerd_namespace") 102 | if namespace != "" { 103 | opts = append(opts, containerd.WithDefaultNamespace(namespace)) 104 | } 105 | var err error 106 | containerdClient, err = containerd.New(sockAddr, opts...) 107 | if err != nil { 108 | return nil, err 109 | } 110 | } 111 | 112 | return &ContainerParser{ 113 | docker: docker, 114 | containerd: containerdClient, 115 | pidCache: NewCache(config.GetInt("pid_cache")), 116 | dockerCache: NewCache(config.GetInt("docker_cache")), 117 | containerdCache: NewCache(config.GetInt("containerd_cache")), 118 | }, nil 119 | } 120 | 121 | // Find `pid=` in a message and adds the container ids to the Extra object 122 | func (c ContainerParser) Parse(am *AuditMessage) { 123 | switch am.Type { 124 | case 1300, 1326: 125 | am.Containers = c.getContainersForPid(getPid(am.Data)) 126 | } 127 | } 128 | 129 | func (c ContainerParser) getContainersForPid(pid, ppid int) map[string]string { 130 | if pid == 0 { 131 | return nil 132 | } 133 | cid, err := c.getPidContainerID(pid) 134 | if err != nil { 135 | // pid might have exited before we could check it, try the ppid 136 | return c.getContainersForPid(ppid, 0) 137 | } 138 | 139 | if cid == "" { 140 | return nil 141 | } 142 | 143 | if c.docker != nil { 144 | container, err := c.getDockerContainer(cid) 145 | 146 | if err != nil { 147 | el.Printf("failed to query docker for container id: %s: %v\n", cid, err) 148 | } else { 149 | return map[string]string{ 150 | "id": cid, 151 | "image": container.Config.Image, 152 | "name": container.Config.Labels["io.kubernetes.container.name"], 153 | "pod_uid": container.Config.Labels["io.kubernetes.pod.uid"], 154 | "pod_name": container.Config.Labels["io.kubernetes.pod.name"], 155 | "pod_namespace": container.Config.Labels["io.kubernetes.pod.namespace"], 156 | } 157 | } 158 | } 159 | 160 | if c.containerd != nil { 161 | container, err := c.getContainerdContainer(cid) 162 | 163 | if err != nil { 164 | el.Printf("failed to query containerd for container id: %s: %v\n", cid, err) 165 | } else { 166 | if container.Labels != nil { 167 | return map[string]string{ 168 | "id": cid, 169 | "image": container.Image, 170 | "name": container.Labels["io.kubernetes.container.name"], 171 | "pod_uid": container.Labels["io.kubernetes.pod.uid"], 172 | "pod_name": container.Labels["io.kubernetes.pod.name"], 173 | "pod_namespace": container.Labels["io.kubernetes.pod.namespace"], 174 | } 175 | } else { 176 | return map[string]string{ 177 | "id": cid, 178 | "image": container.Image, 179 | } 180 | } 181 | } 182 | } 183 | 184 | return map[string]string{ 185 | "id": cid, 186 | } 187 | } 188 | 189 | func (c ContainerParser) getPidContainerID(pid int) (string, error) { 190 | if v, found := c.pidCache.Get(pid); found { 191 | return v.(string), nil 192 | } 193 | cid, err := processContainerID(pid) 194 | if err == nil { 195 | c.pidCache.Add(pid, cid) 196 | } 197 | return cid, err 198 | } 199 | 200 | func (c ContainerParser) getDockerContainer(containerID string) (dockertypes.ContainerJSON, error) { 201 | if v, found := c.dockerCache.Get(containerID); found { 202 | return v.(dockertypes.ContainerJSON), nil 203 | } 204 | 205 | container, err := c.docker.ContainerInspect(context.TODO(), containerID) 206 | if err == nil { 207 | c.dockerCache.Add(containerID, container) 208 | } 209 | return container, err 210 | } 211 | 212 | func (c ContainerParser) getContainerdContainer(containerID string) (*containers.Container, error) { 213 | if v, found := c.containerdCache.Get(containerID); found { 214 | return v.(*containers.Container), nil 215 | } 216 | 217 | container, err := c.containerd.LoadContainer(context.TODO(), containerID) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | info, err := container.Info(context.TODO()) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | c.containerdCache.Add(containerID, &info) 228 | 229 | return &info, nil 230 | } 231 | -------------------------------------------------------------------------------- /audit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/flate" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "log/syslog" 10 | "os" 11 | "os/exec" 12 | "os/signal" 13 | "os/user" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | "syscall" 18 | 19 | "github.com/spf13/viper" 20 | "gopkg.in/Graylog2/go-gelf.v2/gelf" 21 | ) 22 | 23 | // A version string that can be set with 24 | // 25 | // -ldflags "-X main.Build=SOMEVERSION" 26 | // 27 | // at compile-time. 28 | var Build string 29 | 30 | var l = log.New(os.Stdout, "", 0) 31 | var el = log.New(os.Stderr, "", 0) 32 | 33 | type executor func(string, ...string) error 34 | 35 | func lExec(s string, a ...string) error { 36 | return exec.Command(s, a...).Run() 37 | } 38 | 39 | func loadConfig(configFile string) (*viper.Viper, error) { 40 | config := viper.New() 41 | config.SetConfigFile(configFile) 42 | 43 | config.SetDefault("events.min", 1300) 44 | config.SetDefault("events.max", 1399) 45 | config.SetDefault("message_tracking.enabled", true) 46 | config.SetDefault("message_tracking.log_out_of_order", false) 47 | config.SetDefault("message_tracking.max_out_of_order", 500) 48 | config.SetDefault("output.syslog.enabled", false) 49 | config.SetDefault("output.syslog.priority", int(syslog.LOG_LOCAL0|syslog.LOG_WARNING)) 50 | config.SetDefault("output.syslog.tag", "go-audit") 51 | config.SetDefault("output.syslog.attempts", "3") 52 | config.SetDefault("output.gelf.attempts", 3) 53 | config.SetDefault("output.gelf.network", "udp") 54 | config.SetDefault("output.gelf.compression.level", int(flate.BestSpeed)) 55 | config.SetDefault("output.gelf.compression.type", int(gelf.CompressGzip)) 56 | config.SetDefault("log.flags", 0) 57 | 58 | if err := config.ReadInConfig(); err != nil { 59 | return nil, err 60 | } 61 | 62 | l.SetFlags(config.GetInt("log.flags")) 63 | el.SetFlags(config.GetInt("log.flags")) 64 | 65 | return config, nil 66 | } 67 | 68 | func setRules(config *viper.Viper, e executor) error { 69 | // Clear existing rules 70 | if err := e("auditctl", "-D"); err != nil { 71 | return fmt.Errorf("Failed to flush existing audit rules. Error: %s", err) 72 | } 73 | 74 | l.Println("Flushed existing audit rules") 75 | 76 | // Add ours in 77 | if rules := config.GetStringSlice("rules"); len(rules) != 0 { 78 | for i, v := range rules { 79 | // Skip rules with no content 80 | if v == "" { 81 | continue 82 | } 83 | 84 | if err := e("auditctl", strings.Fields(v)...); err != nil { 85 | return fmt.Errorf("Failed to add rule #%d. Error: %s", i+1, err) 86 | } 87 | 88 | l.Printf("Added audit rule #%d\n", i+1) 89 | } 90 | } else { 91 | return errors.New("No audit rules found") 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func createOutput(config *viper.Viper) (*AuditWriter, error) { 98 | var writer *AuditWriter 99 | var err error 100 | i := 0 101 | 102 | if config.GetBool("output.syslog.enabled") == true { 103 | i++ 104 | writer, err = createSyslogOutput(config) 105 | if err != nil { 106 | return nil, err 107 | } 108 | } 109 | 110 | if config.GetBool("output.file.enabled") == true { 111 | i++ 112 | writer, err = createFileOutput(config) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | go handleLogRotation(config, writer) 118 | } 119 | 120 | if config.GetBool("output.stdout.enabled") == true { 121 | i++ 122 | writer, err = createStdOutOutput(config) 123 | if err != nil { 124 | return nil, err 125 | } 126 | } 127 | 128 | if config.GetBool("output.gelf.enabled") == true { 129 | i++ 130 | writer, err = createGELFOutput(config) 131 | if err != nil { 132 | return nil, err 133 | } 134 | } 135 | 136 | if i > 1 { 137 | return nil, errors.New("Only one output can be enabled at a time") 138 | } 139 | 140 | if writer == nil { 141 | return nil, errors.New("No outputs were configured") 142 | } 143 | 144 | return writer, nil 145 | } 146 | 147 | func createGELFOutput(config *viper.Viper) (*AuditWriter, error) { 148 | attempts := config.GetInt("output.gelf.attempts") 149 | if attempts < 1 { 150 | return nil, fmt.Errorf("Output attempts for GELF must be at least 1, %v provided", attempts) 151 | } 152 | 153 | address := config.GetString("output.gelf.address") 154 | if address == "" { 155 | return nil, fmt.Errorf("Output address for GELF must be set") 156 | } 157 | 158 | switch config.GetString("output.gelf.network") { 159 | case "udp": 160 | writer, err := gelf.NewUDPWriter(address) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | writer.CompressionType = gelf.CompressType(config.GetInt("output.gelf.compression.type")) 166 | writer.CompressionLevel = config.GetInt("output.gelf.compression.level") 167 | 168 | return NewAuditWriter(writer, attempts), nil 169 | case "tcp": 170 | writer, err := gelf.NewTCPWriter(address) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | return NewAuditWriter(writer, attempts), nil 176 | default: 177 | return nil, fmt.Errorf("unsupported network by GELF library") 178 | } 179 | 180 | } 181 | 182 | func createSyslogOutput(config *viper.Viper) (*AuditWriter, error) { 183 | attempts := config.GetInt("output.syslog.attempts") 184 | if attempts < 1 { 185 | return nil, fmt.Errorf("Output attempts for syslog must be at least 1, %v provided", attempts) 186 | } 187 | 188 | syslogWriter, err := syslog.Dial( 189 | config.GetString("output.syslog.network"), 190 | config.GetString("output.syslog.address"), 191 | syslog.Priority(config.GetInt("output.syslog.priority")), 192 | config.GetString("output.syslog.tag"), 193 | ) 194 | 195 | if err != nil { 196 | return nil, fmt.Errorf("Failed to open syslog writer. Error: %v", err) 197 | } 198 | 199 | return NewAuditWriter(syslogWriter, attempts), nil 200 | } 201 | 202 | func createFileOutput(config *viper.Viper) (*AuditWriter, error) { 203 | attempts := config.GetInt("output.file.attempts") 204 | if attempts < 1 { 205 | return nil, fmt.Errorf("Output attempts for file must be at least 1, %v provided", attempts) 206 | } 207 | 208 | mode := os.FileMode(config.GetInt("output.file.mode")) 209 | if mode < 1 { 210 | return nil, errors.New("Output file mode should be greater than 0000") 211 | } 212 | 213 | f, err := os.OpenFile( 214 | config.GetString("output.file.path"), 215 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, mode, 216 | ) 217 | 218 | if err != nil { 219 | return nil, fmt.Errorf("Failed to open output file. Error: %s", err) 220 | } 221 | 222 | if err := f.Chmod(mode); err != nil { 223 | return nil, fmt.Errorf("Failed to set file permissions. Error: %s", err) 224 | } 225 | 226 | uname := config.GetString("output.file.user") 227 | u, err := user.Lookup(uname) 228 | if err != nil { 229 | return nil, fmt.Errorf("Could not find uid for user %s. Error: %s", uname, err) 230 | } 231 | 232 | gname := config.GetString("output.file.group") 233 | g, err := user.LookupGroup(gname) 234 | if err != nil { 235 | return nil, fmt.Errorf("Could not find gid for group %s. Error: %s", gname, err) 236 | } 237 | 238 | uid, err := strconv.ParseInt(u.Uid, 10, 32) 239 | if err != nil { 240 | return nil, fmt.Errorf("Found uid could not be parsed. Error: %s", err) 241 | } 242 | 243 | gid, err := strconv.ParseInt(g.Gid, 10, 32) 244 | if err != nil { 245 | return nil, fmt.Errorf("Found gid could not be parsed. Error: %s", err) 246 | } 247 | 248 | if err = f.Chown(int(uid), int(gid)); err != nil { 249 | return nil, fmt.Errorf("Could not chown output file. Error: %s", err) 250 | } 251 | 252 | return NewAuditWriter(f, attempts), nil 253 | } 254 | 255 | func handleLogRotation(config *viper.Viper, writer *AuditWriter) { 256 | // Re-open our log file. This is triggered by a USR1 signal and is meant to be used upon log rotation 257 | 258 | sigc := make(chan os.Signal, 1) 259 | signal.Notify(sigc, syscall.SIGUSR1) 260 | 261 | for range sigc { 262 | newWriter, err := createFileOutput(config) 263 | if err != nil { 264 | el.Fatalln("Error re-opening log file. Exiting.") 265 | } 266 | 267 | oldFile := writer.w.(*os.File) 268 | writer.w = newWriter.w 269 | writer.e = newWriter.e 270 | 271 | err = oldFile.Close() 272 | if err != nil { 273 | el.Printf("Error closing old log file: %+v\n", err) 274 | } 275 | } 276 | } 277 | 278 | func createStdOutOutput(config *viper.Viper) (*AuditWriter, error) { 279 | attempts := config.GetInt("output.stdout.attempts") 280 | if attempts < 1 { 281 | return nil, fmt.Errorf("Output attempts for stdout must be at least 1, %v provided", attempts) 282 | } 283 | 284 | // l logger is no longer stdout 285 | l.SetOutput(os.Stderr) 286 | 287 | return NewAuditWriter(os.Stdout, attempts), nil 288 | } 289 | 290 | func createFilters(config *viper.Viper) ([]AuditFilter, error) { 291 | var err error 292 | var ok bool 293 | 294 | fs := config.Get("filters") 295 | filters := []AuditFilter{} 296 | 297 | if fs == nil { 298 | return filters, nil 299 | } 300 | 301 | ft, ok := fs.([]interface{}) 302 | if !ok { 303 | return filters, fmt.Errorf("Could not parse filters object") 304 | } 305 | 306 | for i, f := range ft { 307 | f2, ok := f.(map[string]interface{}) 308 | if !ok { 309 | return filters, fmt.Errorf("Could not parse filter %d; '%+v'", i+1, f) 310 | } 311 | 312 | af := AuditFilter{} 313 | for k, v := range f2 { 314 | switch k { 315 | case "message_type": 316 | if ev, ok := v.(string); ok { 317 | fv, err := strconv.ParseUint(ev, 10, 64) 318 | if err != nil { 319 | return filters, fmt.Errorf("`message_type` in filter %d could not be parsed; Value: `%+v`; Error: %s", i+1, v, err) 320 | } 321 | af.messageType = uint16(fv) 322 | 323 | } else if ev, ok := v.(int); ok { 324 | af.messageType = uint16(ev) 325 | 326 | } else { 327 | return filters, fmt.Errorf("`message_type` in filter %d could not be parsed; Value: `%+v`", i+1, v) 328 | } 329 | 330 | case "regex": 331 | re, ok := v.(string) 332 | if !ok { 333 | return filters, fmt.Errorf("`regex` in filter %d could not be parsed; Value: `%+v`", i+1, v) 334 | } 335 | 336 | if af.regex, err = regexp.Compile(re); err != nil { 337 | return filters, fmt.Errorf("`regex` in filter %d could not be parsed; Value: `%+v`; Error: %s", i+1, v, err) 338 | } 339 | 340 | case "syscall": 341 | if af.syscall, ok = v.(string); ok { 342 | // All is good 343 | } else if ev, ok := v.(int); ok { 344 | af.syscall = strconv.Itoa(ev) 345 | } else { 346 | return filters, fmt.Errorf("`syscall` in filter %d could not be parsed; Value: `%+v`", i+1, v) 347 | } 348 | } 349 | } 350 | 351 | if af.regex == nil { 352 | return filters, fmt.Errorf("Filter %d is missing the `regex` entry", i+1) 353 | } 354 | 355 | if af.messageType == 0 { 356 | return filters, fmt.Errorf("Filter %d is missing the `message_type` entry", i+1) 357 | } 358 | 359 | filters = append(filters, af) 360 | l.Printf("Ignoring syscall `%v` containing message type `%v` matching string `%s`\n", af.syscall, af.messageType, af.regex.String()) 361 | } 362 | 363 | return filters, nil 364 | } 365 | 366 | func main() { 367 | configFile := flag.String("config", "", "Config file location") 368 | printVersion := flag.Bool("version", false, "Print version") 369 | 370 | flag.Parse() 371 | 372 | if *printVersion { 373 | fmt.Printf("Version: %s\n", Build) 374 | os.Exit(0) 375 | } 376 | 377 | if *configFile == "" { 378 | el.Println("A config file must be provided") 379 | flag.Usage() 380 | os.Exit(1) 381 | } 382 | 383 | config, err := loadConfig(*configFile) 384 | if err != nil { 385 | el.Fatal(err) 386 | } 387 | 388 | // output needs to be created before anything that write to stdout 389 | writer, err := createOutput(config) 390 | if err != nil { 391 | el.Fatal(err) 392 | } 393 | 394 | if err := setRules(config, lExec); err != nil { 395 | el.Fatal(err) 396 | } 397 | 398 | filters, err := createFilters(config) 399 | if err != nil { 400 | el.Fatal(err) 401 | } 402 | 403 | nlClient, err := NewNetlinkClient(config.GetInt("socket_buffer.receive")) 404 | if err != nil { 405 | el.Fatal(err) 406 | } 407 | 408 | marshaller := NewAuditMarshaller( 409 | writer, 410 | uint16(config.GetInt("events.min")), 411 | uint16(config.GetInt("events.max")), 412 | config.GetBool("message_tracking.enabled"), 413 | config.GetBool("message_tracking.log_out_of_order"), 414 | config.GetInt("message_tracking.max_out_of_order"), 415 | filters, 416 | createExtraParsers(config), 417 | ) 418 | 419 | l.Printf("Started processing events in the range [%d, %d]\n", config.GetInt("events.min"), config.GetInt("events.max")) 420 | 421 | //Main loop. Get data from netlink and send it to the json lib for processing 422 | for { 423 | msg, err := nlClient.Receive() 424 | if err != nil { 425 | el.Printf("Error during message receive: %+v\n", err) 426 | continue 427 | } 428 | 429 | if msg == nil { 430 | continue 431 | } 432 | 433 | marshaller.Consume(msg) 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /audit_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/flate" 5 | "errors" 6 | "log/syslog" 7 | "net" 8 | "os" 9 | "os/user" 10 | "path" 11 | "strconv" 12 | "syscall" 13 | "testing" 14 | "time" 15 | 16 | "github.com/spf13/viper" 17 | "github.com/stretchr/testify/assert" 18 | "gopkg.in/Graylog2/go-gelf.v2/gelf" 19 | ) 20 | 21 | func Test_loadConfig(t *testing.T) { 22 | file := createTempFile(t, "defaultValues.test.yaml", "") 23 | defer os.Remove(file) 24 | 25 | // defaults 26 | config, err := loadConfig(file) 27 | assert.Equal(t, 1300, config.GetInt("events.min"), "events.min should default to 1300") 28 | assert.Equal(t, 1399, config.GetInt("events.max"), "events.max should default to 1399") 29 | assert.Equal(t, true, config.GetBool("message_tracking.enabled"), "message_tracking.enabled should default to true") 30 | assert.Equal(t, false, config.GetBool("message_tracking.log_out_of_order"), "message_tracking.log_out_of_order should default to false") 31 | assert.Equal(t, 500, config.GetInt("message_tracking.max_out_of_order"), "message_tracking.max_out_of_order should default to 500") 32 | assert.Equal(t, false, config.GetBool("output.syslog.enabled"), "output.syslog.enabled should default to false") 33 | assert.Equal(t, 132, config.GetInt("output.syslog.priority"), "output.syslog.priority should default to 132") 34 | assert.Equal(t, "go-audit", config.GetString("output.syslog.tag"), "output.syslog.tag should default to go-audit") 35 | assert.Equal(t, 3, config.GetInt("output.syslog.attempts"), "output.syslog.attempts should default to 3") 36 | assert.Equal(t, false, config.GetBool("output.gelf.enabled"), "output.gelf.enabled should default to false") 37 | assert.Equal(t, 3, config.GetInt("output.gelf.attempts"), "output.gelf.attempts should default to 3") 38 | assert.Equal(t, "udp", config.GetString("output.gelf.network"), "output.gelf.network should default to udp") 39 | assert.Equal(t, int(flate.BestSpeed), config.GetInt("output.gelf.compression.level"), "output.gelf.compression.level should default to flate.BestSpeed") 40 | assert.Equal(t, int(gelf.CompressGzip), config.GetInt("output.gelf.compression.type"), "output.gelf.compression.type should default to gelf.CompressGzip") 41 | assert.Equal(t, 0, config.GetInt("log.flags"), "log.flags should default to 0") 42 | assert.Equal(t, 0, l.Flags(), "stdout log flags was wrong") 43 | assert.Equal(t, 0, el.Flags(), "stderr log flags was wrong") 44 | assert.Nil(t, err) 45 | 46 | // parse error 47 | file = createTempFile(t, "defaultValues.test.yaml", "this is bad") 48 | config, err = loadConfig(file) 49 | assert.EqualError(t, err, "While parsing config: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `this is...` into map[string]interface {}") 50 | assert.Nil(t, config) 51 | } 52 | 53 | func Test_setRules(t *testing.T) { 54 | defer resetLogger() 55 | 56 | // fail to flush rules 57 | config := viper.New() 58 | 59 | err := setRules(config, func(s string, a ...string) error { 60 | if s == "auditctl" && a[0] == "-D" { 61 | return errors.New("testing") 62 | } 63 | 64 | return nil 65 | }) 66 | 67 | assert.EqualError(t, err, "Failed to flush existing audit rules. Error: testing") 68 | 69 | // fail on 0 rules 70 | err = setRules(config, func(s string, a ...string) error { return nil }) 71 | assert.EqualError(t, err, "No audit rules found") 72 | 73 | // failure to set rule 74 | r := 0 75 | config.Set("rules", []string{"-a -1 -2", "", "-a -3 -4"}) 76 | err = setRules(config, func(s string, a ...string) error { 77 | if a[0] != "-D" { 78 | return errors.New("testing rule") 79 | } 80 | 81 | r++ 82 | 83 | return nil 84 | }) 85 | 86 | assert.Equal(t, 1, r, "Wrong number of rule set attempts") 87 | assert.EqualError(t, err, "Failed to add rule #1. Error: testing rule") 88 | 89 | // properly set rules 90 | r = 0 91 | err = setRules(config, func(s string, a ...string) error { 92 | // Skip the flush rules 93 | if a[0] != "-a" { 94 | return nil 95 | } 96 | 97 | if (a[1] == "-1" && a[2] == "-2") || (a[1] == "-3" && a[2] == "-4") { 98 | r++ 99 | } 100 | 101 | return nil 102 | }) 103 | 104 | assert.Equal(t, 2, r, "Wrong number of correct rule set attempts") 105 | assert.Nil(t, err) 106 | } 107 | 108 | func Test_createFileOutput(t *testing.T) { 109 | // attempts error 110 | c := viper.New() 111 | c.Set("output.file.attempts", 0) 112 | w, err := createFileOutput(c) 113 | assert.EqualError(t, err, "Output attempts for file must be at least 1, 0 provided") 114 | assert.Nil(t, w) 115 | 116 | // failure to create/open file 117 | c = viper.New() 118 | c.Set("output.file.attempts", 1) 119 | c.Set("output.file.path", "/do/not/exist/please") 120 | c.Set("output.file.mode", 0644) 121 | w, err = createFileOutput(c) 122 | assert.EqualError(t, err, "Failed to open output file. Error: open /do/not/exist/please: no such file or directory") 123 | assert.Nil(t, w) 124 | 125 | // chmod error 126 | c = viper.New() 127 | c.Set("output.file.attempts", 1) 128 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 129 | w, err = createFileOutput(c) 130 | assert.EqualError(t, err, "Output file mode should be greater than 0000") 131 | assert.Nil(t, w) 132 | 133 | // uid error 134 | c = viper.New() 135 | c.Set("output.file.attempts", 1) 136 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 137 | c.Set("output.file.mode", 0644) 138 | w, err = createFileOutput(c) 139 | assert.EqualError(t, err, "Could not find uid for user . Error: user: unknown user ") 140 | assert.Nil(t, w) 141 | 142 | uid := os.Getuid() 143 | gid := os.Getgid() 144 | u, _ := user.LookupId(strconv.Itoa(uid)) 145 | g, _ := user.LookupGroupId(strconv.Itoa(gid)) 146 | 147 | // travis-ci is silly 148 | if u.Username == "" { 149 | u.Username = g.Name 150 | } 151 | 152 | // gid error 153 | c = viper.New() 154 | c.Set("output.file.attempts", 1) 155 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 156 | c.Set("output.file.mode", 0644) 157 | c.Set("output.file.user", u.Username) 158 | w, err = createFileOutput(c) 159 | assert.EqualError(t, err, "Could not find gid for group . Error: group: unknown group ") 160 | assert.Nil(t, w) 161 | 162 | // chown error 163 | c = viper.New() 164 | c.Set("output.file.attempts", 1) 165 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 166 | c.Set("output.file.mode", 0644) 167 | c.Set("output.file.user", "root") 168 | c.Set("output.file.group", "root") 169 | w, err = createFileOutput(c) 170 | assert.EqualError(t, err, "Could not chown output file. Error: chown /tmp/go-audit.test.log: operation not permitted") 171 | assert.Nil(t, w) 172 | 173 | // All good 174 | c = viper.New() 175 | c.Set("output.file.attempts", 1) 176 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 177 | c.Set("output.file.mode", 0644) 178 | c.Set("output.file.user", u.Username) 179 | c.Set("output.file.group", g.Name) 180 | w, err = createFileOutput(c) 181 | assert.Nil(t, err) 182 | assert.NotNil(t, w) 183 | assert.IsType(t, &os.File{}, w.w) 184 | } 185 | 186 | func Test_createSyslogOutput(t *testing.T) { 187 | // attempts error 188 | c := viper.New() 189 | c.Set("output.syslog.attempts", 0) 190 | w, err := createSyslogOutput(c) 191 | assert.EqualError(t, err, "Output attempts for syslog must be at least 1, 0 provided") 192 | assert.Nil(t, w) 193 | 194 | // dial error 195 | c = viper.New() 196 | c.Set("output.syslog.attempts", 1) 197 | c.Set("output.syslog.priority", -1) 198 | w, err = createSyslogOutput(c) 199 | assert.EqualError(t, err, "Failed to open syslog writer. Error: log/syslog: invalid priority") 200 | assert.Nil(t, w) 201 | 202 | // All good 203 | l, err := net.Listen("tcp", ":0") 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | defer l.Close() 209 | 210 | c = viper.New() 211 | c.Set("output.syslog.attempts", 1) 212 | c.Set("output.syslog.network", "tcp") 213 | c.Set("output.syslog.address", l.Addr().String()) 214 | w, err = createSyslogOutput(c) 215 | assert.Nil(t, err) 216 | assert.NotNil(t, w) 217 | assert.IsType(t, &syslog.Writer{}, w.w) 218 | } 219 | 220 | func Test_createStdOutOutput(t *testing.T) { 221 | // attempts error 222 | c := viper.New() 223 | c.Set("output.stdout.attempts", 0) 224 | w, err := createStdOutOutput(c) 225 | assert.EqualError(t, err, "Output attempts for stdout must be at least 1, 0 provided") 226 | assert.Nil(t, w) 227 | 228 | // All good 229 | c = viper.New() 230 | c.Set("output.stdout.attempts", 1) 231 | w, err = createStdOutOutput(c) 232 | assert.Nil(t, err) 233 | assert.NotNil(t, w) 234 | assert.IsType(t, &os.File{}, w.w) 235 | } 236 | 237 | func Test_createGELFOutput(t *testing.T) { 238 | t.Run("When attempts is less than one, should retun an expected error", func(t *testing.T) { 239 | c := viper.New() 240 | c.Set("output.gelf.attempts", 0) 241 | w, err := createGELFOutput(c) 242 | assert.EqualError(t, err, "Output attempts for GELF must be at least 1, 0 provided") 243 | assert.Nil(t, w) 244 | }) 245 | 246 | t.Run("When address is not set, should return an expected error", func(t *testing.T) { 247 | c := viper.New() 248 | c.Set("output.gelf.attempts", 3) 249 | w, err := createGELFOutput(c) 250 | assert.EqualError(t, err, "Output address for GELF must be set") 251 | assert.Nil(t, w) 252 | }) 253 | 254 | t.Run("When using UDP network, should return a gelf.UDPWriter writer", func(t *testing.T) { 255 | l, err := net.ListenUDP("udp", &net.UDPAddr{}) 256 | if err != nil { 257 | t.Fatal(err) 258 | } 259 | 260 | defer l.Close() 261 | 262 | c := viper.New() 263 | c.Set("output.gelf.attempts", 3) 264 | c.Set("output.gelf.network", "udp") 265 | c.Set("output.gelf.address", l.LocalAddr().String()) 266 | writer, err := createGELFOutput(c) 267 | assert.Nil(t, err) 268 | assert.IsType(t, &gelf.UDPWriter{}, writer.w) 269 | }) 270 | 271 | t.Run("When using TCP network, should return a gelf.TCPWriter writer", func(t *testing.T) { 272 | l, err := net.Listen("tcp", ":0") 273 | if err != nil { 274 | t.Fatal(err) 275 | } 276 | 277 | defer l.Close() 278 | 279 | c := viper.New() 280 | c.Set("output.gelf.attempts", 3) 281 | c.Set("output.gelf.network", "tcp") 282 | c.Set("output.gelf.address", l.Addr().String()) 283 | writer, err := createGELFOutput(c) 284 | assert.Nil(t, err) 285 | assert.Equal(t, writer.attempts, 3) 286 | assert.IsType(t, &gelf.TCPWriter{}, writer.w) 287 | }) 288 | 289 | t.Run("When using an unsupported network (not UDP or TCP), should return an expected error", func(t *testing.T) { 290 | c := viper.New() 291 | c.Set("output.gelf.attempts", 3) 292 | c.Set("output.gelf.address", "/var/run/gelf.sock") 293 | 294 | unsupportedNetworks := []string{"unix", "unixgram", "unixpacket"} 295 | 296 | for _, network := range unsupportedNetworks { 297 | c.Set("output.gelf.network", network) 298 | _, err := createGELFOutput(c) 299 | assert.EqualError(t, err, "unsupported network by GELF library") 300 | } 301 | }) 302 | 303 | t.Run("When using a custom compreession settings, should return a gelf.UPDWriter with expected compression value", func(t *testing.T) { 304 | c := viper.New() 305 | c.Set("output.gelf.attempts", 3) 306 | c.Set("output.gelf.network", "udp") 307 | c.Set("output.gelf.address", "localhost:12201") 308 | c.Set("output.gelf.compression.level", int(flate.BestCompression)) 309 | c.Set("output.gelf.compression.type", int(gelf.CompressZlib)) 310 | 311 | w, err := createGELFOutput(c) 312 | assert.Nil(t, err) 313 | 314 | udpWriter, ok := w.w.(*gelf.UDPWriter) 315 | assert.True(t, ok) 316 | assert.Equal(t, udpWriter.CompressionLevel, flate.BestCompression) 317 | assert.Equal(t, udpWriter.CompressionType, gelf.CompressZlib) 318 | }) 319 | } 320 | 321 | func Test_createOutput(t *testing.T) { 322 | // no outputs 323 | c := viper.New() 324 | w, err := createOutput(c) 325 | assert.EqualError(t, err, "No outputs were configured") 326 | assert.Nil(t, w) 327 | 328 | // multiple outputs 329 | uid := os.Getuid() 330 | gid := os.Getgid() 331 | u, _ := user.LookupId(strconv.Itoa(uid)) 332 | g, _ := user.LookupGroupId(strconv.Itoa(gid)) 333 | 334 | // travis-ci is silly 335 | if u.Username == "" { 336 | u.Username = g.Name 337 | } 338 | 339 | l, err := net.Listen("tcp", ":0") 340 | if err != nil { 341 | t.Fatal(err) 342 | } 343 | 344 | defer l.Close() 345 | 346 | c = viper.New() 347 | c.Set("output.syslog.enabled", true) 348 | c.Set("output.syslog.attempts", 1) 349 | c.Set("output.syslog.network", "tcp") 350 | c.Set("output.syslog.address", l.Addr().String()) 351 | 352 | c.Set("output.file.enabled", true) 353 | c.Set("output.file.attempts", 1) 354 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 355 | c.Set("output.file.mode", 0644) 356 | c.Set("output.file.user", u.Username) 357 | c.Set("output.file.group", g.Name) 358 | 359 | w, err = createOutput(c) 360 | assert.EqualError(t, err, "Only one output can be enabled at a time") 361 | assert.Nil(t, w) 362 | 363 | // syslog error 364 | c = viper.New() 365 | c.Set("output.syslog.enabled", true) 366 | c.Set("output.syslog.attempts", 0) 367 | w, err = createOutput(c) 368 | assert.EqualError(t, err, "Output attempts for syslog must be at least 1, 0 provided") 369 | assert.Nil(t, w) 370 | 371 | // file error 372 | c = viper.New() 373 | c.Set("output.file.enabled", true) 374 | c.Set("output.file.attempts", 0) 375 | w, err = createOutput(c) 376 | assert.EqualError(t, err, "Output attempts for file must be at least 1, 0 provided") 377 | assert.Nil(t, w) 378 | 379 | // stdout error 380 | c = viper.New() 381 | c.Set("output.stdout.enabled", true) 382 | c.Set("output.stdout.attempts", 0) 383 | w, err = createOutput(c) 384 | assert.EqualError(t, err, "Output attempts for stdout must be at least 1, 0 provided") 385 | assert.Nil(t, w) 386 | 387 | // All good syslog 388 | c = viper.New() 389 | c.Set("output.syslog.attempts", 1) 390 | c.Set("output.syslog.network", "tcp") 391 | c.Set("output.syslog.address", l.Addr().String()) 392 | w, err = createSyslogOutput(c) 393 | assert.Nil(t, err) 394 | assert.NotNil(t, w) 395 | assert.IsType(t, &syslog.Writer{}, w.w) 396 | 397 | // All good file 398 | c = viper.New() 399 | c.Set("output.file.enabled", true) 400 | c.Set("output.file.attempts", 1) 401 | c.Set("output.file.path", path.Join(os.TempDir(), "go-audit.test.log")) 402 | c.Set("output.file.mode", 0644) 403 | c.Set("output.file.user", u.Username) 404 | c.Set("output.file.group", g.Name) 405 | w, err = createOutput(c) 406 | assert.Nil(t, err) 407 | assert.NotNil(t, w) 408 | assert.IsType(t, &AuditWriter{}, w) 409 | assert.IsType(t, &os.File{}, w.w) 410 | 411 | // File rotation 412 | os.Rename(path.Join(os.TempDir(), "go-audit.test.log"), path.Join(os.TempDir(), "go-audit.test.log.rotated")) 413 | _, err = os.Stat(path.Join(os.TempDir(), "go-audit.test.log")) 414 | assert.True(t, os.IsNotExist(err)) 415 | syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) 416 | time.Sleep(100 * time.Millisecond) 417 | _, err = os.Stat(path.Join(os.TempDir(), "go-audit.test.log")) 418 | assert.Nil(t, err) 419 | } 420 | 421 | func Test_createFilters(t *testing.T) { 422 | lb, elb := hookLogger() 423 | defer resetLogger() 424 | 425 | // no filters 426 | c := viper.New() 427 | f, err := createFilters(c) 428 | assert.Nil(t, err) 429 | assert.Empty(t, f) 430 | 431 | // Bad outer filter value 432 | c = viper.New() 433 | c.Set("filters", 1) 434 | f, err = createFilters(c) 435 | assert.EqualError(t, err, "Could not parse filters object") 436 | assert.Empty(t, f) 437 | 438 | // Bad inner filter value 439 | c = viper.New() 440 | rf := make([]interface{}, 0) 441 | rf = append(rf, "bad filter definition") 442 | c.Set("filters", rf) 443 | f, err = createFilters(c) 444 | assert.EqualError(t, err, "Could not parse filter 1; 'bad filter definition'") 445 | assert.Empty(t, f) 446 | 447 | // Bad message type - string 448 | c = viper.New() 449 | rf = make([]interface{}, 0) 450 | rf = append(rf, map[string]interface{}{"message_type": "bad message type"}) 451 | c.Set("filters", rf) 452 | f, err = createFilters(c) 453 | assert.EqualError(t, err, "`message_type` in filter 1 could not be parsed; Value: `bad message type`; Error: strconv.ParseUint: parsing \"bad message type\": invalid syntax") 454 | assert.Empty(t, f) 455 | 456 | // Bad message type - unknown 457 | c = viper.New() 458 | rf = make([]interface{}, 0) 459 | rf = append(rf, map[string]interface{}{"message_type": false}) 460 | c.Set("filters", rf) 461 | f, err = createFilters(c) 462 | assert.EqualError(t, err, "`message_type` in filter 1 could not be parsed; Value: `false`") 463 | assert.Empty(t, f) 464 | 465 | // Bad regex - not string 466 | c = viper.New() 467 | rf = make([]interface{}, 0) 468 | rf = append(rf, map[string]interface{}{"regex": false}) 469 | c.Set("filters", rf) 470 | f, err = createFilters(c) 471 | assert.EqualError(t, err, "`regex` in filter 1 could not be parsed; Value: `false`") 472 | assert.Empty(t, f) 473 | 474 | // Bad regex - un-parse-able 475 | c = viper.New() 476 | rf = make([]interface{}, 0) 477 | rf = append(rf, map[string]interface{}{"regex": "["}) 478 | c.Set("filters", rf) 479 | f, err = createFilters(c) 480 | assert.EqualError(t, err, "`regex` in filter 1 could not be parsed; Value: `[`; Error: error parsing regexp: missing closing ]: `[`") 481 | assert.Empty(t, f) 482 | 483 | // Bad syscall - not string or int 484 | c = viper.New() 485 | rf = make([]interface{}, 0) 486 | rf = append(rf, map[string]interface{}{"syscall": []string{}}) 487 | c.Set("filters", rf) 488 | f, err = createFilters(c) 489 | assert.EqualError(t, err, "`syscall` in filter 1 could not be parsed; Value: `[]`") 490 | assert.Empty(t, f) 491 | 492 | // Missing regex 493 | c = viper.New() 494 | rf = make([]interface{}, 0) 495 | rf = append(rf, map[string]interface{}{"syscall": "1", "message_type": "1"}) 496 | c.Set("filters", rf) 497 | f, err = createFilters(c) 498 | assert.EqualError(t, err, "Filter 1 is missing the `regex` entry") 499 | assert.Empty(t, f) 500 | 501 | // Missing message_type 502 | c = viper.New() 503 | rf = make([]interface{}, 0) 504 | rf = append(rf, map[string]interface{}{"syscall": "1", "regex": "1"}) 505 | c.Set("filters", rf) 506 | f, err = createFilters(c) 507 | assert.EqualError(t, err, "Filter 1 is missing the `message_type` entry") 508 | assert.Empty(t, f) 509 | 510 | // Good with strings 511 | c = viper.New() 512 | rf = make([]interface{}, 0) 513 | rf = append(rf, map[string]interface{}{"message_type": "1", "regex": "1", "syscall": "1"}) 514 | c.Set("filters", rf) 515 | f, err = createFilters(c) 516 | assert.Nil(t, err) 517 | assert.NotEmpty(t, f) 518 | assert.Equal(t, "1", f[0].syscall) 519 | assert.Equal(t, uint16(1), f[0].messageType) 520 | assert.Equal(t, "1", f[0].regex.String()) 521 | assert.Empty(t, elb.String()) 522 | assert.Equal(t, "Ignoring syscall `1` containing message type `1` matching string `1`\n", lb.String()) 523 | 524 | // Good with ints 525 | lb.Reset() 526 | elb.Reset() 527 | c = viper.New() 528 | rf = make([]interface{}, 0) 529 | rf = append(rf, map[string]interface{}{"message_type": 1, "regex": "1", "syscall": 1}) 530 | c.Set("filters", rf) 531 | f, err = createFilters(c) 532 | assert.Nil(t, err) 533 | assert.NotEmpty(t, f) 534 | assert.Equal(t, "1", f[0].syscall) 535 | assert.Equal(t, uint16(1), f[0].messageType) 536 | assert.Equal(t, "1", f[0].regex.String()) 537 | assert.Empty(t, elb.String()) 538 | assert.Equal(t, "Ignoring syscall `1` containing message type `1` matching string `1`\n", lb.String()) 539 | } 540 | 541 | func Benchmark_MultiPacketMessage(b *testing.B) { 542 | marshaller := NewAuditMarshaller(NewAuditWriter(&noopWriter{}, 1), uint16(1300), uint16(1399), false, false, 1, []AuditFilter{}, nil) 543 | 544 | data := make([][]byte, 6) 545 | 546 | //&{1300,,arch=c000003e,syscall=59,success=yes,exit=0,a0=cc4e68,a1=d10bc8,a2=c69808,a3=7fff2a700900,items=2,ppid=11552,pid=11623,auid=1000,uid=1000,gid=1000,euid=1000,suid=1000,fsuid=1000,egid=1000,sgid=1000,fsgid=1000,tty=pts0,ses=35,comm="ls",exe="/bin/ls",key=(null),1222763,1459376866.885} 547 | data[0] = []byte{34, 1, 0, 0, 20, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 117, 100, 105, 116, 40, 49, 52, 53, 57, 51, 55, 54, 56, 54, 54, 46, 56, 56, 53, 58, 49, 50, 50, 50, 55, 54, 51, 41, 58, 32, 97, 114, 99, 104, 61, 99, 48, 48, 48, 48, 48, 51, 101, 32, 115, 121, 115, 99, 97, 108, 108, 61, 53, 57, 32, 115, 117, 99, 99, 101, 115, 115, 61, 121, 101, 115, 32, 101, 120, 105, 116, 61, 48, 32, 97, 48, 61, 99, 99, 52, 101, 54, 56, 32, 97, 49, 61, 100, 49, 48, 98, 99, 56, 32, 97, 50, 61, 99, 54, 57, 56, 48, 56, 32, 97, 51, 61, 55, 102, 102, 102, 50, 97, 55, 48, 48, 57, 48, 48, 32, 105, 116, 101, 109, 115, 61, 50, 32, 112, 112, 105, 100, 61, 49, 49, 53, 53, 50, 32, 112, 105, 100, 61, 49, 49, 54, 50, 51, 32, 97, 117, 105, 100, 61, 49, 48, 48, 48, 32, 117, 105, 100, 61, 49, 48, 48, 48, 32, 103, 105, 100, 61, 49, 48, 48, 48, 32, 101, 117, 105, 100, 61, 49, 48, 48, 48, 32, 115, 117, 105, 100, 61, 49, 48, 48, 48, 32, 102, 115, 117, 105, 100, 61, 49, 48, 48, 48, 32, 101, 103, 105, 100, 61, 49, 48, 48, 48, 32, 115, 103, 105, 100, 61, 49, 48, 48, 48, 32, 102, 115, 103, 105, 100, 61, 49, 48, 48, 48, 32, 116, 116, 121, 61, 112, 116, 115, 48, 32, 115, 101, 115, 61, 51, 53, 32, 99, 111, 109, 109, 61, 34, 108, 115, 34, 32, 101, 120, 101, 61, 34, 47, 98, 105, 110, 47, 108, 115, 34, 32, 107, 101, 121, 61, 40, 110, 117, 108, 108, 41} 548 | 549 | //&{1309,,argc=3,a0="ls",a1="--color=auto",a2="-alF",1222763,1459376866.885} 550 | data[1] = []byte{73, 0, 0, 0, 29, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 117, 100, 105, 116, 40, 49, 52, 53, 57, 51, 55, 54, 56, 54, 54, 46, 56, 56, 53, 58, 49, 50, 50, 50, 55, 54, 51, 41, 58, 32, 97, 114, 103, 99, 61, 51, 32, 97, 48, 61, 34, 108, 115, 34, 32, 97, 49, 61, 34, 45, 45, 99, 111, 108, 111, 114, 61, 97, 117, 116, 111, 34, 32, 97, 50, 61, 34, 45, 97, 108, 70, 34} 551 | 552 | //&{1307,,,cwd="/home/ubuntu/src/slack-github.com/rhuber/go-audit-new",1222763,1459376866.885} 553 | data[2] = []byte{91, 0, 0, 0, 27, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 117, 100, 105, 116, 40, 49, 52, 53, 57, 51, 55, 54, 56, 54, 54, 46, 56, 56, 53, 58, 49, 50, 50, 50, 55, 54, 51, 41, 58, 32, 32, 99, 119, 100, 61, 34, 47, 104, 111, 109, 101, 47, 117, 98, 117, 110, 116, 117, 47, 115, 114, 99, 47, 115, 108, 97, 99, 107, 45, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109, 47, 114, 104, 117, 98, 101, 114, 47, 103, 111, 45, 97, 117, 100, 105, 116, 45, 110, 101, 119, 34} 554 | 555 | //&{1302,,item=0,name="/bin/ls",inode=262316,dev=ca:01,mode=0100755,ouid=0,ogid=0,rdev=00:00,nametype=NORMAL,1222763,1459376866.885} 556 | data[3] = []byte{129, 0, 0, 0, 22, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 117, 100, 105, 116, 40, 49, 52, 53, 57, 51, 55, 54, 56, 54, 54, 46, 56, 56, 53, 58, 49, 50, 50, 50, 55, 54, 51, 41, 58, 32, 105, 116, 101, 109, 61, 48, 32, 110, 97, 109, 101, 61, 34, 47, 98, 105, 110, 47, 108, 115, 34, 32, 105, 110, 111, 100, 101, 61, 50, 54, 50, 51, 49, 54, 32, 100, 101, 118, 61, 99, 97, 58, 48, 49, 32, 109, 111, 100, 101, 61, 48, 49, 48, 48, 55, 53, 53, 32, 111, 117, 105, 100, 61, 48, 32, 111, 103, 105, 100, 61, 48, 32, 114, 100, 101, 118, 61, 48, 48, 58, 48, 48, 32, 110, 97, 109, 101, 116, 121, 112, 101, 61, 78, 79, 82, 77, 65, 76} 557 | 558 | //&{1302,,item=1,name="/lib64/ld-linux-x86-64.so.2",inode=396037,dev=ca:01,mode=0100755,ouid=0,ogid=0,rdev=00:00,nametype=NORMAL,1222763,1459376866.885} 559 | data[4] = []byte{149, 0, 0, 0, 22, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 117, 100, 105, 116, 40, 49, 52, 53, 57, 51, 55, 54, 56, 54, 54, 46, 56, 56, 53, 58, 49, 50, 50, 50, 55, 54, 51, 41, 58, 32, 105, 116, 101, 109, 61, 49, 32, 110, 97, 109, 101, 61, 34, 47, 108, 105, 98, 54, 52, 47, 108, 100, 45, 108, 105, 110, 117, 120, 45, 120, 56, 54, 45, 54, 52, 46, 115, 111, 46, 50, 34, 32, 105, 110, 111, 100, 101, 61, 51, 57, 54, 48, 51, 55, 32, 100, 101, 118, 61, 99, 97, 58, 48, 49, 32, 109, 111, 100, 101, 61, 48, 49, 48, 48, 55, 53, 53, 32, 111, 117, 105, 100, 61, 48, 32, 111, 103, 105, 100, 61, 48, 32, 114, 100, 101, 118, 61, 48, 48, 58, 48, 48, 32, 110, 97, 109, 101, 116, 121, 112, 101, 61, 78, 79, 82, 77, 65, 76} 560 | 561 | //&{1320,,,1222763,1459376866.885} 562 | data[5] = []byte{31, 0, 0, 0, 40, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 97, 117, 100, 105, 116, 40, 49, 52, 53, 57, 51, 55, 54, 56, 54, 54, 46, 56, 56, 53, 58, 49, 50, 50, 50, 55, 54, 51, 41, 58, 32} 563 | 564 | for i := 0; i < b.N; i++ { 565 | for n := 0; n < len(data); n++ { 566 | nlen := len(data[n]) 567 | msg := &syscall.NetlinkMessage{ 568 | Header: syscall.NlMsghdr{ 569 | Len: Endianness.Uint32(data[n][0:4]), 570 | Type: Endianness.Uint16(data[n][4:6]), 571 | Flags: Endianness.Uint16(data[n][6:8]), 572 | Seq: Endianness.Uint32(data[n][8:12]), 573 | Pid: Endianness.Uint32(data[n][12:16]), 574 | }, 575 | Data: data[n][syscall.SizeofNlMsghdr:nlen], 576 | } 577 | marshaller.Consume(msg) 578 | } 579 | } 580 | } 581 | 582 | type noopWriter struct{ t *testing.T } 583 | 584 | func (t *noopWriter) Write(a []byte) (int, error) { 585 | return 0, nil 586 | } 587 | 588 | func createTempFile(t *testing.T, name string, contents string) string { 589 | file := os.TempDir() + string(os.PathSeparator) + "go-audit." + name 590 | if err := os.WriteFile(file, []byte(contents), os.FileMode(0644)); err != nil { 591 | t.Fatal("Failed to create temp file", err) 592 | } 593 | return file 594 | } 595 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= 3 | cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 5 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 6 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= 7 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= 8 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 9 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 12 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 13 | github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= 14 | github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= 15 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 16 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 19 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 20 | github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= 21 | github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= 22 | github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= 23 | github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= 24 | github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= 25 | github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= 26 | github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= 27 | github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= 28 | github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= 29 | github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 30 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 31 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 32 | github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= 33 | github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= 34 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 35 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 36 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 37 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 38 | github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= 39 | github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= 40 | github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= 41 | github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= 42 | github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= 43 | github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= 44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 48 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 49 | github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= 50 | github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 51 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 52 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 53 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= 54 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 55 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 56 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 57 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 58 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 60 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 61 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 62 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 63 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 64 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 65 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 66 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 67 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 68 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 69 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 70 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 71 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 72 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 73 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 74 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 75 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 77 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 78 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 79 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 80 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 81 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 82 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 84 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 85 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 86 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 87 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 88 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 89 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 94 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 95 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 98 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 99 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 100 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 101 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 102 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 103 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 104 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 105 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 106 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 107 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 108 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 109 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 110 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 111 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 112 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 113 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 114 | github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= 115 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 116 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 117 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 118 | github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= 119 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 120 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 121 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 122 | github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= 123 | github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= 124 | github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= 125 | github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 126 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 127 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 128 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= 129 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 130 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 131 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 132 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 133 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 134 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 135 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 136 | github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= 137 | github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 138 | github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84= 139 | github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s= 140 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 141 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 142 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 143 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 145 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 146 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 147 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 148 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 149 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 150 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 151 | github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= 152 | github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= 153 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 154 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 155 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= 156 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= 157 | github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= 158 | github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= 159 | github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= 160 | github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 161 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 162 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 163 | github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= 164 | github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 165 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 166 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 167 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 168 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 169 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 170 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 171 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 172 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 173 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 174 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 175 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 176 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 177 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 178 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 179 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 180 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 181 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 182 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 183 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 184 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 185 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 186 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 187 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 188 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 189 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 190 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 191 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 192 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 193 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 194 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 195 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 196 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 197 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 198 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 199 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 200 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 201 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 202 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 203 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 204 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 205 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 206 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 207 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 208 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 209 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 212 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 213 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 214 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 217 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 218 | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 219 | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 220 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 221 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 227 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 228 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 234 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 237 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 238 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 239 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 240 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 241 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 242 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 243 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 244 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 245 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 246 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 247 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 248 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 249 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 250 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 251 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 252 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 253 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 254 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 256 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 257 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 258 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 259 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 260 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 261 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 262 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= 263 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= 264 | google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f h1:M65LEviCfuZTfrfzwwEoxVtgvfkFkBUbFnRbxCXuXhU= 265 | google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= 266 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= 267 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= 268 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 269 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 270 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 271 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 272 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 273 | google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= 274 | google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= 275 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 276 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 277 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 278 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 279 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 280 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 281 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 282 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 283 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 284 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 285 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 286 | gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0 h1:Xg23ydYYJLmb9AK3XdcEpplHZd1MpN3X2ZeeMoBClmY= 287 | gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0/go.mod h1:CeDeqW4tj9FrgZXF/dQCWZrBdcZWWBenhJtxLH4On2g= 288 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 289 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 290 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 291 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 292 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 293 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 294 | gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 295 | gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= 296 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 297 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 298 | --------------------------------------------------------------------------------