From d989a01e5b458899e97ee639cab696f3119c4ae6 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Mon, 22 Jun 2026 12:54:20 +0000 Subject: [PATCH 1/2] MVP polish: add TLS rule RS-TLS-001, deploy script, update status badge (closes #153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detection-rules/retail/tls_downgrade_pos.kql: RS-TLS-001 detects POS endpoints negotiating TLS 1.0/1.1 or weak ciphers (RC4, DES, NULL, EXPORT) — MITRE T1557, PCI-DSS v4.0 req 4.2.1. Passes validate_kql.py. Rule count: 23 → 24 (all 24/24 pass). - scripts/deploy_all.py: one-command deployment script using Azure SDK (azure-mgmt-securityinsight) with --dry-run support. - README.md: status badge updated from In Development to Active MVP. --- README.md | 2 +- detection-rules/retail/tls_downgrade_pos.kql | 46 +++++++ scripts/deploy_all.py | 137 +++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 detection-rules/retail/tls_downgrade_pos.kql create mode 100644 scripts/deploy_all.py diff --git a/README.md b/README.md index b6843dd..6c55d99 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/detection-rules/retail/tls_downgrade_pos.kql b/detection-rules/retail/tls_downgrade_pos.kql new file mode 100644 index 0000000..a3a4c49 --- /dev/null +++ b/detection-rules/retail/tls_downgrade_pos.kql @@ -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 diff --git a/scripts/deploy_all.py b/scripts/deploy_all.py new file mode 100644 index 0000000..31c74ee --- /dev/null +++ b/scripts/deploy_all.py @@ -0,0 +1,137 @@ +#!/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 --resource-group + +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 + from azure.mgmt.securityinsight.models import ScheduledAlertRule +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(f"\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() From ef5056a62c16b747ddf14c37c6fb09d2d3df957c Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Tue, 23 Jun 2026 17:44:09 +0000 Subject: [PATCH 2/2] fix(lint): remove unused import and bare f-string in deploy_all.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F401 — ScheduledAlertRule imported but unused (removed). F541 — f-string with no placeholders on line 71 (converted to plain string). --- scripts/deploy_all.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/deploy_all.py b/scripts/deploy_all.py index 31c74ee..33392c7 100644 --- a/scripts/deploy_all.py +++ b/scripts/deploy_all.py @@ -21,7 +21,6 @@ try: from azure.identity import AzureCliCredential from azure.mgmt.securityinsight import SecurityInsights - from azure.mgmt.securityinsight.models import ScheduledAlertRule except ImportError: print("ERROR: Missing dependencies. Run: pip install azure-identity azure-mgmt-securityinsight") sys.exit(1) @@ -68,7 +67,7 @@ def deploy_rules(workspace: str, resource_group: str, dry_run: bool = False): print("ERROR: No rule templates found in sentinel/analytics-rules/") sys.exit(1) - print(f"\nRetailShield Deployment") + print("\nRetailShield Deployment") print(f" Workspace: {workspace}") print(f" Resource group: {resource_group}") print(f" Rules found: {len(templates)}")