Add DevSecOpsApp code with updated Jenkins pipeline for multi-environment deployment

This commit is contained in:
2025-11-30 15:24:19 +05:30
parent 30a2782a79
commit 3d49b0f4de
22 changed files with 22129 additions and 52 deletions

44
.gitignore vendored
View File

@@ -1,3 +1,20 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Production builds
/build
/dist
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode/
.idea/
@@ -8,15 +25,22 @@
.DS_Store
Thumbs.db
# Dependencies
node_modules/
package-lock.json
yarn.lock
# Logs
logs/
*.log
# Build
dist/
build/
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Environment
.env
.env.local
# Coverage directory used by tools like istanbul
coverage/
# Docker
.dockerignore
# Temporary files
tmp/
temp/

307
JENKINS_SETUP.md Normal file
View File

@@ -0,0 +1,307 @@
# Jenkins Setup Instructions
## Prerequisites
### 1. Jenkins Installation
- Jenkins server with Docker support
- Required plugins:
- Pipeline Plugin
- Docker Pipeline Plugin
- NodeJS Plugin
- Git Plugin
- Blue Ocean (optional, for better UI)
- Coverage Plugin
- Test Results Analyzer
### 2. Prerequisites on Jenkins Agent
The pipeline now uses a simplified approach that doesn't require specific tool configurations in Jenkins Global Tools. Instead, ensure these tools are available on your Jenkins agent:
#### Required Tools
```bash
# Node.js 18+ (Required)
node --version # Should show v18.x.x or higher
npm --version # Should be available with Node.js
# Docker (Optional - for containerization stages)
docker --version
# Git (Usually pre-installed)
git --version
# Curl (Usually pre-installed)
curl --version
```
#### Installation Commands for Jenkins Agent
```bash
# Install Node.js 18 (Ubuntu/Debian)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install Docker (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y docker.io
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
# Install Trivy for security scanning (Optional)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
```
### 3. Alternative: Using Jenkins Global Tools (Advanced)
If you prefer to use Jenkins Global Tools Configuration:
#### Node.js Tool Configuration
- Go to **Manage Jenkins > Global Tool Configuration**
- **Name**: `nodejs` (keep it simple)
- **Version**: NodeJS 18.x.x
- **Install automatically**: ✅
Then update the Jenkinsfile to include:
```groovy
tools {
nodejs 'nodejs'
}
```
### 3. Required Software on Jenkins Agent
```bash
# Install Trivy for security scanning
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Install Docker Compose
curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
```
## Pipeline Features
### ✅ Current Implementation
1. **Multi-stage Pipeline**: Organized stages for better visibility
2. **Parallel Execution**: Dependencies and tests run in parallel
3. **Environment Detection**: Different actions for different branches
4. **Docker Integration**: Build and scan container images
5. **Security Scanning**: Trivy integration for vulnerability scanning
6. **Artifact Management**: Archive build artifacts and reports
7. **Integration Testing**: Health checks and API testing
8. **Branch-based Deployment**: Different environments for different branches
### 🔄 Pipeline Stages
#### 1. **Checkout**
- Clone repository
- Get Git commit information
#### 2. **Environment Info**
- Display tool versions
- Build information
#### 3. **Install Dependencies** (Parallel)
- Backend: `npm ci`
- Frontend: `npm ci`
#### 4. **Code Quality & Security** (Parallel)
- Linting for both frontend and backend
- Security audit with `npm audit`
#### 5. **Test** (Parallel)
- Backend unit tests
- Frontend tests with coverage
#### 6. **Build** (Parallel)
- Build backend (if build script exists)
- Build frontend React application
#### 7. **Docker Build** (Conditional)
- Build Docker images for both services
- Only on main/development/release branches
#### 8. **Docker Security Scan** (Conditional)
- Scan images with Trivy
- Generate security reports
#### 9. **Integration Tests** (Conditional)
- Start services with docker-compose
- Run health checks and API tests
#### 10. **Deployment** (Branch-specific)
- **Development**: Auto-deploy to dev environment
- **Release**: Deploy to staging
- **Main**: Manual approval for production
## Branch Strategy
### 🌿 Development Branch
- Automatic deployment to development environment
- Full testing pipeline
- Security scanning
### 🚀 Release Branch
- Deploy to staging environment
- Full security validation
- Performance testing ready
### 📦 Main Branch
- Production deployment with manual approval
- Complete security validation
- Artifact archival
## Security Features
### 🔒 Implemented Security Checks
1. **Dependency Scanning**: `npm audit` for known vulnerabilities
2. **Container Scanning**: Trivy for Docker image vulnerabilities
3. **Code Quality**: Linting for code standards
4. **Security Reports**: JSON reports archived as artifacts
### 🛡️ Future Security Enhancements
- SAST (Static Application Security Testing)
- DAST (Dynamic Application Security Testing)
- Infrastructure as Code scanning
- Secret scanning
- License compliance checking
## Environment Variables
### Required Environment Variables
```bash
# Docker Registry (update in Jenkinsfile)
REGISTRY=your-docker-registry.com
# Notification settings
SLACK_WEBHOOK=your-slack-webhook
EMAIL_RECIPIENTS=team@company.com
```
## Usage
### 1. Create Pipeline Job
1. Go to Jenkins Dashboard
2. Click "New Item"
3. Choose "Pipeline"
4. Configure SCM to point to your repository
5. Set script path to `Jenkinsfile`
### 2. Configure Webhooks
Add webhook in GitHub repository settings:
- URL: `http://your-jenkins-server/github-webhook/`
- Events: Push, Pull Request
### 3. First Run
- The pipeline will auto-detect the branch
- Development branch triggers full pipeline with dev deployment
- Main branch requires manual approval for production
## Monitoring & Notifications
### 📊 Build Artifacts
- Test results and coverage reports
- Security scan reports
- Built frontend application
- Docker image information
### 📧 Notifications
- Success/failure notifications
- Security alert notifications
- Deployment confirmations
## Troubleshooting
### Common Issues
#### 1. **Tool not found errors**
```
Tool type "nodejs" does not have an install of "NodeJS-18" configured
```
**Solution**:
- Current Jenkinsfile doesn't require tool configuration
- Ensure Node.js is installed on Jenkins agent: `node --version`
- If needed, install with: `curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - && sudo apt-get install -y nodejs`
#### 2. **Node.js not available**
```
Node.js is not available. Please install Node.js 18+ on the Jenkins agent.
```
**Solution**: Install Node.js on the Jenkins agent machine:
```bash
# Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# CentOS/RHEL
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
sudo yum install -y nodejs
```
#### 2. **Missing FilePath context in post actions**
```
Required context class hudson.FilePath is missing
```
**Solution**: Already fixed in current Jenkinsfile with proper script blocks
#### 3. **Docker permission denied**
```
docker: Got permission denied while trying to connect to the Docker daemon socket
```
**Solution**: Add Jenkins user to docker group:
```bash
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
```
#### 4. **Trivy not found**
```
trivy: command not found
```
**Solution**: Install Trivy on Jenkins agent:
```bash
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
```
#### 5. **Port conflicts during integration tests**
```
curl: (7) Failed to connect to localhost port 3001
```
**Solution**: Ensure ports 3000, 3001, 80 are available on Jenkins agent
### Debug Commands
```bash
# Check Jenkins agent tools
which node npm docker trivy
# Verify Docker access
docker ps
# Test repository access
git clone https://github.com/K0ngS3ng/DevSecOpsApp.git
# Check tool configurations in Jenkins
curl -u admin:password http://jenkins-url/manage/configureTools/
```
### Pipeline Configuration Examples
#### Minimal Configuration (No Docker)
If Docker is not available, the pipeline will gracefully skip Docker-related stages:
```groovy
// Pipeline will automatically skip Docker stages if tools are not available
// Error handling is built-in for all Docker operations
```
#### Custom Tool Names
If you have different tool names configured:
```groovy
tools {
nodejs 'Node18' // Your custom NodeJS name
dockerTool 'MyDocker' // Your custom Docker name
}
```
## Next Steps
1. Configure actual deployment environments
2. Add more comprehensive tests
3. Integrate with monitoring tools
4. Set up notification channels
5. Add performance testing stages

99
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,99 @@
pipeline {
// 1. Run on your specific agent
agent { label 'jenkins-agent' }
environment {
REGISTRY_URL = 'registry.digitalocean.com/kongseng'
// 2. Dynamic Naming: Image tag includes Branch Name to prevent conflicts
// Example: registry.../backend:Dev-42 or registry.../backend:main-42
BACKEND_IMAGE = "${REGISTRY_URL}/devsecops-backend:${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
FRONTEND_IMAGE = "${REGISTRY_URL}/devsecops-frontend:${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
echo "Installing dependencies for ${env.BRANCH_NAME} branch..."
dir('backend') { sh 'npm install' }
dir('frontend') { sh 'npm install' }
}
}
stage('Build Docker Images') {
steps {
script {
echo "Building Images for branch: ${env.BRANCH_NAME}..."
sh "docker build -t ${BACKEND_IMAGE} ./backend"
sh "docker build -t ${FRONTEND_IMAGE} ./frontend"
}
}
}
stage('Push to Registry') {
steps {
script {
echo "Pushing images to DigitalOcean..."
sh "docker push ${BACKEND_IMAGE}"
sh "docker push ${FRONTEND_IMAGE}"
}
}
}
// --- DYNAMIC DEPLOYMENT ---
stage('Deploy') {
steps {
script {
// Define ports and names based on the branch
def appPort = "3000" // Fallback
def containerName = "backend-app"
if (env.BRANCH_NAME == 'Dev') {
appPort = "3001"
containerName = "backend-dev"
echo "Deploying to DEV Environment (Port 3001)"
}
else if (env.BRANCH_NAME == 'Release') {
appPort = "3002"
containerName = "backend-release"
echo "Deploying to STAGING Environment (Port 3002)"
}
else if (env.BRANCH_NAME == 'main') {
appPort = "3003"
containerName = "backend-prod"
echo "Deploying to PRODUCTION Environment (Port 3003)"
}
else {
// Logic for any future feature branches
appPort = "3004"
containerName = "backend-feature-${env.BRANCH_NAME}"
echo "Deploying Feature Branch"
}
// 1. Clean up old container
try {
sh "docker stop ${containerName} || true"
sh "docker rm ${containerName} || true"
} catch (Exception e) {
echo "No container to stop"
}
// 2. Run new container on the assigned port
sh """
docker run -d \
--name ${containerName} \
--restart always \
-p ${appPort}:3000 \
${BACKEND_IMAGE}
"""
}
}
}
}
}

46
PR_DESCRIPTION.md Normal file
View File

@@ -0,0 +1,46 @@
# 🔒 Feature: Enhanced TruffleHog Secret Detection Testing
## Purpose
This feature branch contains **intentional test secrets** to validate our enhanced TruffleHog configuration and ensure proper secret detection in our DevSecOps CI/CD pipeline.
## Changes Made
- ✅ Added comprehensive test secrets to `backend/server.js`:
- AWS Access Key & Secret Access Key
- GitHub Personal Access Token
- Database Password
- JWT Secret Key
- Stripe Secret Key
- SendGrid API Key
- Slack Webhook URL
- MongoDB Connection String
## Expected DevSecOps Pipeline Behavior
🚨 **This PR should FAIL the Jenkins build** due to secret detection:
1. **Jenkins Pipeline Trigger**: PR creation should trigger multibranch pipeline
2. **Secret Scan Stage**: TruffleHog should scan local workspace (`trufflehog filesystem . --fail`)
3. **Multiple Secret Detection**: Should detect 8+ different types of secrets
4. **Build Failure**: Pipeline should fail at "Secret Scan" stage
5. **Security Gate**: PR should be blocked from merging
## DevSecOps Learning Objectives
This feature validates our security controls:
- ✅ Local workspace scanning (vs remote GitHub scanning)
- ✅ Multiple secret pattern detection
- ✅ CI/CD security gate enforcement
- ✅ Automated security failure notifications
## Merge Strategy
**Target Branch**: `development`
## Post-Validation Steps
After confirming TruffleHog detection works:
1. Remove all test secrets from `server.js`
2. Update PR to pass security scan
3. Merge clean code into development
4. Document security scanning success
---
**⚠️ SECURITY WARNING: This PR contains test secrets and should NOT be merged until all secrets are removed!**
**📚 DevSecOps Learning**: This demonstrates "shift-left" security practices by catching secrets early in the development cycle.

View File

@@ -1,43 +1,6 @@
# DevSecOps-Lab
Final documentation update
Updated project with latest improvements
Sample Application for Learning DevSecOps
## Sample Web Page
A simple, responsive web application built with HTML, CSS, and JavaScript.
### Features
- Responsive design for all devices
- Modern UI with gradient backgrounds
- Smooth navigation and scrolling
- Contact form functionality
- Service cards showcase
### Project Structure
```
.
├── index.html # Main HTML file
├── styles.css # CSS styling
├── script.js # JavaScript functionality
└── README.md # This file
```
### Quick Start
1. Open `index.html` in your browser
2. Navigate through sections using the navigation menu
3. Click "Get Started" button to see the CTA functionality
4. Fill in the contact form to test form submission
### Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
### License
MIT License - Feel free to use this project as a starting point for your own web applications.
## Testing Pipeline Integration
This update tests the enhanced Jenkins pipeline with TruffleHog secret scanning.
Date: Mon Sep 15 00:43:23 IST 2025

34
backend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Simple Node.js backend Dockerfile
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeapp -u 1001
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy application files
COPY server.js ./
# Create logs directory and set permissions
RUN mkdir -p logs && chown -R nodeapp:nodejs /app
# Switch to non-root user
USER nodeapp
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
# Expose port
EXPOSE 3001
# Start the application
CMD ["node", "server.js"]

868
backend/package-lock.json generated Normal file
View File

@@ -0,0 +1,868 @@
{
"name": "todo-backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "todo-backend",
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

22
backend/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "todo-backend",
"version": "1.0.0",
"description": "Simple Backend API for Todo Application",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js",
"test": "echo \"No tests specified\" && exit 0",
"build": "echo \"Backend build completed\" && exit 0",
"lint": "echo \"Linting not configured yet\" && exit 0",
"security": "npm audit --audit-level=high"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"keywords": ["todo", "api", "express"],
"author": "DevOps Learner",
"license": "MIT"
}

195
backend/server.js Normal file
View File

@@ -0,0 +1,195 @@
const express = require('express');
const cors = require('cors');
require('dotenv').config();
// TESTING: Dummy secrets for TruffleHog detection - SHOULD TRIGGER SECURITY SCAN!
const AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE';
const AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
const GITHUB_TOKEN = 'ghp_1234567890abcdef1234567890abcdef12345678';
// Additional test secrets for comprehensive detection
const DATABASE_PASSWORD = 'super_secret_db_password_123!';
const JWT_SECRET = 'jwt_super_secret_key_for_authentication_2024';
const STRIPE_SECRET_KEY = 'sk_test_51234567890abcdef1234567890abcdef12345678';
const SENDGRID_API_KEY = 'SG.1234567890abcdef.1234567890abcdef1234567890abcdef1234567890abcdef';
const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX';
const MONGODB_CONNECTION = 'mongodb://admin:supersecret123@localhost:27017/devdb';
// FINAL TEST: Additional secret to verify TruffleHog with fixed Jenkinsfile
const TWITTER_API_KEY = 'twitter_api_key_1234567890abcdef1234567890abcdef1234567890';
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json());
// In-memory data store for simplicity
let todos = [
{ id: 1, title: 'Learn DevOps', completed: false, priority: 'high' },
{ id: 2, title: 'Setup CI/CD Pipeline', completed: false, priority: 'medium' },
{ id: 3, title: 'Deploy to Cloud', completed: false, priority: 'low' },
{ id: 4, title: 'Monitor Application', completed: true, priority: 'medium' }
];
let nextId = 5;
// Routes
// Health check
app.get('/health', (req, res) => {
// ⚠️ WARNING: This file contains test secrets for TruffleHog detection
// Real secrets like: password123, secret_api_key should never be hardcoded!
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
message: 'Backend server is running!'
});
});
// Get all todos
app.get('/api/todos', (req, res) => {
res.json({
success: true,
data: todos,
count: todos.length
});
});
// Get single todo
app.get('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(t => t.id === id);
if (!todo) {
return res.status(404).json({
success: false,
message: 'Todo not found'
});
}
res.json({
success: true,
data: todo
});
});
// Create new todo
app.post('/api/todos', (req, res) => {
const { title, priority = 'medium' } = req.body;
if (!title) {
return res.status(400).json({
success: false,
message: 'Title is required'
});
}
const newTodo = {
id: nextId++,
title,
completed: false,
priority
};
todos.push(newTodo);
res.status(201).json({
success: true,
data: newTodo,
message: 'Todo created successfully'
});
});
// Update todo
app.put('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
message: 'Todo not found'
});
}
const { title, completed, priority } = req.body;
if (title !== undefined) todos[todoIndex].title = title;
if (completed !== undefined) todos[todoIndex].completed = completed;
if (priority !== undefined) todos[todoIndex].priority = priority;
res.json({
success: true,
data: todos[todoIndex],
message: 'Todo updated successfully'
});
});
// Delete todo
app.delete('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
message: 'Todo not found'
});
}
todos.splice(todoIndex, 1);
res.json({
success: true,
message: 'Todo deleted successfully'
});
});
// Get todo statistics
app.get('/api/stats', (req, res) => {
const stats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
pending: todos.filter(t => !t.completed).length,
byPriority: {
high: todos.filter(t => t.priority === 'high').length,
medium: todos.filter(t => t.priority === 'medium').length,
low: todos.filter(t => t.priority === 'low').length
}
};
res.json({
success: true,
data: stats
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Endpoint not found'
});
});
// Error handler
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
success: false,
message: 'Internal server error'
});
});
// Start server
app.listen(PORT, () => {
console.log(`🚀 Backend server running on port ${PORT}`);
console.log(`📊 Health check: http://localhost:${PORT}/health`);
console.log(`📝 API endpoints: http://localhost:${PORT}/api/todos`);
console.log(`📈 Stats: http://localhost:${PORT}/api/stats`);
});
module.exports = app;
const API_KEY = 'sk-1234567890abcdef1234567890abcdef12345678';

45
docker-compose.yml Normal file
View File

@@ -0,0 +1,45 @@
version: '3.8'
services:
# Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: todo-backend
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3001
ports:
- "3001:3001"
networks:
- todo-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
# Frontend React App
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: todo-frontend
restart: unless-stopped
ports:
- "80:80"
depends_on:
- backend
networks:
- todo-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
todo-network:
driver: bridge

47
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
# Multi-stage build for React frontend
FROM node:18-alpine AS build
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY public/ ./public/
COPY src/ ./src/
# Build the application
RUN npm run build
# Production stage with Nginx
FROM nginx:alpine AS production
# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built React app
COPY --from=build /app/build /usr/share/nginx/html
# Create a non-root user for nginx
RUN addgroup -g 1001 -S nginxgroup && \
adduser -S nginxuser -u 1001 -G nginxgroup
# Change ownership of nginx directories
RUN chown -R nginxuser:nginxgroup /var/cache/nginx && \
chown -R nginxuser:nginxgroup /var/log/nginx && \
chown -R nginxuser:nginxgroup /etc/nginx/conf.d && \
chown -R nginxuser:nginxgroup /usr/share/nginx/html
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

86
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,86 @@
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Handle React Router
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to backend
location /api {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

19477
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
frontend/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "todo-frontend",
"version": "1.0.0",
"description": "Simple React frontend for Todo Application",
"private": true,
"dependencies": {
"@types/node": "^16.18.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^4.9.5",
"web-vitals": "^3.4.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --coverage --watchAll=false",
"test:watch": "react-scripts test",
"eject": "react-scripts eject",
"lint": "echo \"Linting not configured yet\" && exit 0",
"security": "npm audit --audit-level=high"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:3001"
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="DevSecOps Todo Application - A comprehensive todo app for learning DevSecOps practices"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>DevSecOps Todo App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

331
frontend/src/App.css Normal file
View File

@@ -0,0 +1,331 @@
.App {
text-align: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.App-header {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 2rem 0;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.App-header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.App-header p {
margin: 0.5rem 0 0 0;
font-size: 1.2rem;
opacity: 0.9;
}
.App-main {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
display: grid;
gap: 2rem;
}
.loading {
color: white;
font-size: 1.5rem;
padding: 2rem;
}
.error-message {
background-color: #ff4757;
color: white;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Statistics Section */
.stats-section {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.stats-section h2 {
margin-top: 0;
color: #333;
font-size: 1.8rem;
margin-bottom: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 1.5rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
/* Todo Section */
.todo-section {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.todo-section h2 {
margin-top: 0;
color: #333;
font-size: 1.8rem;
margin-bottom: 1.5rem;
}
.add-todo-form {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.todo-input {
flex: 1;
min-width: 200px;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.todo-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.priority-select {
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
background-color: white;
cursor: pointer;
transition: border-color 0.2s ease;
}
.priority-select:focus {
outline: none;
border-color: #667eea;
}
.add-button {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.add-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
}
.no-todos {
color: #666;
font-style: italic;
padding: 2rem;
background-color: #f8f9fa;
border-radius: 8px;
margin: 1rem 0;
}
.todo-list {
display: grid;
gap: 1rem;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.todo-item:hover {
border-color: #667eea;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.todo-item.completed {
opacity: 0.7;
background-color: #f8f9fa;
}
.todo-content {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.todo-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.todo-title {
font-size: 1rem;
color: #333;
flex: 1;
text-align: left;
}
.todo-item.completed .todo-title {
text-decoration: line-through;
color: #666;
}
.todo-priority {
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.delete-button {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 6px;
transition: background-color 0.2s ease;
}
.delete-button:hover {
background-color: #ffebee;
}
/* Info Section */
.info-section {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.info-section h3 {
margin-top: 0;
color: #333;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.info-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.info-section li {
padding: 0.5rem 0;
color: #555;
font-size: 1rem;
border-bottom: 1px solid #eee;
}
.info-section li:last-child {
border-bottom: none;
}
/* Responsive Design */
@media (max-width: 768px) {
.App-main {
padding: 1rem;
gap: 1rem;
}
.App-header h1 {
font-size: 2rem;
}
.add-todo-form {
flex-direction: column;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.todo-content {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.todo-priority {
align-self: flex-start;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.App-header {
padding: 1rem 0;
}
.App-header h1 {
font-size: 1.8rem;
}
.App-header p {
font-size: 1rem;
}
}

255
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,255 @@
import React, { useState, useEffect } from 'react';
import './App.css';
interface Todo {
id: number;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
}
interface Stats {
total: number;
completed: number;
pending: number;
byPriority: {
high: number;
medium: number;
low: number;
};
}
const API_BASE = process.env.REACT_APP_API_URL || '/api';
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [newTodoTitle, setNewTodoTitle] = useState('');
const [newTodoPriority, setNewTodoPriority] = useState<'low' | 'medium' | 'high'>('medium');
useEffect(() => {
fetchTodos();
fetchStats();
}, []);
const fetchTodos = async () => {
try {
const response = await fetch(`${API_BASE}/todos`);
const data = await response.json();
if (data.success) {
setTodos(data.data);
} else {
setError('Failed to fetch todos');
}
} catch (err) {
setError('Failed to connect to server');
console.error('Error fetching todos:', err);
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
try {
const response = await fetch(`${API_BASE}/stats`);
const data = await response.json();
if (data.success) {
setStats(data.data);
}
} catch (err) {
console.error('Error fetching stats:', err);
}
};
const addTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTodoTitle.trim()) return;
try {
const response = await fetch(`${API_BASE}/todos`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: newTodoTitle,
priority: newTodoPriority,
}),
});
const data = await response.json();
if (data.success) {
setTodos([...todos, data.data]);
setNewTodoTitle('');
fetchStats();
} else {
setError('Failed to add todo');
}
} catch (err) {
setError('Failed to add todo');
console.error('Error adding todo:', err);
}
};
const toggleTodo = async (id: number) => {
const todo = todos.find(t => t.id === id);
if (!todo) return;
try {
const response = await fetch(`${API_BASE}/todos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
completed: !todo.completed,
}),
});
const data = await response.json();
if (data.success) {
setTodos(todos.map(t => t.id === id ? data.data : t));
fetchStats();
}
} catch (err) {
console.error('Error updating todo:', err);
}
};
const deleteTodo = async (id: number) => {
try {
const response = await fetch(`${API_BASE}/todos/${id}`, {
method: 'DELETE',
});
const data = await response.json();
if (data.success) {
setTodos(todos.filter(t => t.id !== id));
fetchStats();
}
} catch (err) {
console.error('Error deleting todo:', err);
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return '#ff4444';
case 'medium': return '#ff8800';
case 'low': return '#44aa44';
default: return '#666';
}
};
if (loading) return <div className="loading">Loading...</div>;
return (
<div className="App">
<header className="App-header">
<h1>🚀 DevSecOps Todo Application</h1>
<p>A simple todo app for learning DevOps practices</p>
</header>
<main className="App-main">
{error && <div className="error-message">{error}</div>}
<div className="stats-section">
<h2>📊 Statistics</h2>
{stats && (
<div className="stats-grid">
<div className="stat-card">
<div className="stat-value">{stats.total}</div>
<div className="stat-label">Total</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.completed}</div>
<div className="stat-label">Completed</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.pending}</div>
<div className="stat-label">Pending</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.byPriority.high}</div>
<div className="stat-label">High Priority</div>
</div>
</div>
)}
</div>
<div className="todo-section">
<h2>📝 Add New Todo</h2>
<form onSubmit={addTodo} className="add-todo-form">
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="Enter todo title..."
className="todo-input"
/>
<select
value={newTodoPriority}
onChange={(e) => setNewTodoPriority(e.target.value as 'low' | 'medium' | 'high')}
className="priority-select"
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<button type="submit" className="add-button">Add Todo</button>
</form>
<h2>📋 Todo List</h2>
{todos.length === 0 ? (
<p className="no-todos">No todos yet! Add one above.</p>
) : (
<div className="todo-list">
{todos.map(todo => (
<div key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<div className="todo-content">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="todo-checkbox"
/>
<span className="todo-title">{todo.title}</span>
<span
className="todo-priority"
style={{ backgroundColor: getPriorityColor(todo.priority) }}
>
{todo.priority}
</span>
</div>
<button
onClick={() => deleteTodo(todo.id)}
className="delete-button"
>
🗑
</button>
</div>
))}
</div>
)}
</div>
<div className="info-section">
<h3>🛠 DevOps Features Ready</h3>
<ul>
<li> Frontend & Backend Communication</li>
<li> RESTful API Endpoints</li>
<li> CORS Configuration</li>
<li> Error Handling</li>
<li><EFBFBD> Ready for CI/CD Pipeline</li>
<li>🐳 Ready for Dockerization</li>
<li>📊 Monitoring & Health Checks</li>
</ul>
</div>
</main>
</div>
);
}
export default App;

35
frontend/src/index.css Normal file
View File

@@ -0,0 +1,35 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
box-sizing: border-box;
}
.MuiButton-root {
text-transform: none !important;
}
.error-message {
color: #d32f2f;
font-size: 0.875rem;
margin-top: 3px;
margin-left: 14px;
}
.success-message {
color: #2e7d32;
font-size: 0.875rem;
margin-top: 3px;
margin-left: 14px;
}

14
frontend/src/index.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App.tsx';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

152
scripts/health-check.sh Executable file
View File

@@ -0,0 +1,152 @@
#!/bin/bash
# Health Check Script for DevSecOps Todo Application
# This script verifies that both frontend and backend services are running correctly
set -e # Exit on any error
echo "🏥 Starting health checks for DevSecOps Todo Application..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
BACKEND_URL="${BACKEND_URL:-http://localhost:3001}"
FRONTEND_URL="${FRONTEND_URL:-http://localhost:3000}"
MAX_RETRIES=5
RETRY_DELAY=2
# Function to check service health
check_service() {
local url=$1
local service_name=$2
local retries=0
echo "🔍 Checking $service_name at $url..."
while [ $retries -lt $MAX_RETRIES ]; do
if curl -f -s "$url/health" > /dev/null 2>&1; then
echo -e "${GREEN}$service_name is healthy${NC}"
return 0
else
retries=$((retries + 1))
echo -e "${YELLOW}$service_name not ready, attempt $retries/$MAX_RETRIES${NC}"
if [ $retries -lt $MAX_RETRIES ]; then
sleep $RETRY_DELAY
fi
fi
done
echo -e "${RED}$service_name health check failed after $MAX_RETRIES attempts${NC}"
return 1
}
# Function to check API endpoints
check_api_endpoints() {
echo "🔍 Testing API endpoints..."
# Test todos endpoint
if curl -f -s "$BACKEND_URL/api/todos" > /dev/null 2>&1; then
echo -e "${GREEN}✅ Todos API endpoint is working${NC}"
else
echo -e "${RED}❌ Todos API endpoint failed${NC}"
return 1
fi
# Test stats endpoint
if curl -f -s "$BACKEND_URL/api/stats" > /dev/null 2>&1; then
echo -e "${GREEN}✅ Stats API endpoint is working${NC}"
else
echo -e "${RED}❌ Stats API endpoint failed${NC}"
return 1
fi
return 0
}
# Function to test basic functionality
test_basic_functionality() {
echo "🧪 Testing basic functionality..."
# Test creating a todo
local response=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Health Check Todo","priority":"low"}' \
"$BACKEND_URL/api/todos")
if echo "$response" | grep -q "Health Check Todo"; then
echo -e "${GREEN}✅ Todo creation test passed${NC}"
else
echo -e "${RED}❌ Todo creation test failed${NC}"
return 1
fi
return 0
}
# Main health check execution
main() {
echo "=================================================="
echo "🚀 DevSecOps Todo Application Health Check"
echo "=================================================="
local failed=0
# Check backend health
if ! check_service "$BACKEND_URL" "Backend"; then
failed=1
fi
# Check frontend health (if running on different port)
if [ "$FRONTEND_URL" != "$BACKEND_URL" ]; then
if ! check_service "$FRONTEND_URL" "Frontend"; then
failed=1
fi
fi
# Test API endpoints
if ! check_api_endpoints; then
failed=1
fi
# Test basic functionality
if ! test_basic_functionality; then
failed=1
fi
echo "=================================================="
if [ $failed -eq 0 ]; then
echo -e "${GREEN}🎉 All health checks passed!${NC}"
echo -e "${GREEN}📊 Application is ready for use${NC}"
exit 0
else
echo -e "${RED}💥 Some health checks failed!${NC}"
echo -e "${RED}🔧 Please check the application logs${NC}"
exit 1
fi
}
# Show usage if help requested
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
echo "DevSecOps Todo Application Health Check Script"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Environment Variables:"
echo " BACKEND_URL Backend service URL (default: http://localhost:3001)"
echo " FRONTEND_URL Frontend service URL (default: http://localhost:3000)"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Check local services"
echo " BACKEND_URL=http://api.example.com $0 # Check remote backend"
exit 0
fi
# Run main function
main "$@"

1
test.txt Normal file
View File

@@ -0,0 +1 @@
test

6
webhook-test.md Normal file
View File

@@ -0,0 +1,6 @@
# Webhook Test
This file is created to test GitHub webhook integration with Jenkins.
Created on: 14 September 2025
Purpose: Verify that changes trigger Jenkins builds automatically.