Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![Azure Sentinel](https://img.shields.io/badge/SIEM-Microsoft%20Sentinel-0078D4?logo=microsoftazure)](https://azure.microsoft.com/en-gb/products/microsoft-sentinel)
[![MITRE ATT&CK](https://img.shields.io/badge/Framework-MITRE%20ATT%26CK-red)](https://attack.mitre.org/)
[![Built With KQL](https://img.shields.io/badge/Language-KQL-orange)](https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/)
[![Status](https://img.shields.io/badge/Status-In%20Development-yellow)]()
[![Status](https://img.shields.io/badge/Status-Active%20MVP-brightgreen)]()
[![Author](https://img.shields.io/badge/Author-Tanvir%20Farhad%20%7C%20ShieldTech%20Ltd-informational)]()

> **A retail threat detection and incident response content pack for Microsoft Sentinel.**
Expand Down
46 changes: 46 additions & 0 deletions detection-rules/retail/tls_downgrade_pos.kql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ============================================================
// RetailShield — POS TLS Downgrade / Weak Cipher Detection Rule
// Rule ID : RS-TLS-001
// MITRE ATT&CK : T1557 — Adversary-in-the-Middle
// Tactic : Credential Access / Collection
// Severity : High
// Frequency : Every 30 minutes | Lookback: 1 hour
// Source table : CommonSecurityLog (firewall / proxy CEF connector)
// Author : Tanvir Farhad — ShieldTech Ltd, London
// Notes : A payment-handling endpoint suddenly negotiating TLS 1.0/1.1
// or a weak cipher suite indicates a downgrade attack, a
// misconfigured terminal, or a compromised device — all of
// which breach PCI-DSS v4.0 requirement 4.2.1.
// ============================================================

let LookbackPeriod = 1h;
let POSSubnets = dynamic(["10.10.20.", "10.10.21.", "192.168.50."]);
let WeakTLSVersions = dynamic(["TLSv1", "TLSv1.0", "TLSv1.1", "SSLv3", "SSLv2"]);
let WeakCiphers = dynamic(["RC4", "DES", "3DES", "MD5", "NULL", "EXPORT", "anon"]);

CommonSecurityLog
| where TimeGenerated > ago(LookbackPeriod)
| where isnotempty(SourceIP)
| where SourceIP has_any (POSSubnets) or DestinationIP has_any (POSSubnets)
| extend TLSInfo = strcat(AdditionalExtensions, " ", RequestContext)
| extend
NegotiatedTLS = extract(@"(SSLv2|SSLv3|TLSv1\.?[012]?)(?![\.\d])", 1, TLSInfo),
CipherSuite = extract(@"cipher[=:]?\s?([A-Za-z0-9_\-]+)", 1, TLSInfo)
| where NegotiatedTLS in (WeakTLSVersions)
or CipherSuite has_any (WeakCiphers)
| summarize
EventCount = count(),
TLSVersions = make_set(NegotiatedTLS, 5),
Ciphers = make_set(CipherSuite, 5),
Destinations = make_set(DestinationIP, 10),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by SourceIP
| extend
Severity = iff(EventCount > 20, "Critical", "High"),
ComplianceImpact = "PCI-DSS v4.0 req 4.2.1 — strong cryptography for cardholder data in transit"
| project
FirstSeen, LastSeen, SourceIP, Destinations,
TLSVersions, Ciphers, EventCount,
Severity, ComplianceImpact
| order by EventCount desc
136 changes: 136 additions & 0 deletions scripts/deploy_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
RetailShield — One-command deployment script
Deploys all analytics rules to a Microsoft Sentinel workspace via Azure REST API.

Usage:
python scripts/deploy_all.py --workspace <workspace-name> --resource-group <rg-name>

Requirements:
pip install azure-identity azure-mgmt-securityinsight
az login (Azure CLI must be authenticated)
"""

import argparse
import json
import os
import sys
import glob
from pathlib import Path

try:
from azure.identity import AzureCliCredential
from azure.mgmt.securityinsight import SecurityInsights
except ImportError:
print("ERROR: Missing dependencies. Run: pip install azure-identity azure-mgmt-securityinsight")
sys.exit(1)


def get_subscription_id():
"""Get active Azure subscription ID from CLI."""
import subprocess
result = subprocess.run(
["az", "account", "show", "--query", "id", "-o", "tsv"],
capture_output=True, text=True
)
if result.returncode != 0:
print("ERROR: Not logged in to Azure CLI. Run: az login")
sys.exit(1)
return result.stdout.strip()


def load_arm_templates(rules_dir: Path):
"""Load all analytics rule ARM templates from sentinel/analytics-rules/."""
templates = []
pattern = str(rules_dir / "*.json")
for path in sorted(glob.glob(pattern)):
with open(path) as f:
try:
data = json.load(f)
templates.append((os.path.basename(path), data))
except json.JSONDecodeError as e:
print(f" WARN: Skipping {path} — invalid JSON: {e}")
return templates


def deploy_rules(workspace: str, resource_group: str, dry_run: bool = False):
"""Deploy all RetailShield analytics rules to the target Sentinel workspace."""
repo_root = Path(__file__).parent.parent
rules_dir = repo_root / "sentinel" / "analytics-rules"

if not rules_dir.exists():
print(f"ERROR: Rules directory not found: {rules_dir}")
sys.exit(1)

templates = load_arm_templates(rules_dir)
if not templates:
print("ERROR: No rule templates found in sentinel/analytics-rules/")
sys.exit(1)

print("\nRetailShield Deployment")
print(f" Workspace: {workspace}")
print(f" Resource group: {resource_group}")
print(f" Rules found: {len(templates)}")
print(f" Dry run: {dry_run}\n")

if dry_run:
for name, _ in templates:
print(f" [DRY RUN] Would deploy: {name}")
print(f"\nDry run complete. {len(templates)} rules would be deployed.")
return

subscription_id = get_subscription_id()
credential = AzureCliCredential()
client = SecurityInsights(credential, subscription_id)

deployed = 0
failed = 0

for name, template in templates:
rule_name = name.replace(".json", "")
try:
resources = template.get("resources", [])
if not resources:
print(f" SKIP {name}: no resources in template")
continue

rule_props = resources[0].get("properties", {})
print(f" Deploying {rule_name}...", end=" ")

client.alert_rules.create_or_update(
resource_group_name=resource_group,
workspace_name=workspace,
rule_id=rule_name,
alert_rule={
"kind": "Scheduled",
**rule_props
}
)
print("OK")
deployed += 1

except Exception as e:
print(f"FAILED — {e}")
failed += 1

print(f"\nDeployment complete: {deployed} deployed, {failed} failed.")
if failed:
print("Check Azure Portal > Sentinel > Analytics for any partially deployed rules.")


def main():
parser = argparse.ArgumentParser(description="Deploy RetailShield to Microsoft Sentinel")
parser.add_argument("--workspace", required=True, help="Sentinel workspace name")
parser.add_argument("--resource-group", required=True, help="Azure resource group name")
parser.add_argument("--dry-run", action="store_true", help="Preview without deploying")
args = parser.parse_args()

deploy_rules(
workspace=args.workspace,
resource_group=args.resource_group,
dry_run=args.dry_run
)


if __name__ == "__main__":
main()
Loading