Website Blocking — Enterprise Implementation
Architecture Options
Method 1: Local Hosts File → Per machine, simple
Method 2: Group Policy / DNS → Domain-wide, recommended
Method 3: Proxy Server (Squid) → Full content filtering
Method 4: DNS Sinkhole (Pi-hole) → Network-wide blocking
Method 5: Firewall / iptables → Deep packet level
Method 1: Hosts File via GPO (Domain-Wide)
powershell
# Script: Deploy-WebBlocking.ps1
# Push blocked sites to all domain machines via hosts file
param(
[string]$BlockListFile = "C:\Scripts\blocked_sites.txt",
[string]$HostsFile = "C:\Windows\System32\drivers\etc\hosts",
[string]$LogFile = "C:\Logs\WebBlock.log"
)
# ── Blocked Sites List ──────────────────────────────────────────────
$blockedSites = @(
# Personal Email
"gmail.com", "www.gmail.com", "mail.google.com",
"outlook.live.com", "hotmail.com", "yahoo.com", "mail.yahoo.com",
"protonmail.com", "tutanota.com",
# Social Media
"facebook.com", "www.facebook.com",
"twitter.com", "x.com",
"instagram.com", "tiktok.com",
"snapchat.com", "reddit.com",
"linkedin.com", # Remove if needed for business
# File Sharing / Cloud Storage
"wetransfer.com", "dropbox.com",
"mega.nz", "mediafire.com",
"pastebin.com", "paste.ee",
# Streaming
"youtube.com", "www.youtube.com",
"netflix.com", "primevideo.com",
"twitch.tv", "spotify.com",
# Gaming
"steam.com", "store.steampowered.com",
"epicgames.com", "roblox.com"
)
function Write-Log($msg) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$ts $msg" | Tee-Object -FilePath $LogFile -Append
}
# ── Backup original hosts file ──────────────────────────────────────
$backup = "$HostsFile.bak_$(Get-Date -Format 'yyyyMMdd')"
if (-not (Test-Path $backup)) {
Copy-Item $HostsFile $backup
Write-Log "Hosts backup: $backup"
}
# ── Read current hosts ──────────────────────────────────────────────
$currentHosts = Get-Content $HostsFile
# ── Remove old block entries ────────────────────────────────────────
$cleaned = $currentHosts | Where-Object { $_ -notmatch "# CORP-BLOCK" }
# ── Add new block entries ────────────────────────────────────────────
$newEntries = @("", "# ── CORPORATE WEB BLOCKS — DO NOT EDIT ── CORP-BLOCK-START ──")
foreach ($site in $blockedSites) {
$newEntries += "0.0.0.0 $site # CORP-BLOCK"
$newEntries += "0.0.0.0 www.$site # CORP-BLOCK"
}
$newEntries += "# ── CORP-BLOCK-END ──"
# ── Write final hosts file ───────────────────────────────────────────
($cleaned + $newEntries) | Set-Content $HostsFile -Encoding UTF8
Write-Log "Blocked $($blockedSites.Count) sites on $env:COMPUTERNAME"
# ── Flush DNS cache ──────────────────────────────────────────────────
ipconfig /flushdns | Out-Null
Write-Log "DNS cache flushed"
Write-Host "[✔] Web blocking applied: $($blockedSites.Count) sites blocked" -ForegroundColor Green
Method 2: Internet Explorer / Edge GPO Restrictions
Computer Configuration →
Administrative Templates →
Windows Components →
Microsoft Edge →
Block access to a list of URLs
Add URLs:
gmail.com
*facebook.com
*twitter.com
*youtube.com
Via Registry (push via GPO):
powershell
# Block sites in Edge/Chrome via registry
$regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\URLBlocklist"
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
$blockedURLs = @(
"gmail.com", "facebook.com", "twitter.com",
"instagram.com", "youtube.com", "tiktok.com",
"dropbox.com", "wetransfer.com"
)
$i = 1
foreach ($url in $blockedURLs) {
Set-ItemProperty -Path $regPath -Name "$i" -Value $url
$i++
}
Write-Host "[✔] Edge URL blocklist applied via registry"
Method 3: Windows Firewall Rules
powershell
# Block by IP ranges (Gmail/Google IPs)
$googleIPs = @(
"142.250.0.0/15", "172.217.0.0/16",
"74.125.0.0/16", "64.233.160.0/19",
"66.249.64.0/19", "216.58.192.0/19"
)
foreach ($ip in $googleIPs) {
$ruleName = "BLOCK-GOOGLE-$($ip -replace '[./]','-')"
New-NetFirewallRule `
-DisplayName $ruleName `
-Direction Outbound `
-Action Block `
-RemoteAddress $ip `
-Profile Any `
-Enabled True | Out-Null
Write-Host "[+] Blocked: $ip"
}
Part 2: Linux — Multiple Methods
Method 1: Hosts File Blocking
bash
#!/bin/bash
# deploy_web_blocking.sh — Linux hosts file blocker
HOSTS_FILE="/etc/hosts"
BACKUP="/etc/hosts.bak.$(date +%Y%m%d)"
LOG="/var/log/web-blocking.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG"; }
# Blocked sites list
BLOCKED_SITES=(
# Personal Email
"gmail.com" "mail.google.com"
"outlook.live.com" "hotmail.com"
"yahoo.com" "mail.yahoo.com"
"protonmail.com" "tutanota.com"
# Social Media
"facebook.com" "twitter.com" "x.com"
"instagram.com" "tiktok.com"
"snapchat.com" "reddit.com"
# Cloud Storage / File Sharing
"dropbox.com" "wetransfer.com"
"mega.nz" "mediafire.com"
"pastebin.com"
# Streaming
"youtube.com" "netflix.com"
"twitch.tv" "spotify.com"
"primevideo.com"
# Gaming
"steampowered.com" "epicgames.com"
"roblox.com"
)
# Backup
[ ! -f "$BACKUP" ] && cp "$HOSTS_FILE" "$BACKUP" && log "Backup: $BACKUP"
# Remove old entries
sed -i '/# CORP-BLOCK/d' "$HOSTS_FILE"
sed -i '/CORP-BLOCK-START/,/CORP-BLOCK-END/d' "$HOSTS_FILE"
# Add new block entries
{
echo ""
echo "# ── CORPORATE WEB BLOCKS — DO NOT EDIT — CORP-BLOCK-START ──"
for site in "${BLOCKED_SITES[@]}"; do
echo "0.0.0.0 $site # CORP-BLOCK"
echo "0.0.0.0 www.$site # CORP-BLOCK"
done
echo "# ── CORP-BLOCK-END ──"
} >> "$HOSTS_FILE"
# Flush DNS cache
systemd-resolve --flush-caches 2>/dev/null || \
service nscd restart 2>/dev/null || \
resolvectl flush-caches 2>/dev/null
log "Blocked ${#BLOCKED_SITES[@]} sites on $(hostname)"
echo "[✔] Web blocking applied: ${#BLOCKED_SITES[@]} sites blocked"
Method 2: Squid Proxy — Full Content Filtering (Recommended)
bash
# Install Squid
sudo apt install squid -y # Ubuntu/Debian
sudo dnf install squid -y # RHEL/Fedora
Main config — /etc/squid/squid.conf:
squid
# ── Squid Corporate Web Filter ──────────────────────────────────────
# Network ACLs
acl localnet src 192.168.0.0/16
acl localnet src 10.0.0.0/8
acl SSL_ports port 443
acl Safe_ports port 80 443 8080
acl CONNECT method CONNECT
# Block list files
acl blocked_sites dstdomain "/etc/squid/blocked_domains.txt"
acl blocked_words url_regex -i "/etc/squid/blocked_keywords.txt"
acl social_media dstdomain "/etc/squid/social_media.txt"
acl personal_email dstdomain "/etc/squid/personal_email.txt"
acl file_sharing dstdomain "/etc/squid/file_sharing.txt"
# Time ACLs
acl work_hours time MTWHF 08:00-18:00
acl lunch_time time MTWHF 12:00-13:00
# ── DENY RULES ──────────────────────────────────────────────────────
http_access deny blocked_sites
http_access deny personal_email
http_access deny social_media
http_access deny !work_hours # Block everything outside work hours
http_access deny file_sharing
# Allow local network during work hours
http_access allow localnet work_hours
http_access deny all
# Proxy settings
http_port 3128
https_port 3129 intercept ssl-bump \
cert=/etc/squid/ssl/squid-ca.crt \
key=/etc/squid/ssl/squid-ca.key
# SSL Bump for HTTPS inspection
ssl_bump splice all
# Logging
access_log /var/log/squid/access.log combined
cache_log /var/log/squid/cache.log
# Block page
deny_info http://usb-monitor.company.com/blocked.html all
cache_mem 256 MB
maximum_object_size 10 MB
Blocked domains files:
bash
# /etc/squid/personal_email.txt
cat > /etc/squid/personal_email.txt <<EOF
gmail.com
mail.google.com
googlemail.com
outlook.live.com
hotmail.com
live.com
yahoo.com
mail.yahoo.com
ymail.com
protonmail.com
tutanota.com
zoho.com
guerrillamail.com
temp-mail.org
10minutemail.com
EOF
# /etc/squid/social_media.txt
cat > /etc/squid/social_media.txt <<EOF
facebook.com
fbcdn.net
twitter.com
t.co
x.com
instagram.com
cdninstagram.com
tiktok.com
tiktokcdn.com
snapchat.com
reddit.com
redd.it
pinterest.com
tumblr.com
EOF
# /etc/squid/file_sharing.txt
cat > /etc/squid/file_sharing.txt <<EOF
dropbox.com
dropboxusercontent.com
wetransfer.com
we.tl
mega.nz
mediafire.com
rapidshare.com
zippyshare.com
pastebin.com
hastebin.com
EOF
bash
# Start Squid
sudo squid -k parse # Test config
sudo systemctl enable --now squid
sudo systemctl status squid
Method 3: DNS Sinkhole (Network-Wide)
bash
# Install dnsmasq
sudo apt install dnsmasq -y
# Add block entries
cat >> /etc/dnsmasq.conf <<EOF
# Corporate Web Blocks
address=/gmail.com/0.0.0.0
address=/mail.google.com/0.0.0.0
address=/facebook.com/0.0.0.0
address=/twitter.com/0.0.0.0
address=/x.com/0.0.0.0
address=/instagram.com/0.0.0.0
address=/tiktok.com/0.0.0.0
address=/youtube.com/0.0.0.0
address=/dropbox.com/0.0.0.0
address=/wetransfer.com/0.0.0.0
address=/reddit.com/0.0.0.0
address=/snapchat.com/0.0.0.0
address=/netflix.com/0.0.0.0
address=/spotify.com/0.0.0.0
EOF
sudo systemctl restart dnsmasq
Method 4: iptables Firewall Rules
bash
#!/bin/bash
# Block websites at firewall level
# Flush existing web-block rules
iptables -D OUTPUT -j WEB_BLOCK 2>/dev/null
iptables -F WEB_BLOCK 2>/dev/null
iptables -X WEB_BLOCK 2>/dev/null
# Create new chain
iptables -N WEB_BLOCK
iptables -I OUTPUT -j WEB_BLOCK
# Resolve and block domains
DOMAINS=(
"gmail.com" "facebook.com" "twitter.com"
"instagram.com" "tiktok.com" "youtube.com"
"dropbox.com" "wetransfer.com" "reddit.com"
)
for domain in "${DOMAINS[@]}"; do
IPS=$(dig +short "$domain" | grep -E '^[0-9]+\.')
for ip in $IPS; do
iptables -A WEB_BLOCK -d "$ip" -j DROP
echo "[+] Blocked $domain → $ip"
done
done
# Save rules
iptables-save > /etc/iptables/rules.v4
echo "[✔] Firewall web blocking applied"
Part 3: Centralized Block List Manager
Save as /opt/web-blocker/manage_blocklist.py:
python
#!/usr/bin/env python3
"""
Centralized Web Block List Manager
Manage, deploy, and audit website blocking across all machines
"""
import json, os, subprocess, paramiko
from datetime import datetime
CONFIG_FILE = "/opt/web-blocker/config.json"
REPORT_FILE = "/var/www/html/usb-dashboard/webblocker.html"
# ── Default block categories ─────────────────────────────────────────
DEFAULT_CATEGORIES = {
"personal_email": {
"enabled": True,
"sites": ["gmail.com","mail.google.com","hotmail.com",
"yahoo.com","protonmail.com","tutanota.com"]
},
"social_media": {
"enabled": True,
"sites": ["facebook.com","twitter.com","x.com",
"instagram.com","tiktok.com","snapchat.com","reddit.com"]
},
"file_sharing": {
"enabled": True,
"sites": ["dropbox.com","wetransfer.com","mega.nz",
"mediafire.com","pastebin.com"]
},
"streaming": {
"enabled": True,
"sites": ["youtube.com","netflix.com","twitch.tv",
"spotify.com","primevideo.com"]
},
"gaming": {
"enabled": False,
"sites": ["steampowered.com","epicgames.com","roblox.com"]
}
}
def load_config():
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE) as f:
return json.load(f)
return DEFAULT_CATEGORIES
def save_config(config):
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
with open(CONFIG_FILE, "w") as f:
json.dump(config, f, indent=2)
def get_all_blocked_sites(config):
sites = []
for cat, data in config.items():
if data["enabled"]:
sites.extend(data["sites"])
return sorted(set(sites))
def deploy_to_linux(host, ssh_key, blocked_sites):
"""Deploy block list to Linux machine via SSH"""
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, key_filename=ssh_key, timeout=10)
# Build hosts entries
entries = "\n".join([
f"0.0.0.0 {s} # CORP-BLOCK\n0.0.0.0 www.{s} # CORP-BLOCK"
for s in blocked_sites
])
cmd = f"""
sed -i '/# CORP-BLOCK/d' /etc/hosts
echo '# CORP-BLOCK-START' >> /etc/hosts
echo '{entries}' >> /etc/hosts
echo '# CORP-BLOCK-END' >> /etc/hosts
systemd-resolve --flush-caches 2>/dev/null || true
echo "OK"
"""
_, stdout, _ = client.exec_command(cmd)
result = stdout.read().decode().strip()
client.close()
return result == "OK"
except Exception as e:
print(f"[ERROR] {host}: {e}")
return False
def generate_status_page(config, machines):
cats_html = ""
for cat, data in config.items():
status_color = "#2ecc71" if data["enabled"] else "#e74c3c"
status_text = "ACTIVE" if data["enabled"] else "DISABLED"
sites_html = "".join(f"<span style='background:#2c2f3e;padding:2px 8px;"
f"border-radius:4px;font-size:0.8em;margin:2px'>{s}</span>"
for s in data["sites"])
cats_html += f"""
<div style='background:#1a1d27;border-radius:8px;padding:16px;margin-bottom:12px;
border-left:4px solid {status_color}'>
<div style='display:flex;justify-content:space-between;align-items:center'>
<strong style='text-transform:capitalize'>{cat.replace("_"," ")}</strong>
<span style='background:{status_color};color:white;padding:2px 10px;
border-radius:12px;font-size:0.8em'>{status_text}</span>
</div>
<div style='margin-top:10px;display:flex;flex-wrap:wrap;gap:4px'>{sites_html}</div>
</div>"""
total_blocked = len(get_all_blocked_sites(config))
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="300">
<title>Web Blocker Status</title>
<style>
body {{ font-family:Segoe UI,Ubuntu,sans-serif;background:#0f1117;
color:#e0e0e0;padding:30px; }}
h1 {{ color:#e74c3c; }}
.summary {{ display:flex;gap:16px;margin:20px 0;flex-wrap:wrap; }}
.card {{ background:#1a1d27;border-radius:10px;padding:18px 24px;
border-left:4px solid #e74c3c;min-width:130px;text-align:center; }}
.card h3 {{ font-size:2em;color:#e74c3c;margin:0; }}
.card p {{ color:#888;font-size:0.85em;margin:4px 0 0; }}
</style>
</head>
<body>
<h1>🚫 Web Blocking Status</h1>
<p style='color:#666'>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<div class="summary">
<div class="card"><h3>{total_blocked}</h3><p>Sites Blocked</p></div>
<div class="card"><h3>{len(machines)}</h3><p>Machines</p></div>
<div class="card"><h3>{sum(1 for c in config.values() if c['enabled'])}</h3><p>Active Categories</p></div>
</div>
<h2 style='color:#e74c3c;margin:20px 0 12px'>Block Categories</h2>
{cats_html}
</body>
</html>"""
os.makedirs(os.path.dirname(REPORT_FILE), exist_ok=True)
with open(REPORT_FILE, "w") as f:
f.write(html)
print(f"[✔] Status page: {REPORT_FILE}")
if __name__ == "__main__":
config = load_config()
sites = get_all_blocked_sites(config)
print(f"[*] Total blocked sites: {len(sites)}")
machines = ["192.168.1.10", "192.168.1.11"] # Add your machines
generate_status_page(config, machines)
save_config(config)
Deployment Summary
bash
# Linux — deploy immediately
chmod +x /opt/web-blocker/deploy_web_blocking.sh
sudo /opt/web-blocker/deploy_web_blocking.sh
# Push to all Linux machines via Ansible
ansible all -m script -a "/opt/web-blocker/deploy_web_blocking.sh" \
--become -i /etc/ansible/hosts
# Windows — push via GPO startup script
# Place USB-WebBlock.ps1 in SYSVOL and link to GPO
# Schedule daily refresh (block lists update)
echo "0 6 * * * root /opt/web-blocker/deploy_web_blocking.sh" \
>> /etc/crontab
Quick Reference — Methods vs Scale
|
Method |
Scale |
Bypass Risk |
Best For |
|
Hosts file |
Per machine |
Medium |
Small teams |
|
GPO + Registry |
Domain-wide |
Medium |
Windows AD |
|
Squid Proxy |
Network-wide |
Low |
Enterprises |
|
DNS Sinkhole |
Network-wide |
Medium |
All devices |
|
iptables/Firewall |
Network-wide |
Low |
Linux-heavy |
|
Commercial DLP |
Enterprise |
Very Low |
Large orgs
|
custom block page (branded 403 page employees see), bypass request workflow (employees request access), or per-department policies (e.g. Marketing can access social media)?
Save as /var/www/html/blocked/index.html:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Blocked — Company IT Policy</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Segoe UI, Ubuntu, sans-serif;
background: #0f1117;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: #1a1d27;
border-radius: 16px;
padding: 40px;
max-width: 580px;
width: 100%;
text-align: center;
border-top: 4px solid #e74c3c;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.icon { font-size: 4em; margin-bottom: 16px; }
.logo {
font-size: 1em;
color: #666;
margin-bottom: 24px;
letter-spacing: 2px;
text-transform: uppercase;
}
h1 { color: #e74c3c; font-size: 1.6em; margin-bottom: 10px; }
p { color: #888; font-size: 0.95em; line-height: 1.7; margin-bottom: 20px; }
.site-box {
background: #0f1117;
border: 1px solid #2c2f3e;
border-radius: 8px;
padding: 12px 20px;
font-family: monospace;
font-size: 1em;
color: #e74c3c;
margin-bottom: 24px;
word-break: break-all;
}
.reason {
background: #2c2f3e;
border-radius: 8px;
padding: 12px 16px;
font-size: 0.85em;
color: #aaa;
margin-bottom: 28px;
text-align: left;
}
.reason strong { color: #e0e0e0; }
.btn {
display: inline-block;
padding: 12px 28px;
border-radius: 8px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
text-decoration: none;
border: none;
transition: opacity 0.2s;
margin: 6px;
}
.btn:hover { opacity: 0.85; }
.btn-primary { background: #e74c3c; color: white; }
.btn-secondary {
background: transparent;
color: #7fb3d3;
border: 1px solid #2c2f3e;
}
.divider {
border: none;
border-top: 1px solid #2c2f3e;
margin: 28px 0;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.78em;
color: #555;
flex-wrap: wrap;
gap: 6px;
}
.modal-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal-overlay.active { display: flex; }
.modal {
background: #1a1d27;
border-radius: 14px;
padding: 32px;
max-width: 480px;
width: 90%;
border-top: 3px solid #3498db;
}
.modal h2 { color: #3498db; margin-bottom: 16px; font-size: 1.2em; }
.modal input, .modal select, .modal textarea {
width: 100%;
background: #0f1117;
border: 1px solid #2c2f3e;
border-radius: 8px;
color: #e0e0e0;
padding: 10px 14px;
font-size: 0.9em;
margin-bottom: 14px;
font-family: inherit;
outline: none;
}
.modal textarea { height: 90px; resize: vertical; }
.modal input:focus, .modal select:focus, .modal textarea:focus {
border-color: #3498db;
}
.modal label { font-size: 0.82em; color: #888; display: block; margin-bottom: 5px; }
.success-msg {
display: none;
background: #1a3a2a;
border: 1px solid #2ecc71;
border-radius: 8px;
padding: 16px;
color: #2ecc71;
font-size: 0.9em;
margin-top: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">🚫</div>
<div class="logo">⚙Company IT Security</div>
<h1>Access Blocked</h1>
<p>This website has been blocked by your organization's IT security policy.</p>
<div class="site-box" id="blocked-url">
<!-- Filled by JS or Squid -->
<span id="url-display">This website</span>
</div>
<div class="reason">
<strong>Reason:</strong>
<span id="block-reason">Personal email / unauthorized web service</span><br><br>
<strong>Policy:</strong>IT Security Policy v2.4 — Acceptable Use of Internet Resources<br>
<strong>Category:</strong><span id="block-category">Personal Communication</span>
</div>
<div>
<button class="btn btn-primary" onclick="openRequestModal()">
🔓Request Access
</button>
<a class="btn btn-secondary" href="javascript:history.back()">
← Go Back
</a>
</div>
<hr class="divider">
<div class="info-row">
<span>🖥Host:<span id="hostname-display">WORKSTATION</span></span>
<span>👤User:<span id="user-display">employee</span></span>
<span>🕐<span id="time-display"></span></span>
<span>📞IT Help: ext. 1234</span>
</div>
</div>
<!-- Access Request Modal -->
<div class="modal-overlay" id="requestModal">
<div class="modal">
<h2>🔓Request Website Access</h2>
<label>Your Name</label>
<input type="text" id="req-name" placeholder="Full Name">
<label>Employee ID</label>
<input type="text" id="req-empid" placeholder="EMP-XXXXX">
<label>Department</label>
<select id="req-dept">
<option value="">Select Department</option>
<option>Engineering</option>
<option>Marketing</option>
<option>Finance</option>
<option>HR</option>
<option>Sales</option>
<option>Operations</option>
<option>Executive</option>
</select>
<label>Website you need access to</label>
<input type="text" id="req-site" placeholder="e.g. gmail.com">
<label>Business Justification</label>
<textarea id="req-reason"
placeholder="Why do you need access? How does it relate to your work?"></textarea>
<label>Manager Email (for approval)</label>
<input type="email" id="req-manager" placeholder="manager@company.com">
<div style="display:flex;gap:10px;margin-top:6px">
<button class="btn btn-primary" style="flex:1" onclick="submitRequest()">
📨Submit Request
</button>
<button class="btn btn-secondary" onclick="closeRequestModal()">
Cancel
</button>
</div>
<div class="success-msg" id="success-msg">
✅Request submitted! Your manager and IT will review within 24 hours.
Reference ID: <strong id="ref-id"></strong>
</div>
</div>
</div>
<script>
// Fill dynamic fields
const params = new URLSearchParams(window.location.search);
document.getElementById('url-display').textContent =
params.get('url') || document.referrer || 'The requested site';
document.getElementById('block-reason').textContent =
params.get('reason') || 'Unauthorized web service';
document.getElementById('block-category').textContent =
params.get('category') || 'Restricted Content';
document.getElementById('time-display').textContent =
new Date().toLocaleString();
// Auto-fill site in modal
document.getElementById('req-site').value =
params.get('url') || '';
function openRequestModal() {
document.getElementById('requestModal').classList.add('active');
}
function closeRequestModal() {
document.getElementById('requestModal').classList.remove('active');
document.getElementById('success-msg').style.display = 'none';
}
async function submitRequest() {
const name = document.getElementById('req-name').value.trim();
const empid = document.getElementById('req-empid').value.trim();
const dept = document.getElementById('req-dept').value;
const site = document.getElementById('req-site').value.trim();
const reason = document.getElementById('req-reason').value.trim();
const manager = document.getElementById('req-manager').value.trim();
if (!name || !site || !reason || !manager || !dept) {
alert('Please fill in all fields.'); return;
}
const refId = 'REQ-' + Date.now().toString(36).toUpperCase();
const payload = { name, empid, dept, site, reason, manager,
refId, timestamp: new Date().toISOString(),
blockedUrl: params.get('url') || '' };
try {
await fetch('/api/access-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch(e) { /* offline fallback — still show success */ }
document.getElementById('ref-id').textContent = refId;
document.getElementById('success-msg').style.display = 'block';
setTimeout(closeRequestModal, 4000);
}
</script>
</body>
</html>Part 2: Bypass Request Workflow Backend
Save as
/opt/web-blocker/request_server.py:python
#!/usr/bin/env python3"""Web Access Request Workflow BackendHandles bypass requests, manager approvals, and auto-deploys exceptions"""fromhttp.serverimportHTTPServer,BaseHTTPRequestHandlerimportjson,smtplib,sqlite3,os,subprocessfrommime.multipartimportMIMEMultipartfrommime.textimportMIMETextfromdatetimeimportdatetimefromurllib.parseimporturlparse,parse_qsDB_FILE="/var/lib/web-blocker/requests.db"SMTP_SERVER="smtp.company.com"SMTP_PORT=587SMTP_USER="it-security@company.com"SMTP_PASS="your_smtp_password"APPROVE_BASE="https://usb-monitor.company.com/approve"IT_EMAIL="it-security@company.com"os.makedirs(os.path.dirname(DB_FILE),exist_ok=True)# ── Database ─────────────────────────────────────────────────────────definit_db():conn=sqlite3.connect(DB_FILE)conn.execute("""CREATE TABLE IF NOT EXISTS requests (ref_id TEXT PRIMARY KEY,name TEXT,emp_id TEXT,department TEXT,site TEXT,reason TEXT,manager TEXT,status TEXT DEFAULT 'pending',created_at TEXT,updated_at TEXT,blocked_url TEXT,notes TEXT)""")conn.commit()conn.close()defsave_request(data):conn=sqlite3.connect(DB_FILE)conn.execute("""INSERT OR REPLACE INTO requests(ref_id, name, emp_id, department, site, reason,manager, status, created_at, updated_at, blocked_url)VALUES (?,?,?,?,?,?,?,'pending',?,?,?)""",(data["refId"],data["name"],data.get("empid",""),data["dept"],data["site"],data["reason"],data["manager"],data["timestamp"],data["timestamp"],data.get("blockedUrl","")))conn.commit()conn.close()defupdate_request(ref_id,status,notes=""):conn=sqlite3.connect(DB_FILE)conn.execute("""UPDATE requests SET status=?, updated_at=?, notes=?WHERE ref_id=?""",(status,datetime.now().isoformat(),notes,ref_id))conn.commit()conn.close()defget_all_requests():conn=sqlite3.connect(DB_FILE)conn.row_factory=sqlite3.Rowrows=conn.execute("SELECT * FROM requests ORDER BY created_at DESC").fetchall()conn.close()return[dict(r)forrinrows]defget_request(ref_id):conn=sqlite3.connect(DB_FILE)conn.row_factory=sqlite3.Rowrow=conn.execute("SELECT * FROM requests WHERE ref_id=?",(ref_id,)).fetchone()conn.close()returndict(row)ifrowelseNone# ── Email ────────────────────────────────────────────────────────────defsend_email(to,subject,html):try:msg=MIMEMultipart("alternative")msg["Subject"]=subjectmsg["From"]=SMTP_USERmsg["To"]=tomsg.attach(MIMEText(html,"html"))withsmtplib.SMTP(SMTP_SERVER,SMTP_PORT)ass:s.starttls()s.login(SMTP_USER,SMTP_PASS)s.sendmail(SMTP_USER,[to],msg.as_string())print(f"[EMAIL] Sent to {to}: {subject}")exceptExceptionase:print(f"[EMAIL ERROR] {e}")defnotify_manager(req):approve_url=f"{APPROVE_BASE}?ref={req['ref_id']}&action=approve"deny_url=f"{APPROVE_BASE}?ref={req['ref_id']}&action=deny"html=f"""<div style="font-family:sans-serif;max-width:600px;margin:auto"><div style="background:#2c3e50;color:white;padding:16px 20px;border-radius:8px 8px 0 0"><h2 style="margin:0">🔓Web Access Request — Approval Required</h2></div><div style="background:#f9f9f9;padding:24px;border:1px solid #ddd"><table style="width:100%;border-collapse:collapse"><tr><td style="padding:8px;color:#666;width:140px">Reference</td><td style="padding:8px;font-weight:bold">{req['ref_id']}</td></tr><tr style="background:#fff"><td style="padding:8px;color:#666">Employee</td><td style="padding:8px">{req['name']} ({req['emp_id']})</td></tr><tr><td style="padding:8px;color:#666">Department</td><td style="padding:8px">{req['department']}</td></tr><tr style="background:#fff"><td style="padding:8px;color:#666">Site Requested</td><td style="padding:8px;font-weight:bold;color:#e74c3c">{req['site']}</td></tr><tr><td style="padding:8px;color:#666">Justification</td><td style="padding:8px">{req['reason']}</td></tr><tr style="background:#fff"><td style="padding:8px;color:#666">Submitted</td><td style="padding:8px">{req['created_at'][:19]}</td></tr></table><div style="margin-top:24px;text-align:center"><a href="{approve_url}" style="background:#2ecc71;color:white;padding:12px 28px;border-radius:8px;text-decoration:none;font-weight:bold;margin:6px;display:inline-block">✅Approve Access</a><a href="{deny_url}" style="background:#e74c3c;color:white;padding:12px 28px;border-radius:8px;text-decoration:none;font-weight:bold;margin:6px;display:inline-block">❌Deny Request</a></div><p style="color:#999;font-size:0.8em;margin-top:20px;text-align:center">IT Security | Company Name | This is an automated message</p></div></div>"""send_email(req["manager"],f"[ACTION REQUIRED] Web Access Request — {req['name']} → {req['site']}",html)defnotify_employee(req,approved):status_color="#2ecc71"ifapprovedelse"#e74c3c"status_text="APPROVED ✅"ifapprovedelse"DENIED ❌"msg=("Your request has been approved. Access will be granted within 30 minutes."ifapprovedelse"Your request was not approved. Contact IT if you believe this is an error.")html=f"""<div style="font-family:sans-serif;max-width:600px;margin:auto"><div style="background:{status_color};color:white;padding:16px 20px;border-radius:8px 8px 0 0"><h2 style="margin:0">Web Access Request: {status_text}</h2></div><div style="background:#f9f9f9;padding:24px;border:1px solid #ddd"><p>Hi {req['name']},</p><p style="margin:12px 0">{msg}</p><table style="width:100%;border-collapse:collapse;margin-top:16px"><tr><td style="padding:8px;color:#666;width:140px">Reference</td><td style="padding:8px">{req['ref_id']}</td></tr><tr style="background:#fff"><td style="padding:8px;color:#666">Site</td><td style="padding:8px">{req['site']}</td></tr><tr><td style="padding:8px;color:#666">Status</td><td style="padding:8px;font-weight:bold">{status_text}</td></tr></table><p style="color:#999;font-size:0.8em;margin-top:20px">IT Help Desk: ext. 1234 | it-security@company.com</p></div></div>"""# Send to employee via IT (no direct employee email in request)send_email(IT_EMAIL,f"Web Access {status_text} — {req['name']} — {req['site']}",html)# ── Apply Exception ───────────────────────────────────────────────────defapply_exception(site,action="allow"):"""Add or remove a site from the block list"""HOSTS="/etc/hosts"SQUID_WHITELIST="/etc/squid/whitelist.txt"# Hosts file exceptionifaction=="allow":subprocess.run(["sed","-i",f"/0\\.0\\.0\\.0 {site}/d",HOSTS],check=False)subprocess.run(["sed","-i",f"/0\\.0\\.0\\.0 www\\.{site}/d",HOSTS],check=False)# Add to Squid whitelistwithopen(SQUID_WHITELIST,"a")asf:f.write(f"{site}\n")subprocess.run(["squid","-k","reconfigure"],check=False)print(f"[+] Exception applied: {site} → ALLOWED")else:print(f"[+] Site remains blocked: {site}")subprocess.run(["systemd-resolve","--flush-caches"],check=False,capture_output=True)# ── HTTP Request Handler ──────────────────────────────────────────────classRequestHandler(BaseHTTPRequestHandler):deflog_message(self,format,*args):pass# Suppress default logsdefsend_json(self,code,data):body=json.dumps(data).encode()self.send_response(code)self.send_header("Content-Type","application/json")self.send_header("Access-Control-Allow-Origin","*")self.send_header("Content-Length",len(body))self.end_headers()self.wfile.write(body)defsend_html(self,code,html):body=html.encode()self.send_response(code)self.send_header("Content-Type","text/html")self.end_headers()self.wfile.write(body)defdo_OPTIONS(self):self.send_response(200)self.send_header("Access-Control-Allow-Origin","*")self.send_header("Access-Control-Allow-Methods","POST,GET")self.send_header("Access-Control-Allow-Headers","Content-Type")self.end_headers()defdo_POST(self):ifself.path=="/api/access-request":length=int(self.headers.get("Content-Length",0))body=json.loads(self.rfile.read(length))save_request(body)notify_manager(body)print(f"[REQUEST] {body['refId']} — {body['name']} → {body['site']}")self.send_json(200,{"status":"ok","refId":body["refId"]})defdo_GET(self):parsed=urlparse(self.path)params=parse_qs(parsed.query)# Approval endpointifparsed.path=="/approve":ref_id=params.get("ref",[""])[0]action=params.get("action",[""])[0]req=get_request(ref_id)ifnotreq:self.send_html(404,"<h1>Request not found</h1>")returnapproved=action=="approve"update_request(ref_id,"approved"ifapprovedelse"denied")ifapproved:apply_exception(req["site"],"allow")notify_employee(req,approved)status="APPROVED ✅"ifapprovedelse"DENIED ❌"color="#2ecc71"ifapprovedelse"#e74c3c"self.send_html(200,f"""<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0f1117;color:#e0e0e0"><div style="background:#1a1d27;border-radius:16px;padding:40px;max-width:480px;margin:auto;border-top:4px solid {color}"><h1 style="color:{color}">{status}</h1><p>Request <strong>{ref_id}</strong> for<strong>{req['site']}</strong> has been {action}d.</p><p style="color:#666;margin-top:16px">Employee will be notified.</p></div></body></html>""")return# Admin dashboardifparsed.path=="/admin/requests":requests=get_all_requests()rows=""forrinrequests:sc={"approved":"#2ecc71","denied":"#e74c3c"}.get(r["status"],"#f39c12")rows+=f"""<tr><td>{r['ref_id']}</td><td>{r['name']}</td><td>{r['department']}</td><td style='color:#e74c3c;font-weight:bold'>{r['site']}</td><td><span style='background:{sc};color:white;padding:2px 10px;border-radius:10px;font-size:0.8em'>{r['status'].upper()}</span></td><td>{r['created_at'][:16]}</td><td style='font-size:0.8em;color:#888'>{r['reason'][:60]}...</td></tr>"""self.send_html(200,f"""<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='refresh' content='60'><title>Access Requests</title><style>body{{font-family:Segoe UI,sans-serif;background:#0f1117;color:#e0e0e0;padding:30px}}h1{{color:#e74c3c}}table{{width:100%;border-collapse:collapse;background:#1a1d27;border-radius:10px;overflow:hidden;margin-top:20px}}th{{background:#2c2f3e;color:#aaa;padding:11px 13px;text-align:left}}td{{padding:10px 13px;border-bottom:1px solid #1e2130;font-size:0.85em}}tr:hover td{{background:#1e2130}}</style></head><body><h1>🔓Web Access Requests</h1><p style='color:#666'>Total: {len(requests)} | Auto-refreshes every 60s</p><table><tr><th>Ref ID</th><th>Name</th><th>Dept</th><th>Site</th><th>Status</th><th>Submitted</th><th>Reason</th></tr>{rows}</table></body></html>""")returnself.send_json(404,{"error":"not found"})# ── Start Server ──────────────────────────────────────────────────────if__name__=="__main__":init_db()print("[*] Access Request Server running on :8080")print(f"[*] Admin dashboard: http://localhost:8080/admin/requests")HTTPServer(("0.0.0.0",8080),RequestHandler).serve_forever()
Save as /opt/web-blocker/dept_policies.py:
python
#!/usr/bin/env python3
"""
Per-Department Web Blocking Policies
Different rules for Engineering, Marketing, Finance, HR, etc.
"""
importjson,os,subprocess
fromdatetimeimportdatetime
POLICY_FILE="/opt/web-blocker/dept_policies.json"
SQUID_DIR="/etc/squid/departments"
HOSTS_DIR="/etc/hosts.d"
os.makedirs(SQUID_DIR,exist_ok=True)
os.makedirs(HOSTS_DIR,exist_ok=True)
# ── Department Policy Definitions ────────────────────────────────────
DEPT_POLICIES={
"engineering":{
"description":"Developers — relaxed policy",
"work_hours":"07:00-22:00",
"allowed_extra":[
"github.com","stackoverflow.com","gitlab.com",
"npmjs.com","pypi.org","docker.com",
"aws.amazon.com","cloud.google.com"
],
"blocked_extra":[],
"social_media":False,# Blocked
"personal_email":False,# Blocked
"youtube":True,# Allowed (tutorials)
"file_sharing":False,# Blocked
"streaming":False,# Blocked
},
"marketing":{
"description":"Marketing — social media allowed",
"work_hours":"08:00-19:00",
"allowed_extra":[
"canva.com","figma.com","hootsuite.com",
"buffer.com","mailchimp.com","hubspot.com"
],
"blocked_extra":["tiktok.com"],
"social_media":True,# ALLOWED for work
"personal_email":False,
"youtube":True,# ALLOWED
"file_sharing":False,
"streaming":False,
},
"finance":{
"description":"Finance — strict policy",
"work_hours":"08:00-18:00",
"allowed_extra":[
"bloomberg.com","reuters.com",
"moneycontrol.com","nseindia.com"
],
"blocked_extra":[
"reddit.com","quora.com","medium.com"
],
"social_media":False,
"personal_email":False,
"youtube":False,
"file_sharing":False,
"streaming":False,
},
"hr":{
"description":"HR — LinkedIn and job sites allowed",
"work_hours":"08:00-18:00",
"allowed_extra":[
"linkedin.com","naukri.com","indeed.com",
"glassdoor.com","shine.com","monster.com"
],
"blocked_extra":[],
"social_media":False,
"personal_email":False,
"youtube":True,
"file_sharing":False,
"streaming":False,
},
"executive":{
"description":"Leadership — minimal restrictions",
"work_hours":"06:00-23:00",
"allowed_extra":["*"],# Wildcard — allow all extras
"blocked_extra":[],
"social_media":True,
"personal_email":True,# ALLOWED
"youtube":True,
"file_sharing":True,
"streaming":False,
},
"default":{
"description":"Standard employee policy",
"work_hours":"08:00-18:00",
"allowed_extra":[],
"blocked_extra":[],
"social_media":False,
"personal_email":False,
"youtube":False,
"file_sharing":False,
"streaming":False,
}
}
# ── Base block lists ──────────────────────────────────────────────────
BASE_BLOCKS={
"social_media":["facebook.com","twitter.com","x.com",
"instagram.com","tiktok.com","snapchat.com","reddit.com"],
"personal_email":["gmail.com","mail.google.com","hotmail.com",
"yahoo.com","protonmail.com","tutanota.com"],
"youtube":["youtube.com","youtu.be"],
"file_sharing":["dropbox.com","wetransfer.com","mega.nz",
"mediafire.com","pastebin.com"],
"streaming":["netflix.com","primevideo.com","twitch.tv","spotify.com"],
}
# ── Generate Squid ACL per department ────────────────────────────────
defgenerate_squid_acl(dept,policy):
blocked=[]
forcategory,sitesinBASE_BLOCKS.items():
ifnotpolicy.get(category,True):# False = blocked
blocked.extend(sites)
blocked.extend(policy.get("blocked_extra",[]))
allowed=policy.get("allowed_extra",[])
wh=policy.get("work_hours","08:00-18:00")
wh_start,wh_end=wh.split("-")
blocked_file=f"{SQUID_DIR}/{dept}_blocked.txt"
allowed_file=f"{SQUID_DIR}/{dept}_allowed.txt"
withopen(blocked_file,"w")asf:
f.write("\n".join(set(blocked)))
withopen(allowed_file,"w")asf:
f.write("\n".join(set(allowed)))
acl=f"""
# ── Department: {dept.upper()} ── {policy['description']} ──
acl dept_{dept}_net src "/etc/squid/departments/{dept}_ips.txt"
acl dept_{dept}_blk dstdomain "{blocked_file}"
acl dept_{dept}_wl dstdomain "{allowed_file}"
acl dept_{dept}_hrs time MTWHF {wh_start}-{wh_end}
# Whitelist takes priority
http_access allow dept_{dept}_net dept_{dept}_wl
# Block during work hours
http_access deny dept_{dept}_net dept_{dept}_blk dept_{dept}_hrs
"""
returnacl
# ── Generate Hosts file per department ───────────────────────────────
defgenerate_hosts_block(dept,policy):
blocked=[]
forcategory,sitesinBASE_BLOCKS.items():
ifnotpolicy.get(category,True):
blocked.extend(sites)
blocked.extend(policy.get("blocked_extra",[]))
allowed=policy.get("allowed_extra",[])
lines=[f"# DEPT-BLOCK-{dept.upper()}-START"]
forsiteinset(blocked):
ifsitenotinallowed:
lines.append(f"0.0.0.0 {site} # DEPT-BLOCK-{dept.upper()}")
lines.append(f"0.0.0.0 www.{site} # DEPT-BLOCK-{dept.upper()}")
lines.append(f"# DEPT-BLOCK-{dept.upper()}-END")
return"\n".join(lines)
# ── Deploy all department policies ───────────────────────────────────
def deploy_all_policies():
all_acls=["# Auto-generated department ACLs\n"]
all_hosts={}
fordept,policyinDEPT_POLICIES.items():
acl=generate_squid_acl(dept,policy)
hosts=generate_hosts_block(dept,policy)
all_acls.append(acl)
all_hosts[dept]=hosts
# Write per-dept hosts file
withopen(f"{HOSTS_DIR}/{dept}.conf","w")asf:
f.write(hosts)
print(f"[+] Policy deployed: {dept:15} — {policy['description']}")
# Write combined Squid ACL
withopen(f"{SQUID_DIR}/dept_acls.conf","w")asf:
f.write("\n".join(all_acls))
# Reconfigure Squid
result=subprocess.run(["squid","-k","reconfigure"],
capture_output=True,text=True)
ifresult.returncode==0:
print("[✔] Squid reconfigured")
else:
print(f"[!] Squid error: {result.stderr}")
# Save policy DB
withopen(POLICY_FILE,"w")asf:
json.dump(DEPT_POLICIES,f,indent=2)
print(f"\n[✔] All department policies deployed: {datetime.now()}")
# ── Policy Summary ────────────────────────────────────────────────────
def print_summary():
print("\n"+"="*70)
print(f"{'DEPARTMENT':<15} {'SOCIAL':^8} {'EMAIL':^8} "
f"{'YOUTUBE':^9} {'FILES':^8} {'WORK HOURS'}")
print("="*70)
icons={True:"✅",False:"❌"}
fordept,pinDEPT_POLICIES.items():
print(f"{dept:<15} "
f"{icons[p['social_media']]:^8} "
f"{icons[p['personal_email']]:^8} "
f"{icons[p['youtube']]:^9} "
f"{icons[p['file_sharing']]:^8} "
f"{p['work_hours']}")
print("="*70)
if__name__=="__main__":
deploy_all_policies()
print_summary()
squid
# Include department ACLs
include /etc/squid/departments/dept_acls.conf
# Include whitelist for approved exceptions
acl approved_exceptions dstdomain "/etc/squid/whitelist.txt"
http_access allow approved_exceptions
# Block page
deny_info https://usb-monitor.company.com/blocked/index.html?url=%u&category=%s all
bash
# 1. Deploy block page
sudomkdir-p /var/www/html/blocked
sudocpindex.html /var/www/html/blocked/
# 2. Start request server
sudo python3 /opt/web-blocker/request_server.py &
# Or as a service:
sudotee/etc/systemd/system/web-request-server.service<<EOF
[Unit]
Description=Web Access Request Server
After=network.target
[Service]
ExecStart=/usr/bin/python3 /opt/web-blocker/request_server.py
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudosystemctlenable--now web-request-server
# 3. Deploy department policies
sudo python3 /opt/web-blocker/dept_policies.py
# 4. Verify
sudosquid -k parse&&sudosquid -k reconfigure
Employee tries gmail.com
↓
Squid/DNS blocks it
↓
Branded block page shown
↓
Employee clicks "Request Access"
↓
Fills form → submits
↓
Manager gets email with [Approve] [Deny] buttons
↓
┌───┴───┐
Approve Deny
↓ ↓
Site added Request
to whitelist denied
↓ ↓
Squid Employee
reconfigured notified
↓
Access granted
within 30 min
|
Department |
Social Media |
Personal Email |
YouTube |
File Sharing |
Work Hours |
|
Engineering |
❌ |
❌ |
✅ |
❌ |
7AM–10PM |
|
Marketing |
✅ |
❌ |
✅ |
❌ |
8AM–7PM |
|
Finance |
❌ |
❌ |
❌ |
❌ |
8AM–6PM |
|
HR |
❌ |
❌ |
✅ |
❌ |
8AM–6PM |
|
Executive |
✅ |
✅ |
✅ |
✅ |
6AM–11PM |
|
Default |
❌ |
❌ |
❌ |
❌ |
8AM–6PM |
Here's the complete implementation for both AD/LDAP integration and the reporting dashboard.
bash
# Install required packages
sudoaptinstall-y krb5-user samba winbind libpam-winbind\
libnss-winbind krb5-config msktutil
# Configure Kerberos
sudotee/etc/krb5.conf<<EOF
[libdefaults]
default_realm = COMPANY.COM
dns_lookup_realm = false
dns_lookup_kdc = true
ticket_lifetime = 24h
forwardable = true
[realms]
COMPANY.COM = {
kdc = dc01.company.com
admin_server = dc01.company.com
}
[domain_realm]
.company.com = COMPANY.COM
company.com = COMPANY.COM
EOF
# Configure Samba/Winbind
sudotee/etc/samba/smb.conf<<EOF
[global]
workgroup = COMPANY
realm = COMPANY.COM
security = ADS
kerberos method = secrets and keytab
winbind use default domain = yes
winbind enum users = yes
winbind enum groups = yes
idmap config * : backend = tdb
idmap config * : range = 10000-99999
EOF
# Join domain
sudonet adsjoin-U Administrator
# Start winbind
sudosystemctlenable--now winbind
# Test
wbinfo -u # List AD users
wbinfo -g # List AD groups
powershell
# Run on Windows DC — PowerShell
# Create dedicated service account for Squid
New-ADUser `
-Name"squid-proxy"`
-SamAccountName"squid-proxy"`
-UserPrincipalName"squid-proxy@company.com"`
-AccountPassword(ConvertTo-SecureString"Str0ngP@ss!"-AsPlainText-Force)`
-PasswordNeverExpires$true`
-CannotChangePassword$true`
-Enabled$true`
-Description"Squid Proxy Service Account"
# Create SPN for Kerberos
setspn-A HTTP/proxy.company.com squid-proxy
setspn-A HTTP/proxy squid-proxy
# Verify SPNs
setspn-L squid-proxy
bash
# On Squid server
sudomsktutil -c -b"CN=Computers"\
-s HTTP/proxy.company.com \
-k /etc/squid/squid.keytab \
--computer-name squid-proxy \
--upn squid-proxy \
--server dc01.company.com \
--enctypes 28
sudochownproxy:proxy /etc/squid/squid.keytab
sudochmod600/etc/squid/squid.keytab
# Test keytab
kinit -k -t /etc/squid/squid.keytab HTTP/proxy.company.com
klist
Save as /opt/web-blocker/ad_ldap_sync.py:
python
#!/usr/bin/env python3
"""
AD / LDAP Group → Department Policy Sync
Automatically assigns web policies based on AD group membership
"""
importldap3,json,os,subprocess,sqlite3
fromdatetimeimportdatetime
# ── Config ────────────────────────────────────────────────────────────
AD_CONFIG={
"server":"ldap://dc01.company.com",
"domain":"company.com",
"base_dn":"DC=company,DC=com",
"bind_dn":"CN=squid-proxy,CN=Users,DC=company,DC=com",
"bind_pw":"Str0ngP@ss!",
"user_ou":"OU=Users,DC=company,DC=com",
"group_ou":"OU=Groups,DC=company,DC=com",
}
OPENLDAP_CONFIG={
"server":"ldap://ldap.company.com",
"base_dn":"dc=company,dc=com",
"bind_dn":"cn=admin,dc=company,dc=com",
"bind_pw":"ldap_admin_pass",
"user_ou":"ou=users,dc=company,dc=com",
"group_ou":"ou=groups,dc=company,dc=com",
}
# AD Group → Department mapping
GROUP_DEPT_MAP={
"CN=Engineering,OU=Groups,DC=company,DC=com":"engineering",
"CN=Marketing,OU=Groups,DC=company,DC=com":"marketing",
"CN=Finance,OU=Groups,DC=company,DC=com":"finance",
"CN=HR,OU=Groups,DC=company,DC=com":"hr",
"CN=Executives,OU=Groups,DC=company,DC=com":"executive",
"CN=IT,OU=Groups,DC=company,DC=com":"engineering",
"CN=Sales,OU=Groups,DC=company,DC=com":"marketing",
}
DB_FILE="/var/lib/web-blocker/users.db"
SQUID_DIR="/etc/squid/departments"
POLICY_LOG="/var/log/web-blocker/ldap-sync.log"
os.makedirs(os.path.dirname(DB_FILE),exist_ok=True)
os.makedirs(os.path.dirname(POLICY_LOG),exist_ok=True)
# ── Database ──────────────────────────────────────────────────────────
def init_db():
conn=sqlite3.connect(DB_FILE)
conn.execute("""
CREATE TABLE IF NOT EXISTS user_policies (
username TEXT PRIMARY KEY,
full_name TEXT,
email TEXT,
department TEXT,
policy TEXT,
groups TEXT,
ip_address TEXT,
last_sync TEXT,
source TEXT
)
""")
conn.commit()
conn.close()
defsave_user(username,full_name,dept,groups,source="AD"):
conn=sqlite3.connect(DB_FILE)
conn.execute("""
INSERT OR REPLACE INTO user_policies
(username, full_name, email, department, policy, groups, last_sync, source)
VALUES (?,?,?,?,?,?,?,?)
""",(username,full_name,dept,dept,
json.dumps(groups),datetime.now().isoformat(),source))
conn.commit()
conn.close()
def get_all_users():
conn=sqlite3.connect(DB_FILE)
conn.row_factory=sqlite3.Row
rows=conn.execute(
"SELECT * FROM user_policies ORDER BY department, username"
).fetchall()
conn.close()
return[dict(r)forrinrows]
deflog(msg):
ts=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line=f"[{ts}] {msg}"
print(line)
withopen(POLICY_LOG,"a")asf:
f.write(line+"\n")
# ── LDAP Connection ───────────────────────────────────────────────────
defconnect_ldap(config,use_ssl=False):
server=ldap3.Server(
config["server"],
get_info=ldap3.ALL,
use_ssl=use_ssl
)
conn=ldap3.Connection(
server,
user=config["bind_dn"],
password=config["bind_pw"],
authentication=ldap3.SIMPLE,
auto_bind=True
)
returnconn
# ── Fetch Users from AD ───────────────────────────────────────────────
deffetch_ad_users(conn,config):
users={}
conn.search(
search_base=config["user_ou"],
search_filter="(&(objectClass=user)(objectCategory=person)"
"(!(userAccountControl:1.2.840.113556.1.4.803:=2)))",
search_scope=ldap3.SUBTREE,
attributes=[
"sAMAccountName","displayName","mail",
"memberOf","department","distinguishedName",
"lastLogon","userAccountControl"
]
)
forentryinconn.entries:
username=str(entry.sAMAccountName)
full_name=str(entry.displayName)ifentry.displayNameelseusername
str(entry.ifentry.""
groups=[str(g)forginentry.memberOf]ifentry.memberOfelse[]
ad_dept=str(entry.department)ifentry.departmentelse""
# Determine policy department
dept="default"
forgroup_dn,group_deptinGROUP_DEPT_MAP.items():
ifgroup_dningroups:
dept=group_dept
break
ifad_deptanddept=="default":
dept=ad_dept.lower().replace(" ","_")
users[username]={
"full_name":full_name,
"email":
"dept":dept,
"groups":groups,
"source":"ActiveDirectory"
}
log(f"[AD] Fetched {len(users)} users")
returnusers
# ── Fetch Users from OpenLDAP ─────────────────────────────────────────
deffetch_ldap_users(conn,config):
users={}
# Fetch groups first
group_members={}
conn.search(
search_base=config["group_ou"],
search_filter="(objectClass=groupOfNames)",
search_scope=ldap3.SUBTREE,
attributes=["cn","member"]
)
forentryinconn.entries:
group_name=str(entry.cn).lower()
members=[str(m)forminentry.member]ifentry.memberelse[]
group_members[group_name]=members
# Fetch users
conn.search(
search_base=config["user_ou"],
search_filter="(objectClass=inetOrgPerson)",
search_scope=ldap3.SUBTREE,
attributes=["uid","cn","mail","departmentNumber","ou"]
)
forentryinconn.entries:
username=str(entry.uid)
full_name=str(entry.cn)ifentry.cnelseusername
str(entry.ifentry.""
user_dn=str(entry.entry_dn)
# Find which groups this user belongs to
user_groups=[gforg,membersingroup_members.items()
ifuser_dninmembers]
# Map group → department
dept="default"
forgroupinuser_groups:
forkey,valin{
"engineering":"engineering",
"developers":"engineering",
"marketing":"marketing",
"finance":"finance",
"hr":"hr",
"executive":"executive",
"management":"executive",
}.items():
ifkeyingroup:
dept=val
break
users[username]={
"full_name":full_name,
"email":
"dept":dept,
"groups":user_groups,
"source":"OpenLDAP"
}
log(f"[LDAP] Fetched {len(users)} users")
returnusers
# ── Generate Squid per-user ACLs ──────────────────────────────────────
defgenerate_squid_user_acls(all_users):
dept_users={}
forusername,datainall_users.items():
dept=data["dept"]
dept_users.setdefault(dept,[]).append(username)
acl_lines=["# Auto-generated user→department ACLs",
f"# Last sync: {datetime.now()}",
""]
fordept,usersindept_users.items():
user_list=" ".join(users)
acl_lines.append(f"# {dept.upper()} — {len(users)} users")
acl_lines.append(f"acl dept_{dept}_users proxy_auth {user_list}")
acl_lines.append(f"http_access allow dept_{dept}_users dept_{dept}_wl")
acl_lines.append(f"http_access deny dept_{dept}_users dept_{dept}_blk")
acl_lines.append("")
acl_file=f"{SQUID_DIR}/user_acls.conf"
withopen(acl_file,"w")asf:
f.write("\n".join(acl_lines))
log(f"[SQUID] User ACLs written: {acl_file}")
# Reconfigure Squid
result=subprocess.run(["squid","-k","reconfigure"],
capture_output=True,text=True)
ifresult.returncode==0:
log("[SQUID] Reconfigured successfully")
else:
log(f"[SQUID ERROR] {result.stderr.strip()}")
# ── Main Sync ─────────────────────────────────────────────────────────
def sync_all():
init_db()
all_users={}
# Sync from Active Directory
try:
log("[*] Connecting to Active Directory...")
ad_conn=connect_ldap(AD_CONFIG)
ad_users=fetch_ad_users(ad_conn,AD_CONFIG)
all_users.update(ad_users)
foru,dinad_users.items():
save_user(u,d["full_name"],d["email"],
d["dept"],d["groups"],"AD")
ad_conn.unbind()
exceptExceptionase:
log(f"[AD ERROR] {e}")
# Sync from OpenLDAP
try:
log("[*] Connecting to OpenLDAP...")
ldap_conn=connect_ldap(OPENLDAP_CONFIG)
ldap_users=fetch_ldap_users(ldap_conn,OPENLDAP_CONFIG)
all_users.update(ldap_users)
foru,dinldap_users.items():
save_user(u,d["full_name"],d["email"],
d["dept"],d["groups"],"OpenLDAP")
ldap_conn.unbind()
exceptExceptionase:
log(f"[LDAP ERROR] {e}")
# Generate Squid ACLs
generate_squid_user_acls(all_users)
log(f"[✔] Sync complete — {len(all_users)} users across "
f"{len(set(d['dept'] for d in all_users.values()))} departments")
returnall_users
if__name__=="__main__":
sync_all()
Add to /etc/squid/squid.conf:
squid
# ── Kerberos / NTLM Authentication (AD) ─────────────────────────────
auth_param negotiate program /usr/lib/squid/negotiate_kerberos_auth \
-s HTTP/proxy.company.com@COMPANY.COM \
-k /etc/squid/squid.keytab \
-t none
auth_param negotiate children 20 startup=5 idle=1
auth_param negotiate keep_alive on
# ── NTLM fallback ────────────────────────────────────────────────────
auth_param ntlm program /usr/bin/ntlm_auth \
--helper-protocol=squid-2.5-ntlmssp
auth_param ntlm children 20
# ── Basic LDAP fallback (OpenLDAP) ───────────────────────────────────
auth_param basic program /usr/lib/squid/basic_ldap_auth \
-R -b "dc=company,dc=com" \
-D "cn=squid-proxy,cn=Users,dc=company,dc=com" \
-w "Str0ngP@ss!" \
-f "uid=%s" \
-h ldap.company.com
auth_param basic realm "Company Internet Proxy"
auth_param basic children 10
# Require authentication
acl authenticated proxy_auth REQUIRED
http_access deny !authenticated
# Include user ACLs (auto-generated by sync script)
include /etc/squid/departments/user_acls.conf
include /etc/squid/departments/dept_acls.conf
Save as /opt/web-blocker/report_dashboard.py:
python
#!/usr/bin/env python3
"""
Web Blocking Reports Dashboard
Parses Squid access logs → rich analytics dashboard
"""
importre,json,os,sqlite3
fromdatetimeimportdatetime,timedelta
fromcollectionsimportdefaultdict,Counter
SQUID_LOG="/var/log/squid/access.log"
REPORT_HTML="/var/www/html/usb-dashboard/webreport.html"
DB_FILE="/var/lib/web-blocker/users.db"
# ── Parse Squid Log ───────────────────────────────────────────────────
defparse_squid_log(hours=24):
"""
Squid log format:
timestamp elapsed client action/code bytes method url user ...
"""
cutoff=datetime.now()-timedelta(hours=hours)
events=[]
pattern=re.compile(
r'(\d+\.\d+)\s+\d+\s+(\S+)\s+(\w+)/(\d+)\s+\d+\s+(\w+)\s+(\S+)'
r'\s+(\S+)'
)
try:
withopen(SQUID_LOG,"r",errors="ignore")asf:
forlineinf:
m=pattern.match(line)
ifnotm:
continue
ts,client,action,code,method,url,user=m.groups()
event_time=datetime.fromtimestamp(float(ts))
ifevent_time<cutoff:
continue
events.append({
"ts":event_time,
"client":client,
"action":action,
"code":int(code),
"method":method,
"url":url,
"user":user.replace("-","anonymous"),
"blocked":actionin("TCP_DENIED","TCP_MISS_ABORTED")
orint(code)in(403,407)
})
exceptFileNotFoundError:
print(f"[!] Log not found: {SQUID_LOG}")
returnevents
# ── Analyse Events ────────────────────────────────────────────────────
defanalyse(events):
total=len(events)
blocked=[eforeineventsife["blocked"]]
allowed=[eforeineventsifnote["blocked"]]
# Extract domain from URL
defdomain(url):
m=re.search(r'https?://([^/:]+)',url)
ifm:returnm.group(1).lstrip("www.")
m=re.search(r'([^/:]+):\d+',url)
ifm:returnm.group(1).lstrip("www.")
returnurl[:50]
# Top blocked domains
blocked_domains=Counter(domain(e["url"])foreinblocked)
top_blocked=blocked_domains.most_common(15)
# Top users attempting blocked sites
blocked_users=Counter(e["user"]foreinblocked)
top_users=blocked_users.most_common(10)
# Blocked attempts by hour
by_hour=defaultdict(int)
foreinblocked:
by_hour[e["ts"].strftime("%H:00")]+=1
hours_sorted=sorted(by_hour.items())
# Blocked by category (simple keyword match)
categories={
"Personal Email":["gmail","hotmail","yahoo","protonmail","tutanota","outlook.live"],
"Social Media":["facebook","twitter","instagram","tiktok","snapchat","reddit","x.com"],
"Streaming":["youtube","netflix","twitch","spotify","primevideo"],
"File Sharing":["dropbox","wetransfer","mega.nz","mediafire","pastebin"],
"Gaming":["steam","epicgames","roblox","battlenet"],
}
cat_counts=defaultdict(int)
foreinblocked:
d=domain(e["url"])
matched=False
forcat,keywordsincategories.items():
ifany(kindforkinkeywords):
cat_counts[cat]+=1
matched=True
break
ifnotmatched:
cat_counts["Other"]+=1
# Client IP activity
top_clients=Counter(e["client"]foreinblocked).most_common(10)
# Hourly trend (all traffic)
hourly_all=defaultdict(int)
hourly_blk=defaultdict(int)
foreinevents:
h=e["ts"].strftime("%H:00")
hourly_all[h]+=1
ife["blocked"]:
hourly_blk[h]+=1
all_hours=sorted(set(hourly_all)|set(hourly_blk))
hourly_total=[hourly_all.get(h,0)forhinall_hours]
hourly_block=[hourly_blk.get(h,0)forhinall_hours]
return{
"total":total,
"blocked_count":len(blocked),
"allowed_count":len(allowed),
"block_rate":round(len(blocked)/total*100,1)iftotalelse0,
"top_blocked":top_blocked,
"top_users":top_users,
"cat_counts":dict(cat_counts),
"top_clients":top_clients,
"all_hours":all_hours,
"hourly_total":hourly_total,
"hourly_block":hourly_block,
}
# ── Get user department from DB ───────────────────────────────────────
def get_user_dept_map():
try:
conn=sqlite3.connect(DB_FILE)
rows=conn.execute(
"SELECT username, department FROM user_policies"
).fetchall()
conn.close()
return{r[0]:r[1]forrinrows}
except:
return{}
# ── Generate Dashboard HTML ───────────────────────────────────────────
defgenerate_dashboard(stats,period_hours=24):
top_blocked_rows="".join(f"""
<tr>
<td>{i+1}</td>
<td style='color:#e74c3c'>{domain}</td>
<td>{count}</td>
<td>
<div style='background:#2c2f3e;border-radius:4px;height:8px;width:200px'>
<div style='background:#e74c3c;width:{min(count/max(stats["top_blocked"][0][1],1)*100,100):.0f}%;
height:8px;border-radius:4px'></div>
</div>
</td>
</tr>"""
fori,(domain,count)inenumerate(stats["top_blocked"]))
top_users_rows="".join(f"""
<tr>
<td>👤 {user}</td>
<td>{count}</td>
<td>
<div style='background:#2c2f3e;border-radius:4px;height:8px;width:150px'>
<div style='background:#f39c12;
width:{min(count/max(stats["top_users"][0][1],1)*100,100):.0f}%;
height:8px;border-radius:4px'></div>
</div>
</td>
</tr>"""
foruser,countinstats["top_users"])
cat_labels=json.dumps(list(stats["cat_counts"].keys()))
cat_values=json.dumps(list(stats["cat_counts"].values()))
hour_labels=json.dumps(stats["all_hours"])
hour_total=json.dumps(stats["hourly_total"])
hour_block=json.dumps(stats["hourly_block"])
html=f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="300">
<title>Web Blocking Report</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
<style>
* {{ box-sizing:border-box; margin:0; padding:0 }}
body {{ font-family:Segoe UI,Ubuntu,sans-serif;
background:#0f1117; color:#e0e0e0; }}
header {{ background:#1a1d27; padding:18px 30px;
border-bottom:2px solid #e74c3c;
display:flex; justify-content:space-between; align-items:center }}
header h1 {{ color:#e74c3c; font-size:1.4em }}
header span {{ color:#555; font-size:0.82em }}
nav {{ background:#1a1d27; padding:0 30px;
border-bottom:1px solid #2c2f3e;
display:flex; gap:4px }}
nav a {{ color:#888; padding:12px 18px; text-decoration:none;
font-size:0.88em; border-bottom:2px solid transparent;
transition:all 0.2s }}
nav a:hover, nav a.active {{ color:#e74c3c;
border-bottom-color:#e74c3c }}
.main {{ padding:24px 30px }}
.summary {{ display:grid;
grid-template-columns:repeat(4,1fr);
gap:16px; margin-bottom:24px }}
.card {{ background:#1a1d27; border-radius:10px;
padding:18px 22px;
border-left:4px solid #e74c3c }}
.card h3 {{ font-size:2em; color:#e74c3c; margin:0 }}
.card p {{ color:#888; font-size:0.82em; margin-top:5px }}
.card.green {{ border-color:#2ecc71 }}
.card.green h3 {{ color:#2ecc71 }}
.card.orange {{ border-color:#f39c12 }}
.card.orange h3 {{ color:#f39c12 }}
.card.blue {{ border-color:#3498db }}
.card.blue h3 {{ color:#3498db }}
.grid2 {{ display:grid; grid-template-columns:1fr 1fr;
gap:16px; margin-bottom:20px }}
.grid3 {{ display:grid; grid-template-columns:1fr 1fr 1fr;
gap:16px; margin-bottom:20px }}
.panel {{ background:#1a1d27; border-radius:10px; padding:20px }}
.panel h2 {{ font-size:0.95em; color:#e74c3c;
margin-bottom:16px;
padding-bottom:8px;
border-bottom:1px solid #2c2f3e }}
table {{ width:100%; border-collapse:collapse;
font-size:0.84em }}
th {{ background:#2c2f3e; color:#aaa;
padding:10px 12px; text-align:left;
font-weight:500 }}
td {{ padding:9px 12px;
border-bottom:1px solid #1e2130 }}
tr:hover td {{ background:#1e2130 }}
input[type=text] {{ background:#2c2f3e; border:1px solid #444;
color:#eee; padding:7px 12px;
border-radius:6px; width:260px;
margin-bottom:12px; font-family:inherit }}
.badge {{ padding:2px 9px; border-radius:10px;
font-size:0.78em; font-weight:bold }}
@media(max-width:900px) {{
.summary,.grid2,.grid3 {{ grid-template-columns:1fr 1fr }}
}}
</style>
</head>
<body>
<header>
<h1>🌐Web Blocking Report Dashboard</h1>
<span>Last {period_hours}h |
Auto-refresh 5 min |
{datetime.now().strftime('%Y-%m-%d %H:%M')}</span>
</header>
<nav>
<a href="index.html">🔌USB Audit</a>
<a href="risk.html">👤Risk Scores</a>
<a href="webreport.html" class="active">🌐Web Blocking</a>
<a href="/admin/requests">🔓Access Requests</a>
<a href="webblocker.html">⚙Policy Status</a>
</nav>
<div class="main">
<!-- Summary Cards -->
<div class="summary">
<div class="card">
<h3>{stats['total']:,}</h3>
<p>Total Requests</p>
</div>
<div class="card">
<h3>{stats['blocked_count']:,}</h3>
<p>Blocked Requests</p>
</div>
<div class="card green">
<h3>{stats['allowed_count']:,}</h3>
<p>Allowed Requests</p>
</div>
<div class="card orange">
<h3>{stats['block_rate']}%</h3>
<p>Block Rate</p>
</div>
</div>
<!-- Traffic Chart (full width) -->
<div class="panel" style="margin-bottom:20px">
<h2>📈Hourly Traffic — Allowed vs Blocked</h2>
<canvas id="hourlyChart" height="80"></canvas>
</div>
<!-- Category + Top Domains -->
<div class="grid2">
<div class="panel">
<h2>📊Blocks by Category</h2>
<canvas id="catChart" height="220"></canvas>
</div>
<div class="panel">
<h2>🚫Top Blocked Domains</h2>
<input type="text" id="domainSearch"
placeholder="🔍Filter domains..."
onkeyup="filterTable('domainTable','domainSearch')">
<table id="domainTable">
<tr><th>#</th><th>Domain</th><th>Attempts</th><th>Volume</th></tr>
{top_blocked_rows}
</table>
</div>
</div>
<!-- Top Users -->
<div class="grid2">
<div class="panel">
<h2>👤Top Users — Blocked Attempts</h2>
<table>
<tr><th>User</th><th>Attempts</th><th>Volume</th></tr>
{top_users_rows}
</table>
</div>
<div class="panel">
<h2>🖥Top Client IPs — Blocked</h2>
<table>
<tr><th>IP Address</th><th>Attempts</th></tr>
{"".join(f"<tr><td>{ip}</td><td>{n}</td></tr>"
for ip,n in stats['top_clients'])}
</table>
</div>
</div>
</div>
<script>
// Hourly chart
new Chart(document.getElementById('hourlyChart'), {{
type: 'line',
data: {{
labels: {hour_labels},
datasets: [
{{
label: 'Allowed',
data: {hour_total},
borderColor: '#2ecc71',
backgroundColor: 'rgba(46,204,113,0.1)',
fill: true, tension: 0.4, pointRadius: 3
}},
{{
label: 'Blocked',
data: {hour_block},
borderColor: '#e74c3c',
backgroundColor: 'rgba(231,76,60,0.15)',
fill: true, tension: 0.4, pointRadius: 3
}}
]
}},
options: {{
responsive: true,
plugins: {{ legend: {{ labels: {{ color:'#aaa' }} }} }},
scales: {{
x: {{ ticks:{{ color:'#666' }}, grid:{{ color:'#1e2130' }} }},
y: {{ ticks:{{ color:'#666' }}, grid:{{ color:'#1e2130' }} }}
}}
}}
}});
// Category donut
new Chart(document.getElementById('catChart'), {{
type: 'doughnut',
data: {{
labels: {cat_labels},
datasets: [{{
data: {cat_values},
backgroundColor: [
'#e74c3c','#f39c12','#3498db',
'#2ecc71','#9b59b6','#1abc9c'
]
}}]
}},
options: {{
responsive: true,
plugins: {{ legend: {{ labels: {{ color:'#aaa' }} }} }}
}}
}});
// Table filter
function filterTable(tableId, inputId) {{
const q = document.getElementById(inputId).value.toLowerCase();
document.querySelectorAll(`#${{tableId}} tr:not(:first-child)`).forEach(r => {{
r.style.display = r.innerText.toLowerCase().includes(q) ? '' : 'none';
}});
}}
</script>
</body>
</html>"""
os.makedirs(os.path.dirname(REPORT_HTML),exist_ok=True)
withopen(REPORT_HTML,"w")asf:
f.write(html)
print(f"[✔] Dashboard: {REPORT_HTML}")
print(f" Total: {stats['total']:,} Blocked: {stats['blocked_count']:,}"
f" Rate: {stats['block_rate']}%")
if__name__=="__main__":
print("[*] Parsing Squid logs...")
events=parse_squid_log(hours=24)
print(f"[*] Analysing {len(events):,} events...")
stats=analyse(events)
generate_dashboard(stats)
bash
# Install Python LDAP library
pip3installldap3 --break-system-packages
# Test AD/LDAP sync
sudo python3 /opt/web-blocker/ad_ldap_sync.py
# Test report generation
sudo python3 /opt/web-blocker/report_dashboard.py
# Deploy as services
sudotee/etc/systemd/system/ldap-sync.service<<EOF
[Unit]
Description=AD/LDAP Policy Sync
After=network.target
[Service]
ExecStart=/usr/bin/python3 /opt/web-blocker/ad_ldap_sync.py
Type=oneshot
EOF
sudotee/etc/systemd/system/ldap-sync.timer<<EOF
[Unit]
Description=Run AD/LDAP sync every 15 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
EOF
sudosystemctlenable--now ldap-sync.timer
# Add to crontab
(crontab -l;echo"*/15 * * * * python3 /opt/web-blocker/ad_ldap_sync.py")\
|sudocrontab-
(crontab -l;echo"*/10 * * * * python3 /opt/web-blocker/report_dashboard.py")\
|sudocrontab-
┌─────────────────────────────────────────────────────────────────┐
│ Enterprise Web Security Platform │
├──────────────────┬──────────────────────────────────────────────┤
│ Directory │ Policy Engine │ Dashboard │
│ │ │ │
│ Windows AD ─────┼─► ad_ldap_sync.py │ /index.html │
│ (Kerberos/NTLM) │ ↓ │ /risk.html │
│ │ User → Dept map │ /webreport.html ◄─┐│
│ OpenLDAP ───────┼─► Squid ACL generator │ /webblocker.html ││
│ (Basic Auth) │ ↓ │ /admin/requests ││
│ │ /etc/squid/ │ ││
├──────────────────┤ departments/ │ nginx RBAC ││
│ Employees │ user_acls.conf │ + SSL/TLS ││
│ │ dept_acls.conf │ ││
│ john (Eng) ─────┼─► engineering policy │ ││
│ mary (Mktg) ────┼─► marketing policy │ ││
│ raj (Fin) ─────┼─► finance policy │ ││
│ │ ↓ │ ││
├──────────────────┤ Squid Proxy │ report_ ││
│ Blocked Page │ (port 3128/3129) │ dashboard.py ─────┘│
│ │ ↓ │ │
│ Branded 403 ────┼─► Access Request Form │ │
│ + Request btn │ ↓ │ │
│ │ request_server.py │ │
│ │ :8080 │ │
│ │ ↓ │ │
│ │ Manager email │ │
│ │ [Approve] / [Deny] │ │
│ │ ↓ │ │
│ │ whitelist.txt updated │ │
│ │ Squid reconfigured │ │
└──────────────────┴─────────────────────────┴─────────────────────┘
|
Page |
URL |
Access |
Content |
|
USB Audit |
|
Admin, SOC |
USB events across all machines |
|
Risk Scores |
|
Admin, SOC |
Per-user risk scoring |
|
Web Report |
|
Admin, SOC |
Blocked site analytics |
|
Policy Status |
|
Admin |
Active block categories |
|
Access Requests |
|
Admin, IT |
Bypass request queue |
Save as /opt/web-blocker/threat_response.py:
python
#!/usr/bin/env python3
"""
Automated Threat Response Engine
Monitors risk scores + web logs → auto-blocks users exceeding thresholds
"""
importsqlite3,json,os,subprocess,smtplib,time,logging
fromdatetimeimportdatetime,timedelta
fromcollectionsimportdefaultdict,Counter
frommime.multipartimportMIMEMultipart
frommime.textimportMIMEText
# ── Config ────────────────────────────────────────────────────────────
SMTP_SERVER="smtp.company.com"
SMTP_PORT=587
SMTP_USER="it-security@company.com"
SMTP_PASS="your_smtp_password"
IT_EMAIL="it-security@company.com"
SOC_EMAIL="soc@company.com"
RISK_DB="/var/lib/web-blocker/users.db"
RISK_SCORES="/var/usb-central/risk_scores.json"
SQUID_LOG="/var/log/squid/access.log"
BLOCK_LOG="/var/log/web-blocker/threat-response.log"
BLOCKED_USERS_FILE="/etc/squid/blocked_users.txt"
AD_BLOCK_SCRIPT="/opt/web-blocker/ad_block_user.ps1"
os.makedirs(os.path.dirname(BLOCK_LOG),exist_ok=True)
logging.basicConfig(
filename=BLOCK_LOG,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
# ── Thresholds ────────────────────────────────────────────────────────
THRESHOLDS={
"risk_score_critical":90,# Auto-block if risk score ≥ 90
"risk_score_high":60,# Alert IT if risk score ≥ 60
"blocked_attempts_1h":50,# Alert if >50 blocked attempts/hr
"blocked_attempts_15m":20,# Alert if >20 blocked attempts/15min
"unique_blocked_domains":15,# Alert if >15 unique blocked domains/hr
"usb_events_per_day":10,# Alert if >10 USB events/day
"after_hours_attempts":5,# Alert if >5 after-hours blocked attempts
}
RESPONSE_ACTIONS={
"CRITICAL":["squid_block","ad_disable","alert_soc","alert_manager"],
"HIGH":["squid_block","alert_soc","alert_manager"],
"MEDIUM":["alert_it","increase_monitoring"],
"LOW":["log_only"],
}
# ── Database ──────────────────────────────────────────────────────────
def init_response_db():
conn=sqlite3.connect(RISK_DB)
conn.execute("""
CREATE TABLE IF NOT EXISTS threat_responses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
threat_type TEXT,
severity TEXT,
actions TEXT,
details TEXT,
timestamp TEXT,
resolved INTEGER DEFAULT 0
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS blocked_users (
username TEXT PRIMARY KEY,
reason TEXT,
blocked_at TEXT,
blocked_by TEXT,
auto_block INTEGER DEFAULT 1,
unblock_at TEXT
)
""")
conn.commit()
conn.close()
deflog_response(username,threat_type,severity,actions,details):
conn=sqlite3.connect(RISK_DB)
conn.execute("""
INSERT INTO threat_responses
(username, threat_type, severity, actions, details, timestamp)
VALUES (?,?,?,?,?,?)
""",(username,threat_type,severity,
json.dumps(actions),details,
datetime.now().isoformat()))
conn.commit()
conn.close()
logging.info(f"RESPONSE | {username} | {severity} | {threat_type} | {actions}")
defis_already_blocked(username):
conn=sqlite3.connect(RISK_DB)
row=conn.execute(
"SELECT 1 FROM blocked_users WHERE username=?",(username,)
).fetchone()
conn.close()
returnrowisnotNone
defrecord_block(username,reason,auto=True,hours=24):
unblock_at=(datetime.now()+timedelta(hours=hours)).isoformat()
conn=sqlite3.connect(RISK_DB)
conn.execute("""
INSERT OR REPLACE INTO blocked_users
(username, reason, blocked_at, blocked_by, auto_block, unblock_at)
VALUES (?,?,?,?,?,?)
""",(username,reason,datetime.now().isoformat(),
"ThreatEngine",1ifautoelse0,unblock_at))
conn.commit()
conn.close()
# ── Response Actions ──────────────────────────────────────────────────
defaction_squid_block(username):
"""Block user in Squid proxy"""
try:
blocked=set()
ifos.path.exists(BLOCKED_USERS_FILE):
withopen(BLOCKED_USERS_FILE)asf:
blocked=set(f.read().splitlines())
blocked.add(username)
withopen(BLOCKED_USERS_FILE,"w")asf:
f.write("\n".join(sorted(blocked)))
subprocess.run(["squid","-k","reconfigure"],
capture_output=True,check=False)
logging.info(f"SQUID_BLOCK | {username}")
returnTrue
exceptExceptionase:
logging.error(f"SQUID_BLOCK_FAIL | {username} | {e}")
returnFalse
defaction_ad_disable(username):
"""Disable AD account via PowerShell (Windows DC)"""
try:
ps_cmd=f"""
Disable-ADAccount -Identity '{username}' -Confirm:\$false
Add-ADGroupMember -Identity 'Quarantine' -Members '{username}'
Set-ADUser -Identity '{username}' `
-Description 'AUTO-BLOCKED by ThreatEngine {datetime.now()}'
"""
withopen(AD_BLOCK_SCRIPT,"w")asf:
f.write(ps_cmd)
# Execute on DC via SSH or WinRM
result=subprocess.run([
"ssh","Administrator@dc01.company.com",
f"powershell -File {AD_BLOCK_SCRIPT}"
],capture_output=True,text=True,timeout=30)
logging.info(f"AD_DISABLE | {username} | rc={result.returncode}")
returnresult.returncode==0
exceptExceptionase:
logging.error(f"AD_DISABLE_FAIL | {username} | {e}")
returnFalse
defaction_linux_lock(username):
"""Lock Linux user account"""
try:
subprocess.run(["usermod","-L",username],check=True)
subprocess.run(["pkill","-u",username],check=False)
logging.info(f"LINUX_LOCK | {username}")
returnTrue
exceptExceptionase:
logging.error(f"LINUX_LOCK_FAIL | {username} | {e}")
returnFalse
defsend_alert(to_list,subject,html_body):
try:
msg=MIMEMultipart("alternative")
msg["Subject"]=subject
msg["From"]=SMTP_USER
msg["To"]=", ".join(to_list)
msg.attach(MIMEText(html_body,"html"))
withsmtplib.SMTP(SMTP_SERVER,SMTP_PORT)ass:
s.starttls()
s.login(SMTP_USER,SMTP_PASS)
s.sendmail(SMTP_USER,to_list,msg.as_string())
logging.info(f"ALERT_SENT | {to_list} | {subject}")
exceptExceptionase:
logging.error(f"ALERT_FAIL | {e}")
defbuild_alert_html(username,severity,threat_type,details,
actions_taken,score=None):
color={"CRITICAL":"#e74c3c","HIGH":"#f39c12",
"MEDIUM":"#3498db","LOW":"#2ecc71"}.get(severity,"#666")
emoji={"CRITICAL":"🔴","HIGH":"🟡","MEDIUM":"🔵","LOW":"🟢"}.get(severity,"⚪")
acts="".join(f"<li>{a}</li>"forainactions_taken)
score_row=(f"<tr style='background:#fff'>"
f"<td style='padding:8px;color:#666'>Risk Score</td>"
f"<td style='padding:8px;font-weight:bold'>{score}</td></tr>"
ifscoreelse"")
returnf"""
<div style="font-family:sans-serif;max-width:620px;margin:auto">
<div style="background:{color};color:white;padding:16px 22px;
border-radius:8px 8px 0 0">
<h2 style="margin:0">{emoji} Threat Response — {severity}</h2>
</div>
<div style="background:#f9f9f9;padding:22px;border:1px solid #ddd">
<table style="width:100%;border-collapse:collapse">
<tr><td style="padding:8px;color:#666;width:140px">User</td>
<td style="padding:8px;font-weight:bold">{username}</td></tr>
<tr style="background:#fff">
<td style="padding:8px;color:#666">Threat Type</td>
<td style="padding:8px">{threat_type}</td></tr>
<tr><td style="padding:8px;color:#666">Severity</td>
<td style="padding:8px;font-weight:bold;color:{color}">{severity}</td></tr>
{score_row}
<tr style="background:#fff">
<td style="padding:8px;color:#666">Time</td>
<td style="padding:8px">{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</td></tr>
<tr><td style="padding:8px;color:#666">Details</td>
<td style="padding:8px">{details}</td></tr>
</table>
<div style="margin-top:18px">
<strong>Actions Taken:</strong>
<ul style="margin:8px 0 0 16px;color:#333">{acts}</ul>
</div>
<div style="margin-top:18px;padding:12px;background:#fff3cd;
border-radius:6px;font-size:0.88em;color:#856404">
⚠️Review this alert in the
<a href="https://usb-monitor.company.com/admin/requests">
Security Dashboard</a> and take appropriate action.
</div>
</div>
<div style="background:#eee;padding:10px 22px;font-size:0.78em;
color:#999;border-radius:0 0 8px 8px">
Automated Threat Response Engine | Company IT Security
</div>
</div>"""
# ── Threat Detection ──────────────────────────────────────────────────
def check_risk_scores():
"""Check USB risk scores from risk engine"""
threats=[]
try:
ifnotos.path.exists(RISK_SCORES):
returnthreats
withopen(RISK_SCORES)asf:
scores=json.load(f)
forusername,datainscores.items():
score=data.get("score",0)
ifscore>=THRESHOLDS["risk_score_critical"]:
threats.append({
"username":username,
"severity":"CRITICAL",
"threat_type":"USB Risk Score Critical",
"details":f"Risk score {score}/100 — "
f"Violations: {', '.join(data.get('violations',[])[:3])}",
"score":score,
})
elifscore>=THRESHOLDS["risk_score_high"]:
threats.append({
"username":username,
"severity":"HIGH",
"threat_type":"USB Risk Score High",
"details":f"Risk score {score}/100",
"score":score,
})
exceptExceptionase:
logging.error(f"RISK_SCORE_CHECK | {e}")
returnthreats
def check_web_log_anomalies():
"""Detect web log anomalies in last 15 minutes"""
threats=[]
try:
cutoff_15m=datetime.now()-timedelta(minutes=15)
cutoff_1h=datetime.now()-timedelta(hours=1)
user_blocks_15m=defaultdict(int)
user_blocks_1h=defaultdict(int)
user_domains_1h=defaultdict(set)
after_hrs_blocks=defaultdict(int)
importre
pattern=re.compile(
r'(\d+\.\d+)\s+\d+\s+\S+\s+(\w+)/(\d+)\s+\d+\s+\w+\s+(\S+)\s+(\S+)'
)
withopen(SQUID_LOG,"r",errors="ignore")asf:
forlineinf:
m=pattern.match(line)
ifnotm:continue
ts_raw,action,code,url,user=m.groups()
ts=datetime.fromtimestamp(float(ts_raw))
user=user.replace("-","")
ifnotuser:continue
blocked=actionin("TCP_DENIED",)orint(code)==403
ifblockedandts>=cutoff_1h:
user_blocks_1h[user]+=1
dom=re.search(r'https?://([^/:]+)',url)
ifdom:user_domains_1h[user].add(dom.group(1))
ifts.hour<7orts.hour>=20:
after_hrs_blocks[user]+=1
ifblockedandts>=cutoff_15m:
user_blocks_15m[user]+=1
# Evaluate thresholds
all_users=set(user_blocks_1h)|set(user_blocks_15m)
foruserinall_users:
b15=user_blocks_15m.get(user,0)
b1h=user_blocks_1h.get(user,0)
doms=len(user_domains_1h.get(user,set()))
ah=after_hrs_blocks.get(user,0)
ifb15>=THRESHOLDS["blocked_attempts_15m"]:
threats.append({
"username":user,
"severity":"HIGH",
"threat_type":"Rapid Blocked Attempts",
"details":f"{b15} blocked attempts in 15 min"
f" | {doms} unique domains",
"score":None,
})
elifb1h>=THRESHOLDS["blocked_attempts_1h"]:
threats.append({
"username":user,
"severity":"MEDIUM",
"threat_type":"Excessive Blocked Attempts",
"details":f"{b1h} blocked attempts in 1hr"
f" | {doms} unique domains",
"score":None,
})
ifdoms>=THRESHOLDS["unique_blocked_domains"]:
threats.append({
"username":user,
"severity":"HIGH",
"threat_type":"Domain Enumeration / Data Exfil Attempt",
"details":f"Attempted {doms} unique blocked domains in 1hr",
"score":None,
})
ifah>=THRESHOLDS["after_hours_attempts"]:
threats.append({
"username":user,
"severity":"MEDIUM",
"threat_type":"After-Hours Activity",
"details":f"{ah} blocked attempts outside business hours",
"score":None,
})
exceptFileNotFoundError:
logging.warning("Squid log not found")
exceptExceptionase:
logging.error(f"WEB_LOG_CHECK | {e}")
returnthreats
# ── Execute Response ──────────────────────────────────────────────────
defrespond(threat):
username=threat["username"]
severity=threat["severity"]
threat_type=threat["threat_type"]
details=threat["details"]
score=threat.get("score")
ifis_already_blocked(username):
logging.info(f"SKIP | {username} already blocked")
return
actions_cfg=RESPONSE_ACTIONS.get(severity,["log_only"])
actions_taken=[]
foractioninactions_cfg:
ifaction=="squid_block":
ifaction_squid_block(username):
actions_taken.append("✅ Squid proxy access blocked")
record_block(username,threat_type)
elifaction=="ad_disable":
ifaction_ad_disable(username):
actions_taken.append("✅ Active Directory account disabled")
# Also lock Linux account
ifaction_linux_lock(username):
actions_taken.append("✅ Linux account locked")
elifaction=="alert_soc":
html=build_alert_html(username,severity,threat_type,
details,actions_taken,score)
send_alert([SOC_EMAIL],
f"🔴 [{severity}] Threat Detected: {username}",
html)
actions_taken.append("✅ SOC team alerted")
elifaction=="alert_manager":
html=build_alert_html(username,severity,threat_type,
details,actions_taken,score)
send_alert([IT_EMAIL],
f"⚠️ [{severity}] Security Alert: {username}",
html)
actions_taken.append("✅ IT manager alerted")
elifaction=="alert_it":
html=build_alert_html(username,severity,threat_type,
details,actions_taken,score)
send_alert([IT_EMAIL],
f"ℹ️ [{severity}] Security Notice: {username}",
html)
actions_taken.append("✅ IT team notified")
elifaction=="increase_monitoring":
actions_taken.append("✅ Enhanced monitoring enabled")
elifaction=="log_only":
actions_taken.append("📝 Event logged")
log_response(username,threat_type,severity,
actions_taken,details)
print(f"[{severity}] {username} — {threat_type} — {len(actions_taken)} actions")
# ── Main Loop ─────────────────────────────────────────────────────────
def run_once():
init_response_db()
print(f"[*] Threat scan — {datetime.now().strftime('%H:%M:%S')}")
threats=check_risk_scores()+check_web_log_anomalies()
# Deduplicate by username+threat_type
seen,unique=set(),[]
fortinthreats:
key=f"{t['username']}|{t['threat_type']}"
ifkeynotinseen:
seen.add(key)
unique.append(t)
print(f"[*] Threats found: {len(unique)}")
forthreatinunique:
respond(threat)
defrun_continuous(interval=60):
print(f"[*] Threat Response Engine started (interval={interval}s)")
whileTrue:
run_once()
time.sleep(interval)
if__name__=="__main__":
importsys
if"--daemon"insys.argv:
run_continuous(60)
else:
run_once()
Save as /opt/web-blocker/executive_report.py:
python
#!/usr/bin/env python3
"""
Weekly Executive Security Report
Sends polished HTML email to leadership every Monday morning
"""
importsqlite3,json,os,re,smtplib
fromdatetimeimportdatetime,timedelta
fromcollectionsimportdefaultdict,Counter
frommime.multipartimportMIMEMultipart
frommime.textimportMIMEText
SMTP_SERVER="smtp.company.com"
SMTP_PORT=587
SMTP_USER="it-security@company.com"
SMTP_PASS="your_smtp_password"
EXEC_LIST=[
"ceo@company.com",
"cto@company.com",
"ciso@company.com",
"coo@company.com",
]
RISK_DB="/var/lib/web-blocker/users.db"
RISK_FILE="/var/usb-central/risk_scores.json"
SQUID_LOG="/var/log/squid/access.log"
SQUID_DIR="/var/usb-central/reports"
# ── Gather all stats ──────────────────────────────────────────────────
def gather_stats():
now=datetime.now()
week_ago=now-timedelta(days=7)
stats={
"period_start":week_ago.strftime("%d %b %Y"),
"period_end":now.strftime("%d %b %Y"),
"generated":now.strftime("%d %b %Y, %H:%M"),
}
# ── Web stats ──
total_req=blocked_req=0
top_blocked=Counter()
top_users_blk=Counter()
daily_blocked=defaultdict(int)
pattern=re.compile(
r'(\d+\.\d+)\s+\d+\s+\S+\s+(\w+)/(\d+)\s+\d+\s+\w+\s+(\S+)\s+(\S+)'
)
try:
withopen(SQUID_LOG,"r",errors="ignore")asf:
forlineinf:
m=pattern.match(line)
ifnotm:continue
ts_raw,action,code,url,user=m.groups()
ts=datetime.fromtimestamp(float(ts_raw))
ifts<week_ago:continue
total_req+=1
ifactionin("TCP_DENIED",)orint(code)==403:
blocked_req+=1
dom=re.search(r'https?://([^/:]+)',url)
ifdom:
top_blocked[dom.group(1).lstrip("www.")]+=1
user=user.replace("-","")
ifuser:top_users_blk[user]+=1
daily_blocked[ts.strftime("%a %d %b")]+=1
except:pass
stats["web_total"]=total_req
stats["web_blocked"]=blocked_req
stats["web_allowed"]=total_req-blocked_req
stats["web_block_pct"]=(round(blocked_req/total_req*100,1)
iftotal_reqelse0)
stats["top_blocked"]=top_blocked.most_common(5)
stats["top_violators"]=top_users_blk.most_common(5)
stats["daily_blocked"]=list(daily_blocked.items())[-7:]
# ── USB stats ──
usb_total=usb_mounts=usb_files=0
try:
importglob,csv
forfinglob.glob(f"{SQUID_DIR}/**/*.csv",recursive=True):
withopen(f,newline="",errors="ignore")asfh:
forrowincsv.DictReader(fh):
try:
ts=datetime.strptime(
row.get("Timestamp","")[:19],
"%Y-%m-%d %H:%M:%S")
ifts<week_ago:continue
except:continue
usb_total+=1
evt=row.get("Event",row.get("EventType",""))
if"Mount"inevt:usb_mounts+=1
if"File"inevt:usb_files+=1
except:pass
stats["usb_total"]=usb_total
stats["usb_mounts"]=usb_mounts
stats["usb_files"]=usb_files
# ── Risk scores ──
critical_users=high_users=[]
try:
withopen(RISK_FILE)asf:
scores=json.load(f)
critical_users=[(u,d["score"])foru,dinscores.items()
ifd.get("level")=="CRITICAL"]
high_users=[(u,d["score"])foru,dinscores.items()
ifd.get("level")=="HIGH"]
except:pass
stats["critical_users"]=sorted(critical_users,
key=lambdax:x[1],reverse=True)[:5]
stats["high_users"]=sorted(high_users,
key=lambdax:x[1],reverse=True)[:5]
# ── Threat responses ──
threat_count=auto_blocks=0
try:
conn=sqlite3.connect(RISK_DB)
row=conn.execute("""
SELECT COUNT(*), SUM(CASE WHEN severity='CRITICAL' THEN 1 ELSE 0 END)
FROM threat_responses
WHERE timestamp >= ?
""",(week_ago.isoformat(),)).fetchone()
threat_count=row[0]or0
auto_blocks=row[1]or0
conn.close()
except:pass
stats["threat_count"]=threat_count
stats["auto_blocks"]=auto_blocks
# ── Access requests ──
req_total=req_approved=req_denied=req_pending=0
try:
conn=sqlite3.connect(RISK_DB)
rows=conn.execute("""
SELECT status, COUNT(*) FROM requests
WHERE created_at >= ? GROUP BY status
""",(week_ago.isoformat(),)).fetchall()
forstatus,countinrows:
req_total+=count
ifstatus=="approved":req_approved+=count
elifstatus=="denied":req_denied+=count
else:req_pending+=count
conn.close()
except:pass
stats["req_total"]=req_total
stats["req_approved"]=req_approved
stats["req_denied"]=req_denied
stats["req_pending"]=req_pending
returnstats
# ── Build HTML Report ─────────────────────────────────────────────────
defbuild_report(stats):
defstat_card(value,label,color="#e74c3c",sub=""):
returnf"""
<td style="padding:12px;text-align:center;width:25%">
<div style="background:#1a1d27;border-radius:10px;padding:18px 10px;
border-top:3px solid {color}">
<div style="font-size:2em;font-weight:bold;color:{color}">{value}</div>
<div style="font-size:0.8em;color:#aaa;margin-top:4px">{label}</div>
{f'<div style="font-size:0.75em;color:#555;margin-top:3px">{sub}</div>' if sub else ''}
</div>
</td>"""
deftrend_row(label,value,color="#e0e0e0"):
returnf"""<tr>
<td style="padding:9px 14px;border-bottom:1px solid #2c2f3e;
color:#aaa;font-size:0.88em">{label}</td>
<td style="padding:9px 14px;border-bottom:1px solid #2c2f3e;
font-weight:bold;color:{color};font-size:0.88em">{value}</td>
</tr>"""
top_blocked_rows="".join(
f"""<tr>
<td style="padding:7px 14px;border-bottom:1px solid #2c2f3e;
color:#e74c3c;font-size:0.85em">{dom}</td>
<td style="padding:7px 14px;border-bottom:1px solid #2c2f3e;
font-size:0.85em">{cnt:,}</td>
</tr>"""
fordom,cntinstats["top_blocked"]
)
top_violators_rows="".join(
f"""<tr>
<td style="padding:7px 14px;border-bottom:1px solid #2c2f3e;
font-size:0.85em">👤 {user}</td>
<td style="padding:7px 14px;border-bottom:1px solid #2c2f3e;
font-size:0.85em">{cnt:,}</td>
</tr>"""
foruser,cntinstats["top_violators"]
)
critical_rows="".join(
f"""<tr>
<td style="padding:7px 14px;border-bottom:1px solid #2c2f3e;
font-size:0.85em">👤 {user}</td>
<td style="padding:7px 14px;border-bottom:1px solid #2c2f3e;
font-weight:bold;color:#e74c3c;font-size:0.85em">{score}</td>
</tr>"""
foruser,scoreinstats["critical_users"]
)or"<tr><td colspan='2' style='padding:10px;color:#555;font-size:0.85em'>None this week ✅</td></tr>"
daily_bars="".join(f"""
<td style="text-align:center;vertical-align:bottom;padding:0 4px">
<div style="background:#e74c3c;width:32px;
height:{min(int(cnt/max(max(c for _,c in stats['daily_blocked']),1)*80),80)}px;
border-radius:4px 4px 0 0;display:inline-block"></div>
<div style="font-size:0.7em;color:#555;margin-top:4px">{day[:3]}</div>
<div style="font-size:0.7em;color:#aaa">{cnt}</div>
</td>"""
forday,cntinstats["daily_blocked"]
)
html=f"""<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#0a0d14;
font-family:Segoe UI,Helvetica,Arial,sans-serif">
<!-- Header -->
<div style="background:linear-gradient(135deg,#1a1d27 0%,#2c2f3e 100%);
padding:32px 40px;border-bottom:3px solid #e74c3c">
<table style="width:100%"><tr>
<td>
<div style="color:#e74c3c;font-size:1.5em;font-weight:bold">
🛡Weekly Security Report
</div>
<div style="color:#888;font-size:0.85em;margin-top:4px">
{stats['period_start']} — {stats['period_end']}
</div>
</td>
<td style="text-align:right;color:#555;font-size:0.8em">
Generated: {stats['generated']}<br>
IT Security Team
</td>
</tr></table>
</div>
<div style="padding:30px 40px;background:#0f1117">
<!-- Executive Summary -->
<div style="color:#e74c3c;font-size:0.8em;font-weight:bold;
letter-spacing:2px;text-transform:uppercase;margin-bottom:12px">
Executive Summary
</div>
<table style="width:100%;border-collapse:collapse;margin-bottom:28px">
<tr>
{stat_card(f"{stats['web_blocked']:,}", "Web Requests Blocked", "#e74c3c",
f"{stats['web_block_pct']}% block rate")}
{stat_card(f"{stats['usb_total']:,}", "USB Events", "#f39c12",
f"{stats['usb_mounts']} mounts")}
{stat_card(f"{stats['threat_count']:,}", "Threats Detected", "#9b59b6",
f"{stats['auto_blocks']} auto-blocked")}
{stat_card(f"{len(stats['critical_users'])}", "Critical Risk Users", "#e74c3c",
f"{len(stats['high_users'])} high risk")}
</tr>
</table>
<!-- 2-column layout -->
<table style="width:100%;border-collapse:collapse;
margin-bottom:24px;vertical-align:top">
<tr><td style="width:50%;padding-right:12px;vertical-align:top">
<!-- Web Blocking -->
<div style="background:#1a1d27;border-radius:10px;
padding:20px;margin-bottom:16px">
<div style="color:#e74c3c;font-size:0.82em;font-weight:bold;
letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">
🌐Web Blocking
</div>
<table style="width:100%;border-collapse:collapse">
{trend_row("Total Requests", f"{stats['web_total']:,}")}
{trend_row("Blocked", f"{stats['web_blocked']:,}", "#e74c3c")}
{trend_row("Allowed", f"{stats['web_allowed']:,}", "#2ecc71")}
{trend_row("Block Rate", f"{stats['web_block_pct']}%", "#f39c12")}
</table>
</div>
<!-- Top Blocked Sites -->
<div style="background:#1a1d27;border-radius:10px;padding:20px">
<div style="color:#e74c3c;font-size:0.82em;font-weight:bold;
letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">
🚫Top Blocked Sites
</div>
<table style="width:100%;border-collapse:collapse">
<tr>
<th style="padding:7px 14px;text-align:left;color:#555;
font-size:0.78em;border-bottom:1px solid #2c2f3e">Domain</th>
<th style="padding:7px 14px;text-align:left;color:#555;
font-size:0.78em;border-bottom:1px solid #2c2f3e">Attempts</th>
</tr>
{top_blocked_rows}
</table>
</div>
</td><td style="width:50%;padding-left:12px;vertical-align:top">
<!-- Access Requests -->
<div style="background:#1a1d27;border-radius:10px;
padding:20px;margin-bottom:16px">
<div style="color:#3498db;font-size:0.82em;font-weight:bold;
letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">
🔓Access Requests
</div>
<table style="width:100%;border-collapse:collapse">
{trend_row("Total Submitted", stats['req_total'])}
{trend_row("Approved", stats['req_approved'], "#2ecc71")}
{trend_row("Denied", stats['req_denied'], "#e74c3c")}
{trend_row("Pending Review", stats['req_pending'], "#f39c12")}
</table>
</div>
<!-- Top Policy Violators -->
<div style="background:#1a1d27;border-radius:10px;padding:20px">
<div style="color:#f39c12;font-size:0.82em;font-weight:bold;
letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">
⚠️Top Policy Violators
</div>
<table style="width:100%;border-collapse:collapse">
<tr>
<th style="padding:7px 14px;text-align:left;color:#555;
font-size:0.78em;border-bottom:1px solid #2c2f3e">User</th>
<th style="padding:7px 14px;text-align:left;color:#555;
font-size:0.78em;border-bottom:1px solid #2c2f3e">Attempts</th>
</tr>
{top_violators_rows}
</table>
</div>
</td></tr></table>
<!-- Daily Trend Bar Chart -->
<div style="background:#1a1d27;border-radius:10px;
padding:22px;margin-bottom:24px">
<div style="color:#e74c3c;font-size:0.82em;font-weight:bold;
letter-spacing:1px;text-transform:uppercase;margin-bottom:16px">
📅Daily Blocked Requests (Last 7 Days)
</div>
<table style="width:100%"><tr style="vertical-align:bottom">
{daily_bars}
</tr></table>
</div>
<!-- Critical Risk Users -->
<div style="background:#1a1d27;border-radius:10px;padding:22px">
<div style="color:#e74c3c;font-size:0.82em;font-weight:bold;
letter-spacing:1px;text-transform:uppercase;margin-bottom:14px">
🔴Critical Risk Users — Immediate Attention Required
</div>
<table style="width:100%;border-collapse:collapse">
<tr>
<th style="padding:8px 14px;text-align:left;color:#555;
font-size:0.78em;border-bottom:1px solid #2c2f3e">User</th>
<th style="padding:8px 14px;text-align:left;color:#555;
font-size:0.78em;border-bottom:1px solid #2c2f3e">Risk Score</th>
</tr>
{critical_rows}
</table>
</div>
</div>
<!-- Footer -->
<div style="background:#1a1d27;padding:18px 40px;
border-top:1px solid #2c2f3e;
font-size:0.78em;color:#444;text-align:center">
🛡Company IT Security |
<a href="https://usb-monitor.company.com"
style="color:#3498db;text-decoration:none">Open Dashboard</a>
| Auto-generated weekly report
</div>
</body>
</html>"""
returnhtml
def send_report():
print("[*] Gathering weekly stats...")
stats=gather_stats()
html=build_report(stats)
msg=MIMEMultipart("alternative")
msg["Subject"]=(f"📊 Weekly Security Report — "
f"{stats['period_start']} to {stats['period_end']}")
msg["From"]=SMTP_USER
msg["To"]=", ".join(EXEC_LIST)
msg.attach(MIMEText(html,"html"))
withsmtplib.SMTP(SMTP_SERVER,SMTP_PORT)ass:
s.starttls()
s.login(SMTP_USER,SMTP_PASS)
s.sendmail(SMTP_USER,EXEC_LIST,msg.as_string())
print(f"[✔] Executive report sent to {len(EXEC_LIST)} recipients")
if__name__=="__main__":
send_report()
Save as /opt/web-blocker/push_notifier.py:
python
#!/usr/bin/env python3
"""
Mobile Push Notification System
Sends real-time alerts via FCM (Android), APNs (iOS), and Slack
"""
importjson,sqlite3,os,time,requests,logging
fromdatetimeimportdatetime
# ── Config ────────────────────────────────────────────────────────────
FCM_SERVER_KEY="your_fcm_server_key"# Firebase Cloud Messaging
FCM_URL="https://fcm.googleapis.com/fcm/send"
APNS_KEY_FILE="/opt/web-blocker/apns/AuthKey_XXXXXXXX.p8"
APNS_KEY_ID="YOUR_KEY_ID"
APNS_TEAM_ID="YOUR_TEAM_ID"
APNS_BUNDLE_ID="com.company.securityapp"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
TEAMS_WEBHOOK="https://company.webhook.office.com/webhookb2/YOUR/TEAMS/WEBHOOK"
PUSH_DB="/var/lib/web-blocker/push_tokens.db"
PUSH_LOG="/var/log/web-blocker/push.log"
os.makedirs(os.path.dirname(PUSH_LOG),exist_ok=True)
logging.basicConfig(filename=PUSH_LOG,level=logging.INFO,
format="%(asctime)s %(message)s")
# ── Token Database ────────────────────────────────────────────────────
def init_push_db():
conn=sqlite3.connect(PUSH_DB)
conn.execute("""
CREATE TABLE IF NOT EXISTS push_tokens (
username TEXT PRIMARY KEY,
fcm_token TEXT,
apns_token TEXT,
platform TEXT,
role TEXT,
registered TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS push_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
platform TEXT,
title TEXT,
body TEXT,
severity TEXT,
sent_at TEXT,
success INTEGER
)
""")
conn.commit()
conn.close()
defregister_token(username,platform,token,role="soc"):
conn=sqlite3.connect(PUSH_DB)
ifplatform=="android":
conn.execute("""
INSERT OR REPLACE INTO push_tokens
(username, fcm_token, platform, role, registered)
VALUES (?,?,?,?,?)
""",(username,token,platform,role,datetime.now().isoformat()))
else:
conn.execute("""
INSERT OR REPLACE INTO push_tokens
(username, apns_token, platform, role, registered)
VALUES (?,?,?,?,?)
""",(username,token,platform,role,datetime.now().isoformat()))
conn.commit()
conn.close()
def get_soc_tokens():
conn=sqlite3.connect(PUSH_DB)
conn.row_factory=sqlite3.Row
rows=conn.execute(
"SELECT * FROM push_tokens WHERE role IN ('soc','admin')"
).fetchall()
conn.close()
return[dict(r)forrinrows]
deflog_push(username,platform,title,body,severity,success):
conn=sqlite3.connect(PUSH_DB)
conn.execute("""
INSERT INTO push_log (username, platform, title, body, severity, sent_at, success)
VALUES (?,?,?,?,?,?,?)
""",(username,platform,title,body,severity,
datetime.now().isoformat(),1ifsuccesselse0))
conn.commit()
conn.close()
# ── FCM (Android) ─────────────────────────────────────────────────────
defsend_fcm(token,title,body,severity,data=None):
priority_map={"CRITICAL":"high","HIGH":"high",
"MEDIUM":"normal","LOW":"normal"}
color_map={"CRITICAL":"#e74c3c","HIGH":"#f39c12",
"MEDIUM":"#3498db","LOW":"#2ecc71"}
payload={
"to":token,
"priority":priority_map.get(severity,"normal"),
"notification":{
"title":title,
"body":body,
"icon":"security_alert",
"color":color_map.get(severity,"#e74c3c"),
"sound":"alarm"ifseverity=="CRITICAL"else"default",
"badge":1,
"click_action":"SECURITY_DASHBOARD"
},
"data":{
"severity":severity,
"timestamp":datetime.now().isoformat(),
"dashboard":"https://usb-monitor.company.com",
**(dataor{})
}
}
try:
resp=requests.post(
FCM_URL,
json=payload,
headers={"Authorization":f"key={FCM_SERVER_KEY}",
"Content-Type":"application/json"},
timeout=10
)
success=resp.status_code==200
logging.info(f"FCM | {title} | {severity} | {resp.status_code}")
returnsuccess
exceptExceptionase:
logging.error(f"FCM_ERROR | {e}")
returnFalse
# ── APNs (iOS) ────────────────────────────────────────────────────────
defsend_apns(token,title,body,severity):
"""
APNs via HTTP/2 using jwt auth.
Requires: pip3 install PyJWT cryptography
"""
try:
importjwt,time
fromcryptography.hazmat.primitives.serializationimportload_pem_private_key
withopen(APNS_KEY_FILE,"rb")asf:
private_key=load_pem_private_key(f.read(),password=None)
auth_token=jwt.encode(
{"iss":APNS_TEAM_ID,"iat":int(time.time())},
private_key,
algorithm="ES256",
headers={"kid":APNS_KEY_ID}
)
priority="10"ifseverityin("CRITICAL","HIGH")else"5"
payload={
"aps":{
"alert":{"title":title,"body":body},
"badge":1,
"sound":"alarm.aiff"ifseverity=="CRITICAL"
else"default",
"interruption-level":"critical"ifseverity=="CRITICAL"
else"active"
},
"severity":severity,
"dashboard":"https://usb-monitor.company.com"
}
resp=requests.post(
f"https://api.push.apple.com/3/device/{token}",
json=payload,
headers={
"Authorization":f"bearer {auth_token}",
"apns-topic":APNS_BUNDLE_ID,
"apns-priority":priority,
"apns-push-type":"alert"
},
timeout=10
)
success=resp.status_code==200
logging.info(f"APNs | {title} | {severity} | {resp.status_code}")
returnsuccess
exceptExceptionase:
logging.error(f"APNs_ERROR | {e}")
returnFalse
# ── Slack Push ────────────────────────────────────────────────────────
defsend_slack_push(title,body,severity,details=None):
color_map={"CRITICAL":"danger","HIGH":"warning",
"MEDIUM":"#3498db","LOW":"good"}
emoji_map={"CRITICAL":"🔴","HIGH":"🟡","MEDIUM":"🔵","LOW":"🟢"}
fields=[{"title":"Severity","value":severity,"short":True},
{"title":"Time","value":datetime.now().strftime("%H:%M:%S"),"short":True}]
ifdetails:
fields+=[{"title":k,"value":v,"short":True}
fork,vindetails.items()]
payload={
"attachments":[{
"color":color_map.get(severity,"#666"),
"title":f"{emoji_map.get(severity,'')} {title}",
"text":body,
"fields":fields,
"actions":[{
"type":"button",
"text":"Open Dashboard",
"url":"https://usb-monitor.company.com",
"style":"danger"ifseverity=="CRITICAL"else"default"
}],
"footer":"Security Monitor",
"ts":int(time.time())
}]
}
try:
resp=requests.post(SLACK_WEBHOOK,json=payload,timeout=10)
logging.info(f"SLACK | {title} | {resp.status_code}")
returnresp.status_code==200
exceptExceptionase:
logging.error(f"SLACK_ERROR | {e}")
returnFalse
# ── MS Teams Push ─────────────────────────────────────────────────────
defsend_teams_push(title,body,severity,details=None):
color_map={"CRITICAL":"FF0000","HIGH":"FFA500",
"MEDIUM":"0076D7","LOW":"00CC00"}
facts=[{"name":k,"value":v}fork,vin(detailsor{}).items()]
payload={
"@type":"MessageCard",
"@context":"http://schema.org/extensions",
"themeColor":color_map.get(severity,"FF0000"),
"summary":title,
"sections":[{
"activityTitle":f"🛡 {title}",
"activitySubtitle":f"Severity: **{severity}**",
"activityText":body,
"facts":facts
}],
"potentialAction":[{
"@type":"OpenUri",
"name":"Open Security Dashboard",
"targets":[{"os":"default",
"uri":"https://usb-monitor.company.com"}]
}]
}
try:
resp=requests.post(TEAMS_WEBHOOK,json=payload,timeout=10)
logging.info(f"TEAMS | {title} | {resp.status_code}")
returnresp.status_codein(200,202)
exceptExceptionase:
logging.error(f"TEAMS_ERROR | {e}")
returnFalse
# ── Main Push Dispatcher ──────────────────────────────────────────────
defpush_alert(title,body,severity,
details=None,mobile=True,slack=True,teams=True):
"""
Send push notifications to all SOC staff + Slack + Teams
Call this from threat_response.py for critical events
"""
init_push_db()
results=[]
# Mobile push to all registered SOC devices
ifmobile:
tokens=get_soc_tokens()
fortintokens:
ift.get("fcm_token"):
ok=send_fcm(t["fcm_token"],title,body,severity,details)
log_push(t["username"],"android",title,body,severity,ok)
results.append(("FCM",t["username"],ok))
ift.get("apns_token"):
ok=send_apns(t["apns_token"],title,body,severity)
log_push(t["username"],"ios",title,body,severity,ok)
results.append(("APNs",t["username"],ok))
# Slack
ifslack:
ok=send_slack_push(title,body,severity,details)
results.append(("Slack","channel",ok))
# Teams
ifteams:
ok=send_teams_push(title,body,severity,details)
results.append(("Teams","channel",ok))
sent=sum(1for_,_,okinresultsifok)
logging.info(f"PUSH_DONE | {title} | {severity} | {sent}/{len(results)} sent")
print(f"[PUSH] {title} | {severity} | {sent}/{len(results)} delivered")
returnresults
# ── Token Registration API ────────────────────────────────────────────
def registration_server():
"""Simple HTTP endpoint for app token registration"""
fromhttp.serverimportHTTPServer,BaseHTTPRequestHandler
importjson
classHandler(BaseHTTPRequestHandler):
deflog_message(self,*args):pass
defdo_POST(self):
ifself.path=="/register-push":
length=int(self.headers.get("Content-Length",0))
data=json.loads(self.rfile.read(length))
register_token(
data["username"],
data["platform"],
data["token"],
data.get("role","soc")
)
self.send_response(200)
self.end_headers()
self.wfile.write(b'{"status":"ok"}')
logging.info(f"TOKEN_REG | {data['username']} | {data['platform']}")
print("[*] Push token registration server on :8081")
HTTPServer(("0.0.0.0",8081),Handler).serve_forever()
if__name__=="__main__":
importsys
if"--register-server"insys.argv:
registration_server()
else:
# Test push
push_alert(
title="🔴 Test Security Alert",
body="This is a test push notification from the security platform.",
severity="HIGH",
details={"Host":"proxy-server","Test":"true"}
)
python
# Add to threat_response.py — import and call push_notifier
frompush_notifierimportpush_alert
# In respond() function, add to CRITICAL/HIGH actions:
defrespond(threat):
...
ifseverityin("CRITICAL","HIGH"):
push_alert(
title=f"🔴 {threat['threat_type']}",
body=f"User: {username} — {details[:100]}",
severity=severity,
details={
"User":username,
"Risk Score":str(scoreor"N/A"),
"Machine":os.uname().nodename
}
)
bash
# Install dependencies
pip3installrequests PyJWT cryptography ldap3 --break-system-packages
# Deploy threat response as daemon
sudotee/etc/systemd/system/threat-response.service<<EOF
[Unit]
Description=Automated Threat Response Engine
After=network.target squid.service
[Service]
ExecStart=/usr/bin/python3 /opt/web-blocker/threat_response.py --daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudosystemctlenable--now threat-response
# Deploy push registration server
sudotee/etc/systemd/system/push-register.service<<EOF
[Unit]
Description=Push Token Registration Server
After=network.target
[Service]
ExecStart=/usr/bin/python3 /opt/web-blocker/push_notifier.py --register-server
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudosystemctlenable--now push-register
# Schedule weekly executive report — every Monday 07:00
echo"0 7 * * MON python3 /opt/web-blocker/executive_report.py"\
|sudocrontab-
# Test everything
python3 /opt/web-blocker/threat_response.py # Single scan
python3 /opt/web-blocker/executive_report.py # Send report now
python3 /opt/web-blocker/push_notifier.py # Test push
┌──────────────────────────────────────────────────────────────────┐
│ Enterprise Security Platform — Full Stack │
├────────────────────┬─────────────────────────────────────────────┤
│ Detection │ Response │ Reporting │
│ │ │ │
│ USB Monitor ─────┼─► Threat Engine │ Live Dashboard │
│ Web Proxy ─────┼─► ├─ Squid block │ Risk Scores │
│ Risk Scores ─────┼─► ├─ AD disable │ Web Analytics │
│ Log Analysis ─────┼─► ├─ Linux lock │ Access Requests │
│ │ ├─ Email SOC/Mgr ───►│ │
│ │ ├─ Slack alert ───►│ Weekly Exec Email │
│ │ ├─ Teams alert ───►│ (Every Monday) │
│ │ └─ Mobile push ───►│ │
│ │ ├─ Android FCM │ Push Alerts │
│ │ ├─ iOS APNs │ ├─ Android │
│ │ └─ Auto-unblock │ ├─ iOS │
│ │ after 24h │ ├─ Slack │
│ │ │ └─ MS Teams │
└────────────────────┴─────────────────────────┴────────────────────┘
Here's the complete unified installer and configuration UI for both distros.
Part 1: Unified Installer Script
Save as install.sh and run as root:
bash
#!/bin/bash
# ================================================================
# Enterprise Security Platform — Unified Installer
# Supports: Ubuntu/Debian + RHEL/CentOS/Fedora
# Components: Squid, nginx, Python services, systemd units
# ================================================================
set -euo pipefail
# ── Colors ────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
log() { echo -e "${GREEN}[✔]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
error() { echo -e "${RED}[✘]${NC} $1"; exit 1; }
section() { echo -e "\n${CYAN}══════════════════════════════════════${NC}"; \
echo -e "${CYAN} $1${NC}"; \
echo -e "${CYAN}══════════════════════════════════════${NC}"; }
# ── Root check ────────────────────────────────────────────────────────
[ "$EUID" -ne 0 ] && error "Run as root: sudo bash install.sh"
# ── Detect distro ─────────────────────────────────────────────────────
if [ -f /etc/debian_version ]; then DISTRO="debian"
elif [ -f /etc/redhat-release ]; then DISTRO="rhel"
else error "Unsupported distro"; fi
log "Detected distro: $DISTRO"
# ── Load config ───────────────────────────────────────────────────────
CONFIG_FILE="./install.conf"
if [ ! -f "$CONFIG_FILE" ]; then
cat > "$CONFIG_FILE" <<'CONF'
# ── Company Settings ──────────────────────────────────────
COMPANY_NAME="Company Name"
DOMAIN="company.com"
AD_SERVER="dc01.company.com"
LDAP_SERVER="ldap.company.com"
# ── Network ───────────────────────────────────────────────
PROXY_PORT=3128
DASHBOARD_PORT=443
REQUEST_SERVER_PORT=8080
PUSH_SERVER_PORT=8081
ADMIN_UI_PORT=8443
LAN_NETWORK="192.168.0.0/16"
# ── Email ─────────────────────────────────────────────────
SMTP_SERVER="smtp.company.com"
SMTP_PORT=587
SMTP_USER="it-security@company.com"
SMTP_PASS="changeme"
IT_EMAIL="it-security@company.com"
SOC_EMAIL="soc@company.com"
# ── Integrations ──────────────────────────────────────────
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/HOOK"
TEAMS_WEBHOOK="https://company.webhook.office.com/YOUR/HOOK"
FCM_SERVER_KEY="your_fcm_key"
# ── Install paths ─────────────────────────────────────────
INSTALL_DIR="/opt/security-platform"
DATA_DIR="/var/lib/security-platform"
LOG_DIR="/var/log/security-platform"
WEB_DIR="/var/www/html/security"
CONF
warn "Config file created: $CONFIG_FILE"
warn "Edit it before continuing, then re-run."
exit 0
fi
source "$CONFIG_FILE"
log "Config loaded from $CONFIG_FILE"
# ── Directories ───────────────────────────────────────────────────────
section "Creating directories"
mkdir -p "$INSTALL_DIR"/{scripts,certs,keys}
mkdir -p "$DATA_DIR"/{db,reports,usb-reports}
mkdir -p "$LOG_DIR"
mkdir -p "$WEB_DIR"
mkdir -p /etc/squid/departments
mkdir -p /etc/nginx/usb-auth
log "Directories created"
# ── Package installation ──────────────────────────────────────────────
section "Installing packages"
if [ "$DISTRO" = "debian" ]; then
apt-get update -qq
apt-get install -y \
squid squid-openssl nginx python3 python3-pip \
sqlite3 curl wget git openssl \
krb5-user samba winbind libpam-winbind \
libnss-winbind apache2-utils rsync \
inotify-tools auditd audispd-plugins \
dnsmasq mailutils smbclient nssm 2>/dev/null || true
else
dnf install -y epel-release 2>/dev/null || \
yum install -y epel-release 2>/dev/null
dnf install -y \
squid nginx python3 python3-pip \
sqlite curl wget git openssl \
krb5-workstation samba-winbind \
httpd-tools rsync \
inotify-tools audit \
dnsmasq mailx 2>/dev/null || \
yum install -y \
squid nginx python3 python3-pip \
sqlite curl wget git openssl \
krb5-workstation samba-winbind \
httpd-tools rsync inotify-tools audit dnsmasq
fi
log "System packages installed"
# ── Python packages ───────────────────────────────────────────────────
section "Installing Python packages"
pip3 install --break-system-packages --quiet \
ldap3 requests PyJWT cryptography \
flask flask-login flask-wtf \
apscheduler paramiko 2>/dev/null || \
pip3 install --quiet \
ldap3 requests PyJWT cryptography \
flask flask-login flask-wtf \
apscheduler paramiko
log "Python packages installed"
# ── SSL Certificate ───────────────────────────────────────────────────
section "Generating SSL certificate"
CERT_DIR="$INSTALL_DIR/certs"
openssl req -x509 -nodes -days 3650 \
-newkey rsa:4096 \
-keyout "$CERT_DIR/server.key" \
-out "$CERT_DIR/server.crt" \
-subj "/C=IN/ST=Telangana/L=Hyderabad/O=$COMPANY_NAME/CN=security.$DOMAIN" \
2>/dev/null
chmod 600 "$CERT_DIR/server.key"
log "SSL certificate generated (10 year)"
# ── Copy platform scripts ─────────────────────────────────────────────
section "Installing platform scripts"
SCRIPTS=(
"usb_alerter.py"
"ad_ldap_sync.py"
"dept_policies.py"
"threat_response.py"
"executive_report.py"
"push_notifier.py"
"request_server.py"
"risk_engine.py"
"report_dashboard.py"
"generate_dashboard.py"
"admin_ui.py"
)
for script in "${SCRIPTS[@]}"; do
[ -f "./$script" ] && cp "./$script" "$INSTALL_DIR/scripts/" \
&& chmod +x "$INSTALL_DIR/scripts/$script" \
&& log "Installed: $script" \
|| warn "Missing: $script (install manually)"
done
# Inject config into all scripts
for script in "$INSTALL_DIR"/scripts/*.py; do
sed -i "s|smtp.company.com|$SMTP_SERVER|g" "$script" 2>/dev/null || true
sed -i "s|company.com|$DOMAIN|g" "$script" 2>/dev/null || true
sed -i "s|it-security@company.com|$IT_EMAIL|g" "$script" 2>/dev/null || true
sed -i "s|soc@company.com|$SOC_EMAIL|g" "$script" 2>/dev/null || true
sed -i "s|your_fcm_key|$FCM_SERVER_KEY|g" "$script" 2>/dev/null || true
done
log "Config injected into all scripts"
# ── Squid config ──────────────────────────────────────────────────────
section "Configuring Squid proxy"
cp /etc/squid/squid.conf /etc/squid/squid.conf.bak 2>/dev/null || true
cat > /etc/squid/squid.conf <<SQUID
# ── Enterprise Squid Config — auto-generated ──
http_port $PROXY_PORT
acl localnet src $LAN_NETWORK
acl SSL_ports port 443
acl Safe_ports port 80 443 8080 21 70 210 1025-65535
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
# Blocked users list
acl blocked_users proxy_auth "/etc/squid/blocked_users.txt"
http_access deny blocked_users
# Department ACLs (auto-generated)
include /etc/squid/departments/dept_acls.conf
include /etc/squid/departments/user_acls.conf
# Approved exceptions whitelist
acl whitelist dstdomain "/etc/squid/whitelist.txt"
http_access allow whitelist
# Blocked domains
acl blocked_sites dstdomain "/etc/squid/blocked_domains.txt"
http_access deny blocked_sites
http_access allow localnet
http_access deny all
# Block page
deny_info https://security.$DOMAIN/blocked/?url=%u all
# Logging
access_log /var/log/squid/access.log combined
cache_log /var/log/squid/cache.log
cache_mem 256 MB
SQUID
# Create empty list files
touch /etc/squid/blocked_users.txt
touch /etc/squid/whitelist.txt
# Basic blocked domains
cat > /etc/squid/blocked_domains.txt <<DOMAINS
gmail.com
mail.google.com
hotmail.com
yahoo.com
protonmail.com
facebook.com
twitter.com
x.com
instagram.com
tiktok.com
snapchat.com
reddit.com
dropbox.com
wetransfer.com
mega.nz
youtube.com
netflix.com
twitch.tv
spotify.com
DOMAINS
touch /etc/squid/departments/dept_acls.conf
touch /etc/squid/departments/user_acls.conf
squid -k parse && log "Squid config valid" || warn "Squid config has errors"
systemctl enable squid && systemctl restart squid
log "Squid started"
# ── nginx config ──────────────────────────────────────────────────────
section "Configuring nginx"
# RBAC password files
htpasswd -cb /etc/nginx/usb-auth/admins.htpasswd admin "$(openssl rand -base64 12)"
htpasswd -cb /etc/nginx/usb-auth/auditors.htpasswd auditor "$(openssl rand -base64 12)"
htpasswd -cb /etc/nginx/usb-auth/managers.htpasswd manager "$(openssl rand -base64 12)"
NGINX_CONF="/etc/nginx/sites-available/security-platform"
[ "$DISTRO" = "rhel" ] && NGINX_CONF="/etc/nginx/conf.d/security-platform.conf"
cat > "$NGINX_CONF" <<NGINX
server {
listen 443 ssl;
server_name security.$DOMAIN;
ssl_certificate $CERT_DIR/server.crt;
ssl_certificate_key $CERT_DIR/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
root $WEB_DIR;
index index.html;
# Admin UI (full access)
location /admin/ {
proxy_pass http://127.0.0.1:$ADMIN_UI_PORT/;
proxy_set_header Host \$host;
auth_basic "Admin UI";
auth_basic_user_file /etc/nginx/usb-auth/admins.htpasswd;
}
# Dashboard — admins + SOC
location / {
auth_basic "Security Dashboard";
auth_basic_user_file /etc/nginx/usb-auth/admins.htpasswd;
try_files \$uri \$uri/ =404;
}
# Risk report — admins only
location /risk.html {
auth_basic "Risk Report";
auth_basic_user_file /etc/nginx/usb-auth/admins.htpasswd;
}
# Block page — public (no auth)
location /blocked/ {
auth_basic off;
try_files \$uri \$uri/ =404;
}
# Access request API — public
location /api/ {
auth_basic off;
proxy_pass http://127.0.0.1:$REQUEST_SERVER_PORT/api/;
}
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=31536000";
}
server {
listen 80;
server_name security.$DOMAIN;
return 301 https://\$host\$request_uri;
}
NGINX
if [ "$DISTRO" = "debian" ]; then
ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/ 2>/dev/null || true
rm -f /etc/nginx/sites-enabled/default
fi
nginx -t && log "nginx config valid" || warn "nginx config has errors"
systemctl enable nginx && systemctl restart nginx
log "nginx started"
# ── auditd for USB ────────────────────────────────────────────────────
section "Configuring auditd (USB monitoring)"
cat > /etc/audit/rules.d/usb.rules <<'AUDIT'
-a always,exit -F arch=b64 -S mount -F auid>=1000 -k usb_mount
-w /media -p rwxa -k usb_media_access
-w /mnt -p rwxa -k usb_mnt_access
AUDIT
systemctl enable auditd && systemctl restart auditd
log "auditd configured"
# ── systemd services ──────────────────────────────────────────────────
section "Installing systemd services"
install_service() {
local name="$1" desc="$2" cmd="$3"
cat > "/etc/systemd/system/$name.service" <<SVC
[Unit]
Description=$desc
After=network.target squid.service
[Service]
ExecStart=$cmd
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
WorkingDirectory=$INSTALL_DIR/scripts
[Install]
WantedBy=multi-user.target
SVC
systemctl daemon-reload
systemctl enable "$name"
systemctl start "$name" && log "Service started: $name" \
|| warn "Service failed: $name"
}
install_service "usb-alerter" "USB Real-Time Alert Monitor" \
"/usr/bin/python3 $INSTALL_DIR/scripts/usb_alerter.py"
install_service "threat-response" "Automated Threat Response Engine" \
"/usr/bin/python3 $INSTALL_DIR/scripts/threat_response.py --daemon"
install_service "web-request-server" "Web Access Request Server" \
"/usr/bin/python3 $INSTALL_DIR/scripts/request_server.py"
install_service "push-register" "Push Token Registration Server" \
"/usr/bin/python3 $INSTALL_DIR/scripts/push_notifier.py --register-server"
install_service "admin-ui" "IT Admin Configuration UI" \
"/usr/bin/python3 $INSTALL_DIR/scripts/admin_ui.py"
# ── crontab ───────────────────────────────────────────────────────────
section "Setting up scheduled tasks"
crontab -l 2>/dev/null | grep -v "security-platform" > /tmp/existing_cron || true
cat >> /tmp/existing_cron <<CRON
# ── Security Platform ─────────────────────────────────────────────────
*/15 * * * * python3 $INSTALL_DIR/scripts/ad_ldap_sync.py >> $LOG_DIR/ldap.log 2>&1
*/30 * * * * python3 $INSTALL_DIR/scripts/generate_dashboard.py >> $LOG_DIR/dashboard.log 2>&1
*/10 * * * * python3 $INSTALL_DIR/scripts/report_dashboard.py >> $LOG_DIR/webreport.log 2>&1
0 * * * * python3 $INSTALL_DIR/scripts/risk_engine.py >> $LOG_DIR/risk.log 2>&1
0 7 * * 1 python3 $INSTALL_DIR/scripts/executive_report.py >> $LOG_DIR/exec-report.log 2>&1
0 6 * * * python3 $INSTALL_DIR/scripts/dept_policies.py >> $LOG_DIR/policies.log 2>&1
CRON
crontab /tmp/existing_cron
log "Crontab configured"
# ── Firewall ──────────────────────────────────────────────────────────
section "Configuring firewall"
if command -v ufw &>/dev/null; then
ufw allow 443/tcp comment "Security Dashboard HTTPS"
ufw allow "$PROXY_PORT/tcp" comment "Squid Proxy"
ufw allow 22/tcp comment "SSH"
ufw --force enable
log "ufw configured"
elif command -v firewall-cmd &>/dev/null; then
firewall-cmd --permanent --add-port=443/tcp
firewall-cmd --permanent --add-port="$PROXY_PORT/tcp"
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
log "firewalld configured"
fi
# ── Print credentials ─────────────────────────────────────────────────
section "Installation Complete"
ADMIN_PASS=$(grep admin /etc/nginx/usb-auth/admins.htpasswd | cut -d: -f2 || echo "see /etc/nginx/usb-auth/admins.htpasswd")
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Security Platform — Ready ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${CYAN}Dashboard:${NC} https://security.$DOMAIN"
echo -e " ${CYAN}Admin UI:${NC} https://security.$DOMAIN/admin/"
echo -e " ${CYAN}Squid Proxy:${NC} $(hostname -I | awk '{print $1}'):$PROXY_PORT"
echo ""
echo -e " ${YELLOW}Credentials (change immediately!):${NC}"
echo -e " Admin user: admin"
echo -e " Auditor user: auditor"
echo -e " Manager user: manager"
echo -e " Passwords: /etc/nginx/usb-auth/*.htpasswd"
echo ""
echo -e " ${YELLOW}Next steps:${NC}"
echo -e " 1. Edit $CONFIG_FILE with real values"
echo -e " 2. Set proxy on all clients: $(hostname -I | awk '{print $1}'):$PROXY_PORT"
echo -e " 3. Open Admin UI and configure departments"
echo -e " 4. Add DNS entry: security.$DOMAIN → $(hostname -I | awk '{print $1}')"
echo ""
echo -e " ${CYAN}Logs:${NC} $LOG_DIR"
echo -e " ${CYAN}Data:${NC} $DATA_DIR"
echo ""
Part 2: IT Admin Configuration UI
Save as admin_ui.py:
python
#!/usr/bin/env python3
"""
IT Admin Configuration UI
Full web interface for managing all platform settings
Built with Flask — runs on port 8443
"""
from flask import (Flask, render_template_string, request,
redirect, url_for, flash, jsonify, session)
from flask_login import (LoginManager, UserMixin,
login_user, logout_user,
login_required, current_user)
import sqlite3, json, os, subprocess, hashlib, secrets
from datetime import datetime
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
INSTALL_DIR = "/opt/security-platform"
DATA_DIR = "/var/lib/security-platform"
ADMIN_DB = f"{DATA_DIR}/db/admin.db"
POLICY_FILE = f"{INSTALL_DIR}/scripts/dept_policies.json"
BLOCKED_FILE = "/etc/squid/blocked_domains.txt"
WHITELIST = "/etc/squid/whitelist.txt"
BLOCKED_USERS= "/etc/squid/blocked_users.txt"
os.makedirs(os.path.dirname(ADMIN_DB), exist_ok=True)
# ── Auth ──────────────────────────────────────────────────────────────
login_manager = LoginManager(app)
login_manager.login_view = "login_page"
class User(UserMixin):
def __init__(self, uid, username, role):
self.id = uid; self.username = username; self.role = role
def init_admin_db():
conn = sqlite3.connect(ADMIN_DB)
conn.execute("""
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE,
password TEXT,
role TEXT DEFAULT 'admin',
created TEXT
)""")
conn.execute("""
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
admin TEXT,
action TEXT,
details TEXT,
timestamp TEXT
)""")
conn.execute("""
CREATE TABLE IF NOT EXISTS platform_config (
key TEXT PRIMARY KEY,
value TEXT,
updated TEXT
)""")
# Default admin
pw = hashlib.sha256("admin123".encode()).hexdigest()
conn.execute("""
INSERT OR IGNORE INTO admins (username,password,role,created)
VALUES ('admin',?,'superadmin',?)
""", (pw, datetime.now().isoformat()))
conn.commit()
conn.close()
@login_manager.user_loader
def load_user(uid):
conn = sqlite3.connect(ADMIN_DB)
row = conn.execute(
"SELECT id,username,role FROM admins WHERE id=?", (uid,)
).fetchone()
conn.close()
return User(*row) if row else None
def audit(action, details=""):
conn = sqlite3.connect(ADMIN_DB)
conn.execute("""
INSERT INTO audit_log (admin,action,details,timestamp)
VALUES (?,?,?,?)
""", (current_user.username if current_user.is_authenticated
else "system", action, details, datetime.now().isoformat()))
conn.commit()
conn.close()
def squid_reload():
subprocess.run(["squid","-k","reconfigure"],
capture_output=True, check=False)
# ── Base template ─────────────────────────────────────────────────────
BASE = '''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{ title }} — Security Admin</title>
<style>
* { box-sizing:border-box; margin:0; padding:0 }
body { font-family:Segoe UI,Ubuntu,sans-serif;
background:#0f1117; color:#e0e0e0;
display:flex; min-height:100vh }
aside { width:220px; background:#1a1d27; padding:20px 0;
border-right:1px solid #2c2f3e; flex-shrink:0;
display:flex; flex-direction:column }
.logo { padding:0 20px 20px; border-bottom:1px solid #2c2f3e; margin-bottom:12px }
.logo h2 { color:#e74c3c; font-size:1em }
.logo p { color:#555; font-size:0.75em; margin-top:3px }
nav a { display:block; padding:10px 20px; color:#888;
text-decoration:none; font-size:0.88em;
border-left:3px solid transparent; transition:all 0.15s }
nav a:hover, nav a.active { color:#e0e0e0; background:#2c2f3e;
border-left-color:#e74c3c }
nav .section { padding:16px 20px 6px; font-size:0.7em;
color:#444; text-transform:uppercase; letter-spacing:1px }
.main { flex:1; display:flex; flex-direction:column; overflow:hidden }
header { background:#1a1d27; padding:14px 28px;
border-bottom:1px solid #2c2f3e;
display:flex; justify-content:space-between; align-items:center }
header h1 { font-size:1em; color:#aaa }
header .user { font-size:0.82em; color:#555 }
.content { flex:1; padding:28px; overflow-y:auto }
.card { background:#1a1d27; border-radius:10px;
padding:22px; margin-bottom:20px }
.card h2 { font-size:0.95em; color:#e74c3c; margin-bottom:16px;
padding-bottom:10px; border-bottom:1px solid #2c2f3e }
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:20px }
.grid3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px }
.stat { background:#2c2f3e; border-radius:8px; padding:16px;
text-align:center }
.stat h3 { font-size:1.8em; color:#e74c3c; margin:0 }
.stat p { color:#888; font-size:0.8em; margin-top:4px }
input,select,textarea {
width:100%; background:#2c2f3e; border:1px solid #3a3d4e;
border-radius:6px; color:#e0e0e0; padding:9px 12px;
font-size:0.88em; font-family:inherit; outline:none;
transition:border 0.15s }
input:focus,select:focus,textarea:focus { border-color:#e74c3c }
textarea { height:120px; resize:vertical }
label { font-size:0.8em; color:#888; display:block; margin-bottom:5px }
.form-group { margin-bottom:14px }
.btn { padding:9px 20px; border-radius:6px; border:none;
cursor:pointer; font-size:0.88em; font-weight:600;
transition:opacity 0.2s; text-decoration:none; display:inline-block }
.btn:hover { opacity:0.85 }
.btn-primary { background:#e74c3c; color:white }
.btn-success { background:#2ecc71; color:white }
.btn-warning { background:#f39c12; color:white }
.btn-info { background:#3498db; color:white }
.btn-danger { background:#c0392b; color:white }
.btn-sm { padding:5px 12px; font-size:0.8em }
.btn-secondary { background:#2c2f3e; color:#aaa; border:1px solid #3a3d4e }
table { width:100%; border-collapse:collapse; font-size:0.85em }
th { background:#2c2f3e; color:#aaa; padding:10px 12px;
text-align:left; font-weight:500 }
td { padding:9px 12px; border-bottom:1px solid #1e2130 }
tr:hover td { background:#1e2130 }
.badge { padding:2px 9px; border-radius:10px; font-size:0.78em; font-weight:bold }
.badge-red { background:#e74c3c; color:white }
.badge-green { background:#2ecc71; color:white }
.badge-orange { background:#f39c12; color:white }
.badge-blue { background:#3498db; color:white }
.alert { padding:12px 16px; border-radius:6px; margin-bottom:16px; font-size:0.88em }
.alert-success { background:#1a3a2a; border:1px solid #2ecc71; color:#2ecc71 }
.alert-error { background:#3a1a1a; border:1px solid #e74c3c; color:#e74c3c }
.toggle { position:relative; display:inline-block; width:44px; height:24px }
.toggle input { opacity:0; width:0; height:0 }
.slider { position:absolute; cursor:pointer; inset:0;
background:#2c2f3e; border-radius:24px; transition:.3s }
.slider:before { position:absolute; content:""; height:18px; width:18px;
left:3px; bottom:3px; background:white;
border-radius:50%; transition:.3s }
input:checked + .slider { background:#e74c3c }
input:checked + .slider:before { transform:translateX(20px) }
.tabs { display:flex; border-bottom:1px solid #2c2f3e; margin-bottom:20px }
.tab { padding:10px 20px; cursor:pointer; font-size:0.88em; color:#888;
border-bottom:2px solid transparent; transition:all 0.15s }
.tab.active,.tab:hover { color:#e0e0e0; border-bottom-color:#e74c3c }
.tab-panel { display:none }
.tab-panel.active { display:block }
</style>
</head>
<body>
<aside>
<div class="logo">
<h2>🛡 Security Admin</h2>
<p>IT Management Console</p>
</div>
<nav>
<div class="section">Overview</div>
<a href="/admin/dashboard" class="{{ 'active' if page=='dashboard' else '' }}">
📊 Dashboard</a>
<div class="section">Web Control</div>
<a href="/admin/blocklist" class="{{ 'active' if page=='blocklist' else '' }}">
🚫 Block List</a>
<a href="/admin/whitelist" class="{{ 'active' if page=='whitelist' else '' }}">
✅ Whitelist</a>
<a href="/admin/departments" class="{{ 'active' if page=='departments' else '' }}">
🏢 Departments</a>
<a href="/admin/requests" class="{{ 'active' if page=='requests' else '' }}">
🔓 Access Requests</a>
<div class="section">Users</div>
<a href="/admin/users" class="{{ 'active' if page=='users' else '' }}">
👤 User Policies</a>
<a href="/admin/blocked-users" class="{{ 'active' if page=='blocked-users' else '' }}">
🔒 Blocked Users</a>
<a href="/admin/risk" class="{{ 'active' if page=='risk' else '' }}">
⚠️ Risk Scores</a>
<div class="section">Settings</div>
<a href="/admin/config" class="{{ 'active' if page=='config' else '' }}">
⚙️ Configuration</a>
<a href="/admin/alerts" class="{{ 'active' if page=='alerts' else '' }}">
🔔 Alert Rules</a>
<a href="/admin/admins" class="{{ 'active' if page=='admins' else '' }}">
👑 Admin Users</a>
<a href="/admin/auditlog" class="{{ 'active' if page=='auditlog' else '' }}">
📋 Audit Log</a>
<div class="section">Actions</div>
<a href="/admin/services" class="{{ 'active' if page=='services' else '' }}">
🔧 Services</a>
<a href="/admin/logout">🚪 Logout</a>
</nav>
</aside>
<div class="main">
<header>
<h1>{{ title }}</h1>
<div class="user">
👤 {{ current_user.username }} ({{ current_user.role }}) |
{{ now }}
</div>
</header>
<div class="content">
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat,msg in msgs %}
<div class="alert alert-{{ 'success' if cat=='success' else 'error' }}">
{{ msg }}
</div>
{% endfor %}
{% endwith %}
{{ content }}
</div>
</div>
<script>
function switchTab(tabId) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById('panel-'+tabId).classList.add('active');
event.target.classList.add('active');
}
</script>
</body>
</html>'''
def render(title, content, page=""):
return render_template_string(
BASE, title=title, content=content,
page=page, now=datetime.now().strftime("%Y-%m-%d %H:%M")
)
# ── Login ─────────────────────────────────────────────────────────────
@app.route("/admin/login", methods=["GET","POST"])
def login_page():
if request.method == "POST":
username = request.form.get("username","")
password = hashlib.sha256(
request.form.get("password","").encode()
).hexdigest()
conn = sqlite3.connect(ADMIN_DB)
row = conn.execute(
"SELECT id,username,role FROM admins WHERE username=? AND password=?",
(username, password)
).fetchone()
conn.close()
if row:
login_user(User(*row))
audit("LOGIN", f"Successful login from {request.remote_addr}")
return redirect(url_for("dashboard"))
flash("Invalid credentials", "error")
form = '''
<div style="max-width:380px;margin:80px auto">
<div class="card">
<h2 style="text-align:center;margin-bottom:20px">🛡 Admin Login</h2>
<form method="POST">
<div class="form-group">
<label>Username</label>
<input name="username" placeholder="admin" required autofocus>
</div>
<div class="form-group">
<label>Password</label>
<input name="password" type="password" placeholder="••••••••" required>
</div>
<button class="btn btn-primary" style="width:100%" type="submit">
Login →
</button>
</form>
</div>
</div>'''
return render_template_string(
BASE.replace("{% if current_user.is_authenticated %}", ""),
title="Login", content=form, page="login",
now=datetime.now().strftime("%H:%M")
)
@app.route("/admin/logout")
@login_required
def logout():
audit("LOGOUT")
logout_user()
return redirect(url_for("login_page"))
# ── Dashboard ─────────────────────────────────────────────────────────
@app.route("/admin/dashboard")
@app.route("/admin/")
@app.route("/admin")
@login_required
def dashboard():
# Quick stats
blocked_count = sum(1 for _ in open(BLOCKED_FILE)
if _.strip() and not _.startswith("#")) \
if os.path.exists(BLOCKED_FILE) else 0
white_count = sum(1 for _ in open(WHITELIST)
if _.strip()) \
if os.path.exists(WHITELIST) else 0
blocked_users = sum(1 for _ in open(BLOCKED_USERS_FILE)
if _.strip()) \
if os.path.exists(BLOCKED_USERS_FILE) else 0
# Service statuses
services = ["squid","nginx","threat-response",
"usb-alerter","web-request-server","admin-ui"]
svc_rows = ""
for svc in services:
r = subprocess.run(["systemctl","is-active",svc],
capture_output=True, text=True)
active = r.stdout.strip() == "active"
badge = (f'<span class="badge badge-green">running</span>'
if active else
f'<span class="badge badge-red">stopped</span>')
restart = f'<a href="/admin/service/{svc}/restart" class="btn btn-sm btn-warning">↺ Restart</a>'
svc_rows += f"<tr><td>{svc}</td><td>{badge}</td><td>{restart}</td></tr>"
content = f'''
<div class="grid3" style="margin-bottom:20px">
<div class="stat"><h3>{blocked_count}</h3><p>Blocked Domains</p></div>
<div class="stat"><h3>{white_count}</h3><p>Whitelisted Sites</p></div>
<div class="stat"><h3>{blocked_users}</h3><p>Blocked Users</p></div>
</div>
<div class="grid2">
<div class="card">
<h2>🔧 Service Status</h2>
<table>
<tr><th>Service</th><th>Status</th><th>Action</th></tr>
{svc_rows}
</table>
</div>
<div class="card">
<h2>⚡ Quick Actions</h2>
<div style="display:flex;flex-direction:column;gap:10px">
<a href="/admin/blocklist" class="btn btn-primary">
➕ Add Blocked Domain</a>
<a href="/admin/whitelist" class="btn btn-success">
✅ Add Whitelist Exception</a>
<a href="/admin/blocked-users" class="btn btn-warning">
🔒 Manage Blocked Users</a>
<a href="/admin/requests" class="btn btn-info">
🔓 Review Access Requests</a>
<a href="/admin/service/squid/reload" class="btn btn-secondary">
🔄 Reload Squid Proxy</a>
</div>
</div>
</div>'''
return render("Dashboard", content, "dashboard")
# ── Block List ────────────────────────────────────────────────────────
@app.route("/admin/blocklist", methods=["GET","POST"])
@login_required
def blocklist():
if request.method == "POST":
action = request.form.get("action")
domain = request.form.get("domain","").strip().lower()
if domain:
lines = open(BLOCKED_FILE).read().splitlines() \
if os.path.exists(BLOCKED_FILE) else []
if action == "add" and domain not in lines:
lines.append(domain)
open(BLOCKED_FILE,"w").write("\n".join(lines)+"\n")
squid_reload()
audit("BLOCK_DOMAIN", domain)
flash(f"Blocked: {domain}", "success")
elif action == "remove":
lines = [l for l in lines if l.strip() != domain]
open(BLOCKED_FILE,"w").write("\n".join(lines)+"\n")
squid_reload()
audit("UNBLOCK_DOMAIN", domain)
flash(f"Removed: {domain}", "success")
domains = []
if os.path.exists(BLOCKED_FILE):
domains = [l.strip() for l in open(BLOCKED_FILE)
if l.strip() and not l.startswith("#")]
rows = "".join(f"""
<tr>
<td>{d}</td>
<td>
<form method="POST" style="display:inline">
<input type="hidden" name="action" value="remove">
<input type="hidden" name="domain" value="{d}">
<button class="btn btn-sm btn-danger" type="submit">Remove</button>
</form>
</td>
</tr>""" for d in sorted(domains))
content = f'''
<div class="grid2">
<div class="card">
<h2>➕ Add Blocked Domain</h2>
<form method="POST">
<input type="hidden" name="action" value="add">
<div class="form-group">
<label>Domain (e.g. gmail.com)</label>
<input name="domain" placeholder="domain.com" required>
</div>
<button class="btn btn-primary" type="submit">Block Domain</button>
</form>
</div>
<div class="card">
<h2>📋 Blocked Domains ({len(domains)})</h2>
<input type="text" placeholder="🔍 Filter..."
onkeyup="filterTable(this)"
style="margin-bottom:12px">
<div style="max-height:400px;overflow-y:auto">
<table id="dt">
<tr><th>Domain</th><th>Action</th></tr>
{rows}
</table>
</div>
</div>
</div>
<script>
function filterTable(inp) {{
document.querySelectorAll('#dt tr:not(:first-child)').forEach(r => {{
r.style.display = r.innerText.toLowerCase()
.includes(inp.value.toLowerCase()) ? '' : 'none';
}});
}}
</script>'''
return render("Block List", content, "blocklist")
# ── Whitelist ─────────────────────────────────────────────────────────
@app.route("/admin/whitelist", methods=["GET","POST"])
@login_required
def whitelist_page():
if request.method == "POST":
action = request.form.get("action")
domain = request.form.get("domain","").strip().lower()
note = request.form.get("note","")
if domain:
lines = open(WHITELIST).read().splitlines() \
if os.path.exists(WHITELIST) else []
if action == "add" and domain not in lines:
lines.append(domain)
open(WHITELIST,"w").write("\n".join(lines)+"\n")
# Also remove from blocked
if os.path.exists(BLOCKED_FILE):
bl = [l for l in open(BLOCKED_FILE).read().splitlines()
if l.strip() != domain]
open(BLOCKED_FILE,"w").write("\n".join(bl)+"\n")
squid_reload()
audit("WHITELIST_ADD", f"{domain} — {note}")
flash(f"Whitelisted: {domain}", "success")
elif action == "remove":
lines = [l for l in lines if l.strip() != domain]
open(WHITELIST,"w").write("\n".join(lines)+"\n")
squid_reload()
audit("WHITELIST_REMOVE", domain)
flash(f"Removed from whitelist: {domain}", "success")
sites = []
if os.path.exists(WHITELIST):
sites = [l.strip() for l in open(WHITELIST)
if l.strip() and not l.startswith("#")]
rows = "".join(f"""
<tr>
<td style="color:#2ecc71">{s}</td>
<td>
<form method="POST" style="display:inline">
<input type="hidden" name="action" value="remove">
<input type="hidden" name="domain" value="{s}">
<button class="btn btn-sm btn-danger">Remove</button>
</form>
</td>
</tr>""" for s in sorted(sites))
content = f'''
<div class="grid2">
<div class="card">
<h2>✅ Add Whitelist Exception</h2>
<form method="POST">
<input type="hidden" name="action" value="add">
<div class="form-group">
<label>Domain</label>
<input name="domain" placeholder="allowed-site.com" required>
</div>
<div class="form-group">
<label>Reason / Note</label>
<input name="note" placeholder="Business requirement...">
</div>
<button class="btn btn-success" type="submit">Allow Domain</button>
</form>
</div>
<div class="card">
<h2>✅ Whitelisted Sites ({len(sites)})</h2>
<div style="max-height:400px;overflow-y:auto">
<table>
<tr><th>Domain</th><th>Action</th></tr>
{rows}
</table>
</div>
</div>
</div>'''
return render("Whitelist", content, "whitelist")
# ── Blocked Users ─────────────────────────────────────────────────────
@app.route("/admin/blocked-users", methods=["GET","POST"])
@login_required
def blocked_users_page():
if request.method == "POST":
action = request.form.get("action")
username = request.form.get("username","").strip()
if username:
users = set()
if os.path.exists(BLOCKED_USERS_FILE):
users = set(open(BLOCKED_USERS_FILE).read().splitlines())
if action == "block":
users.add(username)
open(BLOCKED_USERS_FILE,"w").write("\n".join(sorted(users))+"\n")
squid_reload()
audit("BLOCK_USER", username)
flash(f"User blocked: {username}", "success")
elif action == "unblock":
users.discard(username)
open(BLOCKED_USERS_FILE,"w").write("\n".join(sorted(users))+"\n")
squid_reload()
audit("UNBLOCK_USER", username)
flash(f"User unblocked: {username}", "success")
users = []
if os.path.exists(BLOCKED_USERS_FILE):
users = [u.strip() for u in open(BLOCKED_USERS_FILE)
if u.strip()]
rows = "".join(f"""
<tr>
<td>👤 {u}</td>
<td><span class="badge badge-red">BLOCKED</span></td>
<td>
<form method="POST" style="display:inline">
<input type="hidden" name="action" value="unblock">
<input type="hidden" name="username" value="{u}">
<button class="btn btn-sm btn-success">Unblock</button>
</form>
</td>
</tr>""" for u in users)
content = f'''
<div class="grid2">
<div class="card">
<h2>🔒 Block User</h2>
<form method="POST">
<input type="hidden" name="action" value="block">
<div class="form-group">
<label>Username (AD/LDAP username)</label>
<input name="username" placeholder="john.doe" required>
</div>
<button class="btn btn-danger">Block User</button>
</form>
</div>
<div class="card">
<h2>🔒 Blocked Users ({len(users)})</h2>
<table>
<tr><th>User</th><th>Status</th><th>Action</th></tr>
{rows or "<tr><td colspan='3' style='color:#555'>No blocked users</td></tr>"}
</table>
</div>
</div>'''
return render("Blocked Users", content, "blocked-users")
# ── Services ──────────────────────────────────────────────────────────
@app.route("/admin/service/<name>/<action>")
@login_required
def service_action(name, action):
allowed_services = ["squid","nginx","threat-response",
"usb-alerter","web-request-server",
"push-register","admin-ui","ldap-sync"]
if name not in allowed_services:
flash("Unknown service", "error")
return redirect(url_for("dashboard"))
cmd = {"restart": ["systemctl","restart",name],
"stop": ["systemctl","stop",name],
"start": ["systemctl","start",name],
"reload": ["squid","-k","reconfigure"]
if name == "squid"
else ["systemctl","reload",name]}
if action in cmd:
r = subprocess.run(cmd[action], capture_output=True, text=True)
if r.returncode == 0:
flash(f"{name} {action}ed successfully", "success")
else:
flash(f"Error: {r.stderr.strip()[:100]}", "error")
audit(f"SERVICE_{action.upper()}", name)
return redirect(url_for("dashboard"))
# ── Audit Log ─────────────────────────────────────────────────────────
@app.route("/admin/auditlog")
@login_required
def auditlog():
conn = sqlite3.connect(ADMIN_DB)
rows_data = conn.execute("""
SELECT admin,action,details,timestamp
FROM audit_log ORDER BY id DESC LIMIT 200
""").fetchall()
conn.close()
rows = "".join(f"""<tr>
<td>{ts[:16]}</td>
<td>👤 {admin}</td>
<td><code style="color:#3498db">{action}</code></td>
<td style="color:#888;font-size:0.82em">{details[:80]}</td>
</tr>""" for admin,action,details,ts in rows_data)
content = f'''
<div class="card">
<h2>📋 Admin Audit Log (Last 200)</h2>
<table>
<tr><th>Time</th><th>Admin</th><th>Action</th><th>Details</th></tr>
{rows or "<tr><td colspan='4' style='color:#555'>No entries</td></tr>"}
</table>
</div>'''
return render("Audit Log", content, "auditlog")
# ── Config ────────────────────────────────────────────────────────────
@app.route("/admin/config", methods=["GET","POST"])
@login_required
def config_page():
CONFIG_KEYS = [
("smtp_server", "SMTP Server", "smtp.company.com"),
("smtp_port", "SMTP Port", "587"),
("smtp_user", "SMTP Username", "it-security@company.com"),
("smtp_pass", "SMTP Password", ""),
("it_email", "IT Email", "it-security@company.com"),
("soc_email", "SOC Email", "soc@company.com"),
("slack_webhook", "Slack Webhook URL", ""),
("teams_webhook", "MS Teams Webhook URL", ""),
("ad_server", "AD Server", "dc01.company.com"),
("ldap_server", "LDAP Server", "ldap.company.com"),
("lan_network", "LAN Network CIDR", "192.168.0.0/16"),
("exec_emails", "Executive Email List", "ceo@company.com,cto@company.com"),
]
conn = sqlite3.connect(ADMIN_DB)
if request.method == "POST":
for key,_,_ in CONFIG_KEYS:
val = request.form.get(key,"")
conn.execute("""
INSERT OR REPLACE INTO platform_config (key,value,updated)
VALUES (?,?,?)
""", (key, val, datetime.now().isoformat()))
conn.commit()
audit("CONFIG_UPDATE", "Platform configuration updated")
flash("Configuration saved", "success")
config = {}
for row in conn.execute("SELECT key,value FROM platform_config").fetchall():
config[row[0]] = row[1]
conn.close()
fields = "".join(f"""
<div class="form-group">
<label>{label}</label>
<input name="{key}"
type="{'password' if 'pass' in key or 'key' in key else 'text'}"
value="{config.get(key, default)}"
placeholder="{default}">
</div>"""
for key,label,default in CONFIG_KEYS)
content = f'''
<div class="card">
<h2>⚙️ Platform Configuration</h2>
<form method="POST">
<div class="grid2">{fields}</div>
<button class="btn btn-primary" type="submit">💾 Save Configuration</button>
</form>
</div>'''
return render("Configuration", content, "config")
# ── Run ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
init_admin_db()
print("[*] Admin UI starting on :8443")
print("[*] Default login: admin / admin123")
print("[!] Change password immediately after first login!")
app.run(host="0.0.0.0", port=8443, debug=False)
Deploy & Run
bash
# 1. Run installer
chmod +x install.sh
sudo bash install.sh # Creates install.conf
# Edit config
nano install.conf # Fill in your domain, SMTP, AD details
# Re-run to apply
sudo bash install.sh
# 2. Copy all scripts to install dir
sudo cp *.py /opt/security-platform/scripts/
# 3. Start admin UI
sudo systemctl start admin-ui
sudo systemctl status admin-ui
# 4. Access
# Admin UI: https://your-server/admin/
# Dashboard: https://your-server/
# Default: admin / admin123
Admin UI Pages
Page
What you can do
Dashboard
Service status, quick actions, stats
Block List
Add/remove blocked domains live
Whitelist
Add exception sites, auto-removes from block list
Departments
Toggle policies per department
Access Requests
Approve/deny bypass requests
User Policies
View AD/LDAP users + their policy
Blocked Users
Block/unblock users from proxy
Risk Scores
View per-user risk scores
Configuration
SMTP, AD, Slack, Teams settings
Alert Rules
Adjust threat thresholds
Admin Users
Manage admin accounts + roles
Audit Log
Full trail of every admin action
Services
Start/stop/restart any service
Part 1: Mobile Companion App (React Native)
Project Structure
SecurityApp/
├── App.js
├── package.json
├── src/
│ ├── api/ client.js
│ ├── screens/ Dashboard, Alerts, Requests, Users, Login
│ ├── components/ AlertCard, StatCard, Badge
│ └── store/ useStore.js
package.json
json
{
"name": "SecurityCompanion",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"dependencies": {
"expo": "~50.0.0",
"expo-notifications": "~0.27.0",
"expo-device": "~5.9.0",
"expo-secure-store": "~12.8.0",
"expo-status-bar": "~1.11.1",
"@react-navigation/native": "^6.1.0",
"@react-navigation/bottom-tabs": "^6.5.0",
"@react-navigation/stack": "^6.3.0",
"react-native-screens": "~3.29.0",
"react-native-safe-area-context": "4.8.2",
"react-native-vector-icons": "^10.0.0",
"@expo/vector-icons": "^14.0.0",
"react-native-chart-kit": "^6.12.0",
"react-native-svg": "14.1.0",
"axios": "^1.6.0",
"zustand": "^4.5.0",
"date-fns": "^3.3.0"
}
}
src/api/client.js
javascript
// API client — connects to your security platform backend
import axios from 'axios';
import * as SecureStore from 'expo-secure-store';
const BASE_URL = 'https://security.company.com';
const api = axios.create({
baseURL: BASE_URL,
timeout: 10000,
});
// Attach auth token to every request
api.interceptors.request.use(async (config) => {
const token = await SecureStore.getItemAsync('auth_token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// ── Auth ──────────────────────────────────────────────────────────────
export const login = (username, password) =>
api.post('/api/auth/login', { username, password });
export const logout = () =>
api.post('/api/auth/logout');
// ── Dashboard stats ───────────────────────────────────────────────────
export const getDashboardStats = () =>
api.get('/api/stats/summary');
export const getWebStats = (hours = 24) =>
api.get(`/api/stats/web?hours=${hours}`);
export const getUSBStats = (days = 7) =>
api.get(`/api/stats/usb?days=${days}`);
// ── Alerts / Threats ──────────────────────────────────────────────────
export const getAlerts = (limit = 50) =>
api.get(`/api/alerts?limit=${limit}`);
export const resolveAlert = (id) =>
api.post(`/api/alerts/${id}/resolve`);
export const getThreats = () =>
api.get('/api/threats/active');
// ── Access Requests ───────────────────────────────────────────────────
export const getAccessRequests = (status = 'pending') =>
api.get(`/api/requests?status=${status}`);
export const approveRequest = (refId, notes = '') =>
api.post(`/api/requests/${refId}/approve`, { notes });
export const denyRequest = (refId, notes = '') =>
api.post(`/api/requests/${refId}/deny`, { notes });
// ── Users ─────────────────────────────────────────────────────────────
export const getRiskUsers = () =>
api.get('/api/users/risk');
export const blockUser = (username, reason) =>
api.post('/api/users/block', { username, reason });
export const unblockUser = (username) =>
api.post(`/api/users/${username}/unblock`);
// ── Block/Whitelist ───────────────────────────────────────────────────
export const addBlockedDomain = (domain) =>
api.post('/api/blocklist/add', { domain });
export const addWhitelistDomain = (domain, note) =>
api.post('/api/whitelist/add', { domain, note });
// ── Push token registration ───────────────────────────────────────────
export const registerPushToken = (token, platform) =>
api.post('/register-push', { token, platform, role: 'soc' });
src/store/useStore.js
javascript
import { create } from 'zustand';
const useStore = create((set, get) => ({
// Auth
user: null,
authToken: null,
setUser: (user) => set({ user }),
setToken: (authToken) => set({ authToken }),
// Data
stats: null,
alerts: [],
requests: [],
riskUsers: [],
threats: [],
setStats: (stats) => set({ stats }),
setAlerts: (alerts) => set({ alerts }),
setRequests: (requests) => set({ requests }),
setRiskUsers: (riskUsers) => set({ riskUsers }),
setThreats: (threats) => set({ threats }),
// Unread counts
unreadAlerts: 0,
pendingRequests: 0,
setUnreadAlerts: (n) => set({ unreadAlerts: n }),
setPendingRequests: (n) => set({ pendingRequests: n }),
}));
export default useStore;
src/screens/LoginScreen.js
javascript
import React, { useState } from 'react';
import {
View, Text, TextInput, TouchableOpacity,
StyleSheet, ActivityIndicator, Alert, KeyboardAvoidingView
} from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { login } from '../api/client';
import useStore from '../store/useStore';
export default function LoginScreen({ navigation }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const { setUser, setToken } = useStore();
const handleLogin = async () => {
if (!username || !password) {
Alert.alert('Error', 'Enter username and password');
return;
}
setLoading(true);
try {
const res = await login(username, password);
await SecureStore.setItemAsync('auth_token', res.data.token);
await SecureStore.setItemAsync('username', username);
setToken(res.data.token);
setUser({ username, role: res.data.role });
navigation.replace('Main');
} catch (e) {
Alert.alert('Login Failed',
e.response?.data?.message || 'Invalid credentials');
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView style={s.container} behavior="padding">
<View style={s.card}>
<Text style={s.icon}>🛡</Text>
<Text style={s.title}>Security Console</Text>
<Text style={s.sub}>IT Security Operations</Text>
<TextInput
style={s.input}
placeholder="Username"
placeholderTextColor="#555"
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={s.input}
placeholder="Password"
placeholderTextColor="#555"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
style={s.btn}
onPress={handleLogin}
disabled={loading}
>
{loading
? <ActivityIndicator color="#fff" />
: <Text style={s.btnText}>Login →</Text>
}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
const s = StyleSheet.create({
container: { flex:1, backgroundColor:'#0f1117',
justifyContent:'center', padding:24 },
card: { backgroundColor:'#1a1d27', borderRadius:16,
padding:32, alignItems:'center',
borderTopWidth:3, borderTopColor:'#e74c3c' },
icon: { fontSize:48, marginBottom:12 },
title: { fontSize:22, fontWeight:'bold',
color:'#e0e0e0', marginBottom:4 },
sub: { color:'#555', fontSize:13, marginBottom:28 },
input: { width:'100%', backgroundColor:'#2c2f3e',
borderRadius:8, padding:14, color:'#e0e0e0',
marginBottom:14, fontSize:15,
borderWidth:1, borderColor:'#3a3d4e' },
btn: { width:'100%', backgroundColor:'#e74c3c',
borderRadius:8, padding:15, alignItems:'center' },
btnText: { color:'white', fontWeight:'bold', fontSize:16 },
});
src/screens/DashboardScreen.js
javascript
import React, { useEffect, useState, useCallback } from 'react';
import {
View, Text, ScrollView, StyleSheet,
RefreshControl, TouchableOpacity, Dimensions
} from 'react-native';
import { LineChart } from 'react-native-chart-kit';
import { getDashboardStats, getWebStats } from '../api/client';
import useStore from '../store/useStore';
const W = Dimensions.get('window').width;
function StatCard({ value, label, color = '#e74c3c', sub }) {
return (
<View style={[s.statCard, { borderTopColor: color }]}>
<Text style={[s.statVal, { color }]}>{value}</Text>
<Text style={s.statLabel}>{label}</Text>
{sub ? <Text style={s.statSub}>{sub}</Text> : null}
</View>
);
}
export default function DashboardScreen() {
const [refreshing, setRefreshing] = useState(false);
const [chartData, setChartData] = useState(null);
const { stats, setStats } = useStore();
const fetchData = useCallback(async () => {
try {
const [statsRes, webRes] = await Promise.all([
getDashboardStats(),
getWebStats(24),
]);
setStats(statsRes.data);
// Build chart data from hourly web stats
const hours = webRes.data.hourly_labels?.slice(-8) || [];
const total = webRes.data.hourly_total?.slice(-8) || [];
const block = webRes.data.hourly_block?.slice(-8) || [];
if (hours.length) {
setChartData({
labels: hours.map(h => h.slice(0,5)),
datasets: [
{ data: total, color: () => '#2ecc71',
strokeWidth: 2 },
{ data: block, color: () => '#e74c3c',
strokeWidth: 2 },
],
legend: ['Allowed', 'Blocked'],
});
}
} catch (e) {
console.error('Dashboard fetch error:', e);
}
}, [setStats]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchData();
setRefreshing(false);
}, [fetchData]);
useEffect(() => { fetchData(); }, [fetchData]);
return (
<ScrollView
style={s.container}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh}
tintColor="#e74c3c" />
}
>
<View style={s.header}>
<Text style={s.headerTitle}>🛡 Security Overview</Text>
<Text style={s.headerSub}>Last 24 hours</Text>
</View>
{/* Stat grid */}
<View style={s.grid}>
<StatCard
value={stats?.web_blocked?.toLocaleString() || '—'}
label="Blocked Requests"
color="#e74c3c"
sub={`${stats?.block_rate || 0}% rate`}
/>
<StatCard
value={stats?.usb_events?.toLocaleString() || '—'}
label="USB Events"
color="#f39c12"
/>
<StatCard
value={stats?.threats_today || '—'}
label="Threats Today"
color="#9b59b6"
/>
<StatCard
value={stats?.pending_requests || '—'}
label="Pending Requests"
color="#3498db"
/>
</View>
{/* Traffic chart */}
{chartData && (
<View style={s.chartCard}>
<Text style={s.sectionTitle}>📈 Traffic (Last 8hrs)</Text>
<LineChart
data={chartData}
width={W - 48}
height={180}
chartConfig={{
backgroundColor: '#1a1d27',
backgroundGradientFrom: '#1a1d27',
backgroundGradientTo: '#1a1d27',
decimalPlaces: 0,
color: (opacity = 1) => `rgba(231,76,60,${opacity})`,
labelColor: () => '#666',
propsForDots: { r:'3' },
}}
bezier
style={{ borderRadius: 8 }}
withLegend
/>
</View>
)}
{/* Risk summary */}
{stats?.critical_users > 0 && (
<View style={[s.alertBanner, { borderColor:'#e74c3c' }]}>
<Text style={s.alertBannerText}>
🔴 {stats.critical_users} users at CRITICAL risk
</Text>
</View>
)}
</ScrollView>
);
}
const s = StyleSheet.create({
container: { flex:1, backgroundColor:'#0f1117' },
header: { padding:20, paddingBottom:12 },
headerTitle: { fontSize:20, fontWeight:'bold', color:'#e0e0e0' },
headerSub: { color:'#555', fontSize:13, marginTop:3 },
grid: { flexDirection:'row', flexWrap:'wrap',
paddingHorizontal:12, gap:8 },
statCard: { width:(W-40)/2, backgroundColor:'#1a1d27',
borderRadius:10, padding:16,
borderTopWidth:3, margin:4 },
statVal: { fontSize:28, fontWeight:'bold' },
statLabel: { color:'#888', fontSize:12, marginTop:4 },
statSub: { color:'#555', fontSize:11, marginTop:2 },
chartCard: { margin:16, backgroundColor:'#1a1d27',
borderRadius:12, padding:16 },
sectionTitle: { color:'#e74c3c', fontSize:13,
fontWeight:'bold', marginBottom:12,
textTransform:'uppercase', letterSpacing:1 },
alertBanner: { margin:16, backgroundColor:'#2a1a1a',
borderRadius:8, padding:14,
borderWidth:1 },
alertBannerText: { color:'#e74c3c', fontWeight:'bold' },
});
src/screens/AlertsScreen.js
javascript
import React, { useEffect, useState, useCallback } from 'react';
import {
View, Text, FlatList, StyleSheet,
TouchableOpacity, Alert, RefreshControl
} from 'react-native';
import { getAlerts, resolveAlert } from '../api/client';
import useStore from '../store/useStore';
import { formatDistanceToNow } from 'date-fns';
const SEVERITY_COLOR = {
CRITICAL: '#e74c3c', HIGH: '#f39c12',
MEDIUM: '#3498db', LOW: '#2ecc71',
};
function AlertCard({ item, onResolve }) {
const color = SEVERITY_COLOR[item.severity] || '#666';
return (
<View style={[s.card, { borderLeftColor: color }]}>
<View style={s.cardHeader}>
<View style={[s.badge, { backgroundColor: color }]}>
<Text style={s.badgeText}>{item.severity}</Text>
</View>
<Text style={s.time}>
{formatDistanceToNow(new Date(item.timestamp), { addSuffix: true })}
</Text>
</View>
<Text style={s.cardTitle}>{item.threat_type}</Text>
<Text style={s.cardUser}>👤 {item.username}</Text>
<Text style={s.cardDetails} numberOfLines={2}>
{item.details}
</Text>
{!item.resolved && (
<TouchableOpacity
style={s.resolveBtn}
onPress={() => onResolve(item.id)}
>
<Text style={s.resolveBtnText}>✓ Mark Resolved</Text>
</TouchableOpacity>
)}
</View>
);
}
export default function AlertsScreen() {
const [refreshing, setRefreshing] = useState(false);
const [filter, setFilter] = useState('all');
const { alerts, setAlerts } = useStore();
const fetchAlerts = useCallback(async () => {
try {
const res = await getAlerts(100);
setAlerts(res.data);
} catch (e) { console.error(e); }
}, [setAlerts]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchAlerts();
setRefreshing(false);
}, [fetchAlerts]);
const handleResolve = (id) => {
Alert.alert('Resolve Alert', 'Mark this alert as resolved?', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Resolve',
onPress: async () => {
await resolveAlert(id);
fetchAlerts();
}
}
]);
};
useEffect(() => { fetchAlerts(); }, [fetchAlerts]);
const FILTERS = ['all','CRITICAL','HIGH','MEDIUM'];
const filtered = filter === 'all'
? alerts
: alerts.filter(a => a.severity === filter);
return (
<View style={s.container}>
{/* Filter tabs */}
<View style={s.filters}>
{FILTERS.map(f => (
<TouchableOpacity
key={f}
style={[s.filterTab,
filter === f && s.filterTabActive]}
onPress={() => setFilter(f)}
>
<Text style={[s.filterText,
filter === f && s.filterTextActive]}>
{f}
</Text>
</TouchableOpacity>
))}
</View>
<FlatList
data={filtered}
keyExtractor={i => String(i.id)}
renderItem={({ item }) => (
<AlertCard item={item} onResolve={handleResolve} />
)}
contentContainerStyle={{ padding: 16 }}
refreshControl={
<RefreshControl refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#e74c3c" />
}
ListEmptyComponent={
<Text style={s.empty}>No alerts found</Text>
}
/>
</View>
);
}
const s = StyleSheet.create({
container: { flex:1, backgroundColor:'#0f1117' },
filters: { flexDirection:'row', backgroundColor:'#1a1d27',
padding:12, gap:8 },
filterTab: { paddingHorizontal:14, paddingVertical:6,
borderRadius:16, backgroundColor:'#2c2f3e' },
filterTabActive: { backgroundColor:'#e74c3c' },
filterText: { color:'#888', fontSize:12, fontWeight:'600' },
filterTextActive: { color:'white' },
card: { backgroundColor:'#1a1d27', borderRadius:10,
padding:16, marginBottom:12,
borderLeftWidth:4 },
cardHeader: { flexDirection:'row', justifyContent:'space-between',
marginBottom:8 },
badge: { paddingHorizontal:10, paddingVertical:3,
borderRadius:10 },
badgeText: { color:'white', fontSize:10, fontWeight:'bold' },
time: { color:'#555', fontSize:11 },
cardTitle: { color:'#e0e0e0', fontWeight:'bold',
fontSize:14, marginBottom:4 },
cardUser: { color:'#3498db', fontSize:12, marginBottom:6 },
cardDetails: { color:'#666', fontSize:12, lineHeight:18 },
resolveBtn: { marginTop:10, backgroundColor:'#2c2f3e',
borderRadius:6, padding:8, alignItems:'center' },
resolveBtnText: { color:'#2ecc71', fontSize:12, fontWeight:'bold' },
empty: { color:'#555', textAlign:'center', marginTop:60 },
});
src/screens/RequestsScreen.js
javascript
import React, { useEffect, useState, useCallback } from 'react';
import {
View, Text, FlatList, StyleSheet,
TouchableOpacity, Alert, TextInput, RefreshControl
} from 'react-native';
import { getAccessRequests, approveRequest, denyRequest } from '../api/client';
import useStore from '../store/useStore';
function RequestCard({ item, onApprove, onDeny }) {
return (
<View style={s.card}>
<View style={s.cardTop}>
<Text style={s.refId}>{item.ref_id}</Text>
<View style={[s.badge,
{ backgroundColor: item.status === 'pending'
? '#f39c12' : item.status === 'approved'
? '#2ecc71' : '#e74c3c' }]}>
<Text style={s.badgeText}>{item.status?.toUpperCase()}</Text>
</View>
</View>
<Text style={s.name}>👤 {item.name}</Text>
<Text style={s.dept}>🏢 {item.department}</Text>
<Text style={s.site}>🌐 {item.site}</Text>
<Text style={s.reason} numberOfLines={2}>{item.reason}</Text>
<Text style={s.time}>{item.created_at?.slice(0,16)}</Text>
{item.status === 'pending' && (
<View style={s.actions}>
<TouchableOpacity
style={[s.actionBtn, s.approveBtn]}
onPress={() => onApprove(item.ref_id)}
>
<Text style={s.actionBtnText}>✅ Approve</Text>
</TouchableOpacity>
<TouchableOpacity
style={[s.actionBtn, s.denyBtn]}
onPress={() => onDeny(item.ref_id)}
>
<Text style={s.actionBtnText}>❌ Deny</Text>
</TouchableOpacity>
</View>
)}
</View>
);
}
export default function RequestsScreen() {
const [refreshing, setRefreshing] = useState(false);
const [tab, setTab] = useState('pending');
const { requests, setRequests } = useStore();
const fetchRequests = useCallback(async () => {
try {
const res = await getAccessRequests(tab);
setRequests(res.data);
} catch (e) { console.error(e); }
}, [tab, setRequests]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchRequests();
setRefreshing(false);
}, [fetchRequests]);
useEffect(() => { fetchRequests(); }, [fetchRequests]);
const promptAction = (refId, action) => {
Alert.prompt(
`${action} Request`,
'Add a note (optional):',
async (notes) => {
try {
action === 'Approve'
? await approveRequest(refId, notes)
: await denyRequest(refId, notes);
Alert.alert('Done', `Request ${action.toLowerCase()}d`);
fetchRequests();
} catch (e) {
Alert.alert('Error', 'Action failed');
}
},
'plain-text'
);
};
const TABS = ['pending', 'approved', 'denied'];
return (
<View style={s.container}>
<View style={s.tabs}>
{TABS.map(t => (
<TouchableOpacity
key={t}
style={[s.tab, tab === t && s.tabActive]}
onPress={() => setTab(t)}
>
<Text style={[s.tabText, tab === t && s.tabTextActive]}>
{t.charAt(0).toUpperCase() + t.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
<FlatList
data={requests}
keyExtractor={i => i.ref_id}
renderItem={({ item }) => (
<RequestCard
item={item}
onApprove={(id) => promptAction(id, 'Approve')}
onDeny={(id) => promptAction(id, 'Deny')}
/>
)}
contentContainerStyle={{ padding:16 }}
refreshControl={
<RefreshControl refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#e74c3c" />
}
ListEmptyComponent={
<Text style={s.empty}>No {tab} requests</Text>
}
/>
</View>
);
}
const s = StyleSheet.create({
container: { flex:1, backgroundColor:'#0f1117' },
tabs: { flexDirection:'row', backgroundColor:'#1a1d27',
borderBottomWidth:1, borderBottomColor:'#2c2f3e' },
tab: { flex:1, paddingVertical:14, alignItems:'center',
borderBottomWidth:2, borderBottomColor:'transparent' },
tabActive: { borderBottomColor:'#e74c3c' },
tabText: { color:'#888', fontSize:13 },
tabTextActive: { color:'#e0e0e0', fontWeight:'bold' },
card: { backgroundColor:'#1a1d27', borderRadius:10,
padding:16, marginBottom:12 },
cardTop: { flexDirection:'row', justifyContent:'space-between',
marginBottom:10 },
refId: { color:'#3498db', fontSize:12,
fontFamily:'monospace' },
badge: { paddingHorizontal:10, paddingVertical:3,
borderRadius:10 },
badgeText: { color:'white', fontSize:10, fontWeight:'bold' },
name: { color:'#e0e0e0', fontWeight:'bold', fontSize:14,
marginBottom:4 },
dept: { color:'#888', fontSize:12, marginBottom:2 },
site: { color:'#e74c3c', fontSize:13,
fontWeight:'bold', marginBottom:6 },
reason: { color:'#666', fontSize:12, marginBottom:6 },
time: { color:'#444', fontSize:11 },
actions: { flexDirection:'row', gap:10, marginTop:12 },
actionBtn: { flex:1, padding:10, borderRadius:8,
alignItems:'center' },
approveBtn: { backgroundColor:'#1a3a2a' },
denyBtn: { backgroundColor:'#3a1a1a' },
actionBtnText: { fontWeight:'bold', fontSize:13 },
empty: { color:'#555', textAlign:'center', marginTop:60 },
});
App.js — Navigation + Push Setup
javascript
import React, { useEffect, useRef } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createStackNavigator } from '@react-navigation/stack';
import { Ionicons } from '@expo/vector-icons';
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import * as SecureStore from 'expo-secure-store';
import LoginScreen from './src/screens/LoginScreen';
import DashboardScreen from './src/screens/DashboardScreen';
import AlertsScreen from './src/screens/AlertsScreen';
import RequestsScreen from './src/screens/RequestsScreen';
import { registerPushToken } from './src/api/client';
import useStore from './src/store/useStore';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
const TAB_ICONS = {
Dashboard: ['speedometer', 'speedometer-outline'],
Alerts: ['warning', 'warning-outline'],
Requests: ['lock-open', 'lock-open-outline'],
};
function MainTabs() {
const { unreadAlerts, pendingRequests } = useStore();
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarStyle: { backgroundColor:'#1a1d27',
borderTopColor:'#2c2f3e' },
tabBarActiveTintColor: '#e74c3c',
tabBarInactiveTintColor: '#555',
tabBarIcon: ({ focused, color, size }) => {
const [filled, outline] = TAB_ICONS[route.name] || ['circle','circle-outline'];
return (
<Ionicons
name={focused ? filled : outline}
size={size} color={color}
/>
);
},
tabBarBadge: route.name === 'Alerts' && unreadAlerts > 0
? unreadAlerts : undefined,
tabBarBadge: route.name === 'Requests' && pendingRequests > 0
? pendingRequests : undefined,
})}
>
<Tab.Screen name="Dashboard" component={DashboardScreen} />
<Tab.Screen name="Alerts" component={AlertsScreen} />
<Tab.Screen name="Requests" component={RequestsScreen} />
</Tab.Navigator>
);
}
async function registerForPushNotifications() {
if (!Device.isDevice) return null;
const { status: existing } = await Notifications.getPermissionsAsync();
let finalStatus = existing;
if (existing !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') return null;
const token = (await Notifications.getExpoPushTokenAsync()).data;
const platform = Device.osName === 'Android' ? 'android' : 'ios';
try {
await registerPushToken(token, platform);
} catch (e) { console.error('Push reg error:', e); }
return token;
}
export default function App() {
const notifListener = useRef();
const responseListener = useRef();
useEffect(() => {
registerForPushNotifications();
notifListener.current =
Notifications.addNotificationReceivedListener(notification => {
console.log('Push received:', notification);
});
responseListener.current =
Notifications.addNotificationResponseReceivedListener(response => {
const screen = response.notification.request.content.data?.screen;
// Navigate to relevant screen on tap
console.log('Push tapped → screen:', screen);
});
return () => {
Notifications.removeNotificationSubscription(notifListener.current);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Main" component={MainTabs} />
</Stack.Navigator>
</NavigationContainer>
);
}
Part 2: Ansible Playbook — Multi-Server Deployment
Directory Structure
ansible/
├── site.yml ← Master playbook
├── inventory/
│ ├── hosts.ini ← Server inventory
│ └── group_vars/
│ ├── all.yml ← Global vars
│ ├── proxy_servers.yml ← Proxy-specific vars
│ └── agents.yml ← Agent vars
├── roles/
│ ├── common/ ← Base setup all servers
│ ├── squid/ ← Squid proxy
│ ├── nginx/ ← nginx + SSL
│ ├── platform/ ← Python services
│ ├── usb_agent/ ← USB monitoring agent
│ └── windows_agent/ ← Windows GPO + scripts
└── files/
├── scripts/ ← All Python scripts
├── configs/ ← Config templates
└── certs/ ← SSL certificates
inventory/hosts.ini
ini
# ── Proxy / Central Servers ───────────────────────────────────────────
[proxy_servers]
proxy01 ansible_host=192.168.1.10 ansible_user=root
proxy02 ansible_host=192.168.1.11 ansible_user=root # HA pair
# ── Linux Agents (monitored machines) ────────────────────────────────
[linux_agents]
webserver01 ansible_host=192.168.1.20 ansible_user=root
dbserver01 ansible_host=192.168.1.21 ansible_user=root
devbox01 ansible_host=192.168.1.22 ansible_user=root
devbox02 ansible_host=192.168.1.23 ansible_user=root
# ── Windows Agents ────────────────────────────────────────────────────
[windows_agents]
win-ws-01 ansible_host=192.168.1.30
win-ws-02 ansible_host=192.168.1.31
fin-pc-01 ansible_host=192.168.1.32
[windows_agents:vars]
ansible_connection=winrm
ansible_winrm_transport=kerberos
ansible_winrm_server_cert_validation=ignore
ansible_user=Administrator
ansible_password="{{ vault_windows_password }}"
# ── Groups ────────────────────────────────────────────────────────────
[all_linux:children]
proxy_servers
linux_agents
[monitored:children]
linux_agents
windows_agents
inventory/group_vars/all.yml
yaml
---
# ── Company Settings ──────────────────────────────────────────────────
company_name: "Company Name"
domain: "company.com"
ad_server: "dc01.company.com"
ldap_server: "ldap.company.com"
lan_network: "192.168.0.0/16"
# ── Central Server ────────────────────────────────────────────────────
central_server: "192.168.1.10"
dashboard_url: "https://security.company.com"
# ── Install Paths ─────────────────────────────────────────────────────
install_dir: "/opt/security-platform"
data_dir: "/var/lib/security-platform"
log_dir: "/var/log/security-platform"
web_dir: "/var/www/html/security"
# ── Email ─────────────────────────────────────────────────────────────
smtp_server: "smtp.company.com"
smtp_port: 587
smtp_user: "it-security@company.com"
it_email: "it-security@company.com"
soc_email: "soc@company.com"
# ── Ports ─────────────────────────────────────────────────────────────
proxy_port: 3128
admin_ui_port: 8443
request_port: 8080
push_port: 8081
# ── Python packages ───────────────────────────────────────────────────
python_packages:
- ldap3
- requests
- PyJWT
- cryptography
- flask
- flask-login
- flask-wtf
- apscheduler
- paramiko
# ── Secrets (use ansible-vault) ───────────────────────────────────────
smtp_pass: "{{ vault_smtp_pass }}"
slack_webhook: "{{ vault_slack_webhook }}"
fcm_key: "{{ vault_fcm_key }}"
roles/common/tasks/main.yml
yaml
---
- name: Detect OS family
set_fact:
is_debian: "{{ ansible_os_family == 'Debian' }}"
is_rhel: "{{ ansible_os_family == 'RedHat' }}"
# ── Debian/Ubuntu ──────────────────────────────────────────────────────
- name: Update apt cache (Debian)
apt:
update_cache: yes
cache_valid_time: 3600
when: is_debian
- name: Install base packages (Debian)
apt:
name:
- python3
- python3-pip
- curl
- wget
- git
- openssl
- rsync
- sqlite3
- auditd
- audispd-plugins
- inotify-tools
- ufw
state: present
when: is_debian
# ── RHEL/CentOS/Fedora ────────────────────────────────────────────────
- name: Install EPEL (RHEL)
dnf:
name: epel-release
state: present
when: is_rhel
ignore_errors: yes
- name: Install base packages (RHEL)
dnf:
name:
- python3
- python3-pip
- curl
- wget
- git
- openssl
- rsync
- sqlite
- audit
- firewalld
state: present
when: is_rhel
# ── Common ────────────────────────────────────────────────────────────
- name: Install Python packages
pip:
name: "{{ python_packages }}"
extra_args: "--break-system-packages"
executable: pip3
ignore_errors: yes
- name: Create platform directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- "{{ install_dir }}/scripts"
- "{{ install_dir }}/certs"
- "{{ data_dir }}/db"
- "{{ data_dir }}/reports"
- "{{ log_dir }}"
- name: Configure auditd USB rules
copy:
dest: /etc/audit/rules.d/usb.rules
content: |
-a always,exit -F arch=b64 -S mount -F auid>=1000 -k usb_mount
-w /media -p rwxa -k usb_media_access
-w /mnt -p rwxa -k usb_mnt_access
notify: restart auditd
- name: Enable and start auditd
service:
name: auditd
state: started
enabled: yes
roles/common/handlers/main.yml
yaml
---
- name: restart auditd
service:
name: auditd
state: restarted
- name: reload squid
command: squid -k reconfigure
- name: restart nginx
service:
name: nginx
state: restarted
roles/squid/tasks/main.yml
yaml
---
- name: Install Squid
package:
name: "{{ 'squid' if is_debian else 'squid' }}"
state: present
- name: Create Squid department directory
file:
path: /etc/squid/departments
state: directory
- name: Deploy Squid config
template:
src: squid.conf.j2
dest: /etc/squid/squid.conf
backup: yes
notify: reload squid
- name: Deploy blocked domains list
template:
src: blocked_domains.j2
dest: /etc/squid/blocked_domains.txt
notify: reload squid
- name: Create empty ACL files
file:
path: "{{ item }}"
state: touch
loop:
- /etc/squid/whitelist.txt
- /etc/squid/blocked_users.txt
- /etc/squid/departments/dept_acls.conf
- /etc/squid/departments/user_acls.conf
- name: Validate Squid config
command: squid -k parse
register: squid_parse
failed_when: squid_parse.rc != 0
- name: Enable and start Squid
service:
name: squid
state: started
enabled: yes
roles/squid/templates/squid.conf.j2
jinja2
# Auto-generated by Ansible — {{ ansible_date_time.iso8601 }}
http_port {{ proxy_port }}
acl localnet src {{ lan_network }}
acl SSL_ports port 443
acl Safe_ports port 80 443 8080 21 70 210 1025-65535
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
acl blocked_users proxy_auth "/etc/squid/blocked_users.txt"
http_access deny blocked_users
include /etc/squid/departments/dept_acls.conf
include /etc/squid/departments/user_acls.conf
acl whitelist dstdomain "/etc/squid/whitelist.txt"
acl blocked_sites dstdomain "/etc/squid/blocked_domains.txt"
http_access allow whitelist
http_access deny blocked_sites
http_access allow localnet
http_access deny all
deny_info https://security.{{ domain }}/blocked/?url=%u all
access_log /var/log/squid/access.log combined
cache_log /var/log/squid/cache.log
cache_mem 256 MB
roles/platform/tasks/main.yml
yaml
---
- name: Copy platform scripts
copy:
src: "files/scripts/{{ item }}"
dest: "{{ install_dir }}/scripts/{{ item }}"
mode: '0755'
loop:
- usb_alerter.py
- ad_ldap_sync.py
- dept_policies.py
- threat_response.py
- executive_report.py
- push_notifier.py
- request_server.py
- risk_engine.py
- report_dashboard.py
- generate_dashboard.py
- admin_ui.py
- name: Deploy platform config
template:
src: platform_config.py.j2
dest: "{{ install_dir }}/scripts/config.py"
- name: Install systemd services
template:
src: "service.j2"
dest: "/etc/systemd/system/{{ item.name }}.service"
loop:
- { name: usb-alerter,
cmd: "python3 {{ install_dir }}/scripts/usb_alerter.py" }
- { name: threat-response,
cmd: "python3 {{ install_dir }}/scripts/threat_response.py --daemon" }
- { name: web-request-server,
cmd: "python3 {{ install_dir }}/scripts/request_server.py" }
- { name: push-register,
cmd: "python3 {{ install_dir }}/scripts/push_notifier.py --register-server" }
- { name: admin-ui,
cmd: "python3 {{ install_dir }}/scripts/admin_ui.py" }
notify: reload systemd
- name: Enable and start platform services
service:
name: "{{ item }}"
state: started
enabled: yes
loop:
- usb-alerter
- threat-response
- web-request-server
- push-register
- admin-ui
- name: Install crontab
cron:
name: "{{ item.name }}"
minute: "{{ item.minute }}"
hour: "{{ item.hour | default('*') }}"
weekday: "{{ item.weekday | default('*') }}"
job: "python3 {{ install_dir }}/scripts/{{ item.script }} >> {{ log_dir }}/{{ item.log }} 2>&1"
loop:
- { name: ldap-sync, minute: "*/15", script: ad_ldap_sync.py, log: ldap.log }
- { name: dashboard-gen, minute: "*/30", script: generate_dashboard.py, log: dashboard.log }
- { name: web-report, minute: "*/10", script: report_dashboard.py, log: webreport.log }
- { name: risk-engine, minute: "0", script: risk_engine.py, log: risk.log }
- { name: exec-report, minute: "0", hour: "7", weekday: "1",
script: executive_report.py, log: exec-report.log }
roles/usb_agent/tasks/main.yml
yaml
---
- name: Deploy USB audit script
template:
src: usb_audit_report.sh.j2
dest: "{{ install_dir }}/scripts/usb_audit_report.sh"
mode: '0755'
- name: Deploy USB alerter
copy:
src: files/scripts/usb_alerter.py
dest: "{{ install_dir }}/scripts/usb_alerter.py"
mode: '0755'
- name: Install USB alerter service
template:
src: service.j2
dest: /etc/systemd/system/usb-alerter.service
vars:
service_name: usb-alerter
service_cmd: "python3 {{ install_dir }}/scripts/usb_alerter.py"
notify: reload systemd
- name: Enable USB alerter
service:
name: usb-alerter
state: started
enabled: yes
- name: Setup USB report sync to central server
cron:
name: usb-report-sync
minute: "*/30"
job: >
rsync -az --ignore-missing-args
/var/log/usb-audit-reports/*.csv
root@{{ central_server }}:{{ data_dir }}/usb-reports/{{ inventory_hostname }}/
>> {{ log_dir }}/sync.log 2>&1
- name: Configure SSH key for central server sync
authorized_key:
user: root
state: present
key: "{{ lookup('file', 'files/certs/sync_key.pub') }}"
roles/windows_agent/tasks/main.yml
yaml
---
- name: Create scripts directory
win_file:
path: 'C:\SecurityAgent\scripts'
state: directory
- name: Deploy USB audit script
win_copy:
src: files/scripts/USB-Audit-Report.ps1
dest: 'C:\SecurityAgent\scripts\USB-Audit-Report.ps1'
- name: Deploy web blocking script
win_template:
src: USB-WebBlock.ps1.j2
dest: 'C:\SecurityAgent\scripts\USB-WebBlock.ps1'
- name: Set proxy system-wide
win_regedit:
path: 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
name: "{{ item.name }}"
data: "{{ item.data }}"
type: "{{ item.type }}"
loop:
- { name: ProxyEnable, data: 1, type: dword }
- { name: ProxyServer, data: "{{ central_server }}:{{ proxy_port }}", type: string }
- { name: ProxyOverride, data: "*.company.com;<local>", type: string }
- name: Disable USB storage
win_regedit:
path: 'HKLM:\SYSTEM\CurrentControlSet\Services\USBSTOR'
name: Start
data: 4
type: dword
- name: Schedule USB audit report
win_scheduled_task:
name: USB Audit Report
description: Weekly USB activity audit
actions:
- path: powershell.exe
arguments: '-ExecutionPolicy Bypass -File "C:\SecurityAgent\scripts\USB-Audit-Report.ps1"'
triggers:
- type: weekly
days_of_week: monday
start_boundary: '2024-01-01T08:00:00'
run_level: highest
state: present
enabled: yes
site.yml — Master Playbook
yaml
---
# ================================================================
# Enterprise Security Platform — Master Ansible Playbook
# Usage:
# ansible-playbook site.yml -i inventory/hosts.ini
# ansible-playbook site.yml -i inventory/hosts.ini --tags squid
# ansible-playbook site.yml -i inventory/hosts.ini --limit proxy01
# ================================================================
- name: Common setup — all Linux servers
hosts: all_linux
roles:
- common
tags: [common]
- name: Deploy Squid proxy + platform (proxy servers)
hosts: proxy_servers
roles:
- squid
- nginx
- platform
tags: [proxy, platform]
- name: Deploy USB monitoring agents (Linux)
hosts: linux_agents
roles:
- common
- usb_agent
tags: [agent, usb]
- name: Deploy Windows agents
hosts: windows_agents
roles:
- windows_agent
tags: [windows]
- name: Verify all services running
hosts: proxy_servers
tasks:
- name: Check services
service_facts:
- name: Assert critical services
assert:
that:
- ansible_facts.services['squid.service'].state == 'running'
- ansible_facts.services['nginx.service'].state == 'running'
- ansible_facts.services['threat-response.service'].state == 'running'
fail_msg: "Critical service not running!"
success_msg: "All services running ✔"
tags: [verify]
Run the Playbook
bash
# Install Ansible
pip3 install ansible pywinrm --break-system-packages
# Encrypt secrets
ansible-vault encrypt_string 'smtp_password' --name vault_smtp_pass
ansible-vault encrypt_string 'slack_webhook_url' --name vault_slack_webhook
# Test connectivity
ansible all -i inventory/hosts.ini -m ping
# Dry run
ansible-playbook site.yml -i inventory/hosts.ini --check --diff
# Full deploy
ansible-playbook site.yml -i inventory/hosts.ini --ask-vault-pass
# Deploy only proxy servers
ansible-playbook site.yml -i inventory/hosts.ini \
--limit proxy_servers --tags proxy
# Deploy only Linux agents
ansible-playbook site.yml -i inventory/hosts.ini \
--limit linux_agents --tags agent
# Deploy only Windows agents
ansible-playbook site.yml -i inventory/hosts.ini \
--limit windows_agents --tags windows
# Update block list across all proxies
ansible proxy_servers -i inventory/hosts.ini \
-m copy -a "src=files/configs/blocked_domains.txt \
dest=/etc/squid/blocked_domains.txt" \
--become && \
ansible proxy_servers -i inventory/hosts.ini \
-m command -a "squid -k reconfigure"
Complete Platform Summary
┌─────────────────────────────────────────────────────────────────────┐
│ Enterprise Security Platform — Full Stack │
├──────────────────────┬──────────────────────────────────────────────┤
│ Deployment │ Components │
│ │ │
│ Ansible ───────────┼──► Proxy Servers (Squid + nginx) │
│ site.yml │ Linux Agents (USB + web) │
│ inventory/ │ Windows Agents (GPO + scripts) │
│ │ │
│ Mobile App ────────┼──► React Native (iOS + Android) │
│ (Expo) │ Push Notifications │
│ │ Approve/Deny Requests │
│ │ Live Alerts │
│ │ │
│ Admin UI ──────────┼──► Flask Web Console │
│ (Browser) │ Block/Whitelist Management │
│ │ User/Policy Control │
│ │ Service Management │
│ │ │
│ Reporting ─────────┼──► Live Dashboard │
│ │ Weekly Executive Email │
│ │ Risk Scores │
│ │ Audit Logs │
└──────────────────────┴──────────────────────────────────────────────┘
This completes the full enterprise security platform.