diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d823482..5ff6613 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -9,26 +9,119 @@ on: - '**' env: NODE_VERSION: "20" + OPENAPI_FILE: 'index.yaml' + PORT: '3000' jobs: - node-ci: + lint: runs-on: ubuntu-latest steps: - # 1. Checkout repository - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + - run: npm ci --prefer-offline --no-audit --no-fund + + + - name: Ensure minimal ESLint config + run: | + node -e "const fs=require('fs');const has=['.eslintrc.json','.eslintrc.js','eslint.config.js'].some(f=>fs.existsSync(f));if(!has){fs.writeFileSync('.eslintrc.json',JSON.stringify({root:true,env:{es2021:true,node:true,browser:true},parserOptions:{ecmaVersion:2022,sourceType:'module'},ignorePatterns:['node_modules/','dist/','build/','coverage/'],rules:{curly:'off',eqeqeq:'off','no-undef':'off','no-unused-vars':'off'}},null,2));}" + + - name: Run ESLint (non-blocking) + run: npx -y eslint@8.57.0 . --no-error-on-unmatched-pattern --quiet || true + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + - run: npm ci --prefer-offline --no-audit --no-fund + + + - name: Run fast tests (non-blocking) + shell: bash + run: | + set -e + if node -e "const s=(require('./package.json').scripts)||{};process.exit(s['test:unit']?0:1)"; then + npm run test:unit -- --ci || true + elif node -e "const p=require('./package.json');const hasJest=(p.devDependencies&&p.devDependencies.jest)||(p.dependencies&&p.dependencies.jest);process.exit(hasJest?0:1)"; then + npx jest --ci --passWithNoTests || true + else + npm test --silent || true + fi + + openapi-validate: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 - # 2. Setup Node.js with caching enabled - - name: Setup Node with cache - uses: actions/setup-node@v4 with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: package-lock.json - # 3. Install dependencies (reproducible and cache-friendly) - - name: Install dependencies - run: npm ci + - run: npm ci + + - name: Install swagger-cli + + run: npx -y swagger-cli@4.0.4 --version + + + + - name: Validate OpenAPI (non-blocking) + + run: | + + REPORT=openapi-validate.log + + if [ ! -f "${{ env.OPENAPI_FILE }}" ]; then + + echo "::warning::OpenAPI file '${{ env.OPENAPI_FILE }}' not found at repo root. Skipping validation." | tee "$REPORT" + + exit 0 + + fi + + # Run validation, capture output; do not fail the step + + npx swagger-cli validate "${{ env.OPENAPI_FILE }}" >"$REPORT" 2>&1 || { + + echo "::warning::OpenAPI validation failed. See artifact '$REPORT' for details." + + } + + # Always succeed + + exit 0 + + - name: Upload OpenAPI validation report + + uses: actions/upload-artifact@v4 + + with: + + name: openapi-validate-report + + path: openapi-validate.log + + if-no-files-found: ignore + + + run-security-scan: runs-on: ubuntu-latest diff --git a/middleware/authorizeRoles.js b/middleware/authorizeRoles.js index aa811d2..109832d 100644 --- a/middleware/authorizeRoles.js +++ b/middleware/authorizeRoles.js @@ -1,13 +1,19 @@ /** - * Role-based access control (RBAC) middleware + * Role-based access control (RBAC) middleware with violation logging */ +const { createClient } = require('@supabase/supabase-js'); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY // ✅ still using anon key +); + function authorizeRoles(...allowedRoles) { - return (req, res, next) => { - // Supabase JWTs: "role" is usually "authenticated" or "service_role" - // Custom JWTs: you explicitly set "role" in payload + return async (req, res, next) => { const userRole = req.user?.role || req.user?.user_roles || null; if (!userRole) { + await logViolation(req, userRole, "ROLE_MISSING"); return res.status(403).json({ success: false, error: "Role missing in token", @@ -15,13 +21,11 @@ function authorizeRoles(...allowedRoles) { }); } - // Normalize role value (lowercase string) const roleValue = String(userRole).toLowerCase(); - - // Normalize allowed roles too const normalizedAllowed = allowedRoles.map(r => r.toLowerCase()); if (!normalizedAllowed.includes(roleValue)) { + await logViolation(req, roleValue, "ACCESS_DENIED"); return res.status(403).json({ success: false, error: "Access denied: insufficient role", @@ -29,8 +33,32 @@ function authorizeRoles(...allowedRoles) { }); } + // ✅ If role is allowed, continue next(); }; } + //feature/rbac-extension +async function logViolation(req, role, status) { + const payload = { + user_id: req.user?.userId || "unknown", + email: req.user?.email || "unknown", // ✅ added email + role: role || "unknown", + endpoint: req.originalUrl, + method: req.method, + status + }; + + try { + const { error } = await supabase.from("rbac_violation_logs").insert([payload]); + if (error) { + console.error("❌ Supabase insert error:", error.message); + } else { + console.log("✅ RBAC violation logged:", payload); + } + } catch (err) { + console.error("❌ RBAC log exception:", err.message); + } +} module.exports = authorizeRoles; + diff --git a/package-lock.json b/package-lock.json index 059759b..c33089a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7858,4 +7858,4 @@ } } } -} \ No newline at end of file +}