diff --git a/CHANGELOG.md b/CHANGELOG.md
index 697c968..9440d7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,25 @@ The source of truth for the entries below is [docs/changelog.md](docs/changelog.
## [Unreleased]
+## [3.1.0] — LLM core extraction, LangChain4j, WebClient DNS gap, GraalVM hints
+
+### Added
+
+- New module **`ssrf-guard-llm`** — framework-agnostic JSON tool-input validator (`ToolInputGuard` + `JsonToolInputGuard`). Holds the URL detection + policy validation logic the adapter modules share.
+- New module **`ssrf-guard-langchain4j`** — wraps LangChain4j `ToolExecutor`. Same defense as `-springai` for the other major Java LLM framework. Spring auto-wrap via `BeanPostProcessor`; `SsrfGuardedToolExecutors.wrap(...)` for non-Spring.
+- **GraalVM native-image hints** via `META-INF/spring/aot.factories` in `-llm`. Adapter modules get free AOT support from Spring Boot 3.
+
+### Changed
+
+- `ssrf-guard-springai` refactored to a ~30-line thin adapter over `-llm`. Public API unchanged.
+- Error payload backed by typed `SsrfBlockPayload` record (was `Map.of`). Same JSON wire shape; better AOT introspection.
+
+### Fixed
+
+- **WebClient DNS-time defense gap closed.** `SsrfGuardReactorAddressResolverGroup` filters reactor-netty's resolved IPs against the private/loopback ranges — closes the v3.0.x DNS-rebinding window WebFlux apps had.
+
+Full notes in [docs/changelog.md](docs/changelog.md#310--llm-core-extraction-langchain4j-webclient-dns-gap-graalvm-hints).
+
## [3.0.1] — Fix metrics bean classpath gate
### Fixed
@@ -58,7 +77,8 @@ semantic-release rollup. Tagged but **never published to Maven Central**.
Initial public release. Tagged but **never published to Maven Central**.
-[Unreleased]: https://github.com/devslab-kr/ssrf-guard/compare/v3.0.1...HEAD
+[Unreleased]: https://github.com/devslab-kr/ssrf-guard/compare/v3.1.0...HEAD
+[3.1.0]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v3.1.0
[3.0.1]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v3.0.1
[3.0.0]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v3.0.0
[2.0.0]: https://github.com/devslab-kr/ssrf-guard/releases/tag/v2.0.0
diff --git a/README.ko.md b/README.ko.md
index 9ba324c..5c7993c 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -87,17 +87,17 @@ ToolCallback[] safe = SsrfGuardedToolCallbacks.wrap(raw, urlPolicy);
kr.devslab
ssrf-guard
- 3.0.0
+ 3.1.0
```
### Gradle (Kotlin DSL)
```kotlin
-implementation("kr.devslab:ssrf-guard:3.0.0")
+implementation("kr.devslab:ssrf-guard:3.1.0")
```
-> **v2.0.0에서 업그레이드?** 메타 `kr.devslab:ssrf-guard:3.0.0`이 v2.0.0 API 유지 — `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. `kr.devslab.ssrfguard.security.*`를 직접 import한 코드는 업데이트 필요 — [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) 패키지 매핑 참고.
+> **v2.0.0에서 업그레이드?** 메타 `kr.devslab:ssrf-guard:3.1.0`이 v2.0.0 API 유지 — `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. `kr.devslab.ssrfguard.security.*`를 직접 import한 코드는 업데이트 필요 — [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) 패키지 매핑 참고.
## 설정
diff --git a/README.md b/README.md
index 0c99e90..3e079f9 100644
--- a/README.md
+++ b/README.md
@@ -87,17 +87,17 @@ Auto-config picks it up — any `@Bean ToolCallback` gets wrapped via a `BeanPos
kr.devslab
ssrf-guard
- 3.0.0
+ 3.1.0
```
### Gradle (Kotlin DSL)
```kotlin
-implementation("kr.devslab:ssrf-guard:3.0.0")
+implementation("kr.devslab:ssrf-guard:3.1.0")
```
-> **Upgrading from v2.0.0?** The meta `kr.devslab:ssrf-guard:3.0.0` keeps the v2.0.0 API working — pulls in `-core`, `-httpclient5`, `-restclient` transitively. Direct imports of `kr.devslab.ssrfguard.security.*` need updates — see the [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) for the package-rename mapping.
+> **Upgrading from v2.0.0?** The meta `kr.devslab:ssrf-guard:3.1.0` keeps the v2.0.0 API working — pulls in `-core`, `-httpclient5`, `-restclient` transitively. Direct imports of `kr.devslab.ssrfguard.security.*` need updates — see the [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) for the package-rename mapping.
## Configuration
diff --git a/docs/changelog.ko.md b/docs/changelog.ko.md
index aec0f8f..4a3f667 100644
--- a/docs/changelog.ko.md
+++ b/docs/changelog.ko.md
@@ -6,28 +6,30 @@ ssrf-guard의 주요 변경 사항을 기록합니다.
## [Unreleased]
-v3.1.0 작업 추적. 릴리즈된 항목은 태그 시 별도 섹션으로 이동.
+## [3.1.0] — LLM 코어 추출, LangChain4j, WebClient DNS 공백 메움, GraalVM hints
### Added
-- **`ssrf-guard-llm`** — 신규. 모든 LLM 툴 어댑터가 공유하는 JSON 트리 walk + URL 추출 + 정책 검증을 담는 framework-agnostic 코어 모듈. `ToolInputGuard` (인터페이스)와 `JsonToolInputGuard` (기본 구현) 노출. 이전에 `SsrfGuardedToolCallback` 내부에 있던 ~200줄 로직을 이쪽으로 이동.
+- **`ssrf-guard-llm`** — 신규. 모든 LLM 툴 어댑터가 공유하는 JSON 트리 walk + URL 추출 + 정책 검증을 담는 framework-agnostic 코어 모듈. `ToolInputGuard` (인터페이스)와 `JsonToolInputGuard` (기본 구현) 노출. 이전에 `SsrfGuardedToolCallback` 내부에 있던 ~200줄 로직을 이쪽으로 통합.
- **`ssrf-guard-langchain4j`** — 신규. LangChain4j의 `ToolExecutor`를 wrap하는 어댑터 모듈. `ssrf-guard-springai`와 동일한 LLM 에이전트 SSRF 표면을 LangChain4j 생태계 (Java LLM 양대 프레임워크 중 다른 하나) 대응. Spring Boot 사용자는 `BeanPostProcessor`로 자동 wrap; 비-Spring/프로그래매틱 사용자용 `SsrfGuardedToolExecutors.wrap(...)` 헬퍼 제공.
+- **GraalVM native-image 친화성.** `ssrf-guard-llm`이 `RuntimeHintsRegistrar`를 등록 (`META-INF/spring/aot.factories` 통해) — Spring Boot AOT 프로세서가 런타임 reflection surface (매 차단마다 Jackson이 직렬화하는 `SsrfBlockPayload` record, Jackson에 의해 `label`이 읽히는 `BlockReason` enum)를 인식. 어댑터 모듈 (`-springai`, `-langchain4j`, 모든 Spring 자동설정)은 Spring Boot 3 AOT가 무료로 처리 — `@Bean` 팩토리 메서드와 `BeanPostProcessor`만 호스팅하므로.
### Changed
- **`ssrf-guard-springai`가 thin adapter (~30줄)로 리팩토링.** 새 `-llm` 모듈의 `JsonToolInputGuard`에 위임. Public API 변경 없음 — `SsrfGuardedToolCallback`의 모든 생성자/메서드 시그니처 유지. v3.0.x 소비자는 API 변경 못 느낌 — 단지 `ssrf-guard-llm`을 transitive로 받게 됨.
+- **에러 payload 형태 안정화.** SSRF 차단 시 LLM이 보는 JSON 객체가 이제 typed `SsrfBlockPayload` record (`{error, reason, url, message, guidance}`) 기반. v3.0.x와 wire 호환 — 동일 필드명, 동일 값. 이전 `Map.of(...)` 형태의 JDK-내부 `ImmutableCollections.MapN` 백킹 타입이 AOT introspect 안 되던 문제 해결. wire shape에 의존한 스크립트 변경 불필요; 기존 substring 테스트도 그대로 통과.
-### Added (이어서)
+### Fixed
-- **GraalVM native-image 친화성.** `ssrf-guard-llm`이 `RuntimeHintsRegistrar`를 등록 (`META-INF/spring/aot.factories` 통해) — Spring Boot AOT 프로세서가 런타임 reflection surface (매 차단마다 Jackson이 직렬화하는 `SsrfBlockPayload` record, 그리고 `label`이 Jackson에 의해 읽히는 `BlockReason` enum)를 인식. 이전 `Map.of(...)` payload 형태가 JVM-내부 `ImmutableCollections.MapN` 타입을 써서 AOT가 introspect 못 했던 문제 해결. 어댑터 모듈 (`-springai`, `-langchain4j`, 모든 Spring 자동설정)은 Spring Boot 3 AOT가 무료로 처리 — `@Bean` 팩토리 메서드와 `BeanPostProcessor`만 호스팅하므로.
+- **WebClient DNS-time 방어 공백 메움.** v3.0.x의 `ssrf-guard-webclient`는 URL-time 필터만 실행 — 화이트리스트를 통과한 호스트가 DNS 시점에 사설 IP로 resolve될 수 있었음 (전형적인 DNS 리바인딩 → 메타데이터 공격). 새 `SsrfGuardReactorAddressResolverGroup`이 reactor-netty의 `AddressResolverGroup`에 후킹해서 RestClient 모듈이 Apache HttpClient `DnsResolver` 단계에서 적용하는 것과 동일한 사설 IP 필터를 적용. WebFlux 앱도 이제 차단형 RestClient 앱이 갖던 2단계 방어 (URL + DNS) 동등하게 받음. reactor-netty 클래스패스 의존 — 비-Netty WebFlux 백엔드 (Jetty Reactive, Helidon)는 URL-time 필터만 작동하고 connector 교체는 스킵.
-### Changed (이어서)
+### Migration
-- **에러 payload 형태 안정화.** SSRF 차단 시 LLM이 보는 JSON 객체가 이제 typed `SsrfBlockPayload` record (`{error, reason, url, message, guidance}`) 기반. v3.0.x와 wire 호환 — 동일 필드명, 동일 값. 기존 테스트가 substring 매치로 검증하고 그대로 통과; wire shape에 의존한 스크립트도 변경 불필요.
+v3.1.0로 그냥 올리기 — 소비자 코드 변경 없음. 메타 `kr.devslab:ssrf-guard:3.1.0`도 v3.0.x처럼 `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. 새 모듈 (`-llm`, `-langchain4j`)은 opt-in — 좌표 추가 안 하면 따라오지 않음.
-### Fixed
+기존 `SecurityException` catch 코드는 그대로 동작 (`SsrfGuardException`이 여전히 `SecurityException` 서브클래스). 구조화 `e.reason()` 접근하려면 `SsrfGuardException`을 catch.
-- **WebClient DNS-time 방어 공백 메움.** v3.0.x의 `ssrf-guard-webclient`는 URL-time 필터만 실행 — 화이트리스트를 통과한 호스트가 DNS 시점에 사설 IP로 resolve될 수 있었음 (전형적인 DNS 리바인딩 → 메타데이터 공격). 새 `SsrfGuardReactorAddressResolverGroup`이 reactor-netty의 `AddressResolverGroup`에 후킹해서 RestClient 모듈이 Apache HttpClient `DnsResolver` 단계에서 적용하는 것과 동일한 사설 IP 필터를 적용. WebFlux 앱도 이제 차단형 RestClient 앱이 갖던 2단계 방어 (URL + DNS) 동등하게 받음. reactor-netty 클래스패스 의존 — 비-Netty WebFlux 백엔드 (Jetty Reactive, Helidon)는 URL-time 필터만 작동하고 connector 교체는 스킵.
+Native-image 소비자: `kr.devslab:ssrf-guard-llm:3.1.0` (또는 transitive로 끌어오는 모듈) 추가하면 AOT 프로세서가 등록된 hints 자동 인식.
## [3.0.1] — 메트릭 빈 classpath 게이트 수정
diff --git a/docs/changelog.md b/docs/changelog.md
index ceb35e6..6311303 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,28 +6,30 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
## [Unreleased]
-Tracking v3.1.0 work. Released items will move into their own section below when the tag lands.
+## [3.1.0] — LLM core extraction, LangChain4j, WebClient DNS gap, GraalVM hints
### Added
-- **`ssrf-guard-llm`** — new framework-agnostic core module holding the JSON walking + URL extraction + policy validation that every LLM tool adapter shares. Exposes `ToolInputGuard` (interface) and `JsonToolInputGuard` (default impl). Replaces ~200 lines of logic that previously lived inside `SsrfGuardedToolCallback`.
+- **`ssrf-guard-llm`** — new framework-agnostic core module holding the JSON walking + URL extraction + policy validation that every LLM tool adapter shares. Exposes `ToolInputGuard` (interface) and `JsonToolInputGuard` (default impl). Holds the ~200 lines of logic that previously lived inside `SsrfGuardedToolCallback`.
- **`ssrf-guard-langchain4j`** — new adapter module wrapping LangChain4j's `ToolExecutor`. Closes the same LLM-agent SSRF surface as `ssrf-guard-springai` but for the LangChain4j ecosystem (the other major Java LLM framework). Auto-wrap via `BeanPostProcessor` for Spring Boot users; `SsrfGuardedToolExecutors.wrap(...)` helpers for non-Spring / programmatic users.
+- **GraalVM native-image friendliness.** `ssrf-guard-llm` registers a `RuntimeHintsRegistrar` (via `META-INF/spring/aot.factories`) so Spring Boot's AOT processor learns about the reflective surface this library uses at runtime: the new `SsrfBlockPayload` record (Jackson-serialised on every block) and the `BlockReason` enum (Jackson-touched for its `label`). Adapter modules (`-springai`, `-langchain4j`, all Spring autoconfigs) get free AOT coverage from Spring Boot 3 — they only host `@Bean` factory methods and a `BeanPostProcessor`, both of which the AOT processor already handles.
### Changed
- **`ssrf-guard-springai` refactored to a thin adapter (~30 lines).** Delegates to `JsonToolInputGuard` from the new `-llm` module. Public API unchanged — every constructor and method on `SsrfGuardedToolCallback` keeps the same shape. v3.0.x consumers see no API change; they just pick up `ssrf-guard-llm` transitively.
+- **Error-payload shape stabilised.** The JSON object an LLM sees on an SSRF block is now backed by the typed `SsrfBlockPayload` record (`{error, reason, url, message, guidance}`). Wire-compatible with v3.0.x — same field names, same values. Replaces the previous `Map.of(...)` form whose JDK-private `ImmutableCollections.MapN` backing types AOT couldn't introspect. If you scripted around the wire shape you don't need to change anything; substring assertions in existing tests keep passing.
-### Added (continued)
+### Fixed
-- **GraalVM native-image friendliness.** `ssrf-guard-llm` registers a `RuntimeHintsRegistrar` (via `META-INF/spring/aot.factories`) so Spring Boot's AOT processor learns about the reflective surface this library uses at runtime: the new `SsrfBlockPayload` record (Jackson-serialised on every block) and the `BlockReason` enum (also Jackson-touched for its `label`). Replaces the previous `Map.of(...)` payload form, which used JVM-private `ImmutableCollections.MapN` types that AOT couldn't introspect. Adapter modules (`-springai`, `-langchain4j`, all Spring autoconfigs) get free AOT coverage from Spring Boot 3 — they only host `@Bean` factory methods and a `BeanPostProcessor`, both of which the AOT processor already handles.
+- **WebClient DNS-time defense gap closed.** v3.0.x's `ssrf-guard-webclient` only ran the URL-time filter — a host that passed the whitelist could still resolve to a private IP at DNS time (the classic DNS-rebinding-to-metadata attack). The new `SsrfGuardReactorAddressResolverGroup` plugs into reactor-netty's `AddressResolverGroup` and filters resolved IPs against the same private-IP ranges the RestClient module checks at the Apache HttpClient `DnsResolver` step. WebFlux apps now get the same two-layer defense (URL + DNS) the blocking RestClient apps already had. Gated on reactor-netty being on the classpath — non-Netty WebFlux backends (Jetty Reactive, Helidon) still get the URL-time filter and just skip the connector swap.
-### Changed (continued)
+### Migration
-- **Error-payload shape stabilised.** The JSON object an LLM sees on an SSRF block is now backed by the typed `SsrfBlockPayload` record (`{error, reason, url, message, guidance}`). Wire-compatible with v3.0.x — same field names, same values. Existing tests assert on substring matches against those names and keep passing; if you scripted around the wire shape you don't need to change anything.
+Drop in v3.1.0 — no consumer code changes. The meta `kr.devslab:ssrf-guard:3.1.0` still transitively pulls in `-core`, `-httpclient5`, `-restclient` like v3.0.x. New modules (`-llm`, `-langchain4j`) are opt-in; consumers who don't add their coordinate don't pull the new jars.
-### Fixed
+If you previously caught `SecurityException` you still match `SsrfGuardException` (it's still a `SecurityException` subclass). Catch `SsrfGuardException` to read the structured `e.reason()`.
-- **WebClient DNS-time defense gap closed.** v3.0.x's `ssrf-guard-webclient` only ran the URL-time filter — a host that passed the whitelist could still resolve to a private IP at DNS time (the classic DNS-rebinding-to-metadata attack). The new `SsrfGuardReactorAddressResolverGroup` plugs into reactor-netty's `AddressResolverGroup` and filters resolved IPs against the same private-IP ranges the RestClient module checks at the Apache HttpClient `DnsResolver` step. WebFlux apps now get the same two-layer defense (URL + DNS) the blocking RestClient apps already had. Gated on reactor-netty being on the classpath — non-Netty WebFlux backends (Jetty Reactive, Helidon) still get the URL-time filter and just skip the connector swap.
+For native-image consumers: add `kr.devslab:ssrf-guard-llm:3.1.0` (or any module that depends on it transitively) and the AOT processor will pick up the registered hints automatically.
## [3.0.1] — Fix metrics bean classpath gate
diff --git a/docs/getting-started/installation.ko.md b/docs/getting-started/installation.ko.md
index 7c98050..4a36c8e 100644
--- a/docs/getting-started/installation.ko.md
+++ b/docs/getting-started/installation.ko.md
@@ -9,7 +9,7 @@
## 모듈 선택
-ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다. 실제로 쓰는 모듈만 고르면 됩니다.
+ssrf-guard 3.0+은 HTTP 클라이언트 경계로 분리되어 있습니다. 실제로 쓰는 모듈만 고르면 됩니다.
=== "RestClient (Spring Boot 3.x 기본)"
@@ -17,7 +17,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard
- 3.0.0
+ 3.1.0
```
@@ -29,7 +29,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard-resttemplate
- 3.0.0
+ 3.1.0
```
@@ -41,7 +41,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard-webclient
- 3.0.0
+ 3.1.0
```
@@ -53,7 +53,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard-feign
- 3.0.0
+ 3.1.0
```
@@ -65,7 +65,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard-springai
- 3.0.0
+ 3.1.0
```
@@ -108,7 +108,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard-jdkhttp
- 3.0.0
+ 3.1.0
```
@@ -123,7 +123,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
kr.devslab
ssrf-guard-okhttp
- 3.0.0
+ 3.1.0
```
@@ -135,7 +135,7 @@ ssrf-guard v3.0.0은 HTTP 클라이언트 경계로 분리되어 있습니다.
```
!!! tip "최신 버전"
- `3.0.0`을 [Maven Central](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard) 최신으로 교체.
+ `3.1.0`을 [Maven Central](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard) 최신으로 교체.
!!! info "v2.0.0에서 올라온다면"
`kr.devslab:ssrf-guard` 좌표는 여전히 동작 — 메타 아티팩트가 `-core`, `-httpclient5`, `-restclient`를 끌어옴. 대부분 버전만 올리고 다시 빌드하면 끝. `kr.devslab.ssrfguard.security.*` 직접 import한 코드는 [v3.0.0 changelog](../changelog.md#300--multi-module--llm-agent-ssrf-defense)의 패키지 매핑 참고.
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 2df4953..0dd46e3 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -9,7 +9,7 @@
## Picking your module
-ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) matching what you actually use. Most consumers want just one.
+ssrf-guard 3.0+ is split along HTTP-client boundaries — pick the module(s) matching what you actually use. Most consumers want just one.
=== "RestClient (Spring Boot 3.x default)"
@@ -17,7 +17,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard
- 3.0.0
+ 3.1.0
```
@@ -29,7 +29,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard-resttemplate
- 3.0.0
+ 3.1.0
```
@@ -41,7 +41,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard-webclient
- 3.0.0
+ 3.1.0
```
@@ -53,7 +53,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard-feign
- 3.0.0
+ 3.1.0
```
@@ -65,7 +65,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard-springai
- 3.0.0
+ 3.1.0
```
@@ -108,7 +108,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard-jdkhttp
- 3.0.0
+ 3.1.0
```
@@ -123,7 +123,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
kr.devslab
ssrf-guard-okhttp
- 3.0.0
+ 3.1.0
```
@@ -135,7 +135,7 @@ ssrf-guard v3.0.0 is split along HTTP-client boundaries — pick the module(s) m
```
!!! tip "Latest version"
- Replace `3.0.0` with the latest from [Maven Central](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard).
+ Replace `3.1.0` with the latest from [Maven Central](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard).
!!! info "Coming from v2.0.0?"
The `kr.devslab:ssrf-guard` coordinate still works — it's a meta artifact that pulls in `-core`, `-httpclient5`, and `-restclient`. Most consumers can upgrade by changing the version and rebuilding. Direct imports of `kr.devslab.ssrfguard.security.*` need to be updated — see the [v3.0.0 changelog](../changelog.md#300--multi-module--llm-agent-ssrf-defense) for the package-rename mapping.