
Auditing Log Coverage: Finding the Events You Are Not Recording
Introduction
Logging is not the same as having the right logs. Most applications write something—error stacks, request traces, maybe a few console.log calls that survived code review. But when a security incident lands on your desk, the question is never “do you have logs.” It is “do you have the log that shows what actually happened.”
Missing events are invisible until you need them. I have walked into too many codebases that log every incoming request but silently swallow failed authentication, deny decisions, or privilege escalations. That gap turns a one‑hour investigation into a three‑day guessing game.
What Does “Log Coverage” Mean?
Log coverage is the set of security‑relevant events your application actually records. It is not about log volume. It is about whether critical signals—failed logins, access‑denied responses, permission changes, sensitive data reads—reach your log pipeline at all.
A decent monitoring setup can still have zero coverage for authorization abuse if the only thing you log is GET /dashboard 200.
The Systematic Audit Approach
You cannot fix coverage by staring at dashboards. You need a structured walk through the application.
Define Your Event Inventory
Start with a short, concrete list of events a defender or incident responder would kill to have. Not everything—focus on high‑signal security events:
- Successful and failed authentication
- Account lockout / brute‑force signals
- MFA enrollment or bypass
- Privilege escalation or role changes
- Access to sensitive resources (admin panels, billing, user PII)
- Dangerous configuration changes (CORS, CSP, feature flags)
- Data export or bulk download
Write these down in a spreadsheet. This is your target.
Map Code Paths to Events
Now pick each event and trace the real code paths that trigger it. Do not assume a path is logged because one branch is. I usually grep for log, logger, or console near authentication middleware, authorization checks, and API handlers.
// routes/auth.js – looks complete, right?
async function login(req, res) {
const user = await findUser(req.body.email);
if (!user) {
// ❌ no log here – attacker probes valid emails silently
return res.status(401).json({ error: 'Invalid credentials' });
}
const valid = await bcrypt.compare(req.body.password, user.hash);
if (!valid) {
audit.log('LOGIN_FAILED', { email: req.body.email }); // ✅ logged
return res.status(401).json({ error: 'Invalid credentials' });
}
// ... session creation, logs success
}
Don't rely on code review alone. Static analysis with pattern matching (grep for 401, 403, audit) catches many gaps that eyes skip.
Collect Evidence
For each event in your inventory, record one real log entry—or a clear absent location. Evidence can be a sample from a test environment, a code snippet, or a test that proves the log fires. If you cannot produce the log, you have a gap.
Practical Walkthrough: Auditing a Login Flow
Let's ground this in a concrete example.
Expected Log Events for Login
A login flow should produce at minimum:
LOGIN_SUCCESS– with user ID, IP, maybe user agentLOGIN_FAILED– with username/email, failure reason (bad password, no user, locked) -ACCOUNT_LOCKED– when a threshold is hit -MFA_CHALLENGE_ISSUEDandMFA_FAILEDif applicable
Checking the Actual Code/Logs
I reviewed a Node.js/Express backend that had a single audit.info call inside the catch‑all error handler. The login route itself had no logging for the case where the user does not exist—only a generic 401 response. In production logs, I could see requests but could not distinguish between “invalid email” and “wrong password” without additional tooling.
Gaps Found
The gaps:
- No log for unknown user – allowed user enumeration detection to go blind
- No differentiation of failure types – password vs. locked vs. MFA timeout all looked like a simple 401
- No log for successful login after password reset – an important signal for account takeover
Those three gaps removed the ability to spot credential‑stuffing patterns and made incident triage rely on network‑level heuristics.
Analyzing and Prioritizing Gaps
Not every missing log is an emergency. I rank gaps by:
- Attacker value: can an attacker use the lack of visibility to hide? (e.g., unlogged privilege escalation)
- Incident response need: would a responder immediately ask for this event?
- Regulatory / compliance weight: PCI, SOC2, and others require specific audit trails.
Authentication failures nearly always land at the top. Unlogged access‑denied decisions are a close second.
Closing the Gaps
Add the missing log calls directly into the code. Keep the format consistent:
- structured JSON
- mandatory fields: timestamp, event type, actor, outcome
- correlation ID for tracing the session
Write a quick integration test that asserts the log appears for every significant branch. This prevents regressions when someone refactors the login flow later.
One test per log event is lightweight and pays off quickly. I drop these into the same test suite that already exercises authentication.
Further Reading
- OWASP Logging Cheat Sheet – baseline guidance on what to log and why
- Application Logging Vocabulary (OWASP) – standard field names that make correlation easier across services


