Security Log Anomaly Detector

Build a Python tool that ranks suspicious authentication events with AI.

Introduction

30 Second Summary

One loud burst of failed sign-ins is easy to notice. A quieter attack can hide across several details that each look harmless on their own.

In this project, you will build a command-line security log analyzer that surfaces suspicious authentication events. You will compare rule-based detection with IsolationForest to rank unusual activity through unsupervised learning.

What You'll Build

You will run one analyzer in a Windows terminal to watch a stealth event rise into the model's top anomalies after a simple rule misses it.

By the end of this project, you'll have:

  • Reveal a fixed rule's blind spot by flagging evt-041 while seeing evt-042 stay below the failed-attempt threshold.
  • Review an ML-ranked queue where evt-042 appears among the top three anomalies with reasons such as off-hours access or a new device.
  • Publish a reproducible project in a public GitHub repository that reviewers can inspect or run for themselves.
  • Secret Mission: Add a context-aware risk layer that boosts privileged-account events while reducing known service noise.

Are there any prerequisites?

You need basic Python familiarity. You should also recognize successful or failed sign-ins in an authentication log.

Before We Start

This checkpoint is your moment to commit to the security problem you are solving. You will define what the detector must surface and why that result matters to an analyst.

Set Up the Windows ML Workspace

A dependable detector needs a workspace that another reviewer can reproduce. Unknown tool versions can turn a working script into a package conflict.

First, you will confirm a compatible Python version. Next, you will confirm a compatible Git version.

A virtual environment will isolate scikit-learn inside this project.

In this step, get ready to:
  • Open the project folder in Visual Studio Code with a Windows Command Prompt terminal.
  • Confirm Python 3.11 or newer plus Git for Windows 2.29 or newer.
  • Finish a reproducible environment with a pinned dependency plus Git exclusions.
Prepare the workspace and check versions

The project folder gives every file one predictable home. The version checks confirm that your tools support the workflow ahead.

  • Press the Windows key to open the Windows search bar.
  • Type Visual Studio Code into the search bar.
  • Press Enter to open Visual Studio Code.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop in the folder picker.
  • Use the New folder control to create security-log-anomaly-detector.
  • Choose security-log-anomaly-detector with the Select Folder button.

Good start. Visual Studio Code now has a stable home for every detector file you create.

  • Select Terminal from the top menu bar.
  • Select New Terminal.
  • Choose Command Prompt from the terminal profile selector.
  • Check the available Python runtimes plus your Git version by running these commands:
py list
git --version

What Do These Checks Show?

  • The Python runtime list shows which versions are available on your Windows machine.
  • The Git version output confirms whether your installation supports the GitHub authentication flow used later.

Choose the Python result that matches your terminal output.

✔️ I see Python 3.11 or newer

Your Python runtime meets the package requirement. Keep the Command Prompt terminal open for the environment setup.

ⓧ I only see Python older than 3.11

Your listed runtime cannot install the pinned scikit-learn package. The current stable Python runtime is 3.14.7.

  • Visit the official Python downloads page.
  • Install the current stable runtime through the Python Install Manager.
  • Return to Visual Studio Code after the installation finishes.
  • Close the existing terminal with the terminal panel's trash control.
  • Select Terminal from the top menu bar.
  • Select New Terminal.
  • Repeat the Python version check shown above.

ⓧ I do not see a compatible Python runtime

The Python Install Manager provides a compatible runtime from the official Windows download page.

  • Visit the official Python downloads page.
  • Install the current stable Python runtime through the Python Install Manager.
  • Return to Visual Studio Code after the installation finishes.
  • Close the existing terminal with the terminal panel's trash control.
  • Select Terminal from the top menu bar.
  • Select New Terminal.
  • Repeat the Python version check shown above.

Python Version Still Missing?

A terminal opened before the installation may retain the old runtime list. Close that terminal before creating a fresh Command Prompt terminal.

If the compatible runtime still does not appear, use this help prompt: Help me find why py list does not show my newly installed Python runtime on Windows.

Now choose the Git result that matches your terminal output.

✔️ I see Git 2.29 or newer

Your Git installation supports the GitHub authentication workflow used later. The workspace prerequisites are ready.

ⓧ I see a Git version older than 2.29

This installation needs an update before you publish the finished project. WinGet may ask you to review source terms before it continues.

  • Update Git for Windows by running this command:
winget install --id Git.Git -e --source winget

What Does This Command Do?

WinGet installs the maintained Git for Windows package. The updated installation includes Git Credential Manager for browser-based GitHub sign-in.

  • Accept the source terms if WinGet requests confirmation.
  • Close the current terminal after the installation finishes.
  • Select Terminal from the top menu bar.
  • Select New Terminal.
  • Repeat the Git version check shown above.

Git Update Not Completing?

Check that WinGet is available on your Windows installation. Also confirm that the terminal has network access.

Use this prompt with the terminal output: Help me troubleshoot my Git for Windows installation command.

ⓧ Git command not found

Git for Windows needs to be installed before this project can become a local repository. WinGet may request confirmation for its package source.

  • Install Git for Windows by running this command:
winget install --id Git.Git -e --source winget

What Does This Command Do?

WinGet installs Git for Windows from the specified package source. The installation includes Git Credential Manager for the later GitHub sign-in.

  • Accept the source terms if WinGet requests confirmation.
  • Close the current terminal after the installation finishes.
  • Select Terminal from the top menu bar.
  • Select New Terminal.
  • Repeat the Git version check shown above.

Git Still Unavailable?

A terminal created before the installation may not recognize the new command. Close it before opening a fresh Command Prompt terminal.

Use this prompt if the command remains unavailable: Help me make Git available in my Visual Studio Code Command Prompt terminal.

Create the isolated Python environment

The project environment keeps its machine-learning package separate from other Python projects. Pinning the dependency gives future reviewers the same package version.

  • Create the sklearn-env environment plus activate it by running these commands:
python -m venv sklearn-env
sklearn-env\Scripts\activate

What Do These Commands Do?

  • The first command creates an isolated Python environment named sklearn-env inside the project folder.
  • The second command activates that environment for the current Command Prompt session.
  • Check the beginning of your terminal prompt for (sklearn-env).

You should see (sklearn-env) before the project path. That prefix confirms future package commands target the isolated environment.

Environment Not Activating?

Confirm that the terminal profile is Command Prompt. The activation command in this project is written for that shell.

Also confirm that sklearn-env appears in the Visual Studio Code Explorer. Use this prompt for help: Help me activate my sklearn-env virtual environment in Windows Command Prompt.

The dependency file records the exact library release that powers the anomaly detector.

  • Create requirements.txt in the security-log-anomaly-detector folder with the Visual Studio Code Explorer's New File control.
  • Add the dependency pin by pasting this line into requirements.txt:
scikit-learn==1.9.0

Why Pin the Dependency?

The double equals pin tells the package installer to use scikit-learn 1.9.0. This keeps the detector reproducible across different machines.

  • Save requirements.txt with Ctrl+S.
  • Confirm requirements.txt appears in the Explorer under security-log-anomaly-detector.

Dependency File Looks Different?

Check that the filename ends with .txt. Also compare the dependency line character by character.

Use this prompt if the file is not appearing correctly: Help me check my requirements.txt file in Visual Studio Code.

  • Install the pinned project dependency by running this command in the activated terminal:
python -m pip install -r requirements.txt

What Does This Installation Do?

Python runs the package installer from the active environment. The installer reads the exact dependency pin from requirements.txt.

The terminal may stay busy while packages download. Wait until the Command Prompt returns before continuing.

You should see installation progress followed by confirmation that the required packages were installed.

Package Installation Failing?

Confirm that the terminal prompt begins with (sklearn-env). Also confirm that the terminal is inside security-log-anomaly-detector.

Use this prompt with the complete terminal output: Help me fix my scikit-learn installation inside sklearn-env.

Protect the project and verify scikit-learn

The environment contains generated files that do not belong in version control. A Git ignore file keeps those files out of the repository you publish later.

  • Create .gitignore in the security-log-anomaly-detector folder with the Explorer's New File control.
  • Add the project exclusions by pasting these lines into .gitignore:
sklearn-env/
__pycache__/
*.pyc

What Do These Exclusions Protect?

  • The sklearn-env/ entry keeps the local virtual environment out of Git.
  • The __pycache__/ entry excludes Python's generated cache folders.
  • The *.pyc pattern excludes compiled Python files.
  • Save .gitignore with Ctrl+S.
  • Confirm .gitignore appears beside requirements.txt in the Explorer.

Git Ignore File Not Visible?

Confirm that the filename begins with a full stop. Make sure Visual Studio Code did not append .txt.

Use this prompt if the file still looks wrong: Help me create the correct .gitignore file for my Python project.

Use these tabs to compare both reproducibility files before the final environment check.

✔️ Awesome, I've got everything!

Both files are saved in security-log-anomaly-detector. Your dependency pin plus Git exclusions are ready.

ⓧ I'd like to double check the full code

Compare your complete requirements.txt with this file:

scikit-learn==1.9.0

Requirements File Check

This file contains one exact dependency pin. Remove any extra characters or spaces from the line.

Compare your complete .gitignore with this file:

sklearn-env/
__pycache__/
*.pyc

Git Ignore File Check

These three entries exclude the local environment plus Python's generated cache files. Keep each pattern on its own line.

Before you run the final check, which scikit-learn version do you expect the environment report to show?

  • Verify the installed scikit-learn environment by running this command:
python -c "import sklearn; sklearn.show_versions()"

What Does This Verification Prove?

The command imports scikit-learn from the active Python environment. It then prints the installed library version plus environment details.

You should see scikit-learn 1.9.0 in the output. That result proves the pinned package is available inside sklearn-env.

Seeing the Wrong Version?

Check that the terminal prompt begins with (sklearn-env). An unactivated terminal may import a package from another Python installation.

Use this prompt with the full version report: Help me verify scikit-learn 1.9.0 inside my sklearn-env environment.

Your Windows machine-learning workspace is isolated and reproducible. Next, you will build the fixed-threshold detector that exposes the stealth event it cannot catch.

Build the Rule That Misses the Attack

Your Windows machine learning workspace is ready. You can now turn the empty project into a security detector with evidence you can inspect.

A single threshold makes rule-based detection easy to explain. However, suspicious behavior can be spread across several weak signals that each stay below the threshold.

In this step, get ready to:
  • Generate 40 routine authentication events plus three unusual events.
  • Build a detector with a failed-attempt threshold of 10.
  • Run the rule to reveal which suspicious event escapes the threshold.
Create the authentication dataset

Feature engineering turns authentication details into consistent fields that a detector can compare. A synthetic dataset gives you repeatable suspicious events without exposing real security logs.

  • Select the Explorer view in Visual Studio Code's Activity Bar.
  • Select the New File button in the Explorer.
  • Enter security_data.py as the file name.
  • Press Enter to create the file inside security-log-anomaly-detector.
  • Create the feature list and 40 routine events by pasting this code into security_data.py:
FEATURE_NAMES = (
    "failed_attempts",
    "login_hour",
    "country_mismatch",
    "known_device",
    "requests_per_minute",
)

NORMAL_EVENTS = []

for index in range(40):
    NORMAL_EVENTS.append(
        {
            "event_id": f"evt-{index + 1:03}",
            "user": f"user-{(index % 8) + 1}",
            "source_ip": f"10.0.{index % 4}.{index + 10}",
            "failed_attempts": index % 3,
            "login_hour": 8 + (index % 11),
            "country_mismatch": 0,
            "known_device": 1,
            "requests_per_minute": 3 + (index % 5),
            "is_privileged": 1 if index % 13 == 0 else 0,
            "known_service": 0,
        }
    )

What does this code do?

  • The FEATURE_NAMES tuple identifies the five numeric signals used to describe authentication activity.
  • The NORMAL_EVENTS list stores the routine activity that forms your baseline.
  • The loop creates 40 repeatable events with daytime logins and low request volumes.
  • The index value varies users and event details while keeping the dataset deterministic.
  • Save security_data.py by pressing Ctrl+S.
  • Check that the routine dataset parses by running this command in the activated Command Prompt:
python security_data.py

What does this check prove?

Python loads the feature names and generates the routine events. A clean return to the activated prompt confirms that the dictionaries and loop parsed successfully.

Seeing a Python error?

Check that every dictionary key has matching quotation marks. Confirm that the closing parentheses align with NORMAL_EVENTS.append(.

Compare the indentation inside the for loop because every line in the appended dictionary must stay nested under it.

Help me fix the routine-event code in security_data.py.

The baseline now represents ordinary activity. The next records add distinct combinations of failed attempts, login timing, device familiarity, country mismatch, request volume, account privilege, and service context.

  • Start the EVENTS list by pasting this section directly below the loop. Keep the list open because the following block completes it:
EVENTS = NORMAL_EVENTS + [
    {
        "event_id": "evt-041",
        "user": "alex",
        "source_ip": "10.99.0.41",
        "failed_attempts": 18,
        "login_hour": 2,
        "country_mismatch": 1,
        "known_device": 0,
        "requests_per_minute": 55,
        "is_privileged": 0,
        "known_service": 0,
    },
    {
        "event_id": "evt-042",
        "user": "admin",
        "source_ip": "10.99.0.42",
        "failed_attempts": 2,
        "login_hour": 3,
        "country_mismatch": 1,
        "known_device": 0,
        "requests_per_minute": 40,
        "is_privileged": 1,
        "known_service": 0,
    },

What do these events represent?

  • Event evt-041 has 18 failed attempts. This gives the threshold an obvious brute-force pattern to catch.
  • Event evt-042 has only 2 failed attempts. Its other fields capture an off-hours login from a new device with a country mismatch and elevated request volume.
  • Complete the open EVENTS list by pasting this continuation directly below the previous block:
    {
        "event_id": "evt-043",
        "user": "backup-service",
        "source_ip": "10.99.0.43",
        "failed_attempts": 0,
        "login_hour": 23,
        "country_mismatch": 1,
        "known_device": 1,
        "requests_per_minute": 120,
        "is_privileged": 0,
        "known_service": 1,
    },
]

Why include a service event?

Event evt-043 looks unusual because it runs late and generates many requests. Its known_service field records context that can matter during security triage.

  • Save the completed security_data.py file.
  • Check the complete dataset syntax by running this command:
python security_data.py

What confirms the dataset is ready?

You should return to the activated Command Prompt without a traceback. That clean run confirms that Python can create all 43 event dictionaries.

Dataset still not loading?

Check that the first unusual-event block ends with a comma after the evt-042 dictionary. Confirm that the final square bracket appears after the evt-043 dictionary.

Help me repair the EVENTS list in security_data.py.

✔️ Awesome, I've got everything!

Great. Your security_data.py file now contains 40 routine events plus three unusual events.

ⓧ I'd like to double check the full code

Compare your complete security_data.py file with this reference:

FEATURE_NAMES = (
    "failed_attempts",
    "login_hour",
    "country_mismatch",
    "known_device",
    "requests_per_minute",
)

NORMAL_EVENTS = []

for index in range(40):
    NORMAL_EVENTS.append(
        {
            "event_id": f"evt-{index + 1:03}",
            "user": f"user-{(index % 8) + 1}",
            "source_ip": f"10.0.{index % 4}.{index + 10}",
            "failed_attempts": index % 3,
            "login_hour": 8 + (index % 11),
            "country_mismatch": 0,
            "known_device": 1,
            "requests_per_minute": 3 + (index % 5),
            "is_privileged": 1 if index % 13 == 0 else 0,
            "known_service": 0,
        }
    )

EVENTS = NORMAL_EVENTS + [
    {
        "event_id": "evt-041",
        "user": "alex",
        "source_ip": "10.99.0.41",
        "failed_attempts": 18,
        "login_hour": 2,
        "country_mismatch": 1,
        "known_device": 0,
        "requests_per_minute": 55,
        "is_privileged": 0,
        "known_service": 0,
    },
    {
        "event_id": "evt-042",
        "user": "admin",
        "source_ip": "10.99.0.42",
        "failed_attempts": 2,
        "login_hour": 3,
        "country_mismatch": 1,
        "known_device": 0,
        "requests_per_minute": 40,
        "is_privileged": 1,
        "known_service": 0,
    },
    {
        "event_id": "evt-043",
        "user": "backup-service",
        "source_ip": "10.99.0.43",
        "failed_attempts": 0,
        "login_hour": 23,
        "country_mismatch": 1,
        "known_device": 1,
        "requests_per_minute": 120,
        "is_privileged": 0,
        "known_service": 1,
    },
]
Add the fixed-threshold rule

A threshold rule evaluates one measurable condition. This detector selects events whose failed_attempts value reaches 10.

  • Select the New File button in Visual Studio Code's Explorer.
  • Enter rule_detector.py as the file name.
  • Press Enter to create the file inside security-log-anomaly-detector.
  • Define the threshold and filtering function by pasting this code into rule_detector.py:
from security_data import EVENTS

FAILURE_THRESHOLD = 10
STEALTH_EVENT_ID = "evt-042"


def detect_by_rule(events):
    return [
        event
        for event in events
        if event["failed_attempts"] >= FAILURE_THRESHOLD
    ]

How does the rule work?

  • The import gives the detector access to every event from security_data.py.
  • The FAILURE_THRESHOLD constant sets the cutoff at 10 failed attempts.
  • The detect_by_rule(events) function returns only events that meet or exceed the cutoff.
  • The STEALTH_EVENT_ID constant identifies the event used to test the rule's coverage.
  • Save rule_detector.py.
  • Check that the import and filtering function load by running:
python rule_detector.py

What does this first run prove?

You should return to the activated prompt without a traceback. That confirms rule_detector.py can import the dataset and define the threshold function.

The filter can now select alerts. The remaining code prints those alerts and checks whether the selected set contains the stealth event.

  • Complete rule_detector.py by pasting this code directly below detect_by_rule(events):


def main():
    alerts = detect_by_rule(EVENTS)

    print("Rule-based alerts:")
    for event in alerts:
        print(
            f"- {event['event_id']} | user={event['user']} | "
            f"failed_attempts={event['failed_attempts']}"
        )

    stealth_flagged = any(
        event["event_id"] == STEALTH_EVENT_ID for event in alerts
    )
    print(f"\nStealth event {STEALTH_EVENT_ID} flagged: {stealth_flagged}")


if __name__ == "__main__":
    main()

What does the output logic do?

  • The alerts variable stores the events returned by the threshold rule.
  • The loop prints each selected event with its user and failed-attempt count.
  • The stealth_flagged check searches the selected alerts for evt-042.
  • The final guard runs main() when you execute the file directly.
  • Save the completed rule_detector.py file.
Run the rule against the stealth event

Before you run the detector, which events do you think a threshold of 10 failed attempts will select?

  • Run the completed rule detector in the activated Command Prompt:
python rule_detector.py

What should you see?

You should see evt-041 beneath Rule-based alerts: because it has 18 failed attempts.

The final line should report Stealth event evt-042 flagged: False. Its 2 failed attempts remain below the threshold.

That miss is intentional. You now have visible evidence that one failed-attempt threshold cannot combine several weaker warning signs.

Not seeing the expected events?

Confirm that security_data.py and rule_detector.py appear beside each other in the security-log-anomaly-detector folder.

Check that FAILURE_THRESHOLD equals 10. Verify that the comparison uses the failed_attempts field.

Help me debug the output from rule_detector.py.

✔️ Awesome, I've got everything!

Your detector flags evt-041 while leaving evt-042 outside the alert list.

ⓧ I'd like to double check the full code

Compare your complete rule_detector.py file with this reference:

from security_data import EVENTS

FAILURE_THRESHOLD = 10
STEALTH_EVENT_ID = "evt-042"


def detect_by_rule(events):
    return [
        event
        for event in events
        if event["failed_attempts"] >= FAILURE_THRESHOLD
    ]


def main():
    alerts = detect_by_rule(EVENTS)

    print("Rule-based alerts:")
    for event in alerts:
        print(
            f"- {event['event_id']} | user={event['user']} | "
            f"failed_attempts={event['failed_attempts']}"
        )

    stealth_flagged = any(
        event["event_id"] == STEALTH_EVENT_ID for event in alerts
    )
    print(f"\nStealth event {STEALTH_EVENT_ID} flagged: {stealth_flagged}")


if __name__ == "__main__":
    main()

Your rule detector has exposed the stealth gap with a reproducible terminal result. Next, you will use IsolationForest to combine the unusual signals and rank the missed event for review.

Surface the Stealth Event with IsolationForest

Your rule detector catches evt-041 through a clear failed-attempt threshold. The stealth event evt-042 stays invisible because no single signal reaches that threshold.

This step uses IsolationForest to apply unsupervised learning across several authentication signals. The model ranks unusual combinations that deserve an analyst's attention.

In this step, get ready to:
  • Transform authentication events into numeric feature rows.
  • Score each event with a reproducible IsolationForest model.
  • Rank the results to reveal evt-042 among the top three anomalies.
Transform events into feature rows

Machine-learning models work with numeric rows. Feature engineering turns each event dictionary into a consistent row based on FEATURE_NAMES.

  • Create anomaly_detector.py inside security-log-anomaly-detector using the file creation control in the Explorer sidebar of Visual Studio Code.
  • Paste this first section into anomaly_detector.py:
from sklearn.ensemble import IsolationForest

from security_data import EVENTS, FEATURE_NAMES

EXPECTED_ANOMALIES = 3
TOP_N = 3
STEALTH_EVENT_ID = "evt-042"


def build_feature_matrix(events):
    return [
        [event[feature_name] for feature_name in FEATURE_NAMES]
        for event in events
    ]

What does this code do?

  • The imports bring in the model plus the event data from security_data.py.
  • EXPECTED_ANOMALIES records how many unusual events the dataset contains.
  • TOP_N limits the final analyst queue to three events.
  • build_feature_matrix() preserves the feature order defined by FEATURE_NAMES.
  • Each inner list becomes one numeric authentication-event row for the model.
  • Save anomaly_detector.py.
  • Check the partial detector in the activated Command Prompt from earlier by running:
python anomaly_detector.py

What did this check prove?

You should return to the activated prompt without a Python traceback. That confirms the imports plus the feature-building function load successfully.

File not loading?

  • Confirm the virtual environment from Step 1 is still active.
  • Compare both import lines with the code above.
  • Check that security_data.py remains beside anomaly_detector.py in the same folder.

Help me diagnose why my anomaly detector does not load.

Fit and score the event set

IsolationForest separates unusual rows through random partitioning. Shorter isolation paths produce lower decision scores for more abnormal events.

  • Place your cursor two blank lines below the closing bracket of build_feature_matrix().
  • Add the model-ranking function by pasting this code:
def rank_events(events):
    feature_matrix = build_feature_matrix(events)
    model = IsolationForest(
        n_estimators=100,
        contamination=EXPECTED_ANOMALIES / len(events),
        random_state=42,
    )
    model.fit(feature_matrix)
    scores = model.decision_function(feature_matrix)

    return sorted(
        zip(events, scores),
        key=lambda event_and_score: event_and_score[1],
    )

How does the ranking work?

  • feature_matrix holds one numeric row for every authentication event.
  • n_estimators=100 builds the ranking from 100 isolation trees.
  • contamination=EXPECTED_ANOMALIES / len(events) sets the expected outlier proportion to three events out of the complete dataset.
  • random_state=42 keeps repeated runs reproducible.
  • decision_function() assigns lower scores to more abnormal rows.
  • sorted() places the lowest scores at the front of the analyst queue.
  • Save anomaly_detector.py.
  • Check the expanded detector by running:
python anomaly_detector.py

What did this check prove?

You should return to the activated prompt without a Python traceback. That confirms the model configuration plus the scoring logic load successfully.

Model function not loading?

  • Confirm rank_events() starts below build_feature_matrix() without extra indentation.
  • Compare the capitalization of IsolationForest with its import.
  • Check that each closing parenthesis matches the model call plus the final sorting call.

Help me fix my IsolationForest ranking function.

Print and verify the ranked anomalies

The sorted scores become useful when an analyst can see the highest-priority events. The final function prints the top three plus the overall rank of evt-042.

  • Place your cursor two blank lines below the closing parenthesis of rank_events().
  • Add the output logic by pasting this code:
def main():
    ranked_events = rank_events(EVENTS)

    print("Top ML-ranked authentication anomalies:")
    for rank, (event, score) in enumerate(ranked_events[:TOP_N], start=1):
        print(
            f"{rank}. {event['event_id']} | user={event['user']} | "
            f"score={score:.4f}"
        )

    stealth_rank = next(
        rank
        for rank, (event, _) in enumerate(ranked_events, start=1)
        if event["event_id"] == STEALTH_EVENT_ID
    )
    print(
        f"\nStealth event {STEALTH_EVENT_ID} rank: "
        f"{stealth_rank} of {len(EVENTS)}"
    )


if __name__ == "__main__":
    main()

What does the output logic do?

  • ranked_events receives the complete low-to-high score ordering.
  • ranked_events[:TOP_N] selects the first three events for review.
  • enumerate() gives each displayed event a rank beginning at one.
  • stealth_rank records the position of evt-042 across all 43 events.
  • The final guard runs main() when you launch this file directly.
  • Save anomaly_detector.py.

Before you run the detector, do you expect the model to repeat the rule's miss or surface evt-042?

  • Run the complete anomaly detector with this command:
python anomaly_detector.py

What should I see?

You should see three ranked event lines beneath Top ML-ranked authentication anomalies:. The event evt-042 should appear within those three lines.

The final line reports the stealth event's position across all 43 events. The exact decimal scores are supporting detail.

Stealth event missing from the top three?

  • Check that random_state=42 appears inside the model configuration.
  • Confirm the sorting key uses the score at index 1.
  • Compare FEATURE_NAMES in security_data.py with the feature-building function.

Help me debug why evt-042 is missing from my top three anomalies.

That is the key win. The model surfaces the stealth event that the fixed threshold missed.

✔️ Awesome, I've got everything!

Great work. Make sure anomaly_detector.py is saved with evt-042 visible among the top three results.

ⓧ I'd like to double check the full code

Compare your anomaly_detector.py with the complete file below:

from sklearn.ensemble import IsolationForest

from security_data import EVENTS, FEATURE_NAMES

EXPECTED_ANOMALIES = 3
TOP_N = 3
STEALTH_EVENT_ID = "evt-042"


def build_feature_matrix(events):
    return [
        [event[feature_name] for feature_name in FEATURE_NAMES]
        for event in events
    ]


def rank_events(events):
    feature_matrix = build_feature_matrix(events)
    model = IsolationForest(
        n_estimators=100,
        contamination=EXPECTED_ANOMALIES / len(events),
        random_state=42,
    )
    model.fit(feature_matrix)
    scores = model.decision_function(feature_matrix)

    return sorted(
        zip(events, scores),
        key=lambda event_and_score: event_and_score[1],
    )


def main():
    ranked_events = rank_events(EVENTS)

    print("Top ML-ranked authentication anomalies:")
    for rank, (event, score) in enumerate(ranked_events[:TOP_N], start=1):
        print(
            f"{rank}. {event['event_id']} | user={event['user']} | "
            f"score={score:.4f}"
        )

    stealth_rank = next(
        rank
        for rank, (event, _) in enumerate(ranked_events, start=1)
        if event["event_id"] == STEALTH_EVENT_ID
    )
    print(
        f"\nStealth event {STEALTH_EVENT_ID} rank: "
        f"{stealth_rank} of {len(EVENTS)}"
    )


if __name__ == "__main__":
    main()

How to use this reference

Compare the imports plus constants first. Continue through each function to find any missing lines or indentation differences.

Your detector now combines weak signals into a ranked anomaly queue. Next, you will add analyst-readable reasons before publishing the project.

Explain and Publish the Detector

Your IsolationForest model now ranks evt-042 among the top anomalies. A raw anomaly score gives an analyst little context for deciding why an event deserves attention.

You will add analyst-readable signals with explain_event(). These signals make each ranked event easier to review.

You will also document the workflow in README.md. Then you will use Git to commit the project before publishing it on GitHub.

In this step, get ready to:
  • Add analyst-readable reasons to each ranked anomaly.
  • Document the setup and detector workflow in README.md.
  • Commit the project locally before publishing it on GitHub.
Add analyst-readable reasons

A useful alert tells an analyst which visible signals deserve attention. The reasons in this project describe event fields that contributed useful context.

  • In the Visual Studio Code Explorer sidebar, select anomaly_detector.py.
  • Find the line that defines rank_events(events).
  • Insert explain_event(event) immediately above that line by pasting this function:
def explain_event(event):
    reasons = []

    if event["failed_attempts"] >= 10:
        reasons.append("many failed attempts")
    if event["login_hour"] < 6 or event["login_hour"] >= 22:
        reasons.append("off-hours login")
    if event["country_mismatch"] == 1:
        reasons.append("country mismatch")
    if event["known_device"] == 0:
        reasons.append("new device")
    if event["requests_per_minute"] >= 30:
        reasons.append("high request volume")

    return ", ".join(reasons) if reasons else "unusual feature combination"

What does this function do?

  • The reasons list collects readable signals found in an event.
  • Each condition checks one field that can help an analyst interpret unusual authentication activity.
  • The returned text provides event context. Formal model attribution requires a separate explanation method.
  • In main(), find the line that currently prints only the model score:
            f"score={score:.4f}"

What does the current line show?

The current output shows the anomaly score without any event context. A reviewer must inspect the dataset separately to understand the ranking.

  • Replace the score line with this version:
            f"score={score:.4f} | reasons={explain_event(event)}"

How does the output improve?

The updated line keeps the model score visible. It also calls explain_event(event) so the analyst can review concrete signals beside that score.

  • Save anomaly_detector.py.

Before you run the detector, which signals do you expect to appear beside evt-042?

  • Return to the activated Command Prompt from earlier.
  • Run the updated anomaly detector with this command:
python anomaly_detector.py

What does this check prove?

The command runs the same deterministic ranking workflow with the new explanation layer. Each displayed anomaly now includes a reasons= field.

You should see evt-042 among the top three anomalies. Its row should include off-hours access, a country mismatch, a new device, and high request volume.

Reasons missing from the output?

Check that the updated score line calls explain_event(event). Confirm that the function sits above rank_events(events) with consistent indentation.

Help me debug the missing anomaly reasons.

✔️ Awesome, I've got everything!

That is the explanation layer working. Save anomaly_detector.py before continuing.

ⓧ I'd like to double check the full code

Compare your complete anomaly_detector.py with this version.

from sklearn.ensemble import IsolationForest

from security_data import EVENTS, FEATURE_NAMES

EXPECTED_ANOMALIES = 3
TOP_N = 3
STEALTH_EVENT_ID = "evt-042"


def build_feature_matrix(events):
    return [
        [event[feature_name] for feature_name in FEATURE_NAMES]
        for event in events
    ]


def explain_event(event):
    reasons = []

    if event["failed_attempts"] >= 10:
        reasons.append("many failed attempts")
    if event["login_hour"] < 6 or event["login_hour"] >= 22:
        reasons.append("off-hours login")
    if event["country_mismatch"] == 1:
        reasons.append("country mismatch")
    if event["known_device"] == 0:
        reasons.append("new device")
    if event["requests_per_minute"] >= 30:
        reasons.append("high request volume")

    return ", ".join(reasons) if reasons else "unusual feature combination"


def rank_events(events):
    feature_matrix = build_feature_matrix(events)
    model = IsolationForest(
        n_estimators=100,
        contamination=EXPECTED_ANOMALIES / len(events),
        random_state=42,
    )
    model.fit(feature_matrix)
    scores = model.decision_function(feature_matrix)

    return sorted(
        zip(events, scores),
        key=lambda event_and_score: event_and_score[1],
    )


def main():
    ranked_events = rank_events(EVENTS)

    print("Top ML-ranked authentication anomalies:")
    for rank, (event, score) in enumerate(ranked_events[:TOP_N], start=1):
        print(
            f"{rank}. {event['event_id']} | user={event['user']} | "
            f"score={score:.4f} | reasons={explain_event(event)}"
        )

    stealth_rank = next(
        rank
        for rank, (event, _) in enumerate(ranked_events, start=1)
        if event["event_id"] == STEALTH_EVENT_ID
    )
    print(
        f"\nStealth event {STEALTH_EVENT_ID} rank: "
        f"{stealth_rank} of {len(EVENTS)}"
    )


if __name__ == "__main__":
    main()
Document and check the project

A reviewer needs enough information to understand the security problem before running the code. Your README captures the comparison between the fixed rule and the machine-learning detector.

  • In the Visual Studio Code Explorer sidebar, select the security-log-anomaly-detector folder.
  • Use the new-file control in the Explorer sidebar.
  • Enter README.md as the file name.
  • Add the project overview by pasting this first section:
# Security Log Anomaly Detector

A compact cybersecurity machine-learning project that compares a fixed failed-login rule with an unsupervised anomaly detector.

## What it demonstrates

- A threshold catches an obvious brute-force event.
- The same threshold misses a stealth event composed of several weaker signals.
- IsolationForest ranks unusual authentication events without requiring labeled attack data.
- Analyst-facing reasons make the ranked output easier to review.

## Environment

- Python 3.11 or newer
- scikit-learn 1.9.0
- Windows Command Prompt

What does this section communicate?

The opening states the security problem in one sentence. The demonstration list shows the progression from a fixed threshold to ranked anomalies with readable context.

The environment section records the runtime and dependency requirements. That gives reviewers a reproducible starting point.

  • Save README.md.

The editor should now show the project title, four demonstration points, and the required environment.

  • Place your cursor after the Windows Command Prompt line.
  • Add the setup, run instructions, and security note by pasting this section:
## Setup

```text
python -m venv sklearn-env
sklearn-env\Scripts\activate
python -m pip install -r requirements.txt
```

## Run the rule detector

```text
python rule_detector.py
```

Confirm that `evt-041` is flagged and `evt-042` is missed.

## Run the anomaly detector

```text
python anomaly_detector.py
```

Confirm that `evt-042` appears among the top three ranked anomalies.

## Security note

The dataset is synthetic. It contains no real usernames, credentials, customer information, or production log data.

Why include these details?

  • The setup commands let another person recreate the project environment.
  • The run sections identify the result that each detector demonstrates.
  • The security note makes the synthetic data boundary explicit.
  • Save README.md.

Your README should now end with a security note explaining that the dataset contains no production log data.

Before you rerun both scripts, which event should separate the rule detector from the anomaly detector?

  • Return to the activated Command Prompt from earlier.
  • Check both documented workflows by running these commands:
python rule_detector.py
python anomaly_detector.py

What does this project check prove?

The first command proves the fixed rule still catches evt-041 while missing evt-042.

The second command proves the model still ranks evt-042 among the top three with readable reasons.

You should see Stealth event evt-042 flagged: False in the rule output. The anomaly output should place the same event among the displayed top three.

Did one of the detector checks fail?

Confirm the Command Prompt is still inside the security-log-anomaly-detector folder. Check that the prompt begins with the activated sklearn-env environment.

Help me restore both detector checks.

✔️ Awesome, I've got everything!

Your documentation now matches the workflow you just tested. Make sure README.md is saved.

ⓧ I'd like to double check the full code

Compare your complete README.md with this version.

# Security Log Anomaly Detector

A compact cybersecurity machine-learning project that compares a fixed failed-login rule with an unsupervised anomaly detector.

## What it demonstrates

- A threshold catches an obvious brute-force event.
- The same threshold misses a stealth event composed of several weaker signals.
- IsolationForest ranks unusual authentication events without requiring labeled attack data.
- Analyst-facing reasons make the ranked output easier to review.

## Environment

- Python 3.11 or newer
- scikit-learn 1.9.0
- Windows Command Prompt

## Setup

```text
python -m venv sklearn-env
sklearn-env\Scripts\activate
python -m pip install -r requirements.txt
```

## Run the rule detector

```text
python rule_detector.py
```

Confirm that `evt-041` is flagged and `evt-042` is missed.

## Run the anomaly detector

```text
python anomaly_detector.py
```

Confirm that `evt-042` appears among the top three ranked anomalies.

## Security note

The dataset is synthetic. It contains no real usernames, credentials, customer information, or production log data.
Publish the repository

A commit creates a reviewable snapshot of the working project. The remote repository gives that snapshot a public home.

  • Initialize the local repository on the main branch by running this command:
git init -b main

What does this command do?

The command initializes Git tracking inside security-log-anomaly-detector. It names the first branch main.

  • Inspect the untracked project files by running:
git status

What should Git report?

Git should list the source files, dependency file, ignore file, and README as untracked project files. The sklearn-env folder should stay absent because .gitignore excludes it.

  • Stage the project files by running:
git add .

What does staging prepare?

Staging selects the current project files for the next snapshot. Files excluded by .gitignore remain outside that snapshot.

  • Create the project commit by running:
git commit -m "Build security log anomaly detector"

What does the commit capture?

The commit records the staged detector code and documentation with the message Build security log anomaly detector.

  • Confirm the committed snapshot by running:
git status

What should the status confirm?

The status should confirm that the committed project has no pending file changes. That means the local snapshot is ready to publish.

Commit not completing?

Git may ask you to configure an author name or email before creating your first commit. Follow the instructions shown in your terminal before rerunning the commit command.

Help me complete my Git commit.

  • Go to GitHub in your browser.
  • Sign in to your account.
  • Select New repository.
  • Enter security-log-anomaly-detector in the repository name field.
  • Select Public for the repository visibility.
  • Keep the repository initialization options empty.
  • Select Create repository.

Why leave the repository empty?

Your local commit already contains the README and ignore file. An empty remote can accept that history without introducing a conflicting initial commit.

Your public repository is ready to receive the detector. The remaining commands connect your local snapshot to it.

  • Copy the HTTPS remote URL from the new repository page.
  • Store the copied URL here: https://github.com/your-username/security-log-anomaly-detector.git.
  • Add the copied repository as origin by running:
git remote add origin [[REMOTE_URL="https://github.com/your-username/security-log-anomaly-detector.git"]]

What does the remote connection do?

The command saves the GitHub repository address under the name origin. Git can now send your local main branch to that destination.

Your first authenticated push may open a browser sign-in through Git Credential Manager. The browser flow authorizes Git to access your GitHub account.

Before you push, do you expect the empty GitHub repository to gain the same files as your local commit?

  • Push the local main branch by running:
git push -u origin main

What does this push do?

The command uploads the committed main branch to origin. The upstream connection lets future pushes target the same branch.

  • Complete the Git Credential Manager browser sign-in if it opens.
  • Return to the Command Prompt after authorization finishes.

Push not reaching GitHub?

Confirm that you copied the HTTPS URL from the repository you just created. Check that the repository name is security-log-anomaly-detector.

If a browser sign-in opened, finish that flow before retrying the push. Keep the Command Prompt open while authentication completes.

Help me troubleshoot my GitHub push.

Before the final check, do you expect the detector to preserve its ranking after being committed and published?

  • Run the finished detector one more time with this command:
python anomaly_detector.py

What should the final run confirm?

Each displayed anomaly should include readable reasons. The ranked top three should still include evt-042.

  • Return to the new repository page in your browser.
  • Refresh the repository page.

The branch selector should show main. The file list should show README.md and requirements.txt.

You should also see security_data.py, rule_detector.py, and anomaly_detector.py on the published branch.

You have turned a raw anomaly ranking into an explainable public project. Reviewers can now reproduce the workflow and inspect why each event deserves attention.

Secret mission

Add Security-Aware Prioritization

Turn the anomaly ranking into a security triage queue. Privileged-account events gain priority while known service activity is reduced.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your detector available, pause your work, or remove it completely. The local workspace and your GitHub Free repository have no ongoing required charges.

Resources you used:

  • Local project folder security-log-anomaly-detector containing the source files, the sklearn-env virtual environment, and the local Git repository.
  • Public GitHub repository named security-log-anomaly-detector.

Keep everything running

No action is needed. Choose this if you want to keep testing the detector or sharing the repository.

  • Keep the security-log-anomaly-detector folder so its source files and virtual environment remain available.
  • Keep the public GitHub repository available so its files remain visible online.

Pause - I'll come back to this later

Close your local tools while keeping the project files for another session. Your repository can remain public at no charge, or you can make it private.

  • Close the activated Command Prompt from earlier.
  • Leave the GitHub repository unchanged if you want it to remain public.
  • Return to the GitHub repository from earlier if you want to make it private.
  • Select Settings in the repository navigation.
  • Scroll to Danger Zone.
  • Select Change repository visibility.
  • Select Make this repository private.
  • Complete GitHub's confirmation prompts.

Delete - I don't want to use this again

Remove the remote repository and the local project folder if you want a complete reset. These deletions are permanent, so save anything you want to keep before continuing.

  • Return to the GitHub repository from earlier.
  • Select Settings in the repository navigation.
  • Select General.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Complete GitHub's confirmation prompts.

The remote repository is removed from your GitHub account after the confirmation completes.

  • Delete the local security-log-anomaly-detector folder by running these commands in the activated Command Prompt from earlier:
cd ..
rmdir /s /q security-log-anomaly-detector

What do these commands remove?

  • The cd command moves the Command Prompt to the folder directly above the project.
  • The rmdir command removes the project folder, its source files, its virtual environment, and its local repository history.
  • Check the parent folder in File Explorer.

You should no longer see the security-log-anomaly-detector folder.

Is the folder still there?

  • Close your code editor if it still has the project folder open.
  • Close any other terminal that is using the project folder.
  • Run the deletion commands again from the folder directly above security-log-anomaly-detector.

Help me remove this Windows project folder.

Nice Work!

Nice Work!

Excellent work. Your published Python security log analyzer exposes a fixed-threshold blind spot before surfacing the stealth event with IsolationForest.

You've learned how to:

  • Use rule-based detection to catch evt-041. Show that the same fixed threshold misses evt-042 despite its combined warning signs.
  • Use scikit-learn's IsolationForest to rank multivariate anomalies. Add analyst-facing reasons for each displayed event.
  • Package the detector as a reproducible Windows project with a pinned dependency. Publish the complete source in a GitHub repository using Git version control.
  • Secret Mission: Add context-aware prioritization to the model ranking. Promote privileged evt-042 for analyst attention. Reduce known-service evt-043 noise.

Ready to quiz yourself?