Repository navigation
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Goal
Build jdt2jar: a CLI tool + minimal container that compiles JTD schemas into standalone validator JARs at build time, eliminating the need for a JDK 24+ runtime.
User Story
# On any machine with Docker (or natively with JDK 24+)
docker run --rm -v $(pwd)/schemas:/schemas -v $(pwd)/out:/out \
ghcr.io/simbo1905/jdt2jar:latest \
/schemas/user.jtd.json --output /out/user-validator.jar
# The output JAR runs on JDK 21+ with zero ClassFile API dependency
java -jar /out/user-validator.jar --validate /data/payload.jsonCLI Interface
jdt2jar <schema.json> [options]
Options:
--output <path> Output JAR path (default: <schema-name>-validator.jar)
--package <name> Java package for generated classes (default: jtd.generated)
--class <name> Validator class name (default: SchemaValidator)
--main Include a main() for standalone CLI validation
--runtime <version> Target bytecode version (default: 21)
--include-sources Also output generated .java files alongside the JAR
--help Show help
Generated JAR Contents
user-validator.jar
├── META-INF/MANIFEST.MF
├── jtd/generated/SchemaValidator.class # implements JtdValidator
├── jtd/generated/SchemaValidator$*.class # helper classes
├── jtd/generated/SchemaValidator.java # (optional, --include-sources)
└── jtd/schema.json # embedded original schema
Usage Patterns
1. Library Integration
// Generated class implements JtdValidator
JtdValidator validator = new jtd.generated.SchemaValidator();
JtdValidationResult result = validator.validate(jsonPayload);
if (!result.isValid()) {
for (var err : result.errors()) {
System.err.println(err.instancePath() + ": " + err.schemaPath());
}
}2. Standalone CLI Validation
# JAR built with --main flag
java -jar user-validator.jar --validate payload.json
# Exit code 0 = valid, 1 = invalid, 2 = parse error
java -jar user-validator.jar --validate payload.json --format json
# Outputs RFC 8927 error pairs as JSON3. CI/CD Pipeline
# GitHub Actions example
- name: Compile JTD validators
run: |
docker run --rm -v $PWD/schemas:/schemas -v $PWD/validators:/out \
ghcr.io/simbo1905/jdt2jar:latest \
/schemas/events.jtd.json --output /out/events-validator.jar --main
- name: Validate test fixtures
run: java -jar validators/events-validator.jar --validate tests/fixtures/event.json4. Maven Plugin (future)
<plugin>
<groupId>io.github.simbo1905.json</groupId>
<artifactId>jdt2jar-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals><goal>compile</goal></goals>
<configuration>
<schemaDir>${project.basedir}/schemas</schemaDir>
<outputDir>${project.build.directory}/generated-classes</outputDir>
</configuration>
</execution>
</executions>
</plugin>Implementation Notes: Container Packaging Specification
Objective
Package the Java CLI tool jdt2jar as:
- a minimal OCI container image
- based on custom
jlinkruntime + Distroless Debian 13 ("trixie") base - suitable for Kubernetes, CI runners, ephemeral execution, rootless runtime
- no shell or package manager in final image
Use: gcr.io/distroless/base-debian13:nonroot
Runtime Requirements
- Java 21 target bytecode
jdt2jarpackaged as fat jar or modular jar- CLI entrypoint:
java -jar /app/jdt2jar.jar
Filesystem Layout
/
├── app/
│ └── jdt2jar.jar
├── jre/
│ ├── bin/java
│ └── lib/...
└── work/
Runtime user: uid=65532, gid=65532 (nonroot distroless default)
Build Strategy
Multi-stage Docker build:
- builder stage: full JDK → compile, tests,
jdeps,jlink - runtime stage: distroless Debian 13 → stripped runtime + application jar
Java Module Minimization
jdeps --ignore-missing-deps --recursive --multi-release 21 --print-module-deps /build/jdt2jar.jar
Expected minimum modules: java.base
Potential additions: java.logging, java.xml, java.naming, jdk.zipfs
jlink Requirements
jlink --add-modules "$MODULES" --strip-debug --compress=2 --no-header-files --no-man-pages --output /opt/jre
Base Image
FROM gcr.io/distroless/base-debian13:nonrootDo not use Java distroless variants — runtime is supplied via jlink.
Container Constraints
- No shell: all Docker directives must use JSON/vector form
- TLS: distroless base includes CA certs
- Writable paths: only
/workis writable - Signal handling: JVM runs as PID 1, must handle SIGTERM/SIGINT
- Logging: stdout/stderr only, no filesystem logging
Recommended JVM Flags
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-Djava.io.tmpdir=/work/tmp
-XX:+ExitOnOutOfMemoryError (optional)
Dockerfile
# -------------------------------------------------
# Build stage
# -------------------------------------------------
FROM eclipse-temurin:24-jdk AS build
WORKDIR /build
COPY . .
RUN ./mvnw clean package -DskipTests
RUN cp target/jdt2jar.jar /build/jdt2jar.jar
RUN jdeps \
--ignore-missing-deps \
--recursive \
--multi-release 21 \
--print-module-deps \
/build/jdt2jar.jar \
> /build/modules.txt
RUN jlink \
--add-modules $(cat /build/modules.txt) \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output /opt/jre
# -------------------------------------------------
# Runtime stage
# -------------------------------------------------
FROM gcr.io/distroless/base-debian13:nonroot
WORKDIR /work
COPY --from=build /opt/jre /jre
COPY --from=build /build/jdt2jar.jar /app/jdt2jar.jar
ENTRYPOINT ["/jre/bin/java","-jar","/app/jdt2jar.jar"]Expected Image Sizes
| Variant | Size |
|---|---|
| Full JDK image | 350MB–500MB |
| Distroless Java runtime | 120MB–180MB |
| jlink + distroless | 35MB–80MB |
Security Characteristics
Final image contains:
- no package manager, no shell, no compiler, no debugger, no build tools
- attack surface minimized to: glibc + JVM runtime modules + application code
CI Validation
# Verify runtime starts
docker run --rm jdt2jar --help
# Verify no shell exists
docker run --rm jdt2jar /bin/sh
# Expected: executable file not found
# Verify nonroot execution
docker run --rm jdt2jar id
# Expected: uid=65532(nonroot)Optional Enhancements
- SBOM generation via
syft - Vulnerability scanning via
grype - Multi-arch builds (
linux/amd64,linux/arm64) viadocker buildx
Reacted by Copilot
Metadata
Metadata
Labels
enhancementNew feature or requestNew feature or request
Activity
simbo1905 commentedon May 20, 2026
Non-Functional Requirements: Image Visibility (syft + grype)
Once you move to distroless, stripped runtimes, and custom jlink images, you lose most normal operational visibility.
syftandgryperestore visibility at the image layer.syft — SBOM Generation
syftgenerates a Software Bill Of Materials, answering: what exactly is inside this container?It inventories:
Output:
This matters because:
SBOMs are now commonly required for: enterprise supply chain, SLSA, FedRAMP, SOC2, ISO 27001, procurement/security review.
grype — Vulnerability Scanning
grypeconsumes an SBOM or image and answers: which known CVEs are in this image?Output:
This is important because distroless reduces attack surface but does NOT eliminate vulnerabilities — especially glibc, openssl, JVM native libs, and bundled JARs.
Why This Matters More With Distroless
Traditional images:
apt list,rpm -qa,apk infoall work.Distroless: no package manager, no shell, minimal metadata. Normal ops tooling breaks.
syftreconstructs component inventory directly from: ELF binaries, JAR manifests, filesystem contents, package DB remnants, JVM module structure.Why This Matters More With jlink
jlink creates custom JVMs with arbitrary subsets of modules. Scanners cannot assume a standard JRE; security teams cannot assume stock distributions.
Example material differences:
java.desktopmissingjdk.crypto.ecpresentjdk.naming.rmiabsentThat materially changes: attack surface, crypto capability, TLS support, exploitability. SBOM tooling lets you prove exactly what shipped.
Practical CI Pipeline
Fail build on: critical CVEs, forbidden packages, unexpected dependencies.
Concrete Example
A transitive Maven dependency pulls
commons-compresswith a vulnerable TAR parser. Without SBOM scanning it's hidden inside the fat JAR, invisible in the distroless runtime. Withsyft+grypethe dependency is identified and the CVE surfaced automatically.Important Limitation
These tools do NOT prove code safety, absence of logic bugs, or absence of RCEs. They only identify known vulnerable components and supply-chain composition. They are inventory + vulnerability correlation systems.