Skip to content

Detecting User Activity Anomalies Using Event-Code Vector Baselines on Windows Hosts

Monitoring user activity on Windows endpoints often focuses on high-risk events, privilege changes, or specific attack techniques. However, one of the most powerful early-warning signals comes from something much simpler: the distribution of incoming Windows Event Codes.

Instead of monitoring individual events in isolation, we can learn a behavioral baseline for each host by analyzing how often different Event Codes appear within a one-hour window. This method groups events into a high-dimensional vector (e.g., size 20,000), where each dimension corresponds to a specific event code. Every hour produces one such vector, representing the "shape" of normal activity for that host.

Key Concepts

Before diving into the details, let's define the essential terms used in this detection method:

Event Code

A numeric identifier assigned to each type of Windows event. For example:

  • 4624 = Successful account logon
  • 4625 = Failed account logon
  • 4688 = A new process has been created
  • 4720 = A user account was created

Every Windows security event has an Event Code that categorizes what type of activity occurred.

Baseline

A learned statistical profile of "normal" behavior. In this context, a baseline captures how a specific host typically behaves during a particular time period (e.g., Monday at 10 AM on workdays). The baseline stores the average behavior (mean) and the typical variation (standard deviation) for comparison with current activity.

Vector

A mathematical structure that contains multiple values in an ordered list. In this detection method:

  • Each position (dimension) in the vector represents a specific Event Code
  • The value at each position is how many times that Event Code appeared in a one-hour window
  • Example: If position 4624 has a value of 150, it means Event Code 4624 occurred 150 times in that hour

Think of it as a fingerprint that describes the hour's activity across all possible Event Codes.

Vector Norm

A single number that summarizes the overall "size" or "magnitude" of a vector. Mathematically, it's the Euclidean distance from the origin to the point represented by the vector. For our purposes:

  • A high vector norm means lots of events occurred in that hour
  • A low vector norm means few events occurred
  • The specific pattern of which Event Codes contributed to that norm is what makes each hour unique

Sigma (σ) / Standard Deviation

A statistical measure of how much values typically vary from the average. In our context:

  • Low sigma = Values are usually close to the average (consistent behavior)
  • High sigma = Values vary widely from the average (inconsistent behavior)

When we say "5 sigma," we mean the current value is 5 standard deviations away from the average—an extremely rare occurrence if behavior is normal.

Z-Score

A standardized measure of how unusual a value is compared to the baseline. It's calculated as:

Z-Score = |Current Value - Average Value| / Standard Deviation
  • Z-Score of 0 = Perfectly normal (exactly at the average)
  • Z-Score of 1-2 = Slightly unusual but common
  • Z-Score of 3 = Rare (occurs ~0.3% of the time in normal distributions)
  • Z-Score of 5 = Extremely rare (occurs ~0.00006% of the time)

In this detection method, Z-Score and Sigma are used interchangeably—they both describe how many standard deviations away from normal the current behavior is.

Temporal Classes

Categories that group similar time periods together because they have similar behavior patterns:

  • Workdays: Monday through Friday when normal business activity occurs
  • Weekends: Saturday and Sunday when activity is typically reduced
  • Holidays: Special days that may have unique patterns (minimal activity or maintenance windows)

By learning separate baselines for each class, the system understands that low activity on Sunday is normal, but the same low activity on Wednesday would be suspicious.

Aggregation

The process of grouping and summarizing individual events. In this method:

  • Events are grouped by host.id (which computer they came from)
  • Within each host, events are counted by event.code (what type of event)
  • Counts are bucketed into one-hour time windows

This aggregation transforms thousands of raw events into a single vector per hour per host.

Simple Example

To illustrate these concepts with concrete numbers:

Scenario: Host WORKSTATION-01 on Monday at 10 AM

Raw Events (simplified):

Event Code 4624 occurred 120 times (successful logons)
Event Code 4688 occurred 45 times (process creations)
Event Code 5156 occurred 2,340 times (network connections)

Vector Representation:

[0, 0, 0, ..., 120, ..., 45, ..., 2340, ..., 0, 0]
 ^              ^         ^        ^
 Position 0    Pos 4624  Pos 4688 Pos 5156

Vector Norm (simplified calculation): √(120² + 45² + 2340²) ≈ 2,343

Baseline (learned from previous Mondays at 10 AM): - Average vector norm: 2,200 - Standard deviation: 100

Current Z-Score Calculation:

Z-Score = |2,343 - 2,200| / 100 = 1.43

Result: Z-Score of 1.43 is normal (below the threshold of 5), so no alert is triggered. This slight variation is within expected behavior for this host at this time.

Now imagine the same host suddenly shows 8,500 network connections instead of the usual 2,340—that dramatic spike would push the Z-Score above 5, triggering an anomaly alert.

Learning the Normal "Vector Shape"

The baseliner collects data over several days and across typical behavioral classes—workdays, weekends, and holidays. Once enough samples are captured (in our case, four valid samples per class), the system learns what a normal hourly vector looks like. The output of this learning phase is a single scalar value: the vector norm. This norm acts as the host's signature for that temporal context.

Turning Deviations Into Sigma

When new activity arrives, the system recomputes the vector norm for the current hour and compares it against the historical mean and standard deviation for that same hour and class. The deviation is expressed as a Z-score:

Z = |CURRENT_VALUE – MEAN_VALUE| / STDEV

This is often called the sigma score. High sigma values indicate that the current pattern of Event Codes is fundamentally different from what the host usually produces.

For example, a spike in logon events, an unusual sequence of service-related codes, or a sudden drop in expected operational noise can all shift the vector norm far enough to produce a high Z-score. When the sigma exceeds a configured threshold—typically 5 or more—the system treats it as a meaningful anomaly.

From Baseline to Detection

This approach is not intended as a high-precision alerting mechanism on its own. Instead, it acts as a behavioral signal for larger correlation rules. If a host shows an abrupt deviation in its event-code vector, this "behavior-anomaly" event can feed into other detection logic and highlight compromised machines or abnormal user actions.

By using vector-based baselining rather than fixed thresholds or individual event filters, this method adapts naturally to noisy enterprise environments. It captures subtle behavioral shifts that would otherwise remain invisible—while staying lightweight enough to operate at scale.

Example Configuration

Below is a complete example of an Event-Code Vector Baseline configuration with detailed explanations of each section.

---
define:
  name: Event Codes Per Host  #(1)
  description: Creates baseline for host and triggers an alert if number of events within a 1-hour window shows an anomaly from the learned baseline based on event codes  #(2)
  type: baseliner  #(3)
  risk_score: 30.0  #(4)

baseline:
  region: Czech Republic  #(5)
  period: day  #(6)
  learning: 4  #(7)
  classes: [workdays, weekends, holidays]  #(8)
  aggregation: vector  #(9)
  vector_size: 20000  #(10)
  weights:  #(11)
    "4624": 0.5  # Reduce weight of successful logons
    "4634": 0.5  # Reduce weight of logoffs
    "5156": 0.3  # Reduce weight of Windows Filtering Platform connections
    "4688": 2.0  # Increase weight of process creation events

# Event Codes baseline is meant for analysis and as input for correlations,
# not an entry to alert management
signal:
  default: false  #(12)

logsource:
  vendor:
    - microsoft  #(13)
  product:
    - windows  #(14)

predicate:
  !AND
  - !IN  #(15)
    what: event.code
    where: !EVENT
  - !IN  #(16)
    what: host.id
    where: !EVENT

evaluate:
  key: host.id  #(17)
  aggregate_by: event.code  #(18)
  timestamp: "@timestamp"  #(19)

analyze:
  test:
    !GT  #(20)
    - !ARG SIGMA  #(21)
    - 5  #(22)

trigger:
  - event:  #(23)
      host.id: !ITEM EVENT dimension

      # Event description
      event.action: "behavior-anomaly"  #(24)
      event.reason: "Activity from a host that deviates from the learned vector baseline."  #(25)
      event.kind: "alert"  #(26)
      event.type: "indicator"  #(27)
  1. Name: A descriptive name for the baseline rule that appears in the LogMan.io interface.

  2. Description: Explains what the baseline does - creates a learned baseline per host and triggers alerts when the event pattern deviates significantly from normal behavior.

  3. Type: Set to baseliner to indicate this is a baseline-based detection rule rather than a correlation or other rule type.

  4. Risk Score: Assigns a risk score of 30.0 to anomalies detected by this rule. This score can be used for prioritization and correlation with other detections.

  5. Region: Specifies the timezone/region for temporal classification (e.g., "Czech Republic" determines local time for workdays/weekends).

  6. Period: Defines the temporal granularity for baseline calculation. Set to day means the system tracks hourly patterns across each day of the week.

  7. Learning: The number of valid samples required per class before the baseline is considered "learned". With learning: 4, the system needs 4 samples for each hour of each class (workday/weekend/holiday).

  8. Classes: Behavioral categories that have different normal patterns. workdays, weekends, and holidays allow the system to learn different baselines for different types of days, as user activity varies significantly between them.

  9. Aggregation: Set to vector to perform vector-based analysis. This creates a high-dimensional vector where each dimension represents a different event code frequency.

  10. Vector Size: Defines the maximum dimensionality of the vector (20,000 dimensions). This must be large enough to accommodate all expected Event Code values.

  11. Weights (Optional): A dictionary that allows you to apply custom weights to specific Event Codes in the vector. This is useful for:

    • Reducing weight of very frequent, low-value events (e.g., 4624 successful logons, 5156 network connections) that might add noise
    • Increasing weight of high-value events (e.g., 4688 process creation, 4672 special privileges assigned) that are strong indicators of anomalous behavior
    • Each entry maps an Event Code (as a string) to a multiplier value (float). Values < 1.0 reduce importance, values > 1.0 increase importance.
    • If an Event Code is not listed in weights, it defaults to a weight of 1.0 (no modification).
  12. Default Signal: Set to false because this baseline is meant for analysis and correlation input, not as a direct alert. This prevents automatic signal creation while still allowing the anomaly to be used in other detection rules.

  13. Vendor: Filters events to only include logs from microsoft vendors. This ensures we only analyze Windows events.

  14. Product: Further filters to only windows products, ensuring we're analyzing Windows Event Logs specifically.

  15. Event Code Predicate: The !IN check verifies that the event.code field exists in the event. This ensures we only process events that have an Event Code.

  16. Host ID Predicate: The second !IN check verifies that the host.id field exists. This is crucial because we're building per-host baselines.

  17. Key: Defines host.id as the baseline key. This means the system creates and maintains a separate baseline for each unique host. Each host learns its own "normal" behavior independently.

  18. Aggregate By: Specifies that events should be aggregated by event.code. This creates the vector dimensions—each unique Event Code becomes one dimension in the vector.

  19. Timestamp: Specifies which field to use for temporal bucketing. The @timestamp field determines which one-hour window each event belongs to.

  20. Greater Than Test: The !GT (greater than) operator compares the calculated SIGMA value against the threshold.

  21. SIGMA Argument: !ARG SIGMA is the Z-score calculated as (|ACTUAL_value - MEAN_value|) / STDEV. This represents how many standard deviations the current vector norm is from the learned baseline.

  22. Threshold: The value 5 means the system triggers an anomaly when the SIGMA exceeds 5 standard deviations. This is a relatively high threshold that filters out normal variations and only captures significant deviations.

  23. Trigger Event: When an anomaly is detected, the system generates a new event with the specified fields.

  24. Event Action: Tags the event with behavior-anomaly to indicate this is an anomaly detection, making it easy to filter and correlate these events.

  25. Event Reason: Provides a human-readable explanation of why the event was triggered, useful for analysts investigating the alert.

  26. Event Kind: Set to alert to classify this as an alerting event in the ECS schema.

  27. Event Type: Set to indicator to mark this as an indicator of potential compromise or unusual activity, following ECS event categorization.

How It Works in Practice

When this baseline rule is active:

  1. Learning Phase: The system collects event data for each host across different temporal classes (workdays, weekends, holidays).

  2. Hourly Vectors: For each hour and each host, the system creates a vector where each dimension represents how often a specific Event Code appeared.

  3. Weight Application: If weights are configured, each Event Code count in the vector is multiplied by its corresponding weight before calculating the vector norm. This emphasizes important events and de-emphasizes noisy ones.

  4. Baseline Calculation: After collecting enough samples (4 per class), the system calculates the mean and standard deviation of the vector norm for each hour and class combination.

  5. Continuous Monitoring: As new events arrive, the system continuously recalculates the current hour's vector norm (with weights applied) and compares it to the baseline.

  6. Anomaly Detection: When the Z-score (SIGMA) exceeds 5 standard deviations, the system recognizes this as an anomaly and can trigger downstream actions.

  7. Correlation Input: The generated behavior-anomaly events can be consumed by other correlation rules to enrich detections with behavioral context.

Use Cases

This vector-based baseline detection is particularly effective for:

  • Detecting compromised accounts: Unusual patterns of authentication events, privilege changes, or process executions. Use weights to emphasize rare privilege-related events (4672, 4648) while reducing noise from routine logons.
  • Identifying lateral movement: Sudden changes in network-related Event Codes on a previously quiet host. Weight process creation (4688) and explicit credential use (4648) higher to catch lateral movement attempts.
  • Spotting malware activity: Abnormal service creation, registry modifications, or file access patterns. Increase weights for service-related events and scheduled task creation to detect persistence mechanisms.
  • Monitoring privileged users: Detecting when administrators perform unusual activities outside their normal patterns. Use weights to focus on administrative actions while filtering routine operations.
  • Baseline drift detection: Identifying when hosts undergo significant configuration or role changes. The vector approach naturally detects when a host's "behavior profile" fundamentally shifts, even if individual events seem normal.

Best Practices

  1. Learning Period: Allow at least 4 days of data collection to ensure all temporal classes (workdays/weekends/holidays) have sufficient samples.

  2. Threshold Tuning: Start with a SIGMA threshold of 5 and adjust based on false positive rates in your environment. Higher thresholds (6-7) reduce noise but may miss subtle anomalies.

  3. Weight Tuning: Use the weights parameter to fine-tune the baseline sensitivity:

  4. Reduce weights (0.1 - 0.5) for high-frequency, low-value events that add noise:
    • 4624: Successful account logons
    • 4634: Account logoffs
    • 5156: Windows Filtering Platform allowed connections
    • 4663: Attempts to access objects (often very noisy)
  5. Increase weights (1.5 - 3.0) for rare, high-value security events:
    • 4688: Process creation
    • 4672: Special privileges assigned to new logon
    • 4720: User account created
    • 4732: Member added to security-enabled local group
    • 4648: Logon attempted using explicit credentials
  6. Start without weights, analyze which Event Codes dominate the vector, then adjust accordingly.

  7. Host Grouping: Consider creating separate baselines for different host types (servers, workstations, domain controllers) by using host roles or tags in the key field.

  8. Exclusions: Add predicates to exclude known noisy Event Codes or maintenance windows that would skew the baseline.

  9. Correlation: Combine this baseline with other detection rules (failed logins, privilege escalations) to create high-confidence composite detections.

Advantages Over Traditional Approaches

  • Adaptive: Automatically adapts to each host's normal behavior without manual threshold tuning
  • Temporal Awareness: Understands that "normal" looks different on Monday morning vs. Saturday night
  • Multidimensional: Captures patterns across thousands of Event Codes simultaneously
  • Tunable Sensitivity: The optional weights parameter allows fine-tuning sensitivity to specific Event Codes without losing the benefits of automated baseline learning
  • Scalable: Lightweight computation that works across large fleets of endpoints
  • Context-Aware: Provides behavioral context for other detection rules rather than just binary yes/no alerts

Additional Resources