File coraza-spoa-0.4.0+git3.obscpio of Package coraza-spoa

07070100000000000081A400000000000000000000000168C6803E00000036000000000000000000000000000000000000002800000000coraza-spoa-0.4.0+git3/.pre-commit.hookecho "Executing precommit checks"
go run mage.go check07070100000001000081A400000000000000000000000168C6803E00002110000000000000000000000000000000000000002400000000coraza-spoa-0.4.0+git3/CHANGELOG.md# Changelog

## [0.4.0](https://github.com/corazawaf/coraza-spoa/compare/v0.3.0...v0.4.0) (2025-09-05)


### Features

* add support for ftw-tests ([dcabd18](https://github.com/corazawaf/coraza-spoa/commit/dcabd18b68987111d28e93df7af69a70e59990ef))
* print matched error logs as json if requested ([f4684ed](https://github.com/corazawaf/coraza-spoa/commit/f4684ededd43455fe0592fad5cb34814e1130854))


### Bug Fixes

* **deps:** update all non-major dependencies in .github/workflows/container-image.yaml ([#261](https://github.com/corazawaf/coraza-spoa/issues/261)) ([9b4a2bb](https://github.com/corazawaf/coraza-spoa/commit/9b4a2bb3a7f99f9c5167ef79d6fcb1d7545e50b6))
* **deps:** update all non-major dependencies in .github/workflows/test.yaml ([#268](https://github.com/corazawaf/coraza-spoa/issues/268)) ([1d83755](https://github.com/corazawaf/coraza-spoa/commit/1d83755b6b44c06ba9a68b9aa315043cf7d8a899))
* **deps:** update module github.com/corazawaf/coraza-coreruleset/v4 to v4.16.0 in go.mod ([#259](https://github.com/corazawaf/coraza-spoa/issues/259)) ([2f9819c](https://github.com/corazawaf/coraza-spoa/commit/2f9819c769d9b52afcb3becc78bfed79ce148f39))
* **deps:** update module github.com/prometheus/client_golang to v1.23.1 in go.mod ([3866c1b](https://github.com/corazawaf/coraza-spoa/commit/3866c1bcc1ca96e7013fc6d87b7ab395724cb0b0))

## [0.3.0](https://github.com/corazawaf/coraza-spoa/compare/v0.2.0...v0.3.0) (2025-07-02)


### Features

* add SetServerName to transaction ([c5252cc](https://github.com/corazawaf/coraza-spoa/commit/c5252cc794600345c028de58f3047baedf732300))

## 0.2.0 (2025-06-26)


### Features

* add -version flag for printing version and build info ([#214](https://github.com/corazawaf/coraza-spoa/issues/214)) ([153d4fb](https://github.com/corazawaf/coraza-spoa/commit/153d4fb4677ed0ea4bedfb15cc6469ab89cb17ec)), closes [#117](https://github.com/corazawaf/coraza-spoa/issues/117)
* replace request hook in example ([07483bc](https://github.com/corazawaf/coraza-spoa/commit/07483bc005c0c9b28aed8f0e0cdb2cd595339ef6)), closes [#111](https://github.com/corazawaf/coraza-spoa/issues/111)
* Reuse haproxy unique_id if present ([bfd8b24](https://github.com/corazawaf/coraza-spoa/commit/bfd8b2466ecd6f52e7193a26250710ed803fe1ca))
* support the users to configure the traffic fields they need to forward in HAProxy configuration file ([11c9415](https://github.com/corazawaf/coraza-spoa/commit/11c9415375c76d9edfd43d711f2f9cfc890abe5d))


### Bug Fixes

* 5 by validating null queries ([d828d74](https://github.com/corazawaf/coraza-spoa/commit/d828d74f60896568c5bbf71eb0de045b986e8182))
* **build:** set arch in magefile ([8482824](https://github.com/corazawaf/coraza-spoa/commit/8482824b360c5d29c0d85296b55dfb22322c7439))
* **ci:** minor corrections from code review ([e3e7b9d](https://github.com/corazawaf/coraza-spoa/commit/e3e7b9df73ab5af3b67fb9705a9a3226cc25df5a))
* **ci:** only use main branch for tags ([69403c6](https://github.com/corazawaf/coraza-spoa/commit/69403c6b8a1124d30f37375cc03df3fb812a8fd7))
* **ci:** run build on all branches ([e8f614b](https://github.com/corazawaf/coraza-spoa/commit/e8f614ba55f49dbf965e7f10c90edb54d37dc9dd))
* **ci:** set correct build output dir ([5dba63d](https://github.com/corazawaf/coraza-spoa/commit/5dba63d9522688884cdad71c4d5ac643a698742c))
* **ci:** use variable instead of fixed name ([e619181](https://github.com/corazawaf/coraza-spoa/commit/e619181264be4ea9cc83463b165c6b0aeea132ec))
* **config:** image build ([#100](https://github.com/corazawaf/coraza-spoa/issues/100)) ([b93d995](https://github.com/corazawaf/coraza-spoa/commit/b93d995fca765c8f27db651fb57dafed84eec34a))
* **deps:** update all non-major dependencies in go.mod ([#207](https://github.com/corazawaf/coraza-spoa/issues/207)) ([1dfb95f](https://github.com/corazawaf/coraza-spoa/commit/1dfb95fad3a7efc7f40a71bef5dd4b47a16ce869))
* **deps:** update all non-major dependencies to v2.16.1 in go.mod ([cfcabe5](https://github.com/corazawaf/coraza-spoa/commit/cfcabe5b78150d0e953adcab945714fe32ac0978))
* **deps:** update all non-major dependencies to v2.17.1 in go.mod ([#193](https://github.com/corazawaf/coraza-spoa/issues/193)) ([e7b0f46](https://github.com/corazawaf/coraza-spoa/commit/e7b0f46dbb28d154e3938b2c8e0f4e118a580bc7))
* **deps:** update all non-major dependencies to v2.18.0 in go.mod ([#199](https://github.com/corazawaf/coraza-spoa/issues/199)) ([a76c32f](https://github.com/corazawaf/coraza-spoa/commit/a76c32fea62f4abbb20b7f3063d4f8f85a7bda4d))
* **deps:** update all non-major dependencies to v2.18.2 in go.mod ([#229](https://github.com/corazawaf/coraza-spoa/issues/229)) ([581a429](https://github.com/corazawaf/coraza-spoa/commit/581a429be6e556291afb8e6e3261c5b2962786d5))
* **deps:** update all non-major dependencies to v2.18.3 in go.mod ([#231](https://github.com/corazawaf/coraza-spoa/issues/231)) ([fbe673b](https://github.com/corazawaf/coraza-spoa/commit/fbe673bbf258eb0bb37a512ce335562be9dc0f08))
* **deps:** update github.com/magefile/mage digest to 32e0107 ([#141](https://github.com/corazawaf/coraza-spoa/issues/141)) ([543600d](https://github.com/corazawaf/coraza-spoa/commit/543600d94a5f331786a84c00a99da17a37abad09))
* **deps:** update github.com/magefile/mage digest to 78acbaf in go.mod ([#232](https://github.com/corazawaf/coraza-spoa/issues/232)) ([7acc427](https://github.com/corazawaf/coraza-spoa/commit/7acc427f246bdb469aaba9fa75ce69ca7c660286))
* **deps:** update module github.com/corazawaf/coraza-coreruleset/v4 to v4.14.0 in go.mod ([#218](https://github.com/corazawaf/coraza-spoa/issues/218)) ([6933218](https://github.com/corazawaf/coraza-spoa/commit/6933218a419f34996d3c6e83fdae1a8ce27360bf))
* **deps:** update module github.com/corazawaf/coraza-coreruleset/v4 to v4.15.0 in go.mod ([#236](https://github.com/corazawaf/coraza-spoa/issues/236)) ([72f72ea](https://github.com/corazawaf/coraza-spoa/commit/72f72ea27c7e202386e2bca2acd85321bfaa8acb))
* **deps:** update module github.com/corazawaf/coraza/v3 to v3.2.2 ([#131](https://github.com/corazawaf/coraza-spoa/issues/131)) ([de7faf4](https://github.com/corazawaf/coraza-spoa/commit/de7faf458f041a24b1dc9c391bc7d6a9d4ea1caa))
* **deps:** update module github.com/corazawaf/coraza/v3 to v3.3.0 ([#154](https://github.com/corazawaf/coraza-spoa/issues/154)) ([87d7dde](https://github.com/corazawaf/coraza-spoa/commit/87d7dde4fa95dc03a5c7aa5cb549c94943a33024))
* **deps:** update module github.com/corazawaf/coraza/v3 to v3.3.2 ([7bb4c86](https://github.com/corazawaf/coraza-spoa/commit/7bb4c86ee715ded8e28c5fd23093a4dcb704148b))
* **deps:** update module github.com/corazawaf/coraza/v3 to v3.3.3 [security] ([39a02d6](https://github.com/corazawaf/coraza-spoa/commit/39a02d68bd636a106859f2b6702268cb7d393a9b))
* **deps:** update module github.com/dropmorepackets/haproxy-go to v0.0.6 in go.mod ([735c7af](https://github.com/corazawaf/coraza-spoa/commit/735c7afb042e89d16d1c11922fae790210560e3a))
* **deps:** update module github.com/dropmorepackets/haproxy-go to v0.0.7 in go.mod ([#226](https://github.com/corazawaf/coraza-spoa/issues/226)) ([5aa72f0](https://github.com/corazawaf/coraza-spoa/commit/5aa72f0f3d3951cfa520d4545782c6402e9d43b0))
* **deps:** update module github.com/mccutchen/go-httpbin/v2 to v2.16.0 ([#172](https://github.com/corazawaf/coraza-spoa/issues/172)) ([b0e8fdc](https://github.com/corazawaf/coraza-spoa/commit/b0e8fdc1c7d4c9c119b24ab2cf5598a4ffd5a3b9))
* **deps:** update module github.com/pires/go-proxyproto to v0.8.0 ([#119](https://github.com/corazawaf/coraza-spoa/issues/119)) ([1046c72](https://github.com/corazawaf/coraza-spoa/commit/1046c725b17f056eae5e7e3334b357ac06be4662))
* **deps:** update module github.com/rs/zerolog to v1.34.0 in go.mod ([#202](https://github.com/corazawaf/coraza-spoa/issues/202)) ([cc7b577](https://github.com/corazawaf/coraza-spoa/commit/cc7b5772da1c203a9aa8f43d696c5b348b4f1e3c))
* renovate config ([6e33b60](https://github.com/corazawaf/coraza-spoa/commit/6e33b6016b87248e339e76620d980b95258f1e9e))
* revert golang major upgrade ([3bfad4f](https://github.com/corazawaf/coraza-spoa/commit/3bfad4f53b166be1c1711e6d6510e3d0f275ab77))
* run mage lint ([7321cc4](https://github.com/corazawaf/coraza-spoa/commit/7321cc460c8297e4eb03d66aaabf1a60495eee7c))


### Miscellaneous Chores

* release 0.2.0 ([#239](https://github.com/corazawaf/coraza-spoa/issues/239)) ([e9ce67e](https://github.com/corazawaf/coraza-spoa/commit/e9ce67e2b246de124b8dc0debefa352375ce284a))
07070100000002000081A400000000000000000000000168C6803E00001462000000000000000000000000000000000000002A00000000coraza-spoa-0.4.0+git3/CODE_OF_CONDUCT.md# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
concat@coraza.io.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior,  harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
07070100000003000081A400000000000000000000000168C6803E00002C5D000000000000000000000000000000000000001F00000000coraza-spoa-0.4.0+git3/LICENSE                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   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.
07070100000004000081A400000000000000000000000168C6803E00001127000000000000000000000000000000000000002100000000coraza-spoa-0.4.0+git3/README.md<h1>
  <img src="https://coraza.io/images/logo_shield_only.png" align="left" height="46px" alt=""/>
  <span>Coraza SPOA - HAProxy Web Application Firewall</span>
</h1>

[![Code Linting](https://github.com/corazawaf/coraza-spoa/actions/workflows/lint.yaml/badge.svg)](https://github.com/corazawaf/coraza-spoa/actions/workflows/lint.yaml)
[![CodeQL Scanning](https://github.com/corazawaf/coraza-spoa/actions/workflows/codeql.yaml/badge.svg)](https://github.com/corazawaf/coraza-spoa/actions/workflows/codeql.yaml)

Coraza SPOA is a system daemon which brings the Coraza Web Application Firewall (WAF) as a backing service for HAProxy. It is written in Go, Coraza supports ModSecurity SecLang rulesets and is 100% compatible with the OWASP Core Rule Set v4.

HAProxy includes a [Stream Processing Offload Engine](https://www.haproxy.com/blog/extending-haproxy-with-the-stream-processing-offload-engine) [SPOE](https://raw.githubusercontent.com/haproxy/haproxy/master/doc/SPOE.txt) to offload request processing to a Stream Processing Offload Agent (SPOA). Coraza SPOA embeds the [Coraza Engine](https://github.com/corazawaf/coraza), loads the ruleset and filters http requests or application responses which are passed forwarded by HAProxy for inspection.

## Compilation

### Build

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 [example/coraza-spoa.yaml](https://github.com/corazawaf/coraza-spoa/blob/main/example/coraza-spoa.yaml), you can copy it and modify the related configuration information. You can start the service by running the command:

```
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/example/haproxy/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
spoe-agent coraza-agent
    ...
    use-backend coraza-spoa

spoe-message coraza-req
    args app=str(sample_app) id=unique-id src-ip=src ...
    event on-frontend-http-request
```

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

Instead of the hard coded application name `str(sample_app)` you can use some HAProxy variables. For example, frontend name `fe_name`.

## HAProxy

Configure HAProxy with a frontend, which contains a `filter` statement to forward requests to the SPOA and deny based on the returned action. Also add a backend section, which is referenced by use-backend in `coraza.cfg`.

```haproxy
# /etc/haproxy/haproxy.cfg
frontend web
    filter spoe engine coraza config /etc/haproxy/coraza.cfg
    ...
    http-request deny deny_status 403 hdr waf-block "request"  if { var(txn.coraza.action) -m str deny }
    ...

backend coraza-spoa
    mode tcp
    server s1 127.0.0.1:9000
```

A comprehensive HAProxy configuration example can be found in [example/haproxy/coraza.cfg](https://github.com/corazawaf/coraza-spoa/blob/main/example/haproxy/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/main/example/haproxy/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/example/haproxy/haproxy.cfg#L37):

If you intend to access coraza-spoa service from another machine, remember to change the binding networking directives (IPAddressAllow/IPAddressDeny) in [contrib/coraza-spoa.service](https://github.com/corazawaf/coraza-spoa/blob/main/contrib/coraza-spoa.service)

## Docker

- Build the coraza-spoa image `cd ./example ; 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:8080/\?x\=/etc/passwd`
07070100000005000081A400000000000000000000000168C6803E00001685000000000000000000000000000000000000002100000000coraza-spoa-0.4.0+git3/config.gopackage main

import (
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"
	"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")
	}

	if cfg.DefaultApplication != "" {
		var found bool
		for _, app := range cfg.Applications {
			if app.Name == cfg.DefaultApplication {
				globalLogger.Debug().Str("app", cfg.DefaultApplication).Msg("configured as default application")
				found = true
				break
			}
		}
		if !found {
			return nil, fmt.Errorf("default application not found among defined applications: %s", cfg.DefaultApplication)
		}
	}

	return &cfg, nil
}

type config struct {
	Bind               string    `yaml:"bind"`
	Log                logConfig `yaml:",inline"`
	DefaultApplication string    `yaml:"default_application"`
	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"`
	} `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) reloadConfig(a *internal.Agent) (*config, error) {
	newCfg, err := readConfig()
	if err != nil {
		return nil, fmt.Errorf("error loading configuration: %w", err)
	}

	if c.Log != newCfg.Log {
		newLogger, err := newCfg.Log.newLogger()
		if err != nil {
			return nil, fmt.Errorf("error creating new global logger: %w", err)
		}
		globalLogger = newLogger
	}

	if c.Bind != newCfg.Bind {
		return nil, fmt.Errorf("changing bind is not supported yet")
	}

	apps, err := newCfg.newApplications()
	if err != nil {
		return nil, fmt.Errorf("error applying configuration: %w", err)
	}

	a.ReplaceApplications(apps)
	globalLogger.Info().Msg("Configuration successfully reloaded")
	return newCfg, nil
}

func (c *config) watchConfig(a *internal.Agent) error {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return fmt.Errorf("failed to create fsnotify watcher: %w", err)
	}
	defer watcher.Close()

	// configmap mounts are symlinks
	// so we have to watch the parent directory instead of the file itself
	configDir := filepath.Dir(configPath)
	err = watcher.Add(configDir)
	if err != nil {
		return fmt.Errorf("failed to add config directory to fsnotify watcher: %w", err)
	}

	for {
		select {
		case event, ok := <-watcher.Events:
			if !ok {
				return nil
			}
			// on configmap change, the directory symlink is recreated
			// so we have to catch this event and readd the directory back to watcher
			if event.Op == fsnotify.Remove {
				globalLogger.Info().Msg("Config directory updated, reloading configuration...")
				err = watcher.Remove(configDir)
				if err != nil {
					return fmt.Errorf("failed to remove config directory from fsnotify watcher: %w", err)
				}
				err = watcher.Add(configDir)
				if err != nil {
					return fmt.Errorf("failed to add config directory to fsnotify watcher: %w", err)
				}
				newCfg, err := c.reloadConfig(a)
				if err != nil {
					globalLogger.Error().Err(err).Msg("Failed to reload configuration, using old configuration")
					continue
				}
				c = newCfg
			}
		case err, ok := <-watcher.Errors:
			if !ok {
				return nil
			}
			globalLogger.Error().Err(err).Msg("Error watching config directory")
		}
	}
}

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,
			LogFormat:      a.Log.Format,
			TransactionTTL: 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
}
07070100000006000041ED00000000000000000000000268C6803E00000000000000000000000000000000000000000000001F00000000coraza-spoa-0.4.0+git3/contrib07070100000007000081A400000000000000000000000168C6803E000000E3000000000000000000000000000000000000003500000000coraza-spoa-0.4.0+git3/contrib/coraza-spoa.logrotate/var/log/coraza-spoa/*.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    delaycompress
    postrotate
        [ ! -x /usr/lib/rsyslog/rsyslog-rotate ] || /usr/lib/rsyslog/rsyslog-rotate
    endscript
}
07070100000008000081A400000000000000000000000168C6803E0000025F000000000000000000000000000000000000003400000000coraza-spoa-0.4.0+git3/contrib/coraza-spoa.postinst#!/bin/sh

set -e

# add unprivileged user & group for the coraza-spoa
addgroup --quiet --system coraza-spoa || true

adduser --quiet --system --ingroup coraza-spoa --no-create-home --home /nonexistent --disabled-password coraza-spoa || true

if [ ! -d /var/log/coraza-spoa ]; then
  mkdir -p /var/log/coraza-spoa /var/log/coraza-spoa/audit
  touch /var/log/coraza-spoa/server.log /var/log/coraza-spoa/error.log \
        /var/log/coraza-spoa/audit.log /var/log/coraza-spoa/debug.log
fi

chown -R coraza-spoa:adm /var/log/coraza-spoa 2> /dev/null || true
chmod 755 /var/log/coraza-spoa 2> /dev/null || true
07070100000009000081A400000000000000000000000168C6803E00002603000000000000000000000000000000000000003300000000coraza-spoa-0.4.0+git3/contrib/coraza-spoa.service[Unit]
Description=Coraza WAF SPOA Daemon
Documentation=https://www.coraza.io

[Service]
ExecStart=/usr/bin/coraza-spoa -config=/etc/coraza-spoa/config.yaml
WorkingDirectory=/
Restart=always
Type=exec
User=coraza-spoa
Group=coraza-spoa

# Hardening
AmbientCapabilities=
MountFlags=private

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=noaccess
ProtectClock=yes
ProtectHostname=yes
ProtectSystem=strict
RestrictSUIDSGID=true
RestrictRealtime=true
SecureBits=no-setuid-fixup-locked noroot-locked

TemporaryFileSystem=/etc
TemporaryFileSystem=/var
BindReadOnlyPaths=-/etc/ca-certificates
BindReadOnlyPaths=-/etc/crypto-policies
BindReadOnlyPaths=-/etc/fdns
BindReadOnlyPaths=-/etc/ld.so.cache
BindReadOnlyPaths=-/etc/ld.so.preload
BindReadOnlyPaths=-/etc/localtime
BindReadOnlyPaths=-/etc/nsswitch.conf
BindReadOnlyPaths=-/etc/passwd
BindReadOnlyPaths=-/etc/pki
BindReadOnlyPaths=-/etc/ssl
BindReadOnlyPaths=-/etc/coraza-spoa
BindPaths=-/var/log/coraza-spoa

InaccessiblePaths=-/opt
InaccessiblePaths=-/srv
#InaccessiblePaths=-/bin
InaccessiblePaths=-/bin/bash
inaccessiblepaths=-/bin/find
InaccessiblePaths=-/bin/less
InaccessiblePaths=-/bin/zcat
InaccessiblePaths=-/bin/rm
InaccessiblePaths=-/bin/readlink
InaccessiblePaths=-/bin/readpath
InaccessiblePaths=-/sbin
InaccessiblePaths=-/efi
InaccessiblePaths=-/run/media
InaccessiblePaths=-/run/mount
InaccessiblePaths=-/efi
InaccessiblePaths=-/boot
InaccessiblePaths=-/dev/kmsg
InaccessiblePaths=-/dev/port
InaccessiblePaths=-/lib/modules
InaccessiblePaths=-/lost+found
InaccessiblePaths=-/proc/bus
InaccessiblePaths=-/proc/config.gz
InaccessiblePaths=-/usr/bin/alsaloop
InaccessiblePaths=-/usr/bin/alsamixer
InaccessiblePaths=-/usr/bin/alsatplg
InaccessiblePaths=-/usr/bin/alsaucm
InaccessiblePaths=-/usr/bin/alsaunmute
InaccessiblePaths=-/usr/bin/attr
InaccessiblePaths=-/usr/bin/balooctl
InaccessiblePaths=-/usr/bin/bash
InaccessiblePaths=-/usr/bin/bootctl
InaccessiblePaths=-/usr/bin/busctl
InaccessiblePaths=-/usr/bin/chacl
InaccessiblePaths=-/usr/bin/chattr
InaccessiblePaths=-/usr/bin/cmp
InaccessiblePaths=-/usr/bin/coredumpctl
InaccessiblePaths=-/usr/bin/crontab
InaccessiblePaths=-/usr/bin/csh
InaccessiblePaths=-/usr/bin/dash
InaccessiblePaths=-/usr/bin/dd
InaccessiblePaths=-/usr/bin/df
InaccessiblePaths=-/usr/bin/diff
InaccessiblePaths=-/usr/bin/diff3
InaccessiblePaths=-/usr/bin/dmesg
InaccessiblePaths=-/usr/bin/dnf
InaccessiblePaths=-/usr/bin/dotty
InaccessiblePaths=-/usr/bin/dracut
InaccessiblePaths=-/usr/bin/evmctl
InaccessiblePaths=-/usr/bin/free
InaccessiblePaths=-/usr/bin/ftp
InaccessiblePaths=-/usr/bin/getfacl
InaccessiblePaths=-/usr/bin/getfattr
InaccessiblePaths=-/usr/bin/grotty
InaccessiblePaths=-/usr/bin/grub2-file
InaccessiblePaths=-/usr/bin/grub2-menulst2cfg
InaccessiblePaths=-/usr/bin/grub2-mkimage
InaccessiblePaths=-/usr/bin/grub2-mkrelpath
InaccessiblePaths=-/usr/bin/grub2-render-label
InaccessiblePaths=-/usr/bin/grub2-script-check
InaccessiblePaths=-/usr/bin/hostnamectl
InaccessiblePaths=-/usr/bin/htop
InaccessiblePaths=-/usr/bin/ipcmk
InaccessiblePaths=-/usr/bin/journalctl
InaccessiblePaths=-/usr/bin/keyctl
InaccessiblePaths=-/usr/bin/kill
InaccessiblePaths=-/usr/bin/killall
InaccessiblePaths=-/usr/bin/ksh
InaccessiblePaths=-/usr/bin/last
InaccessiblePaths=-/usr/bin/localectl
InaccessiblePaths=-/usr/bin/locate
InaccessiblePaths=-/usr/bin/loginctl
InaccessiblePaths=-/usr/bin/ls
InaccessiblePaths=-/usr/bin/lsattr
InaccessiblePaths=-/usr/bin/lsb_release
InaccessiblePaths=-/usr/bin/lsblk
InaccessiblePaths=-/usr/bin/lscpu
InaccessiblePaths=-/usr/bin/lsdiff
InaccessiblePaths=-/usr/bin/lsinitrd
InaccessiblePaths=-/usr/bin/lsipc
InaccessiblePaths=-/usr/bin/lslocks
InaccessiblePaths=-/usr/bin/lslogins
InaccessiblePaths=-/usr/bin/lsmem
InaccessiblePaths=-/usr/bin/lsns
InaccessiblePaths=-/usr/bin/lsof
InaccessiblePaths=-/usr/bin/lsscsi
InaccessiblePaths=-/usr/bin/lsusb
InaccessiblePaths=-/usr/bin/lua
InaccessiblePaths=-/usr/bin/lynis
InaccessiblePaths=-/usr/bin/mail
InaccessiblePaths=-/usr/bin/mkfifo
InaccessiblePaths=-/usr/bin/mkinitrd
InaccessiblePaths=-/usr/bin/mkisofs
InaccessiblePaths=-/usr/bin/mknod
InaccessiblePaths=-/usr/bin/mount
InaccessiblePaths=-/usr/bin/mountpoint
InaccessiblePaths=-/usr/bin/nc
InaccessiblePaths=-/usr/bin/netcap
InaccessiblePaths=-/usr/bin/netstat
InaccessiblePaths=-/usr/bin/netstat-nat
InaccessiblePaths=-/usr/bin/networkctl
InaccessiblePaths=-/usr/bin/nmap
InaccessiblePaths=-/usr/bin/nping
InaccessiblePaths=-/usr/bin/nsenter
InaccessiblePaths=-/usr/bin/pactl
InaccessiblePaths=-/usr/bin/panelctl
InaccessiblePaths=-/usr/bin/passwd
InaccessiblePaths=-/usr/bin/peekfd
InaccessiblePaths=-/usr/bin/pgrep
InaccessiblePaths=-/usr/bin/pidof
InaccessiblePaths=-/usr/bin/ping
InaccessiblePaths=-/usr/bin/pkill
InaccessiblePaths=-/usr/bin/pkttyagent
InaccessiblePaths=-/usr/bin/pmap
InaccessiblePaths=-/usr/bin/portablectl
InaccessiblePaths=-/usr/bin/prtstat
InaccessiblePaths=-/usr/bin/ps
InaccessiblePaths=-/usr/bin/pslog
InaccessiblePaths=-/usr/bin/pstree
InaccessiblePaths=-/usr/bin/pstree.x11
InaccessiblePaths=-/usr/bin/pulseaudio
InaccessiblePaths=-/usr/bin/pwdx
InaccessiblePaths=-/usr/bin/python
InaccessiblePaths=-/usr/bin/python2
InaccessiblePaths=-/usr/bin/python3
InaccessiblePaths=-/usr/bin/python3.9
InaccessiblePaths=-/usr/bin/resolvectl
InaccessiblePaths=-/usr/bin/rkhunter
InaccessiblePaths=-/usr/bin/rpm
InaccessiblePaths=-/usr/bin/rsync
InaccessiblePaths=-/usr/bin/ruby
InaccessiblePaths=-/usr/bin/run-parts
InaccessiblePaths=-/usr/bin/scp
InaccessiblePaths=-/usr/bin/screen
InaccessiblePaths=-/usr/bin/sdiff
InaccessiblePaths=-/usr/bin/setarch
InaccessiblePaths=-/usr/bin/setcifsacl
InaccessiblePaths=-/usr/bin/setfacl
InaccessiblePaths=-/usr/bin/setfattr
InaccessiblePaths=-/usr/bin/setpriv
InaccessiblePaths=-/usr/bin/setsid
InaccessiblePaths=-/usr/bin/setterm
InaccessiblePaths=-/usr/bin/setxkbmap
InaccessiblePaths=-/usr/bin/sftp
InaccessiblePaths=-/usr/bin/sh
InaccessiblePaths=-/usr/bin/skill
InaccessiblePaths=-/usr/bin/slabtop
InaccessiblePaths=-/usr/bin/snice
InaccessiblePaths=-/usr/bin/ssh
InaccessiblePaths=-/usr/bin/ssh-add
InaccessiblePaths=-/usr/bin/ssh-agent
InaccessiblePaths=-/usr/bin/ssh-copy-id
InaccessiblePaths=-/usr/bin/ssh-keyscan
InaccessiblePaths=-/usr/bin/strace
InaccessiblePaths=-/usr/bin/strace-log-merg
InaccessiblePaths=-/usr/bin/strings
InaccessiblePaths=-/usr/bin/stty
InaccessiblePaths=-/usr/bin/su
InaccessiblePaths=-/usr/bin/sudo
InaccessiblePaths=-/usr/bin/systemctl
InaccessiblePaths=-/usr/bin/systemd-tty-ask-password-agent
InaccessiblePaths=-/usr/bin/tcl
InaccessiblePaths=-/usr/bin/tcptraceroute
InaccessiblePaths=-/usr/bin/tcsh
InaccessiblePaths=-/usr/bin/telnet
InaccessiblePaths=-/usr/bin/timedatectl
InaccessiblePaths=-/usr/bin/tload
InaccessiblePaths=-/usr/bin/tmux
InaccessiblePaths=-/usr/bin/top
InaccessiblePaths=-/usr/bin/touch
InaccessiblePaths=-/usr/bin/tracepath
InaccessiblePaths=-/usr/bin/traceroute
InaccessiblePaths=-/usr/bin/traceroute6
InaccessiblePaths=-/usr/bin/tricklectl
InaccessiblePaths=-/usr/bin/tty
InaccessiblePaths=-/usr/bin/udevadm
InaccessiblePaths=-/usr/bin/udisksctl
InaccessiblePaths=-/usr/bin/umount
InaccessiblePaths=-/usr/bin/uname
InaccessiblePaths=-/usr/bin/unlink
InaccessiblePaths=-/usr/bin/updatedb
InaccessiblePaths=-/usr/bin/uptime
InaccessiblePaths=-/usr/bin/users
InaccessiblePaths=-/usr/bin/vi
InaccessiblePaths=-/usr/bin/vim
InaccessiblePaths=-/usr/bin/vim.nox
InaccessiblePaths=-/usr/bin/vim.tiny
InaccessiblePaths=-/usr/bin/vimtutor
InaccessiblePaths=-/usr/bin/vmware-checkvm
InaccessiblePaths=-/usr/bin/vmware-namespace-cmd
InaccessiblePaths=-/usr/bin/vmware-rpctool
InaccessiblePaths=-/usr/bin/vmware-toolbox-cmd
InaccessiblePaths=-/usr/bin/vmware-xferlogs
InaccessiblePaths=-/usr/bin/w
InaccessiblePaths=-/usr/bin/wall
InaccessiblePaths=-/usr/bin/watch
InaccessiblePaths=-/usr/bin/wdctl
InaccessiblePaths=-/usr/bin/wg
InaccessiblePaths=-/usr/bin/wget
InaccessiblePaths=-/usr/bin/who
InaccessiblePaths=-/usr/bin/whoami
InaccessiblePaths=-/usr/bin/zsh
InaccessiblePaths=-/usr/local
InaccessiblePaths=-/usr/sbin
InaccessiblePaths=-/proc/irq
InaccessiblePaths=-/proc/kallsyms
InaccessiblePaths=-/proc/kcore
InaccessiblePaths=-/proc/kmem
#*InaccessiblePaths=-/proc/kmsg
InaccessiblePaths=-/proc/mem
InaccessiblePaths=-/proc/sched_debug
InaccessiblePaths=-/proc/sys/efi/vars
InaccessiblePaths=-/proc/sys/fs/binfmt_misc
#*InaccessiblePaths=-/proc/sys/kernel/core_pattern
InaccessiblePaths=-/proc/sys/kernel/hotplug
#*InaccessiblePaths=-/proc/sys/kernel/modprobe
InaccessiblePaths=-/proc/sys/security
#*InaccessiblePaths=-/proc/sys/vm/panic_on_oom
InaccessiblePaths=-/proc/sysrq-trigger
InaccessiblePaths=-/proc/timer_list
InaccessiblePaths=-/proc/timer_stats
InaccessiblePaths=-/selinux
InaccessiblePaths=-/sys/firmware
InaccessiblePaths=-/sys/fs
InaccessiblePaths=-/sys/hypervisor
InaccessiblePaths=-/sys/kernel/debug
InaccessiblePaths=-/sys/kernel/uevent_helper
InaccessiblePaths=-/sys/kernel/vmcoreinfo
InaccessiblePaths=-/sys/module
InaccessiblePaths=-/sys/power
#*InaccessiblePaths=-/usr/lib/debug
InaccessiblePaths=-/usr/src/linux

LockPersonality=true

LogsDirectory=coraza-spoa
ConfigurationDirectory=coraza-spoa
# coraza-geoip
ReadOnlyPaths=/usr/share/GeoIP
#ReadOnlyPaths=/proc

MemoryDenyWriteExecute=yes

NoNewPrivileges=true

ProtectHome=true
PrivateDevices=true

PrivateUsers=true
PrivateTmp=true

RemoveIPC=true

RestrictAddressFamilies=AF_INET AF_INET6
#RestrictNamespaces=uts ipc pid user cgroup

SystemCallArchitectures=native
SystemCallFilter=@system-service -@setuid -@ipc -@mount

IPAddressDeny=any
IPAddressAllow=localhost

CapabilityBoundingSet=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
0707010000000A000041ED00000000000000000000000268C6803E00000000000000000000000000000000000000000000001F00000000coraza-spoa-0.4.0+git3/example0707010000000B000081A400000000000000000000000168C6803E000003DA000000000000000000000000000000000000002A00000000coraza-spoa-0.4.0+git3/example/Dockerfile# Copyright 2023 The OWASP Coraza contributors
# SPDX-License-Identifier: Apache-2.0

FROM golang:1.23@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 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@sha256:1dbe426d60caed5d19597532a2d74c8056cd7b1674042b88f7328690b5ead8ed

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"]0707010000000C000081A400000000000000000000000168C6803E00000477000000000000000000000000000000000000003000000000coraza-spoa-0.4.0+git3/example/coraza-spoa.yaml# 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

# Optional default application to use when the app from the request
# does not match any of the declared application names
default_application: sample_app

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
      SecRuleEngine On

    # 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
0707010000000D000081A400000000000000000000000168C6803E0000038D000000000000000000000000000000000000003300000000coraza-spoa-0.4.0+git3/example/docker-compose.yamlversion: "3.9"
services:
  httpbin:
    image: mccutchen/go-httpbin:2.18.3@sha256:3992f3763e9ce5a4307eae0a869a78b4df3931dc8feba74ab823dd2444af6a6b
    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.9-alpine@sha256:3e29449a6beed63262e36104adf531b4e41b359f61937303f5ea8607987b3748
    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-spoa0707010000000E000041ED00000000000000000000000268C6803E00000000000000000000000000000000000000000000002700000000coraza-spoa-0.4.0+git3/example/haproxy0707010000000F000081A400000000000000000000000168C6803E000003E5000000000000000000000000000000000000003200000000coraza-spoa-0.4.0+git3/example/haproxy/coraza.cfg# https://github.com/haproxy/haproxy/blob/master/doc/SPOE.txt
# /usr/local/etc/haproxy/coraza.cfg
[coraza]
spoe-agent coraza-agent
    # Uncomment the following line, to process responses also.
    #messages   coraza-res
    groups      coraza-req
    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
    # Arguments are required to be in this order
    args app=var(txn.coraza.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

spoe-message coraza-res
    # Arguments are required to be in this order
    args app=var(txn.coraza.app) id=var(txn.coraza.id) version=res.ver status=status headers=res.hdrs body=res.body
    event on-http-response

spoe-group coraza-req
    messages coraza-req

07070100000010000081A400000000000000000000000168C6803E00000909000000000000000000000000000000000000003300000000coraza-spoa-0.4.0+git3/example/haproxy/haproxy.cfg# https://docs.haproxy.org/
global
    log stdout format raw local0

defaults
    log global
    option httplog
    timeout client 1m
    timeout server 1m
    timeout connect 10s

frontend default
    mode http
    bind *:80
    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\ %[var(txn.coraza.id)]\ spoa-error:\ %[var(txn.coraza.error)]\ waf-hit:\ %[var(txn.coraza.status)]"

    # Emulate Apache behavior by only allowing http 1.0, 1.1, 2.0 
    http-request deny deny_status 400 if !HTTP
    http-request deny deny_status 400 if !HTTP_1.0 !HTTP_1.1 !HTTP_2.0

    # Set coraza app in HAProxy config to allow customized configs per host.
    # You can also just leave this as is or even replace the use of a variable
    # inside the coraza.cfg.
    http-request set-var(txn.coraza.app) str(sample_app)

    # !! Every http-request line will be executed before this !!
    # Execute coraza request check.
    filter spoe engine coraza config /usr/local/etc/haproxy/coraza.cfg
    http-request send-spoe-group coraza coraza-req

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

resolvers host_dns
    parse-resolv-conf

backend httpbin_backend
    mode http
    server backend $BACKEND_HOST

backend coraza-spoa
    option spop-check
    mode tcp
    server coraza_spoa coraza-spoa:9000 check
07070100000011000041ED00000000000000000000000268C6803E00000000000000000000000000000000000000000000001B00000000coraza-spoa-0.4.0+git3/ftw07070100000012000081A400000000000000000000000168C6803E000003D6000000000000000000000000000000000000003200000000coraza-spoa-0.4.0+git3/ftw/Dockerfile.coraza_spoa# Copyright 2023 The OWASP Coraza contributors
# SPDX-License-Identifier: Apache-2.0

FROM golang:1.23@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 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-debian12@sha256:87bce11be0af225e4ca761c40babb06d6d559f5767fbf7dc3c47f0f1a466b92c

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 ./ftw/coraza-spoa.yaml /config.yaml

CMD ["/coraza-spoa", "--config", "/config.yaml"]07070100000013000081A400000000000000000000000168C6803E00000321000000000000000000000000000000000000002A00000000coraza-spoa-0.4.0+git3/ftw/Dockerfile.ftw# Copyright 2025 The OWASP Coraza contributors
# SPDX-License-Identifier: Apache-2.0

FROM ghcr.io/coreruleset/go-ftw:1.3.0@sha256:99e5d772dc0292a1685b23cff2ba40463db71ab96ebf98aa9cf517b641300254

RUN apk update && apk add curl

WORKDIR /workspace

# TODOs:
# - update when new CRS version is tagged: https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.5.0.tar.gz
# - keep it aligned with the github.com/corazawaf/coraza-coreruleset/v4 dependency version used
ENV CRS_VERSION=4.15.0

ADD https://github.com/coreruleset/coreruleset/archive/refs/tags/v${CRS_VERSION}.tar.gz /workspace/coreruleset/
RUN cd coreruleset && tar -xf v${CRS_VERSION}.tar.gz --strip-components 1

COPY ftw.yml /workspace/ftw.yml
COPY tests.sh /workspace/tests.sh

ENTRYPOINT ["sh"]
CMD ["-c", "/workspace/tests.sh"]07070100000014000081A400000000000000000000000168C6803E000005D4000000000000000000000000000000000000002C00000000coraza-spoa-0.4.0+git3/ftw/coraza-spoa.yaml---

bind: 0.0.0.0:9000
log_level: info
log_file: /build/ftw-spoa.log
log_format: console

applications:
  - name: ftw
    directives: |
      Include @coraza.conf-recommended
      # log details on failures
      SecAuditLog /build/ftw-audit.log
      SecAuditLogRelevantStatus "^(?:5)"

      # FTW config
      SecDefaultAction "phase:3,log,auditlog,pass"
      SecDefaultAction "phase:4,log,auditlog,pass"
      SecDefaultAction "phase:5,log,auditlog,pass"
      SecDebugLogLevel 3

      SecAction "id:900005,\
          phase:1,\
          nolog,\
          pass,\
          ctl:ruleEngine=DetectionOnly,\
          ctl:ruleRemoveById=910000,\
          setvar:tx.blocking_paranoia_level=4,\
          setvar:tx.crs_validate_utf8_encoding=1,\
          setvar:tx.arg_name_length=100,\
          setvar:tx.arg_length=400,\
          setvar:tx.total_arg_length=64000,\
          setvar:tx.max_num_args=255,\
          setvar:tx.max_file_size=64100,\
          setvar:tx.combined_file_sizes=65535"

      SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" "id:999999,\
          phase:1,\
          pass,\
          t:none,\
          log,\
          msg:'X-CRS-Test %{MATCHED_VAR}',\
          ctl:ruleRemoveById=1-999999"

      Include @crs-setup.conf.example
      Include @owasp_crs/*.conf

    response_check: true
    transaction_ttl_ms: 60000
    log_level: error  # Printing only logs at error level to reduce what ftw has to parse
    log_file: /build/ftw.log
    log_format: console
07070100000015000081A400000000000000000000000168C6803E000005F2000000000000000000000000000000000000002E00000000coraza-spoa-0.4.0+git3/ftw/docker-compose.yml---

services:
  backend:
    image: ghcr.io/coreruleset/albedo:0.2.0@sha256:bc9b7e4f83a5268ccd2b4d1d26e926b6324e20ff1d91281c339ad82e55f5bab2
    command: ['--port', '8081']
    ports:
      - 8081:8081

  # clean existing logs, enable haproxy to write logs, allow devs to manage logs without sudo
  prepare-logs:
    image: alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
    command:
      - /bin/sh
      - -c
      - rm -f /build/ftw* && touch /build/ftw.log /build/ftw-haproxy.log /build/ftw-haproxy.log /build/ftw-audit.log && chmod 666 /build/ftw*
    volumes:
      - ../build:/build:rw

  coraza-spoa:
    depends_on:
      - prepare-logs
    build:
      context: ..
      dockerfile: ./ftw/Dockerfile.coraza_spoa
      network: host
    volumes:
     - ../build:/build:rw
    ports:
      - 9000:9000

  haproxy:
    depends_on:
      - prepare-logs
      - backend
    image: "haproxy:${FTW_HAPROXY_VERSION:-3.0}-alpine"
    links:
      - coraza-spoa:coraza-spoa
    volumes:
      - type: bind
        source: ./haproxy/
        target: /usr/local/etc/haproxy
      - ../build:/build:rw
    command:
      - /bin/sh
      - -c
      - haproxy -f /usr/local/etc/haproxy/haproxy.cfg > /build/ftw-haproxy.log
    ports:
      - 8080:8080

  ftw:
    depends_on:
      - coraza-spoa
      - haproxy
    build:
      context: .
      dockerfile: Dockerfile.ftw
      network: host
    environment:
      - FTW_CLOUDMODE
      - FTW_INCLUDE
    volumes:
      - ../build:/build
07070100000016000081A400000000000000000000000168C6803E00000929000000000000000000000000000000000000002300000000coraza-spoa-0.4.0+git3/ftw/ftw.yml---

logfile: '/build/ftw.log'
maxmarkerretries: 10
testoverride:
  input:
    dest_addr: 'haproxy'
    port: 8080
  ignore:
    # Imported from https://github.com/corazawaf/coraza/blob/main/testing/coreruleset/.ftw.yml
    920100-4: 'Invalid uri, Coraza not reached - 404 page not found' 

    # Failing tests related to upstream issues:
    921250-1: 'Expected to match $Version in cookies, failing also in upstream'
    921250-2: 'Expected to match $Version in cookies, failing also in upstream'
    933120-2: 'To be investigated: match_regex value  might be ModSec specific'

    # HAProxy specific.
    920270-4: 'Rule works, log contains 920270. Test expects status 400 (Apache behaviour)'
    920290-1: 'Rule works, log contains 920290. Test expects status 400 (Apache behaviour)'
    920430-5: 'Test has expect_error, Go/http, Envoy and HAProxy return 400'
    920620-1: 'Rule works, log contains 920620. Test expects 200 (Apache behavour)'
    920430-8: 'Unknown HTTP Versions are blocked in HAProxy'
    920400-1: 'Limited Body content length because of protocol limitations.'

    # TODO investigate failing tests:
    922130-1:  ''
    922130-2:  ''
    922130-7:  ''
    942521-17: ''
    "920100-10": ''
    "920100-14": ''
    "920100-16": ''
    "920180-1": ''
    "920180-3": ''
    "920190-2": ''
    "920190-3": ''
    "920200-1": ''
    "920200-2": ''
    "920200-4": ''
    "920200-5": ''
    "920200-6": ''
    "920200-8": ''
    "920201-1": ''
    "920201-2": ''
    "920202-1": ''
    "920202-2": ''
    "920210-2": ''
    "920210-3": ''
    "920210-4": ''
    "920210-6": ''
    "920210-7": ''
    "920230-1": ''
    "920240-1": ''
    "920240-5": ''
    "920240-6": ''
    "920250-1": ''
    "920250-2": ''
    "920250-3": ''
    "920250-4": ''
    "920260-1": ''
    "920260-3": ''
    "920274-1": ''
    "920280-1": ''
    "920300-1": ''
    "920310-1": ''
    "920310-4": ''
    "920311-1": ''
    "920320-1": ''
    "920330-1": ''
    "920340-1": ''
    "920340-2": ''
    "920350-1": ''
    "920350-3": ''
    "920350-4": ''
    "920350-5": ''
    "920350-6": ''
    "920390-1": ''
    "920410-1": ''
    "934120-23": ''
    "934120-24": ''
    "934120-25": ''
    "934120-26": ''
    "934120-39": ''
    "942420-1": ''
    "942421-1": ''
    "942430-1": ''
    "942431-1": ''
    "942432-1": ''
    "942460-1": ''07070100000017000041ED00000000000000000000000268C6803E00000000000000000000000000000000000000000000002300000000coraza-spoa-0.4.0+git3/ftw/haproxy07070100000018000081A400000000000000000000000168C6803E0000037C000000000000000000000000000000000000002E00000000coraza-spoa-0.4.0+git3/ftw/haproxy/coraza.cfg# https://github.com/haproxy/haproxy/blob/master/doc/SPOE.txt
# /usr/local/etc/haproxy/coraza.cfg
[coraza]
spoe-agent coraza-agent
    messages    coraza-req      coraza-res
    groups      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(ftw) 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

spoe-message coraza-res
    args app=str(ftw) id=var(txn.coraza.id) version=res.ver status=status headers=res.hdrs body=res.body
    event on-http-response

spoe-group coraza-req
    messages coraza-req

spoe-group coraza-res
    messages coraza-res
07070100000019000081A400000000000000000000000168C6803E000007C5000000000000000000000000000000000000002F00000000coraza-spoa-0.4.0+git3/ftw/haproxy/haproxy.cfg# https://docs.haproxy.org/
global
    log stdout format raw local0

defaults
    log global
    option httplog
    timeout client 1m
    timeout server 1m
    timeout connect 10s

frontend default
    mode http
    bind *:8080
    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\ %[var(txn.coraza.id)]\ spoa-error:\ %[var(txn.coraza.error)]\ waf-hit:\ %[var(txn.coraza.fail)] waf-action:\ %[var(txn.coraza.action)] waf-data:\ %[var(txn.coraza.data)]"

    # Emulate Apache behavior by only allowing http 1.0, 1.1, 2.0 
    http-request deny deny_status 400 if !HTTP
    http-request deny deny_status 400 if !HTTP_1.0 !HTTP_1.1 !HTTP_2.0

    filter spoe engine coraza config /usr/local/etc/haproxy/coraza.cfg
    http-request send-spoe-group coraza coraza-req

    # 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 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 test

resolvers host_dns
    parse-resolv-conf

backend test
    mode http
    server test backend:8081

backend coraza-spoa
    mode tcp
    server coraza_spoa coraza-spoa:9000
0707010000001A000081ED00000000000000000000000168C6803E000005C2000000000000000000000000000000000000002400000000coraza-spoa-0.4.0+git3/ftw/tests.sh#!/bin/sh
# Copyright 2025 The OWASP Coraza contributors
# SPDX-License-Identifier: Apache-2.0

cd /workspace

# Revisited from https://github.com/corazawaf/coraza-proxy-wasm/blob/main/ftw/tests.sh

step=1
total_steps=1
max_retries=15 # Seconds for the server reachability timeout
host=${1:-haproxy}
health_url="http://${host}:8080"
log_file='/build/ftw-haproxy.log'

# Testing if the server is up
echo "[$step/$total_steps] Testing application reachability"
status_code="000"
while [[ "$status_code" -eq "000" ]]; do
  status_code=$(curl --write-out "%{http_code}" --silent --output /dev/null "$health_url")
  sleep 1
  echo -ne "[Wait] Waiting for response from $health_url. Timeout: ${max_retries}s   \r"
  let "max_retries--"
  if [[ "$max_retries" -eq 0 ]]; then
    echo "[Fail] Timeout waiting for response from $health_url, make sure the server is running."
    echo "HAProxy Logs:" && cat "$log_file"
    exit 1
  fi
done
if [[ "${status_code}" -ne "200" ]]; then
  echo "[Fail] Unexpected response with code ${status_code} from ${health_url}, expected 200."
  echo "HAProxy Logs:" && cat "$log_file"
  exit 1
fi
echo -e "\n[Ok] Got status code $status_code, expected 200. Ready to start."

FTW_CLOUDMODE=${FTW_CLOUDMODE:-false}

FTW_INCLUDE=$([ "${FTW_INCLUDE}" == "" ] && echo "" || echo "-i ${FTW_INCLUDE}")

/ftw run -d coreruleset/tests/regression/tests --config ftw.yml --read-timeout=10s --max-marker-retries=50 --cloud=$FTW_CLOUDMODE $FTW_INCLUDE || exit 1
0707010000001B000081A400000000000000000000000168C6803E00000684000000000000000000000000000000000000001E00000000coraza-spoa-0.4.0+git3/go.modmodule github.com/corazawaf/coraza-spoa

go 1.23.0

require (
	github.com/corazawaf/coraza-coreruleset/v4 v4.17.1
	github.com/corazawaf/coraza/v3 v3.3.3
	github.com/dropmorepackets/haproxy-go v0.0.7
	github.com/fsnotify/fsnotify v1.9.0
	github.com/jcchavezs/mergefs v0.1.0
	github.com/magefile/mage v1.15.1-0.20250615140142-78acbaf2e3ae
	github.com/mccutchen/go-httpbin/v2 v2.18.3
	github.com/prometheus/client_golang v1.23.2
	github.com/rs/zerolog v1.34.0
	gopkg.in/yaml.v3 v3.0.1
	istio.io/istio v0.0.0-20240218163812-d80ef7b19049
)

require (
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/corazawaf/libinjection-go v0.2.2 // indirect
	github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/common v0.66.1 // indirect
	github.com/prometheus/procfs v0.16.1 // indirect
	github.com/tidwall/gjson v1.18.0 // indirect
	github.com/tidwall/match v1.1.1 // indirect
	github.com/tidwall/pretty v1.2.1 // indirect
	github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
	go.yaml.in/yaml/v2 v2.4.2 // indirect
	golang.org/x/net v0.43.0 // indirect
	golang.org/x/sync v0.13.0 // indirect
	golang.org/x/sys v0.35.0 // indirect
	google.golang.org/protobuf v1.36.8 // indirect
	gopkg.in/yaml.v2 v2.4.0 // indirect
	rsc.io/binaryregexp v0.2.0 // indirect
)
0707010000001C000081A400000000000000000000000168C6803E000037D7000000000000000000000000000000000000001E00000000coraza-spoa-0.4.0+git3/go.sumgithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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-coreruleset/v4 v4.13.0 h1:02hXtxh0tCLR2LCKU/cmZxxjCgjgC0P1uM8DjJEreMY=
github.com/corazawaf/coraza-coreruleset/v4 v4.13.0/go.mod h1:yeZPZUM23HVL0jMAzLfKF3M7XjCQBXDrvcQT/gkjwhg=
github.com/corazawaf/coraza-coreruleset/v4 v4.14.0 h1:bblMqFRGQyREP81dsJ7RRiHua0LW7apuIrDwKy8qFHg=
github.com/corazawaf/coraza-coreruleset/v4 v4.14.0/go.mod h1:yeZPZUM23HVL0jMAzLfKF3M7XjCQBXDrvcQT/gkjwhg=
github.com/corazawaf/coraza-coreruleset/v4 v4.15.0 h1:rfdp5NK7ehB8+f0wcCaEAmPJ3h/7pD7KT085xyNVKIo=
github.com/corazawaf/coraza-coreruleset/v4 v4.15.0/go.mod h1:yeZPZUM23HVL0jMAzLfKF3M7XjCQBXDrvcQT/gkjwhg=
github.com/corazawaf/coraza-coreruleset/v4 v4.16.0 h1:xbC785u2JYTkoZpYDchW3NOys8sKdFBmh2JTpva1Czc=
github.com/corazawaf/coraza-coreruleset/v4 v4.16.0/go.mod h1:yeZPZUM23HVL0jMAzLfKF3M7XjCQBXDrvcQT/gkjwhg=
github.com/corazawaf/coraza-coreruleset/v4 v4.17.1 h1:p/ukmri8hNJcZI5PA1OY54sw1eVndRH7gwwMWMhtEgw=
github.com/corazawaf/coraza-coreruleset/v4 v4.17.1/go.mod h1:yeZPZUM23HVL0jMAzLfKF3M7XjCQBXDrvcQT/gkjwhg=
github.com/corazawaf/coraza/v3 v3.3.3 h1:kqjStHAgWqwP5dh7n0vhTOF0a3t+VikNS/EaMiG0Fhk=
github.com/corazawaf/coraza/v3 v3.3.3/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA=
github.com/corazawaf/libinjection-go v0.2.2 h1:Chzodvb6+NXh6wew5/yhD0Ggioif9ACrQGR4qjTCs1g=
github.com/corazawaf/libinjection-go v0.2.2/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.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 h1:a6aT2UrdS9MvV60ZLZnXFgi19jxRvVg/lJFQCiFYDFA=
github.com/dropmorepackets/haproxy-go v0.0.5/go.mod h1:4a2AmmVjvg2zPNdizGZrMN8ZSUpj90U43VlcdbOIBnU=
github.com/dropmorepackets/haproxy-go v0.0.6 h1:0u0u4MLS+mbIrYCQrIkHq8PQvt6ePJgF6ogTIFZQzx8=
github.com/dropmorepackets/haproxy-go v0.0.6/go.mod h1:4a2AmmVjvg2zPNdizGZrMN8ZSUpj90U43VlcdbOIBnU=
github.com/dropmorepackets/haproxy-go v0.0.7 h1:atXkB0MSRBZrAgpq+Vj/E4KysQ4CiI0O5QGUr+HvfTw=
github.com/dropmorepackets/haproxy-go v0.0.7/go.mod h1:4a2AmmVjvg2zPNdizGZrMN8ZSUpj90U43VlcdbOIBnU=
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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/jcchavezs/mergefs v0.1.0 h1:7oteO7Ocl/fnfFMkoVLJxTveCjrsd//UB0j89xmnpec=
github.com/jcchavezs/mergefs v0.1.0/go.mod h1:eRLTrsA+vFwQZ48hj8p8gki/5v9C2bFtHH5Mnn4bcGk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 h1:aAO0L0ulox6m/CLRYvJff+jWXYYCKGpEm3os7dM/Z+M=
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.15.1-0.20250615140142-78acbaf2e3ae h1:yyMUG1VUd6IjV5jonMKpLXgwm9AzkfRsYisdCXc5OVI=
github.com/magefile/mage v1.15.1-0.20250615140142-78acbaf2e3ae/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
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.18.1 h1:Zsi6gLCUS/kqZf36BicIbZsO8o0VyO2nSaOv/J8XKlQ=
github.com/mccutchen/go-httpbin/v2 v2.18.1/go.mod h1:GBy5I7XwZ4ZLhT3hcq39I4ikwN9x4QUt6EAxNiR8Jus=
github.com/mccutchen/go-httpbin/v2 v2.18.2 h1:UU5rd5ohZFX7ZyuTwINL4EBnParq0nM2JoJIVjA6hGQ=
github.com/mccutchen/go-httpbin/v2 v2.18.2/go.mod h1:GBy5I7XwZ4ZLhT3hcq39I4ikwN9x4QUt6EAxNiR8Jus=
github.com/mccutchen/go-httpbin/v2 v2.18.3 h1:DyckIScjHLJtmlSju+rgjqqI1nL8AdMZHsLSljlbnMU=
github.com/mccutchen/go-httpbin/v2 v2.18.3/go.mod h1:GBy5I7XwZ4ZLhT3hcq39I4ikwN9x4QUt6EAxNiR8Jus=
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/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/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.1 h1:w6gXMLQGgd0jXXlote9lRHMe0nG01EbnJT+C0EJru2Y=
github.com/prometheus/client_golang v1.23.1/go.mod h1:br8j//v2eg2K5Vvna5klK8Ku5pcU5r4ll73v6ik5dIQ=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
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.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.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=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
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 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=
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=
0707010000001D000041ED00000000000000000000000268C6803E00000000000000000000000000000000000000000000002000000000coraza-spoa-0.4.0+git3/internal0707010000001E000081A400000000000000000000000168C6803E00000C7A000000000000000000000000000000000000002900000000coraza-spoa-0.4.0+git3/internal/agent.gopackage internal

import (
	"context"
	"errors"
	"net"
	"sync"

	"github.com/dropmorepackets/haproxy-go/pkg/encoding"
	"github.com/dropmorepackets/haproxy-go/spop"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/rs/zerolog"
)

type Agent struct {
	Context            context.Context
	DefaultApplication *Application
	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) {
	timer := prometheus.NewTimer(handleSPOEDuration)
	defer timer.ObserveDuration()

	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", string(k.NameBytes())).Msg("unexpected kv entry")
		return
	}

	a.mtx.RLock()
	app := a.Applications[appName]
	a.mtx.RUnlock()
	if app == nil && a.DefaultApplication != nil {
		// If we cannot resolve the app but the default app is configured,
		// we use the latter to process the request.
		app = a.DefaultApplication
		a.Logger.Debug().Str("app", appName).Msg("app not found, using default app")
	}
	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")
}
0707010000001F000081A400000000000000000000000168C6803E00002EBE000000000000000000000000000000000000002F00000000coraza-spoa-0.4.0+git3/internal/application.gopackage internal

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/netip"
	"strings"
	"sync"
	"time"

	coreruleset "github.com/corazawaf/coraza-coreruleset/v4"
	"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
	TransactionTTL time.Duration
	LogFormat      string
}

type Application struct {
	waf   coraza.WAF
	cache cache.ExpiringCache

	AppConfig
}

type transaction struct {
	tx types.Transaction
	m  sync.Mutex
}

type applicationRequest struct {
	SrcIp   netip.Addr
	SrcPort int64
	DstIp   netip.Addr
	DstPort int64
	Method  string
	ID      string
	Path    []byte
	Query   []byte
	Version string
	Headers []byte
	Body    []byte
}

func (a *Application) HandleRequest(ctx context.Context, writer *encoding.ActionWriter, message *encoding.Message) (err error) {
	k := encoding.AcquireKVEntry()
	// run defer via anonymous function to not directly evaluate the arguments.
	defer func() {
		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
			// run defer via anonymous function to not directly evaluate the arguments.
			defer func() {
				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
			// run defer via anonymous function to not directly evaluate the arguments.
			defer func() {
				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
			// run defer via anonymous function to not directly evaluate the arguments.
			defer func() {
				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
			// run defer via anonymous function to not directly evaluate the arguments.
			defer func() {
				encoding.ReleaseKVEntry(currK)
			}()

			req.Body = currK.ValueBytes()
			// acquire a new kv entry to continue reading other message values.
			k = encoding.AcquireKVEntry()
		case "id":
			req.ID = string(k.ValueBytes())
		default:
			a.Logger.Debug().Str("name", name).Msg("unknown kv entry")
		}
	}

	// Check if we have received an id from haproxy
	if len(req.ID) == 0 {
		const idLength = 16
		var sb strings.Builder
		sb.Grow(idLength)
		for i := 0; i < idLength; i++ {
			sb.WriteRune(rune('A' + rand.Intn(26)))
		}
		req.ID = sb.String()
	}

	tx := a.waf.NewTransactionWithID(req.ID)
	defer func() {
		if err == nil && a.ResponseCheck {
			a.cache.SetWithExpiration(tx.ID(), &transaction{tx: tx}, a.TransactionTTL)
			return
		}

		tx.ProcessLogging()
		if err := tx.Close(); err != nil {
			a.Logger.Error().Str("tx", tx.ID()).Err(err).Msg("failed to close transaction")
		}
	}()

	if err := writer.SetString(encoding.VarScopeTransaction, "id", tx.ID()); err != nil {
		return err
	}

	if tx.IsRuleEngineOff() {
		a.Logger.Warn().Msg("Rule engine is Off, Coraza is not going to process any rule")
		return nil
	}

	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, tx.SetServerName); 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}
	}

	return nil
}

func readHeaders(headers []byte, hdrCallback func(key string, value string), hostCallback func(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])

		if hostCallback != nil && string(key) == "host" {
			hostCallback(string(value))
		}

		hdrCallback(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) (err error) {
	if !a.ResponseCheck {
		return fmt.Errorf("got response but response check is disabled")
	}

	k := encoding.AcquireKVEntry()
	// run defer via anonymous function to not directly evaluate the arguments.
	defer func() {
		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
			// run defer via anonymous function to not directly evaluate the arguments.
			defer func() {
				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
			// run defer via anonymous function to not directly evaluate the arguments.
			defer func() {
				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 not found: %s", res.ID)
	}
	a.cache.Remove(res.ID)

	t := cv.(*transaction)
	if !t.m.TryLock() {
		return fmt.Errorf("transaction is already being deleted: %s", res.ID)
	}
	tx := t.tx

	defer func() {
		tx.ProcessLogging()
		if err := tx.Close(); err != nil {
			a.Logger.Error().Str("tx", tx.ID()).Err(err).Msg("failed to close transaction")
		}
	}()

	if tx.IsRuleEngineOff() {
		goto exit
	}

	if err := readHeaders(res.Headers, tx.AddResponseHeader, nil); 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}
	}

exit:
	return nil
}

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.
		t := value.(*transaction)
		if !t.m.TryLock() {
			// We lost a race and the transaction is already somewhere in use.
			a.Logger.Info().Str("tx", t.tx.ID()).Msg("eviction called on currently used transaction")
			return
		}

		// Process Logging won't do anything if TX was already logged.
		t.tx.ProcessLogging()
		if err := t.tx.Close(); err != nil {
			a.Logger.Error().Err(err).Str("tx", t.tx.ID()).Msg("error closing transaction")
		}
	})

	return &app, nil
}

func matchedRuleErrorJson(mr types.MatchedRule) []byte {
	type errorLog struct {
		Client     string   `json:"client"`
		File       string   `json:"file"`
		Line       int      `json:"line"`
		RuleID     int      `json:"rule_id"`
		Revision   string   `json:"revision"`
		Msg        string   `json:"msg"`
		Data       string   `json:"data"`
		Severity   string   `json:"severity"`
		SeverityID int      `json:"severity_id"`
		Version    string   `json:"version"`
		Maturity   int      `json:"maturity"`
		Accuracy   int      `json:"accuracy"`
		Tags       []string `json:"tags"`
		Server     string   `json:"server"`
		URI        string   `json:"uri"`
		UniqueID   string   `json:"unique_id"`
		Disruptive bool     `json:"disruptive"`
		PhaseID    int      `json:"phase_id"`
		Phase      string   `json:"phase"`
	}

	r := mr.Rule()
	j, _ := json.Marshal(errorLog{
		File:       r.File(),
		Line:       r.Line(),
		RuleID:     r.ID(),
		Revision:   r.Revision(),
		Severity:   r.Severity().String(),
		SeverityID: r.Severity().Int(),
		Version:    r.Version(),
		Maturity:   r.Maturity(),
		Accuracy:   r.Accuracy(),
		Tags:       r.Tags(),
		Msg:        mr.Message(),
		Data:       mr.Data(),
		Client:     mr.ClientIPAddress(),
		Server:     mr.ServerIPAddress(),
		Disruptive: mr.Disruptive(),
		URI:        mr.URI(),
		UniqueID:   mr.TransactionID(),
		PhaseID:    int(r.Phase()),
		Phase:      phaseToString(r.Phase()),
	})
	return j
}

func phaseToString(phase types.RulePhase) string {
	switch phase {
	case types.PhaseRequestHeaders:
		return "request-headers"
	case types.PhaseRequestBody:
		return "request-body"
	case types.PhaseResponseHeaders:
		return "response-headers"
	case types.PhaseResponseBody:
		return "response-body"
	case types.PhaseLogging:
		return "logging"
	default:
		return "unknown"
	}
}

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()
	}
	switch a.LogFormat {
	case "json":
		l.RawJSON("match", matchedRuleErrorJson(mr)).Send()
	default:
		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
}
07070100000020000081A400000000000000000000000168C6803E00001062000000000000000000000000000000000000002C00000000coraza-spoa-0.4.0+git3/internal/e2e_test.go//go:build e2e

package internal

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"sync"
	"testing"
	"time"

	"github.com/corazawaf/coraza/v3/http/e2e"
	"github.com/dropmorepackets/haproxy-go/pkg/testutil"
	"github.com/mccutchen/go-httpbin/v2/httpbin"
	"github.com/rs/zerolog"
)

func TestE2E(t *testing.T) {
	t.Run("coraza e2e suite", func(t *testing.T) {
		config, bin, _ := runCoraza(t)
		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)
		}
	})
	t.Run("high request rate", func(t *testing.T) {
		config, _, _ := runCoraza(t)

		if os.Getenv("CI") != "" {
			t.Skip("CI is too slow for this test.")
		}

		var wg sync.WaitGroup
		for i := 0; i < 10; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				for i := 0; i < 100; i++ {
					req, _ := http.NewRequest("GET", "http://127.0.0.1:"+config.FrontendPort+"/get", http.NoBody)
					req.Header.Set("coraza-e2e", "ok")
					resp, _ := http.DefaultClient.Do(req)
					if resp.StatusCode != http.StatusOK {
						t.Error(resp.Status)
					}
				}
			}()
		}

		wg.Wait()
	})
}

func runCoraza(tb testing.TB) (testutil.HAProxyConfig, string, string) {
	s := httptest.NewServer(httpbin.New())
	tb.Cleanup(s.Close)

	logger := zerolog.New(os.Stderr).With().Timestamp().Logger()

	appCfg := AppConfig{
		Directives:     e2e.Directives,
		ResponseCheck:  true,
		Logger:         logger,
		TransactionTTL: 10 * time.Second,
	}

	application, err := appCfg.NewApplication()
	if err != nil {
		tb.Fatal(err)
	}

	a := Agent{
		Context:            context.Background(),
		DefaultApplication: application,
		Applications: map[string]*Application{
			"default": application,
		},
		Logger: logger,
	}

	// create the listener synchronously to prevent a race
	l := testutil.TCPListener(tb)
	// 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(tb)),
		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()),
	}

	frontendSocket := cfg.Run(tb)

	return cfg, s.URL, frontendSocket
}
07070100000021000081A400000000000000000000000168C6803E0000016A000000000000000000000000000000000000002B00000000coraza-spoa-0.4.0+git3/internal/metrics.gopackage internal

import (
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
	handleSPOEDuration = promauto.NewHistogram(
		prometheus.HistogramOpts{
			Name:    "coraza_handle_spoe_duration_seconds",
			Help:    "Duration of Coraza SPOE handling",
			Buckets: prometheus.DefBuckets,
		},
	)
)
07070100000022000081A400000000000000000000000168C6803E00000153000000000000000000000000000000000000001F00000000coraza-spoa-0.4.0+git3/mage.go// Copyright 2022 The OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

//go:build ignore
// +build ignore

// Entrypoint to mage for running without needing to install the command.
// https://magefile.org/zeroinstall/
package main

import (
	"os"

	"github.com/magefile/mage/mage"
)

func main() {
	os.Exit(mage.Main())
}
07070100000023000081A400000000000000000000000168C6803E00001290000000000000000000000000000000000000002300000000coraza-spoa-0.4.0+git3/magefile.go// Copyright 2024 The OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

//go:build mage
// +build mage

package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"

	"github.com/magefile/mage/mg"
	"github.com/magefile/mage/sh"
)

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.62.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),
		"-c", "The OWASP Coraza contributors",
		"-s=only",
		"-ignore", "**/*.yml",
		"-ignore", "**/*.yaml",
		"-ignore", "examples/**", "."); err != nil {
		return err
	}
	return sh.RunV("go", "run", fmt.Sprintf("github.com/rinchsan/gosimports/cmd/gosimports@%s", gosImportsVer),
		"-w",
		"-local",
		"github.com/corazawaf/coraza-spoa",
		".")
}

func Build() error {
	arch := os.Getenv("ARCH")
	if arch == "" {
		arch = runtime.GOARCH
	}
	gitVersion, _ := sh.Output("git", "describe", "--tags", "--always", "--dirty")
	ldflags := fmt.Sprintf("-X 'main.version=%s'", gitVersion)
	if err := sh.RunWith(map[string]string{
		"GOARCH": arch,
	}, "go", "build", "-ldflags="+ldflags, "-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 := 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
}

// Test runs all tests.
func Test() error {
	if err := sh.RunV("go", "test", "./..."); err != nil {
		return err
	}

	// we specify the package to get streaming test output
	if err := sh.RunV("go", "test", "-race", "-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) {
		return errNoGitDir
	}

	f, err := os.ReadFile(".pre-commit.hook")
	if err != nil {
		return err
	}

	return os.WriteFile(filepath.Join(".git", "hooks", "pre-commit"), f, 0755)
}

// Check runs lint and tests.
func Check() {
	mg.SerialDeps(Lint, Test)
}

// Ftw runs CRS regressions tests. Requires docker.
func Ftw() error {
	env := map[string]string{
		"FTW_CLOUDMODE":       os.Getenv("FTW_CLOUDMODE"),
		"FTW_INCLUDE":         os.Getenv("FTW_INCLUDE"),
		"FTW_HAPROXY_VERSION": os.Getenv("FTW_HAPROXY_VERSION"),
	}
	if err := sh.RunWithV(env, "docker", "compose", "--file", "ftw/docker-compose.yml", "build", "--pull", "--no-cache"); err != nil {
		return err
	}
	defer func() {
		_ = sh.RunWithV(env, "docker", "compose", "--file", "ftw/docker-compose.yml", "down", "-v")
	}()

	return sh.RunWithV(env, "docker", "compose", "--file", "ftw/docker-compose.yml", "run", "--rm", "ftw")
}
07070100000024000081A400000000000000000000000168C6803E000010DA000000000000000000000000000000000000001F00000000coraza-spoa-0.4.0+git3/main.go// Copyright The OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"context"
	"flag"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/signal"
	"runtime"
	"runtime/debug"
	"runtime/pprof"
	"syscall"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/rs/zerolog"

	"github.com/corazawaf/coraza-spoa/internal"
)

var (
	version = "dev"
)

var (
	configPath     string
	validateConfig bool
	autoReload     bool
	cpuProfile     string
	memProfile     string
	metricsAddr    string
	showVersion    bool
	globalLogger   = zerolog.New(os.Stderr).With().Timestamp().Logger()
)

func main() {
	flag.StringVar(&configPath, "config", "", "configuration file")
	flag.BoolVar(&validateConfig, "validate", false, "validate configuration file and exit")
	flag.BoolVar(&autoReload, "autoreload", false, "reload configuration file on k8s configmap update")
	flag.StringVar(&cpuProfile, "cpuprofile", "", "write cpu profile to `file`")
	flag.StringVar(&memProfile, "memprofile", "", "write memory profile to `file`")
	flag.StringVar(&metricsAddr, "metrics-addr", "", "ip:port bind for prometheus metrics")
	flag.BoolVar(&showVersion, "version", false, "show version and exit")
	flag.Parse()

	if showVersion {
		fmt.Printf("version\t%s\n", version)
		if bi, ok := debug.ReadBuildInfo(); ok {
			fmt.Printf("%s", bi.String())
		}
		return
	}

	if configPath == "" {
		globalLogger.Fatal().Msg("Configuration file is not set")
	}

	if cpuProfile != "" {
		f, err := os.Create(cpuProfile)
		if err != nil {
			globalLogger.Fatal().Err(err).Msg("Could not create CPU profile")
		}
		defer f.Close()
		if err := pprof.StartCPUProfile(f); err != nil {
			globalLogger.Fatal().Err(err).Msg("Could not start CPU profile")
		}
		defer pprof.StopCPUProfile()
	}

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

	if validateConfig {
		globalLogger.Info().Msg("Configuration file is valid")
		return
	}

	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,
		DefaultApplication: apps[cfg.DefaultApplication],
		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")
		}
	}()

	if metricsAddr != "" {
		go func() {
			http.Handle("/metrics", promhttp.Handler())
			if err := http.ListenAndServe(metricsAddr, nil); err != nil {
				globalLogger.Error().Err(err).Msg("Metrics server failed")
			}
		}()
	}

	if autoReload {
		go func() {
			if err := cfg.watchConfig(a); err != nil {
				globalLogger.Fatal().Err(err).Msg("Config watcher failed")
			}
		}()
	}

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGINT)
outer:
	for {
		sig := <-sigCh
		switch sig {
		case syscall.SIGTERM:
			globalLogger.Info().Msg("Received SIGTERM, shutting down...")
			// this return will run cancel() and close the server
			break outer
		case syscall.SIGINT:
			globalLogger.Info().Msg("Received SIGINT, shutting down...")
			break outer
		case syscall.SIGHUP:
			globalLogger.Info().Msg("Received SIGHUP, reloading configuration...")
			newCfg, err := cfg.reloadConfig(a)
			if err != nil {
				globalLogger.Error().Err(err).Msg("Failed to reload configuration, using old configuration")
				continue
			}
			cfg = newCfg
		}
	}

	if memProfile != "" {
		f, err := os.Create(memProfile)
		if err != nil {
			globalLogger.Fatal().Err(err).Msg("Could not create memory profile")
		}
		defer f.Close()
		runtime.GC()
		if err := pprof.WriteHeapProfile(f); err != nil {
			globalLogger.Fatal().Err(err).Msg("Could not write memory profile")
		}
	}
}
07070100000025000081A400000000000000000000000168C6803E000000FA000000000000000000000000000000000000002500000000coraza-spoa-0.4.0+git3/renovate.json{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "local>corazawaf/renovate-config"
  ],
  "packageRules": [
    {
      "matchPackageNames": [
        "istio.io/istio"
      ],
      "enabled": false
    }
  ]
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!225 blocks
openSUSE Build Service is sponsored by