Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
node_modules
npm-debug.log
.env
.git
.github
tests
coverage
.eslintrc.json
.prettierrc
stryker.config.json
jest.config.js
Dockerfile
.dockerignore
kubernetes
112 changes: 112 additions & 0 deletions .github/workflows/devsecops.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# High-Assurance DevSecOps Continuous Deployment Pipeline
name: High-Assurance DevSecOps CD Pipeline

# Trigger the pipeline on every push to the 'main' branch
on:
push:
branches: [ main ]

# Global Environment Variables
env:
# Registry to host the Docker images
REGISTRY: ghcr.io
# The name of the image based on the repository name
IMAGE_NAME: ${{ github.repository }}

jobs:
# STAGES 1-10: Security & Quality Gates
# This job must pass before we even consider building a container
quality-and-security:
runs-on: ubuntu-latest
steps:
- # Checkout the latest code from the repository
uses: actions/checkout@v4

- # Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- # Clean install of all dependencies
run: npm ci

- # Stage 1 & 2: Check for code quality and vulnerable dependencies
name: Lint & SCA
run: |
npm run lint
npm run security-audit

- # Stage 3-5: Validate DB Schema, Generate Client, and Run Unit Tests
name: Prisma & Tests
run: |
npm run prisma:validate
npm run db:generate
npm run test:coverage

# STAGE 11: Containerization (Build & Push)
# This job builds the Docker image and uploads it to the GitHub Container Registry
build-and-push:
needs: [quality-and-security]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Required to push to GHCR
steps:
- uses: actions/checkout@v4

- # Authenticate Docker with GitHub's registry using the built-in GITHUB_TOKEN
name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- # Generate dynamic tags (e.g., commit SHA, 'latest') for the image
name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=long
type=ref,event=branch
latest

- # Build the multi-stage Docker image and push it to the registry
name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

# STAGE 12: Kubernetes Orchestration & Deployment
# This final stage applies the updated image to the Kubernetes cluster
deploy:
needs: [build-and-push]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- # Configure kubectl to connect to your specific cluster
name: Set Kubernetes Context
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
# This secret must be added by the user in GitHub Settings
kubeconfig: ${{ secrets.KUBE_CONFIG }}

- # Update the deployment manifest with the fresh image and apply it
name: Deploy to Kubernetes
run: |
# Dynamically swap placeholders in the YAML with the real image path and tag
sed -i "s|\${IMAGE_NAME}|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|g" k8s/deployment.yaml
sed -i "s|\${IMAGE_TAG}|sha-${GITHUB_SHA::7}|g" k8s/deployment.yaml

# Apply the changes to the cluster
kubectl apply -f k8s/deployment.yaml
# Wait for the rollout to finish successfully
kubectl rollout status deployment/scribeboard-api
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}
51 changes: 51 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# --- Build Stage: This stage installs ALL dependencies and builds the app ---
FROM node:18-alpine AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy package files first to leverage Docker layer caching for faster builds
COPY package*.json ./

# Install all dependencies (including devDependencies for building/Prisma)
RUN npm ci

# Copy the entire project source code into the builder stage
COPY . .

# Generate the Prisma Client based on the schema (required for DB interactions)
RUN npx prisma generate

# --- Production Stage: This stage creates the final, slim, and secure image ---
FROM node:18-alpine

# Set the working directory for the runtime environment
WORKDIR /app

# Set Node to production mode (optimizes performance and security)
ENV NODE_ENV=production

# Re-copy package files to install ONLY production-critical dependencies
COPY package*.json ./

# Install only production dependencies to reduce image size and attack surface
RUN npm ci --only=production

# Copy ONLY the necessary built assets from the builder stage
COPY --from=builder /app/src ./src
COPY --from=builder /app/prisma ./prisma
# Copy the generated Prisma client binary
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma

# Document that the container listens on port 3000
EXPOSE 3000

# SECURITY: Create a non-root group and user to run the application
# This prevents an attacker from gaining root access to the host if the app is compromised
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Switch to the non-root user
USER appuser

# Start the application using the production start script
CMD ["npm", "start"]
53 changes: 53 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Import core ESLint and specialized security/node/jest plugins
const security = require('eslint-plugin-security');
const node = require('eslint-plugin-node');
const jest = require('eslint-plugin-jest');
const js = require('@eslint/js');

/** @type {import('eslint').Linter.FlatConfig[]} */
module.exports = [
// Use the standard recommended JavaScript rules
js.configs.recommended,
// Use the specialized security rules to detect risky code patterns (e.g., eval, regex)
security.configs.recommended,
{
// Apply these settings to all JavaScript files
files: ['**/*.js'],
plugins: {
security,
node,
jest
},
languageOptions: {
// Use latest ECMAScript features
ecmaVersion: 'latest',
// The project uses CommonJS (require/module.exports)
sourceType: 'commonjs',
// Define global variables to prevent "not defined" errors during linting
globals: {
node: true,
jest: true,
process: 'readonly',
__dirname: 'readonly',
module: 'readonly',
require: 'readonly',
console: 'readonly',
describe: 'readonly',
it: 'readonly',
expect: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly'
}
},
rules: {
// Warn when console.log is left in code (prevent log spam in production)
'no-console': 'warn',
// Allow object[key] access as it is common in this project's logic
'security/detect-object-injection': 'off',
// Ensure we don't use JS features that aren't supported by the Node version
'node/no-unsupported-features/es-syntax': ['error', {
'ignores': ['modules']
}]
}
}
];
33 changes: 33 additions & 0 deletions fuzz/auth.fuzz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Import the target validator to be fuzzed
const { register } = require('../src/validators/auth.validator');

/**
* Jazzer.js Fuzzing Target
* This function will be called thousands of times with random 'data' inputs
*/
module.exports.fuzz = function(data) {
try {
// Convert the raw buffer data from Jazzer into a string
const input = data.toString();

let payload;
try {
// Attempt to parse the random input as JSON (testing JSON parser resilience)
payload = JSON.parse(input);
} catch (e) {
// If not valid JSON, construct a malformed object using the raw input strings
// This tests how the validator handles unexpected character sequences in fields
payload = { email: input, password: input, firstName: input, lastName: input };
}

// Execute the actual validation logic
register.validate(payload);

} catch (e) {
// SECURITY: We only care about process crashes (TypeErrors, RangeErrors, etc.)
// Validation errors (Joi errors) are expected and caught here safely.
if (e instanceof TypeError) {
console.error('Possible Logic Crash Found:', e);
}
}
};
33 changes: 33 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Jest Testing Configuration
*/
module.exports = {
// Use the Node.js environment for testing (API backend)
testEnvironment: 'node',

// Display individual test results during the run
verbose: true,

// Automatically collect code coverage information
collectCoverage: true,

// Directory where Jest should output coverage reports
coverageDirectory: 'coverage',

// Quality Gates: Minimum coverage required for the CI/CD pipeline to pass
coverageThreshold: {
global: {
// Set to 1% as a baseline for the initial PR; increase this as tests are added
branches: 1,
functions: 1,
lines: 1,
statements: 1
}
},

// Pattern to find test files
testMatch: ['**/tests/**/*.test.js'],

// Files to run after the test environment has been established (for global mocks/env vars)
setupFilesAfterEnv: ['./tests/setup.js']
};
Loading