chore: reorganize docs, document migrations, gitignore operational files for v1.0.0 release
This commit is contained in:
617
docs/security/security-audit-2026-04-01.md
Normal file
617
docs/security/security-audit-2026-04-01.md
Normal file
@@ -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
|
||||
<p className="text-sm text-gray-500 text-center font-mono">
|
||||
Default: <span className="text-intel-accent">admin</span> /
|
||||
<span className="text-intel-accent">admin123</span>
|
||||
</p>
|
||||
```
|
||||
|
||||
**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
|
||||
<iframe
|
||||
src={`${API_BASE}/knowledge-base/${article.id}/content`}
|
||||
title={article.title}
|
||||
className="w-full h-full rounded"
|
||||
>
|
||||
```
|
||||
|
||||
**Impact:** A malicious PDF or HTML file uploaded by an editor could execute JavaScript within the application's origin, accessing `localStorage`, `sessionStorage`, and DOM of the parent page. An attacker with editor access could upload a file that steals session data from any user who views it.
|
||||
|
||||
**Fix:** Add a restrictive `sandbox` attribute:
|
||||
```jsx
|
||||
<iframe
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
src={...}
|
||||
title={article.title}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## High Findings
|
||||
|
||||
---
|
||||
|
||||
### H-1 — /cleanup-sessions Missing Role Check
|
||||
|
||||
**File:** `backend/routes/auth.js:223`
|
||||
|
||||
The comment says "admin only" but the endpoint only checks for any valid session:
|
||||
|
||||
```js
|
||||
router.post('/cleanup-sessions', async (req, res) => {
|
||||
const sessionId = req.cookies?.session_id;
|
||||
if (!sessionId) return res.status(401).json({ error: '...' });
|
||||
// No role check
|
||||
```
|
||||
|
||||
**Fix:** Apply `requireAuth(db)` and `requireRole('admin')`.
|
||||
|
||||
---
|
||||
|
||||
### H-2 — Hardcoded Fallback SESSION_SECRET
|
||||
|
||||
**File:** `backend/server.js:31`
|
||||
|
||||
```js
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET || 'default-secret-change-me';
|
||||
```
|
||||
|
||||
If the `.env` file is missing or the variable is unset, all sessions are signed with a publicly known string. An attacker who knows the secret can forge valid session cookies.
|
||||
|
||||
**Fix:** Fail hard on startup if the secret is not set:
|
||||
```js
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET;
|
||||
if (!SESSION_SECRET) throw new Error('SESSION_SECRET environment variable must be set');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H-3 — Audit Log Parameter Mismatch — Silent Audit Trail Gaps
|
||||
|
||||
**Files:** `backend/routes/archerTickets.js:89–95, 172, 206` and `backend/routes/knowledgeBase.js:235–244, 287–296`
|
||||
|
||||
The `logAudit` helper expects an object with `entityType` and `entityId`. These callers use the wrong keys (`targetType`, `targetId`) or pass positional arguments instead of an object:
|
||||
|
||||
```js
|
||||
// archerTickets.js — wrong keys
|
||||
logAudit(db, { ..., targetType: 'archer_ticket', targetId: this.lastID, ... });
|
||||
|
||||
// knowledgeBase.js — positional (wrong pattern)
|
||||
logAudit(db, req.user.id, req.user.username, 'VIEW_KB_ARTICLE', 'knowledge_base', id, ...);
|
||||
```
|
||||
|
||||
**Impact:** All Archer ticket and Knowledge Base operations produce audit log rows with `NULL` entity type and entity ID. Security investigations and compliance reviews will show these actions occurred but not what was affected.
|
||||
|
||||
**Fix:** Align all callers to the object format expected by `auditLog.js`:
|
||||
```js
|
||||
logAudit(db, { userId, username, action, entityType, entityId, details, ipAddress });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H-4 — Viewers Can Write Compliance Notes
|
||||
|
||||
**Files:** `backend/routes/compliance.js:522` (also flagged by file-upload audit)
|
||||
|
||||
The POST /notes endpoint is protected by authentication but not by role:
|
||||
|
||||
```js
|
||||
router.post('/notes', async (req, res) => { // no requireRole()
|
||||
```
|
||||
|
||||
**Impact:** Any viewer can add notes to any compliance item. Notes surface in the detail panel and influence remediation decisions. False notes cannot be deleted via the API.
|
||||
|
||||
**Fix:** `requireRole('editor', 'admin')` on this route.
|
||||
|
||||
---
|
||||
|
||||
### H-5 — Sync Endpoints Accessible to All Authenticated Users
|
||||
|
||||
**Files:** `backend/routes/ivantiFindings.js:561`, `backend/routes/ivantiWorkflows.js:262`
|
||||
|
||||
POST /sync on both routers requires only authentication, not editor/admin role. Any viewer can trigger expensive Ivanti API calls repeatedly.
|
||||
|
||||
**Impact:** Viewer-role users can cause repeated large API fetches, potentially hitting Ivanti rate limits and blocking legitimate syncs for the team.
|
||||
|
||||
**Fix:** Add `requireRole('editor', 'admin')` to both POST /sync routes.
|
||||
|
||||
---
|
||||
|
||||
### H-6 — HTTP Header Injection via Unsanitized Filename in Content-Disposition
|
||||
|
||||
**File:** `backend/routes/knowledgeBase.js:258, 299`
|
||||
|
||||
The original uploaded filename (user-controlled) is written directly into the `Content-Disposition` response header:
|
||||
|
||||
```js
|
||||
res.setHeader('Content-Disposition', `inline; filename="${row.file_name}"`);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${row.file_name}"`);
|
||||
```
|
||||
|
||||
`row.file_name` stores `uploadedFile.originalname` which is not sanitized for use in HTTP headers. A filename containing `"\r\n` characters can split the response and inject arbitrary headers.
|
||||
|
||||
**Fix:**
|
||||
```js
|
||||
const safeFilename = row.file_name.replace(/["\r\n\\]/g, '');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${safeFilename}"`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H-7 — Race Condition in Knowledge Base File Upload
|
||||
|
||||
**File:** `backend/routes/knowledgeBase.js:91–155`
|
||||
|
||||
The file is moved to its permanent location (line 93) before the database record is created (line 114). If the DB insert fails, the file is orphaned on disk. Two concurrent uploads with the same slug can also bypass the uniqueness check due to the async gap between the slug check query and the insert.
|
||||
|
||||
**Fix:** Keep the file in the temp directory until the DB insert succeeds, then move it:
|
||||
```js
|
||||
db.run(insertSql, [...], function(err) {
|
||||
if (err) { fs.unlinkSync(uploadedFile.path); return res.status(500)...; }
|
||||
fs.renameSync(uploadedFile.path, filePath);
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H-8 — Hardcoded Default Admin Password in setup.js
|
||||
|
||||
**File:** `backend/setup.js:175`
|
||||
|
||||
```js
|
||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
||||
```
|
||||
|
||||
If `setup.js` is re-run on an existing deployment (e.g. during a restore), the admin password resets to a known value. The password is also documented in the README and displayed in the login UI (C-5).
|
||||
|
||||
**Fix:** Generate a random password on first run and print it once to stdout, or require it as a CLI argument. Never hardcode credentials in source.
|
||||
|
||||
---
|
||||
|
||||
### H-9 — ReactMarkdown Renders HTML Without Sanitization
|
||||
|
||||
**File:** `frontend/src/components/KnowledgeBaseViewer.js:169–171`
|
||||
|
||||
```jsx
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
```
|
||||
|
||||
`ReactMarkdown` by default allows raw HTML in markdown (via `rehype-raw`). A knowledge base article containing `<img src=x onerror="...">` or `<script>` tags would execute JavaScript in the viewer's browser.
|
||||
|
||||
**Fix:** Add `rehype-sanitize`:
|
||||
```jsx
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>{content}</ReactMarkdown>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Medium Findings
|
||||
|
||||
---
|
||||
|
||||
### M-1 — No CSRF Token Protection on State-Changing Requests
|
||||
|
||||
**Files:** All POST / PUT / DELETE routes
|
||||
|
||||
Cookies are `SameSite: lax` which provides partial protection, but `lax` still allows top-level cross-site navigations to carry cookies. No CSRF token is validated server-side. Combined with the permissive CORS configuration, cross-site request forgery is possible against editors and admins.
|
||||
|
||||
**Fix:** Either upgrade session cookie to `SameSite: strict`, or implement a CSRF token (double-submit cookie pattern or `csurf` middleware).
|
||||
|
||||
---
|
||||
|
||||
### M-2 — CORS Allows Credentials with Explicit Origin List
|
||||
|
||||
**File:** `backend/server.js:111–114`
|
||||
|
||||
```js
|
||||
app.use(cors({ origin: CORS_ORIGINS, credentials: true }));
|
||||
```
|
||||
|
||||
`credentials: true` with explicit origins means any subdomain compromise or DNS hijacking of a listed origin could allow cross-origin authenticated requests. This is the correct pattern for this use case, but worth hardening.
|
||||
|
||||
**Fix:** Ensure `CORS_ORIGINS` is reviewed whenever the deployment changes. Consider `SameSite: strict` on cookies to reduce reliance on CORS for CSRF protection.
|
||||
|
||||
---
|
||||
|
||||
### M-3 — No Rate Limiting on NVD API Proxy
|
||||
|
||||
**File:** `backend/routes/nvdLookup.js:13`
|
||||
|
||||
Any authenticated user can trigger NVD API calls in rapid succession. NVD enforces a 5 req/30s unauthenticated limit, which can be exhausted by a single user making 5 lookups.
|
||||
|
||||
**Fix:** Add a server-side 1-hour cache keyed by CVE ID to avoid repeated external lookups, plus a per-user rate limit.
|
||||
|
||||
---
|
||||
|
||||
### M-4 — Admin Self-Demotion Check Uses Loose Equality
|
||||
|
||||
**File:** `backend/routes/users.js:118`
|
||||
|
||||
```js
|
||||
if (userId == req.user.id && role && role !== 'admin') {
|
||||
```
|
||||
|
||||
Using `==` allows type coercion. If `userId` is passed as a different type than `req.user.id`, the comparison may not match correctly.
|
||||
|
||||
**Fix:** `String(userId) === String(req.user.id)`.
|
||||
|
||||
---
|
||||
|
||||
### M-5 — Missing Hostname Format Validation
|
||||
|
||||
**File:** `backend/routes/compliance.js:451`
|
||||
|
||||
The hostname route parameter is used in SQL queries and responses. Only length is checked (>300). No format validation rejects characters outside a valid hostname range.
|
||||
|
||||
**Fix:**
|
||||
```js
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(hostname)) {
|
||||
return res.status(400).json({ error: 'Invalid hostname format' });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### M-6 — Vendor Field Validated Before Trim
|
||||
|
||||
**File:** `backend/routes/ivantiTodoQueue.js:8, 56`
|
||||
|
||||
Vendor length is validated before `.trim()` is called. A string of 200 spaces passes validation but becomes an empty string after trimming, which then passes without a vendor value for FP/Archer items that require one.
|
||||
|
||||
**Fix:** Trim first, then validate length and presence.
|
||||
|
||||
---
|
||||
|
||||
### M-7 — Unsanitized Original Filename Stored in Compliance Temp JSON
|
||||
|
||||
**File:** `backend/routes/compliance.js:262`
|
||||
|
||||
```js
|
||||
filename: req.file.originalname, // user-controlled, unsanitized
|
||||
```
|
||||
|
||||
The original filename is stored in the temp JSON and later echoed back to the frontend. Special characters could cause log injection or unexpected display issues.
|
||||
|
||||
**Fix:** `filename: sanitizePathSegment(req.file.originalname)`.
|
||||
|
||||
---
|
||||
|
||||
### M-8 — Hardcoded Frontend Origin in CSP Header
|
||||
|
||||
**File:** `backend/routes/knowledgeBase.js:261`
|
||||
|
||||
```js
|
||||
res.setHeader('Content-Security-Policy',
|
||||
"frame-ancestors 'self' http://71.85.90.9:3000 http://localhost:3000");
|
||||
```
|
||||
|
||||
IP address is hardcoded. If the deployment IP changes, the CSP header will block inline document viewing without an obvious error and require a code change.
|
||||
|
||||
**Fix:** Use `CORS_ORIGINS` from the environment variable.
|
||||
|
||||
---
|
||||
|
||||
### M-9 — Sensitive API Error Messages Forwarded to UI
|
||||
|
||||
**Files:** `frontend/src/App.js:801, 816, 847, 886`
|
||||
|
||||
```js
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
```
|
||||
|
||||
Raw API error messages are displayed in browser alerts. If the backend leaks stack traces or query information in error responses, this information reaches the user directly.
|
||||
|
||||
**Fix:** Show generic user-facing messages; log details to the console in development only.
|
||||
|
||||
---
|
||||
|
||||
### M-10 — User-Supplied Data in window.confirm Dialogs
|
||||
|
||||
**File:** `frontend/src/App.js:806, 891`
|
||||
|
||||
```js
|
||||
if (!window.confirm(`Delete ticket ${ticket.ticket_key}?`)) return;
|
||||
```
|
||||
|
||||
A ticket with a crafted `ticket_key` value (e.g. containing newlines or misleading text) could produce a deceptive confirmation dialog used to social-engineer users.
|
||||
|
||||
**Fix:** Use a React modal component with escaped, controlled text instead of `window.confirm`.
|
||||
|
||||
---
|
||||
|
||||
## Low / Info Findings
|
||||
|
||||
---
|
||||
|
||||
### L-1 — Silent ROLLBACK on Compliance Transaction Failure
|
||||
|
||||
**File:** `backend/routes/compliance.js:167`
|
||||
|
||||
```js
|
||||
await dbRun(db, 'ROLLBACK').catch(() => {});
|
||||
```
|
||||
|
||||
If the rollback itself fails, the error is swallowed entirely. A failed rollback leaves an open transaction that can cause subsequent operations to block.
|
||||
|
||||
**Fix:** Log rollback failures even if execution continues.
|
||||
|
||||
---
|
||||
|
||||
### L-2 — Fire-and-Forget Audit Logging
|
||||
|
||||
**File:** `backend/helpers/auditLog.js:9`
|
||||
|
||||
Audit log writes fail silently. If the database is under load or unavailable, audit records are dropped with no alert.
|
||||
|
||||
**Fix:** Log audit write failures to stderr so they surface in server logs.
|
||||
|
||||
---
|
||||
|
||||
### L-3 — Async Temp File Cleanup With No Error Handling
|
||||
|
||||
**File:** `backend/routes/compliance.js:239, 247, 266, 281, 322`
|
||||
|
||||
```js
|
||||
fs.unlink(req.file.path, () => {});
|
||||
```
|
||||
|
||||
Cleanup failures accumulate silently, potentially causing disk exhaustion over time.
|
||||
|
||||
**Fix:** Log errors on unlink failure (excluding ENOENT which is expected).
|
||||
|
||||
---
|
||||
|
||||
### L-4 — IVANTI_SKIP_TLS Disables Certificate Validation
|
||||
|
||||
**File:** `backend/routes/ivantiFindings.js:385`
|
||||
|
||||
`IVANTI_SKIP_TLS=true` disables TLS verification for all Ivanti API calls, enabling man-in-the-middle attacks against the sync. It is controlled purely by environment variable with no warning.
|
||||
|
||||
**Fix:** Log a prominent warning on startup when this flag is active, and ensure it is never set in production.
|
||||
|
||||
---
|
||||
|
||||
### L-5 — console.error in Production Frontend Code
|
||||
|
||||
**Files:** `frontend/src/contexts/AuthContext.js:26`, `KnowledgeBaseViewer.js:31, 56`
|
||||
|
||||
Full error objects are logged to the browser console in production builds. In a monitored environment, these could expose internal details to anyone with DevTools open.
|
||||
|
||||
**Fix:** Guard with `if (process.env.NODE_ENV === 'development')` or use a structured logging library.
|
||||
|
||||
---
|
||||
|
||||
### L-6 — localStorage Column Config Lacks Structural Validation
|
||||
|
||||
**File:** `frontend/src/components/pages/ReportingPage.js:51–68`
|
||||
|
||||
Column order/visibility is loaded from `localStorage` and merged with defaults. If the stored data is tampered with (via XSS or DevTools), the parsed structure is used with only partial validation.
|
||||
|
||||
**Fix:** Validate each loaded item against the known `COLUMN_DEFS` whitelist before use (a `hasOwnProperty` check is already present; ensure it runs on every item before the merge).
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| ID | Severity | Title | File |
|
||||
|----|----------|-------|------|
|
||||
| C-1 | Critical | Missing auth on Ivanti findings endpoints | ivantiFindings.js:552 |
|
||||
| C-2 | Critical | requireRole(db) call bypasses role check in KB routes | knowledgeBase.js:43,305 |
|
||||
| C-3 | Critical | Unauthenticated finding note writes | ivantiFindings.js:639 |
|
||||
| C-4 | Critical | No brute force protection on login | auth.js:10 |
|
||||
| C-5 | Critical | Default credentials displayed in login UI | LoginForm.js:104 |
|
||||
| C-6 | Critical | Missing sandbox on PDF/document iframe | KnowledgeBaseViewer.js:195 |
|
||||
| H-1 | High | /cleanup-sessions missing role check | auth.js:223 |
|
||||
| H-2 | High | Hardcoded fallback SESSION_SECRET | server.js:31 |
|
||||
| H-3 | High | Audit log parameter mismatch — silent trail gaps | archerTickets.js, knowledgeBase.js |
|
||||
| H-4 | High | Viewers can write compliance notes | compliance.js:522 |
|
||||
| H-5 | High | Sync endpoints accessible to all authenticated users | ivantiFindings.js:561, ivantiWorkflows.js:262 |
|
||||
| H-6 | High | HTTP header injection via Content-Disposition filename | knowledgeBase.js:258,299 |
|
||||
| H-7 | High | Race condition in KB file upload | knowledgeBase.js:91 |
|
||||
| H-8 | High | Hardcoded default admin password in setup.js | setup.js:175 |
|
||||
| H-9 | High | ReactMarkdown renders HTML without sanitization | KnowledgeBaseViewer.js:169 |
|
||||
| M-1 | Medium | No CSRF token protection | All state-changing routes |
|
||||
| M-2 | Medium | CORS credentials with explicit origin list | server.js:111 |
|
||||
| M-3 | Medium | No rate limiting on NVD API proxy | nvdLookup.js:13 |
|
||||
| M-4 | Medium | Admin self-demotion check uses loose equality | users.js:118 |
|
||||
| M-5 | Medium | Missing hostname format validation | compliance.js:451 |
|
||||
| M-6 | Medium | Vendor field validated before trim | ivantiTodoQueue.js:8,56 |
|
||||
| M-7 | Medium | Unsanitized original filename in temp JSON | compliance.js:262 |
|
||||
| M-8 | Medium | Hardcoded frontend IP in CSP header | knowledgeBase.js:261 |
|
||||
| M-9 | Medium | API error messages forwarded to UI | App.js:801,816,847,886 |
|
||||
| M-10 | Medium | User data in window.confirm dialogs | App.js:806,891 |
|
||||
| L-1 | Low | Silent ROLLBACK on transaction failure | compliance.js:167 |
|
||||
| L-2 | Low | Fire-and-forget audit logging | auditLog.js:9 |
|
||||
| L-3 | Low | Async temp file cleanup with no error handling | compliance.js:239+ |
|
||||
| L-4 | Low | IVANTI_SKIP_TLS with no startup warning | ivantiFindings.js:385 |
|
||||
| L-5 | Low | console.error exposed in production frontend | AuthContext.js, KnowledgeBaseViewer.js |
|
||||
| L-6 | Low | localStorage column config lacks structural validation | ReportingPage.js:51 |
|
||||
|
||||
---
|
||||
|
||||
## Remediation Priority
|
||||
|
||||
### Immediate — fix before adding users
|
||||
|
||||
1. **C-1** — Add `requireAuth` import and router-level middleware to `ivantiFindings.js`
|
||||
2. **C-2** — Remove `db` from all `requireRole(db, ...)` calls in `knowledgeBase.js`
|
||||
3. **C-3** — Add `requireAuth(db)` to the finding note PUT route
|
||||
4. **C-4** — Add `express-rate-limit` to the login route (20 attempts / 15 min)
|
||||
5. **C-5** — Remove default credentials from `LoginForm.js`
|
||||
6. **H-2** — Hard-fail on startup if `SESSION_SECRET` is not set in env
|
||||
|
||||
### Short-term — next maintenance window
|
||||
|
||||
7. **C-6** — Add `sandbox` attribute to the KB iframe
|
||||
8. **H-3** — Fix `logAudit` call signatures in `archerTickets.js` and `knowledgeBase.js`
|
||||
9. **H-4** — Add `requireRole('editor', 'admin')` to POST /compliance/notes
|
||||
10. **H-5** — Add `requireRole('editor', 'admin')` to both POST /sync routes
|
||||
11. **H-6** — Sanitize filename for `Content-Disposition` header
|
||||
12. **H-7** — Move file after DB insert succeeds in KB upload
|
||||
13. **H-8** — Remove hardcoded password from `setup.js`; generate random on first run
|
||||
14. **H-9** — Add `rehype-sanitize` to `ReactMarkdown` usage
|
||||
|
||||
### Medium-term
|
||||
|
||||
15. **M-1** — Implement CSRF token or upgrade cookie to `SameSite: strict`
|
||||
16. **M-3** — Add server-side CVE lookup cache
|
||||
17. **M-5** — Add hostname format regex validation
|
||||
18. **M-8** — Pull frontend origin from `CORS_ORIGINS` env var for CSP header
|
||||
19. **M-9** — Replace `alert(err.message)` with user-friendly error messages
|
||||
20. Remaining medium and low findings
|
||||
|
||||
---
|
||||
|
||||
## Positive Security Observations
|
||||
|
||||
The following were explicitly verified as secure and should be preserved:
|
||||
|
||||
- **SQL injection prevention** — all queries use SQLite3 parameterized statements throughout
|
||||
- **Path traversal prevention** — `sanitizePathSegment()` and `isPathWithinUploads()` are comprehensive and consistently applied
|
||||
- **Python script execution** — `spawn('python3', [SCRIPT, filePath])` passes arguments as an array, not a shell string — no command injection possible
|
||||
- **Python scripts** — no `eval()`, `exec()`, `pickle.load()`, or shell calls in any script
|
||||
- **File size enforcement** — 10 MB limit applied via multer before route handlers execute
|
||||
- **File type allowlisting** — extension + MIME prefix validation applied at upload
|
||||
- **Static file serving** — `express.static` with `{ dotfiles: 'deny', index: false }` prevents directory listing
|
||||
- **Temp file path validation** — `isSafeTempPath()` enforces `.json` extension on compliance temp files
|
||||
- **Password hashing** — bcrypt with cost factor 10 used throughout
|
||||
|
||||
---
|
||||
|
||||
*Audit scope: static analysis only. Dynamic testing (active exploitation, fuzzing, dependency CVE scan) not performed.*
|
||||
337
docs/security/security-audit-tracker.md
Normal file
337
docs/security/security-audit-tracker.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Security Audit Tracker — STEAM Security Dashboard
|
||||
|
||||
**Last scan:** 2026-04-20
|
||||
**Scope:** Full repository — backend routes, middleware, helpers, scripts, frontend components
|
||||
**Baseline:** `docs/security-audit-2026-04-01.md` (31 findings), `docs/security-remediation-plan.md` (17 prioritised items)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Remediation Status — April 1 Audit](#remediation-status--april-1-audit)
|
||||
- [New Findings — April 20 Scan](#new-findings--april-20-scan)
|
||||
- [Open Finding Summary](#open-finding-summary)
|
||||
- [Positive Security Observations](#positive-security-observations)
|
||||
- [Scan Metadata](#scan-metadata)
|
||||
|
||||
---
|
||||
|
||||
## Remediation Status — April 1 Audit
|
||||
|
||||
Cross-reference of the 31 original findings against the current codebase. Status: **Fixed**, **Partial**, or **Open**.
|
||||
|
||||
### Critical Findings
|
||||
|
||||
| ID | Title | Status | Evidence |
|
||||
|---|---|---|---|
|
||||
| C-1 | Missing auth on Ivanti findings endpoints | **Fixed** | `ivantiFindings.js` — router uses `requireAuth(db)` at router level, `requireGroup` on sync |
|
||||
| C-2 | `requireRole(db)` bypasses role check in KB routes | **Fixed** | `knowledgeBase.js` — uses `requireGroup('Admin', 'Standard_User')` correctly |
|
||||
| C-3 | Unauthenticated finding note writes | **Fixed** | `ivantiFindings.js` — note routes behind `requireAuth(db)` |
|
||||
| C-4 | No brute force protection on login | **Fixed** | `auth.js` — `loginLimiter` (20 attempts / 15 min) applied to POST /login |
|
||||
| C-5 | Default credentials displayed in login UI | **Fixed** | `LoginForm.js` — no hardcoded credentials in the component |
|
||||
| C-6 | Missing sandbox on KB document iframe | **Fixed** | `KnowledgeBaseViewer.js:282` — `sandbox="allow-same-origin"` applied |
|
||||
|
||||
### High Findings
|
||||
|
||||
| ID | Title | Status | Evidence |
|
||||
|---|---|---|---|
|
||||
| H-1 | `/cleanup-sessions` missing role check | **Fixed** | `auth.js` — `requireAuth(db), requireGroup('Admin')` applied |
|
||||
| H-2 | Hardcoded fallback SESSION_SECRET | **Fixed** | `server.js:34-37` — hard-fails with `process.exit(1)` if unset |
|
||||
| H-3 | Audit log parameter mismatch — silent trail gaps | **Partial** | `knowledgeBase.js` — fixed. `archerTickets.js` — `logAudit` calls missing `username` field (see N-1 below) |
|
||||
| H-4 | Viewers can write compliance notes | **Fixed** | `compliance.js` — `requireGroup('Admin', 'Standard_User')` on POST /notes |
|
||||
| H-5 | Sync endpoints accessible to all authenticated users | **Fixed** | Both `ivantiFindings.js` and `ivantiWorkflows.js` — `requireGroup('Admin', 'Standard_User')` on POST /sync |
|
||||
| H-6 | HTTP header injection via Content-Disposition filename | **Fixed** | `knowledgeBase.js` — filename sanitized with `.replace(/["\r\n\\]/g, '')` |
|
||||
| H-7 | Race condition in KB file upload | **Fixed** | `knowledgeBase.js` — file moved after DB insert succeeds |
|
||||
| H-8 | Hardcoded default admin password in setup.js | **Fixed** | `setup.js` — generates random password via `crypto.randomBytes(12)` |
|
||||
| H-9 | ReactMarkdown renders HTML without sanitization | **Fixed** | `KnowledgeBaseViewer.js` — `rehypeSanitize` plugin applied |
|
||||
|
||||
### Medium Findings
|
||||
|
||||
| ID | Title | Status | Evidence |
|
||||
|---|---|---|---|
|
||||
| M-1 | No CSRF token protection | **Open** | Cookies use `SameSite: lax` — no CSRF token implemented |
|
||||
| M-2 | CORS credentials with explicit origin list | **Open** | Acceptable for this deployment model — monitor |
|
||||
| M-3 | No rate limiting on NVD API proxy | **Open** | No server-side cache or per-user rate limit on `/api/nvd/lookup` |
|
||||
| M-4 | Admin self-demotion check uses loose equality | **Fixed** | `users.js` — uses `String(userId) === String(req.user.id)` |
|
||||
| M-5 | Missing hostname format validation | **Fixed** | `compliance.js` POST /notes — regex validation `^[a-zA-Z0-9._-]+$` |
|
||||
| M-6 | Vendor field validated before trim | **Open** | `ivantiTodoQueue.js:8` — `isValidVendor()` checks length before trim |
|
||||
| M-7 | Unsanitized original filename in temp JSON | **Open** | `compliance.js:344` — `req.file.originalname` passed directly |
|
||||
| M-8 | Hardcoded frontend IP in CSP header | **Fixed** | `knowledgeBase.js:302` — reads from `CORS_ORIGINS` env var |
|
||||
| M-9 | API error messages forwarded to UI | **Open** | Frontend still uses `alert(err.message)` in several places |
|
||||
| M-10 | User data in window.confirm dialogs | **Open** | Frontend still uses `window.confirm` with user-supplied data |
|
||||
|
||||
### Low / Info Findings
|
||||
|
||||
| ID | Title | Status | Evidence |
|
||||
|---|---|---|---|
|
||||
| L-1 | Silent ROLLBACK on transaction failure | **Open** | `compliance.js:167` — `.catch(() => {})` still swallows errors |
|
||||
| L-2 | Fire-and-forget audit logging | **Partial** | `auditLog.js` — now logs to `console.error` on failure, but no alerting |
|
||||
| L-3 | Async temp file cleanup with no error handling | **Open** | `compliance.js` — `fs.unlink(path, () => {})` still used |
|
||||
| L-4 | IVANTI_SKIP_TLS with no startup warning | **Open** | No startup warning when `IVANTI_SKIP_TLS=true` |
|
||||
| L-5 | console.error in production frontend | **Open** | No environment guard on console.error calls |
|
||||
| L-6 | localStorage column config lacks structural validation | **Open** | No change observed |
|
||||
|
||||
### Remediation Plan Items (not in original 31)
|
||||
|
||||
| ID | Title | Status | Evidence |
|
||||
|---|---|---|---|
|
||||
| RP-1 | Authenticate /uploads static file access | **Open** | `server.js:127` — `express.static('uploads')` still unauthenticated |
|
||||
| RP-2 | Sanitize Mermaid SVG output with DOMPurify | **Open** | `KnowledgeBaseViewer.js:38` — `innerHTML = svg` without DOMPurify |
|
||||
| RP-3 | Strip server file paths from compliance preview response | **Open** | `compliance.js:342` — full `tempFilePath` returned to client |
|
||||
| RP-4 | Add SESSION_SECRET to .env.example | **Open** | `.env.example` — no `SESSION_SECRET` entry |
|
||||
|
||||
---
|
||||
|
||||
## New Findings — April 20 Scan
|
||||
|
||||
Findings discovered in this scan that were not present in the April 1 audit.
|
||||
|
||||
---
|
||||
|
||||
### N-1 — Archer Ticket Audit Logs Missing `username` Field (Medium)
|
||||
|
||||
**File:** `backend/routes/archerTickets.js:89, 172, 195`
|
||||
|
||||
All three `logAudit` calls in the Archer tickets router omit the `username` field:
|
||||
|
||||
```js
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
action: 'CREATE_ARCHER_TICKET',
|
||||
// username: req.user.username ← missing
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
The `auditLog.js` helper defaults missing username to `'unknown'`, so all Archer ticket audit entries show `username = 'unknown'` instead of the actual user.
|
||||
|
||||
**Impact:** Audit trail for Archer ticket operations cannot identify which user performed the action. Compliance reviews and incident investigations are degraded.
|
||||
|
||||
**Fix:** Add `username: req.user.username` to all three `logAudit` calls.
|
||||
|
||||
---
|
||||
|
||||
### N-2 — `migrate-to-1.1.js` Contains Hardcoded Admin Password (Medium)
|
||||
|
||||
**File:** `backend/migrate-to-1.1.js:246`
|
||||
|
||||
```js
|
||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
||||
```
|
||||
|
||||
While `setup.js` was fixed to generate random passwords (H-8), the migration script still hardcodes `admin123`. If this migration is run on an existing deployment, it resets the admin password to a known value.
|
||||
|
||||
**Impact:** Running the migration on a production system resets the admin account to a publicly known password.
|
||||
|
||||
**Fix:** Either generate a random password (matching `setup.js` pattern) or skip admin creation if the user already exists.
|
||||
|
||||
---
|
||||
|
||||
### N-3 — Compliance Preview Returns Full Server Filesystem Path (Medium)
|
||||
|
||||
**File:** `backend/routes/compliance.js:342`
|
||||
|
||||
```js
|
||||
tempFile: tempFilePath,
|
||||
```
|
||||
|
||||
The preview endpoint returns the full server-side path (e.g. `/home/cve-dashboard/backend/uploads/temp/compliance_preview_...json`) to the frontend. The commit endpoint then receives this path back and reads the file. This exposes the server's directory structure to any authenticated user.
|
||||
|
||||
**Impact:** Information disclosure — authenticated users learn the server's absolute filesystem layout, which aids further exploitation.
|
||||
|
||||
**Fix:** Return only the filename. Reconstruct the full path server-side in the commit handler:
|
||||
```js
|
||||
tempFile: tempFilename, // just the basename
|
||||
// In commit handler:
|
||||
const tempFile = path.join(TEMP_DIR, path.basename(req.body.tempFile));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### N-4 — `/uploads` Static Directory Served Without Authentication (High)
|
||||
|
||||
**File:** `backend/server.js:127`
|
||||
|
||||
```js
|
||||
app.use('/uploads', express.static('uploads', {
|
||||
dotfiles: 'deny',
|
||||
index: false
|
||||
}));
|
||||
```
|
||||
|
||||
All uploaded files (CVE documents, compliance data, knowledge base articles) are served as static files without any authentication check. Anyone who knows or guesses a file URL can access sensitive vulnerability documentation, compliance reports, and internal knowledge base content.
|
||||
|
||||
**Impact:** Unauthenticated access to all uploaded documents. File paths are predictable (CVE ID + vendor + timestamp-filename pattern).
|
||||
|
||||
**Fix:** Replace with an authenticated route handler:
|
||||
```js
|
||||
app.use('/uploads', requireAuth(db), express.static('uploads', { ... }));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### N-5 — Mermaid SVG Rendered via `innerHTML` Without Sanitization (Medium)
|
||||
|
||||
**File:** `frontend/src/components/KnowledgeBaseViewer.js:38`
|
||||
|
||||
```js
|
||||
ref.current.innerHTML = svg;
|
||||
```
|
||||
|
||||
Mermaid-generated SVG is injected directly into the DOM via `innerHTML`. While Mermaid itself sanitizes most input, a crafted diagram definition in a knowledge base article could potentially produce SVG with embedded event handlers or script elements.
|
||||
|
||||
**Impact:** Stored XSS vector if Mermaid's internal sanitization is bypassed. Any user viewing the article would execute the payload.
|
||||
|
||||
**Fix:** Sanitize the SVG string before injection:
|
||||
```js
|
||||
import DOMPurify from 'dompurify';
|
||||
ref.current.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true } });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### N-6 — `SESSION_SECRET` Not Documented in `.env.example` (Low)
|
||||
|
||||
**File:** `backend/.env.example`
|
||||
|
||||
The `SESSION_SECRET` environment variable is required for the server to start (hard-fail added per H-2 fix), but it is not listed in `.env.example`. Fresh deployments will fail with no guidance on what to set.
|
||||
|
||||
**Fix:** Add to `.env.example`:
|
||||
```
|
||||
# Session signing secret — generate with: openssl rand -hex 32
|
||||
SESSION_SECRET=
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### N-7 — `requireGroup` Error Response Leaks Current User Group (Low)
|
||||
|
||||
**File:** `backend/middleware/auth.js:55-60`
|
||||
|
||||
```js
|
||||
return res.status(403).json({
|
||||
error: 'Insufficient permissions',
|
||||
required: allowedGroups,
|
||||
current: req.user.group
|
||||
});
|
||||
```
|
||||
|
||||
The 403 response includes both the required groups and the user's current group. This is minor information disclosure — an attacker probing endpoints learns the exact group membership of the compromised account and which groups are needed.
|
||||
|
||||
**Fix:** Remove `required` and `current` from the response:
|
||||
```js
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### N-8 — No Content-Security-Policy Header on Main Application (Medium)
|
||||
|
||||
**File:** `backend/server.js:107-113`
|
||||
|
||||
Security headers include `X-Content-Type-Options`, `X-Frame-Options`, `X-XSS-Protection`, `Referrer-Policy`, and `Permissions-Policy`, but no `Content-Security-Policy` header. CSP is the primary browser-side defense against XSS.
|
||||
|
||||
**Impact:** No browser-enforced restriction on script sources. If an XSS vulnerability exists (e.g. N-5), there is no CSP to mitigate it.
|
||||
|
||||
**Fix:** Add a baseline CSP header:
|
||||
```js
|
||||
res.setHeader('Content-Security-Policy',
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
|
||||
"img-src 'self' data:; font-src 'self'; connect-src 'self'");
|
||||
```
|
||||
Start with `Content-Security-Policy-Report-Only` to avoid breaking existing functionality.
|
||||
|
||||
---
|
||||
|
||||
### N-9 — Expired Sessions Not Cleaned Up Automatically (Low)
|
||||
|
||||
**File:** `backend/server.js`, `backend/routes/auth.js`
|
||||
|
||||
The `sessions` table has no automatic cleanup. Expired sessions accumulate indefinitely. The `/cleanup-sessions` endpoint exists but must be triggered manually by an admin.
|
||||
|
||||
**Impact:** Performance degradation over time as the sessions table grows. Not directly exploitable, but expired session rows increase the surface for timing attacks on session lookups.
|
||||
|
||||
**Fix:** Add a cleanup interval on server startup:
|
||||
```js
|
||||
setInterval(() => {
|
||||
db.run("DELETE FROM sessions WHERE expires_at < datetime('now')");
|
||||
}, 6 * 60 * 60 * 1000); // every 6 hours
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Finding Summary
|
||||
|
||||
Prioritised list of all open findings requiring action.
|
||||
|
||||
### High Priority
|
||||
|
||||
| ID | Severity | Title | Source |
|
||||
|---|---|---|---|
|
||||
| N-4 | High | `/uploads` static directory served without authentication | New |
|
||||
|
||||
### Medium Priority
|
||||
|
||||
| ID | Severity | Title | Source |
|
||||
|---|---|---|---|
|
||||
| M-1 | Medium | No CSRF token protection | April 1 |
|
||||
| M-3 | Medium | No rate limiting on NVD API proxy | April 1 |
|
||||
| N-1 | Medium | Archer ticket audit logs missing `username` field | New |
|
||||
| N-2 | Medium | `migrate-to-1.1.js` contains hardcoded admin password | New |
|
||||
| N-3 | Medium | Compliance preview returns full server filesystem path | New |
|
||||
| N-5 | Medium | Mermaid SVG rendered via `innerHTML` without sanitization | New |
|
||||
| N-8 | Medium | No Content-Security-Policy header on main application | New |
|
||||
| M-6 | Medium | Vendor field validated before trim | April 1 |
|
||||
| M-7 | Medium | Unsanitized original filename in temp JSON | April 1 |
|
||||
| M-9 | Medium | API error messages forwarded to UI | April 1 |
|
||||
| M-10 | Medium | User data in `window.confirm` dialogs | April 1 |
|
||||
|
||||
### Low Priority
|
||||
|
||||
| ID | Severity | Title | Source |
|
||||
|---|---|---|---|
|
||||
| N-6 | Low | `SESSION_SECRET` not documented in `.env.example` | New |
|
||||
| N-7 | Low | `requireGroup` error response leaks current user group | New |
|
||||
| N-9 | Low | Expired sessions not cleaned up automatically | New |
|
||||
| L-1 | Low | Silent ROLLBACK on transaction failure | April 1 |
|
||||
| L-3 | Low | Async temp file cleanup with no error handling | April 1 |
|
||||
| L-4 | Low | IVANTI_SKIP_TLS with no startup warning | April 1 |
|
||||
| L-5 | Low | console.error in production frontend | April 1 |
|
||||
| L-6 | Low | localStorage column config lacks structural validation | April 1 |
|
||||
|
||||
---
|
||||
|
||||
## Positive Security Observations
|
||||
|
||||
Verified secure patterns that should be preserved:
|
||||
|
||||
- **SQL injection prevention** — all queries use parameterized statements throughout the entire codebase
|
||||
- **Path traversal prevention** — `sanitizePathSegment()` and `isPathWithinUploads()` consistently applied in `server.js`, `compliance.js`, and `knowledgeBase.js`
|
||||
- **Python script execution** — `spawn('python3', [SCRIPT, filePath])` with argument arrays — no shell injection
|
||||
- **File upload security** — extension allowlist + MIME prefix validation + 10 MB size limit via multer
|
||||
- **Password hashing** — bcrypt with cost factor 10 used for all password storage
|
||||
- **Session management** — 32-byte random session IDs via `crypto.randomBytes`, httpOnly cookies, 24h expiry
|
||||
- **Rate limiting** — login endpoint protected with 20 attempts per 15-minute window
|
||||
- **Audit trail** — comprehensive audit logging on all state-changing operations (with noted exceptions above)
|
||||
- **Self-modification prevention** — admin cannot demote or deactivate their own account
|
||||
- **Ownership-scoped deletion** — Standard_User can only delete resources they created
|
||||
- **Compliance linkage protection** — deletion blocked when tickets are linked to active compliance reports
|
||||
- **Temp file path validation** — `isSafeTempPath()` enforces `.json` extension and `uploads/temp/` directory
|
||||
- **Static file serving** — `dotfiles: 'deny'` and `index: false` prevent directory listing
|
||||
|
||||
---
|
||||
|
||||
## Scan Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Scan date | 2026-04-20 |
|
||||
| Scan type | Full repository static analysis |
|
||||
| Scope | `backend/`, `frontend/src/`, config files |
|
||||
| Baseline | `docs/security-audit-2026-04-01.md` |
|
||||
| Previous findings | 31 (6 Critical, 9 High, 10 Medium, 6 Low/Info) |
|
||||
| Remediated | 20 fully fixed, 2 partially fixed |
|
||||
| Still open (from baseline) | 13 |
|
||||
| New findings | 9 |
|
||||
| Total open | 22 (1 High, 11 Medium, 10 Low) |
|
||||
| Methodology | Static analysis — code review of all route handlers, middleware, helpers, and frontend components |
|
||||
183
docs/security/security-posture-workflow-diagrams.md
Normal file
183
docs/security/security-posture-workflow-diagrams.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Security Posture Workflow — Diagrams
|
||||
|
||||
Mermaid diagrams for the Host Finding Review & Remediation process.
|
||||
Renders natively in GitHub, GitLab, and most modern documentation tools.
|
||||
|
||||
---
|
||||
|
||||
## Diagram 1 — Host Finding Review Workflow (Steps 1–5)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([Open Vulnerability Triage Page]) --> SYNC
|
||||
|
||||
SYNC["① Sync & Sort<br/>Click Sync · Sort Due Date ascending"]
|
||||
SYNC --> DUE{Overdue<br/>findings?}
|
||||
DUE -->|Yes — start here| HOST
|
||||
DUE -->|No — start with amber| HOST
|
||||
|
||||
HOST["② Identify the Host<br/>Verify IP in IPControl / Infoblox"]
|
||||
HOST --> CORRECT{Hostname<br/>correct?}
|
||||
CORRECT -->|No| EDIT["Inline-edit Host / DNS cell<br/>Amber dot marks the override"]
|
||||
EDIT --> OWN
|
||||
CORRECT -->|Yes| OWN
|
||||
|
||||
OWN["③ Identify Asset Ownership<br/>Check BU column"]
|
||||
OWN --> BU{Our BU?}
|
||||
BU -->|"NTS-AEO-STEAM<br/>or ACCESS-ENG"| CVE
|
||||
BU -->|"Other BU<br/>or blank"| CARD["Add to CARD Queue<br/>☑ checkbox → CARD → Add to Queue"]
|
||||
CARD --> CARD2([Process in dedicated CARD session])
|
||||
|
||||
CVE["④ Review CVEs in the Finding<br/>Up to 2 shown · hover +N badge for more"]
|
||||
CVE --> DBCHECK{CVE in<br/>database?}
|
||||
DBCHECK -->|No| ADDCVE["Create CVE entry on Home page<br/>NVD auto-fill populates details"]
|
||||
ADDCVE --> RESEARCH
|
||||
DBCHECK -->|Yes — review existing notes/docs| RESEARCH
|
||||
|
||||
RESEARCH["Research CVE<br/>Vendor advisory · Cisco Bug Search<br/>Juniper PSN · Support ticket"]
|
||||
RESEARCH --> ACTION
|
||||
|
||||
ACTION["⑤ Determine Required Action"]
|
||||
ACTION --> PATH{What does<br/>research show?}
|
||||
|
||||
PATH -->|"Patch available<br/>FW / SW update"| PA
|
||||
PATH -->|"Fix is config<br/>change only"| PB
|
||||
PATH -->|"Not applicable<br/>to platform / version"| PC
|
||||
PATH -->|"Cannot patch<br/>vendor / EOL / business"| PD
|
||||
|
||||
PA["PATH A — Remediation<br/>Firmware or Software Upgrade"]
|
||||
PA --> PA1["Plan & schedule upgrade<br/>Add note to finding row"]
|
||||
PA1 --> PA2(["Finding drops off after<br/>next Ivanti scan ✓"])
|
||||
|
||||
PB["PATH B — Remediation<br/>Configuration Change"]
|
||||
PB --> PB1["☑ checkbox → Vendor → Archer<br/>Add to Queue"]
|
||||
PB1 --> PB2["Open Archer EXC ticket<br/>in dedicated session"]
|
||||
PB2 --> PB3(["Enter EXC-XXXXX<br/>in finding Notes cell ✓"])
|
||||
|
||||
PC["PATH C — False Positive"]
|
||||
PC --> PC1["Take device screenshot<br/>Hostname · IP · SW version"]
|
||||
PC1 --> PC2["Obtain vendor documentation<br/>advisory / email / support ticket"]
|
||||
PC2 --> PC3["Upload evidence to CVE database<br/>Home page → CVE row → Upload"]
|
||||
PC3 --> PC4["☑ checkbox → Vendor → FP<br/>Add to Queue"]
|
||||
PC4 --> PC5(["Submit FP workflow in Ivanti<br/>in dedicated session ✓"])
|
||||
|
||||
PD["PATH D — Risk Acceptance"]
|
||||
PD --> PD1["Take device screenshot<br/>Collect version info"]
|
||||
PD1 --> PD2{Vendor comms<br/>needed?}
|
||||
PD2 -->|Yes| PD3["Open vendor support ticket<br/>Request patch timeline / mitigations"]
|
||||
PD3 --> PD4
|
||||
PD2 -->|No| PD4["☑ checkbox → Vendor → Archer<br/>Add to Queue"]
|
||||
PD4 --> PD5["Open Archer EXC ticket<br/>in dedicated session"]
|
||||
PD5 --> PD6(["Enter EXC-XXXXX<br/>in finding Notes cell ✓"])
|
||||
|
||||
%% Styling
|
||||
classDef step fill:#1e3a5f,stroke:#0ea5e9,stroke-width:2px,color:#e2e8f0
|
||||
classDef decision fill:#1a2e1a,stroke:#10b981,stroke-width:2px,color:#e2e8f0
|
||||
classDef pathA fill:#14391f,stroke:#10b981,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef pathB fill:#2d1f14,stroke:#f59e0b,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef pathC fill:#2d1414,stroke:#ef4444,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef pathD fill:#1a1430,stroke:#8b5cf6,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef card fill:#1a2e1a,stroke:#10b981,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef done fill:#0f172a,stroke:#475569,stroke-width:1.5px,color:#64748b
|
||||
|
||||
class SYNC,HOST,OWN,CVE,RESEARCH,ACTION step
|
||||
class DUE,CORRECT,BU,DBCHECK,PATH decision
|
||||
class PA,PA1,PA2 pathA
|
||||
class PB,PB1,PB2,PB3 pathB
|
||||
class PC,PC1,PC2,PC3,PC4,PC5 pathC
|
||||
class PD,PD1,PD2,PD3,PD4,PD5,PD6 pathD
|
||||
class CARD,CARD2 card
|
||||
class EDIT done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 2 — FP Workflow Badge Status Decision Tree
|
||||
|
||||
What to do when a finding already has a workflow badge in the Vulnerability Triage page.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A([Finding in<br/>Vulnerability Triage]) --> B{"Check<br/>Workflow column"}
|
||||
|
||||
B -->|No badge| C["UNTRIAGED<br/>No action on record"]
|
||||
C --> C1(["Follow the<br/>Step 1–5 triage workflow ↑"])
|
||||
|
||||
B -->|"🔵 Blue<br/>Requested"| D["IN FLIGHT<br/>FP submitted · awaiting approval"]
|
||||
D --> D1{"SLA window<br/>approaching?"}
|
||||
D1 -->|No| D2(["Monitor — no action yet ✓"])
|
||||
D1 -->|Yes| D3(["Follow up with<br/>the approver"])
|
||||
|
||||
B -->|"🟡 Amber<br/>Reworked"| E["NEEDS REVISION<br/>Reviewer returned the ticket"]
|
||||
E --> E1["Open ticket in Ivanti<br/>Review feedback"]
|
||||
E1 --> E2(["Update justification<br/>and resubmit"])
|
||||
|
||||
B -->|"🟡 Amber<br/>Actionable"| F["NEEDS RESPONSE<br/>Ticket flagged for team action"]
|
||||
F --> F1(["Open ticket in Ivanti<br/>Respond to the request"])
|
||||
|
||||
B -->|"🔴 Red<br/>Expired"| G["EXCEPTION LAPSED<br/>Finding has re-opened"]
|
||||
G --> G1(["Submit a new FP request<br/>in Ivanti<br/>Reference previous ticket"])
|
||||
|
||||
B -->|"🔴 Red<br/>Rejected"| H["CONFIRMED VULNERABILITY<br/>Security team denied the FP"]
|
||||
H --> H1(["Remediate the vulnerability<br/>Do not resubmit FP<br/>without new evidence"])
|
||||
|
||||
%% Styling
|
||||
classDef trigger fill:#0f172a,stroke:#0ea5e9,stroke-width:2px,color:#e2e8f0
|
||||
classDef blue fill:#1e3a5f,stroke:#0ea5e9,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef amber fill:#2d2014,stroke:#f59e0b,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef red fill:#2d1414,stroke:#ef4444,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef none fill:#1a1a2e,stroke:#475569,stroke-width:1.5px,color:#94a3b8
|
||||
classDef done fill:#0f172a,stroke:#334155,stroke-width:1px,color:#64748b
|
||||
|
||||
class A,B trigger
|
||||
class D,D1,D2,D3 blue
|
||||
class E,E1,E2,F,F1 amber
|
||||
class G,G1,H,H1 red
|
||||
class C,C1 none
|
||||
class D2,D3,E2,F1,G1,H1 done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 3 — Action Decision Matrix (Quick Reference)
|
||||
|
||||
Condensed view of the five research outcomes and their required actions.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
START(["Research complete<br/>Step 4 done"]) --> Q{"What is the<br/>remediation path?"}
|
||||
|
||||
Q --> R1["Firmware or<br/>Software update available"]
|
||||
R1 --> A1(["No ticket needed<br/>Schedule upgrade<br/>Add note to finding"])
|
||||
|
||||
Q --> R2["Fix is a<br/>configuration change"]
|
||||
R2 --> A2(["Archer EXC ticket required<br/>Stage as Archer in Queue"])
|
||||
|
||||
Q --> R3["Not applicable<br/>to this platform / version"]
|
||||
R3 --> A3(["FP workflow in Ivanti<br/>Evidence in CVE database"])
|
||||
|
||||
Q --> R4["Patch not yet<br/>available from vendor"]
|
||||
R4 --> A4(["Archer EXC ticket<br/>Renew when patch ships"])
|
||||
|
||||
Q --> R5["Device is EOL / EOS<br/>or business constraint"]
|
||||
R5 --> A5(["Archer ticket with<br/>mitigation steps +<br/>remediation plan"])
|
||||
|
||||
Q --> R6["Asset not owned<br/>by our BU"]
|
||||
R6 --> A6(["CARD queue<br/>CARD disposition process"])
|
||||
|
||||
classDef q fill:#1e3a5f,stroke:#0ea5e9,stroke-width:2px,color:#e2e8f0
|
||||
classDef green fill:#14391f,stroke:#10b981,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef amber fill:#2d2014,stroke:#f59e0b,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef red fill:#2d1414,stroke:#ef4444,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef teal fill:#0f2d2d,stroke:#14b8a6,stroke-width:1.5px,color:#e2e8f0
|
||||
|
||||
class START,Q q
|
||||
class R1,A1 green
|
||||
class R2,A2,R4,A4,R5,A5 amber
|
||||
class R3,A3 red
|
||||
class R6,A6 teal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Source document: `docs/security-posture-workflow.md`*
|
||||
175
docs/security/security-posture-workflow-lucidchart.md
Normal file
175
docs/security/security-posture-workflow-lucidchart.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Lucidchart Import — Raw Mermaid Code
|
||||
|
||||
Lucidchart expects raw Mermaid syntax only — no markdown headings or prose.
|
||||
Paste each diagram separately: Insert → Diagram as Code → Mermaid → paste → Generate.
|
||||
|
||||
---
|
||||
|
||||
## DIAGRAM 1 — Host Finding Review Workflow
|
||||
|
||||
Paste everything between the triple-backtick fences below:
|
||||
|
||||
```
|
||||
flowchart TD
|
||||
START([Open Reporting Page]) --> SYNC
|
||||
|
||||
SYNC["① Sync & Sort<br/>Click Sync · Sort Due Date ascending"]
|
||||
SYNC --> DUE{Overdue<br/>findings?}
|
||||
DUE -->|Yes — start here| HOST
|
||||
DUE -->|No — start with amber| HOST
|
||||
|
||||
HOST["② Identify the Host<br/>Verify IP in IPControl / Infoblox"]
|
||||
HOST --> CORRECT{Hostname<br/>correct?}
|
||||
CORRECT -->|No| EDIT["Inline-edit Host / DNS cell<br/>Amber dot marks the override"]
|
||||
EDIT --> OWN
|
||||
CORRECT -->|Yes| OWN
|
||||
|
||||
OWN["③ Identify Asset Ownership<br/>Check BU column"]
|
||||
OWN --> BU{Our BU?}
|
||||
BU -->|"NTS-AEO-STEAM or ACCESS-ENG"| CVE
|
||||
BU -->|"Other BU or blank"| CARD["Add to CARD Queue<br/>checkbox → CARD → Add to Queue"]
|
||||
CARD --> CARD2([Process in dedicated CARD session])
|
||||
|
||||
CVE["④ Review CVEs in the Finding<br/>Up to 2 shown · hover badge for more"]
|
||||
CVE --> DBCHECK{CVE in<br/>database?}
|
||||
DBCHECK -->|No| ADDCVE["Create CVE entry on Home page<br/>NVD auto-fill populates details"]
|
||||
ADDCVE --> RESEARCH
|
||||
DBCHECK -->|Yes — review existing notes/docs| RESEARCH
|
||||
|
||||
RESEARCH["Research CVE<br/>Vendor advisory · Cisco Bug Search<br/>Juniper PSN · Support ticket"]
|
||||
RESEARCH --> ACTION
|
||||
|
||||
ACTION["⑤ Determine Required Action"]
|
||||
ACTION --> PATH{What does<br/>research show?}
|
||||
|
||||
PATH -->|"Patch available — FW / SW update"| PA
|
||||
PATH -->|"Fix is config change only"| PB
|
||||
PATH -->|"Not applicable to platform / version"| PC
|
||||
PATH -->|"Cannot patch — vendor / EOL / business"| PD
|
||||
|
||||
PA["PATH A — Remediation<br/>Firmware or Software Upgrade"]
|
||||
PA --> PA1["Plan & schedule upgrade<br/>Add note to finding row"]
|
||||
PA1 --> PA2(["Finding drops off after<br/>next Ivanti scan ✓"])
|
||||
|
||||
PB["PATH B — Remediation<br/>Configuration Change"]
|
||||
PB --> PB1["checkbox → Vendor → Archer<br/>Add to Queue"]
|
||||
PB1 --> PB2["Open Archer EXC ticket<br/>in dedicated session"]
|
||||
PB2 --> PB3(["Enter EXC-XXXXX<br/>in finding Notes cell ✓"])
|
||||
|
||||
PC["PATH C — False Positive"]
|
||||
PC --> PC1["Take device screenshot<br/>Hostname · IP · SW version"]
|
||||
PC1 --> PC2["Obtain vendor documentation<br/>advisory / email / support ticket"]
|
||||
PC2 --> PC3["Upload evidence to CVE database<br/>Home page → CVE row → Upload"]
|
||||
PC3 --> PC4["checkbox → Vendor → FP<br/>Add to Queue"]
|
||||
PC4 --> PC5(["Submit FP workflow in Ivanti<br/>in dedicated session ✓"])
|
||||
|
||||
PD["PATH D — Risk Acceptance"]
|
||||
PD --> PD1["Take device screenshot<br/>Collect version info"]
|
||||
PD1 --> PD2{Vendor comms<br/>needed?}
|
||||
PD2 -->|Yes| PD3["Open vendor support ticket<br/>Request patch timeline / mitigations"]
|
||||
PD3 --> PD4
|
||||
PD2 -->|No| PD4["checkbox → Vendor → Archer<br/>Add to Queue"]
|
||||
PD4 --> PD5["Open Archer EXC ticket<br/>in dedicated session"]
|
||||
PD5 --> PD6(["Enter EXC-XXXXX<br/>in finding Notes cell ✓"])
|
||||
|
||||
classDef step fill:#1e3a5f,stroke:#0ea5e9,stroke-width:2px,color:#e2e8f0
|
||||
classDef decision fill:#1a2e1a,stroke:#10b981,stroke-width:2px,color:#e2e8f0
|
||||
classDef pathA fill:#14391f,stroke:#10b981,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef pathB fill:#2d1f14,stroke:#f59e0b,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef pathC fill:#2d1414,stroke:#ef4444,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef pathD fill:#1a1430,stroke:#8b5cf6,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef card fill:#1a2e1a,stroke:#10b981,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef done fill:#0f172a,stroke:#475569,stroke-width:1.5px,color:#64748b
|
||||
|
||||
class SYNC,HOST,OWN,CVE,RESEARCH,ACTION step
|
||||
class DUE,CORRECT,BU,DBCHECK,PATH decision
|
||||
class PA,PA1,PA2 pathA
|
||||
class PB,PB1,PB2,PB3 pathB
|
||||
class PC,PC1,PC2,PC3,PC4,PC5 pathC
|
||||
class PD,PD1,PD2,PD3,PD4,PD5,PD6 pathD
|
||||
class CARD,CARD2 card
|
||||
class EDIT done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DIAGRAM 2 — FP Workflow Badge Status Decision Tree
|
||||
|
||||
```
|
||||
flowchart LR
|
||||
A([Finding in Reporting Page]) --> B{"Check Workflow column"}
|
||||
|
||||
B -->|No badge| C["UNTRIAGED<br/>No action on record"]
|
||||
C --> C1(["Follow the Step 1-5 triage workflow"])
|
||||
|
||||
B -->|Blue - Requested| D["IN FLIGHT<br/>FP submitted · awaiting approval"]
|
||||
D --> D1{"SLA window<br/>approaching?"}
|
||||
D1 -->|No| D2(["Monitor — no action yet"])
|
||||
D1 -->|Yes| D3(["Follow up with the approver"])
|
||||
|
||||
B -->|Amber - Reworked| E["NEEDS REVISION<br/>Reviewer returned the ticket"]
|
||||
E --> E1["Open ticket in Ivanti<br/>Review feedback"]
|
||||
E1 --> E2(["Update justification and resubmit"])
|
||||
|
||||
B -->|Amber - Actionable| F["NEEDS RESPONSE<br/>Ticket flagged for team action"]
|
||||
F --> F1(["Open ticket in Ivanti<br/>Respond to the request"])
|
||||
|
||||
B -->|Red - Expired| G["EXCEPTION LAPSED<br/>Finding has re-opened"]
|
||||
G --> G1(["Submit a new FP request in Ivanti<br/>Reference previous ticket"])
|
||||
|
||||
B -->|Red - Rejected| H["CONFIRMED VULNERABILITY<br/>Security team denied the FP"]
|
||||
H --> H1(["Remediate the vulnerability<br/>Do not resubmit FP without new evidence"])
|
||||
|
||||
classDef trigger fill:#0f172a,stroke:#0ea5e9,stroke-width:2px,color:#e2e8f0
|
||||
classDef blue fill:#1e3a5f,stroke:#0ea5e9,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef amber fill:#2d2014,stroke:#f59e0b,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef red fill:#2d1414,stroke:#ef4444,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef none fill:#1a1a2e,stroke:#475569,stroke-width:1.5px,color:#94a3b8
|
||||
classDef done fill:#0f172a,stroke:#334155,stroke-width:1px,color:#64748b
|
||||
|
||||
class A,B trigger
|
||||
class D,D1,D2,D3 blue
|
||||
class E,E1,E2,F,F1 amber
|
||||
class G,G1,H,H1 red
|
||||
class C,C1 none
|
||||
class D2,D3,E2,F1,G1,H1 done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DIAGRAM 3 — Action Decision Matrix
|
||||
|
||||
```
|
||||
flowchart LR
|
||||
START(["Research complete — Step 4 done"]) --> Q{"What is the<br/>remediation path?"}
|
||||
|
||||
Q --> R1["Firmware or software update available"]
|
||||
R1 --> A1(["No ticket needed<br/>Schedule upgrade · Add note to finding"])
|
||||
|
||||
Q --> R2["Fix is a configuration change only"]
|
||||
R2 --> A2(["Archer EXC ticket required<br/>Stage as Archer in Queue"])
|
||||
|
||||
Q --> R3["Not applicable to this platform / version"]
|
||||
R3 --> A3(["FP workflow in Ivanti<br/>Evidence in CVE database"])
|
||||
|
||||
Q --> R4["Patch not yet available from vendor"]
|
||||
R4 --> A4(["Archer EXC ticket<br/>Renew when patch ships"])
|
||||
|
||||
Q --> R5["Device is EOL / EOS or business constraint"]
|
||||
R5 --> A5(["Archer ticket with mitigation steps<br/>and remediation plan"])
|
||||
|
||||
Q --> R6["Asset not owned by our BU"]
|
||||
R6 --> A6(["CARD queue — CARD disposition process"])
|
||||
|
||||
classDef q fill:#1e3a5f,stroke:#0ea5e9,stroke-width:2px,color:#e2e8f0
|
||||
classDef green fill:#14391f,stroke:#10b981,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef amber fill:#2d2014,stroke:#f59e0b,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef red fill:#2d1414,stroke:#ef4444,stroke-width:1.5px,color:#e2e8f0
|
||||
classDef teal fill:#0f2d2d,stroke:#14b8a6,stroke-width:1.5px,color:#e2e8f0
|
||||
|
||||
class START,Q q
|
||||
class R1,A1 green
|
||||
class R2,A2,R4,A4,R5,A5 amber
|
||||
class R3,A3 red
|
||||
class R6,A6 teal
|
||||
```
|
||||
402
docs/security/security-posture-workflow.md
Normal file
402
docs/security/security-posture-workflow.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Security Posture Workflow — Host Finding Review & Remediation
|
||||
|
||||
**Document Type:** Process Guide
|
||||
**Applies To:** STEAM Security Dashboard — All Pages
|
||||
**Audience:** NTS-AEO-STEAM / NTS-AEO-ACCESS-ENG team members
|
||||
**Last Updated:** 2026-03-27
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#1-overview)
|
||||
2. [Dashboard Orientation](#2-dashboard-orientation)
|
||||
3. [Vulnerability Designations](#3-vulnerability-designations)
|
||||
4. [The Host Finding Review Workflow](#4-the-host-finding-review-workflow)
|
||||
- [Step 1 — Sync and Sort by Due Date](#step-1--sync-and-sort-by-due-date)
|
||||
- [Step 2 — Identify the Host](#step-2--identify-the-host)
|
||||
- [Step 3 — Identify Asset Ownership](#step-3--identify-asset-ownership)
|
||||
- [Step 4 — Review the CVEs in the Finding](#step-4--review-the-cves-in-the-finding)
|
||||
- [Step 5 — Determine and Execute the Required Action](#step-5--determine-and-execute-the-required-action)
|
||||
5. [Using the Ivanti Queue](#5-using-the-ivanti-queue)
|
||||
6. [Workflow Status Reference](#6-workflow-status-reference)
|
||||
7. [Quick Reference Card](#7-quick-reference-card)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The STEAM Security Dashboard centralises vulnerability management for the NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG business units. It pulls host findings directly from Ivanti/RiskSense and gives the team a single place to triage, track, and action every open vulnerability.
|
||||
|
||||
**Scope:** This document covers severity findings in the **8.5 – 9.9 VRR range**. All findings in this range require some form of documented action. A finding that is not actioned before its Due Date results in the device being recorded as non-compliant.
|
||||
|
||||
> **SLA Rule:** By default, all vulnerabilities must have an action taken or in-flight within **60 days of detection**. The Due Date column on the Reporting page shows the exact deadline. Metrics and compliance reporting are based on vulnerabilities aged under 60 days.
|
||||
|
||||
---
|
||||
|
||||
## 2. Dashboard Orientation
|
||||
|
||||
### Pages
|
||||
|
||||
| Page | Purpose |
|
||||
|------|---------|
|
||||
| **Home (CVE Management)** | Track and research individual CVEs across vendors. Store supporting documentation. Log Archer EXC ticket numbers against CVE/vendor pairs. |
|
||||
| **Reporting (Host Findings)** | The primary operational page. Live view of all open Ivanti findings with filtering, sorting, inline editing, the Ivanti Queue, and export. |
|
||||
| **Knowledge Base** | Internal document library — policies, runbooks, vendor advisories. |
|
||||
| **Exports** | Bulk export tools for reports and data extracts. |
|
||||
|
||||
### Reporting Page — At a Glance
|
||||
|
||||
When you open the Reporting page for the first time in a session, click **Sync** (top right) to pull the latest findings from Ivanti. The page shows:
|
||||
|
||||
- **Four metric charts** at the top — Open vs Closed, Action Coverage, FP Finding Status, FP Workflow Status
|
||||
- **Findings table** below — every open finding for the configured BUs, one row per host finding
|
||||
- **Ivanti Queue panel** (click the Queue button, top right) — your personal staging list for batch-processing FP and Archer workflows
|
||||
|
||||
The charts and table update together. Clicking a chart segment filters the table to that subset.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vulnerability Designations
|
||||
|
||||
Every finding in the 8.5–9.9 range requires one of three documented actions. Understanding these upfront makes triage faster.
|
||||
|
||||
### 3.1 Remediation
|
||||
|
||||
The vulnerability is addressed by fixing the root cause.
|
||||
|
||||
| Remediation Method | Archer Ticket Required? | Notes |
|
||||
|---|---|---|
|
||||
| Firmware or software update | **No** | Upgrading removes the vulnerability entirely. The finding will fall off the report on the next scan. |
|
||||
| Configuration change | **Yes** | A config change does not remove the vulnerability — if the config is ever rolled back, the vulnerability returns. An Archer Risk Acceptance ticket is required to document this. |
|
||||
|
||||
### 3.2 False Positive (FP)
|
||||
|
||||
A false positive occurs when the scanner detects a vulnerability that is **not actually present** or **does not apply** to the platform or software version in use.
|
||||
|
||||
**An FP workflow must be opened in Ivanti.** The workflow requires:
|
||||
|
||||
1. A **screenshot** taken directly from the device showing:
|
||||
- Hostname
|
||||
- IP address
|
||||
- Software / firmware version
|
||||
> **Important:** This must be a screenshot. CLI text output or copy-pasted command output is not accepted.
|
||||
|
||||
2. **Vendor documentation** confirming the vulnerability does not affect the platform — one of:
|
||||
- Direct vendor communication (email, support ticket)
|
||||
- Published security advisory stating the version or platform is not affected
|
||||
- Proof that the vulnerability does not apply to the currently installed version
|
||||
|
||||
Supporting files (screenshots, emails, advisories) should be saved into the CVE Database (Home page → upload documents against the relevant CVE/vendor pair) for future reference and re-use if the FP expires and needs to be renewed.
|
||||
|
||||
### 3.3 Risk Acceptance / Archer Request
|
||||
|
||||
An Archer Risk Acceptance ticket (EXC-XXXXX) is required when a vulnerability **cannot be patched** for a documented business or technical reason. Common scenarios:
|
||||
|
||||
| Scenario | Required Action |
|
||||
|---|---|
|
||||
| Patch not yet available (waiting on vendor) | Open Archer ticket; close it when patch is deployed |
|
||||
| Device is End-of-Sale (EOS) or End-of-Life (EOL) | Archer ticket required with mitigation steps and a remediation plan |
|
||||
| Business constraint prevents patching | Archer ticket with justification and compensating controls |
|
||||
| Configuration-change-only remediation | Archer ticket required (see Remediation above) |
|
||||
|
||||
For EOL/EOS devices the ticket must include:
|
||||
- Current mitigation steps (network segmentation, compensating controls)
|
||||
- A remediation plan — what will replace or retire the device and when
|
||||
|
||||
If vendor communication is needed (patch timeline, configuration guidance), open a vendor support ticket and use the vendor's response to fill out the Archer remediation plan field.
|
||||
|
||||
> Archer EXC numbers are tracked in the dashboard. Once entered on the Home page against the relevant CVE/vendor pair, the EXC badge appears on that CVE row. Clicking the badge navigates to the Reporting page pre-filtered to findings with that EXC number in their notes.
|
||||
|
||||
---
|
||||
|
||||
## 4. The Host Finding Review Workflow
|
||||
|
||||
Work through the Reporting page top-to-bottom by Due Date. The goal of each session is to ensure every finding either has an action in-flight or gets one started.
|
||||
|
||||
---
|
||||
|
||||
### Step 1 — Sync and Sort by Due Date
|
||||
|
||||
1. Navigate to the **Reporting** page.
|
||||
2. Click **Sync** (top right). Wait for the sync to complete — the timestamp updates when done.
|
||||
3. Click the **Due Date** column header to sort ascending (soonest due date first).
|
||||
- Red due dates = overdue
|
||||
- Amber due dates = due within 30 days
|
||||
- Start with red, then amber
|
||||
|
||||
> If you want to focus on findings with no action yet, click the **Pending** segment on the Action Coverage donut chart. The table will filter to only findings with no FP ticket and no EXC number in notes.
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Identify the Host
|
||||
|
||||
Each finding row includes a **Host** (hostname), **IP Address**, and **DNS** column.
|
||||
|
||||
1. Use the reported **IP address** to verify the hostname in:
|
||||
- **IPControl** (read-only, historical IPAM data)
|
||||
- **Infoblox** (current IPAM — preferred for current state)
|
||||
|
||||
2. If the hostname shown in the dashboard is incorrect (Ivanti sometimes reports stale data):
|
||||
- Click the **Host** cell in the finding row — it is inline editable.
|
||||
- Type the correct hostname and press **Enter** or click away to save.
|
||||
- An amber dot (●) will appear on the cell to indicate an override is in place. The original Ivanti value is preserved and can be restored using the revert button (↻).
|
||||
- The same applies to the **DNS** column.
|
||||
|
||||
> Overrides survive Ivanti re-syncs — your corrections are not overwritten when new data is pulled.
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Identify Asset Ownership
|
||||
|
||||
Check the **BU** column to determine ownership.
|
||||
|
||||
| BU Value | Ownership | Action |
|
||||
|---|---|---|
|
||||
| `NTS-AEO-STEAM` | Our team | Continue to Step 4 |
|
||||
| `NTS-AEO-ACCESS-ENG` | Our team | Continue to Step 4 |
|
||||
| Any other value, or blank | Not our asset | Add to CARD queue (see below) |
|
||||
|
||||
**If the asset is not owned by our BU:**
|
||||
|
||||
1. Check the checkbox at the left of the finding row.
|
||||
2. A popover will appear. The **CARD** workflow type should already be selected.
|
||||
- No vendor entry is required for CARD — the IP address is captured automatically for use when searching in CARD.
|
||||
3. Click **Add to Queue**.
|
||||
4. The finding is now staged in your Ivanti Queue under the **CARD** section.
|
||||
|
||||
CARD queue items are processed in a separate session — see the [Ivanti Queue](#5-using-the-ivanti-queue) section and the dedicated CARD process documentation.
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Review the CVEs in the Finding
|
||||
|
||||
Each finding has one or more CVEs listed in the **CVEs** column (up to 2 shown; hover the "+N" badge to see the rest).
|
||||
|
||||
For each CVE in the finding:
|
||||
|
||||
1. **Check if the CVE already exists in the database.**
|
||||
- Navigate to the **Home** page.
|
||||
- Search for the CVE ID in the search bar.
|
||||
- If an entry exists for this CVE and vendor, review what's already documented — there may be existing notes, documents, or an Archer ticket already linked.
|
||||
|
||||
2. **If no entry exists, create one:**
|
||||
- Click **Add CVE** on the Home page.
|
||||
- Enter the CVE ID — the NVD auto-fill will populate the description, CVSS severity, and published date automatically.
|
||||
- Select the correct vendor/platform.
|
||||
- Save the entry.
|
||||
|
||||
3. **Research the CVE** to determine the required action:
|
||||
- Check the vendor's security advisory portal (e.g., Juniper Security Advisories, Cisco Security Advisories / Bug Search Tool)
|
||||
- Determine whether the CVE: (a) is a False Positive for this platform/version, (b) can be Remediated, or (c) requires a Risk Acceptance
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Determine and Execute the Required Action
|
||||
|
||||
Based on your research in Step 4, choose the path below.
|
||||
|
||||
---
|
||||
|
||||
#### Path A — Remediation (Firmware or Software Update)
|
||||
|
||||
> No Archer ticket required if the fix is a firmware or software upgrade.
|
||||
|
||||
1. Plan and schedule the upgrade with the relevant team.
|
||||
2. No dashboard action is required beyond ensuring a note is added to the finding (click the **Notes** cell) confirming the upgrade is planned or complete.
|
||||
3. After the device is upgraded, the finding will fall off the Reporting page on the next Ivanti scan if the vulnerability is no longer detected.
|
||||
|
||||
---
|
||||
|
||||
#### Path B — Remediation (Configuration Change)
|
||||
|
||||
> An Archer Risk Acceptance ticket **is required** when the fix is a configuration change.
|
||||
|
||||
1. Check the checkbox at the left of the finding row.
|
||||
2. In the popover, enter the **Vendor / Platform** (e.g., Juniper, Cisco, ADTRAN).
|
||||
3. Select **Archer** as the workflow type.
|
||||
4. Click **Add to Queue**.
|
||||
5. Process the Archer ticket in a dedicated session — see [Ivanti Queue](#5-using-the-ivanti-queue) and the Archer process documentation.
|
||||
|
||||
---
|
||||
|
||||
#### Path C — False Positive
|
||||
|
||||
1. **Collect the required evidence:**
|
||||
- Log into the device and **take a screenshot** showing the hostname, IP address, and software/firmware version.
|
||||
- Obtain vendor documentation confirming the CVE does not affect this platform or version (security advisory, vendor email, etc.).
|
||||
|
||||
2. **Save supporting files to the database:**
|
||||
- Go to the Home page and find (or create) the CVE entry for this vendor.
|
||||
- Upload the screenshot as type `screenshot` and the vendor communication as type `advisory` or `email`.
|
||||
- This ensures the evidence is accessible when the FP expires and needs to be renewed.
|
||||
|
||||
3. **Stage the finding in the queue:**
|
||||
- Check the checkbox at the left of the finding row on the Reporting page.
|
||||
- Enter the **Vendor / Platform**.
|
||||
- Select **FP** as the workflow type.
|
||||
- Click **Add to Queue**.
|
||||
|
||||
4. **Open the False Positive workflow in Ivanti:**
|
||||
- Process queued FP items in a dedicated session.
|
||||
- See the dedicated FP workflow documentation for the full Ivanti submission steps.
|
||||
|
||||
---
|
||||
|
||||
#### Path D — Risk Acceptance (Archer Ticket)
|
||||
|
||||
1. **Collect information** as you would for a False Positive (device screenshot, version info).
|
||||
2. If vendor communication is required (patch timeline, EOL statement, recommended mitigations):
|
||||
- Open a vendor support ticket requesting remediation steps, configuration guidance, or a patch commitment date.
|
||||
- Use the vendor's response to fill out the Archer remediation plan.
|
||||
3. **Stage the finding in the queue:**
|
||||
- Check the checkbox on the finding row.
|
||||
- Enter the **Vendor / Platform**.
|
||||
- Select **Archer** as the workflow type.
|
||||
- Click **Add to Queue**.
|
||||
4. **Open the Archer Risk Acceptance ticket:**
|
||||
- Process queued Archer items in a dedicated session.
|
||||
- See the dedicated Archer process documentation for the full submission steps.
|
||||
5. Once the EXC number is assigned, enter it in the finding's **Notes** cell on the Reporting page (format: `EXC-XXXXX`). The dashboard will recognise the pattern and include it in the Action Coverage chart under "Archer Exception".
|
||||
|
||||
---
|
||||
|
||||
## 5. Using the Ivanti Queue
|
||||
|
||||
The Ivanti Queue is a personal staging list built into the Reporting page. Rather than interrupting your review to context-switch into Ivanti, you tag findings as you go and then batch-process all the Ivanti work in one focused session.
|
||||
|
||||
### Adding Items to the Queue
|
||||
|
||||
1. On the Reporting page, check the **checkbox at the far left** of any finding row.
|
||||
2. A popover appears anchored to the row.
|
||||
3. For **FP** and **Archer** items: enter the **Vendor / Platform** (free text — e.g., "Juniper MX", "Cisco IOS-XE").
|
||||
4. Select the **workflow type**:
|
||||
- **FP** — False Positive request to be submitted in Ivanti
|
||||
- **Archer** — Archer Risk Acceptance ticket to be opened
|
||||
- **CARD** — Asset not owned by our BU; IP address is captured automatically
|
||||
5. Click **Add to Queue**. The row checkbox turns solid blue to indicate it is queued.
|
||||
|
||||
### Opening the Queue Panel
|
||||
|
||||
Click the **Queue** button in the top-right of the Reporting page. A slide-out panel opens from the right showing all your queued items.
|
||||
|
||||
- **CARD** items appear at the top of the panel in their own green section, with the IP address displayed for easy CARD search.
|
||||
- **FP and Archer** items are grouped alphabetically by vendor/platform below.
|
||||
- Each item shows: Finding ID, CVEs (or IP for CARD), and the workflow type badge (amber = FP, sky = Archer, green = CARD).
|
||||
|
||||
### Working the Queue
|
||||
|
||||
**Marking items complete:**
|
||||
Once you have submitted the FP or Archer ticket in Ivanti (or actioned the CARD item), check the item's green checkbox to mark it complete. Completed items are shown with a strikethrough at reduced opacity.
|
||||
|
||||
**Deleting items:**
|
||||
- Click the trash icon on an individual item to remove it.
|
||||
- To remove multiple items at once: check the small red selection checkbox on the left of each item you want to remove, then click **Delete (N)** in the footer.
|
||||
|
||||
**Clearing completed items:**
|
||||
Click **Clear Completed** in the footer to remove all marked-complete items at once.
|
||||
|
||||
> Queue items are stored in the database and are **personal to your login** — they persist across sessions and page refreshes. Other team members see only their own queue.
|
||||
|
||||
---
|
||||
|
||||
## 6. Workflow Status Reference
|
||||
|
||||
The **Workflow** column on the Reporting page tracks FP# tickets — False Positive requests submitted in Ivanti. The badge shows the ticket ID and its current state, colour-coded by urgency.
|
||||
|
||||
> SYS# workflows are auto-generated system tracking records. They are not displayed and do not require team action.
|
||||
|
||||
### Status Colour Codes
|
||||
|
||||
#### 🔴 Red — Act Immediately
|
||||
|
||||
| State | Meaning | Required Action |
|
||||
|---|---|---|
|
||||
| **Expired** | An FP# ticket existed but the exception window has lapsed. The finding has re-opened. | Log into Ivanti and submit a **new FP request** for this finding. Reference the previous ticket if relevant. |
|
||||
| **Rejected** | The security team reviewed the FP and denied it. The finding is a confirmed, exploitable vulnerability. | **Remediate the vulnerability.** Apply the relevant patch, configuration change, or compensating control. Do not resubmit an FP without new evidence. |
|
||||
|
||||
#### 🟡 Amber — Action Required Soon
|
||||
|
||||
| State | Meaning | Required Action |
|
||||
|---|---|---|
|
||||
| **Reworked** | The FP request was challenged by the reviewer and returned for revision. | Open the ticket in Ivanti, review the feedback, update the justification, and **resubmit**. |
|
||||
| **Actionable** | The FP ticket has been flagged as needing team action. | Open the ticket in Ivanti and respond to what is required. |
|
||||
|
||||
#### 🔵 Blue — In Flight, Monitor
|
||||
|
||||
| State | Meaning | Required Action |
|
||||
|---|---|---|
|
||||
| **Requested** | An FP# ticket has been submitted and is awaiting security team approval. | No immediate action. Monitor for approval or rejection. If the SLA window is approaching with no response, follow up with the approver. |
|
||||
|
||||
#### — (No Badge) — Untriaged
|
||||
|
||||
| State | Meaning | Required Action |
|
||||
|---|---|---|
|
||||
| **No workflow badge** | No FP ticket has ever been submitted for this finding. | Triage the finding using the workflow in Section 4. Determine whether to remediate, submit an FP, or open an Archer ticket. |
|
||||
|
||||
### Decision Flowchart
|
||||
|
||||
```
|
||||
Finding appears in Reporting page
|
||||
│
|
||||
├── Check the Workflow column
|
||||
│
|
||||
├── No badge (—)
|
||||
│ └── Triage → follow Section 4 workflow
|
||||
│
|
||||
└── Has a badge → check the colour:
|
||||
│
|
||||
├── 🔵 BLUE (Requested)
|
||||
│ └── Monitor. Follow up if SLA window is approaching.
|
||||
│
|
||||
├── 🟡 AMBER (Reworked / Actionable)
|
||||
│ └── Open Ivanti ticket → review feedback → update → resubmit
|
||||
│
|
||||
└── 🔴 RED
|
||||
│
|
||||
├── Expired → Submit a new FP request in Ivanti
|
||||
│
|
||||
└── Rejected → Remediate the vulnerability
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Quick Reference Card
|
||||
|
||||
### Action Decision Matrix
|
||||
|
||||
| Research Outcome | Config Change? | Action Required |
|
||||
|---|---|---|
|
||||
| Can be patched (firmware/software) | N/A | Upgrade device — no ticket needed |
|
||||
| Can be patched (configuration change only) | Yes | Archer Risk Acceptance ticket (EXC-XXXXX) |
|
||||
| False Positive — not applicable to platform/version | N/A | FP workflow in Ivanti + evidence in CVE database |
|
||||
| Cannot be patched — patch pending from vendor | N/A | Archer Risk Acceptance ticket (renew when patched) |
|
||||
| Cannot be patched — EOL/EOS device | N/A | Archer ticket with mitigation steps + remediation plan |
|
||||
| Asset not owned by our BU | N/A | CARD queue → CARD asset disposition process |
|
||||
|
||||
### Workflow Badge Quick Reference
|
||||
|
||||
| Badge | State | One-Line Action |
|
||||
|---|---|---|
|
||||
| 🔴 Red | Expired | Renew FP request in Ivanti |
|
||||
| 🔴 Red | Rejected | Remediate the vulnerability |
|
||||
| 🟡 Amber | Reworked | Update and resubmit FP ticket |
|
||||
| 🟡 Amber | Actionable | Review ticket in Ivanti and respond |
|
||||
| 🔵 Blue | Requested | Monitor — no action yet |
|
||||
| — | No badge | Triage: follow Section 4 |
|
||||
|
||||
### Dashboard Shortcut Reference
|
||||
|
||||
| Task | How |
|
||||
|---|---|
|
||||
| See only untriaged findings | Click **Pending** segment on Action Coverage chart |
|
||||
| See findings due this week | Click a date on the Home page calendar widget |
|
||||
| See all findings for a specific Archer ticket | Click the EXC badge on the Home page CVE row |
|
||||
| Correct a wrong hostname | Click the Host cell inline on the Reporting page |
|
||||
| Save a screenshot or advisory to a CVE | Home page → CVE row → Upload document |
|
||||
| Stage findings for a batch FP/Archer session | Use the Ivanti Queue (checkbox column on Reporting page) |
|
||||
| Filter to a specific vendor or SLA status | Click the filter icon (⊙) on the relevant column header |
|
||||
|
||||
---
|
||||
|
||||
*Related documentation: FP Workflow Submission (Ivanti) · Archer Risk Acceptance Process · CARD Asset Disposition Process · MOP: Workflow Status Colour Codes*
|
||||
154
docs/security/security-remediation-plan.md
Normal file
154
docs/security/security-remediation-plan.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Security Remediation Plan
|
||||
|
||||
Based on the External Data Handling security audit (April 2026). 17 findings total — 0 Critical, 2 High, 6 Medium, 6 Low, 3 Informational. Ordered by priority based on real-world exploitability and effort.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Data Exposure & XSS (High Priority)
|
||||
|
||||
### 1. L-4: Authenticate /uploads static file access
|
||||
**Location:** `server.js:127`
|
||||
**Risk:** Uploaded documents (vulnerability data, compliance files) served without authentication. Anyone with the URL can access them.
|
||||
**Fix:** Replace `express.static('/uploads')` with a route handler that runs `requireAuth(db)` before streaming the file. Use `res.sendFile()` with the validated path.
|
||||
**Effort:** Small — single route change.
|
||||
|
||||
### 2. M-6: Sanitize Mermaid SVG output with DOMPurify
|
||||
**Location:** `frontend/src/components/KnowledgeBaseViewer.js:38`
|
||||
**Risk:** Mermaid renders SVG which is injected via `innerHTML`. If KB content contains malicious markup, this is a stored XSS vector.
|
||||
**Fix:** Install `dompurify`, sanitize the SVG string before assigning to `innerHTML`. Use `DOMPurify.sanitize(svgString, { USE_PROFILES: { svg: true } })`.
|
||||
**Effort:** Small — add dependency, wrap one line.
|
||||
|
||||
### 3. M-4: Strip server file paths from compliance preview response
|
||||
**Location:** `backend/routes/compliance.js:278`
|
||||
**Risk:** Full server-side file path returned to client. Helps attackers map the filesystem.
|
||||
**Fix:** Return only the filename (use `path.basename()`) instead of the full path. Or return a reference ID that maps to the file server-side.
|
||||
**Effort:** Small — one-line change.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Deployment & Setup Hygiene
|
||||
|
||||
### 4. H-2: Add SESSION_SECRET to .env.example and setup-env.sh
|
||||
**Location:** `backend/.env.example`, `backend/setup-env.sh`
|
||||
**Risk:** Fresh deployments fail with no guidance on required env vars.
|
||||
**Fix:** Add `SESSION_SECRET=` to `.env.example` with a comment explaining it should be a random 64+ character string. Add generation logic to `setup-env.sh` (e.g., `openssl rand -hex 32`).
|
||||
**Effort:** Small.
|
||||
|
||||
### 5. I-3: Set user_group on default admin in setup.js
|
||||
**Location:** `backend/setup.js:180`
|
||||
**Risk:** Default admin created without `user_group`, potentially locked out of `requireGroup`-protected routes on fresh install.
|
||||
**Fix:** Set `user_group = 'Admin'` in the INSERT statement for the default admin user.
|
||||
**Effort:** Trivial — one column added to the INSERT.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Error Message Sanitization (Batch)
|
||||
|
||||
### 6. L-2: Sanitize Python parser error messages
|
||||
**Location:** `backend/routes/compliance.js:284`
|
||||
**Risk:** Stack traces and server paths leaked to client when Python parser fails.
|
||||
**Fix:** Catch the error, log the full details server-side, return a generic "Compliance file parsing failed" message to the client.
|
||||
**Effort:** Small.
|
||||
|
||||
### 7. L-3: Sanitize Ivanti API error responses
|
||||
**Location:** `backend/routes/ivantiFpWorkflow.js:393`
|
||||
**Risk:** Raw Ivanti API error body forwarded to client, potentially exposing internal API details.
|
||||
**Fix:** Log the raw error server-side, return a generic "Ivanti API request failed" message to the client.
|
||||
**Effort:** Small.
|
||||
|
||||
### 8. L-6: Remove group name from requireGroup error response
|
||||
**Location:** `backend/middleware/auth.js:60`
|
||||
**Risk:** Error response leaks the user's current group name, which is minor info disclosure.
|
||||
**Fix:** Change the error message from something like "User group 'Viewer' not authorized" to "Insufficient permissions."
|
||||
**Effort:** Trivial.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Security Headers
|
||||
|
||||
### 9. M-1: Add Content-Security-Policy header
|
||||
**Location:** `server.js:107-113`
|
||||
**Risk:** No CSP means no browser-side XSS mitigation layer.
|
||||
**Fix:** Add a CSP header via middleware. Start with a report-only policy to avoid breaking things, then tighten. Suggested baseline:
|
||||
```
|
||||
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'
|
||||
```
|
||||
Note: `'unsafe-inline'` for styles is needed because the app uses inline style objects extensively. Evaluate whether `script-src 'self'` breaks anything (it shouldn't with CRA).
|
||||
**Effort:** Medium — needs testing to ensure nothing breaks.
|
||||
|
||||
### 10. M-2: Add Strict-Transport-Security (HSTS) header
|
||||
**Location:** `server.js:107-113`
|
||||
**Risk:** No HSTS means browsers don't enforce HTTPS on subsequent visits.
|
||||
**Fix:** Add `Strict-Transport-Security: max-age=31536000; includeSubDomains` header. Only apply when running behind HTTPS (check `req.secure` or a trusted proxy header). Do NOT enable if the app is accessed over plain HTTP.
|
||||
**Effort:** Small, but verify deployment is HTTPS-only first.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Operational Maintenance
|
||||
|
||||
### 11. L-5: Add expired session cleanup
|
||||
**Location:** `backend/middleware/auth.js:271`
|
||||
**Risk:** Sessions table grows indefinitely. Not a security exploit, but degrades performance over time.
|
||||
**Fix:** Add a cleanup function that runs on server startup (and optionally on a setInterval) to DELETE sessions where `expires_at < CURRENT_TIMESTAMP`. Run once at boot, then every 6 hours.
|
||||
**Effort:** Small.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Session Signing (Larger Effort)
|
||||
|
||||
### 12. H-1: Use SESSION_SECRET for HMAC-signed session tokens
|
||||
**Location:** `server.js:33`
|
||||
**Risk:** Session tokens are random bytes stored in DB with no signing. An attacker with DB read access can replay any session. For self-hosted SQLite, DB access already implies full compromise, so this is a defense-in-depth measure.
|
||||
**Fix:** When creating a session, generate a random token and store its HMAC (using SESSION_SECRET) in the DB. On validation, recompute the HMAC and compare. This means a DB dump alone isn't enough to forge sessions — the attacker also needs the secret.
|
||||
**Effort:** Medium — touches session creation, validation, and requires SESSION_SECRET to actually be wired in.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Investigate Before Changing
|
||||
|
||||
### 13. M-3: Review application/octet-stream in MIME allowlist
|
||||
**Location:** `server.js:62`
|
||||
**Risk:** Allows uploads that bypass MIME type checking. May be intentional for specific file types.
|
||||
**Action:** Check what file types are uploaded that resolve to `application/octet-stream`. If none are legitimate, remove it from the allowlist. If some are (e.g., `.db` files, binary exports), consider adding those specific MIME types instead.
|
||||
**Effort:** Investigation first, then trivial change.
|
||||
|
||||
### 14. M-5: Evaluate CORS HTTP origin policy
|
||||
**Location:** `server.js:38-40`
|
||||
**Risk:** CORS allows HTTP origins, no HTTPS enforcement.
|
||||
**Action:** Check if production runs behind a reverse proxy with HTTPS termination. If yes, the backend legitimately sees HTTP origins from the proxy. If production traffic is ever plain HTTP end-to-end, restrict CORS to HTTPS origins only.
|
||||
**Effort:** Investigation first, then small config change.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Low Priority / Monitor
|
||||
|
||||
### 15. L-1: Add startup warning for IVANTI_SKIP_TLS=true
|
||||
**Location:** `backend/helpers/ivantiApi.js:28`
|
||||
**Risk:** TLS validation disabled silently. Acceptable in dev, risky if accidentally left on in production.
|
||||
**Fix:** Add a `console.warn('⚠ IVANTI_SKIP_TLS is enabled — TLS certificate validation is disabled')` at startup when the flag is set.
|
||||
**Effort:** Trivial.
|
||||
|
||||
### 16. I-1: Monitor react-scripts version
|
||||
**Location:** `frontend/package.json`
|
||||
**Risk:** Build-time only, not runtime. No immediate action needed.
|
||||
**Action:** Upgrade to latest react-scripts when convenient. Consider migrating to Vite if a major frontend overhaul is planned.
|
||||
|
||||
### 17. I-2: Monitor xlsx dependency
|
||||
**Location:** `frontend/package.json`
|
||||
**Risk:** Community fork, unmaintained since 2022. Used for spreadsheet parsing.
|
||||
**Action:** Monitor for security advisories. If a vulnerability is found, evaluate alternatives (e.g., `exceljs`, `sheetjs` pro). No immediate action needed unless a CVE is published against it.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Items | Effort | Impact |
|
||||
|-------|-------|--------|--------|
|
||||
| 1 — Data Exposure & XSS | L-4, M-6, M-4 | Small | High |
|
||||
| 2 — Deployment Hygiene | H-2, I-3 | Small | Medium |
|
||||
| 3 — Error Sanitization | L-2, L-3, L-6 | Small | Low-Medium |
|
||||
| 4 — Security Headers | M-1, M-2 | Medium | Medium |
|
||||
| 5 — Session Cleanup | L-5 | Small | Low |
|
||||
| 6 — Session Signing | H-1 | Medium | Medium |
|
||||
| 7 — Investigate | M-3, M-5 | Investigation | TBD |
|
||||
| 8 — Monitor | L-1, I-1, I-2 | Trivial | Low |
|
||||
Reference in New Issue
Block a user