🚀 Executive Summary

TL;DR: Manually monitoring Windows Event Logs for failed login attempts (Event ID 4625) is inefficient and can lead to missed security incidents. This guide provides a Python-based automation solution using wevtutil.exe and smtplib to query security logs, format a report, and email daily alerts, significantly improving server security posture and reducing manual effort.

🎯 Key Takeaways

  • The built-in `wevtutil.exe` command-line tool is effective for querying Windows Security Event Logs using structured XML queries, specifically targeting Event ID 4625 for failed login attempts within a defined timeframe.
  • Python’s `subprocess` module facilitates executing `wevtutil.exe` and capturing its output, while `smtplib` and `email.mime.multipart` are used to securely send formatted email alerts containing the extracted log data.
  • Windows Task Scheduler is crucial for automating the Python script, ensuring it runs with necessary permissions (‘Run with highest privileges’, ‘Run whether user is logged on or not’) at regular intervals for proactive security monitoring.

Monitor Failed Login Attempts on Windows Server (Event Log to Email)

Monitor Failed Login Attempts on Windows Server (Event Log to Email)

Hey team, Darian here. I want to talk about a quick win that has saved me a surprising amount of time. I used to spend a chunk of my Monday morning manually checking Windows Event Logs for failed RDP and local login attempts across our servers. It was tedious, and honestly, easy to miss things. After one too many “I wonder who tried to log in over the weekend?” moments, I built this simple automation. It queries the logs, formats a clean report, and emails it to me. Now, I get a daily digest and can spot anomalies in 30 seconds. This is how you can set it up.

Prerequisites

Before we dive in, make sure you have the following ready:

  • A Windows Server machine where you have administrative privileges.
  • Python 3 installed and added to your system’s PATH.
  • An email account that can be used for sending notifications (like a dedicated service account or a provider like SendGrid).
  • Access to the Windows Task Scheduler to automate the script.

The Guide: From Log to Inbox

Step 1: The Python Script to Query Event Logs

First, let’s get the data. We could use a heavy PowerShell script or a third-party library, but I prefer using what’s already on the box. The built-in `wevtutil.exe` command-line tool is perfect for this. It can query the event log using a structured XML query and export the results.

I’ll skip the standard project setup steps like creating a directory or a virtual environment; you’ve got your own workflow for that. Let’s get straight to the Python logic. We’ll use the `subprocess` module to run `wevtutil` and capture its output.

The key is the XML query. We’re telling Windows to find all events in the ‘Security’ log with Event ID `4625` (an account failed to log on) that occurred in the last 24 hours.


import subprocess
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv
from datetime import datetime, timedelta

def get_failed_logins():
    """Queries the Windows Event Log for failed login attempts (ID 4625) in the last 24 hours."""
    
    # Time calculation for the last 24 hours in the required format
    yesterday = datetime.utcnow() - timedelta(days=1)
    time_query = yesterday.strftime('%Y-%m-%dT%H:%M:%SZ')

    # This XML query filters for Event ID 4625 from the last 24 hours.
    # Note the escaped < and > characters for HTML compatibility.
    xml_query = f"""
    <QueryList>
      <Query Id="0" Path="Security">
        <Select Path="Security">
          *[System[(EventID=4625) and TimeCreated[@SystemTime>='{time_query}']]]
        </Select>
      </Query>
    </QueryList>
    """

    # We write the query to a temporary file because wevtutil prefers it.
    query_file = 'query.xml'
    with open(query_file, 'w') as f:
        f.write(xml_query)

    command = [
        'wevtutil', 'qe', 'Security', f'/q:@{query_file}', '/f:Text'
    ]
    
    print("Running command to fetch logs...")
    try:
        # We use capture_output=True to get the stdout/stderr streams.
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        os.remove(query_file) # Clean up the temp file
        
        if result.stdout:
            print("Successfully fetched failed login events.")
            return result.stdout
        else:
            print("No failed login events found in the last 24 hours.")
            return None
            
    except subprocess.CalledProcessError as e:
        print(f"An error occurred while fetching event logs: {e.stderr}")
        os.remove(query_file) # Still clean up on failure
        return None

Pro Tip: The `TimeCreated` value in the XML query is powerful. You can easily change `timedelta(days=1)` to `timedelta(hours=1)` for more frequent checks or `timedelta(weeks=1)` for a weekly summary.

Step 2: Sending the Email Alert

Now that we have the log data as a string, we need to email it. I use the standard `smtplib` and `email` libraries for this. This function takes the log data and sends it out. Notice how we’re preparing to load credentials from environment variables—never hardcode them!


def send_email_alert(log_data):
    """Sends an email with the failed login data."""
    
    # Load credentials from config.env file
    load_dotenv('config.env')
    
    sender_email = os.getenv('SENDER_EMAIL')
    receiver_email = os.getenv('RECEIVER_EMAIL')
    smtp_password = os.getenv('SMTP_PASSWORD')
    smtp_server = os.getenv('SMTP_SERVER')
    smtp_port = int(os.getenv('SMTP_PORT', 587)) # Default to 587 if not set

    if not all([sender_email, receiver_email, smtp_password, smtp_server]):
        print("Email configuration is missing. Please check your config.env file.")
        return

    message = MIMEMultipart("alternative")
    message["Subject"] = f"Windows Server Failed Login Report - {datetime.now().strftime('%Y-%m-%d')}"
    message["From"] = sender_email
    message["To"] = receiver_email

    # Create a simple text body and an HTML body for the email
    text_body = f"Please find the attached report for failed logins in the last 24 hours.\n\n{log_data}"
    html_body = f"""
    <html>
      <body>
        <h2>Failed Login Attempts Report</h2>
        <p>Log entries from the last 24 hours:</p>
        <pre style="background-color:#f4f4f4; padding: 10px; border: 1px solid #ddd;">{log_data}</pre>
      </body>
    </html>
    """
    
    message.attach(MIMEText(text_body, "plain"))
    message.attach(MIMEText(html_body, "html"))

    try:
        print(f"Connecting to SMTP server at {smtp_server}:{smtp_port}...")
        with smtplib.SMTP(smtp_server, smtp_port) as server:
            server.starttls() # Secure the connection
            server.login(sender_email, smtp_password)
            server.sendmail(sender_email, receiver_email, message.as_string())
            print("Email alert sent successfully!")
    except Exception as e:
        print(f"Failed to send email: {e}")

Step 3: Managing Configuration and Tying It Together

To keep our script clean and secure, we’ll store all our settings in a `config.env` file. You’ll need to install the `python-dotenv` package for this to work. You can do that via pip from your command line.

Create a file named `config.env` in the same directory as your Python script:


# Email Configuration
SENDER_EMAIL=your-sender-email@example.com
RECEIVER_EMAIL=your-admin-email@example.com
SMTP_PASSWORD=your_email_app_password
SMTP_SERVER=smtp.example.com
SMTP_PORT=587

Finally, let’s create our main execution block that calls our functions in order.


def main():
    """Main function to run the monitor."""
    failed_logins = get_failed_logins()
    if failed_logins:
        send_email_alert(failed_logins)
    else:
        print("No action needed.")

if __name__ == "__main__":
    main()

Put all three Python code blocks into a single file, for example, `log_monitor.py`.

Step 4: Automate with Windows Task Scheduler

A script is only useful if it runs automatically. Here’s how to set it up in Windows Task Scheduler:

  1. Open Task Scheduler from the Start Menu.
  2. In the right-hand Actions pane, click Create Basic Task…
  3. Give it a name like “Daily Failed Login Report” and a description. Click Next.
  4. For the Trigger, choose Daily and set a time you’d like the report, like early in the morning. Click Next.
  5. For the Action, select Start a program. Click Next.
  6. In the “Program/script” box, browse to your Python executable. It’s often in a path like `C:\Python39\python.exe`.
  7. In the “Add arguments (optional)” box, enter the full path to your script, e.g., `C:\YourScripts\log_monitor.py`.
  8. In the “Start in (optional)” box, enter the directory where your script and `config.env` file are located, e.g., `C:\YourScripts\`. This is important so the script can find the config file.
  9. Click Next, review the details, and click Finish.

Pro Tip: After creating the task, find it in the Task Scheduler Library, right-click it, and go to Properties. On the General tab, select “Run whether user is logged on or not” and check the “Run with highest privileges” box. This ensures the script has the necessary permissions to query the security event log even if you’re not logged in.

Common Pitfalls (Where I Usually Mess Up)

  • Firewall Rules: The most common issue I run into is the server’s firewall blocking outbound SMTP traffic on port 587 or 465. Always double-check your outbound rules.
  • Email Authentication: If you’re using a service like Gmail, you can’t use your regular password if you have 2-Factor Authentication enabled. You need to generate an “App Password” and use that in your `config.env` file.
  • Task Scheduler Permissions: If the task runs but you get no email, it’s often a permissions issue. The account running the scheduled task needs permission to read the Security event log. Running with the highest privileges, as mentioned above, usually solves this.

Conclusion

And that’s it. You now have a lightweight, dependency-free (besides Python itself) monitoring script that turns raw Windows Event Log data into a proactive security alert. It’s a classic DevOps mindset: identify a repetitive manual task and automate it. This frees you up to focus on more important things, while still keeping a close eye on your server’s security posture. Let me know if you have any questions.

Cheers,
Darian Vance

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How do I monitor failed login attempts on Windows Server and get notified?

You can monitor failed login attempts on Windows Server by querying the Security Event Log for Event ID 4625 using `wevtutil.exe`. A Python script can then parse these logs and send email notifications via `smtplib`, automated by Windows Task Scheduler.

âť“ How does this Python script solution compare to using PowerShell or third-party SIEM tools for monitoring failed logins?

This Python solution offers a lightweight, dependency-free (beyond Python) approach using built-in `wevtutil.exe`, making it simpler than complex PowerShell scripts for basic alerting. Compared to full SIEM tools, it’s less comprehensive in analysis and correlation but provides a cost-effective, quick-win solution for specific, proactive security alerts without external software.

âť“ What are common issues encountered when implementing this failed login monitoring script?

Common pitfalls include firewall rules blocking outbound SMTP traffic (port 587/465), incorrect email authentication (e.g., needing an ‘App Password’ for 2FA-enabled accounts), and insufficient permissions for the scheduled task to read the Security event log. Ensure the task runs with ‘highest privileges’ and ‘whether user is logged on or not’.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading