diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index 7a3abf2..0000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: "CodeQL Scanning" - -on: - push: - branches: - - "*" - pull_request: - branches: - - "*" - schedule: - - cron: '0 6 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - language: ['go'] - - steps: - - name: Checkout respository - uses: actions/checkout@v3 - - - name: Install CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/container-image.yaml b/.github/workflows/container-image.yaml index 52e71ef..c775b61 100644 --- a/.github/workflows/container-image.yaml +++ b/.github/workflows/container-image.yaml @@ -11,15 +11,6 @@ on: pull_request: workflow_dispatch: - -env: - HAPROXY_IMAGES: > - haproxy:2.2-alpine - haproxy:2.4-alpine - haproxy:2.5-alpine - haproxy:2.6-alpine - haproxy:2.7-alpine - jobs: build: runs-on: ubuntu-latest @@ -30,14 +21,6 @@ jobs: - name: Check out code uses: actions/checkout@v3 - - name: Run e2e tests against the example - shell: bash - run: > - for image in $HAPROXY_IMAGES; do - echo "Running e2e with Haproxy image $image" - HAPROXY_IMAGE=$image docker compose -f docker-compose.e2e.yaml up --abort-on-container-exit tests - done - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -56,7 +39,7 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Docker metadata - Main + - name: Docker metadata id: meta-main uses: docker/metadata-action@v4 with: @@ -68,44 +51,14 @@ jobs: type=ref,event=branch type=ref,event=pr - - name: Image - Main + - name: Image uses: docker/build-push-action@v3 with: context: . cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64 - file: Dockerfile + file: example/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta-main.outputs.tags }} - labels: ${{ steps.meta-main.outputs.labels }} - - - - name: Docker metadata - CRS4 - id: meta-crs4 - uses: docker/metadata-action@v4 - with: - images: ghcr.io/${{ github.repository }} - flavor: | - suffix=-crs4,onlatest=true - tags: | - type=raw,value=snapshot,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=ref,event=branch - type=ref,event=pr - - - name: Image - CRS4 - uses: docker/build-push-action@v3 - with: - context: . - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64 - file: Dockerfile - push: ${{ github.event_name != 'pull_request' }} - target: coreruleset - build-args: | - CORERULESET_VERSION=v4.0.0-rc1 - tags: ${{ steps.meta-crs4.outputs.tags }} - labels: ${{ steps.meta-crs4.outputs.labels }} + labels: ${{ steps.meta-main.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 38e628c..fdec0b4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -name: Lint (pre-commit) +name: Lint on: pull_request: @@ -8,9 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install Go - uses: actions/setup-go@v3 + - name: Set up Go + uses: actions/setup-go@v4 with: - go-version: v1.19.x - cache: true + go-version: '1.21' - run: go run mage.go lint diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 4782b83..15fa7d8 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -44,11 +44,8 @@ jobs: run: | sudo apt update && sudo apt -y install make pkg-config rubygems && sudo gem install fpm - # Download corazawaf/coraza/coraza.conf for distribution - curl https://raw.githubusercontent.com/corazawaf/coraza/v3.0.0/coraza.conf-recommended > coraza.conf - - name: Build binary - run: VERSION=${PACKAGE_VERSION} ARCH=${{ matrix.arch }} make + run: VERSION=${PACKAGE_VERSION} GOARCH=${{ matrix.arch }} go run mage.go build - name: Build package run: | @@ -62,11 +59,9 @@ jobs: --deb-systemd ./contrib/coraza-spoa.service \ --deb-systemd-enable \ --config-files /etc/coraza-spoa/config.yaml \ - ./coraza-spoa_${{matrix.arch}}=/usr/bin/coraza-spoa \ - ./doc/config/=/usr/share/doc/coraza-spoa/haproxy-config \ + ./coraza-spoa=/usr/bin/coraza-spoa \ ./LICENSE=/usr/share/doc/coraza-spoa/ \ - ./config.yaml.default=/etc/coraza-spoa/config.yaml \ - ./coraza.conf=/etc/coraza-spoa/coraza.conf + ./example/coraza-spoa.yaml=/etc/coraza-spoa/config.yaml ## Publish to the "testing" repo - name: Cloudsmith Push:debian/coraza-spoa-snapshots diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..e61520b --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,27 @@ +name: Test + +on: + pull_request: + push: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: setup environment + run: | + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:vbernat/haproxy-2.8 + sudo apt-get update + sudo apt-get install -y haproxy + haproxy -vv + + - name: Test + run: go run mage.go test \ No newline at end of file diff --git a/.gitignore b/.gitignore index f84664f..53175ce 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,5 @@ *.out vendor/ -# local files -config.yaml -logs/ -rules/ \ No newline at end of file +# Build output +build/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a320b69..0000000 --- a/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2023 The OWASP Coraza contributors -# SPDX-License-Identifier: Apache-2.0 - -FROM --platform=$BUILDPLATFORM golang:1.19-alpine3.17 AS builder - -WORKDIR /build -COPY . /build - -# Download dependencies for all platforms once -RUN go mod download - -ARG TARGETOS -ARG TARGETARCH - -RUN apk add --no-cache make ca-certificates \ - && update-ca-certificates - -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg \ - OS=${TARGETOS} ARCH=${TARGETARCH} make - -# --- -FROM alpine:3.17 AS main - -ARG TARGETARCH - -LABEL org.opencontainers.image.authors="The OWASP Coraza contributors" \ - org.opencontainers.image.description="OWASP Coraza WAF (Haproxy SPOA)" \ - org.opencontainers.image.documentation="https://coraza.io/connectors/coraza-spoa/" \ - org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.source="https://github.com/corazawaf/coraza-spoa" \ - org.opencontainers.image.title="coraza-spoa" - -RUN apk add --no-cache tini socat ca-certificates \ - && update-ca-certificates - -# Add unprivileged user & group for the coraza-spoa -RUN addgroup --system coraza-spoa \ - && adduser --system --ingroup coraza-spoa --no-create-home --home /nonexistent --disabled-password coraza-spoa - -RUN mkdir -p /etc/coraza-spoa /var/log/coraza-spoa \ - && chown coraza-spoa:coraza-spoa /var/log/coraza-spoa - -COPY --from=builder /build/coraza-spoa_${TARGETARCH} /usr/bin/coraza-spoa -COPY --from=builder /build/config.yaml.default /etc/coraza-spoa/config.yaml -COPY --from=builder /build/docker/coraza-spoa/coraza.conf /etc/coraza-spoa/coraza.conf -COPY --from=builder /build/docker/coraza-spoa/docker-entrypoint.sh /docker-entrypoint.sh - -EXPOSE 9000 -USER coraza-spoa - -HEALTHCHECK --interval=10s --timeout=2s --retries=2 CMD "/usr/bin/socat /dev/null TCP:0.0.0.0:9000" - -ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] - -CMD ["/usr/bin/coraza-spoa", "--config", "/etc/coraza-spoa/config.yaml"] - -# --- -FROM main AS coreruleset - -ARG CORERULESET_VERSION=v4.0.0-rc1 -ARG CORERULESET_SHA256SUM=a8f0d1cac941bf2158988b92a91519f093a8bce64a260e46fa352d608c7de3e6 - -# Switch to root for crs installation -USER root - -# Download the core rule set -RUN set -xe \ - && wget -O /tmp/crs.tgz https://github.com/coreruleset/coreruleset/archive/refs/tags/${CORERULESET_VERSION}.tar.gz - -RUN echo "$CORERULESET_SHA256SUM /tmp/crs.tgz" | sha256sum -c - -RUN set -xe \ - && mkdir crs \ - && tar --strip-components 1 -C crs -xf /tmp/crs.tgz \ - && mv crs/crs-setup.conf.example /etc/coraza-spoa/crs-setup.conf \ - && mv crs/rules /etc/coraza-spoa \ - && if [[ -d crs/plugins ]] ; then mv crs/plugins /etc/coraza-spoa ; fi \ - && rm -rf crs /tmp/crs.tgz - -USER coraza-spoa diff --git a/Makefile b/Makefile deleted file mode 100644 index b3ba83d..0000000 --- a/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2022 The Corazawaf Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -BINARY = coraza-spoa - -VERSION ?= "dev" -REVISION ?= $(shell git rev-parse HEAD) - -ARCH ?= $(shell which go >/dev/null 2>&1 && go env GOARCH) - -ifeq ($(ARCH),) - $(error mandatory variable ARCH is empty, either set it when calling the command or make sure 'go env GOARCH' works) -endif - -OS ?= $(shell which go >/dev/null 2>&1 && go env GOOS) - -ifeq ($(OS),) - $(error mandatory variable OS is empty, either set it when calling the cammand or make sure 'go env GOOS' works) -endif - -#LDFLAGS = -ldflags "-X main.Version=${VERSION} -X main.Revision=${REVISION}" - - -default: build - -build: - GOARCH=$(ARCH) GOOS=$(OS) CGO_ENABLED=0 go build -v ${LDFLAGS} -o $(BINARY)_$(ARCH) cmd/coraza-spoa/main.go - -clean: - rm -f $(BINARY)_amd64 $(BINARY)_arm64 $(BINARY)_386 diff --git a/README.md b/README.md index 6d2c9e4..aac2d99 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,23 @@ HAProxy includes a [Stream Processing Offload Engine](https://www.haproxy.com/bl ### Build -The command `make` will compile the source code and produce the executable file `coraza-spoa`. - -### Clean - -When you need to re-compile the source code, you can use the command `make clean` to clean the executable file. +The command `go run mage.go build` will compile the source code and produce the executable file `coraza-spoa`. ## Configuration ## Coraza SPOA -The example configuration file is [config.yaml.default](https://github.com/corazawaf/coraza-spoa/blob/main/config.yaml.default), you can copy it and modify the related configuration information. You can start the service by running the command: +The example configuration file is [examples/coraza-spoa.yaml](https://github.com/corazawaf/coraza-spoa/blob/main/examples/coraza-spoa.yaml), you can copy it and modify the related configuration information. You can start the service by running the command: ``` -coraza-spoa -config /etc/coraza-spoa/coraza.yaml +coraza-spoa -f /etc/coraza-spoa/coraza-spoa.yaml ``` You will also want to download & extract the [OWASP Core Ruleset]( https://github.com/coreruleset/coreruleset/releases) (version 4+ supported) to the `/etc/coraza-spoa` directory. ## HAProxy SPOE -Configure HAProxy to exchange messages with the SPOA. The example SPOE configuration file is [coraza.cfg](https://github.com/corazawaf/coraza-spoa/blob/main/doc/config/coraza.cfg), you can copy it and modify the related configuration information. Default directory to place the config is `/etc/haproxy/coraza.cfg`. +Configure HAProxy to exchange messages with the SPOA. The example SPOE configuration file is [coraza.cfg](https://github.com/corazawaf/coraza-spoa/blob/main/examples/coraza.cfg), you can copy it and modify the related configuration information. Default directory to place the config is `/etc/haproxy/coraza.cfg`. ```ini # /etc/haproxy/coraza.cfg @@ -47,7 +43,7 @@ spoe-message coraza-req event on-frontend-http-request ``` -The application name from `config.yaml` must match the `app=` name, or the `default_application` will be used. +The application name from `config.yaml` must match the `app=` name. The backend defined in `use-backend` must match a `haproxy.cfg` backend which directs requests to the SPOA daemon reachable via `127.0.0.1:9000`. @@ -70,12 +66,12 @@ backend coraza-spoa server s1 127.0.0.1:9000 ``` -A comprehensive HAProxy configuration example can be found in [docs/config/haproxy.cfg](https://github.com/corazawaf/coraza-spoa/blob/main/doc/config/coraza.cfg). +A comprehensive HAProxy configuration example can be found in [examples/haproxy.cfg](https://github.com/corazawaf/coraza-spoa/blob/main/examples/coraza.cfg). -Because, in the SPOE configuration file (coraza.cfg), we declare to use the backend [coraza-spoa](https://github.com/corazawaf/coraza-spoa/blob/88b4e54ab3ddcb58d946ed1d6389eff73745575b/doc/config/coraza.cfg#L14) to communicate with the service, so we need also to define it in the [HAProxy file](https://github.com/corazawaf/coraza-spoa/blob/dd5eb86d1e9abbdd5fe568249f36a6d85257eba7/doc/config/haproxy.cfg#L37): +Because, in the SPOE configuration file (coraza.cfg), we declare to use the backend [coraza-spoa](https://github.com/corazawaf/coraza-spoa/blob/main/examples/coraza.cfg#L14) to communicate with the service, so we need also to define it in the [HAProxy file](https://github.com/corazawaf/coraza-spoa/blob/main/examples/haproxy.cfg#L37): ## Docker -- Build the coraza-spoa image `docker-compose build` -- Run haproxy, coraza-spoa and a mock server `docker-compose up` +- Build the coraza-spoa image `docker compose build` +- Run haproxy, coraza-spoa and a mock server `docker compose up` - Perform a request which gets blocked by the WAF: `curl http://localhost:4000/\?x\=/etc/passwd` \ No newline at end of file diff --git a/cmd/coraza-spoa/main.go b/cmd/coraza-spoa/main.go deleted file mode 100644 index 2490c24..0000000 --- a/cmd/coraza-spoa/main.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "flag" - - "github.com/corazawaf/coraza-spoa/config" - "github.com/corazawaf/coraza-spoa/internal" -) - -func main() { - cfg := flag.String("config", "", "configuration file") - if cfg == nil { - panic("configuration file is not set") - } - flag.Parse() - if err := config.InitConfig(*cfg); err != nil { - panic(err) - } - spoa, err := internal.New(config.Global) - if err != nil { - panic(err) - } - if err := spoa.Start(config.Global.Bind); err != nil { - panic(err) - } -} diff --git a/config.go b/config.go new file mode 100644 index 0000000..573c74c --- /dev/null +++ b/config.go @@ -0,0 +1,139 @@ +package main + +import ( + "fmt" + "io" + "net/url" + "os" + "time" + + "github.com/rs/zerolog" + "gopkg.in/yaml.v3" + + "github.com/corazawaf/coraza-spoa/internal" +) + +func readConfig() (*config, error) { + open, err := os.Open(configPath) + if err != nil { + return nil, err + } + defer open.Close() + + d := yaml.NewDecoder(open) + d.KnownFields(true) + + var cfg config + if err := d.Decode(&cfg); err != nil { + return nil, err + } + + if len(cfg.Applications) == 0 { + globalLogger.Warn().Msg("no applications defined") + } + + return &cfg, nil +} + +type config struct { + Bind string `yaml:"bind"` + Log logConfig `yaml:",inline"` + Applications []struct { + Log logConfig `yaml:",inline"` + Name string `yaml:"name"` + Directives string `yaml:"directives"` + ResponseCheck bool `yaml:"response_check"` + TransactionTTLMS int `yaml:"transaction_ttl_ms"` + TransactionActiveLimit int `yaml:"transaction_active_limit"` + TransactionActiveLimitReject bool `yaml:"transaction_active_limit_reject"` + } `yaml:"applications"` +} + +func (c config) networkAddressFromBind() (network string, address string) { + bindUrl, err := url.Parse(c.Bind) + if err == nil { + return bindUrl.Scheme, bindUrl.Path + } + + return "tcp", c.Bind +} + +func (c config) newApplications() (map[string]*internal.Application, error) { + allApps := make(map[string]*internal.Application) + + for name, a := range c.Applications { + logger, err := a.Log.newLogger() + if err != nil { + return nil, fmt.Errorf("creating logger for application %q: %v", name, err) + } + + appConfig := internal.AppConfig{ + Logger: logger, + Directives: a.Directives, + ResponseCheck: a.ResponseCheck, + TransactionTTLMS: time.Duration(a.TransactionTTLMS) * time.Millisecond, + } + + application, err := appConfig.NewApplication() + if err != nil { + return nil, fmt.Errorf("initializing application %q: %v", name, err) + } + + allApps[a.Name] = application + } + + return allApps, nil +} + +type logConfig struct { + Level string `yaml:"log_level"` + File string `yaml:"log_file"` + Format string `yaml:"log_format"` +} + +func (lc logConfig) outputWriter() (io.Writer, error) { + var out io.Writer + if lc.File == "" || lc.File == "/dev/stdout" { + out = os.Stdout + } else if lc.File == "/dev/stderr" { + out = os.Stderr + } else if lc.File == "/dev/null" { + out = io.Discard + } else { + // TODO: Close the handle if not used anymore. + // Currently these are leaked as soon as we reload. + f, err := os.OpenFile(lc.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return nil, err + } + out = f + } + return out, nil +} + +func (lc logConfig) newLogger() (zerolog.Logger, error) { + out, err := lc.outputWriter() + if err != nil { + return globalLogger, err + } + + switch lc.Format { + case "console": + out = zerolog.ConsoleWriter{ + Out: out, + } + case "json": + default: + return globalLogger, fmt.Errorf("unknown log format: %v", lc.Format) + } + + if lc.Level == "" { + lc.Level = "info" + } + lvl, err := zerolog.ParseLevel(lc.Level) + if err != nil { + return globalLogger, err + } + + return zerolog.New(out).Level(lvl).With().Timestamp().Logger(), nil +} diff --git a/config.yaml.default b/config.yaml.default deleted file mode 100644 index 4a6cf88..0000000 --- a/config.yaml.default +++ /dev/null @@ -1,32 +0,0 @@ - -# The SPOA server bind address -bind: 0.0.0.0:9000 - -# Process request and response with this application if provided app name is not found. -# You can remove or comment out this config param if you don't need "default_application" functionality. -default_application: sample_app - -applications: - sample_app: - # Get the coraza.conf from https://github.com/corazawaf/coraza - # - # Download the OWASP CRS from https://github.com/coreruleset/coreruleset/releases - # and copy crs-setup.conf & the rules, plugins directories to /etc/coraza-spoa - directives: | - Include /etc/coraza-spoa/coraza.conf - Include /etc/coraza-spoa/crs-setup.conf - Include /etc/coraza-spoa/rules/*.conf - - # HAProxy configured to send requests only, that means no cache required - # NOTE: there are still some memory & caching issues, so use this with care - no_response_check: true - - # The transaction cache lifetime in milliseconds (60000ms = 60s) - transaction_ttl_ms: 60000 - # The maximum number of transactions which can be cached - transaction_active_limit: 100000 - - # The log level configuration, one of: debug/info/warn/error/panic/fatal - log_level: info - # The log file path - log_file: /dev/stdout \ No newline at end of file diff --git a/config/config.go b/config/config.go deleted file mode 100644 index d5df3f8..0000000 --- a/config/config.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "fmt" - "os" - - yaml "gopkg.in/yaml.v3" -) - -// Global is used to store the configuration. -var Global *Config - -// Config is used to configure coraza-server. -type Config struct { - Bind string `yaml:"bind"` - DefaultApplication string `yaml:"default_application"` - Applications map[string]*Application `yaml:"applications"` -} - -// Application is used to manage the haproxy configuration and waf rules. -type Application struct { - LogLevel string `yaml:"log_level"` - LogFile string `yaml:"log_file"` - NoResponseCheck bool `yaml:"no_response_check"` - Directives string `yaml:"directives"` - // Deprecated: use directives instead, this will be removed in the near future. - Rules []string `yaml:"rules"` - TransactionTTLMilliseconds int `yaml:"transaction_ttl_ms"` - TransactionActiveLimit int `yaml:"transaction_active_limit"` -} - -// InitConfig initializes the configuration. -func InitConfig(file string) error { - f, err := os.Open(file) - if err != nil { - return err - } - defer f.Close() - - err = yaml.NewDecoder(f).Decode(&Global) - if err != nil { - return err - } - - // validate the configuration - err = validateConfig() - if err != nil { - return err - } - return nil -} - -func validateConfig() error { - fmt.Printf("Loading %d applications\n", len(Global.Applications)) - for _, app := range Global.Applications { - if app.LogLevel == "" { - app.LogLevel = "warn" - } - if app.TransactionTTLMilliseconds < 0 { - return fmt.Errorf("SPOA transaction ttl must be greater than 0") - } - - if app.TransactionActiveLimit < 0 { - return fmt.Errorf("SPOA transaction active limit must be greater than 0") - } - } - return nil -} diff --git a/doc/config/coraza.cfg b/doc/config/coraza.cfg deleted file mode 100644 index a588091..0000000 --- a/doc/config/coraza.cfg +++ /dev/null @@ -1,26 +0,0 @@ -# https://github.com/haproxy/haproxy/blob/master/doc/SPOE.txt -# /etc/haproxy/coraza.cfg -[coraza] -spoe-agent coraza-agent - # Process HTTP requests only (the responses are not evaluated) - messages coraza-req - # Comment the previous line and add coraza-res, to process responses also. - # NOTE: there are still some memory & caching issues, so use this with care - #messages coraza-req coraza-res - option var-prefix coraza - option set-on-error error - timeout hello 2s - timeout idle 2m - timeout processing 500ms - use-backend coraza-spoa - log global - -spoe-message coraza-req - args app=str(sample_app) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body - event on-frontend-http-request - -spoe-message coraza-res - args app=str(sample_app) id=unique-id version=res.ver status=status headers=res.hdrs body=res.body - event on-http-response - - diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml deleted file mode 100644 index 5d1c5e9..0000000 --- a/docker-compose.e2e.yaml +++ /dev/null @@ -1,46 +0,0 @@ -version: "3.9" -services: - httpbin: - image: mccutchen/go-httpbin:v2.5.0 - ports: - - 8080:8080 - coraza: - build: - context: . - target: coreruleset - volumes: - - ./docker/e2e/e2e-rules.conf:/etc/coraza-spoa/rules/000-e2e-rules.conf - haproxy: - depends_on: - - coraza - - httpbin - image: ${HAPROXY_IMAGE:-haproxy:2.7-alpine} - command: - [ - "sh", - "-c", - "haproxy -f /usr/local/etc/haproxy/haproxy.cfg | tee /var/lib/haproxy/hap.log" - ] - ports: [ "4000:80" ] - links: - - "coraza:coraza" - - "httpbin:httpbin" - volumes: - - type: bind - source: ./docker/haproxy - target: /usr/local/etc/haproxy - - hap:/var/lib/haproxy - tests: - depends_on: - - haproxy - - coraza - links: - - "haproxy:haproxy" - - "httpbin:httpbin" - build: - context: docker/e2e - dockerfile: ./Dockerfile.curl - volumes: - - hap:/haproxy -volumes: - hap: diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index d3d2374..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: "3.9" -services: - httpbin: - restart: unless-stopped - image: mccutchen/go-httpbin:v2.5.0 - ports: - - 8080:8080 - coraza: - restart: unless-stopped - build: - context: . - target: coreruleset - haproxy: - restart: unless-stopped - image: haproxy:2.7-alpine - ports: [ "4000:80", "4443:443", "4001:4001" ] - links: - - "coraza:coraza" - - "httpbin:httpbin" - volumes: - - type: bind - source: ./docker/haproxy - target: /usr/local/etc/haproxy diff --git a/docker/coraza-spoa/coraza.conf b/docker/coraza-spoa/coraza.conf deleted file mode 100644 index 08d7874..0000000 --- a/docker/coraza-spoa/coraza.conf +++ /dev/null @@ -1,252 +0,0 @@ -# -- Rule engine initialization ---------------------------------------------- - -# Enable Coraza, attaching it to every transaction. Use detection -# only to start with, because that minimises the chances of post-installation -# disruption. -# -SecRuleEngine On - - -# -- Request body handling --------------------------------------------------- - -# Allow Coraza to access request bodies. If you don't, Coraza -# won't be able to see any POST parameters, which opens a large security -# hole for attackers to exploit. -# -SecRequestBodyAccess On - -# Enable XML request body parser. -# Initiate XML Processor in case of xml content-type -# -SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \ - "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" - -# Enable JSON request body parser. -# Initiate JSON Processor in case of JSON content-type; change accordingly -# if your application does not use 'application/json' -# -SecRule REQUEST_HEADERS:Content-Type "application/json" \ - "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" - -# Sample rule to enable JSON request body parser for more subtypes. -# Uncomment or adapt this rule if you want to engage the JSON -# Processor for "+json" subtypes -# -#SecRule REQUEST_HEADERS:Content-Type "^application/.+[+]json$" \ -# "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" - -# Maximum request body size we will accept for buffering. If you support -# file uploads then the value given on the first line has to be as large -# as the largest file you are willing to accept. The second value refers -# to the size of data, with files excluded. You want to keep that value as -# low as practical. -# -SecRequestBodyLimit 13107200 -SecRequestBodyNoFilesLimit 131072 - -# What to do if the request body size is above our configured limit. -# Keep in mind that this setting will automatically be set to ProcessPartial -# when SecRuleEngine is set to DetectionOnly mode in order to minimize -# disruptions when initially deploying Coraza. -# -SecRequestBodyLimitAction Reject - -# Verify that we've correctly processed the request body. -# As a rule of thumb, when failing to process a request body -# you should reject the request (when deployed in blocking mode) -# or log a high-severity alert (when deployed in detection-only mode). -# -SecRule REQBODY_ERROR "!@eq 0" \ -"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - -# By default be strict with what we accept in the multipart/form-data -# request body. If the rule below proves to be too strict for your -# environment consider changing it to detection-only. You are encouraged -# _not_ to remove it altogether. -# -SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ -"id:'200003',phase:2,t:none,log,deny,status:400, \ -msg:'Multipart request body failed strict validation: \ -PE %{REQBODY_PROCESSOR_ERROR}, \ -BQ %{MULTIPART_BOUNDARY_QUOTED}, \ -BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ -DB %{MULTIPART_DATA_BEFORE}, \ -DA %{MULTIPART_DATA_AFTER}, \ -HF %{MULTIPART_HEADER_FOLDING}, \ -LF %{MULTIPART_LF_LINE}, \ -SM %{MULTIPART_MISSING_SEMICOLON}, \ -IQ %{MULTIPART_INVALID_QUOTING}, \ -IP %{MULTIPART_INVALID_PART}, \ -IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ -FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" - -# Did we see anything that might be a boundary? -# -# Here is a short description about the Coraza Multipart parser: the -# parser returns with value 0, if all "boundary-like" line matches with -# the boundary string which given in MIME header. In any other cases it returns -# with different value, eg. 1 or 2. -# -# The RFC 1341 descript the multipart content-type and its syntax must contains -# only three mandatory lines (above the content): -# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING -# * --BOUNDARY_STRING -# * --BOUNDARY_STRING-- -# -# First line indicates, that this is a multipart content, second shows that -# here starts a part of the multipart content, third shows the end of content. -# -# If there are any other lines, which starts with "--", then it should be -# another boundary id - or not. -# -# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive. -# -# If multipart content contains the three necessary lines with correct order, but -# there are one or more lines with "--", then parser returns with value 2 (non-zero). -# -# If some of the necessary lines (usually the start or end) misses, or the order -# is wrong, then parser returns with value 1 (also a non-zero). -# -# You can choose, which one is what you need. The example below contains the -# 'strict' mode, which means if there are any lines with start of "--", then -# Coraza blocked the content. But the next, commented example contains -# the 'permissive' mode, then you check only if the necessary lines exists in -# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."), -# or other text files, which contains eg. HTTP headers. -# -# The difference is only the operator - in strict mode (first) the content blocked -# in case of any non-zero value. In permissive mode (second, commented) the -# content blocked only if the value is explicit 1. If it 0 or 2, the content will -# allowed. -# - -# -# See #1747 and #1924 for further information on the possible values for -# MULTIPART_UNMATCHED_BOUNDARY. -# -SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \ - "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" - -# Some internal errors will set flags in TX and we will need to look for these. -# All of these are prefixed with "MSC_". The following flags currently exist: -# -# COR_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. -# -SecRule TX:/^COR_/ "!@streq 0" \ - "id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'" - - -# -- Response body handling -------------------------------------------------- - -# Allow Coraza to access response bodies. -# You should have this directive enabled in order to identify errors -# and data leakage issues. -# -# Do keep in mind that enabling this directive does increases both -# memory consumption and response latency. -# -SecResponseBodyAccess On - -# Which response MIME types do you want to inspect? You should adjust the -# configuration below to catch documents but avoid static files -# (e.g., images and archives). -# -SecResponseBodyMimeType text/plain text/html text/xml - -# Buffer response bodies of up to 512 KB in length. -SecResponseBodyLimit 524288 - -# What happens when we encounter a response body larger than the configured -# limit? By default, we process what we have and let the rest through. -# That's somewhat less secure, but does not break any legitimate pages. -# -SecResponseBodyLimitAction ProcessPartial - - -# -- Filesystem configuration ------------------------------------------------ - -# The location where Coraza stores temporary files (for example, when -# it needs to handle a file upload that is larger than the configured limit). -# -# This default setting is chosen due to all systems have /tmp available however, -# this is less than ideal. It is recommended that you specify a location that's private. -# -SecTmpDir /tmp/ - -# The location where Coraza will keep its persistent data. This default setting -# is chosen due to all systems have /tmp available however, it -# too should be updated to a place that other users can't access. -# -SecDataDir /tmp/ - - -# -- File uploads handling configuration ------------------------------------- - -# The location where Coraza stores intercepted uploaded files. This -# location must be private to Coraza. You don't want other users on -# the server to access the files, do you? -# -#SecUploadDir /opt/coraza/var/upload/ - -# By default, only keep the files that were determined to be unusual -# in some way (by an external inspection script). For this to work you -# will also need at least one file inspection rule. -# -#SecUploadKeepFiles RelevantOnly - -# Uploaded files are by default created with permissions that do not allow -# any other user to access them. You may need to relax that if you want to -# interface Coraza to an external program (e.g., an anti-virus). -# -#SecUploadFileMode 0600 - - -# -- Debug log configuration ------------------------------------------------- - -# Default debug log path -# Debug levels: -# 1: fatal -# 2: panic -# 3: error -# 4: warn -# 5: info -# 6: debug -# Most logging has not been implemented because it will be replaced with -# advanced rule profiling options -#SecDebugLog /var/log/coraza-spoa/debug.log -#SecDebugLogLevel 5 - - -# -- Audit log configuration ------------------------------------------------- - -# Log the transactions that are marked by a rule, as well as those that -# trigger a server error (determined by a 5xx or 4xx, excluding 404, -# level response status codes). -# -SecAuditEngine Off -SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$" - -# Log everything we know about a transaction. -SecAuditLogParts ABIJDEFHZ - -# Use a single file for logging. This is much easier to look at, but -# assumes that you will use the audit log only occasionally. -# -#SecAuditLog /var/log/coraza-spoa/audit.log -#SecAuditLogFormat json -#SecAuditLogType Serial - - -# -- Miscellaneous ----------------------------------------------------------- - -# Use the most commonly used application/x-www-form-urlencoded parameter -# separator. There's probably only one application somewhere that uses -# something else so don't expect to change this value. -# -SecArgumentSeparator & - -# Settle on version 0 (zero) cookies, as that is what most applications -# use. Using an incorrect cookie version may open your installation to -# evasion attacks (against the rules that examine named cookies). -# -SecCookieFormat 0 diff --git a/docker/coraza-spoa/docker-entrypoint.sh b/docker/coraza-spoa/docker-entrypoint.sh deleted file mode 100755 index 4633059..0000000 --- a/docker/coraza-spoa/docker-entrypoint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -set -e - -# Allow users to run arbitrary commands within the container - -if [ $# -gt 0 ] && [ "$1" = "${1#-}" ]; then - # First char isn't `-`, probably a `docker run -ti ` - # Just exec and exit - exec "$@" - exit -fi - -exec coraza-spoa --config /etc/coraza-spoa/config.yaml diff --git a/docker/e2e/Dockerfile.curl b/docker/e2e/Dockerfile.curl deleted file mode 100644 index 26cddc5..0000000 --- a/docker/e2e/Dockerfile.curl +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2022 The OWASP Coraza contributors -# SPDX-License-Identifier: Apache-2.0 - -FROM curlimages/curl -USER root - -WORKDIR /workspace - -RUN apk add --no-cache bash - -COPY ./e2e.sh /workspace/e2e.sh - -ENV HAPROXY_HOST=haproxy:80 -ENV HTTPBIN_HOST=httpbin:8080 - -CMD ["bash", "/workspace/e2e.sh"] diff --git a/docker/e2e/e2e-rules.conf b/docker/e2e/e2e-rules.conf deleted file mode 100644 index aae10b6..0000000 --- a/docker/e2e/e2e-rules.conf +++ /dev/null @@ -1,9 +0,0 @@ -SecRuleEngine On -SecRequestBodyAccess On -SecRule REQUEST_URI "/e2e-deny" "id:101,phase:1,t:lowercase,log,deny" -SecRule REQUEST_URI "/e2e-drop" "id:102,phase:1,t:lowercase,log,drop" -SecRule REQUEST_URI "/e2e-redirect" "id:103,phase:1,t:lowercase,log,redirect:http://www.example.org/denied" -SecRule REQUEST_BODY "@rx maliciouspayload" "id:104,phase:2,t:lowercase,log,deny" -SecRule RESPONSE_STATUS "@streq 406" "id:105,phase:3,t:lowercase,log,deny" -SecRule RESPONSE_HEADERS::e2eblock "true" "id:106,phase:4,t:lowercase,log,deny" -SecRule RESPONSE_BODY "@contains responsebodycode" "id:107,phase:4,t:lowercase,log,deny" diff --git a/docker/e2e/e2e.sh b/docker/e2e/e2e.sh deleted file mode 100755 index 69372c2..0000000 --- a/docker/e2e/e2e.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/bash -# Copyright 2022 The OWASP Coraza contributors -# SPDX-License-Identifier: Apache-2.0 -# -# Script derived from the original in coraza-proxy-wasm & extended for haproxy -# https://github.com/corazawaf/coraza-proxy-wasm/blob/main/e2e/e2e-example.sh - -HAPROXY_HOST=${HAPROXY_HOST:-"localhost:4000"} -HTTPBIN_HOST=${HTTPBIN_HOST:-"localhost:8080"} -HAPROXY_LOGS='/haproxy/hap.log' - -[[ "${DEBUG}" == "true" ]] && set -x - -# if env variables are in place, default values are overridden -health_url="http://${HTTPBIN_HOST}" -url_unfiltered="http://${HAPROXY_HOST}" -url_filtered_deny="${url_unfiltered}/e2e-deny" -url_filtered_drop="${url_unfiltered}/e2e-drop" -url_filtered_redirect="${url_unfiltered}/e2e-redirect" -url_filtered_resp_header="${url_unfiltered}/response-headers?e2eblock=true" -url_echo="${url_unfiltered}/anything" - -trueNegativeBodyPayload="This is a payload" -truePositiveBodyPayload="maliciouspayload" -trueNegativeBodyPayloadForResponseBody="Hello world" -truePositiveBodyPayloadForResponseBody="responsebodycode" - -# wait_for_service waits until the given URL returns a 200 status code. -# $1: The URL to send requests to. -# $2: The max number of requests to send before giving up. -function wait_for_service() { - local status_code="000" - local url=${1} - local max=${2} - while [[ "${status_code}" -ne "200" ]]; do - status_code=$(curl --write-out "%{http_code}" --silent --output /dev/null "${url}") - sleep 1 - echo -ne "[Wait] Waiting for response from ${url}. Timeout: ${max}s \r" - ((max-=1)) - if [[ "${max}" -eq 0 ]]; then - echo "[Fail] Timeout waiting for response from ${url}, make sure the server is running." - exit 1 - fi - done - echo -e "\n[Ok] Got status code ${status_code}" -} - -# check_status sends HTTP requests to the given URL and expects a given response code. -# $1: The URL to send requests to. -# $2: The expected status code. -# $3-N: The rest of the arguments will be passed to the curl command as additional arguments -# to customize the HTTP call. -function check_status() { - local url=${1} - local status=${2} - local args=("${@:3}" --write-out '%{http_code}' --silent --output /dev/null) - status_code=$(curl "${args[@]}" "${url}") - if [[ "${status_code}" -ne ${status} ]] ; then - echo "[Fail] Unexpected response with code ${status_code} from ${url}" - exit 1 - fi - echo "[Ok] Got status code ${status_code}, expected ${status}" -} - -# check_body sends the given HTTP request and checks the response body. -# $1: The URL to send requests to. -# $2: true/false indicating if an empty, or null body is expected or not. -# $3-N: The rest of the arguments will be passed to the curl command as additional arguments -# to customize the HTTP call. -function check_body() { - local url=${1} - local empty=${2} - local args=("${@:3}" --silent) - response_body=$(curl "${args[@]}" "${url}") - if [[ "${empty}" == "true" ]] && [[ -n "${response_body}" ]]; then - echo -e "[Fail] Unexpected response with a body. Body dump:\n${response_body}" - exit 1 - fi - if [[ "${empty}" != "true" ]] && [[ -z "${response_body}" ]]; then - echo -e "[Fail] Unexpected response with a body. Body dump:\n${response_body}" - exit 1 - fi - echo "[Ok] Got response with an expected body (empty=${empty})" -} - -# check_hap_logs checks HAProxy logs for the given regexp. -# $1: The regexp to check logs aginst. -function check_hap_logs() { - local regex=${1} - if [[ $(grep -q -e "$regex" "$HAPROXY_LOGS") ]]; then - echo -e "[Fail] No log lines matches pattern '$regex'" - exit 1 - fi - echo "[Ok] Got logs with an expected pattern '$regex'" -} - -step=1 -total_steps=17 - -## Testing that basic coraza phases are working - -# Testing if the server is up -echo "[${step}/${total_steps}] Testing application reachability" -wait_for_service "${health_url}" 15 - -# Testing container reachability with an unfiltered GET request -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true negative request" -wait_for_service "${url_echo}?arg=arg_1" 20 - -# Testing filtered request (deny) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule - deny" -check_status "${url_filtered_deny}" 403 - -# Testing filtered request (drop) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule - drop" -check_status "${url_filtered_drop}" 000 - -# Testing filtered request (redirect) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule - redirect" -check_status "${url_filtered_redirect}" 302 - -# Testing body true negative -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing true negative request (body)" -check_status "${url_echo}" 200 -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${trueNegativeBodyPayload}" - -# Testing body detection -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing true positive request (body)" -check_status "${url_echo}" 403 -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${truePositiveBodyPayload}" - -# TODO - Testing response headers detection TODO -#((step+=1)) -#echo "[${step}/${total_steps}] (onResponseHeaders) Testing true positive" -#check_status "${url_filtered_resp_header}" 403 - -# TODO(M4tteoP): Update response body e2e after https://github.com/corazawaf/coraza-proxy-wasm/issues/26 -# Testing response body true negative -((step+=1)) -echo "[${step}/${total_steps}] (onResponseBody) Testing true negative" -check_body "${url_echo}" false -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${trueNegativeBodyPayloadForResponseBody}" - -# TODO - Testing response body detection -#((step+=1)) -#echo "[${step}/${total_steps}] (onResponseBody) Testing true positive" -#check_body "${url_echo}" true -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${truePositiveBodyPayloadForResponseBody}" - -## Testing extra requests examples from the readme and some CRS rules in anomaly score mode. - -# Testing XSS detection during phase 1 -((step+=1)) -echo "[${step}/${total_steps}] Testing XSS detefction at request headers" -check_status "${url_echo}?arg=" 403 - -# Testing SQLI detection during phase 2 -((step+=1)) -echo "[${step}/${total_steps}] Testing SQLi detection at request body" -check_status "${url_echo}" 403 -X POST --data "1%27%20ORDER%20BY%203--%2B" - -# Triggers a CRS scanner detection rule (913100) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing CRS rule 913100" -check_status "${url_echo}" 403 --user-agent "Grabber/0.1 (X11; U; Linux i686; en-US; rv:1.7)" -H "Host: localhost" -H "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" - -# True negative GET request with an usual user-agent -((step+=1)) -echo "[${step}/${total_steps}] True negative GET request with user-agent" -check_status "${url_echo}" 200 --user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" - -# Find Allow action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: allow)" -check_hap_logs "waf-action: allow" - -# Find Deny action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: deny)" -check_hap_logs "waf-action: deny" - -# Find Drop action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: drop)" -check_hap_logs "waf-action: drop" - -# Find Redirect action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: redirect)" -check_hap_logs "waf-action: redirect" - -# Find no error -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (spoa-error: -)" -check_hap_logs "spoa-error: -" - -echo "[Done] All tests passed" diff --git a/docker/haproxy/example.com.pem b/docker/haproxy/example.com.pem deleted file mode 100644 index f6bb046..0000000 --- a/docker/haproxy/example.com.pem +++ /dev/null @@ -1,45 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICrDCCAZQCCQDUSeGLwr83ADANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA0q -LmV4YW1wbGUuY29tMB4XDTIyMDcwMzExMTQwNFoXDTMyMDYzMDExMTQwNFowGDEW -MBQGA1UEAwwNKi5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAKtD+lh3P8PWgRoQosndVJtU4LmQMwGW/Jvd5U43qiUt4fr+mqwBtal8 -8XSj2Aa133UUi1VHVJVf2ftVKfCZJU2EFiIkUULkawx/L8ydGnw4jzHfQrilqoIU -hGGwLi0b1SPl4EkErBrIKvBJDzwf7MAwgQ2kqZ5OqZJu7lvOYhObMxFsyS7OfP8Z -y9nELsJnA7FZr6lssc97L1W0GOA7AJYVeRndAnlKuql8EvFBdYhsDoas9B0UpRQA -F7dDnxXkmkIFdDK6nWsMB5wwZDA7kVhIbzCcV92LrgiNLuSncQ98vM2q4cZCJWA+ -7zf68s+bfhDSUBs/2eshVKZlnDnRIp0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA -UBMan7o53uHUEtLwYhNtcEUxOya8ibsb3jxbmRqUB2xdFL+BVC8Yje8RSbHIlako -9S3gAcvl08oeUpg6cobsqD4EOyM5Ebrgkt3FTWycEPDqfJeRgT51ULoapIkONMRm -Q7CiAdGAjSLxOP1E+olmkAJIxtHRe+eEI9wzgs15zHqbeRBSOebLw+cdY9ynyA5h -VqZh4wI1SIZ+nH4AGQInI9/LOrVPOSg255oeDjSwPLRkUXQzgXffNVDiecoCZQw8 -7PGNs0jJ23TefRHftheiDNUUZC+51NvH3ekXmz1A5DueElSjcI5Bf6m7pv6TYAi4 -54NyLXTtMdr5XW1fLeuTmg== ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrQ/pYdz/D1oEa -EKLJ3VSbVOC5kDMBlvyb3eVON6olLeH6/pqsAbWpfPF0o9gGtd91FItVR1SVX9n7 -VSnwmSVNhBYiJFFC5GsMfy/MnRp8OI8x30K4paqCFIRhsC4tG9Uj5eBJBKwayCrw -SQ88H+zAMIENpKmeTqmSbu5bzmITmzMRbMkuznz/GcvZxC7CZwOxWa+pbLHPey9V -tBjgOwCWFXkZ3QJ5SrqpfBLxQXWIbA6GrPQdFKUUABe3Q58V5JpCBXQyup1rDAec -MGQwO5FYSG8wnFfdi64IjS7kp3EPfLzNquHGQiVgPu83+vLPm34Q0lAbP9nrIVSm -ZZw50SKdAgMBAAECggEBAJX3qYDLswdejp/vT0yqRYra0QlMTo2m0738mO1b6t4x -hj8NsQzLVnJ6WMhFLEX7/hb8jWF4W4WNcEgXc+kdgT+WWPc+i/WiJGbF/GoUa3u4 -3xLRqBAd6OeM5brQ1i5jv8h2Y2Ys9DNjcc4Ee5WvctV745W5yVk5El2KS6gSWWlG -P7ZJp8L96rZMhq/jf81S4FJ5cuDVsiUcyeajyV8MR2h74PTPA/sh1S+4zIG2rFnb -tRtIYg5nMmnJuEUQGVIUpfamMiO2G4OTZooyQaHw4MSkBvlmNE0iwnvdcBIKp1rV -WZwBi3urPc7evtmxAsIa4cs1xHxaMhIcI5K21RhyTTUCgYEA1TiVLI2GpYQ/uQKz -prwLtyVyzz6E/7ix1flxpRpw9nVBJBzmB+fySg29265DD/1Mo7omrRec4WASLkcc -IOogI0o+E9KpuJcTFh/lW4OhsjNb7U2CAy2vlDGD3HBBjeZywO6mDdKu6ZLhcZR/ -6zzCBEGWNEfcxe68q5DPw4gHMAsCgYEAzaB8S9EycH+EVZnGXp1diRfg1uNEgXBY -O6F6F++bjfWCCNrLSOj+rzA9J8aGFbVE+VgHgmW+IvmoX90ZA/C9i6ybVsjex1Vs -wx7MEmKC8HNLaUB8xY+EhoO/HVhUu7bpX1byM4ofyo+MJ0lFpeDmVSvgilKcKa/g -0sEuumvaWPcCgYAQggr6ohJ0qiKM3mquVAzMJzgWV47QhaovGNr1n3BzhGyAsUQW -BJ07ooi6g2lpyYCvhHAOIq/guyFtN9AztCy8zrizrDG7uuadZebKCQzx0Qwi+UdB -m73mSVceMArDQ75Kta9hjphAOUD/HDIzMkcLMVDBx3aOVIeC+Pk8+EEmNQKBgElb -O3yAwRHJLBitp0sRsNC7qAaBRWs3/QIo9TmczU2zjBREmckE9fbhqq+J70PHSzf4 -45TtXwtzLVIlukrhk31EspYfSvqpywdA8WSNqFDHkNuXmeuQ66JhVcjOPkxJULAm -gHlKiVTmKQXK6gHnXcR8xroSBowIppgJOvZei6K5AoGBAKYod7uq7V49+SeLR80L -sjeLM6lQTB5tuh0Q+J2rza0XV3xdJqq2hccHOrsYFc9GG5DLcA0uMMVodQq5CvJN -3fR1Bv6GoamjlJpESBJy4j3TrHK2ItitHtCRoIKOEPgQdqn2LxOiQ2eXpBHNrVp6 -TIZvZsA585UpztkokRhgWtkm ------END PRIVATE KEY----- diff --git a/docker/haproxy/haproxy.cfg b/docker/haproxy/haproxy.cfg deleted file mode 100644 index a654294..0000000 --- a/docker/haproxy/haproxy.cfg +++ /dev/null @@ -1,59 +0,0 @@ -# https://www.haproxy.com/documentation/hapee/latest/onepage/#home -global - log stdout format raw local0 - -defaults - log global - option httplog - timeout client 1m - timeout server 1m - timeout connect 10s - timeout http-keep-alive 2m - timeout queue 15s - timeout tunnel 4h # for websocket - -frontend stats - mode http - bind :4001 - stats enable - stats uri / - stats refresh 10s - #stats show-modules - -frontend test_frontend - mode http - bind *:80 - bind *:443 ssl crt /usr/local/etc/haproxy/example.com.pem alpn h2,http/1.1 - unique-id-format %[uuid()] - unique-id-header X-Unique-ID - log-format "%ci:%cp\ [%t]\ %ft\ %b/%s\ %Th/%Ti/%TR/%Tq/%Tw/%Tc/%Tr/%Tt\ %ST\ %B\ %CC\ %CS\ %tsc\ %ac/%fc/%bc/%sc/%rc\ %sq/%bq\ %hr\ %hs\ %{+Q}r\ %ID\ spoa-error:\ %[var(txn.coraza.error)]\ waf-action:\ %[var(txn.coraza.action)]" - - filter spoe engine coraza config /usr/local/etc/haproxy/coraza.cfg - - # Currently haproxy cannot use variables to set the code or deny_status, so this needs to be manually configured here - http-request redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect } - http-response redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect } - - http-request deny deny_status 403 hdr waf-block "request" if { var(txn.coraza.action) -m str deny } - http-response deny deny_status 403 hdr waf-block "response" if { var(txn.coraza.action) -m str deny } - - http-request silent-drop if { var(txn.coraza.action) -m str drop } - http-response silent-drop if { var(txn.coraza.action) -m str drop } - - # Deny in case of an error, when processing with the Coraza SPOA - http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 } - http-response deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 } - - # Deprecated, use action instead of fail - #http-request deny deny_status 401 hdr waf-block "request" if { var(txn.coraza.fail) -m int eq 1 } - #http-response deny deny_status 401 hdr waf-block "response" if { var(txn.coraza.fail) -m int eq 1 } - - use_backend test_backend - -backend test_backend - mode http - server s1 httpbin:8080 - -backend coraza-spoa - mode tcp - server s1 coraza:9000 diff --git a/example/Dockerfile b/example/Dockerfile new file mode 100644 index 0000000..6b1f7dc --- /dev/null +++ b/example/Dockerfile @@ -0,0 +1,26 @@ +# Copyright 2023 The OWASP Coraza contributors +# SPDX-License-Identifier: Apache-2.0 + +FROM golang:1.21 as build + +WORKDIR /go/src/app +COPY . . + +RUN go mod download +RUN go vet -v ./... + +RUN CGO_ENABLED=0 go build -o /go/bin/coraza-spoa + +FROM gcr.io/distroless/static-debian11 + +LABEL org.opencontainers.image.authors="The OWASP Coraza contributors" \ + org.opencontainers.image.description="OWASP Coraza WAF (Haproxy SPOA)" \ + org.opencontainers.image.documentation="https://coraza.io/connectors/coraza-spoa/" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.source="https://github.com/corazawaf/coraza-spoa" \ + org.opencontainers.image.title="coraza-spoa" + +COPY --from=build /go/bin/coraza-spoa / +COPY ./example/coraza-spoa.yaml /config.yaml + +CMD ["/coraza-spoa", "--config", "/config.yaml"] \ No newline at end of file diff --git a/example/coraza-spoa.yaml b/example/coraza-spoa.yaml new file mode 100644 index 0000000..81f51a4 --- /dev/null +++ b/example/coraza-spoa.yaml @@ -0,0 +1,40 @@ +# The SPOA server bind address +bind: 0.0.0.0:9000 + +# The log level configuration, one of: debug/info/warn/error/panic/fatal +log_level: info +# The log file path +log_file: /dev/stdout +# The log format, one of: console/json +log_format: console + +applications: + # name is used as key to identify the directives + - name: sample_app + # Some example rules. + # The built-in OWASP CRS rules are available in @owasp_crs/ + directives: | + Include @coraza.conf-recommended + Include @crs-setup.conf.example + Include @owasp_crs/*.conf + + # HAProxy configured to send requests only, that means no cache required + response_check: false + + # The transaction cache lifetime in milliseconds (60000ms = 60s) + transaction_ttl_ms: 60000 + + # The log level configuration, one of: debug/info/warn/error/panic/fatal + log_level: info + # The log file path + log_file: /dev/stdout + # The log format, one of: console/json + log_format: console + + # The maximum number of transactions which can be cached + transaction_active_limit: 1000 + + # After reaching the maximum number of transactions: + # If true then the new transactions will be rejected with deny and status code 503 + # If false, the new transaction will be processed, ignoring the response + transaction_active_limit_reject: false \ No newline at end of file diff --git a/example/docker-compose.yaml b/example/docker-compose.yaml new file mode 100644 index 0000000..295ba0e --- /dev/null +++ b/example/docker-compose.yaml @@ -0,0 +1,34 @@ +version: "3.9" +services: + httpbin: + image: mccutchen/go-httpbin:v2.13.4 + environment: + - MAX_BODY_SIZE=15728640 # 15 MiB + command: [ "/bin/go-httpbin", "-port", "8081" ] + ports: + - "8081:8081" + + coraza-spoa: + restart: unless-stopped + build: + context: .. + dockerfile: ./example/Dockerfile + ports: + - "9000:9000" + + haproxy: + restart: unless-stopped + image: haproxy:2.7-alpine + ports: [ "8080:80", "8443:443", "8082:8082"] + depends_on: + - httpbin + links: + - "coraza-spoa:coraza-spoa" + - "httpbin:httpbin" + volumes: + - type: bind + source: ./haproxy/ + target: /usr/local/etc/haproxy + environment: + - BACKEND_HOST=httpbin:8081 + - CORAZA_SPOA_HOST=coraza-spoa \ No newline at end of file diff --git a/docker/haproxy/coraza.cfg b/example/haproxy/coraza.cfg similarity index 61% rename from docker/haproxy/coraza.cfg rename to example/haproxy/coraza.cfg index a588091..f5ae467 100644 --- a/docker/haproxy/coraza.cfg +++ b/example/haproxy/coraza.cfg @@ -1,11 +1,10 @@ # https://github.com/haproxy/haproxy/blob/master/doc/SPOE.txt -# /etc/haproxy/coraza.cfg +# /usr/local/etc/haproxy/coraza.cfg [coraza] spoe-agent coraza-agent # Process HTTP requests only (the responses are not evaluated) messages coraza-req # Comment the previous line and add coraza-res, to process responses also. - # NOTE: there are still some memory & caching issues, so use this with care #messages coraza-req coraza-res option var-prefix coraza option set-on-error error @@ -16,11 +15,13 @@ spoe-agent coraza-agent log global spoe-message coraza-req - args app=str(sample_app) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body + # Arguments are required to be in this order + args app=str(sample_app) src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body event on-frontend-http-request spoe-message coraza-res - args app=str(sample_app) id=unique-id version=res.ver status=status headers=res.hdrs body=res.body + # Arguments are required to be in this order + args app=str(sample_app) id=var(txn.e2e.id) version=res.ver status=status headers=res.hdrs body=res.body event on-http-response diff --git a/doc/config/haproxy.cfg b/example/haproxy/haproxy.cfg similarity index 57% rename from doc/config/haproxy.cfg rename to example/haproxy/haproxy.cfg index 6564bd8..952be94 100644 --- a/doc/config/haproxy.cfg +++ b/example/haproxy/haproxy.cfg @@ -1,4 +1,4 @@ -# https://www.haproxy.com/documentation/hapee/latest/onepage/#home +# https://docs.haproxy.org/ global log stdout format raw local0 @@ -6,19 +6,13 @@ defaults log global option httplog timeout client 1m - timeout server 1m - timeout connect 10s - timeout http-keep-alive 2m - timeout queue 15s - timeout tunnel 4h # for websocket + timeout server 1m + timeout connect 10s -frontend test +frontend default mode http bind *:80 - - unique-id-format %[uuid()] - unique-id-header X-Unique-ID - filter spoe engine coraza config /etc/haproxy/coraza.cfg + filter spoe engine coraza config /usr/local/etc/haproxy/coraza.cfg # Currently haproxy cannot use variables to set the code or deny_status, so this needs to be manually configured here http-request redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect } @@ -31,18 +25,18 @@ frontend test http-response silent-drop if { var(txn.coraza.action) -m str drop } # Deny in case of an error, when processing with the Coraza SPOA - http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 } - http-response deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 } + http-request deny deny_status 500 if { var(txn.coraza.error) -m int gt 0 } + http-response deny deny_status 500 if { var(txn.coraza.error) -m int gt 0 } + + use_backend httpbin_backend - use_backend test_backend +resolvers host_dns + parse-resolv-conf -backend test_backend +backend httpbin_backend mode http - http-request return status 200 content-type "text/plain" string "Welcome!\n" + server backend $BACKEND_HOST backend coraza-spoa mode tcp - balance roundrobin - timeout connect 5s # greater than hello timeout - timeout server 3m # greater than idle timeout - server s1 127.0.0.1:9000 \ No newline at end of file + server coraza_spoa coraza-spoa:9000 diff --git a/go.mod b/go.mod index 6f55b4e..295c84a 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,29 @@ module github.com/corazawaf/coraza-spoa -go 1.19 +go 1.21 require ( - github.com/bluele/gcache v0.0.2 - github.com/corazawaf/coraza/v3 v3.0.1 - github.com/criteo/haproxy-spoe-go v1.0.6 + github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc + github.com/corazawaf/coraza/v3 v3.1.0 + github.com/dropmorepackets/haproxy-go v0.0.5-0.20240218203010-94d697923f61 + github.com/jcchavezs/mergefs v0.0.0-20230503083351-07f27d256761 github.com/magefile/mage v1.15.0 - go.uber.org/zap v1.24.0 + github.com/mccutchen/go-httpbin/v2 v2.13.4 + github.com/rs/zerolog v1.32.0 gopkg.in/yaml.v3 v3.0.1 + istio.io/istio v0.0.0-20240218163812-d80ef7b19049 ) require ( - github.com/corazawaf/libinjection-go v0.1.2 // indirect - github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tidwall/gjson v1.14.4 // indirect + github.com/corazawaf/libinjection-go v0.1.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e // indirect + github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.17.0 // indirect rsc.io/binaryregexp v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index f044cfe..09e7316 100644 --- a/go.sum +++ b/go.sum @@ -1,70 +1,76 @@ -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= -github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= -github.com/corazawaf/coraza/v3 v3.0.1 h1:akPpTofIUhabGU1Zbo+YVBZK/HdcxjGy8yXkjoqVFMQ= -github.com/corazawaf/coraza/v3 v3.0.1/go.mod h1:zvldIncYMuW8xmRcOs37OWRhY3CPWBKbTngIGzR5v4Y= -github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM= -github.com/corazawaf/libinjection-go v0.1.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/criteo/haproxy-spoe-go v1.0.6 h1:3GDQ8hm/fIkn4wxxI/pN0OoBfKon4ROzvpU5fIriYII= -github.com/criteo/haproxy-spoe-go v1.0.6/go.mod h1:o04s69MOZ7SvPthMtUt/tfn1hcorQQAS/nwzKPBlXQU= +github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc= +github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU= +github.com/corazawaf/coraza/v3 v3.1.0 h1:CB6YxNXdbZjUJS/0FVFoFvS8eOVFbIvlNuHNC5dh88c= +github.com/corazawaf/coraza/v3 v3.1.0/go.mod h1:S0bhYQfTu1Ew3YKdI37X1WWu6t4En4Tvw28aKyQFJaU= +github.com/corazawaf/libinjection-go v0.1.3 h1:PUplAYho1BBl0tIVbhDsNRuVGIeUYSiCEc9oQpb2rJU= +github.com/corazawaf/libinjection-go v0.1.3/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dropmorepackets/haproxy-go v0.0.5-0.20240218203010-94d697923f61 h1:KSiohgg4l/fKmw9UCK9jDANF99Am3Mcq00YRn0gk6oU= +github.com/dropmorepackets/haproxy-go v0.0.5-0.20240218203010-94d697923f61/go.mod h1:OGwftKhVqRvI1QtonOPCvPHKgDQLLaZpT2aF25ReQ2Q= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jcchavezs/mergefs v0.0.0-20230503083351-07f27d256761 h1:Wbw8wlehLICFiXFPmQjMnYkcC6qH4a0d1iNbp6md+tk= +github.com/jcchavezs/mergefs v0.0.0-20230503083351-07f27d256761/go.mod h1:BGD4X4tm4ZCbtShoISaG4Ama2L3NOq7y6cvuOxbYgzs= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mccutchen/go-httpbin/v2 v2.13.4 h1:KjUeehEAcWG+ce5WJVtP3cyquL0Qe/jQ4UWe/N1BVDw= +github.com/mccutchen/go-httpbin/v2 v2.13.4/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e h1:POJco99aNgosh92lGqmx7L1ei+kCymivB/419SD15PQ= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +istio.io/istio v0.0.0-20240218163812-d80ef7b19049 h1:jR4INLKnkLNgQRNMBjkAt1ctPnuTq+vQ9wlZSOtR1+o= +istio.io/istio v0.0.0-20240218163812-d80ef7b19049/go.mod h1:5ATT2TaGbT/L1SwCYvs2ArNeLxHkPKwhvT7r3TPMu6M= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/go.work b/go.work new file mode 100644 index 0000000..96825b1 --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.21.5 + +use . diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..515e014 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,474 @@ +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= +cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/monitoring v1.17.0 h1:blrdvF0MkPPivSO041ihul7rFMhXdVp8Uq7F59DKXTU= +cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= +cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYvzJc= +cloud.google.com/go/security v1.15.4/go.mod h1:oN7C2uIZKhxCLiAAijKUCuHLZbIt/ghYEo8MqwD/Ty4= +cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= +cloud.google.com/go/trace v1.10.4/go.mod h1:Nso99EDIK8Mj5/zmB+iGr9dosS/bzWCJ8wGmE6TXNWY= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/alecholmes/xfccparser v0.1.0 h1:/PBnzDBxfHJ66AinLNglzZH4oWLrc1/QTKlSoNNnei8= +github.com/alecholmes/xfccparser v0.1.0/go.mod h1:c1S35dudNR5aZ4Vf9zKCrEwC8iqwF4TcDAbU+RXQ5yY= +github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= +github.com/alecthomas/participle v0.7.1/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df h1:YWiVl53v0R8Knj/k+4slO0SXPL67Y4dXWiOIWNzrkew= +github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df/go.mod h1:7jguE759ADzy2EkxGRXigiC0ER1Yq2IFk2qNtwgzc7U= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= +github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ= +github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= +github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= +github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ= +github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= +github.com/containernetworking/plugins v1.4.0 h1:+w22VPYgk7nQHw7KT92lsRmuToHvb7wwSv9iTbXzzic= +github.com/containernetworking/plugins v1.4.0/go.mod h1:UYhcOyjefnrQvKvmmyEKsUA+M9Nfn7tqULPpH0Pkcj0= +github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= +github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= +github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= +github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= +github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.12.1-0.20240217204037-bc093a22968f h1:q224Q8f6DbMDTV0SJV8GwAXnjRY8OT058BaWPyX8CoQ= +github.com/envoyproxy/go-control-plane v0.12.1-0.20240217204037-bc093a22968f/go.mod h1:lFu6itz1hckLR2A3aJ+ZKf3lu8HpjTsJSsqvVF6GL6g= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/florianl/go-nflog/v2 v2.0.1 h1:8csWIqFQ2vDaZJkxezY3dXDB7bEFg0VRFsYd2Bzj3II= +github.com/florianl/go-nflog/v2 v2.0.1/go.mod h1:g+SOgM/SuePn9bvS/eo3Ild7J71nSb29OzbxR+7cln0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic= +github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240125082051-42cd04596328 h1:oI+lCI2DY1BsRrdzMJBhIMxBBdlZJl31YNQC11EiyvA= +github.com/google/pprof v0.0.0-20240125082051-42cd04596328/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= +github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/howardjohn/unshare-go v0.3.0 h1:ZxNO46MubN+TVgA8PGFzhIv05aKjgaHi9tKXYnmWrtM= +github.com/howardjohn/unshare-go v0.3.0/go.mod h1:cJjyFAN6qTA70ovC2VR23iAZuJ8X3J/ibAbT693pJ8g= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v1.0.0 h1:eeMi54M/Un/I29qQxlZWKN871R4jD61TQJOEpo9pZrI= +github.com/imdario/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA= +github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/buildkit v0.12.5 h1:RNHH1l3HDhYyZafr5EgstEu8aGNCwyfvMtrQDtjH9T0= +github.com/moby/buildkit v0.12.5/go.mod h1:YGwjA2loqyiYfZeEo8FtI7z4x5XponAaIWsWcSjWwso= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/openshift/api v0.0.0-20240125191952-1e2afa0f76cf h1:oAjm2e/Xjcdc0mpzAccQCQI39zuwx8VDLlJftqTRq0M= +github.com/openshift/api v0.0.0-20240125191952-1e2afa0f76cf/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= +github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA= +github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/prometheus v0.49.1 h1:90mDvjrFnca2m+0qPSIDr3y7iHPTAagOAElz7j+HtGk= +github.com/prometheus/prometheus v0.49.1/go.mod h1:aDogiyqmv3aBIWDb5z5Sdcxuuf2BOfiJwOIm9JGpMnI= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k= +github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= +github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +go.etcd.io/etcd/client/pkg/v3 v3.5.11 h1:bT2xVspdiCj2910T0V+/KHcVKjkUrCZVtk8J2JF2z1A= +go.etcd.io/etcd/client/pkg/v3 v3.5.11/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= +go.etcd.io/etcd/client/v3 v3.5.11 h1:ajWtgoNSZJ1gmS8k+icvPtqsqEav+iUorF7b0qozgUU= +go.etcd.io/etcd/client/v3 v3.5.11/go.mod h1:a6xQUEqFJ8vztO1agJh/KQKOMfFI8og52ZconzcDJwE= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/exporters/prometheus v0.45.0 h1:BeIK2KGho0oCWa7LxEGSqfDZbs7Fpv/Viz+FS4P8CXE= +go.opentelemetry.io/otel/exporters/prometheus v0.45.0/go.mod h1:UVJZPLnfDSvHj+eJuZE+E1GjIBD267mEMfAAHJdghWg= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/sdk/metric v1.22.0 h1:ARrRetm1HCVxq0cbnaZQlfwODYJHo3gFL8Z3tSmHBcI= +go.opentelemetry.io/otel/sdk/metric v1.22.0/go.mod h1:KjQGeMIDlBNEOo6HvjhxIec1p/69/kULDcp4gr0oLQQ= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.starlark.net v0.0.0-20231121155337-90ade8b19d09 h1:hzy3LFnSN8kuQK8h9tHl4ndF6UruMj47OqwqsS+/Ai4= +go.starlark.net v0.0.0-20231121155337-90ade8b19d09/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.160.0 h1:SEspjXHVqE1m5a1fRy8JFB+5jSu+V0GEDKDghF3ttO4= +google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.14.1 h1:4AwRLx+wfzlPtvrsbDmWP5PUokGmf9/nAmEdk21vae8= +helm.sh/helm/v3 v3.14.1/go.mod h1:2itvvDv2WSZXTllknfQo6j7u3VVgMAvm8POCDgYH424= +istio.io/api v1.19.0-alpha.1.0.20240215183341-501a12d4b423 h1:ABYi7hDG8ViztiurK05YdtpK7hJos2xcHsmbHrINOvU= +istio.io/api v1.19.0-alpha.1.0.20240215183341-501a12d4b423/go.mod h1:TFCMUCAHRjxBv1CsIsFCsYHPHi4axVI4vdIzVr8eFjY= +istio.io/client-go v1.19.0-alpha.1.0.20240215183941-836bbbbf4a21 h1:z5Rkv9L3nuPk+smTMzYdJBWPRzT2OxxctmR855SX8IM= +istio.io/client-go v1.19.0-alpha.1.0.20240215183941-836bbbbf4a21/go.mod h1:1VeWFH0Vxom4Q+Oo+s2TNs46N7TYCwUXJNCE34fg7qI= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apiextensions-apiserver v0.29.1 h1:S9xOtyk9M3Sk1tIpQMu9wXHm5O2MX6Y1kIpPMimZBZw= +k8s.io/apiextensions-apiserver v0.29.1/go.mod h1:zZECpujY5yTW58co8V2EQR4BD6A9pktVgHhvc0uLfeU= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/apiserver v0.29.1 h1:e2wwHUfEmMsa8+cuft8MT56+16EONIEK8A/gpBSco+g= +k8s.io/apiserver v0.29.1/go.mod h1:V0EpkTRrJymyVT3M49we8uh2RvXf7fWC5XLB0P3SwRw= +k8s.io/cli-runtime v0.29.1 h1:By3WVOlEWYfyxhGko0f/IuAOLQcbBSMzwSaDren2JUs= +k8s.io/cli-runtime v0.29.1/go.mod h1:vjEY9slFp8j8UoMhV5AlO8uulX9xk6ogfIesHobyBDU= +k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= +k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= +k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw= +k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240126223410-2919ad4fcfec h1:iGTel2aR8vCZdxJDgmbeY0zrlXy9Qcvyw4R2sB4HLrA= +k8s.io/kube-openapi v0.0.0-20240126223410-2919ad4fcfec/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/kubectl v0.29.1 h1:rWnW3hi/rEUvvg7jp4iYB68qW5un/urKbv7fu3Vj0/s= +k8s.io/kubectl v0.29.1/go.mod h1:SZzvLqtuOJYSvZzPZR9weSuP0wDQ+N37CENJf0FhDF4= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/gateway-api v1.0.1-0.20240207183254-df978ef82cb6 h1:dtTUm5ryngzSwRwWFiFg6rIY2sUbhYr60+NE7lpiyG0= +sigs.k8s.io/gateway-api v1.0.1-0.20240207183254-df978ef82cb6/go.mod h1:VwoGm4hodIx6BmBnyomgERP2g9UT/miM2L6F9T4C2yA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= +sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0= +sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4= +sigs.k8s.io/mcs-api v0.1.0 h1:edDbg0oRGfXw8TmZjKYep06LcJLv/qcYLidejnUp0PM= +sigs.k8s.io/mcs-api v0.1.0/go.mod h1:gGiAryeFNB4GBsq2LBmVqSgKoobLxt+p7ii/WG5QYYw= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/agent.go b/internal/agent.go new file mode 100644 index 0000000..b0783e3 --- /dev/null +++ b/internal/agent.go @@ -0,0 +1,96 @@ +package internal + +import ( + "context" + "errors" + "net" + "sync" + + "github.com/dropmorepackets/haproxy-go/pkg/encoding" + "github.com/dropmorepackets/haproxy-go/spop" + "github.com/rs/zerolog" +) + +type Agent struct { + Context context.Context + Applications map[string]*Application + Logger zerolog.Logger + + mtx sync.RWMutex +} + +func (a *Agent) Serve(l net.Listener) error { + agent := spop.Agent{ + Handler: a, + BaseContext: a.Context, + } + + return agent.Serve(l) +} + +func (a *Agent) ReplaceApplications(newApps map[string]*Application) { + a.mtx.Lock() + a.Applications = newApps + a.mtx.Unlock() +} + +func (a *Agent) HandleSPOE(ctx context.Context, writer *encoding.ActionWriter, message *encoding.Message) { + const ( + messageCorazaRequest = "coraza-req" + messageCorazaResponse = "coraza-res" + ) + + var messageHandler func(*Application, context.Context, *encoding.ActionWriter, *encoding.Message) error + switch name := string(message.NameBytes()); name { + case messageCorazaRequest: + messageHandler = (*Application).HandleRequest + case messageCorazaResponse: + messageHandler = (*Application).HandleResponse + default: + a.Logger.Debug().Str("message", name).Msg("unknown spoe message") + return + } + + k := encoding.AcquireKVEntry() + defer encoding.ReleaseKVEntry(k) + if !message.KV.Next(k) { + a.Logger.Panic().Msg("failed reading kv entry") + return + } + + appName := string(k.ValueBytes()) + if !k.NameEquals("app") { + // Without knowing the app, we cannot continue. We could fall back to a default application, + // but all following code would have to support that as we now already read one of the kv entries. + a.Logger.Panic().Str("expected", "app").Str("got", appName).Msg("unexpected kv entry") + return + } + + a.mtx.RLock() + app := a.Applications[appName] + a.mtx.RUnlock() + if app == nil { + // If we cannot resolve the app, we fail as this is an invalid configuration. + a.Logger.Panic().Str("app", appName).Msg("app not found") + return + } + + err := messageHandler(app, ctx, writer, message) + if err == nil { + return + } + + var interruption ErrInterrupted + if err != nil && errors.As(err, &interruption) { + _ = writer.SetInt64(encoding.VarScopeTransaction, "status", int64(interruption.Interruption.Status)) + _ = writer.SetString(encoding.VarScopeTransaction, "action", interruption.Interruption.Action) + _ = writer.SetString(encoding.VarScopeTransaction, "data", interruption.Interruption.Data) + _ = writer.SetInt64(encoding.VarScopeTransaction, "ruleid", int64(interruption.Interruption.RuleID)) + + a.Logger.Debug().Err(err).Msg("sending interruption") + return + } + + // If the error is not an ErrInterrupted, we panic to let the spop stream fail. + a.Logger.Panic().Err(err).Msg("Error handling request") +} diff --git a/internal/application.go b/internal/application.go new file mode 100644 index 0000000..4053a7b --- /dev/null +++ b/internal/application.go @@ -0,0 +1,337 @@ +package internal + +import ( + "bufio" + "bytes" + "context" + "fmt" + "math/rand" + "net/netip" + "strings" + "time" + + coreruleset "github.com/corazawaf/coraza-coreruleset" + "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/types" + "github.com/dropmorepackets/haproxy-go/pkg/encoding" + "github.com/jcchavezs/mergefs" + "github.com/jcchavezs/mergefs/io" + "github.com/rs/zerolog" + "istio.io/istio/pkg/cache" +) + +type AppConfig struct { + Directives string + ResponseCheck bool + Logger zerolog.Logger + TransactionTTLMS time.Duration +} + +type Application struct { + waf coraza.WAF + cache cache.ExpiringCache + + AppConfig +} + +type applicationRequest struct { + SrcIp netip.Addr + SrcPort int64 + DstIp netip.Addr + DstPort int64 + Method string + Path []byte + Query []byte + Version string + Headers []byte + Body []byte +} + +func (a *Application) HandleRequest(ctx context.Context, writer *encoding.ActionWriter, message *encoding.Message) error { + k := encoding.AcquireKVEntry() + defer encoding.ReleaseKVEntry(k) + + var req applicationRequest + for message.KV.Next(k) { + switch name := string(k.NameBytes()); name { + case "src-ip": + req.SrcIp = k.ValueAddr() + case "src-port": + req.SrcPort = k.ValueInt() + case "dst-ip": + req.DstIp = k.ValueAddr() + case "dst-port": + req.DstPort = k.ValueInt() + case "method": + req.Method = string(k.ValueBytes()) + case "path": + // make a copy of the pointer and add a defer in case there is another entry + currK := k + defer encoding.ReleaseKVEntry(currK) + + req.Path = currK.ValueBytes() + + // acquire a new kv entry to continue reading other message values. + k = encoding.AcquireKVEntry() + case "query": + // make a copy of the pointer and add a defer in case there is another entry + currK := k + defer encoding.ReleaseKVEntry(currK) + + req.Query = currK.ValueBytes() + // acquire a new kv entry to continue reading other message values. + k = encoding.AcquireKVEntry() + case "version": + req.Version = string(k.ValueBytes()) + case "headers": + // make a copy of the pointer and add a defer in case there is another entry + currK := k + defer encoding.ReleaseKVEntry(currK) + + req.Headers = currK.ValueBytes() + // acquire a new kv entry to continue reading other message values. + k = encoding.AcquireKVEntry() + case "body": + // make a copy of the pointer and add a defer in case there is another entry + currK := k + defer encoding.ReleaseKVEntry(currK) + + req.Body = currK.ValueBytes() + // acquire a new kv entry to continue reading other message values. + k = encoding.AcquireKVEntry() + default: + a.Logger.Debug().Str("name", name).Msg("unknown kv entry") + } + } + + const idLength = 16 + var sb strings.Builder + sb.Grow(idLength) + for i := 0; i < idLength; i++ { + sb.WriteRune(rune('A' + rand.Intn(26))) + } + + tx := a.waf.NewTransactionWithID(sb.String()) + // write transaction as early as possible to prevent cache misses + a.cache.SetWithExpiration(tx.ID(), tx, a.TransactionTTLMS*time.Millisecond) + if err := writer.SetString(encoding.VarScopeTransaction, "id", tx.ID()); err != nil { + return err + } + + tx.ProcessConnection(req.SrcIp.String(), int(req.SrcPort), req.DstIp.String(), int(req.DstPort)) + + url := strings.Builder{} + url.Write(req.Path) + if req.Query != nil { + url.WriteString("?") + url.Write(req.Query) + } + + tx.ProcessURI(url.String(), req.Method, "HTTP/"+req.Version) + + if err := readHeaders(req.Headers, tx.AddRequestHeader); err != nil { + return fmt.Errorf("reading headers: %v", err) + } + + if it := tx.ProcessRequestHeaders(); it != nil { + return ErrInterrupted{it} + } + + switch it, _, err := tx.WriteRequestBody(req.Body); { + case err != nil: + return err + case it != nil: + return ErrInterrupted{it} + } + + switch it, err := tx.ProcessRequestBody(); { + case err != nil: + return err + case it != nil: + return ErrInterrupted{it} + } + + //TODO: add request logging? + + if a.ResponseCheck { + return nil + } + + tx.ProcessLogging() + return tx.Close() +} + +func readHeaders(headers []byte, callback func(key string, value string)) error { + s := bufio.NewScanner(bytes.NewReader(headers)) + for s.Scan() { + line := bytes.TrimSpace(s.Bytes()) + if len(line) == 0 { + continue + } + + kv := bytes.SplitN(line, []byte(":"), 2) + if len(kv) != 2 { + return fmt.Errorf("invalid header: %q", s.Text()) + } + + key, value := bytes.TrimSpace(kv[0]), bytes.TrimSpace(kv[1]) + + callback(string(key), string(value)) + } + + return nil +} + +type applicationResponse struct { + ID string + Version string + Status int64 + Headers []byte + Body []byte +} + +func (a *Application) HandleResponse(ctx context.Context, writer *encoding.ActionWriter, message *encoding.Message) error { + if !a.ResponseCheck { + return fmt.Errorf("got response but response check is disabled") + } + + k := encoding.AcquireKVEntry() + defer encoding.ReleaseKVEntry(k) + + var res applicationResponse + for message.KV.Next(k) { + switch name := string(k.NameBytes()); name { + case "id": + res.ID = string(k.ValueBytes()) + case "version": + res.Version = string(k.ValueBytes()) + case "status": + res.Status = k.ValueInt() + case "headers": + // make a copy of the pointer and add a defer in case there is another entry + currK := k + defer encoding.ReleaseKVEntry(currK) + + res.Headers = currK.ValueBytes() + // acquire a new kv entry to continue reading other message values. + k = encoding.AcquireKVEntry() + case "body": + // make a copy of the pointer and add a defer in case there is another entry + currK := k + defer encoding.ReleaseKVEntry(currK) + + res.Body = currK.ValueBytes() + // acquire a new kv entry to continue reading other message values. + k = encoding.AcquireKVEntry() + default: + a.Logger.Debug().Str("name", name).Msg("unknown kv entry") + } + } + + if res.ID == "" { + return fmt.Errorf("response id is empty") + } + + cv, ok := a.cache.Get(res.ID) + if !ok { + return fmt.Errorf("transaction %q not found", res.ID) + } + // TODO does remove forces eviction? + defer a.cache.Remove(res.ID) + + tx := cv.(types.Transaction) + + if err := readHeaders(res.Headers, tx.AddResponseHeader); err != nil { + return fmt.Errorf("reading headers: %v", err) + } + + if it := tx.ProcessResponseHeaders(int(res.Status), "HTTP/"+res.Version); it != nil { + return ErrInterrupted{it} + } + + switch it, _, err := tx.WriteResponseBody(res.Body); { + case err != nil: + return err + case it != nil: + return ErrInterrupted{it} + } + + switch it, err := tx.ProcessResponseBody(); { + case err != nil: + return err + case it != nil: + return ErrInterrupted{it} + } + + tx.ProcessLogging() + return tx.Close() +} + +func (a AppConfig) NewApplication() (*Application, error) { + app := Application{ + AppConfig: a, + } + + config := coraza.NewWAFConfig(). + WithDirectives(a.Directives). + WithErrorCallback(app.logCallback). + WithRootFS(mergefs.Merge(coreruleset.FS, io.OSFS)) + + waf, err := coraza.NewWAF(config) + if err != nil { + return nil, err + } + app.waf = waf + + const defaultExpire = time.Second * 10 + const defaultEvictionInterval = time.Second * 1 + + app.cache = cache.NewTTLWithCallback(defaultExpire, defaultEvictionInterval, func(key, value any) { + // everytime a transaction runs into a timeout it gets closed. + tx, ok := value.(types.Transaction) + if !ok { + return + } + + // Process Logging won't do anything if TX was already logged. + tx.ProcessLogging() + if err := tx.Close(); err != nil { + a.Logger.Error().Err(err).Str("tx", tx.ID()).Msg("error closing transaction") + } + }) + + return &app, nil +} + +func (a *Application) logCallback(mr types.MatchedRule) { + var l *zerolog.Event + + switch mr.Rule().Severity() { + case types.RuleSeverityWarning: + l = a.Logger.Warn() + case types.RuleSeverityNotice, + types.RuleSeverityInfo: + l = a.Logger.Info() + case types.RuleSeverityDebug: + l = a.Logger.Debug() + default: + l = a.Logger.Error() + } + l.Msg(mr.ErrorLog()) +} + +type ErrInterrupted struct { + Interruption *types.Interruption +} + +func (e ErrInterrupted) Error() string { + return fmt.Sprintf("interrupted with status %d and action %s", e.Interruption.Status, e.Interruption.Action) +} + +func (e ErrInterrupted) Is(target error) bool { + t, ok := target.(*ErrInterrupted) + if !ok { + return false + } + return e.Interruption == t.Interruption +} diff --git a/internal/e2e_test.go b/internal/e2e_test.go new file mode 100644 index 0000000..2249920 --- /dev/null +++ b/internal/e2e_test.go @@ -0,0 +1,136 @@ +//go:build e2e + +package internal + +import ( + "context" + "fmt" + "net/http/httptest" + "os" + "testing" + + "github.com/corazawaf/coraza/v3/http/e2e" + "github.com/mccutchen/go-httpbin/v2/httpbin" + "github.com/rs/zerolog" + + "github.com/dropmorepackets/haproxy-go/pkg/testutil" +) + +const directives = ` + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType application/json + # Custom rule for Coraza config check (ensuring that these configs are used) + SecRule &REQUEST_HEADERS:coraza-e2e "@eq 0" "id:100,phase:1,deny,status:424,log,msg:'Coraza E2E - Missing header'" + # Custom rules for e2e testing + SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,t:lowercase,log,deny" + SecRule REQUEST_BODY "@rx maliciouspayload" "id:102,phase:2,t:lowercase,log,deny" + SecRule RESPONSE_HEADERS:pass "@rx leak" "id:103,phase:3,t:lowercase,log,deny" + SecRule RESPONSE_BODY "@contains responsebodycode" "id:104,phase:4,t:lowercase,log,deny" + # Custom rules mimicking the following CRS rules: 941100, 942100, 913100 + SecRule ARGS_NAMES|ARGS "@detectXSS" "id:9411,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls,log,deny" + SecRule ARGS_NAMES|ARGS "@detectSQLi" "id:9421,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:removeNulls,multiMatch,log,deny" + SecRule REQUEST_HEADERS:User-Agent "@pm grabber masscan" "id:9131,phase:1,t:none,log,deny" +` + +func TestE2E(t *testing.T) { + t.Parallel() + t.Run("coraza e2e suite", withCoraza(t, func(t *testing.T, config testutil.HAProxyConfig, bin string) { + err := e2e.Run(e2e.Config{ + NulledBody: false, + ProxiedEntrypoint: "http://127.0.0.1:" + config.FrontendPort, + HttpbinEntrypoint: bin, + }) + if err != nil { + t.Fatalf("e2e tests failed: %v", err) + } + })) +} + +func withCoraza(t *testing.T, f func(*testing.T, testutil.HAProxyConfig, string)) func(t *testing.T) { + s := httptest.NewServer(httpbin.New()) + t.Cleanup(s.Close) + + logger := zerolog.New(os.Stderr) + + appCfg := AppConfig{ + Directives: directives, + ResponseCheck: true, + Logger: logger, + TransactionTTLMS: 10000, + } + + application, err := appCfg.NewApplication() + if err != nil { + t.Fatal(err) + } + + a := Agent{ + Context: context.Background(), + Applications: map[string]*Application{ + "default": application, + }, + Logger: logger, + } + + // create the listener synchronously to prevent a race + l := testutil.TCPListener(t) + // ignore errors as the listener will be closed by t.Cleanup + go a.Serve(l) + + cfg := testutil.HAProxyConfig{ + EngineAddr: l.Addr().String(), + FrontendPort: fmt.Sprintf("%d", testutil.TCPPort(t)), + CustomFrontendConfig: ` + # Currently haproxy cannot use variables to set the code or deny_status, so this needs to be manually configured here + http-request redirect code 302 location %[var(txn.e2e.data)] if { var(txn.e2e.action) -m str redirect } + http-response redirect code 302 location %[var(txn.e2e.data)] if { var(txn.e2e.action) -m str redirect } + + acl is_deny var(txn.e2e.action) -m str deny + acl status_424 var(txn.e2e.status) -m int 424 + + # Special check for e2e tests as they validate the config. + http-request deny deny_status 424 hdr waf-block "request" if is_deny status_424 + http-response deny deny_status 424 hdr waf-block "response" if is_deny status_424 + + http-request deny deny_status 403 hdr waf-block "request" if is_deny + http-response deny deny_status 403 hdr waf-block "response" if is_deny + + http-request silent-drop if { var(txn.e2e.action) -m str drop } + http-response silent-drop if { var(txn.e2e.action) -m str drop } + + # Deny in case of an error, when processing with the Coraza SPOA + http-request deny deny_status 504 if { var(txn.e2e.error) -m int gt 0 } + http-response deny deny_status 504 if { var(txn.e2e.error) -m int gt 0 } +`, + EngineConfig: ` +[e2e] +spoe-agent e2e + messages coraza-req coraza-res + option var-prefix e2e + option set-on-error error + timeout hello 2s + timeout idle 2m + timeout processing 500ms + use-backend e2e-spoa + log global + +spoe-message coraza-req + args app=str(default) src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body + event on-frontend-http-request + +spoe-message coraza-res + args app=str(default) id=var(txn.e2e.id) version=res.ver status=status headers=res.hdrs body=res.body + event on-http-response +`, + BackendConfig: fmt.Sprintf(` +mode http +server httpbin %s +`, s.Listener.Addr().String()), + } + + return testutil.WithHAProxy(cfg, func(t *testing.T) { + f(t, cfg, s.URL) + }) +} diff --git a/internal/message.go b/internal/message.go deleted file mode 100644 index 549375e..0000000 --- a/internal/message.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "fmt" - "net" - - spoe "github.com/criteo/haproxy-spoe-go" -) - -type message struct { - msg *spoe.Message - args map[string]interface{} -} - -func NewMessage(msg *spoe.Message) (*message, error) { - message := message{ - msg: msg, - args: make(map[string]interface{}, msg.Args.Count()), - } - return &message, nil -} - -func (m *message) findArg(name string) (interface{}, error) { - argVal, exist := m.args[name] - if exist { - return argVal, nil - } - - ai := m.msg.Args - for ai.Next() { - m.args[ai.Arg.Name] = ai.Arg.Value - if ai.Arg.Name == name { - return ai.Arg.Value, nil - } - } - - return nil, &ArgNotFoundError{name} -} - -func (m *message) getStringArg(name string) (string, error) { - argVal, err := m.findArg(name) - if err != nil { - return "", err - } - if argVal == nil { - return "", nil - } - val, ok := argVal.(string) - if !ok { - return "", &typeMismatchError{name, "string", argVal} - } - return val, nil -} - -func (m *message) getIntArg(name string) (int, error) { - argVal, err := m.findArg(name) - if err != nil { - return 0, err - } - if argVal == nil { - return 0, nil - } - val, ok := argVal.(int) - if !ok { - return 0, &typeMismatchError{name, "int", argVal} - } - return val, nil -} - -func (m *message) getByteArrayArg(name string) ([]byte, error) { - argVal, err := m.findArg(name) - if err != nil { - return nil, err - } - if argVal == nil { - return nil, nil - } - val, ok := argVal.([]byte) - if !ok { - return nil, &typeMismatchError{name, "[]byte", argVal} - } - return val, nil -} - -func (m *message) getIpArg(name string) (net.IP, error) { - argVal, err := m.findArg(name) - if err != nil { - return nil, err - } - if argVal == nil { - return nil, nil - } - val, ok := argVal.(net.IP) - if !ok { - return nil, &typeMismatchError{name, "net.IP", argVal} - } - return val, nil -} - -type ArgNotFoundError struct { - argName string -} - -func (e *ArgNotFoundError) Error() string { - return fmt.Sprintf("Argument '%s' not found", e.argName) -} - -type typeMismatchError struct { - key string - expectedType string - actualValue interface{} -} - -func (e *typeMismatchError) Error() string { - return fmt.Sprintf("Invalid argument for %s, %s expected, got %T", e.key, e.expectedType, e.actualValue) -} diff --git a/internal/request.go b/internal/request.go deleted file mode 100644 index d4b91fd..0000000 --- a/internal/request.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "fmt" - "net" - - spoe "github.com/criteo/haproxy-spoe-go" -) - -type request struct { - msg *message - app string - id string - srcIp net.IP - srcPort int - dstIp net.IP - dstPort int - method string - path string - query string - version string - headers string - body []byte -} - -func NewRequest(spoeMsg *spoe.Message) (*request, error) { - msg, err := NewMessage(spoeMsg) - if err != nil { - return nil, err - } - - request := request{} - request.msg = msg - - request.app, err = msg.getStringArg("app") - if err != nil { - return nil, err - } - - request.id, err = request.msg.getStringArg("id") - if err != nil { - return nil, err - } - - return &request, nil -} - -func (req *request) init() error { - var err error - - req.srcIp, err = req.msg.getIpArg("src-ip") - if err != nil { - return err - } - - req.srcPort, err = req.msg.getIntArg("src-port") - if err != nil { - return err - } - - req.dstIp, err = req.msg.getIpArg("dst-ip") - if err != nil { - return err - } - - req.dstPort, err = req.msg.getIntArg("dst-port") - if err != nil { - return err - } - - req.method, err = req.msg.getStringArg("method") - if err != nil { - return err - } - - req.path, err = req.msg.getStringArg("path") - if err != nil { - fmt.Println(err.Error()) - } - - req.query, err = req.msg.getStringArg("query") - if err != nil { - fmt.Println(err.Error()) - } - - req.version, err = req.msg.getStringArg("version") - if err != nil { - fmt.Println(err.Error()) - } - - req.headers, err = req.msg.getStringArg("headers") - if err != nil { - fmt.Println(err.Error()) - } - - req.body, _ = req.msg.getByteArrayArg("body") - - return nil -} diff --git a/internal/response.go b/internal/response.go deleted file mode 100644 index 79dc162..0000000 --- a/internal/response.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "fmt" - - spoe "github.com/criteo/haproxy-spoe-go" -) - -type response struct { - msg *message - app string - id string - version string - status int - headers string - body []byte -} - -func NewResponse(spoeMsg *spoe.Message) (*response, error) { - msg, err := NewMessage(spoeMsg) - if err != nil { - return nil, err - } - - response := response{} - response.msg = msg - - response.app, err = msg.getStringArg("app") - if err != nil { - return nil, err - } - - response.id, err = response.msg.getStringArg("id") - if err != nil { - return nil, err - } - - return &response, nil -} - -func (resp *response) init() error { - var err error - - resp.version, err = resp.msg.getStringArg("version") - if err != nil { - fmt.Println(err.Error()) - } - - resp.status, err = resp.msg.getIntArg("status") - if err != nil { - return err - } - - resp.headers, err = resp.msg.getStringArg("headers") - if err != nil { - fmt.Println(err.Error()) - } - - resp.body, _ = resp.msg.getByteArrayArg("body") - - return nil -} diff --git a/internal/spoa.go b/internal/spoa.go deleted file mode 100644 index 841758f..0000000 --- a/internal/spoa.go +++ /dev/null @@ -1,406 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "fmt" - "net/http" - "os" - "strings" - "time" - "io" - - "github.com/bluele/gcache" - "github.com/corazawaf/coraza-spoa/config" - "github.com/corazawaf/coraza/v3" - "github.com/corazawaf/coraza/v3/types" - spoe "github.com/criteo/haproxy-spoe-go" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -// TODO - in coraza v3 ErrorLogCallback is currently in the internal package -type ErrorLogCallback = func(rule types.MatchedRule) - -type application struct { - name string - cfg *config.Application - waf coraza.WAF - cache gcache.Cache - logger *zap.Logger -} - -// SPOA store the relevant data for starting SPOA. -type SPOA struct { - applications map[string]*application - defaultApplication string -} - -// Start starts the SPOA to detect the security risks. -func (s *SPOA) Start(bind string) error { - // s.logger.Info("Starting SPOA") - - agent := spoe.New(func(messages *spoe.MessageIterator) ([]spoe.Action, error) { - for messages.Next() { - msg := messages.Message - - switch msg.Name { - case "coraza-req": - return s.processRequest(&msg) - case "coraza-res": - return s.processResponse(&msg) - } - } - return nil, nil - }) - defer s.cleanApplications() - if err := agent.ListenAndServe(bind); err != nil { - return err - } - return nil -} - -func (s *SPOA) processInterruption(it *types.Interruption) []spoe.Action { - return []spoe.Action{ - spoe.ActionSetVar{ - Name: "status", - Scope: spoe.VarScopeTransaction, - Value: it.Status, - }, - spoe.ActionSetVar{ - Name: "action", - Scope: spoe.VarScopeTransaction, - Value: it.Action, - }, - spoe.ActionSetVar{ - Name: "data", - Scope: spoe.VarScopeTransaction, - Value: it.Data, - }, - spoe.ActionSetVar{ - Name: "ruleid", - Scope: spoe.VarScopeTransaction, - Value: it.RuleID, - }, - } -} - -func (s *SPOA) allowAction() []spoe.Action { - act := []spoe.Action{ - spoe.ActionSetVar{ - Name: "action", - Scope: spoe.VarScopeTransaction, - Value: "allow", - }, - } - return act -} - -func (s *SPOA) readHeaders(headers string) (http.Header, error) { - h := http.Header{} - hs := strings.Split(headers, "\r\n") - - for _, header := range hs { - if header == "" { - continue - } - - kv := strings.SplitN(header, ":", 2) - if len(kv) != 2 { - return nil, fmt.Errorf("invalid header: %q", header) - } - - h.Add(strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])) - } - return h, nil -} - -func (s *SPOA) cleanApplications() { - for _, app := range s.applications { - if err := app.logger.Sync(); err != nil { - app.logger.Error("failed to sync logger", zap.Error(err)) - } - } -} - -func logError(logger *zap.Logger) ErrorLogCallback { - return func(mr types.MatchedRule) { - data := mr.ErrorLog() - switch mr.Rule().Severity() { - case types.RuleSeverityEmergency: - logger.Error(data) - case types.RuleSeverityAlert: - logger.Error(data) - case types.RuleSeverityCritical: - logger.Error(data) - case types.RuleSeverityError: - logger.Error(data) - case types.RuleSeverityWarning: - logger.Warn(data) - case types.RuleSeverityNotice: - logger.Info(data) - case types.RuleSeverityInfo: - logger.Info(data) - case types.RuleSeverityDebug: - logger.Debug(data) - } - } -} - -// New Create a new SPOA instance. -func New(conf *config.Config) (*SPOA, error) { - apps := make(map[string]*application) - for name, cfg := range conf.Applications { - pe := zap.NewProductionEncoderConfig() - - fileEncoder := zapcore.NewJSONEncoder(pe) - - pe.EncodeTime = zapcore.ISO8601TimeEncoder - - level, err := zapcore.ParseLevel(cfg.LogLevel) - if err != nil { - level = zap.InfoLevel - } - - var f io.Writer - switch cfg.LogFile { - case "/dev/stdout": - f = os.Stdout - case "/dev/stderr": - f = os.Stderr - default: - ff, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err != nil { - return nil, err - } - f = ff - } - - core := zapcore.NewTee( - zapcore.NewCore(fileEncoder, zapcore.AddSync(f), level), - ) - - logger := zap.New(core) - - conf := coraza.NewWAFConfig(). - WithDirectives(cfg.Directives). - WithErrorCallback(logError(logger)) - - //nolint:staticcheck // https://github.com/golangci/golangci-lint/issues/741 - if len(cfg.Rules) > 0 { - // Deprecated: this will soon be removed - logger.Warn("'rules' directive in configuration is deprecated and will be removed soon, use 'directives' instead") - conf = conf.WithDirectives(strings.Join(cfg.Rules, "\n")) - } - - waf, err := coraza.NewWAF(conf) - if err != nil { - logger.Error("unable to create waf instance", zap.String("app", name), zap.Error(err)) - return nil, err - } - - app := &application{ - name: name, - cfg: cfg, - waf: waf, - logger: logger, - } - - app.cache = gcache.New(app.cfg.TransactionActiveLimit). - EvictedFunc(func(key, value interface{}) { - // everytime a transaction is timedout we clean it - tx, ok := value.(types.Transaction) - if !ok { - return - } - // Process Logging won't do anything if TX was already logged. - tx.ProcessLogging() - if err := tx.Close(); err != nil { - app.logger.Error("Failed to clean cache", zap.Error(err)) - } - }).LFU().Expiration(time.Millisecond * time.Duration(cfg.TransactionTTLMilliseconds)).Build() - - apps[name] = app - } - return &SPOA{ - applications: apps, - defaultApplication: conf.DefaultApplication, - }, nil -} - -func (s *SPOA) getApplication(appName string) (*application, error) { - var app *application - - // Looking for app by name from message - if appName != "" { - app, exist := s.applications[appName] - if exist { - return app, nil - } - } - - // Looking for app by default app name - app, exist := s.applications[s.defaultApplication] - if exist { - app.logger.Debug("application not found, using default", zap.Any("application", appName), zap.String("default", s.defaultApplication)) - return app, nil - } - - return nil, fmt.Errorf("application not found, application %s, default: %s", appName, s.defaultApplication) -} - -func (s *SPOA) processRequest(spoeMsg *spoe.Message) ([]spoe.Action, error) { - var ( - err error - req *request - app *application - tx types.Transaction - ) - - defer func() { - if tx == nil || app == nil { - return - } - if tx.IsInterrupted() { - tx.ProcessLogging() - if err := tx.Close(); err != nil { - app.logger.Error("failed to close transaction", zap.String("transaction_id", tx.ID()), zap.String("error", err.Error())) - } - } else { - if app.cfg.NoResponseCheck { - return - } - err := app.cache.SetWithExpire(tx.ID(), tx, time.Millisecond*time.Duration(app.cfg.TransactionTTLMilliseconds)) - if err != nil { - app.logger.Error(fmt.Sprintf("failed to cache transaction: %s", err.Error())) - } - } - }() - - req, err = NewRequest(spoeMsg) - if err != nil { - return nil, err - } - - app, err = s.getApplication(req.app) - if err != nil { - return nil, err - } - - tx = app.waf.NewTransactionWithID(req.id) - if tx.IsRuleEngineOff() { - app.logger.Warn("Rule engine is Off, Coraza is not going to process any rule") - return s.allowAction(), nil - } - - err = req.init() - if err != nil { - return nil, err - } - - headers, err := s.readHeaders(req.headers) - if err != nil { - return nil, err - } - for key, values := range headers { - for _, v := range values { - tx.AddRequestHeader(key, v) - } - } - - it, _, err := tx.WriteRequestBody(req.body) - if err != nil { - return nil, err - } - if it != nil { - return s.processInterruption(it), nil - } - - tx.ProcessConnection(req.srcIp.String(), req.srcPort, req.dstIp.String(), req.dstPort) - tx.ProcessURI(req.path+"?"+req.query, req.method, "HTTP/"+req.version) - - it = tx.ProcessRequestHeaders() - if it != nil { - return s.processInterruption(it), nil - } - - it, err = tx.ProcessRequestBody() - if err != nil { - return nil, err - } - if it != nil { - return s.processInterruption(it), nil - } - - return s.allowAction(), nil -} - -func (s *SPOA) processResponse(spoeMsg *spoe.Message) ([]spoe.Action, error) { - var ( - err error - resp *response - app *application - tx types.Transaction - ) - defer func() { - app.cache.Remove(resp.id) - }() - - resp, err = NewResponse(spoeMsg) - if err != nil { - return nil, err - } - - app, err = s.getApplication(resp.app) - if err != nil { - return nil, err - } - - txInterface, err := app.cache.Get(resp.id) - if err != nil { - return nil, fmt.Errorf("failed to get transaction from cache, transaction_id: %s, app: %s, error: %s", resp.id, app.name, err.Error()) - } - tx, ok := txInterface.(types.Transaction) - if !ok { - return nil, fmt.Errorf("application cache is corrupted, transaction_id: %s, app: %s", resp.id, app.name) - } - - err = resp.init() - if err != nil { - return nil, err - } - - headers, err := s.readHeaders(resp.headers) - if err != nil { - return nil, err - } - for key, values := range headers { - for _, v := range values { - tx.AddResponseHeader(key, v) - } - } - - it, _, err := tx.WriteResponseBody(resp.body) - if err != nil { - return nil, err - } - if it != nil { - return s.processInterruption(it), nil - } - - it = tx.ProcessResponseHeaders(resp.status, "HTTP/"+resp.version) - if it != nil { - return s.processInterruption(it), nil - } - - it, err = tx.ProcessResponseBody() - if err != nil { - return nil, err - } - if it != nil { - return s.processInterruption(it), nil - } - - return s.allowAction(), nil -} diff --git a/magefile.go b/magefile.go index 857119c..462d63a 100644 --- a/magefile.go +++ b/magefile.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright 2024 The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build mage @@ -11,25 +11,33 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" ) -var addLicenseVersion = "v1.0.0" // https://github.com/google/addlicense -// TODO: Use recent version (for example v1.53.2) to run on Go 1.20 (https://github.com/golangci/golangci-lint/pull/3414) -var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases -var gosImportsVer = "v0.1.5" // https://github.com/rinchsan/gosimports/releases/tag/v0.1.5 - -var errRunGoModTidy = errors.New("go.mod/sum not formatted, commit changes") +var addLicenseVersion = "v1.1.1" // https://github.com/google/addlicense/releases +var gosImportsVer = "v0.3.7" // https://github.com/rinchsan/gosimports/releases +var golangCILintVer = "v1.54.0" // https://github.com/golangci/golangci-lint/releases var errNoGitDir = errors.New("no .git directory found") +var errUpdateGeneratedFiles = errors.New("generated files need to be updated") // Format formats code in this repository. func Format() error { + if err := sh.RunV("go", "generate", "./..."); err != nil { + return err + } + if err := sh.RunV("go", "mod", "tidy"); err != nil { return err } + + if err := sh.RunV("go", "work", "sync"); err != nil { + return err + } + // addlicense strangely logs skipped files to stderr despite not being erroneous, so use the long sh.Exec form to // discard stderr too. if _, err := sh.Exec(map[string]string{}, io.Discard, io.Discard, "go", "run", fmt.Sprintf("github.com/google/addlicense@%s", addLicenseVersion), @@ -47,18 +55,53 @@ func Format() error { ".") } +func Build() error { + if err := sh.RunV("go", "build", "-o", "build/coraza-spoa"); err != nil { + return err + } + return nil +} + // Lint verifies code quality. func Lint() error { + if err := sh.RunV("go", "generate", "./..."); err != nil { + return err + } + + if sh.Run("git", "diff", "--exit-code", "--", "'*.gen.go'") != nil { + return errUpdateGeneratedFiles + } + if err := sh.RunV("go", "run", fmt.Sprintf("github.com/golangci/golangci-lint/cmd/golangci-lint@%s", golangCILintVer), "run"); err != nil { return err } - if err := sh.RunV("go", "mod", "tidy"); err != nil { + if err := sh.RunV("go", "work", "sync"); err != nil { return err } - if sh.Run("git", "diff", "--exit-code", "go.mod", "go.sum") != nil { - return errRunGoModTidy + if err := filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() { + return nil + } + + if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil { + cmd := exec.Command("go", "mod", "tidy") + cmd.Dir = path + out, err := cmd.CombinedOutput() + fmt.Printf(string(out)) + if err != nil { + return err + } + } + + return nil + }); err != nil { + return err } return nil @@ -70,9 +113,30 @@ func Test() error { return err } + // we specify the package to get streaming test output + if err := sh.RunV("go", "test", "-v", "-tags=e2e", "./internal"); err != nil { + return err + } + return nil } +// Coverage runs tests with coverage and race detector enabled. +func Coverage() error { + if err := os.MkdirAll("build", 0755); err != nil { + return err + } + if err := sh.RunV("go", "test", "-race", "-coverprofile=build/coverage.txt", "-covermode=atomic", "-coverpkg=./...", "./..."); err != nil { + return err + } + return sh.RunV("go", "tool", "cover", "-html=build/coverage.txt", "-o", "build/coverage.html") +} + +// Doc runs godoc, access at http://localhost:6060 +func Doc() error { + return sh.RunV("go", "run", "golang.org/x/tools/cmd/godoc@latest", "-http=:6060") +} + // Precommit installs a git hook to run check when committing func Precommit() error { if _, err := os.Stat(filepath.Join(".git", "hooks")); os.IsNotExist(err) { diff --git a/main.go b/main.go new file mode 100644 index 0000000..e6b76dd --- /dev/null +++ b/main.go @@ -0,0 +1,114 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "flag" + "net" + "os" + "os/signal" + "syscall" + + "github.com/rs/zerolog" + + "github.com/corazawaf/coraza-spoa/internal" +) + +var configPath string +var globalLogger = zerolog.New(os.Stderr).With().Timestamp().Logger() + +func main() { + flag.StringVar(&configPath, "config", "", "configuration file") + flag.Parse() + + if configPath == "" { + globalLogger.Fatal().Msg("Configuration file is not set") + } + + cfg, err := readConfig() + if err != nil { + globalLogger.Fatal().Err(err).Msg("Failed loading config") + } + + logger, err := cfg.Log.newLogger() + if err != nil { + globalLogger.Fatal().Err(err).Msg("Failed creating global logger") + } + globalLogger = logger + + apps, err := cfg.newApplications() + if err != nil { + globalLogger.Fatal().Err(err).Msg("Failed creating applications") + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + network, address := cfg.networkAddressFromBind() + l, err := (&net.ListenConfig{}).Listen(ctx, network, address) + if err != nil { + globalLogger.Fatal().Err(err).Msg("Failed opening socket") + } + + a := &internal.Agent{ + Context: ctx, + Applications: apps, + Logger: globalLogger, + } + go func() { + defer cancelFunc() + + globalLogger.Info().Msg("Starting coraza-spoa") + if err := a.Serve(l); err != nil { + globalLogger.Fatal().Err(err).Msg("listener closed") + } + }() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGINT) + for { + sig := <-sigCh + switch sig { + case syscall.SIGTERM: + globalLogger.Info().Msg("Received SIGTERM, shutting down...") + // this return will run cancel() and close the server + return + case syscall.SIGINT: + globalLogger.Info().Msg("Received SIGINT, shutting down...") + return + case syscall.SIGHUP: + globalLogger.Info().Msg("Received SIGHUP, reloading configuration...") + + newCfg, err := readConfig() + if err != nil { + globalLogger.Error().Err(err).Msg("Error loading configuration, using old configuration") + continue + } + + if cfg.Log != newCfg.Log { + newLogger, err := newCfg.Log.newLogger() + if err != nil { + globalLogger.Error().Err(err).Msg("Error creating new global logger, using old configuration") + continue + } + globalLogger = newLogger + } + + if cfg.Bind != newCfg.Bind { + globalLogger.Error().Msg("Changing bind is not supported yet, using old configuration") + continue + } + + apps, err := newCfg.newApplications() + if err != nil { + globalLogger.Error().Err(err).Msg("Error applying configuration, using old configuration") + continue + } + + a.ReplaceApplications(apps) + cfg = newCfg + } + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..38c2ad7 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} \ No newline at end of file