Auto-populate description field when creating Jira tickets from the Archer page with ticket metadata (EXC number, CVE, vendor, status, Archer URL). Previously the description was always empty, requiring manual entry. Includes security audit fixes for SQL injection prevention and input validation in compliance, VCL multi-vertical, and CCP metrics routes. Updates security audit tracker documentation.
17 KiB
Security Audit Tracker — STEAM Security Dashboard
Last scan: 2026-06-04
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
- New Findings — April 20 Scan
- New Findings — June 4 Scan
- Open Finding Summary
- Positive Security Observations
- 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 | Fixed | ivantiTodoQueue.js:11-14 — isValidVendor() now trims before length check |
| M-7 | Unsanitized original filename in temp JSON | Open | compliance.js:347 — req.file.originalname.replace(/[^\w.\-() ]/g, '_') sanitizes in temp JSON, but line 355 returns raw req.file.originalname to client |
| 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 App.js, UserManagement.js, KnowledgeBasePage.js, JiraPage.js, and AdminPage.js |
| M-10 | User data in window.confirm dialogs | Partial | App.js replaced with ConfirmModal. JiraPage.js:430 still uses window.confirm('Delete this Jira ticket record?') — static string, reduced risk |
Low / Info Findings
| ID | Title | Status | Evidence |
|---|---|---|---|
| L-1 | Silent ROLLBACK on transaction failure | Fixed | compliance.js — no .catch(() => {}) patterns remain; ROLLBACK is followed by throw err |
| 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 in 5 locations |
| 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 — 30+ instances across frontend |
| L-6 | localStorage column config lacks structural validation | Open | ReportingPage.js:65-80 — parses JSON with try/catch but validates only Array.isArray(saved), not element structure |
Remediation Plan Items (not in original 31)
| ID | Title | Status | Evidence |
|---|---|---|---|
| RP-1 | Authenticate /uploads static file access | Fixed | server.js:127 — requireAuth() middleware applied before express.static('uploads') |
| 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 | Fixed | compliance.js and vclMultiVertical.js — preview returns only the temp filename; commit handler reconstructs full path via path.join(TEMP_DIR, path.basename(tempFile)) |
| RP-4 | Add SESSION_SECRET to .env.example | Fixed | .env.example — SESSION_SECRET= with generation comment present |
New Findings — April 20 Scan
Findings discovered in the April 20 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:
logAudit({
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)
Status: Fixed — file removed from codebase (confirmed 2026-06-04)
N-3 — Compliance Preview Returns Full Server Filesystem Path (Medium) — FIXED
Status: Fixed — preview now returns only the temp filename (tempFilename). The commit handler reconstructs the full path server-side via path.join(TEMP_DIR, path.basename(tempFile)). Applied to both compliance.js and vclMultiVertical.js.
N-4 — /uploads Static Directory Served Without Authentication (High) — FIXED
Status: Fixed — requireAuth() middleware is now applied before express.static('uploads') in server.js. All file access now requires a valid session cookie.
N-5 — Mermaid SVG Rendered via innerHTML Without Sanitization (Medium)
File: frontend/src/components/KnowledgeBaseViewer.js:38
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:
import DOMPurify from 'dompurify';
ref.current.innerHTML = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true } });
N-6 — SESSION_SECRET Not Documented in .env.example (Low)
Status: Fixed — .env.example now includes SESSION_SECRET= with generation instructions (confirmed 2026-06-04)
N-7 — requireGroup Error Response Leaks Current User Group (Low)
File: backend/middleware/auth.js:55-60
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:
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:
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:
setInterval(() => {
pool.query("DELETE FROM sessions WHERE expires_at < NOW()");
}, 6 * 60 * 60 * 1000); // every 6 hours
New Findings — June 4 Scan
Findings discovered in the June 4, 2026 scan.
N-10 — Feedback Route Uses Incorrect TLS Option Name (Low)
File: backend/routes/feedback.js:92, 221
const reqOpts = {
// ...
rejectAuthorized: false,
};
The option is spelled rejectAuthorized instead of the correct rejectUnauthorized. This means the option is silently ignored by Node's https module — TLS verification remains enabled (the secure default), so this is not a vulnerability in itself. However, it indicates the developer intended to disable TLS verification (likely for Charter's SSL inspection proxy), and the code does not achieve its intent. If the GitLab instance uses an internal CA certificate, feedback submissions will fail with a TLS error when the proxy is active.
Impact: Feedback submissions may fail behind the SSL inspection proxy due to unintended TLS verification. Not a security vulnerability — TLS remains enforced.
Fix: If TLS skip is intentional, use the correct option name and gate it behind an env var:
rejectUnauthorized: process.env.GITLAB_SKIP_TLS === 'true' ? false : true,
If TLS skip is not intentional, remove the incorrect option entirely.
N-11 — window.confirm Persists in JiraPage.js (Low)
File: frontend/src/components/pages/JiraPage.js:430
if (!window.confirm('Delete this Jira ticket record?')) return;
The main App.js was migrated to use ConfirmModal (a themed replacement for window.confirm), but JiraPage.js still uses the raw browser confirm() dialog. While the confirmation string is static (not user-supplied data), this is inconsistent with the security pattern established elsewhere in the codebase.
Impact: Low — the string is static so there is no XSS vector. This is a consistency issue rather than a vulnerability.
Fix: Replace with the ConfirmModal pattern used in App.js:
setPendingConfirm({ message: 'Delete this Jira ticket record?', onConfirm: () => doDelete(id) });
Open Finding Summary
Prioritised list of all open findings requiring action.
High Priority
| ID | Severity | Title | Source |
|---|---|---|---|
| — | — | (none — all High findings resolved) | — |
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 |
April 20 |
| N-5 / RP-2 | Medium | Mermaid SVG rendered via innerHTML without sanitization |
April 20 |
| N-8 | Medium | No Content-Security-Policy header on main application | April 20 |
| M-7 | Medium | Unsanitized original filename returned to client in preview response | April 1 |
| M-9 | Medium | API error messages forwarded to UI via alert() |
April 1 |
Low Priority
| ID | Severity | Title | Source |
|---|---|---|---|
| M-10 | Low | User data in window.confirm dialogs (partially fixed — JiraPage only) |
April 1 |
| N-7 | Low | requireGroup error response leaks current user group |
April 20 |
| N-9 | Low | Expired sessions not cleaned up automatically | April 20 |
| N-10 | Low | Feedback route uses incorrect TLS option name | June 4 |
| N-11 | Low | window.confirm persists in JiraPage.js |
June 4 |
| 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 (PostgreSQL
$1placeholders) - Path traversal prevention —
sanitizePathSegment()andisPathWithinUploads()consistently applied inserver.js,compliance.js, andknowledgeBase.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; password change limited to 5 per 15 minutes
- 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.jsonextension anduploads/temp/directory - Static file serving —
dotfiles: 'deny'andindex: falseprevent directory listing - Webhook authentication — GitLab webhook validates
x-gitlab-tokenagainstGITLAB_WEBHOOK_SECRETenv var - SESSION_SECRET enforcement — server hard-fails on startup if
SESSION_SECRETis not set - Input validation coverage — CVE ID, vendor, hostname, metric_id, EXC number, and workflow_type all validated with regex or enum checks
- Error response discipline — backend routes consistently return
'Internal server error.'for 500s, avoiding stack trace leaks
Scan Metadata
| Field | Value |
|---|---|
| Scan date | 2026-06-04 |
| Scan type | Full repository static analysis |
| Scope | backend/server.js, backend/routes/, backend/middleware/, backend/helpers/, backend/scripts/, backend/migrations/, frontend/src/, .env.example |
| Baseline | docs/security-audit-2026-04-01.md |
| Previous findings | 31 (6 Critical, 9 High, 10 Medium, 6 Low/Info) + 9 new (April 20) + 4 remediation plan items |
| Fixed since last scan | 5 (N-2, N-6, M-6, RP-4, L-1) |
| Downgraded | 1 (M-10: Medium → Low, partially fixed) |
| Still open (from baseline) | 10 |
| Still open (from April 20) | 7 |
| New findings (June 4) | 2 |
| Total open | 18 (1 High, 8 Medium, 9 Low) |
| Regressions | 0 |
| Methodology | Static analysis — code review of all route handlers, middleware, helpers, and frontend components |