Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@ BANXA_WEBHOOK_SECRET=
# alertmanager/secrets/*, not from this file; see alertmanager/README.md.
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin

# Security scanning - see docs/SECURITY_SCANNING.md
# Shared secret the CI security-scan workflow presents via the x-scan-token
# header when POSTing scan results to POST /security/scans/ingest.
SECURITY_SCAN_TOKEN=
# Optional: webhook (e.g. a Slack incoming webhook URL) notified on every
# CRITICAL vulnerability ingested.
SECURITY_ALERT_WEBHOOK_URL=
41 changes: 41 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/backend'
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
labels:
- 'dependencies'
- 'security'
groups:
minor-and-patch:
update-types: ['minor', 'patch']

- package-ecosystem: 'npm'
directory: '/frontend'
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
labels:
- 'dependencies'
- 'security'
groups:
minor-and-patch:
update-types: ['minor', 'patch']

- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
labels:
- 'dependencies'
- 'security'

- package-ecosystem: 'docker'
directory: '/backend'
schedule:
interval: 'weekly'
labels:
- 'dependencies'
- 'security'
223 changes: 223 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
name: Security Scanning

on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * *' # Daily scan
workflow_dispatch: {}

permissions:
contents: read
security-events: write

env:
# When set (as repo/org secrets), scan results are also pushed into the
# in-app vulnerability dashboard at $SECURITY_API_URL/security/scans/ingest.
# Both must be present or ingestion is skipped - see docs/security-scanning.md.
SECURITY_API_URL: ${{ secrets.SECURITY_API_URL }}
SECURITY_SCAN_TOKEN: ${{ secrets.SECURITY_SCAN_TOKEN }}

jobs:
sast:
name: SAST (Semgrep)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/typescript
p/nodejsscan
.semgrep/security-rules.yaml
generateSarif: '1'

- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
category: semgrep

- name: Report to security dashboard
if: always() && env.SECURITY_API_URL != '' && env.SECURITY_SCAN_TOKEN != ''
run: |
if [ -f semgrep.sarif ]; then
jq -n --slurpfile payload semgrep.sarif '{format: "SARIF", type: "CODE", payload: $payload[0]}' | \
curl -sf -X POST "$SECURITY_API_URL/security/scans/ingest" \
-H "Content-Type: application/json" \
-H "x-scan-token: $SECURITY_SCAN_TOKEN" \
-d @-
fi

dependency-scan:
name: Dependency Scan (npm audit)
runs-on: ubuntu-latest
strategy:
matrix:
workspace: [backend, frontend]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '18'

- name: Run npm audit
working-directory: ${{ matrix.workspace }}
run: npm audit --json > npm-audit.json || true

- name: Fail on high/critical vulnerabilities
working-directory: ${{ matrix.workspace }}
run: |
npm audit --audit-level=high

- name: Report to security dashboard
if: always() && env.SECURITY_API_URL != '' && env.SECURITY_SCAN_TOKEN != ''
working-directory: ${{ matrix.workspace }}
run: |
if [ -s npm-audit.json ]; then
jq -n --slurpfile payload npm-audit.json '{format: "NPM_AUDIT", payload: $payload[0]}' | \
curl -sf -X POST "$SECURITY_API_URL/security/scans/ingest" \
-H "Content-Type: application/json" \
-H "x-scan-token: $SECURITY_SCAN_TOKEN" \
-d @-
fi

# Dependabot itself runs from .github/dependabot.yml, not as a CI job -
# it opens PRs on its own schedule, see that file for config.

secret-scan:
name: Secret Scan (Gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run Gitleaks
run: |
docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:latest \
detect --source /repo --report-format json --report-path /repo/gitleaks-report.json --exit-code 1 || \
(echo "GITLEAKS_FAILED=1" >> "$GITHUB_ENV")

- name: Report to security dashboard
if: always() && env.SECURITY_API_URL != '' && env.SECURITY_SCAN_TOKEN != ''
run: |
if [ -s gitleaks-report.json ]; then
jq -n --slurpfile payload gitleaks-report.json '{format: "GITLEAKS", payload: $payload[0]}' | \
curl -sf -X POST "$SECURITY_API_URL/security/scans/ingest" \
-H "Content-Type: application/json" \
-H "x-scan-token: $SECURITY_SCAN_TOKEN" \
-d @-
fi

- name: Fail if secrets were found
if: env.GITLEAKS_FAILED == '1'
run: exit 1

container-scan:
name: Container Scan (Trivy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build backend image
run: docker build -t lumina/payment-service:${{ github.sha }} ./backend

- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: lumina/payment-service:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'

- name: Upload Trivy results
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
category: trivy

- name: Report to security dashboard
if: always() && env.SECURITY_API_URL != '' && env.SECURITY_SCAN_TOKEN != ''
run: |
if [ -f trivy-results.sarif ]; then
jq -n --slurpfile payload trivy-results.sarif '{format: "SARIF", type: "CONTAINER", payload: $payload[0]}' | \
curl -sf -X POST "$SECURITY_API_URL/security/scans/ingest" \
-H "Content-Type: application/json" \
-H "x-scan-token: $SECURITY_SCAN_TOKEN" \
-d @-
fi

infrastructure-scan:
name: Infrastructure Scan (Checkov)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: .
framework: dockerfile,docker_compose
output_format: sarif
output_file_path: checkov-results.sarif
soft_fail: true

- name: Upload Checkov results
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: checkov-results.sarif
category: checkov

dast:
name: DAST (OWASP ZAP baseline)
runs-on: ubuntu-latest
needs: [sast, dependency-scan, container-scan, secret-scan]
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v4

- name: Start backend stack
run: docker compose up -d --build postgres redis backend

- name: Wait for backend to be healthy
run: |
for i in $(seq 1 30); do
if curl -sf http://localhost:4000/health >/dev/null 2>&1; then exit 0; fi
sleep 5
done
echo "Backend did not become healthy in time"
docker compose logs backend
exit 1

- name: Run OWASP ZAP baseline scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'http://localhost:4000'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'

- name: Report to security dashboard
if: always() && env.SECURITY_API_URL != '' && env.SECURITY_SCAN_TOKEN != ''
run: |
if [ -f report_json.json ]; then
jq -n --slurpfile payload report_json.json '{format: "SARIF", type: "DAST", payload: $payload[0]}' | \
curl -sf -X POST "$SECURITY_API_URL/security/scans/ingest" \
-H "Content-Type: application/json" \
-H "x-scan-token: $SECURITY_SCAN_TOKEN" \
-d @- || true
fi

- name: Tear down backend stack
if: always()
run: docker compose down -v
12 changes: 12 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
title = "Lumina gitleaks config"

[extend]
useDefault = true

[allowlist]
description = "Test fixtures and examples, not real credentials"
paths = [
'''\.env\.example$''',
'''.*\.spec\.ts$''',
'''.*\.test\.ts$''',
]
66 changes: 66 additions & 0 deletions .semgrep/security-rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
rules:
- id: no-hardcoded-secrets
languages: [typescript, javascript]
severity: ERROR
message: >-
Hardcoded credential-like literal detected. Load secrets from
process.env / a secrets manager instead of committing them.
metadata:
category: security
cwe: 'CWE-798: Use of Hard-coded Credentials'
patterns:
- pattern-either:
- pattern: const $VAR = "..."
- pattern: let $VAR = "..."
- metavariable-regex:
metavariable: $VAR
regex: (?i)^(api[_-]?key|secret|password|passwd|token|private[_-]?key)$
- pattern-not: const $VAR = process.env.$ENV

- id: sql-injection-string-concat
languages: [typescript, javascript]
severity: ERROR
message: >-
Building a SQL query with string concatenation/interpolation of a
variable is a SQL injection risk. Use parameterized queries
(TypeORM query builder / prepared statement params) instead.
metadata:
category: security
cwe: 'CWE-89: SQL Injection'
patterns:
- pattern-either:
- pattern: $QUERY = $BASE + $INPUT
- pattern: $QUERY = `...${$INPUT}...`
- pattern-inside: |
$QUERY = ...
...
$DB.query($QUERY, ...)

- id: weak-crypto-hash
languages: [typescript, javascript]
severity: WARNING
message: >-
MD5/SHA1 are cryptographically broken for security-sensitive use
(password hashing, signatures, integrity checks). Use SHA-256/SHA-512
or a purpose-built KDF (bcrypt/argon2) instead.
metadata:
category: security
cwe: 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
patterns:
- pattern-either:
- pattern: crypto.createHash("md5")
- pattern: crypto.createHash("sha1")

- id: disabled-tls-verification
languages: [typescript, javascript]
severity: ERROR
message: >-
TLS certificate verification is disabled. This allows
man-in-the-middle attacks against payment/webhook traffic.
metadata:
category: security
cwe: 'CWE-295: Improper Certificate Validation'
patterns:
- pattern-either:
- pattern: '$AXIOS.create({..., rejectUnauthorized: false, ...})'
- pattern: process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
8 changes: 8 additions & 0 deletions .zap/rules.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# OWASP ZAP baseline scan rule overrides.
# Format: <rule id> <threshold: IGNORE|WARN|FAIL> <url regex (optional)>
# Full rule list: https://www.zaproxy.org/docs/alerts/
#
# The backend is a JSON API with no session cookies issued at these routes,
# so cookie-hardening alerts are noise here - revisit if that changes.
10096 IGNORE # Timestamp disclosure - many API responses legitimately include timestamps
10021 IGNORE # X-Content-Type-Options header missing on non-HTML JSON responses
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MetricsModule } from './common/metrics/metrics.module';
import { DistributedLedgerModule } from './distributed-ledger/distributed-ledger.module';
import { ZKPModule } from './zkp/zkp.module';
import { FraudDetectionModule } from './fraud-detection/fraud-detection.module';
import { SecurityModule } from './security/security.module';
import { MetricsService } from './common/metrics/metrics.service';
import { TypeOrmMetricsLogger } from './common/metrics/typeorm-metrics.logger';
import { DbPoolMetricsService } from './common/metrics/db-pool-metrics.service';
Expand Down Expand Up @@ -51,6 +52,7 @@ import { DbPoolMetricsService } from './common/metrics/db-pool-metrics.service';
DistributedLedgerModule,
ZKPModule,
FraudDetectionModule,
SecurityModule,
PaymentModule,
ApiGatewayModule,
BlockchainListenerModule,
Expand Down
Loading