I recently developed an interactive security awareness training system that transforms a static office image into a clickable security threat detection exercise. This post will walk you through creating your own version, from image mapping to automated file generation and user tracking through IIS logs.
Overview
The system presents users with an office scene where they must identify security threats while avoiding decoy items. Their interactions are tracked through IIS logs, providing detailed analytics on user performance.
The website then provides feedback when threats or decoys are found as you can see below, however the goal is to find only threats not decoys:
Requirements
- A base image showing an office/workplace environment
- Image editing software to create individual popup images
- Access to image-map.net for creating clickable hotspots
- IIS web server with Windows Authentication enabled
- PowerShell for running the automation scripts
Step 1: Prepare Your Images
First, we need to create:
- One main office overview image (e.g.,
security-overview.PNG) - Individual images for each security threat (e.g.,
dooropen.PNG,tailgating.PNG) - Individual images for each decoy item (e.g.,
no-keypad.PNG,no-monitor.PNG) - Status indicator images (
green-tick.PNGandred-cross.PNG)
Important naming convention: Decoy images must start with "no-" prefix.
Step 2: Create the Image Map
- Navigate to http://www.image-map.net/
- Upload your main office image
- Draw rectangular or circular areas over each security threat and decoy
- Critical: Name each area exactly as your image filename (without .PNG extension)
- For a threat: if the area is named
dooropen, you needdooropen.PNG - For a decoy: if the area is named
no-keypad, you needno-keypad.PNG
- For a threat: if the area is named
- Generate the HTML code
- Save the complete output as
map.txt
Your map.txt should look similar to this:
<!-- Image Map Generated by http://www.image-map.net/ -->
<img src="security-overview.PNG" usemap="#image-map">
<map name="image-map">
<area target="" alt="" title="" href="alarmoffline" coords="1012,197,969,153" shape="rect">
<area target="" alt="" title="" href="tailgating" coords="284,449,461,590" shape="rect">
<area target="" alt="" title="" href="no-keypad" coords="309,88,26" shape="circle">
<!-- more areas... -->
</map>
Step 3: Run the Generator Script
I created a PowerShell script that reads map.txt and automatically generates both the HTML interface and IIS log parser. Save this as infographics-generator.ps1:
# Infographic File Generator
# Reads map.txt and generates index.html and iislogparser.ps1
param(
[string]$MapFile = "map.txt",
[string]$OutputFolder = ".",
[string]$BaseImageName = "security-overview.PNG"
)
Write-Host "=== SecSpot File Generator ===" -ForegroundColor Cyan
Write-Host "Reading from: $MapFile" -ForegroundColor Yellow
Write-Host "Output folder: $OutputFolder" -ForegroundColor Yellow
Write-Host ""
# Read and parse map.txt
if (-not (Test-Path $MapFile)) {
Write-Host "Error: $MapFile not found!" -ForegroundColor Red
exit
}
Write-Host "Parsing map file..." -ForegroundColor Green
$MapContent = Get-Content $MapFile -Raw
# Extract all area tags
$AreaPattern = '<area[^>]+>'
$Areas = [regex]::Matches($MapContent, $AreaPattern)
# Extract image source from img tag
$ImgPattern = '<img src="([^"]+)"'
$ImgMatch = [regex]::Match($MapContent, $ImgPattern)
$ImageSource = if ($ImgMatch.Success) { $ImgMatch.Groups[1].Value } else { $BaseImageName }
# Parse areas and categorize threats vs decoys
$Threats = @()
$Decoys = @()
$AllAreas = @()
foreach ($Area in $Areas) {
$AreaTag = $Area.Value
# Extract href value
$HrefPattern = 'href="([^"]+)"'
$HrefMatch = [regex]::Match($AreaTag, $HrefPattern)
if ($HrefMatch.Success) {
$HrefValue = $HrefMatch.Groups[1].Value
# Extract coordinates
$CoordsPattern = 'coords="([^"]+)"'
$CoordsMatch = [regex]::Match($AreaTag, $CoordsPattern)
$Coords = if ($CoordsMatch.Success) { $CoordsMatch.Groups[1].Value } else { "" }
# Extract shape
$ShapePattern = 'shape="([^"]+)"'
$ShapeMatch = [regex]::Match($AreaTag, $ShapePattern)
$Shape = if ($ShapeMatch.Success) { $ShapeMatch.Groups[1].Value } else { "rect" }
# Create area object
$AreaObj = @{
Name = $HrefValue
Coords = $Coords
Shape = $Shape
}
$AllAreas += $AreaObj
# Categorize as threat or decoy
if ($HrefValue.StartsWith("no-")) {
$Decoys += $HrefValue
} else {
$Threats += $HrefValue
}
}
}
Write-Host "Found $($Threats.Count) threats and $($Decoys.Count) decoys" -ForegroundColor Green
# Generate index.html
Write-Host "`nGenerating index.html..." -ForegroundColor Cyan
$IndexHtml = @'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Overview - Find the Hidden Risks</title>
<!-- Hotjar Tracking Code for SecSpot -->
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:6540139,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.counters-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.counter-display {
color: white;
padding: 15px 30px;
border-radius: 10px;
font-size: 24px;
font-weight: bold;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.threats-counter {
background-color: #28a745;
}
.decoys-counter {
background-color: #dc3545;
}
.counter-number {
color: #ffd700;
font-size: 28px;
}
.container {
max-width: 100%;
padding: 20px;
}
img {
max-width: 100%;
height: auto;
display: block;
}
/* Popup Modal Styles */
.popup-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.popup-content {
background-color: white;
padding: 20px;
border-radius: 15px;
text-align: center;
max-width: 500px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
position: relative;
}
.popup-card-image {
max-width: 100%;
height: auto;
border-radius: 10px;
}
.tick-cross-icon {
position: absolute;
top: 10px;
right: 10px;
width: 60px;
height: 60px;
}
.close-btn {
margin-top: 20px;
padding: 12px 40px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.close-btn:hover {
background-color: #0056b3;
}
.decoy-content {
padding: 20px;
}
.decoy-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
}
.decoy-title {
font-size: 28px;
color: #dc3545;
margin-bottom: 15px;
}
.decoy-text {
font-size: 18px;
color: #666;
margin-bottom: 20px;
}
/* Click indicator on main image */
.click-indicator {
position: absolute;
width: 40px;
height: 40px;
pointer-events: none;
z-index: 100;
animation: popIn 0.3s ease-out;
}
@keyframes popIn {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.image-wrapper {
position: relative;
display: inline-block;
}
</style>
</head>
<body>
<div class="counters-container">
<div class="counter-display threats-counter">
Security Threats Found: <span class="counter-number" id="threat-counter">0</span>/___THREAT_COUNT___
</div>
<div class="counter-display decoys-counter">
Decoys Found: <span class="counter-number" id="decoy-counter">0</span>/___DECOY_COUNT___
</div>
</div>
<div class="container">
<div class="image-wrapper" id="image-wrapper">
<!-- Image Map Generated by http://www.image-map.net/ -->
<img src="___IMAGE_SOURCE___" usemap="#image-map" alt="Office Security Overview" id="main-image">
<map name="image-map">
___MAP_AREAS___
</map>
</div>
</div>
<!-- Popup Modal -->
<div id="popup" class="popup-overlay">
<div class="popup-content" id="popup-content">
<!-- Content will be inserted here by JavaScript -->
</div>
</div>
<script>
// Track found threats and decoys
let threatsFound = 0;
let decoysFound = 0;
const foundThreats = new Set();
const foundDecoys = new Set();
// Get all area elements
const areas = document.querySelectorAll('area');
areas.forEach(area => {
area.addEventListener('click', function(e) {
e.preventDefault(); // Prevent any default behavior
const riskType = this.getAttribute('data-risk');
const coords = this.getAttribute('coords');
const shape = this.getAttribute('shape');
// Calculate click position
const clickPos = calculateClickPosition(coords, shape);
// Check if this is a decoy (starts with "no-")
if (riskType.startsWith('no-')) {
// Wrong answer - not a security risk
addClickIndicator(clickPos.x, clickPos.y, 'red-cross.PNG');
// Create unique ID for this decoy based on coordinates
const decoyId = coords;
if (!foundDecoys.has(decoyId)) {
foundDecoys.add(decoyId);
decoysFound++;
updateDecoyCounter();
}
showNotSecurePopup(riskType);
} else {
// Correct answer - security risk found
addClickIndicator(clickPos.x, clickPos.y, 'green-tick.PNG');
// Increment counter only if this threat hasn't been found yet
if (!foundThreats.has(riskType)) {
foundThreats.add(riskType);
threatsFound++;
updateThreatCounter();
}
showSecurityRiskPopup(riskType);
}
});
});
function calculateClickPosition(coords, shape) {
const coordArray = coords.split(',').map(Number);
let x, y;
if (shape === 'rect') {
// Rectangle: x1,y1,x2,y2 - use center
x = (coordArray[0] + coordArray[2]) / 2;
y = (coordArray[1] + coordArray[3]) / 2;
} else if (shape === 'circle') {
// Circle: x,y,radius - use center
x = coordArray[0];
y = coordArray[1];
}
return { x, y };
}
function addClickIndicator(x, y, iconSrc) {
const wrapper = document.getElementById('image-wrapper');
const img = document.getElementById('main-image');
const indicator = document.createElement('img');
indicator.src = iconSrc;
indicator.className = 'click-indicator';
// Get the actual displayed size of the image
const imgRect = img.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
// Calculate scale factor between natural size and displayed size
const scaleX = imgRect.width / img.naturalWidth;
const scaleY = imgRect.height / img.naturalHeight;
// Scale the coordinates
const scaledX = x * scaleX;
const scaledY = y * scaleY;
indicator.style.left = (scaledX - 20) + 'px'; // Center the 40px icon
indicator.style.top = (scaledY - 20) + 'px';
wrapper.appendChild(indicator);
}
function updateThreatCounter() {
const counter = document.getElementById('threat-counter');
counter.textContent = threatsFound;
// Celebrate when all threats found!
if (threatsFound === ___THREAT_COUNT___) {
setTimeout(() => {
alert('🎉 Congratulations! You found all ___THREAT_COUNT___ security threats!');
}, 500);
}
}
function updateDecoyCounter() {
const counter = document.getElementById('decoy-counter');
counter.textContent = decoysFound;
}
function showSecurityRiskPopup(riskType) {
const popup = document.getElementById('popup');
const content = document.getElementById('popup-content');
const imageSrc = riskType + '.PNG';
content.innerHTML = `
<img class="tick-cross-icon" src="green-tick.PNG" alt="Correct">
<img class="popup-card-image" src="${imageSrc}" alt="Security Risk">
<button class="close-btn" onclick="closePopup()">Continue</button>
`;
popup.style.display = 'flex';
}
function showNotSecurePopup(decoyType) {
const popup = document.getElementById('popup');
const content = document.getElementById('popup-content');
// For decoys, show both the red cross and the specific decoy image
const decoyImageSrc = decoyType + '.PNG';
content.innerHTML = `
<img class="tick-cross-icon" src="red-cross.PNG" alt="Incorrect">
<img class="popup-card-image" src="${decoyImageSrc}" alt="Not a Security Risk">
<button class="close-btn" onclick="closePopup()">Continue Looking</button>
`;
popup.style.display = 'flex';
}
function closePopup() {
const popup = document.getElementById('popup');
popup.style.display = 'none';
}
// Close popup when clicking outside the content box
document.getElementById('popup').addEventListener('click', function(e) {
if (e.target === this) {
closePopup();
}
});
// Close popup with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePopup();
}
});
</script>
</body>
</html>
'@
# Build area tags
$AreaTags = ""
foreach ($Area in $AllAreas) {
$AreaTags += " <area alt=`"`" title=`"`" data-risk=`"$($Area.Name)`" coords=`"$($Area.Coords)`" shape=`"$($Area.Shape)`" style=`"cursor: pointer;`">`n"
}
# Replace placeholders
$IndexHtml = $IndexHtml -replace '___THREAT_COUNT___', $Threats.Count
$IndexHtml = $IndexHtml -replace '___DECOY_COUNT___', $Decoys.Count
$IndexHtml = $IndexHtml -replace '___IMAGE_SOURCE___', $ImageSource
$IndexHtml = $IndexHtml -replace '___MAP_AREAS___', $AreaTags.TrimEnd()
# Save index.html
$IndexPath = Join-Path $OutputFolder "index.html"
$IndexHtml | Out-File -FilePath $IndexPath -Encoding UTF8
Write-Host "Created: $IndexPath" -ForegroundColor Green
# Generate iislogparser.ps1
Write-Host "`nGenerating iislogparser.ps1..." -ForegroundColor Cyan
# Convert arrays to PowerShell array format
$ThreatsArrayStr = ($Threats | ForEach-Object { " `"$_`"" }) -join ",`n"
$DecoysArrayStr = ($Decoys | ForEach-Object { " `"$_`"" }) -join ",`n"
$IISParserScript = @"
# SecSpot IIS Log Analyzer
# Analyzes last 7 days of IIS logs for SecSpot usage
param(
[string]`$ServerName = "st1w1660",
[string]`$LogPath = "\\st1w1660\c$\inetpub\logs\LogFiles\W3SVC1",
[int]`$DaysToAnalyze = 7,
[string]`$VirtualDirectory = "SecSpot2" # Virtual directory parameter
)
# Calculate date threshold
`$DateThreshold = (Get-Date).AddDays(-`$DaysToAnalyze)
Write-Host "=== SecSpot Usage Analyzer ===" -ForegroundColor Cyan
Write-Host "Server: `$ServerName" -ForegroundColor Yellow
Write-Host "Virtual Directory: /`$VirtualDirectory" -ForegroundColor Yellow
Write-Host "Analyzing logs from: `$(`$DateThreshold.ToString('yyyy-MM-dd'))" -ForegroundColor Yellow
Write-Host ""
# Get log files from last 7 days
Write-Host "Retrieving log files..." -ForegroundColor Green
`$LogFiles = Get-ChildItem -Path `$LogPath -Filter "*.log" |
Where-Object { `$_.LastWriteTime -ge `$DateThreshold } |
Sort-Object LastWriteTime
if (`$LogFiles.Count -eq 0) {
Write-Host "No log files found in the specified date range!" -ForegroundColor Red
exit
}
Write-Host "Found `$(`$LogFiles.Count) log file(s) to analyze``n" -ForegroundColor Green
# Initialize tracking variables
`$UserActivity = @{}
`$TotalRequests = 0
# Define threats and decoys based on the HTML structure
# Threats are the actual security risks
`$ThreatsList = @(
$ThreatsArrayStr
)
# Decoys start with "no-"
`$DecoysList = @(
$DecoysArrayStr
)
# Process each log file
foreach (`$LogFile in `$LogFiles) {
Write-Host "Processing: `$(`$LogFile.Name)..." -ForegroundColor Gray
`$Content = Get-Content -Path `$LogFile.FullName
foreach (`$Line in `$Content) {
# Skip comment lines
if (`$Line -match "^#") { continue }
# Check if line contains virtual directory reference (case-insensitive)
if (`$Line -match `$VirtualDirectory) {
`$TotalRequests++
# Parse the log line (space-delimited)
`$Fields = `$Line -split '\s+'
# IIS W3C Extended format fields
if (`$Fields.Count -ge 9) {
`$Date = `$Fields[0]
`$Time = `$Fields[1]
`$Method = `$Fields[3]
`$UriStem = `$Fields[4]
`$Username = `$Fields[7] # STWATER\username format
`$ClientIP = `$Fields[8]
`$StatusCode = if (`$Fields.Count -ge 12) { `$Fields[11] } else { "Unknown" }
# Clean up username (remove domain prefix if present)
if (`$Username -match "\\") {
`$Username = `$Username.Split('\')[-1]
}
# Skip if username is empty or is a dash
if ([string]::IsNullOrWhiteSpace(`$Username) -or `$Username -eq "-") {
`$Username = "Anonymous"
}
# Initialize user tracking if needed
if (-not `$UserActivity.ContainsKey(`$Username)) {
`$UserActivity[`$Username] = [PSCustomObject]@{
Username = `$Username
ClientIP = `$ClientIP
FirstAccess = "`$Date `$Time"
LastAccess = "`$Date `$Time"
TotalRequests = 0
PageViews = 0
ThreatsFound = 0
DecoysClicked = 0
Failed = "no" # Defaults to "no"
ThreatsViewed = New-Object System.Collections.ArrayList
DecoysViewed = New-Object System.Collections.ArrayList
ImagesViewed = New-Object System.Collections.ArrayList
StatusImages = ""
ThreatsDetails = ""
DecoysDetails = ""
}
}
# Update user stats
`$UserActivity[`$Username].TotalRequests++
`$UserActivity[`$Username].LastAccess = "`$Date `$Time"
# Track specific resource types
if (`$UriStem -match "$ImageSource") {
`$UserActivity[`$Username].PageViews++
}
# Check for threat images
elseif (`$UriStem -match "/(`$(`$ThreatsList -join '|'))\.PNG") {
`$ThreatType = `$Matches[1]
if (`$UserActivity[`$Username].ThreatsViewed -notcontains `$ThreatType) {
[void]`$UserActivity[`$Username].ThreatsViewed.Add(`$ThreatType)
}
}
# Check for decoy images
elseif (`$UriStem -match "/(`$(`$DecoysList -join '|'))\.PNG") {
`$DecoyType = `$Matches[1]
if (`$UserActivity[`$Username].DecoysViewed -notcontains `$DecoyType) {
[void]`$UserActivity[`$Username].DecoysViewed.Add(`$DecoyType)
}
}
# Track status images
elseif (`$UriStem -match "(green-tick|red-cross)\.PNG") {
`$ImageType = `$Matches[1]
if (`$UserActivity[`$Username].ImagesViewed -notcontains `$ImageType) {
[void]`$UserActivity[`$Username].ImagesViewed.Add(`$ImageType)
}
}
}
}
}
}
# Update calculated fields for each user
foreach (`$User in `$UserActivity.Values) {
`$User.ThreatsFound = `$User.ThreatsViewed.Count
`$User.DecoysClicked = `$User.DecoysViewed.Count
`$User.StatusImages = `$User.ImagesViewed -join '; '
`$User.ThreatsDetails = `$User.ThreatsViewed -join '; '
`$User.DecoysDetails = `$User.DecoysViewed -join '; '
# Set Failed to "yes" only if 2 or more decoys clicked
if (`$User.DecoysViewed.Count -ge 2) {
`$User.Failed = "yes"
}
}
# Display Results
Write-Host "``n========================================" -ForegroundColor Cyan
Write-Host " USAGE SUMMARY" -ForegroundColor Cyan
Write-Host "========================================``n" -ForegroundColor Cyan
Write-Host "Total `$VirtualDirectory Requests: `$TotalRequests" -ForegroundColor Yellow
Write-Host "Unique Users: `$(`$UserActivity.Count)" -ForegroundColor Yellow
Write-Host ""
if (`$UserActivity.Count -eq 0) {
Write-Host "No `$VirtualDirectory usage found in the analyzed logs." -ForegroundColor Red
exit
}
# Sort users by last access (most recent first)
`$SortedUsers = `$UserActivity.Values | Sort-Object LastAccess -Descending
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " USER ACTIVITY REPORT" -ForegroundColor Cyan
Write-Host "========================================``n" -ForegroundColor Cyan
foreach (`$User in `$SortedUsers) {
Write-Host "User: `$(`$User.Username)" -ForegroundColor Green
Write-Host " Client IP: `$(`$User.ClientIP)" -ForegroundColor White
Write-Host " First Access: `$(`$User.FirstAccess)" -ForegroundColor White
Write-Host " Last Access: `$(`$User.LastAccess)" -ForegroundColor White
Write-Host " Total Requests: `$(`$User.TotalRequests)" -ForegroundColor White
Write-Host " Page Views: `$(`$User.PageViews)" -ForegroundColor White
if (`$User.ImagesViewed.Count -gt 0) {
Write-Host " Status Images: `$(`$User.StatusImages)" -ForegroundColor Cyan
}
# Show threats found
if (`$User.ThreatsViewed.Count -gt 0) {
Write-Host " Threats Found: `$(`$User.ThreatsFound)/$($Threats.Count)" -ForegroundColor Yellow
Write-Host " - `$(`$User.ThreatsDetails -replace '; ', "``n - ")" -ForegroundColor Gray
} else {
Write-Host " Threats Found: 0/$($Threats.Count)" -ForegroundColor Gray
}
# Show decoys clicked
if (`$User.DecoysViewed.Count -gt 0) {
Write-Host " Decoys Clicked: `$(`$User.DecoysClicked)/$($Decoys.Count)" -ForegroundColor Red
Write-Host " - `$(`$User.DecoysDetails -replace '; ', "``n - ")" -ForegroundColor Gray
Write-Host " Failed: `$(`$User.Failed)" -ForegroundColor Red
} else {
Write-Host " Decoys Clicked: 0/$($Decoys.Count)" -ForegroundColor Green
Write-Host " Failed: `$(`$User.Failed)" -ForegroundColor Green
}
Write-Host ""
}
# Summary Statistics
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " STATISTICS" -ForegroundColor Cyan
Write-Host "========================================``n" -ForegroundColor Cyan
`$UsersWhoCompleted = (`$SortedUsers | Where-Object { `$_.ThreatsFound -eq $($Threats.Count) }).Count
`$UsersWhoFailed = (`$SortedUsers | Where-Object { `$_.Failed -eq "yes" }).Count
`$UsersWhoPassed = (`$SortedUsers | Where-Object { `$_.Failed -eq "no" }).Count
`$TotalThreats = (`$SortedUsers | ForEach-Object { `$_.ThreatsFound } | Measure-Object -Sum).Sum
`$TotalDecoys = (`$SortedUsers | ForEach-Object { `$_.DecoysClicked } | Measure-Object -Sum).Sum
`$AverageThreatsFound = if (`$SortedUsers.Count -gt 0) { `$TotalThreats / `$SortedUsers.Count } else { 0 }
`$AverageDecoysClicked = if (`$SortedUsers.Count -gt 0) { `$TotalDecoys / `$SortedUsers.Count } else { 0 }
Write-Host "Users who found all $($Threats.Count) threats: `$UsersWhoCompleted" -ForegroundColor Green
Write-Host "Users who clicked decoys (failed): `$UsersWhoFailed" -ForegroundColor Red
Write-Host "Users who avoided all decoys (passed): `$UsersWhoPassed" -ForegroundColor Green
Write-Host "Average threats found per user: `$([math]::Round(`$AverageThreatsFound, 2))" -ForegroundColor Yellow
Write-Host "Average decoys clicked per user: `$([math]::Round(`$AverageDecoysClicked, 2))" -ForegroundColor Yellow
Write-Host ""
# Export to CSV option
`$ExportChoice = Read-Host "Would you like to export this data to CSV? (Y/N)"
if (`$ExportChoice -eq "Y" -or `$ExportChoice -eq "y") {
`$ExportPath = "`${VirtualDirectory}_Usage_Report_`$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
# Export using the PSCustomObject directly
`$SortedUsers | Select-Object Username, ClientIP, FirstAccess, LastAccess, TotalRequests, PageViews, ThreatsFound, DecoysClicked, Failed, ThreatsDetails, DecoysDetails, StatusImages |
Export-Csv -Path `$ExportPath -NoTypeInformation
Write-Host "``nReport exported to: `$ExportPath" -ForegroundColor Green
}
Write-Host "``n========================================" -ForegroundColor Cyan
Write-Host "Analysis Complete!" -ForegroundColor Cyan
Write-Host "========================================``n" -ForegroundColor Cyan
"@
# Save iislogparser.ps1
$ParserPath = Join-Path $OutputFolder "iislogparser.ps1"
$IISParserScript | Out-File -FilePath $ParserPath -Encoding UTF8
Write-Host "Created: $ParserPath" -ForegroundColor Green
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "File Generation Complete!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Generated files based on:" -ForegroundColor Yellow
Write-Host " - $($Threats.Count) threats: $($Threats -join ', ')" -ForegroundColor Green
Write-Host " - $($Decoys.Count) decoys: $($Decoys -join ', ')" -ForegroundColor Red
Run the generator:
.\infographics-generator.ps1
This creates two files:
1. index.html
The interactive web interface that:
- Displays counters for threats and decoys found
- Shows green ticks for correct identifications
- Shows red crosses for incorrect clicks
- Displays popup images for each clicked item
- Tracks user progress
2. iislogparser.ps1
The log analysis script that generates detailed reports showing:
- User activity (first/last access, total requests)
- Threats found by each user
- Decoys clicked by each user
- Pass/Fail status (users fail if they click 2 or more decoys)
- Summary statistics
- CSV export option
Step 4: Configure IIS
- Create a new virtual directory (e.g.,
/SecuritySpot2) - Critical: Disable Anonymous Authentication
- Enable Windows Authentication
- Copy all files to the virtual directory:
- index.html
- security-overview.PNG
- All threat images (dooropen.PNG, etc.)
- All decoy images (no-keypad.PNG, etc.)
- green-tick.PNG and red-cross.PNG
Step 5: Analyze Results
After users complete the exercise, run the log parser:
.\iislogparser.ps1 -VirtualDirectory "SecuritySpot2" -DaysToAnalyze 7
Example output:
User: JSmith
Client IP: 10.245.187.29
First Access: 2025-11-26 09:15:32
Last Access: 2025-11-26 09:18:45
Total Requests: 22
Page Views: 1
Status Images: green-tick; red-cross
Threats Found: 8/8
- dooropen
- cctvoffline
- alarmoffline
- gateunlocked
- sharingkeys
- tailgating
- managingvisitors
- loitering
Decoys Clicked: 1/5
- no-monitor
Failed: noSuccess Metrics
- Pass: User identifies threats but clicks fewer than 2 decoys
- Fail: User clicks 2 or more decoy items
- Complete: User finds all security threats
This system provides an engaging way to test security awareness while automatically tracking detailed user performance through IIS logs.