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, persistent audit logging, and machine-readable manifest output.
/admin/security with score visualization, category breakdowns, and expandable check detailsdoh audit for terminal and CI/CD integration.doh/manifests/security_audit.json for passive consumption by other CLI tools, AI agents, and automationAdd to your boot.pod.yaml:
host_load:
- security_audit
doh audit # Run all checks, see results, write manifest
That's it. Results are printed to the terminal and saved to .doh/manifests/security_audit.json.
doh audit # Run all checks
doh audit run # Explicit run subcommand
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 and pre-deploy gates.
Navigate to /admin/security. Users must be authenticated and have the access:security_audit permission.
The dashboard provides:
Every audit run (CLI, server API, or programmatic) automatically writes results to:
.doh/manifests/security_audit.json
This file is written on Node.js only (guarded by IsNode()). It follows the same format as the API response and can be read by any tool without triggering an audit. The file is overwritten on each run.
Example use cases:
Schema:
{
"score": 85,
"grade": "B",
"totalChecks": 26,
"passed": 20,
"warned": 4,
"failed": 2,
"skipped": 0,
"timestamp": 1707580800000,
"categories": {
"auth": { "name": "auth", "passed": 8, "warned": 1, "failed": 1, "skipped": 0, "total": 10 }
},
"checks": [
{
"id": "permissions.admin_routes_protected",
"category": "permissions",
"title": "Admin Routes Protection",
"description": "Checks that admin routes require authentication and permissions",
"severity": "critical",
"hasAutofix": false,
"status": "fail",
"message": "2 admin route(s) may lack authentication",
"details": [
{ "file": "doh_modules/my_app/my_app_server.doh.js", "line": 14, "message": "AddRoute('/admin/settings', ..." },
{ "file": "doh_modules/my_app/my_app_server.doh.js", "line": 28, "message": "AddRoute('/api/admin/config', ..." }
],
"remediation": "Add [Users.requireAuth] middleware to all admin routes",
"duration": 45
},
{
"id": "server.helmet_enabled",
"category": "server",
"title": "Helmet Security Headers",
"description": "Checks that Helmet is enabled for security headers",
"severity": "high",
"hasAutofix": false,
"status": "pass",
"message": "Helmet is enabled",
"details": [],
"duration": 1
}
]
}
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() |
permissions.require_auth_without_permit |
Critical | Authenticated routes also enforce authorization via Doh.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 from scoring.
| Grade | Score |
|---|---|
| A | 90-100 |
| B | 80-89 |
| C | 70-79 |
| D | 60-69 |
| F | < 60 |
Any Doh module can register additional security checks by depending on security_audit_engine:
Doh.Module('my_custom_checks', ['security_audit_engine'], function(SecurityAudit) {
SecurityAudit.registerCheck('myapp.api_key_rotation', {
category: 'auth',
title: 'API Key Rotation',
description: 'Checks that API keys are rotated within 90 days',
severity: 'high',
async run() {
const keyAge = getApiKeyAgeDays();
if (keyAge > 90) {
return {
status: 'fail',
message: `API key is ${keyAge} days old (max 90)`,
remediation: 'Rotate API keys via the admin dashboard'
};
}
if (keyAge > 60) {
return {
status: 'warn',
message: `API key is ${keyAge} days old (rotation recommended at 60)`
};
}
return { status: 'pass', message: `API key is ${keyAge} days old` };
},
// Optional: provide an automated fix
async autofix() {
await rotateApiKey();
return 'API key rotated successfully';
}
});
});
Check definition fields:
| Field | Required | Description |
|---|---|---|
category |
Yes | Category name (e.g., 'auth', 'server', or a custom one) |
title |
Yes | Human-readable title for display |
description |
Yes | What the check inspects |
severity |
Yes | 'critical' | 'high' | 'medium' | 'low' | 'info' |
run |
Yes | Async function returning { status, message, details?, remediation? } |
autofix |
No | Async function to auto-fix the issue |
Run return values:
| Field | Required | Description |
|---|---|---|
status |
Yes | 'pass' | 'warn' | 'fail' | 'skip' |
message |
Yes | Human-readable result message |
details |
No | Array of finding objects (normalized to [] if omitted). Each object has message (string) and optionally file (string), line (number), and check-specific fields. |
remediation |
No | How to fix the issue |
| 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 |
const resp = await Doh.ajaxPromise('/api/admin/security/run', {});
// resp.data.results — full results object
// resp.data.results.score — numeric score (0-100)
// resp.data.results.grade — letter grade (A-F)
// Filter by category
const resp = await Doh.ajaxPromise('/api/admin/security/run', { category: 'auth' });
const resp = await Doh.ajaxPromise('/api/admin/security/fix', { checkId: 'auth.jwt_secret_strength' });
// resp.data.success — boolean
// resp.data.message — result message
const resp = await Doh.ajaxPromise('/api/admin/security/log?limit=50&offset=0');
// resp.data.entries — array of log entries
const stats = await Doh.ajaxPromise('/api/admin/security/log/stats');
// stats.data.stats.total — total entries
// stats.data.stats.byType — { audit_run: 5, audit_fix: 2, ... }
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 |
const { SecurityAuditLogger } = await Doh.load('security_audit_logger');
// 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_audit/
security_audit.doh.js # Package definition, pod config, CLI registration
security_audit_engine.doh.js # Check registry, runner, scoring, manifest output
security_audit_checks.doh.js # All 26 built-in checks (AST analysis, file scanning, pod inspection)
security_audit_logger.doh.js # Persistent audit event logging via Dataforge
security_audit_server.doh.js # Express routes and permission definitions
security_audit_browser.doh.js # Browser auto-mount on /admin/security
security_audit_ui.doh.js # Dashboard patterns (score circle, category cards, check rows)
security_audit.css # Dashboard styles
dohball.json # Package version
Environment loading:
| Module | Browser | Node |
|---|---|---|
security_audit_engine |
Yes | Yes |
security_audit_checks |
- | Yes |
security_audit_logger |
- | Yes |
security_audit_server |
- | Yes |
security_audit_browser |
Yes | - |
security_audit_ui |
Yes | - |
security_auditsecurity_audit (assignable)access:security_audit — View audit results and logsmanage:security_audit — Apply autofixesThe module includes a server-side test suite at tests/security_audit/:
# Run via the Doh test runner
doh test test_security_audit
Tests cover:
express_router — Route handlinguser_host — Authentication middlewaredataforge — Audit log persistenceacorn / acorn-walk — AST analysis for code-level checks (Node.js only)