diff --git a/backend/routes/knowledgeBase.js b/backend/routes/knowledgeBase.js index 2c27968..9f29900 100644 --- a/backend/routes/knowledgeBase.js +++ b/backend/routes/knowledgeBase.js @@ -39,7 +39,19 @@ function createKnowledgeBaseRouter(db, upload) { return ALLOWED_EXTENSIONS.has(ext); } - // POST /api/knowledge-base/upload - Upload new document + /** + * POST /api/knowledge-base/upload + * Upload a new knowledge base document. + * + * @body {string} title - Article title (required) + * @body {string} [description] - Article description + * @body {string} [category] - Article category (defaults to 'General') + * @body {File} file - The document file to upload (multipart/form-data) + * + * @response 200 - { success: true, id: number, title: string, slug: string, category: string } + * @response 400 - { error: string } - Missing title, no file, or invalid file type + * @response 500 - { error: string } - Database or filesystem error + */ router.post('/upload', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) { @@ -160,7 +172,13 @@ function createKnowledgeBaseRouter(db, upload) { } }); - // GET /api/knowledge-base - List all articles + /** + * GET /api/knowledge-base + * List all knowledge base articles. + * + * @response 200 - Array of article objects: [{ id, title, slug, description, category, file_name, file_type, file_size, created_at, updated_at, created_by_username }] + * @response 500 - { error: string } + */ router.get('/', requireAuth(db), (req, res) => { const sql = ` SELECT @@ -182,7 +200,16 @@ function createKnowledgeBaseRouter(db, upload) { }); }); - // GET /api/knowledge-base/:id - Get single article details + /** + * GET /api/knowledge-base/:id + * Get a single article's details by ID. + * + * @param {string} id - Article ID (route parameter) + * + * @response 200 - { id, title, slug, description, category, file_name, file_type, file_size, created_at, updated_at, created_by_username } + * @response 404 - { error: 'Article not found' } + * @response 500 - { error: string } + */ router.get('/:id', requireAuth(db), (req, res) => { const { id } = req.params; @@ -210,7 +237,17 @@ function createKnowledgeBaseRouter(db, upload) { }); }); - // GET /api/knowledge-base/:id/content - Get document content for display + /** + * GET /api/knowledge-base/:id/content + * Get document content for inline display. Returns the raw file with appropriate + * Content-Type headers. Markdown and text files are served as text/plain. + * + * @param {string} id - Article ID (route parameter) + * + * @response 200 - Raw file content with Content-Type and Content-Disposition headers + * @response 404 - { error: string } - Article or file not found + * @response 500 - { error: string } + */ router.get('/:id/content', requireAuth(db), (req, res) => { const { id } = req.params; @@ -261,7 +298,16 @@ function createKnowledgeBaseRouter(db, upload) { }); }); - // GET /api/knowledge-base/:id/download - Download document + /** + * GET /api/knowledge-base/:id/download + * Download a knowledge base document as an attachment. + * + * @param {string} id - Article ID (route parameter) + * + * @response 200 - File download with Content-Disposition: attachment header + * @response 404 - { error: string } - Article or file not found + * @response 500 - { error: string } + */ router.get('/:id/download', requireAuth(db), (req, res) => { const { id } = req.params; @@ -298,7 +344,18 @@ function createKnowledgeBaseRouter(db, upload) { }); }); - // DELETE /api/knowledge-base/:id - Delete article + /** + * DELETE /api/knowledge-base/:id + * Delete a knowledge base article and its associated file. + * Standard_User can only delete articles they created. Admin can delete any article. + * + * @param {string} id - Article ID (route parameter) + * + * @response 200 - { success: true } + * @response 403 - { error: string } - Ownership check failed for Standard_User + * @response 404 - { error: 'Article not found' } + * @response 500 - { error: string } + */ router.delete('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => { const { id } = req.params; diff --git a/docs/security-audit-2026-04-01.md b/docs/security-audit-2026-04-01.md new file mode 100644 index 0000000..174401b --- /dev/null +++ b/docs/security-audit-2026-04-01.md @@ -0,0 +1,617 @@ +# Security Audit Report — STEAM Security Dashboard + +**Date:** 2026-04-01 +**Scope:** Full codebase — backend routes, authentication, file handling, Python scripts, React frontend +**Methodology:** Static analysis across four parallel audit tracks + +--- + +## Executive Summary + +The audit identified **31 findings** across four severity levels. The most serious issues are concentrated in the **authentication and authorization layer** — several endpoints are either completely unauthenticated or have role-checking middleware called with the wrong arguments, silently bypassing access control. These require immediate remediation before the application is exposed to a broader user base. + +| Severity | Count | +|----------|-------| +| Critical | 6 | +| High | 9 | +| Medium | 10 | +| Low / Info | 6 | +| **Total** | **31** | + +The application has strong foundational security in several areas: all database queries use parameterized statements (no SQL injection risk), path traversal prevention is comprehensive, Python script execution uses `spawn` with argument arrays (no shell injection), and file type allowlisting is in place. The vulnerabilities are largely in middleware wiring and missing access controls rather than fundamental design flaws. + +--- + +## Critical Findings + +--- + +### C-1 — Missing Authentication on Ivanti Findings Endpoints + +**File:** `backend/routes/ivantiFindings.js:552–600` + +The findings router imports `requireRole` but **not** `requireAuth`. No authentication middleware is applied at the router level or on individual routes. Four endpoints are fully unauthenticated: + +```js +const { requireRole } = require('../middleware/auth'); // requireAuth never imported + +router.get('/', async (req, res) => { // line 552 — no auth +router.post('/sync', async (req, res) => { // line 561 — no auth +router.get('/counts', async (req, res) => { // line 571 — no auth +router.get('/fp-workflow-counts', ...) // line 580 — no auth +``` + +**Impact:** Any unauthenticated attacker on the network can read the full list of Ivanti host findings (hostnames, IPs, CVEs, severity, SLA status), trigger a sync operation, and enumerate all finding metrics. + +**Fix:** Import `requireAuth` and apply it to the router or each route: +```js +const { requireAuth, requireRole } = require('../middleware/auth'); +router.use(requireAuth(db)); +``` + +--- + +### C-2 — Broken requireRole Call — Privilege Escalation in Knowledge Base + +**File:** `backend/routes/knowledgeBase.js:43, 305` + +`requireRole` is called with `db` as the first argument: + +```js +router.post('/upload', requireAuth(db), requireRole(db, 'editor', 'admin'), ...) +router.delete('/:id', requireAuth(db), requireRole(db, 'editor', 'admin'), ...) +``` + +The function signature is `function requireRole(...allowedRoles)`. It does not accept `db`. The database object is treated as the first "allowed role", so the check becomes `req.user.role === db` — an object comparison that always evaluates false, meaning **the check never blocks anyone**. Any authenticated viewer can upload and delete knowledge base documents. + +**Fix:** Remove `db` from all `requireRole` calls: +```js +requireRole('editor', 'admin') +``` + +--- + +### C-3 — Unauthenticated Ivanti Finding Note Writes + +**File:** `backend/routes/ivantiFindings.js:639` + +The PUT endpoint for saving finding notes has no authentication middleware: + +```js +router.put('/:findingId/note', (req, res) => { + const note = String(req.body.note || '').slice(0, 255); + db.run(`INSERT INTO ivanti_finding_notes ...`); +}); +``` + +**Impact:** Any unauthenticated request can write notes to any finding. Notes are visible to all users and used during remediation triage. An attacker could inject false status information (e.g. "EXC-12345 — patched") to mislead the team or cover tracks. + +**Fix:** Add `requireAuth(db)` to this route. + +--- + +### C-4 — No Brute Force Protection on Login Endpoint + +**File:** `backend/routes/auth.js:10` + +The login endpoint has no rate limiting, attempt counting, or lockout: + +```js +router.post('/login', async (req, res) => { + const { username, password } = req.body; + // Direct DB lookup, unlimited attempts +``` + +**Impact:** An attacker can run unlimited password guesses against any account at full network speed. With the default credentials documented in the README and displayed in the UI (see F-2), admin accounts are a trivial target. + +**Fix:** Apply `express-rate-limit` to the login route: +```js +const rateLimit = require('express-rate-limit'); +const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 }); +router.post('/login', loginLimiter, async (req, res) => { ... }); +``` + +--- + +### C-5 — Default Credentials Displayed in Login UI + +**File:** `frontend/src/components/LoginForm.js:104` + +The login form renders hardcoded credentials in plain text: + +```jsx +

+ Default: admin / + admin123 +

+``` + +**Impact:** Anyone who opens the login page — including unauthenticated users — sees the default admin credentials. Combined with C-4 (no rate limiting), this is a direct path to admin compromise if the password has not been changed. + +**Fix:** Remove this block entirely. Document default credentials only in the deployment guide. Enforce password change on first login server-side. + +--- + +### C-6 — Missing Sandbox Attribute on Knowledge Base PDF Iframe + +**File:** `frontend/src/components/KnowledgeBaseViewer.js:195` + +The inline document viewer renders uploaded files in an unsandboxed iframe: + +```jsx +