Skip to content

SecLang Engine WAF is a ModSecurity-compatible Web Application Firewall (WAF) library for the JVM, written in Scala.

License

Notifications You must be signed in to change notification settings

cloud-apim/seclang-engine

Cloud APIM - SecLang Engine

An open-source, enterprise-grade, high-performance Web Application Firewall (WAF) library for the JVM, made to protect your webapps.

CI Maven Central License Scala 2.12 CRS v4 Compatibility

SecLang Engine is written in Scala and implements the ModSecurity SecLang rule language, providing full compatibility with the OWASP Core Rule Set (CRS) v4.

Key Features

  • CRS Compatibility - Runs the OWASP Core Rule Set v4 out of the box, protecting your applications against SQL Injection, Cross-Site Scripting (XSS), Remote Code Execution, and other threats from the OWASP Top Ten.
  • Multi-Tenant by Design - Built from the ground up for multi-tenant environments, allowing different rule configurations per tenant with isolated execution contexts and shared rule presets.
  • JVM Native - Runs natively on the JVM with Scala 2.12 support. Easily integrates with existing Java/Scala applications, API gateways, and reverse proxies.
  • High Performance - Optimized for high-throughput scenarios with compiled rule programs, regex caching, and efficient variable resolution. Handles thousands of requests per second with minimal latency overhead.
  • Library-First - Designed as an embeddable library, not a standalone server. Integrate WAF capabilities directly into your application, gateway, or proxy with a simple API.
  • Extensible - Customize logging, caching, and integration points through the SecLangIntegration trait. Compose rule presets and configurations dynamically at runtime.
  • Battle-Tested - Passes 100% of the official CRS test suite, ensuring reliable protection against real-world attack patterns.

Status

This project is a work in progress. We are currently passing 100% of the CRS test suite

{
  "global_stats" : {
    "failure_percentage" : 0,
    "passing_percentage" : 100,
    "total_tests" : 4135,
    "success_tests" : 4135,
    "failure_tests" : 0
  },
  "time_stats" : {
    "calls" : 4135,
    "total_time_ms" : 35945,
    "min_time_ns" : 1727500,
    "min_time_ms" : 1,
    "max_time_ms" : 4242,
    "avg_time_ms" : 8
  }
}

Simple usage

import com.cloud.apim.seclang.model.Disposition._
import com.cloud.apim.seclang.model._
import com.cloud.apim.seclang.scaladsl.SecLang

class SecLangTest extends munit.FunSuite {
  
  test("simple seclang rules") {
    val rules = """
                  |SecRule REQUEST_HEADERS:User-Agent "@pm firefox" \
                  |    "id:00001,\
                  |    phase:1,\
                  |    block,\
                  |    t:none,t:lowercase,\
                  |    msg:'someone used firefox to access',\
                  |    logdata:'someone used firefox to access',\
                  |    tag:'test',\
                  |    ver:'0.0.0-dev',\
                  |    status:403,\
                  |    severity:'CRITICAL'"
                  |
                  |SecRule REQUEST_URI "@contains /health" \
                  |    "id:00002,\
                  |    phase:1,\
                  |    pass,\
                  |    t:none,t:lowercase,\
                  |    msg:'someone called /health',\
                  |    logdata:'someone called /health',\
                  |    tag:'test',\
                  |    ver:'0.0.0-dev'"
                  |
                  |SecRuleEngine On
                  |""".stripMargin

    val loaded = SecLang.parse(rules).fold(err => sys.error(err), identity)
    val program = SecLang.compile(loaded)
    val engine = SecLang.engine(program)

    val failing_ctx_2 = RequestContext(
      method = "GET",
      uri = "/",
      headers = Map("User-Agent" -> List("Firefox/128.0")),
      query = Map("q" -> List("test")),
      body = None
    )
    val passing_ctx_1 = RequestContext(
      method = "GET",
      uri = "/health",
      headers = Map("User-Agent" -> List("curl/8.0")),
      query = Map("q" -> List("test")),
      body = None
    )
    val passing_ctx_2 = RequestContext(
      method = "GET",
      uri = "/admin",
      headers = Map("User-Agent" -> List("chrome/8.0")),
      query = Map("q" -> List("test")),
      body = None
    )

    val failing_res_2 = engine.evaluate(failing_ctx_2, phases = List(1, 2))
    val passing_res_1 = engine.evaluate(passing_ctx_1, phases = List(1, 2))
    val passing_res_2 = engine.evaluate(passing_ctx_2, phases = List(1, 2))

    assertEquals(failing_res_2.disposition, Block(403, Some("someone used firefox to access"), Some(1)))
    assertEquals(passing_res_1.disposition, Continue)
    assertEquals(passing_res_2.disposition, Continue)
  }
}

Simple usage (Java)

import com.cloud.apim.seclang.javadsl.*;
import com.cloud.apim.seclang.model.CompiledProgram;

import java.util.Arrays;
import java.util.HashMap;

public class SecLangExample {

    public static void main(String[] args) {
        String rules = """
            SecRule REQUEST_HEADERS:User-Agent "@pm firefox" \\
                "id:00001,\\
                phase:1,\\
                block,\\
                t:none,t:lowercase,\\
                msg:'someone used firefox to access',\\
                logdata:'someone used firefox to access',\\
                tag:'test',\\
                ver:'0.0.0-dev',\\
                status:403,\\
                severity:'CRITICAL'"

            SecRule REQUEST_URI "@contains /health" \\
                "id:00002,\\
                phase:1,\\
                pass,\\
                t:none,t:lowercase,\\
                msg:'someone called /health',\\
                logdata:'someone called /health',\\
                tag:'test',\\
                ver:'0.0.0-dev'"

            SecRuleEngine On
            """;

        // Parse and compile rules
        SecLang.ParseResult parseResult = SecLang.parse(rules);
        if (parseResult.isError()) {
            System.err.println("Parse error: " + parseResult.getError());
            return;
        }

        CompiledProgram program = SecLang.compile(parseResult.getConfiguration());

        // Create engine (with default config, no files, default integration)
        JSecLangEngine engine = SecLang.engine(program);

        // Or with custom configuration:
        // JSecLangEngine engine = SecLang.engine(
        //     program,
        //     JSecLangEngineConfig.defaultConfig(),
        //     new HashMap<>(),
        //     JSecLangIntegration.defaultIntegration()
        // );

        // Build request contexts
        JRequestContext failingCtx = JRequestContext.builder()
            .method("GET")
            .uri("/")
            .header("User-Agent", "Firefox/128.0")
            .queryParam("q", "test")
            .build();

        JRequestContext passingCtx1 = JRequestContext.builder()
            .method("GET")
            .uri("/health")
            .header("User-Agent", "curl/8.0")
            .queryParam("q", "test")
            .build();

        JRequestContext passingCtx2 = JRequestContext.builder()
            .method("GET")
            .uri("/admin")
            .header("User-Agent", "chrome/8.0")
            .queryParam("q", "test")
            .build();

        // Evaluate requests against rules
        JEngineResult failingRes = engine.evaluate(failingCtx, Arrays.asList(1, 2));
        JEngineResult passingRes1 = engine.evaluate(passingCtx1, Arrays.asList(1, 2));
        JEngineResult passingRes2 = engine.evaluate(passingCtx2, Arrays.asList(1, 2));

        // Check results
        if (failingRes.isBlocked()) {
            System.out.println("Request blocked!");
            System.out.println("  Status: " + failingRes.getDisposition().getStatus());
            System.out.println("  Message: " + failingRes.getDisposition().getMessage().orElse("N/A"));
            System.out.println("  Rule ID: " + failingRes.getDisposition().getRuleId().orElse(0));
        }

        assert failingRes.isBlocked();
        assert failingRes.getDisposition().getStatus() == 403;
        assert passingRes1.isContinue();
        assert passingRes2.isContinue();
    }
}

Factory with presets usage

import com.cloud.apim.seclang.model.Disposition._
import com.cloud.apim.seclang.model._
import com.cloud.apim.seclang.scaladsl.SecLang

class SecLangFactoryTest extends munit.FunSuite {

  test("simple factory test") {
    val presets = Map(
      "no_firefox" -> SecLangPreset.withNoFiles("no_firefox", """
           |SecRule REQUEST_HEADERS:User-Agent "@pm firefox" \
           |   "id:00001,\
           |   phase:1,\
           |   block,\
           |   t:none,t:lowercase,\
           |   msg:'someone used firefox to access',\
           |   logdata:'someone used firefox to access',\
           |   tag:'test',\
           |   ver:'0.0.0-dev',\
           |   status:403,\
           |   severity:'CRITICAL'"
           |""".stripMargin),
      "health_check" -> SecLangPreset.withNoFiles("health_check", """
           |SecRule REQUEST_URI "@contains /health" \
           |   "id:00002,\
           |   phase:1,\
           |   pass,\
           |   t:none,t:lowercase,\
           |   msg:'someone called /health',\
           |   logdata:'someone called /health',\
           |   tag:'test',\
           |   ver:'0.0.0-dev'"
           |""".stripMargin)
    )
    val factory = SecLang.factory(presets)
    val rulesConfig = List(
      "@import_preset no_firefox",
      "@import_preset health_check",
      "SecRuleEngine On"
    )
    val failing_ctx_2 = RequestContext(
      method = "GET",
      uri = "/",
      headers = Map("User-Agent" -> List("Firefox/128.0")),
      query = Map("q" -> List("test")),
      body = None
    )
    val passing_ctx_1 = RequestContext(
      method = "GET",
      uri = "/health",
      headers = Map("User-Agent" -> List("curl/8.0")),
      query = Map("q" -> List("test")),
      body = None
    )
    val passing_ctx_2 = RequestContext(
      method = "GET",
      uri = "/admin",
      headers = Map("User-Agent" -> List("chrome/8.0")),
      query = Map("q" -> List("test")),
      body = None
    )

    val failing_res_2 = factory.evaluate(rulesConfig, failing_ctx_2, phases = List(1, 2))
    val passing_res_1 = factory.evaluate(rulesConfig, passing_ctx_1, phases = List(1, 2))
    val passing_res_2 = factory.evaluate(rulesConfig, passing_ctx_2, phases = List(1, 2))

    assertEquals(failing_res_2.disposition, Block(403, Some("someone used firefox to access"), Some(1)))
    assertEquals(passing_res_1.disposition, Continue)
    assertEquals(passing_res_2.disposition, Continue)
  }
}

Installation

Add the following dependency to your build.sbt:

libraryDependencies += "com.cloud-apim" %% "seclang-engine" % "1.0.0"

The library is compiled for Scala 2.12

Using Snapshots

To use snapshot versions, add the Sonatype snapshots repository:

resolvers += "Sonatype OSS Snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots"

libraryDependencies += "com.cloud-apim" %% "seclang-engine" % "1.0.0-SNAPSHOT"

Maven

For Maven projects, add to your pom.xml:

<dependency>
  <groupId>com.cloud-apim</groupId>
  <artifactId>seclang-engine_2.12</artifactId>
  <version>1.0.0</version>
</dependency>

Gradle

For Gradle projects:

implementation 'com.cloud-apim:seclang-engine_2.12:1.0.0'

Build

To build the project from source:

# Clone the repository
git clone https://github.com/cloud-apim/seclang-engine.git
cd seclang-engine

# Compile
sbt compile

# Package
sbt package

Testing

The project uses the official OWASP Core Rule Set (CRS) test suite to validate the engine implementation.

Setup

Before running the tests, you need to download the CRS test data:

./setuptest.sh

This script clones the CRS repository into test-data/coreruleset/.

Running tests

# Run all tests
sbt test

# Run a specific test suite
sbt "testOnly *SecLangBasicTest"
sbt "testOnly *SecLangCRSTest"

# Run a specific test
sbt "testOnly *SecLangBasicTest -- *simple*"

Test status

The file crs-tests-status.json contains the current status of the CRS test suite, including passing/failing tests and their failure reasons.

Missing stuff

Missing transformations

  • replaceNulls
  • parityEven7bit
  • parityOdd7bit
  • parityZero7bit
  • sqlHexDecode

Missing variables

  • AUTH_TYPE
  • FULL_REQUEST
  • FULL_REQUEST_LENGTH
  • HIGHEST_SEVERITY
  • INBOUND_DATA_ERROR
  • MODSEC_BUILD
  • MSC_PCRE_LIMITS_EXCEEDED
  • MULTIPART_CRLF_LF_LINES
  • MULTIPART_FILENAME
  • MULTIPART_NAME
  • MULTIPART_STRICT_ERROR
  • MULTIPART_UNMATCHED_BOUNDARY
  • OUTBOUND_DATA_ERROR
  • REQBODY_ERROR
  • REQBODY_ERROR_MSG
  • RULE
  • SDBM_DELETE_ERROR
  • SESSION
  • SESSIONID
  • URLENCODED_ERROR
  • WEBAPPID

Missing operators

  • verifyCC
  • verifyCPF
  • verifySSN
  • rbl
  • rxGlobal
  • fuzzyHash

About

SecLang Engine WAF is a ModSecurity-compatible Web Application Firewall (WAF) library for the JVM, written in Scala.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published