An open-source, enterprise-grade, high-performance Web Application Firewall (WAF) library for the JVM, made to protect your webapps.
SecLang Engine is written in Scala and implements the ModSecurity SecLang rule language, providing full compatibility with the OWASP Core Rule Set (CRS) v4.
- 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
SecLangIntegrationtrait. 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.
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
}
}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)
}
}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();
}
}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)
}
}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
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"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>For Gradle projects:
implementation 'com.cloud-apim:seclang-engine_2.12:1.0.0'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 packageThe project uses the official OWASP Core Rule Set (CRS) test suite to validate the engine implementation.
Before running the tests, you need to download the CRS test data:
./setuptest.shThis script clones the CRS repository into test-data/coreruleset/.
# Run all tests
sbt test
# Run a specific test suite
sbt "testOnly *SecLangBasicTest"
sbt "testOnly *SecLangCRSTest"
# Run a specific test
sbt "testOnly *SecLangBasicTest -- *simple*"The file crs-tests-status.json contains the current status of the CRS test suite, including passing/failing tests and their failure reasons.
replaceNullsparityEven7bitparityOdd7bitparityZero7bitsqlHexDecode
AUTH_TYPEFULL_REQUESTFULL_REQUEST_LENGTHHIGHEST_SEVERITYINBOUND_DATA_ERRORMODSEC_BUILDMSC_PCRE_LIMITS_EXCEEDEDMULTIPART_CRLF_LF_LINESMULTIPART_FILENAMEMULTIPART_NAMEMULTIPART_STRICT_ERRORMULTIPART_UNMATCHED_BOUNDARYOUTBOUND_DATA_ERRORREQBODY_ERRORREQBODY_ERROR_MSGRULESDBM_DELETE_ERRORSESSIONSESSIONIDURLENCODED_ERRORWEBAPPID
verifyCCverifyCPFverifySSNrblrxGlobalfuzzyHash