Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ jobs:
with:
persist-credentials: false

# The engine dependency: guard-core-jvm pinned at 2415a77 (full SHA for the
# The engine dependency: guard-core-jvm pinned at 9067b7b (full SHA for the
# actions/checkout fetch), checked
# out as the sibling directory ./guard-core-jvm and substituted for the
# declared com.guardcore:guard-core coordinates by settings.gradle.kts.
- name: Checkout engine (guard-core-jvm @ 2415a77)
- name: Checkout engine (guard-core-jvm @ 9067b7b)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Guard-Core/guard-core-jvm
ref: 2415a77b35f99dbaa96ad821b7ef4571992e18f4
ref: 9067b7b247d73790cc93b4cb433a1e973265a473
path: guard-core-jvm
persist-credentials: false

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/code-ql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ jobs:

# The engine dependency: the composite includeBuild needs the pinned
# guard-core-jvm checkout next to the repository (same as ci.yml).
- name: Checkout engine (guard-core-jvm @ 2415a77)
- name: Checkout engine (guard-core-jvm @ 9067b7b)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Guard-Core/guard-core-jvm
ref: 2415a77b35f99dbaa96ad821b7ef4571992e18f4
ref: 9067b7b247d73790cc93b4cb433a1e973265a473
path: guard-core-jvm
persist-credentials: false

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ jobs:
persist-credentials: false

# Same engine checkout as ci.yml (the composite default for tests).
- name: Checkout engine (guard-core-jvm @ 2415a77)
- name: Checkout engine (guard-core-jvm @ 9067b7b)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Guard-Core/guard-core-jvm
ref: 2415a77b35f99dbaa96ad821b7ef4571992e18f4
ref: 9067b7b247d73790cc93b4cb433a1e973265a473
path: guard-core-jvm
persist-credentials: false

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changed

- Telemetry agent support: an `AgentHandlerProtocol` bean (for example guard-agent-jvm's `GuardAgentTelemetry`) is now injected into `GuardMiddleware` by the auto-configuration, adopted into the engine composite on initialization, and stopped through the new `GuardMiddleware.shutdown()` when the context closes. Before this, `guard.enable-agent=true` always degraded with a "W7 artifact" message, because the middleware had no way to receive a handler. The degrade path now only applies when no handler bean is registered. The pinned guard-core-jvm engine ref moves to 9067b7b, whose agent telemetry wiring fix (Guard-Core/guard-core-jvm#48) the handler needs to receive anything.
- Dependencies: netty is pinned to 4.1.139.Final through `platform(io.netty:netty-bom)`. lettuce-core 6.8.2 declares netty 4.1.125, which carries twelve advisories fixed by 4.1.137 at the latest. The pin is declared here as well as in the guard-core-jvm facade, so this lockfile stays the same whichever engine revision the build resolves. The four netty osv-scanner suppressions are removed; the Spring Boot test-scope suppressions stay.

## [1.0.0] - 2026-10-13
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ side in the same directory) at the pinned ref:

```bash
git clone https://github.com/Guard-Core/guard-core-jvm ../guard-core-jvm
git -C ../guard-core-jvm checkout 2415a77
git -C ../guard-core-jvm checkout 9067b7b
./gradlew build # detekt + ktfmt check + tests (redis-backed tests skip when redis is down)
```

Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ logs the same "eager init not honored" warning as fastapi-guard.

The engine artifact `com.guard-core:guard-core` (groupId is the Central Portal namespace; Kotlin
packages stay `com.guardcore.*`) is on Maven Central for consumers. This repository's own build pins the engine from source instead: CI and local builds check out `Guard-Core/guard-core-jvm` at the
pinned ref `2415a77` as the sibling directory `./guard-core-jvm`, and `settings.gradle.kts`
pinned ref `9067b7b` as the sibling directory `./guard-core-jvm`, and `settings.gradle.kts`
substitutes the declared coordinates `com.guard-core:guard-core:4.3.3` /
`com.guard-core:guard-core-engine:4.3.3` with the included build's projects (the Gradle analog of
the Rust path+version pattern). The declared dependency coordinates stay
Expand All @@ -152,7 +152,7 @@ Local checkout:

```bash
git clone https://github.com/Guard-Core/guard-core-jvm.git guard-core-jvm
git -C guard-core-jvm checkout 2415a77
git -C guard-core-jvm checkout 9067b7b
./gradlew build
```

Expand All @@ -165,6 +165,32 @@ Rate limiting, IP banning, and behavioral tracking run through the facade manage
Redis 6+ (`guard.redis-url`). The test suite uses the live `localhost:6379` (CI wires a redis
service container); redis-backed tests skip when no server answers.

## Telemetry agent

The adapter never builds the agent itself. Register a guard-agent-jvm handler as an `AgentHandlerProtocol` bean and set `guard.enable-agent=true` (with `guard.agent-api-key`, which the engine config requires when the agent is enabled). The auto-configuration hands the bean to `GuardMiddleware`, which starts it on initialization and stops it, flushing its buffer, when the application context closes:

```kotlin
import com.guardcore.agent.GuardAgent
import com.guardcore.agent.GuardAgentTelemetry
import com.guardcore.agent.defaultConfig
import com.guardcore.protocols.AgentHandlerProtocol
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class GuardAgentConfig {
@Bean
fun guardAgentTelemetry(): AgentHandlerProtocol {
val cfg = defaultConfig()
cfg.apiKey = "YOUR_API_KEY"
cfg.projectId = "YOUR_PROJECT_ID"
return GuardAgentTelemetry(GuardAgent.create(cfg))
}
}
```

With `guard.enable-agent=true` and no such bean, the middleware degrades like the reference adapter: it logs the missing handler and continues without agent functionality (or fails startup when `guard.agent-strict=true`).

## Example

`examples/demo-boot` is a runnable Boot app with guarded + open routes and the README run
Expand Down
2 changes: 1 addition & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ routes, live Redis-backed rate limiting, and per-route decorator configs.
git clone https://github.com/Guard-Core/spring-boot-guard.git
cd spring-boot-guard
git clone https://github.com/Guard-Core/guard-core-jvm.git guard-core-jvm
git -C guard-core-jvm checkout 2415a77
git -C guard-core-jvm checkout 9067b7b
docker run -d -p 6379:6379 redis:7-alpine
./gradlew :examples:demo-boot:bootRun
```
Expand Down
4 changes: 2 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ packages stay `com.guardcore.*`.
## Engine dependency (includeBuild + dependency substitution)

Consumers resolve the engine artifact `com.guard-core:guard-core` from Maven Central. This repository's own build pins the engine from source instead: CI and
local builds check out `Guard-Core/guard-core-jvm` at the pinned ref `2415a77`
local builds check out `Guard-Core/guard-core-jvm` at the pinned ref `9067b7b`
as the sibling directory `./guard-core-jvm`, and `settings.gradle.kts`
substitutes the declared coordinates `com.guard-core:guard-core:4.3.3` /
`com.guard-core:guard-core-engine:4.3.3` with the included build's projects.
Expand All @@ -32,7 +32,7 @@ The declared dependency coordinates stay `com.guard-core:guard-core:4.3.3`

```bash
git clone https://github.com/Guard-Core/guard-core-jvm.git guard-core-jvm
git -C guard-core-jvm checkout 2415a77
git -C guard-core-jvm checkout 9067b7b
./gradlew build
```

Expand Down
4 changes: 2 additions & 2 deletions examples/demo-boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ engine's `SecurityConfig` field names).

Prerequisites:

1. Clone Guard-Core/guard-core-jvm at the pinned ref `2415a77` next to this repository
1. Clone Guard-Core/guard-core-jvm at the pinned ref `9067b7b` next to this repository
(the engine builds from source via includeBuild until the artifacts publish):

```bash
git clone https://github.com/Guard-Core/guard-core-jvm.git guard-core-jvm
git -C guard-core-jvm checkout 2415a77
git -C guard-core-jvm checkout 9067b7b
```

2. A Redis on `localhost:6379` (rate limiting), or set `guard.enable-redis: false` in
Expand Down
4 changes: 2 additions & 2 deletions examples/demo-boot/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ rootProject.name = "demo-boot"

// The adapter and the engine are consumed from source until both publish:
// the adapter repo sits two levels up and the engine at the pinned ref
// 2415a77 sits next to it (the same includeBuild convention as the adapter).
// 9067b7b sits next to it (the same includeBuild convention as the adapter).
includeBuild("../../") {
dependencySubstitution {
substitute(module("com.guardcore:spring-boot-guard")).using(project(":"))
Expand All @@ -35,7 +35,7 @@ val engineDir: File = run {
)
roots.firstOrNull { it.resolve("settings.gradle.kts").isFile }
?: throw GradleException(
"Clone Guard-Core/guard-core-jvm at the pinned ref 2415a77 next to the " +
"Clone Guard-Core/guard-core-jvm at the pinned ref 9067b7b next to the " +
"spring-boot-guard repository first (or point -PguardCoreJvmDir / " +
"GUARD_CORE_JVM_DIR at an existing checkout). Searched: " +
roots.joinToString(", ") { it.path },
Expand Down
4 changes: 2 additions & 2 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pluginManagement {
// GUARD_CORE_USE_COMPOSITE (environment variable):
// unset / "1" / "true" -> composite ON (default). The engine is built from
// source via includeBuild + dependency substitution: CI (and local builds)
// check out Guard-Core/guard-core-jvm at the PINNED REF 2415a77 as the
// check out Guard-Core/guard-core-jvm at the PINNED REF 9067b7b as the
// sibling directory ./guard-core-jvm, and the block below substitutes the
// declared coordinates com.guard-core:guard-core /
// com.guard-core:guard-core-engine with the included build's projects.
Expand Down Expand Up @@ -63,7 +63,7 @@ if (useComposite) {
roots.firstOrNull { it.resolve("settings.gradle.kts").isFile }
?: throw GradleException(
"The guard-core-jvm engine checkout was not found. Clone Guard-Core/guard-core-jvm " +
"at the pinned ref 2415a77 next to this repository (or point -PguardCoreJvmDir / " +
"at the pinned ref 9067b7b next to this repository (or point -PguardCoreJvmDir / " +
"GUARD_CORE_JVM_DIR at an existing checkout). Searched: " +
roots.joinToString(", ") { it.path },
)
Expand Down
81 changes: 70 additions & 11 deletions src/main/kotlin/com/guardcore/spring/GuardMiddleware.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.guardcore.core.bypass.BypassContext
import com.guardcore.core.bypass.BypassHandler
import com.guardcore.core.checks.ChecksFactory
import com.guardcore.core.checks.SecurityCheckPipeline
import com.guardcore.core.events.CompositeAgentHandler
import com.guardcore.core.events.MetricsCollector
import com.guardcore.core.events.SecurityEventBus
import com.guardcore.core.initialization.HandlerInitializer
Expand All @@ -22,6 +23,7 @@ import com.guardcore.errors.GuardRedisError
import com.guardcore.handlers.BehaviorRule
import com.guardcore.handlers.CloudManager
import com.guardcore.handlers.CorsHandler
import com.guardcore.handlers.HandlerAgentSink
import com.guardcore.handlers.RateLimitManager
import com.guardcore.handlers.RedisManager
import com.guardcore.handlers.SecurityHeadersManager
Expand All @@ -33,6 +35,7 @@ import com.guardcore.protocols.GuardResponse
import com.guardcore.protocols.GuardResponseFactory
import com.guardcore.protocols.MiddlewareEventBusProtocol
import com.guardcore.protocols.MiddlewareResponseFactoryProtocol
import com.guardcore.protocols.RedisHandlerProtocol
import com.guardcore.util.IpExtraction
import com.guardcore.util.LoggingUtils
import java.util.logging.Logger
Expand All @@ -55,6 +58,8 @@ public class GuardMiddleware(
override val config: SecurityConfig,
servletResponseFactory: ServletGuardResponseFactory = ServletGuardResponseFactory(),
decorator: BaseSecurityDecorator? = null,
/** An externally supplied agent handler (guard-agent-jvm); null when none is registered. */
public val injectedAgentHandler: AgentHandlerProtocol? = null,
) : GuardMiddlewareProtocol {
override val logger: Logger =
LoggingUtils.setupCustomLogging(config.customLogFile, config.logFormat.pythonName)
Expand Down Expand Up @@ -84,9 +89,9 @@ public class GuardMiddleware(
/** Python `redis_handler` (middleware.py:66-70): Lettuce via the facade manager. */
public val redisHandler: RedisManager? = if (config.enableRedis) RedisManager(config) else null

private var agentHandlerField: AgentHandlerProtocol? = null
private var agentHandlerField: AgentHandlerProtocol? = injectedAgentHandler

/** Python `agent_handler` (middleware.py:72-93); guard-agent-jvm ships at W7. */
/** Python `agent_handler` (middleware.py:72-93): the injected handler, then the composite. */
override val agentHandler: AgentHandlerProtocol?
get() = agentHandlerField

Expand All @@ -105,19 +110,21 @@ public class GuardMiddleware(
get() = plainResponseFactory

/** Python `event_bus` (middleware.py:99). */
override var eventBus: MiddlewareEventBusProtocol = SecurityEventBus(null, config, geoIpHandler)
override var eventBus: MiddlewareEventBusProtocol =
SecurityEventBus(injectedAgentHandler as? HandlerAgentSink, config, geoIpHandler)
private set

/** Python `metrics_collector` (middleware.py:102). */
public var metricsCollector: MetricsCollector = MetricsCollector(null, config)
public var metricsCollector: MetricsCollector =
MetricsCollector(injectedAgentHandler as? HandlerAgentSink, config)
private set

/** Python `handler_initializer` (middleware.py:104-111). */
public val handlerInitializer: HandlerInitializer =
HandlerInitializer(
config = config,
redisHandler = redisHandler,
agentHandler = null,
agentHandler = injectedAgentHandler,
geoIpHandler = geoIpHandler,
rateLimitHandler = rateLimitHandler,
guardDecorator = guardDecoratorField,
Expand Down Expand Up @@ -174,12 +181,13 @@ public class GuardMiddleware(
BypassContext(config, logger, eventBus, routeResolver, errorResponseFactory, validator),
)
behavioralProcessor = buildBehavioralProcessor()
if (config.enableAgent) {
// guard-agent-jvm lands at W7; mirror the AgentPackageNotInstalledError degrade path
// (middleware.py:83-93) until then.
if (config.enableAgent && injectedAgentHandler == null) {
// The adapter never builds the agent itself: an AgentHandlerProtocol bean is the only agent
// source. Mirror the reference AgentPackageNotInstalledError degrade path
// (middleware.py:83-93).
onAgentInitFailed(
"Agent enabled but guard-agent-jvm is not available in this build yet " +
"(W7 artifact); continuing without agent functionality",
"Agent enabled but no agent handler was provided. Register a guard-agent-jvm " +
"AgentHandlerProtocol bean (GuardAgentTelemetry), or disable enable_agent.",
null,
)
}
Expand Down Expand Up @@ -265,7 +273,11 @@ public class GuardMiddleware(
handlerInitializer.initializeRedisHandlers()
handlerInitializer.initializeAgentIntegrations()

if (handlerInitializer.compositeHandler != null) {
val composite = handlerInitializer.compositeHandler
if (composite != null) {
// Python initialize() lines 719-726: adopt the composite as the agent handler and rebuild
// the event bus + metrics collector around it, then the event-dependent contexts.
agentHandlerField = CompositeAgentProtocol(composite)
eventBus = handlerInitializer.buildEventBus(geoIpHandler = geoIpHandler)
metricsCollector = handlerInitializer.buildMetricsCollector()
rebuildEventDependentContexts()
Expand Down Expand Up @@ -423,4 +435,51 @@ public class GuardMiddleware(
logger.severe(message)
logger.warning("Continuing without agent functionality")
}

/**
* Python `shutdown_agent_integrations` on application shutdown: stops the composite, which stops
* the injected agent and flushes its buffer. Spring calls it as the bean's inferred destroy
* method.
*/
public fun shutdown() {
kotlinx.coroutines.runBlocking { handlerInitializer.shutdownAgentIntegrations() }
}

/**
* The composite handler as the agent protocol (python passes the composite as agent_handler; the
* HandlerInitializer builds the same adapter shape over the composite).
*/
private class CompositeAgentProtocol(
private val composite: CompositeAgentHandler,
) : AgentHandlerProtocol {
override suspend fun initializeRedis(redisHandler: RedisHandlerProtocol) {
composite.initializeRedis(redisHandler)
}

override suspend fun sendEvent(event: Any?) {
@Suppress("UNCHECKED_CAST")
composite.sendEvent((event as? Map<String, Any?>) ?: linkedMapOf())
}

override suspend fun sendMetric(metric: Any?) {
@Suppress("UNCHECKED_CAST")
composite.sendMetric((metric as? Map<String, Any?>) ?: linkedMapOf())
}

override suspend fun start() {
composite.start()
}

override suspend fun stop() {
composite.stop()
}

override suspend fun flushBuffer() {
composite.flushBuffer()
}

override suspend fun getDynamicRules(): Any? = composite.getDynamicRules()

override suspend fun healthCheck(): Boolean = composite.healthCheck()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.guardcore.spring

import com.guardcore.config.SecurityConfig
import com.guardcore.config.SecurityConfigBuilder
import com.guardcore.protocols.AgentHandlerProtocol
import jakarta.servlet.DispatcherType
import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.ObjectProvider
Expand Down Expand Up @@ -60,7 +61,13 @@ public open class GuardSecurityAutoConfiguration {
public open fun guardMiddleware(
config: SecurityConfig,
decorator: SpringGuardDecorator,
): GuardMiddleware = GuardMiddleware(config, decorator = decorator)
agentHandler: ObjectProvider<AgentHandlerProtocol>,
): GuardMiddleware =
GuardMiddleware(
config,
decorator = decorator,
injectedAgentHandler = agentHandler.ifAvailable,
)

@Bean
public open fun guardAnnotationConfigurator(
Expand Down
Loading
Loading