Skip to content
Open
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
10 changes: 8 additions & 2 deletions .agents/skills/dependency-cve-remediation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,16 @@ Order of operations for a remediation: `deps-inventory` and `deps-tests` first

| Module | JDK | Notes |
|---|---|---|
| `report-service` | 11 | targets Java 8; the recorded `script:` lookup needs Nashorn (JDK <= 14) |
| `legacy-portal` | 11 | targets Java 11; same Nashorn constraint |
| `report-service` | 17 | Spring Boot 3.2 / Java 17 source level |
| `legacy-portal` | 17 | Spring Boot 3.2 / Java 17 source level |
| `notification-service`, `auth-service` | 17 | Gradle toolchain requires it |

The Maven modules' baselines in `security/deps/expected/*.json` are recorded on the
17 toolchain. On JDK 17 there is no Nashorn, so the `script:` attack case records
as `error` (script engine missing) while commons-text 1.9 is pinned; after
remediation it must read back as `ok` with the literal `${script:...}` template,
which is still a distinct outcome, so the attack policy grades unchanged.

`modules.yaml` carries JDK candidates per module (the CI export, e.g.
`$JAVA_HOME_17_X64`, then the image path) and the harness exports the first that
exists, so `make deps-*` needs no manual `JAVA_HOME`. A module whose candidates are
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ jobs:
- run: npm test || true
- run: npm run build

# Java 8 / Maven - Report Service (LEGACY)
# Java 17 / Maven - Report Service
report-service:
needs: detect-changes
if: needs.detect-changes.outputs.report-service == 'true'
Expand All @@ -329,13 +329,14 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '8'
java-version: '17'
cache: maven
- run: mvn compile -B -q
- run: mvn test -B
- run: mvn package -DskipTests -B -q

# Java 11 / Maven - Legacy Portal. Uses the checked-in wrapper rather than the
# runner's mvn: the portal builds on Spring Boot 2.x and pins its own Maven.
# Java 17 / Maven - Legacy Portal. Uses the checked-in wrapper rather than the
# runner's mvn: the portal pins its own Maven.
legacy-portal:
needs: detect-changes
if: needs.detect-changes.outputs.legacy-portal == 'true'
Expand All @@ -348,7 +349,7 @@ jobs:
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: temurin
java-version: '11'
java-version: '17'
cache: maven
- run: ./mvnw test -B

Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/deps-remediation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@ jobs:
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
version: latest
# 17 first so $JAVA_HOME_17_X64 exists for the Gradle modules, then 11 as the
# ambient JDK the Maven modules build on.
# Every JVM module builds on 17; $JAVA_HOME_17_X64 is what modules.yaml resolves.
- uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4
with:
distribution: temurin
java-version: |
17
11
java-version: '17'
# notification-service ships no Gradle wrapper, so the harness invokes the
# `gradle` on PATH — the runner image does not carry one.
- uses: gradle/actions/setup-gradle@da187c8e6ffbd3802e00f2477aa5a822b25f2dda # v4.4.4
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '8'
java-version: '17'
cache: maven
- run: mvn test -B

legacy-portal-tests:
Expand All @@ -67,7 +68,8 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '11'
java-version: '17'
cache: maven
- run: ./mvnw test -B

build-and-push:
Expand Down
118 changes: 118 additions & 0 deletions MIGRATION_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Java 17 / Spring Boot 3.2 migration notes

Inventory: `docs/java17-migration-intake.yaml`. Every JVM service in the repo is now on
Java 17 in all three places that count — build file, Dockerfile base images, CI `java-version`.

| Service | Build | Before | After | Class |
|---|---|---|---|---|
| `services/report-service` | Maven | Java 8 / Spring Boot 2.5.15 / `maven:3.8.7-eclipse-temurin-8` + `eclipse-temurin:8-jre` / CI `'8'` | Java 17 / Spring Boot 3.2.5 / `maven:3.9-eclipse-temurin-17` + `eclipse-temurin:17-jre-jammy` / CI `'17'` | migrate |
| `services/legacy-portal` | Maven (`./mvnw`) | Java 11 / Spring Boot 2.7.18 / `maven:3.9-eclipse-temurin-11` + `eclipse-temurin:11-jre-jammy` / CI `'11'` | Java 17 / Spring Boot 3.2.5 / `maven:3.9-eclipse-temurin-17` + `eclipse-temurin:17-jre-jammy` / CI `'17'` | migrate |
| `services/auth-service` | Gradle | Java 17 / Spring Boot 3.2.4 / `gradle:8.6-jdk17` + `eclipse-temurin:17-jre-jammy` / CI `'17'` | unchanged (verified) | verify |
| `services/notification-service` | Gradle KTS (Kotlin 1.9.23, Ktor 2.3.9) | `jvmToolchain(17)` / `gradle:8.6-jdk17` + `eclipse-temurin:17-jre-jammy` / CI `'17'` | unchanged (verified) | jdk-only |
| `services/analytics-service` | sbt (Scala 3.4.0, Akka HTTP 10.5.3) | `sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.10…` + `eclipse-temurin:17-jre-jammy` / CI `'17'` | unchanged (verified) | jdk-only |

Excluded: `frontend/client-app/mobile/android` (Android Gradle project, not a server-side JVM service; already on JDK 17).

## report-service (Java 8 / Boot 2.5.15 → Java 17 / Boot 3.2.5)

OpenRewrite `org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2` did:
- Boot parent → 3.2.x, `<java.version>17</java.version>`; dropped the explicit `maven.compiler.*`,
compiler/surefire plugin versions, `mockito-core` version and the `javax.servlet-api` dependency
(all managed by the Boot parent or provided by Tomcat 10).
- `javax.persistence` / `javax.validation` / `javax.annotation` → `jakarta.*` across entities,
controllers and config.
- JUnit 4 → JUnit 5 (`@Test`, `@BeforeEach`, `Assertions`, `Assumptions`) in all 7 test classes and
removed the `junit:junit` dependency and the Jupiter exclusion on `spring-boot-starter-test`.
- `commons-lang` 2.6 → `commons-lang3` (managed) and `StringUtils` import updates.
- Removed `spring.mvc.pathmatch.matching-strategy=ant-path-matcher` (SpringFox workaround).

Hand-fixed after the recipe:
- Pinned the parent to exactly 3.2.5 (recipe picked the latest 3.2.x).
- Spring Security 6: `WebSecurityConfigurerAdapter` → `SecurityFilterChain` bean with the lambda
DSL (`authorizeHttpRequests` / `requestMatchers` / `headers(...)`), plus
`dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()` so 400/404 responses reach the
client as JSON errors instead of empty 403s (Security 6 authorizes the ERROR dispatch too).
- SpringFox 3 (dead, incompatible with Spring 6) → `springdoc-openapi-starter-webmvc-ui` 2.5.0:
`@Api*` → `@Schema` / `@Operation`, `Docket` → `OpenAPI` bean, docs now at `/v3/api-docs` and
`/swagger-ui.html` (were `/v2/api-docs`, `/swagger-ui/`); security matchers updated accordingly.
- Hibernate 6: `Report.errorMessage` (`@Lob String`) annotated `@JdbcTypeCode(SqlTypes.LONGVARCHAR)`
so the column stays `varchar` instead of becoming a Postgres `oid`. Verified: `reports.error_message`
is `character varying(32600)` against Postgres 15.
- Apache HttpClient 4 → HttpClient 5 (Spring 6 dropped the 4.x request factory); read timeout moved to
`SocketConfig` on the pooling connection manager.
- Removed the explicit `hibernate.dialect` properties (Hibernate 6 auto-detects and warns otherwise).
- Dockerfile builder/runtime → `maven:3.9-eclipse-temurin-17` / `eclipse-temurin:17-jre-jammy`.
- Kept deliberately: `commons-text` 1.9 (CVE-2022-42889 fixture for the dependency-remediation lab),
POI 4.1.2, iText 5.5.13.3, Guava 28, Commons IO 2.6 — out of scope for this migration.

`--add-opens`: none. Tests: 50 run / 0 failed / 1 skipped before (JUnit 4, JDK 8) and after (JUnit 5,
JDK 17); the skipped test is the opt-in dependency-transcript emitter.

## legacy-portal (Java 11 / Boot 2.7.18 → Java 17 / Boot 3.2.5)

OpenRewrite did:
- Boot parent → 3.2.x, `<java.version>17</java.version>`, dropped redundant `maven.compiler.*`.
- `javax.persistence` / `javax.validation` / `javax.annotation` → `jakarta.*` in 7 source files
(`Announcement`, `Feedback`, `UserPreference` entities and their controllers, `PortalBrandingSettings`).
- Split the `postgres` profile out of `application.yml` into `application-postgres.yml` (Boot 3 no
longer accepts multi-document profile activation the old way).

Hand-fixed after the recipe: exact 3.2.5 parent; removed explicit Hibernate dialect settings;
Dockerfile builder/runtime → `maven:3.9-eclipse-temurin-17` / `eclipse-temurin:17-jre-jammy`;
removed stale "Java 11" comments in `README.md`/`Dockerfile`. No Spring Security, no `@Lob`, no
`spring.factories`, no JAXB/Nashorn — as expected coming from 11, there was little beyond the
namespace change. `commons-configuration2` 2.8.0 kept (transitive `commons-text` 1.9 fixture).

`--add-opens`: none. Tests: 16 run / 0 failed / 1 skipped before and after.

## Shared files (lead)

- `.github/workflows/ci.yml`, `.github/workflows/docker-build.yml`: report-service and legacy-portal
jobs now `setup-java` Temurin `'17'` with `cache: maven`.
- `.github/workflows/deps-remediation.yml`, `security/deps/modules.yaml`,
`.agents/skills/dependency-cve-remediation/SKILL.md`: the dependency-remediation harness measures
the two Maven modules on JDK 17 instead of JDK 11.
- `README.md`: service table no longer describes report-service as Java 8 / Boot 2.5.

## Runtime verification (JDK 17, against Postgres 15 via `docker-compose.infra.yml`)

Both migrated apps start on Boot 3.2.5 with no `spring-boot-properties-migrator` findings
(only the default `spring.jpa.open-in-view` notice that Boot 2.x also logged).

report-service (`:8091`): `/health`, `/actuator/health`, `/v3/api-docs`, `/swagger-ui/index.html` → 200;
`POST /api/v1/reports` → 202, report reaches `COMPLETED` (CSV, 25 rows), `GET /api/v1/reports/{id}` → 200,
list → 200, `DELETE` → 204, unknown id → 404, bad id → 400 JSON.

legacy-portal (`:8095`, `postgres` profile): `/health`, `/actuator/health` → 200; announcements
create 201 / publish 200 / list 200; preferences put+get 200; feedback submit 201 (validation errors
still 400) / list 200 / average-rating 200; unknown announcement → 404.

Behaviour differences vs `main` worth knowing:
- Trailing-slash URLs (`/api/v1/reports/`, `/api/announcements/`) now 404 (Boot 3 / Spring 6 default).
No caller uses them — the API gateway routes `/api/v1/reports` and the frontend never calls either
service directly.
- report-service paths not covered by a security matcher (e.g. `/nope`) now return 403 instead of 404
(Security 6 denies unmatched requests). Not reachable through the gateway.
- Swagger moved from `/v2/api-docs` + `/swagger-ui/` to `/v3/api-docs` + `/swagger-ui.html`.

## Dependency-remediation harness

`security/deps/expected/{report-service,legacy-portal}.json` were re-recorded on JDK 17
(`make deps-record MODULE=<id> ALLOW_RERECORD=1 REASON=...`). The previous recordings came from
JDK 11, where `${script:javascript:3+4}` resolved to `7` via Nashorn; on 17 there is no script
engine, so the vulnerable-baseline outcome for `attack-script-lookup` is now an
`IllegalArgumentException`. The remediated contract is unaffected: with a fixed commons-text the
case must read back `ok` with the literal template, which is still a different outcome from the
baseline, so the `attack` policy grades exactly as before. Legacy-portal's cases contain no
script lookup; only its recording metadata changed. `make deps-transcript-baseline` passes on 17.

## Follow-ups

- `services/report-service/UPGRADE_GUIDE.md` and `docs/{CI_STRATEGY,EVENT_DRIVEN_SECURITY,SDLC-COVERAGE}.md`,
`docs/labs/security-sprint-guide.md`, `.devin/wiki.json` still describe report-service as Java 8;
the Trivy exclusion for report-service in `security-scan.yml` can be reconsidered now that it is on 17.
- `infrastructure/helm/report-service/Chart.yaml` description says "Java 8/Spring Boot 2.5" — charts
ship from upstream `main`, so this needs an upstream PR rather than a change here.
- Library modernisation deliberately left out: POI 4 → 5, iText 5 → OpenPDF, Guava 28 → 33,
Commons IO 2.6 → 2.15, `java.util.Date` → `java.time`, RestTemplate → RestClient.
- Add JWT validation on `/api/v1/reports/**` (pre-existing TODO, unchanged).
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ make down
| Analytics Service | Scala 3.4 | Akka HTTP | 8088 | Usage analytics, data aggregation |
| Admin Service | Ruby 3.3 | Rails 7.1 | 8089 | Admin dashboard backend |
| Audit Service | C# 12 | ASP.NET 8 | 8090 | Immutable audit trail, compliance |
| Report Service *(legacy)* | Java 8 | Spring Boot 2.5 | 8091 | PDF/CSV/Excel report generation (tech-debt: upgrade target Java 17+, Spring Boot 3.2+) |
| Report Service | Java 17 | Spring Boot 3.2 | 8091 | PDF/CSV/Excel report generation (Maven; still carries legacy library pins — see `MIGRATION_NOTES.md`) |

> **Note:** The Report Service intentionally uses outdated dependencies (Java 8, Spring Boot 2.5, JUnit 4, javax.\*) and is a candidate for a framework-upgrade exercise. See `services/report-service/pom.xml` for details.
> **Note:** The Report Service was migrated from Java 8 / Spring Boot 2.5 to Java 17 / Spring Boot 3.2 (see `MIGRATION_NOTES.md`). It still intentionally pins older libraries (POI 4.x, iText 5, Guava 28, commons-text 1.9) for dependency-remediation exercises.

## Frontend Applications

Expand Down Expand Up @@ -170,7 +170,7 @@ otterworks/
│ ├── analytics-service/ # Scala / Akka HTTP
│ ├── admin-service/ # Ruby / Rails
│ ├── audit-service/ # C# / ASP.NET
│ └── report-service/ # Java 8 / Spring Boot 2.5 (legacy)
│ └── report-service/ # Java 17 / Spring Boot 3.2 (Maven)
├── frontend/ # Web app (React/Next.js) + Admin dashboard (Angular)
├── infrastructure/
│ ├── terraform/ # App-specific AWS resources (S3, RDS, DynamoDB, etc.)
Expand Down
59 changes: 59 additions & 0 deletions docs/java17-migration-intake.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Intake inventory for the Java 17 / Spring Boot 3.2 migration. The per-service
# java / framework / dockerfile / ci fields record the state FOUND at intake (pre-migration),
# which is what `class` was decided from; `targets` is the end state. The post-migration
# state of every service is in MIGRATION_NOTES.md.
repo: https://github.com/COG-GTM/otterworks
default_branch: main
recorded_from: main@e66f76ca
services:
- name: report-service
path: services/report-service
build: maven
java: 8
framework: spring-boot 2.5.15
dockerfile: maven:3.8.7-eclipse-temurin-8 / eclipse-temurin:8-jre
ci: ci.yml report-service (java-version '8'), docker-build.yml report-service-tests (java-version '8')
class: migrate
notes: SpringFox 3.0, JUnit 4, WebSecurityConfigurerAdapter, @Lob String on Report
- name: legacy-portal
path: services/legacy-portal
build: maven (./mvnw 3.9.9)
java: 11
framework: spring-boot 2.7.18
dockerfile: maven:3.9-eclipse-temurin-11 / eclipse-temurin:11-jre-jammy
ci: ci.yml legacy-portal (java-version '11'), docker-build.yml legacy-portal-tests (java-version '11')
class: migrate
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
- name: auth-service
path: services/auth-service
build: gradle
java: 17 # sourceCompatibility = JavaVersion.VERSION_17
framework: spring-boot 3.2.4
dockerfile: gradle:8.6-jdk17 / eclipse-temurin:17-jre-jammy
ci: ci.yml + docker-build.yml (java-version '17')
class: verify
- name: notification-service
path: services/notification-service
build: gradle-kts
java: 17 # jvmToolchain(17)
framework: ktor 2.3.9 (kotlin 1.9.23)
dockerfile: gradle:8.6-jdk17 / eclipse-temurin:17-jre-jammy
ci: ci.yml (java-version '17')
class: jdk-only
- name: analytics-service
path: services/analytics-service
build: sbt
java: 17 # sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.10 builder
framework: akka-http 10.5.3 (scala 3.4.0)
dockerfile: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.10_7_1.9.9_3.4.1 / eclipse-temurin:17-jre-jammy
ci: ci.yml (java-version '17')
class: jdk-only
shared:
- .github/workflows/ci.yml # report-service / legacy-portal job blocks pin 8 / 11
- .github/workflows/docker-build.yml # report-service-tests / legacy-portal-tests pin 8 / 11
- .github/workflows/deps-remediation.yml # installs 17 + 11 (11 as ambient JDK for the Maven modules)
excluded:
- frontend/client-app/mobile/android # Android Gradle project, not a JVM service (already JDK 17 per PR #14)
targets:
java: 17
spring_boot: 3.2.5
demo_mode: true
4 changes: 2 additions & 2 deletions security/deps/expected/legacy-portal.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"advisory": "CVE-2022-42889",
"artifact": "org.apache.commons:commons-text",
"cases_sha256": "ecde5a24ace4f1501d955b9a959d2ba05d07ba3f79095f66900b21fb106cd8ef",
"recorded_at": "2026-08-17T22:51:49.397577+00:00",
"reason": "baseline: commons-text 1.9 behavior before CVE-2022-42889 remediation",
"recorded_at": "2026-09-03T09:26:05.809990+00:00",
"reason": "baseline on commons-text 1.9 re-recorded on JDK 17 (Java 17 / Spring Boot 3.2.5 migration): Nashorn is gone, so the ${script:javascript:...} lookup now errors instead of resolving",
"cases": [
{
"id": "settings-banner-resolves-keys",
Expand Down
9 changes: 5 additions & 4 deletions security/deps/expected/report-service.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"advisory": "CVE-2022-42889",
"artifact": "org.apache.commons:commons-text",
"cases_sha256": "ea3ac7b9e1fc1dc7049222d7b4157e34b435927389036138b00feaabb28f225c",
"recorded_at": "2026-08-17T22:51:46.629685+00:00",
"reason": "baseline: commons-text 1.9 behavior before CVE-2022-42889 remediation",
"recorded_at": "2026-09-03T09:26:01.056330+00:00",
"reason": "baseline on commons-text 1.9 re-recorded on JDK 17 (Java 17 / Spring Boot 3.2.5 migration): Nashorn is gone, so the ${script:javascript:...} lookup now errors instead of resolving",
"cases": [
{
"id": "banner-title",
Expand Down Expand Up @@ -49,8 +49,9 @@
},
{
"id": "attack-script-lookup",
"outcome": "ok",
"value": "7"
"outcome": "error",
"error_type": "java.lang.IllegalArgumentException",
"error_message": "Error in script engine [javascript] evaluating script [3+4]."
},
{
"id": "attack-dns-lookup",
Expand Down
Loading
Loading