Review Security Logs

Su Suriya Ravichandran Updated 10 Nov 2026
6 min read ·Lesson 180 of 200

Evidence That the Defenses Are Working

Every defense in this section was designed to stop something that might happen. Logs show whether it is happening, and how often, and who is responsible. A tool denial that repeats three times in an hour, an injection screen that flags a document, a key that is suddenly used from an unexpected service, or a user whose request volume jumps tenfold: each of these is visible in the logs if the application recorded it, and each is invisible otherwise. Without a review of the logs, the protections from Lessons 171 through 179 operate unobserved, and a failure in them is discovered only when its cost arrives.

This lesson defines what a security log should record, how to keep it useful without exposing the content it describes, how to turn it into alerts that a person will act on, and how to review it on a schedule. It builds on the structured logging of Lesson 147, extending it from errors to security events.


What to Record

Security logs record decisions and the identities behind them, not the content of the work. Each entry answers the questions an investigator asks first: who did what, when, through which tool, and with what outcome.

FieldWhy it is needed
TimeOrdering events and correlating them across services
User or service identityAttributing the event to a person or a workload
Event kindDistinguishing a denial from a success, and a flag from a normal result
Tool or operation nameShowing which capability was involved
OutcomeWhether the call ran, was refused, or was held for confirmation
Request or trace identifierLinking the event to the request and its attempts (Lesson 148)

Content such as prompts, answers, and tool results is left out, following the principle from Lesson 147. When an investigation needs content, it should be retrieved from a separate, access-controlled store under its own policy, not copied into the security log.


Events Worth Recording

Some events matter more than others, and a security log should capture at least these:

EventMeaning
Tool deniedA permission check refused a call (Lesson 176)
Injection flagA screen marked content as suspicious (Lesson 175)
Confirmation requestedA destructive action was held for a person (Lesson 177)
Budget exceededA reservation was refused (Lesson 179)
Authentication failureA credential was rejected (Lesson 141)
Key use from a new sourceA key was used from a service or location that does not normally use it (Lesson 171)

Each of these is a signal, and a signal alone is not a conclusion. A denial is expected when a user tries something they should not, and it is alarming when the same account is denied repeatedly, or denied on many tools at once. Rules that combine events with counts and time windows turn raw signals into something a person can act on.


Alerts From Patterns

An alert should fire on a pattern a person would want to know about, and stay quiet otherwise. Each rule is a count of one event kind for one identity within a time window, compared with a threshold. Rules of this kind are simple to state and easy to tune, and the thresholds should be chosen from observed normal behavior, not from guesses.

The function below applies three such rules to a batch of events. It groups events by identity, kind, and hour, and reports any group that crosses its threshold:

"""security_alerts.py: turn security events into alerts when a pattern crosses a threshold."""

from collections import Counter


def alerts(events: list[dict]) -> list[str]:
    counts = Counter((e["user"], e["hour"], e["kind"]) for e in events)
    found = []
    for (user, hour, kind), n in sorted(counts.items()):
        if kind == "tool_denied" and n >= 3:
            found.append(f"repeated denials: {user} at hour {hour} ({n})")
        if kind == "injection_flag" and n >= 1:
            found.append(f"injection flag: {user} at hour {hour} ({n})")
        if kind == "request" and n >= 50:
            found.append(f"volume spike: {user} at hour {hour} ({n})")
    return found


if __name__ == "__main__":
    events = (
        [{"user": "s-101", "kind": "tool_denied", "hour": 9}] * 3
        + [{"user": "s-101", "kind": "injection_flag", "hour": 10}]
        + [{"user": "s-204", "kind": "request", "hour": 9}] * 5
        + [{"user": "s-204", "kind": "request", "hour": 11}] * 60
    )
    for line in alerts(events):
        print(line)

Output:

repeated denials: s-101 at hour 9 (3)
injection flag: s-101 at hour 10 (1)
volume spike: s-204 at hour 11 (60)

How It Works

  1. Events are counted by identity, kind, and hour. The counter collapses many events into one count per group, which is the form a threshold needs.
  2. The denial rule fires at three in one hour, so a single refusal does not alert, but three refusals in an hour from one account do. The first identity produces that alert at hour nine.
  3. The injection rule fires on any single flag. A flagged document is unusual enough that one occurrence justifies a look, while the denial rule needs repetition.
  4. The volume rule needs fifty requests in one hour. The five requests at hour nine stay below it, and the sixty at hour eleven cross it, which is the pattern worth checking.
  5. The output is sorted by identity and hour, so the alerts read in the order they happened for each account, which is how a reviewer follows them.

The thresholds in this example are illustrations. A real application sets them from its own normal traffic, and revisits them as usage grows.


Keeping Logs Safe

The security log is itself sensitive. It reveals who uses the application, what they tried, and where the defenses fired, which is useful information for an attacker. Access should be restricted to the people who review the logs, retention should be limited to what investigations need, and the log should never contain secrets or content. Lesson 147 covered these points for error logs, and they apply with more force here.

A useful discipline is to log the decision and not the reasoning behind it, when the reasoning could help an attacker tune their approach. A refusal should be recorded as a refusal, with its rule name, and the full list of checks that failed does not need to be exposed in every log line.


Reviewing on a Schedule

Alerts catch patterns that were anticipated. Review catches the rest. A scheduled review, weekly for most applications and daily for high-risk ones, looks at the summary of events, compares the counts with the previous period, and examines any change in the proportions: more denials, more flags, new tools in use, or new sources for a key. The review should end with a recorded decision, even when the decision is to take no action, so that the log of reviews is itself evidence that the logs are read.

Reviews also feed back into the defenses. A pattern of injection flags from one source suggests tightening the handling of that source (Lesson 175). A cluster of denials for one tool suggests that its permissions or its description need attention (Lesson 176).


Summary

Security logs record who did what, when, through which tool, and with what outcome, without recording the content of the work. Certain events, such as denials, injection flags, held confirmations, refused budgets, and unexpected key use, are the signals that matter. Alerts fire when counts cross thresholds within time windows, and the thresholds come from observed behavior. The logs are sensitive in their own right, and they are reviewed on a schedule, with each review leading to a recorded decision. This completes the security section, and the next section covers testing and deployment.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Review Security Logs and get answers drawn from it.

Signed-in readers only.