Initial commit: webshop frontend with legacy tracking module

Node/Express, intentionally outdated deps (express 4.16.0, lodash 4.17.15,
axios 0.21.1) for SCA demo. CWE-79, CWE-327, CWE-798, CWE-330 in legacy/tracking.js.
This commit is contained in:
2026-09-02 11:33:16 +00:00
parent ab5881b0be
commit 2049754b67
12 changed files with 427 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
rules:
- id: no-eval
patterns:
- pattern: eval(...)
message: "eval() is banned — remote code execution risk (CWE-95)"
severity: ERROR
languages: [javascript]
- id: no-child-process-exec
patterns:
- pattern: require('child_process').exec(...)
message: "child_process.exec() is banned — command injection risk (CWE-78)"
severity: ERROR
languages: [javascript]
+13
View File
@@ -0,0 +1,13 @@
# Build stage: standard node image (root, npm can write). Runtime: Chainguard zero-CVE base.
FROM public.ecr.aws/docker/library/node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY src/ ./src/
COPY legacy/ ./legacy/
FROM cgr.dev/chainguard/node:latest
WORKDIR /app
COPY --from=build /app ./
EXPOSE 3000
CMD ["src/server.js"]
Vendored
+233
View File
@@ -0,0 +1,233 @@
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: build
image: public.ecr.aws/docker/library/node:20
command: ["sleep", "999999"]
resources: { limits: { memory: 1Gi } }
- name: semgrep
image: semgrep/semgrep:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 1Gi } }
- name: gitleaks
image: ghcr.io/gitleaks/gitleaks:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 256Mi } }
- name: trivy
image: ghcr.io/aquasecurity/trivy:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 1Gi } }
- name: sonar
image: sonarsource/sonar-scanner-cli:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 2Gi } }
env:
- name: SONAR_TOKEN
valueFrom: { secretKeyRef: { name: sonar-token, key: SONAR_TOKEN } }
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
command: ["/busybox/sh", "-c", "sleep 999999"]
resources: { limits: { memory: 1Gi } }
- name: checkov
image: bridgecrew/checkov:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 512Mi } }
- name: cosign
image: public.ecr.aws/docker/library/alpine:3.20
command: ["sleep", "999999"]
resources: { limits: { memory: 256Mi } }
env:
- name: COSIGN_PASSWORD
valueFrom: { secretKeyRef: { name: cosign-keys, key: password } }
volumeMounts:
- { name: cosign-keys, mountPath: /keys, readOnly: true }
- name: zap
image: ghcr.io/zaproxy/zaproxy:stable
command: ["sleep", "999999"]
resources: { limits: { memory: 1Gi } }
- name: git
image: alpine/git:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 128Mi } }
- name: uploader
image: curlimages/curl:latest
command: ["sleep", "999999"]
resources: { limits: { memory: 128Mi } }
env:
- name: DD_TOKEN
valueFrom: { secretKeyRef: { name: defectdojo-token, key: DD_TOKEN } }
volumes:
- name: cosign-keys
secret: { secretName: cosign-keys }
'''
}
}
environment {
REGISTRY = "10.0.1.215:30500"
IMAGE = "webshop-ui"
SONAR_HOST = "http://sonarqube-sonarqube.platform.svc:9000"
DD_URL = "http://defectdojo-django.security.svc"
APP_URL = "http://webshop-ui.demo-app.svc:3000"
}
stages {
stage('Install & Test') {
steps { container('build') { sh 'npm install --no-audit --no-fund && npm test' } }
}
stage('SAST — Semgrep audit') {
steps {
container('semgrep') {
sh 'semgrep scan --config auto --json --output semgrep.json --metrics=off || true'
}
}
}
stage('SAST Gate') {
steps {
container('semgrep') {
sh 'semgrep scan --config .semgrep-gate.yml --error --exclude=legacy/tracking.js --metrics=off .'
}
}
}
stage('Secrets — Gitleaks') {
steps {
container('gitleaks') {
sh 'gitleaks detect --source . --no-git --report-format json --report-path gitleaks.json || true'
}
}
}
stage('SCA — Trivy deps') {
steps {
container('trivy') {
sh 'trivy fs --scanners vuln --format json --output trivy-fs.json .'
sh 'trivy fs --scanners vuln --severity HIGH,CRITICAL .'
}
}
}
stage('SonarQube') {
steps {
container('sonar') {
sh 'sonar-scanner -Dsonar.host.url=$SONAR_HOST -Dsonar.token=$SONAR_TOKEN -Dsonar.qualitygate.wait=false'
}
}
}
stage('Build — Kaniko') {
steps {
container('kaniko') {
sh '''/kaniko/executor \
--context "dir://$(pwd)" \
--dockerfile Dockerfile \
--destination "$REGISTRY/$IMAGE:$BUILD_NUMBER" \
--tar-path image.tar \
--insecure --insecure-pull'''
}
}
}
stage('Image Scan — Trivy') {
steps {
container('trivy') {
sh 'trivy image --input image.tar --format json --output trivy-image.json'
}
}
}
stage('SBOM — CycloneDX') {
steps {
container('trivy') {
sh 'trivy image --input image.tar --format cyclonedx --output sbom.cdx.json'
}
}
}
stage('IaC — Checkov') {
steps {
container('checkov') {
sh 'checkov -d . --quiet -o json > checkov.json || true'
}
}
}
stage('Sign — Cosign') {
steps {
container('cosign') {
sh '''
wget -qO /tmp/cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x /tmp/cosign
/tmp/cosign sign --key /keys/cosign.key --allow-insecure-registry --tlog-upload=false --yes "$REGISTRY/$IMAGE:$BUILD_NUMBER"
'''
}
}
}
stage('Deploy — GitOps bump') {
steps {
container('git') {
sh '''
git config --global --add safe.directory "$(pwd)"
git config user.name "Jenkins CI"
git config user.email "jenkins@demo.local"
sed -i "s/^ tag: .*/ tag: \\"$BUILD_NUMBER\\"/" helm/values.yaml
if git diff --quiet helm/values.yaml; then
echo "values.yaml already points at build $BUILD_NUMBER — nothing to push"
else
git add helm/values.yaml
git commit -m "ci: deploy build $BUILD_NUMBER [ci skip]"
git push http://demo:devsecops@gitea-http.platform.svc:3000/demo/$IMAGE.git HEAD:main
fi
'''
}
}
}
stage('DAST — OWASP ZAP') {
steps {
container('uploader') {
sh '''
echo "Waiting for deployment to become healthy..."
for i in $(seq 1 60); do
if curl -sf "$APP_URL/health" > /dev/null; then echo "App is up"; break; fi
sleep 10
done
'''
}
container('zap') {
sh '''
rm -rf /zap/wrk && ln -s "$(pwd)" /zap/wrk
zap-baseline.py -t "$APP_URL" -J zap.json -I || true
'''
}
}
}
stage('Publish — DefectDojo') {
steps {
container('uploader') {
sh '''
upload() {
[ -f "$2" ] || { echo "skip $1 ($2 missing)"; return 0; }
curl -sf -X POST "$DD_URL/api/v2/import-scan/" \
-H "Authorization: Token $DD_TOKEN" \
-F scan_type="$1" \
-F file=@"$2" \
-F product_name=$IMAGE \
-F engagement_name="CI Build $BUILD_NUMBER" \
-F auto_create_context=true \
-F active=true -F verified=true \
> /dev/null && echo "uploaded: $1" || echo "FAILED: $1"
}
upload "Semgrep JSON Report" semgrep.json
upload "Gitleaks Scan" gitleaks.json
upload "Trivy Scan" trivy-fs.json
upload "Trivy Scan" trivy-image.json
upload "Checkov Scan" checkov.json
upload "ZAP Scan" zap.json
'''
}
}
}
}
post {
always {
archiveArtifacts artifacts: '*.json', allowEmptyArchive: true
}
success { echo 'Pipeline PASSED — findings aggregated in DefectDojo.' }
failure { echo 'Pipeline FAILED — a security gate blocked the release.' }
}
}
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v2
name: webshop-ui
version: 1.0.0
description: DevSecOps demo application (webshop-ui)
+44
View File
@@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
team: platform-demo
environment: demo
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
team: platform-demo
environment: demo
spec:
securityContext:
runAsNonRoot: true
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 3000
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources: {{ toYaml .Values.resources | nindent 12 }}
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.port }}
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
+15
View File
@@ -0,0 +1,15 @@
replicaCount: 1
image:
repository: 10.0.1.215:30500/webshop-ui
tag: "1"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 3000
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 192Mi
cpu: 200m
+25
View File
@@ -0,0 +1,25 @@
// LEGACY MODULE — INTENTIONAL VULNERABILITIES FOR DEMO
// Included in full security scans, excluded from the blocking SAST gate.
const crypto = require('crypto');
// CWE-798: Hardcoded credentials
const ANALYTICS_API_KEY = 'AIzaSyDEMO-FakeGoogleKey-1234567890abcd';
const JWT_SECRET = 'webshop-jwt-secret-2018';
const AWS_ACCESS_KEY_ID = 'AKIAW3BSH0PDEMOTRACK';
// CWE-327: Weak hash
function trackingId(userEmail) {
return crypto.createHash('md5').update(userEmail).digest('hex');
}
// CWE-79: Unescaped template building
function renderBanner(campaign) {
return `<div class="banner" onclick="track('${campaign}')">${campaign}</div>`;
}
// CWE-330: Insecure randomness for session tokens
function sessionToken() {
return Math.random().toString(36).slice(2);
}
module.exports = { trackingId, renderBanner, sessionToken, JWT_SECRET };
+19
View File
@@ -0,0 +1,19 @@
{
"name": "webshop-ui",
"version": "1.0.0",
"description": "Webshop frontend demo",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"test": "jest"
},
"dependencies": {
"express": "4.16.0",
"lodash": "4.17.15",
"axios": "0.21.1"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^6.3.3"
}
}
+4
View File
@@ -0,0 +1,4 @@
sonar.projectKey=webshop-ui
sonar.projectName=Webshop UI
sonar.sources=src,legacy
sonar.tests=test
+31
View File
@@ -0,0 +1,31 @@
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const products = [
{ id: 1, name: 'Notebook Pro 14', price: 32990 },
{ id: 2, name: 'USB-C Dock', price: 2490 },
{ id: 3, name: 'Mechanical Keyboard', price: 3190 },
];
app.get('/health', (req, res) => res.json({ status: 'healthy' }));
app.get('/', (req, res) => {
const rows = products
.map((p) => `<tr><td>${p.id}</td><td>${p.name}</td><td>${p.price} CZK</td></tr>`)
.join('');
res.send(`<html><body><h1>Demo Webshop</h1><table>${rows}</table></body></html>`);
});
app.get('/api/products', (req, res) => res.json(products));
// Reflected output — DAST playground
app.get('/search', (req, res) => {
const q = req.query.q || '';
res.send(`<html><body><h1>Results for: ${q}</h1><p>No products found.</p></body></html>`);
});
if (require.main === module) {
app.listen(PORT, () => console.log(`Webshop running on ${PORT}`));
}
module.exports = app;
+15
View File
@@ -0,0 +1,15 @@
const request = require('supertest');
const app = require('../src/server');
describe('Webshop', () => {
test('GET /health returns healthy', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
});
test('GET /api/products returns products', async () => {
const res = await request(app).get('/api/products');
expect(res.status).toBe(200);
expect(res.body.length).toBeGreaterThan(0);
});
});