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
57 changes: 56 additions & 1 deletion .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,61 @@ jobs:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/vulnerablenode
SESSION_SECRET: ci-test-secret-key

sbom-and-scan:
name: SBOM Generation & Vulnerability Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci

# ── SBOM generation ──────────────────────────────────────────────────────
- name: Generate SBOM with Syft (CycloneDX JSON)
uses: anchore/sbom-action@v0
with:
path: "."
format: "cyclonedx-json"
output-file: "sbom-ci.json"
artifact-name: "sbom"

# ── Vulnerability scan: Grype (Anchore) ──────────────────────────────────
- name: Scan dependencies with Grype
uses: anchore/scan-action@v6
id: grype-scan
with:
path: "."
fail-build: true
severity-cutoff: high
output-format: table

# ── Vulnerability scan: Trivy (Aqua Security) ───────────────────────────
- name: Scan filesystem with Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
format: "table"
severity: "CRITICAL,HIGH"
exit-code: "1"
ignore-unfixed: true

# ── npm audit gate ────────────────────────────────────────────────────────
- name: npm audit (HIGH/CRITICAL gate — production deps only)
run: npm run audit:check

# ── Upload evidence artifacts ─────────────────────────────────────────────
- name: Upload vulnerability reports
if: always()
uses: actions/upload-artifact@v4
with:
name: security-reports
path: reports/vulnerability/
retention-days: 30

sonarcloud:
name: SonarCloud Analysis
runs-on: ubuntu-latest
Expand All @@ -68,7 +123,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v4.3.0
with:
name: coverage-report
path: coverage/
Expand Down
25 changes: 25 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
echo "Scanning staged files for secrets..."

# Get list of staged files (exclude deleted files)
STAGED=$(git diff --cached --name-only --diff-filter=d)

if [ -z "$STAGED" ]; then
echo "No staged files to check."
exit 0
fi

# Run secretlint on each staged file
echo "$STAGED" | xargs npx secretlint --no-color

EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
echo ""
echo "ERROR: Potential secrets detected in staged files."
echo " Remove secrets before committing."
echo " Use 'git commit --no-verify' to bypass (not recommended)."
echo ""
exit 1
fi

echo "No secrets detected. Proceeding with commit."
9 changes: 9 additions & 0 deletions .secretlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
coverage/
package-lock.json
sbom.json
reports/
public/
logs/
*.min.js
.github/
7 changes: 7 additions & 0 deletions .secretlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend"
}
]
}
29 changes: 22 additions & 7 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import csrf from 'csurf';
import crypto from 'crypto';
import config from './config.js';
import requestId from './src/interface/http/middleware/requestId.js';
import { apiLimiter, loginLimiter } from './src/interface/http/middleware/rateLimiter.js';
Expand Down Expand Up @@ -65,13 +65,28 @@ app.use(session({
name: 'sessionId'
}));

// CSRF protection
const csrfProtection = csrf({ cookie: false }); // Use session-based CSRF (not cookie)
app.use(csrfProtection);

// Make CSRF token available to all templates
// CSRF protection (synchronizer token pattern, session-based — replaces deprecated csurf)
app.use(function(req, res, next) {
res.locals.csrfToken = req.csrfToken();
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
req.csrfToken = () => req.session.csrfToken;
res.locals.csrfToken = req.session.csrfToken;

const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
if (!safeMethods.includes(req.method)) {
// Unauthenticated requests bypass CSRF — they will be redirected to /login
// by the route auth guard. CSRF attacks require an authenticated session.
if (!req.session.logged) {
return next();
}
const submitted = req.body?._csrf || req.headers['x-csrf-token'];
if (submitted !== req.session.csrfToken) {
const err = new Error('Invalid CSRF token');
err.code = 'EBADCSRFTOKEN';
return next(err);
}
}
next();
});

Expand Down
Loading
Loading