DevSecOps · Self-directed · Jenkins shared-pipeline demo

filmapp — a DevSecOps pipeline, not just a React app

A React + TypeScript streaming-app demo whose interesting part is the Jenkins pipeline around it. Static analysis with SonarQube, dependency CVE scanning with OWASP Dependency-Check, filesystem + image scanning with Trivy, and a final rollout to Kubernetes — every gate enforced before an image reaches the registry.

React TypeScript Jenkins SonarQube OWASP DC Trivy Docker Kubernetes DockerHub

01The point of this project

The app itself is a thin React + TypeScript frontend that pulls movie metadata from the TMDB API. It's a vehicle — the real deliverable is a Jenkinsfile that demonstrates a full "shift-left" security posture on every commit.

Most CI pipelines I see stop at "unit tests pass, push image." This one asks four more questions before it promotes anything:

  • Is the code clean? → SonarQube quality gate.
  • Are the dependencies safe? → OWASP Dependency-Check.
  • Is the filesystem clean of known CVEs? → Trivy FS scan.
  • Is the image we just built clean? → Trivy image scan.

02Pipeline stages

01 · Clean workspaceFresh agent workspace — no carry-over from previous runs.
02 · CheckoutPull the repo from main.
03 · SonarQubeFull project scan — bugs, code smells, coverage, duplication.
04 · Quality gateWait for SonarQube's server-side verdict before continuing.
05 · npm installDeterministic install from the lockfile.
06 · OWASP DCDependency CVE scan with an XML report published to Jenkins.
07 · Trivy FS scanFilesystem-level CVE scan of the checked-out code.
08 · Docker build & pushBuild with TMDB API key injected from Jenkins credentials; push to DockerHub.
09 · Trivy image scanRe-scan the just-published image — catches anything the base image brought in.
10 · DeployContainer rollout — locally via docker run for the demo; Kubernetes manifests for the real deploy.

03Secrets stay in Jenkins

The original version of this Jenkinsfile had the TMDB API key baked straight into docker build --build-arg. After GitHub's secret scanner (rightly) caught it, the key moved into Jenkins credentials and is injected as an environment variable only at build time:

environment {
  SCANNER_HOME    = tool 'sonar-scanner'
  // Injected from Jenkins credentials — never hardcoded.
  TMDB_V3_API_KEY = credentials('tmdb-v3-api-key')
}

stage('Docker Build & Push') {
  steps {
    script {
      withDockerRegistry(credentialsId: 'docker', toolName: 'docker') {
        sh 'docker build --build-arg TMDB_V3_API_KEY=$TMDB_V3_API_KEY -t filmapp .'
        sh 'docker tag filmapp ofdengiz/filmapp:latest'
        sh 'docker push ofdengiz/filmapp:latest'
      }
    }
  }
}

04Kubernetes rollout

The same image that passed all four gates deploys into Kubernetes via a plain Deployment + Service pair. Replicas=2 for demo purposes; the selector/label pair matches the Deployment and Service so one can't drift from the other.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: filmapp
  labels:
    app: filmapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: filmapp
  template:
    metadata:
      labels:
        app: filmapp
    spec:
      containers:
        - name: filmapp
          image: ofdengiz/filmapp:latest
          ports:
            - containerPort: 80

05Decisions I'd defend

Gate on quality, fail loudly

waitForQualityGate on the SonarQube step means if the gate is red, the pipeline stops. Not a warning — a stop. If the gate is not doing that, it is decoration, not enforcement.

Scan twice — FS and image

Filesystem scan catches CVEs in the repo; image scan catches CVEs the base image pulled in. They overlap but are not redundant, and Trivy is cheap enough to run both.

Secrets via credentials(), not env vars

The original TMDB key was hardcoded — after a secret-scanner ping, it moved into Jenkins credentials with a Groovy helper so the value is never written into the Jenkinsfile or the job log.

node 20, not node 16

Node 16 went EOL in September 2023. Pipelines that still pin to it on a demo repo are a signal that the repo isn't maintained — so this one runs on node 20 (LTS).