Skip to content

Commit d694dbb

Browse files
Setting up fuzzing to run in pipelines
1 parent bb9fc17 commit d694dbb

File tree

16 files changed

+827
-4
lines changed

16 files changed

+827
-4
lines changed

.clusterfuzzlite/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM gcr.io/oss-fuzz-base/base-builder-jvm:v1
2+
3+
# The logboekdataverwerking-wrapper dependency is compiled with Java 21,
4+
# so we need JDK 21 for compilation (base-builder-jvm ships JDK 17).
5+
RUN apt-get update && apt-get install -y openjdk-21-jdk rsync
6+
7+
ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
8+
ENV PATH=$JAVA_HOME/bin:$PATH
9+
10+
COPY . $SRC/moza-profiel-service
11+
WORKDIR $SRC/moza-profiel-service
12+
RUN chmod +x mvnw
13+
COPY .clusterfuzzlite/build.sh $SRC/

.clusterfuzzlite/build.sh

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/bin/bash -eu
2+
3+
# Build the project with H2 database for fuzzing.
4+
# quarkus.datasource.db-kind is a build-time property — must be set during compilation.
5+
./mvnw package -DskipTests -Djacoco.skip=true \
6+
-Dquarkus.datasource.db-kind=h2 \
7+
-Dquarkus.datasource.jdbc.url=jdbc:h2:mem:fuzztest \
8+
-Dlogboekdataverwerking.enabled=false \
9+
-B
10+
11+
# Copy all dependencies to $OUT/lib
12+
mkdir -p $OUT/lib
13+
./mvnw dependency:copy-dependencies -DoutputDirectory=$OUT/lib -B
14+
15+
# Copy compiled application and test classes
16+
cp -r target/classes $OUT/classes
17+
cp -r target/test-classes $OUT/test-classes
18+
19+
# Copy the full quarkus-app directory so the EndpointFuzzer wrapper can start
20+
# Quarkus as a subprocess (java -jar quarkus-run.jar). H2 is baked in at build time.
21+
cp -r target/quarkus-app $OUT/quarkus-app
22+
23+
# Bundle the ENTIRE JDK 21 runtime to $OUT so the runner can execute Java 21 bytecode.
24+
# Uses rsync -aL to dereference symlinks (critical for JDK directory structure).
25+
# Pattern taken from the oss-fuzz tomcat project.
26+
mkdir -p "$OUT/open-jdk-21"
27+
rsync -aL --exclude='*.zip' "$JAVA_HOME/" "$OUT/open-jdk-21/"
28+
29+
# Create a wrapper script for every standalone fuzzer
30+
# (classes that define the static fuzzerTestOneInput method expected by jazzer_driver)
31+
for fuzzer in $(grep -rl "fuzzerTestOneInput" src/test/java/ || true); do
32+
class_name=$(echo "$fuzzer" | sed 's|src/test/java/||;s|\.java$||;s|/|.|g')
33+
simple_name=$(basename -s .java "$fuzzer")
34+
35+
echo "Creating fuzzer wrapper: $simple_name -> $class_name"
36+
37+
if [ "$simple_name" = "EndpointFuzzer" ]; then
38+
# ── Special wrapper for EndpointFuzzer ──
39+
# Starts Quarkus as a subprocess (java -jar quarkus-run.jar) before
40+
# launching jazzer_driver. Quarkus was built with H2 at compile time.
41+
cat > "$OUT/$simple_name" << 'WRAPPER_EOF'
42+
#!/bin/bash
43+
# LLVMFuzzerTestOneInput for jvm
44+
this_dir=$(dirname "$0")
45+
46+
if [[ "$@" =~ (^| )-runs=[0-9]+($| ) ]]; then
47+
mem_settings='-Xmx1900m:-Xss900k'
48+
else
49+
mem_settings='-Xmx2048m:-Xss1024k'
50+
fi
51+
52+
# Start Quarkus as a background process with H2 in-memory database.
53+
JAVA_HOME="$this_dir/open-jdk-21" \
54+
LD_LIBRARY_PATH="$this_dir/open-jdk-21/lib/server" \
55+
"$this_dir/open-jdk-21/bin/java" \
56+
-Dquarkus.http.port=8081 \
57+
-Dquarkus.log.level=WARN \
58+
-Dquarkus.rest-client.basisprofiel-api.url=http://localhost:9999 \
59+
-Dquarkus.rest-client.email-api.url=http://localhost:9999 \
60+
-Dlogboekdataverwerking.enabled=false \
61+
-jar "$this_dir/quarkus-app/quarkus-run.jar" &
62+
QUARKUS_PID=$!
63+
trap "kill $QUARKUS_PID 2>/dev/null || true" EXIT
64+
65+
# Wait for Quarkus to accept connections (up to 20 seconds)
66+
for i in $(seq 1 80); do
67+
if curl -sf -o /dev/null http://localhost:8081/ 2>/dev/null; then
68+
break
69+
fi
70+
sleep 0.25
71+
done
72+
73+
# Build classpath from compiled classes and all dependency jars
74+
CP="$this_dir/test-classes:$this_dir/classes"
75+
for jar in "$this_dir"/lib/*.jar; do
76+
CP="$CP:$jar"
77+
done
78+
79+
JAVA_HOME="$this_dir/open-jdk-21" \
80+
LD_LIBRARY_PATH="$this_dir/open-jdk-21/lib/server":"$this_dir" \
81+
"$this_dir/jazzer_driver" \
82+
--agent_path="$this_dir/jazzer_agent_deploy.jar" \
83+
--cp="$CP" \
84+
--target_class=TARGET_CLASS_PLACEHOLDER \
85+
--jvm_args="$mem_settings" \
86+
"$@"
87+
WRAPPER_EOF
88+
else
89+
# ── Generic wrapper for in-process fuzzers ──
90+
cat > "$OUT/$simple_name" << 'WRAPPER_EOF'
91+
#!/bin/bash
92+
# LLVMFuzzerTestOneInput for jvm
93+
this_dir=$(dirname "$0")
94+
95+
if [[ "$@" =~ (^| )-runs=[0-9]+($| ) ]]; then
96+
mem_settings='-Xmx1900m:-Xss900k'
97+
else
98+
mem_settings='-Xmx2048m:-Xss1024k'
99+
fi
100+
101+
# Build classpath from compiled classes and all dependency jars
102+
CP="$this_dir/test-classes:$this_dir/classes"
103+
for jar in "$this_dir"/lib/*.jar; do
104+
CP="$CP:$jar"
105+
done
106+
107+
# Set JAVA_HOME and LD_LIBRARY_PATH inline so jazzer_driver loads
108+
# the bundled JDK 21 libjvm.so (not the runner's JDK 17).
109+
# Pattern taken from the oss-fuzz tomcat project.
110+
JAVA_HOME="$this_dir/open-jdk-21" \
111+
LD_LIBRARY_PATH="$this_dir/open-jdk-21/lib/server":"$this_dir" \
112+
"$this_dir/jazzer_driver" \
113+
--agent_path="$this_dir/jazzer_agent_deploy.jar" \
114+
--cp="$CP" \
115+
--target_class=TARGET_CLASS_PLACEHOLDER \
116+
--jvm_args="$mem_settings" \
117+
"$@"
118+
WRAPPER_EOF
119+
fi
120+
121+
sed -i "s|TARGET_CLASS_PLACEHOLDER|$class_name|" "$OUT/$simple_name"
122+
chmod +x "$OUT/$simple_name"
123+
done

.dockerignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
*
2+
3+
# ClusterFuzzLite Docker build (see .clusterfuzzlite/Dockerfile)
4+
!.clusterfuzzlite/*
5+
!src/**
6+
!pom.xml
7+
!mvnw
8+
!.mvn/**
9+
10+
# Quarkus production Docker builds (see src/main/docker/)
211
!target/*-runner
312
!target/*-runner.jar
413
!target/lib/*
5-
!target/quarkus-app/*
14+
!target/quarkus-app/*

.github/workflows/cflite_batch.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: ClusterFuzzLite Batch
2+
on:
3+
schedule:
4+
- cron: '0 0 * * 6' # Every week on saturday at midnight
5+
workflow_dispatch:
6+
permissions: read-all
7+
jobs:
8+
Batch:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v4
13+
- name: Build Fuzzers
14+
id: build
15+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
16+
with:
17+
language: jvm
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
- name: Run Fuzzers
20+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
21+
with:
22+
github-token: ${{ secrets.GITHUB_TOKEN }}
23+
fuzz-seconds: 3600
24+
mode: 'batch'

.github/workflows/cflite_cron.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: ClusterFuzzLite Cron
2+
on:
3+
schedule:
4+
- cron: '0 0 * * 0' # Every week on Sunday at midnight
5+
workflow_dispatch:
6+
permissions: read-all
7+
jobs:
8+
Cron:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v4
13+
- name: Build Fuzzers
14+
id: build
15+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
16+
with:
17+
language: jvm
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
- name: Run Fuzzers
20+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
21+
with:
22+
github-token: ${{ secrets.GITHUB_TOKEN }}
23+
fuzz-seconds: 600
24+
mode: 'coverage'
25+
- name: Run Fuzzers (Pruning)
26+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
27+
with:
28+
github-token: ${{ secrets.GITHUB_TOKEN }}
29+
fuzz-seconds: 600
30+
mode: 'prune'

.github/workflows/cflite_pr.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: ClusterFuzzLite PR
2+
on:
3+
pull_request:
4+
branches: [ main ]
5+
permissions: read-all
6+
jobs:
7+
PR:
8+
runs-on: ubuntu-latest
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
11+
cancel-in-progress: true
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
- name: Build Fuzzers
16+
id: build
17+
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
18+
with:
19+
language: jvm
20+
github-token: ${{ secrets.GITHUB_TOKEN }}
21+
- name: Run Fuzzers
22+
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
23+
with:
24+
github-token: ${{ secrets.GITHUB_TOKEN }}
25+
fuzz-seconds: 300
26+
mode: 'code-change'

.github/workflows/maven.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
push:
1515
branches: [ "main" ]
1616
pull_request:
17-
branches: [ "main" ]
17+
branches: [ main ]
1818

1919
jobs:
2020
build:

FUZZING.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Fuzz Testing in Moza Profiel Service
2+
3+
To boost the security of our application, we have implemented fuzz testing for our REST endpoints and core components. Fuzzing is a testing technique that provides semi-random data as input to the application to find bugs, crashes, or security vulnerabilities.
4+
5+
## What has been implemented?
6+
7+
We have implemented **two types** of fuzz tests:
8+
9+
### 1. JUnit-based Fuzz Tests (for local development)
10+
11+
- **EndpointFuzzTest.java**: A JUnit test class using `@FuzzTest` annotations and `RestAssured` to fuzz REST endpoints in a running Quarkus test instance.
12+
- **Purpose**: Quick feedback during development and as part of the regular test suite.
13+
- **Runs**: As part of `mvn test`, with configurable duration using `-Djazzer.duration=<time>`.
14+
15+
### 2. Standalone Fuzz Targets (for ClusterFuzzLite)
16+
17+
These are lightweight, standalone classes that implement the `fuzzerTestOneInput` method expected by Jazzer's native driver. They run in ClusterFuzzLite's continuous fuzzing pipeline:
18+
19+
- **EndpointFuzzer.java**: Coverage-guided fuzzing of all REST endpoints by starting Quarkus as a subprocess and sending HTTP requests.
20+
- **HashHelperFuzzer.java**: Tests the SHA-256 hashing functionality with arbitrary string inputs to verify determinism and detect edge cases.
21+
- **JsonDeserializationFuzzer.java**: Fuzzes JSON deserialization of all request DTOs (ContactgegevenRequest, VoorkeurRequest, DienstverlenerRequest, etc.) to find parsing vulnerabilities.
22+
23+
**Key Difference**: The JUnit tests run Quarkus in-process with `@QuarkusTest`, while the ClusterFuzzLite targets are optimized for long-running, coverage-guided fuzzing in CI/CD pipelines.
24+
25+
## How to run Fuzz Tests
26+
27+
By default, fuzz tests run as part of the normal test suite but with only a few iterations. To run them for a longer period (which is recommended for finding real issues), you can use the following Maven command:
28+
29+
```bash
30+
mvn test -Dtest=EndpointFuzzTest -Djazzer.duration=1m -Djacoco.skip=true
31+
```
32+
33+
The `-Djazzer.duration=1m` flag tells Jazzer to run for 1 minute. You can increase this (e.g., `10m`, `1h`) for more thorough testing.
34+
35+
> **Note**: We use `-Djacoco.skip=true` because running only a subset of tests (like just the fuzz tests) will likely fail the JaCoCo coverage check, as the overall project coverage threshold won't be met.
36+
37+
### Running a specific Fuzz Test
38+
39+
To run only one specific fuzz test method:
40+
41+
```bash
42+
mvn test -Dtest=EndpointFuzzTest#fuzzGetPartij -Djazzer.duration=1m -Djacoco.skip=true
43+
```
44+
45+
## Adding more Fuzz Tests
46+
47+
### Adding a JUnit-based Fuzz Test
48+
49+
To add a new fuzz test to `EndpointFuzzTest`:
50+
51+
1. Add a method to `EndpointFuzzTest` (or create a new test class).
52+
2. Annotate it with `@FuzzTest`.
53+
3. Use `FuzzedDataProvider` to generate semi-random input data.
54+
4. Use `RestAssured` to send this data to your endpoint.
55+
56+
Example:
57+
58+
```java
59+
@FuzzTest
60+
public void fuzzMyNewEndpoint(FuzzedDataProvider data) {
61+
String input = data.consumeRemainingAsString();
62+
63+
RestAssured.given()
64+
.body(input)
65+
.when()
66+
.post("/api/my-endpoint")
67+
.then()
68+
.extract().response();
69+
}
70+
```
71+
72+
### Adding a Standalone Fuzzer for ClusterFuzzLite
73+
74+
To add a new standalone fuzzer that runs in the CI/CD pipeline:
75+
76+
1. Create a new class in `src/test/java/nl/rijksoverheid/moz/fuzzing/`.
77+
2. Implement a `public static void fuzzerTestOneInput(FuzzedDataProvider data)` method.
78+
3. Use `data` to consume input and test your functionality.
79+
4. The fuzzer will automatically be detected by `.clusterfuzzlite/build.sh` and included in continuous fuzzing.
80+
81+
Example:
82+
83+
```java
84+
package nl.rijksoverheid.moz.fuzzing;
85+
86+
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
87+
88+
public class MyComponentFuzzer {
89+
90+
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
91+
String input = data.consumeRemainingAsString();
92+
93+
// Test your component with the fuzzed input
94+
MyComponent.process(input);
95+
}
96+
}
97+
```
98+
99+
## What to look for?
100+
101+
Jazzer will automatically stop and report if it finds:
102+
- An unhandled exception that causes the JVM to crash (e.g., `OutOfMemoryError`, `StackOverflowError`).
103+
- Any security-relevant exceptions if configured.
104+
- Assertions that fail.
105+
106+
If Jazzer finds a "finding", it will create a `fuzz-test-*.repro` file. You can use this file to reproduce the exact input that caused the failure.
107+
108+
## GitHub Scorecard & Continuous Fuzzing
109+
110+
To satisfy the [GitHub Scorecard](https://github.com/ossf/scorecard) "Fuzzing" requirement, this project is configured to use **ClusterFuzzLite (CFL)**.
111+
112+
### How it works:
113+
114+
1. **ClusterFuzzLite**: We have integrated ClusterFuzzLite via GitHub Actions (see `.clusterfuzzlite/` and `.github/workflows/cflite_*.yml`).
115+
2. **Continuous Testing**:
116+
- **PR Mode**: Every pull request triggers a short fuzzing session (5 minutes) to catch regressions before they are merged. This is configured to run on all pull requests with the target branch being `main`.
117+
- **Batch Mode**: A longer daily fuzzing session (1 hour) runs on the `main` branch to discover deeper issues.
118+
- **Manual Trigger**: Fuzzing workflows can also be triggered manually via the GitHub Actions "Run workflow" button.
119+
3. **Scorecard Recognition**: By having these workflows in place and running them, the OpenSSF Scorecard will recognize that the project is being actively fuzzed, which improves our security score.
120+
121+
### Configuration
122+
123+
- `.clusterfuzzlite/Dockerfile`: Defines the build environment (based on `oss-fuzz-base`).
124+
- `.clusterfuzzlite/build.sh`: Script that compiles the application and prepares the fuzzing targets for Jazzer.
125+
- `.github/workflows/cflite_pr.yml`: GitHub Action for PR fuzzing.
126+
- `.github/workflows/cflite_batch.yml`: GitHub Action for scheduled batch fuzzing.

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ Documentatie over de Profiel Service is te vinden op [de documentatie website va
99
## Quarkus
1010
Dit project draait op Quarkus. Meer informatie hierover staat in [quarkus.md](quarkus.md)
1111

12-
## Configuratie
1312
In de application.properties file staat een notifyNl gedeelte, deze moeten worden gevuld met information van https://admin.notifynl.nl/ vraag de developers van dit project voor deze gegevens.
1413

1514
Plaats deze gegevens vervolgens NIET in de `application.properties` file maar maak een file `/src/main/resources/application-dev.properties` aan en zet de values hier in. Deze file staat in de `.gitignore`.

0 commit comments

Comments
 (0)