Skip to content

Repository files navigation

Genesis — Secure Spring Boot API + DevSecOps Pipeline

A minimal but real Spring Boot player-wallet API, wired to a production-shaped CI/CD security pipeline. Built as a learning sandbox and interview portfolio piece for an Application Security Engineer role.

The whole repo is organised around one idea:

The PR is the security control point, and branch protection is what makes it binding. The application code embodies the secure patterns; the pipeline enforces them.


Why this repo exists

Most "AppSec portfolios" are either an app with no pipeline, or a pipeline with no app to scan. This has both, so the story is end-to-end: a developer opens a PR → scanners run → the gate blocks on critical/high → merge builds and signs the artifact you actually deploy → posture is graded continuously.

The app is small on purpose. The interesting part is that every endpoint is the secure counterpart to a classic vulnerability, so you can point at real code when you talk.


What the app does

A tiny slice of a lottery-style platform: register a player, log in for a JWT, read your wallet.

Endpoint Notes
POST /api/auth/register Input-validated; password stored as a BCrypt hash
POST /api/auth/login Generic error (no user enumeration); issues a signed, algorithm-pinned JWT
GET /api/wallet/me Your wallet, resolved from the verified token — no id in the path
GET /api/wallet/{walletId} IDOR/BOLA-safe: scoped to the owner at the data layer → foreign id returns 404
GET /actuator/health The only public actuator endpoint

Demo users are seeded at startup (dev profile only): alice / bob, password correct-horse-battery.

Secure patterns demonstrated in code

  • Broken access control / IDOR (CWE-639)WalletRepository.findByIdAndOwnerId(...) scopes authz at the data layer.
  • SQL injection (CWE-89) → derived/bound queries only; no string concatenation.
  • Weak password hashing (CWE-916)BCryptPasswordEncoder(12).
  • JWT signature bypass / alg confusion (CWE-347) → signature always verified, algorithm pinned, iss/aud/exp validated.
  • Hardcoded secrets (CWE-798) → signing secret comes from JWT_SECRET; nothing sensitive in source.
  • Information exposure (CWE-209) → global handler returns generic errors; stack traces never leak.

Run it locally

Requires JDK 21 and Maven 3.6.3+ (built and tested against 3.8.7). CI and Docker use their own Maven (3.9), which is independent of your local version.

# 1) build, run tests (incl. the IDOR-block test), generate an SBOM
mvn -B verify

# 2) run
export JWT_SECRET="$(openssl rand -base64 48)"   # optional; ephemeral key if unset
mvn spring-boot:run

# 3) try it
curl -s localhost:8080/actuator/health

TOKEN=$(curl -s localhost:8080/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"correct-horse-battery"}' | jq -r .accessToken)

curl -s localhost:8080/api/wallet/me -H "Authorization: Bearer $TOKEN"

Run with Docker (distroless, non-root image):

docker build -t genesis .
docker run -p 8080:8080 -e JWT_SECRET="$(openssl rand -base64 48)" genesis

The pipeline

 developer            PULL REQUEST (the gate)              MERGE → main               continuous
 ─────────      ────────────────────────────────      ─────────────────────       ─────────────
 (local)        pr-security.yml                        build-sign-scan.yml         scorecard.yml
                ├─ gitleaks     (secrets)  →SARIF      ├─ build image              (repo posture)
                ├─ Semgrep OSS  (SAST)     →SARIF      ├─ Trivy scan the IMAGE     dast.yml
                ├─ Trivy fs     (SCA)      →SARIF      ├─ Syft SBOM                (ZAP, scheduled,
                ├─ Maven build + tests + SBOM          ├─ cosign keyless sign       informational)
                └─ security-gate (fails on any)        └─ SLSA provenance attest
                       │
                 branch protection = required check + CODEOWNERS + up-to-date + no direct push
File Purpose
.github/workflows/pr-security.yml The PR gate: gitleaks + Semgrep + Trivy + build/test, aggregated security-gate job that fails on any critical/high.
.github/workflows/build-sign-scan.yml Trunk pipeline: scans the built image, SBOM, keyless cosign signing, provenance attestation, push to GHCR.
.github/workflows/dast.yml OWASP ZAP baseline against a running instance (scheduled, informational).
.github/workflows/scorecard.yml OSSF Scorecard supply-chain grading.
.github/CODEOWNERS Required review on risky paths and on the pipeline's own config.
.gitleaks.toml / .trivyignore.yaml Governed allowlists (reason + expiry required).
scripts/setup-branch-protection.sh Makes the gate binding on main.
Dockerfile Hardened multi-stage → distroless, non-root runtime.

Details, design rationale, the Checkmarx drop-in, and deploy-time verification are in docs/PIPELINE.md.


First-time setup on your GitHub

  1. Push this repo (public repo recommended so cosign/Scorecard work without extra config).
  2. Replace @your-github-username in .github/CODEOWNERS with your handle.
  3. ./scripts/setup-branch-protection.sh <owner>/genesis-secure-pipeline (needs the gh CLI).
  4. Settings → Code security → enable secret scanning + push protection.
  5. Settings → Rules → enable a merge queue (so the gate re-runs against post-merge state).
  6. Open a PR and watch the checks run. Then try the exercise below.

Learn by breaking the gate

The best way to understand each control is to watch it fail. See docs/DEMO-break-the-gate.md — small, reversible changes that each trip a specific gate (introduce SQLi → Semgrep goes red; downgrade a dependency → Trivy goes red; commit a fake key → gitleaks goes red; break the IDOR scope → the integration test goes red).


Honest scope notes (talking points, not hidden)

  • Semgrep OSS stands in for Checkmarx so the repo runs with no paid secrets; docs/PIPELINE.md shows the one-job swap. The JD names Checkmarx — the integration pattern (incremental on PR, full nightly, delta-gate on new findings, SARIF to the Security tab) is identical.
  • Actions use version tags, not SHAs. SHA-pinning is the recommended hardening step; Dependabot (github-actions ecosystem) is configured to maintain those pins. Left as tags here for readability — a deliberate, documented trade-off.
  • DAST is informational. A token-gated JSON API gives a baseline crawl little to work with; meaningful DAST would need authenticated ZAP scripting. Wired up and running, but honestly scoped.

About

No description, website, or topics provided.

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages