A comprehensive security scanning and auditing system for Doh applications. Evaluates application security posture across authentication, server configuration, database, input validation, and permissions — with a web dashboard, CLI tool, and persistent audit logging.
/admin/security with score visualization, category breakdowns, and expandable check detailsdoh audit for terminal and CI/CD integrationAdd to your boot.pod.yaml:
host_load:
- security_audit
Navigate to /admin/security to access the dashboard. Users must be authenticated and have the access:security_audit permission.
doh audit # Run all checks
doh audit run # Explicit run
doh audit --category auth # Run only auth checks
doh audit --json # JSON output for CI/CD
doh audit --fix # Run checks then apply autofixes
doh audit log # Show recent audit log
doh audit log --limit 100 # Show 100 recent entries
doh audit --help # Show help
The CLI exits with code 1 if any checks fail, making it suitable for CI/CD pipelines.
Assign the security_audit permission group to users who should access the audit system:
await Doh.assignPermissionGroup(user, 'security_audit');
This grants both access:security_audit (view results and logs) and manage:security_audit (apply autofixes).
| Check | Severity | Description |
|---|---|---|
auth.jwt_secret_strength |
Critical | JWT secret length (64+ chars) and entropy (3.5+) |
auth.jwt_secrets_differ |
Critical | JWT and refresh secrets are different values |
auth.token_expiration |
Medium | Access token ≤7d, refresh token ≤90d |
auth.secure_cookie_flags |
High | Cookies set secure, httpOnly, sameSite |
auth.password_policy |
Medium | Min length ≥8, requires letter + number + special |
auth.bcrypt_rounds |
Medium | Bcrypt rounds ≥10 |
auth.login_rate_limit |
High | Max login attempts ≤10, lockout ≥60s |
auth.password_reset_rate_limit |
High | Password reset routes have rate limiting |
auth.oauth_xss_intermediate |
Critical | No unescaped template literals in OAuth redirect scripts |
auth.oauth_token_storage |
High | Tokens not written to localStorage via inline script |
| Check | Severity | Description |
|---|---|---|
server.helmet_enabled |
High | Helmet middleware enabled |
server.csp_configured |
High | Content Security Policy not disabled |
server.cors_not_wildcard |
High | CORS hosts configured, no wildcard * |
server.forbidden_paths |
High | Sensitive paths (.env, .git, pod.yaml) blocked |
server.body_size_limit |
Medium | Request body limit configured and ≤50MB |
server.rate_limit_enabled |
High | Global rate limiting configured |
server.debug_mode |
Medium | Debug flags (auth_debug, debug_mode) disabled |
| Check | Severity | Description |
|---|---|---|
database.parameterized_queries |
High | No SQL injection via template literals or string concat |
database.db_file_permissions |
Medium | Database files not world-readable (Unix only) |
database.idea_table_id_sanitization |
Medium | Client input sanitized before use as Idea table IDs |
| Check | Severity | Description |
|---|---|---|
input.username_sanitization |
High | Username sanitizer includes SanitizeEmail |
input.password_sanitization |
High | Password sanitizer includes SanitizePassword |
input.xss_in_templates |
High | No unescaped user data in res.send, SendContent, or inline scripts |
input.eval_usage |
Critical | No eval() or new Function() in application code |
| Check | Severity | Description |
|---|---|---|
permissions.admin_routes_protected |
Critical | Admin routes require requireAuth or permit() |
Each check has a severity-based weight:
| Severity | Weight |
|---|---|
| Critical | 10 |
| High | 5 |
| Medium | 3 |
| Low | 1 |
| Info | 0 |
Check results map to scores: pass = full weight, warn = 50% weight, fail = 0, skip = excluded.
| Grade | Score |
|---|---|
| A | 90–100 |
| B | 80–89 |
| C | 70–79 |
| D | 60–69 |
| F | < 60 |
| Route | Method | Permission | Description |
|---|---|---|---|
/admin/security |
GET | access:security_audit |
Dashboard page |
/api/admin/security/run |
POST | access:security_audit |
Execute audit checks |
/api/admin/security/results |
GET | access:security_audit |
Get cached results |
/api/admin/security/fix |
POST | manage:security_audit |
Apply autofix for a check |
/api/admin/security/log |
GET | access:security_audit |
Get paginated log entries |
/api/admin/security/log/stats |
GET | access:security_audit |
Get log statistics |
Security events are persisted to a Dataforge Idea table (security.audit_log) with 90-day retention (configurable).
| Event | Source | Description |
|---|---|---|
audit_run |
Server / CLI | Audit was executed |
audit_fix |
Server | Autofix was applied |
// Log an event
await SecurityAuditLogger.log('audit_run', 'username', { score: 85, grade: 'B' });
// Get recent entries (newest first)
const entries = await SecurityAuditLogger.getRecent(50, 0);
// Get aggregate stats by event type
const stats = await SecurityAuditLogger.getStats();
# boot.pod.yaml or pod.yaml
security_audit:
enabled: true # Enable/disable the module (default: true)
log_retention_days: 90 # How long to keep audit log entries (default: 90)
The module also inspects these pod settings during checks:
express_config — helmet, CSP, CORS, rate limiting, body limit, ignored pathsUsers — JWT secrets, token expiration, password policy, login rate limiting, sanitizerssecurity_auditsecurity_audit (assignable)access:security_audit — View audit results and logsmanage:security_audit — Apply autofixesexpress_router — Route handlinguser_host — Authentication middlewaredataforge — Audit log persistence