{briefing}
+ diff --git a/detection-rules/ai_voice_fraud.kql b/detection-rules/ai_voice_fraud.kql new file mode 100644 index 0000000..a22c13d --- /dev/null +++ b/detection-rules/ai_voice_fraud.kql @@ -0,0 +1,151 @@ +// ============================================================ +// RetailShield — AI Voice Fraud Detection +// Rule ID : RS-VOI-001 +// MITRE ATT&CK : T1598 — Phishing for Information (Voice) +// Tactic : Reconnaissance +// Severity : High +// Frequency : Every 30 minutes | Lookback: 30 minutes +// Author : Tanvir Farhad — ShieldTech Ltd, London +// ============================================================ +// Detects AI-generated deepfake voice calls targeting retail +// finance and management staff. Covers high AI confidence score +// calls, urgent financial requests from unverified callers, +// caller ID spoofing patterns, and after-hours vishing attempts. +// Retail-specific: targets CFO/finance-role impersonation and +// urgent payment override requests unique to retail operations. +// ============================================================ + +let LookbackWindow = 30m; +let BusinessHoursStart = 7; // 07:00 UTC +let BusinessHoursEnd = 20; // 20:00 UTC +let AIConfidenceThreshold = 0.85; // AI voice deepfake confidence above this is high-risk +let HighRiskSuspicionScore = 85; + +// Roles that are high-value targets for voice fraud +let SensitiveTargetRoles = dynamic([ + "finance", "director", "manager", "cfo", "ceo", + "payment", "accounts", "payroll", "head" +]); + +// Impersonation personas used in retail voice fraud +let KnownImpersonationEntities = dynamic([ + "Area Manager", "Head Office", "CFO", "CEO", + "Finance Director", "Regional Director", "DHL", + "HMRC", "Bank", "IT Support" +]); + +// Keywords that signal urgency / bypass request — fraud indicators +let FraudKeywords = dynamic([ + "urgent", "immediate", "emergency", "override", + "bypass", "redirect", "transfer", "wire", "bacs", + "authorise", "approve now", "time sensitive", "confidential" +]); + +// ── Signal 1 — High AI confidence score on detected voice call ─────────────── +let HighConfidenceVoiceFraud = + RetailShield_Logs_CL + | where ingestion_time() > ago(LookbackWindow) + | where EventType_s == "AI_Voice_Fraud" + | where todouble(AIConfidenceScore_d) >= AIConfidenceThreshold + | project + TimeGenerated, + SignalType = "HighConfidenceVoiceFraud", + TargetEmployee = tostring(TargetEmployee_s), + AIConfidenceScore = AIConfidenceScore_d, + ImpersonatingEntity = tostring(ImpersonatingEntity_s), + RequestMade = tostring(RequestMade_s), + SuspicionScore = todouble(SuspicionScore_d); + +// ── Signal 2 — Urgent financial / bypass request on voice channel ──────────── +let UrgentFinancialRequest = + RetailShield_Logs_CL + | where ingestion_time() > ago(LookbackWindow) + | where EventType_s == "AI_Voice_Fraud" + | where tolower(tostring(RequestMade_s)) has_any (FraudKeywords) + | where tolower(tostring(TargetEmployee_s)) has_any (SensitiveTargetRoles) + or tostring(ImpersonatingEntity_s) in (KnownImpersonationEntities) + | project + TimeGenerated, + SignalType = "UrgentFinancialRequest", + TargetEmployee = tostring(TargetEmployee_s), + AIConfidenceScore = AIConfidenceScore_d, + ImpersonatingEntity = tostring(ImpersonatingEntity_s), + RequestMade = tostring(RequestMade_s), + SuspicionScore = todouble(SuspicionScore_d); + +// ── Signal 3 — Caller ID spoofing pattern detected ─────────────────────────── +let SpoofedCallerID = + RetailShield_Logs_CL + | where ingestion_time() > ago(LookbackWindow) + | where EventType_s == "AI_Voice_Fraud" + | where SuspicionScore_d >= HighRiskSuspicionScore + | where isnotempty(CallerID_s) + | where tostring(CallerID_s) matches regex @"^(\+44|0044|0)[0-9]{9,10}$" + | project + TimeGenerated, + SignalType = "SpoofedCallerID", + TargetEmployee = tostring(TargetEmployee_s), + AIConfidenceScore = AIConfidenceScore_d, + ImpersonatingEntity = tostring(ImpersonatingEntity_s), + RequestMade = tostring(RequestMade_s), + SuspicionScore = todouble(SuspicionScore_d); + +// ── Signal 4 — After-hours vishing attempt ─────────────────────────────────── +let AfterHoursVoiceFraud = + RetailShield_Logs_CL + | where ingestion_time() > ago(LookbackWindow) + | where EventType_s == "AI_Voice_Fraud" + | extend CallHour = hourofday(TimeGenerated) + | where CallHour < BusinessHoursStart or CallHour >= BusinessHoursEnd + | project + TimeGenerated, + SignalType = "AfterHoursVoiceFraud", + TargetEmployee = tostring(TargetEmployee_s), + AIConfidenceScore = AIConfidenceScore_d, + ImpersonatingEntity = tostring(ImpersonatingEntity_s), + RequestMade = tostring(RequestMade_s), + SuspicionScore = todouble(SuspicionScore_d); + +// ── Union all signals ───────────────────────────────────────────────────────── +union HighConfidenceVoiceFraud, UrgentFinancialRequest, SpoofedCallerID, AfterHoursVoiceFraud +| summarize + SignalCount = count(), + Signals = make_set(SignalType), + FirstSeen = min(TimeGenerated), + LastSeen = max(TimeGenerated), + MaxAIScore = max(todouble(AIConfidenceScore)), + MaxSuspicionScore = max(todouble(SuspicionScore)), + TargetEmployee = take_any(TargetEmployee), + ImpersonatingEntity = take_any(ImpersonatingEntity), + RequestMade = take_any(RequestMade) + by bin(TimeGenerated, LookbackWindow) +| where SignalCount >= 1 +| extend + RiskScore = case(SignalCount >= 3, 90, SignalCount == 2, 78, 65), + MitreTechnique = "T1598", + MitreTactic = "Reconnaissance", + AlertSeverity = case(SignalCount >= 3, "CRITICAL", "HIGH"), + Severity = "HIGH", + PlaybookTrigger = "notify_soc", + DeviceName = "VOIP-SYSTEM", + AlertTitle = strcat("AI Voice Fraud Detected — Target: ", TargetEmployee, + " | Impersonating: ", ImpersonatingEntity) +| project + AlertTitle, + DeviceName, + TargetEmployee, + ImpersonatingEntity, + RequestMade, + AlertSeverity, + Severity, + RiskScore, + SignalCount, + Signals, + MaxAIScore, + MaxSuspicionScore, + MitreTechnique, + MitreTactic, + PlaybookTrigger, + FirstSeen, + LastSeen +| sort by RiskScore desc diff --git a/detection-rules/pos_anomaly.kql b/detection-rules/pos_anomaly.kql new file mode 100644 index 0000000..ff52818 --- /dev/null +++ b/detection-rules/pos_anomaly.kql @@ -0,0 +1,154 @@ +// ============================================================ +// RetailShield — POS Anomaly Detection +// Rule ID : RS-POS-001 +// MITRE ATT&CK : T1056.001 — Keylogging / RAM Scraping +// Tactic : Collection +// Severity : High +// Frequency : Every 15 minutes | Lookback: 30 minutes +// Author : Tanvir Farhad — ShieldTech Ltd, London +// ============================================================ +// Detects RAM-scraping keyloggers and anomalous activity on +// retail POS terminals. Covers unknown DLL injection into POS +// processes, abnormal transaction volume spikes (statistical +// anomaly), process memory dumps, and suspicious outbound +// network connections from POS endpoints to threat IPs. +// ============================================================ + +let LookbackWindow = 30m; +let BaselineWindow = 30d; +let TransactionVolumeThreshold = 3.5; // std deviations above 30-day baseline + +// POS application process names +let POSProcessNames = dynamic([ + "xstore.exe", "aloha.exe", "tcxpos.exe", + "posready.exe", "retail.exe", "revel.exe", "squarepos.exe" +]); + +// Hostname prefixes that identify POS / till hardware +let RetailTerminalPrefix = dynamic([ + "pos-", "till-", "kiosk-", "ped-", "term-" +]); + +// Folders that are trusted for DLL loads — flag anything outside +let TrustedDLLPaths = dynamic([ + @"C:\Windows\System32", + @"C:\Windows\SysWOW64", + @"C:\Program Files", + @"C:\Program Files (x86)" +]); + +// ── Signal 1 — Unknown unsigned DLL injected into POS process ──────────────── +let UnknownDLLInjection = + DeviceEvents + | where ingestion_time() > ago(LookbackWindow) + | where ActionType == "ImageLoaded" + | where InitiatingProcessFileName has_any (POSProcessNames) + | where FileName endswith ".dll" or FileName endswith ".sys" + | where isempty(SHA1) or Signer == "" or isempty(Signer) + | where not(FolderPath has_any (TrustedDLLPaths)) + | extend DeviceLower = tolower(DeviceName) + | where DeviceLower has_any (RetailTerminalPrefix) + | project + TimeGenerated, DeviceName, DeviceId, + SignalType = "UnknownDLLInjection", + POSProcess = InitiatingProcessFileName, + SuspiciousDLL = FileName, + DLLPath = FolderPath, + SHA256 = InitiatingProcessSHA256; + +// ── Signal 2 — Abnormal transaction volume (statistical spike) ─────────────── +let AbnormalTransactionVolume = + RetailShield_Logs_CL + | where ingestion_time() > ago(BaselineWindow) + | where EventType_s == "POS_Transaction" + | summarize HourlyCount = count() by TerminalID_s, bin(TimeGenerated, 1h) + | summarize AvgCount = avg(HourlyCount), StdDev = stdev(HourlyCount) + by TerminalID_s + | join kind=inner ( + RetailShield_Logs_CL + | where ingestion_time() > ago(LookbackWindow) + | where EventType_s == "POS_Transaction" + | summarize RecentCount = count() by TerminalID_s + ) on TerminalID_s + | where RecentCount > AvgCount + (TransactionVolumeThreshold * StdDev) + | extend AnomalyScore = round((RecentCount - AvgCount) / StdDev, 2) + | project + TimeGenerated = now(), DeviceName = TerminalID_s, DeviceId = "", + SignalType = "AbnormalTransactionVolume", + POSProcess = "POS_Transaction", + SuspiciousDLL = "", + DLLPath = "", + SHA256 = ""; + +// ── Signal 3 — Process memory dump targeting a POS application ─────────────── +let ProcessMemoryDump = + DeviceEvents + | where ingestion_time() > ago(LookbackWindow) + | where ActionType in ("ProcessDumped", "MemoryDumpCreated") + or (ActionType == "ProcessAccessed" + and ProcessCommandLine has_any ("MiniDump", "ProcDump", "createdump", "procdump")) + | where tolower(TargetProcessFileName) has_any (POSProcessNames) + | project + TimeGenerated, DeviceName, DeviceId, + SignalType = "ProcessMemoryDump", + POSProcess = TargetProcessFileName, + SuspiciousDLL = InitiatingProcessFileName, + DLLPath = ProcessCommandLine, + SHA256 = InitiatingProcessSHA256; + +// ── Signal 4 — Suspicious outbound network from POS to threat IP ───────────── +let SuspiciousNetworkFromPOS = + DeviceNetworkEvents + | where ingestion_time() > ago(LookbackWindow) + | where tolower(DeviceName) has_any (RetailTerminalPrefix) + | where RemoteIPType == "Public" + | where not(RemotePort in (443, 80, 8443, 8080)) + | join kind=inner ( + _GetWatchlist("RetailIOCWatchlist") + | project ThreatIP = tostring(column_ifexists("SearchKey", "")) + ) on $left.RemoteIP == $right.ThreatIP + | project + TimeGenerated, DeviceName, DeviceId, + SignalType = "SuspiciousNetworkFromPOS", + POSProcess = InitiatingProcessFileName, + SuspiciousDLL = RemoteIP, + DLLPath = tostring(RemotePort), + SHA256 = ""; + +// ── Union all signals ───────────────────────────────────────────────────────── +union UnknownDLLInjection, AbnormalTransactionVolume, ProcessMemoryDump, SuspiciousNetworkFromPOS +| summarize + SignalCount = count(), + Signals = make_set(SignalType), + FirstSeen = min(TimeGenerated), + LastSeen = max(TimeGenerated), + SuspiciousDLL = take_any(SuspiciousDLL), + POSProcess = take_any(POSProcess), + DeviceId = take_any(DeviceId) + by DeviceName +| where SignalCount >= 1 +| extend + RiskScore = case(SignalCount >= 3, 95, SignalCount == 2, 82, 65), + MitreTechnique = "T1056.001", + MitreTactic = "Collection", + Severity = case(SignalCount >= 2, "HIGH", "HIGH"), + AlertSeverity = case(SignalCount >= 3, "CRITICAL", "HIGH"), + PlaybookTrigger = "suspend_terminal", + AlertTitle = strcat("POS Anomaly — Potential RAM Scraping on ", DeviceName) +| project + AlertTitle, + DeviceName, + DeviceId, + AlertSeverity, + Severity, + RiskScore, + SignalCount, + Signals, + SuspiciousDLL, + POSProcess, + MitreTechnique, + MitreTactic, + PlaybookTrigger, + FirstSeen, + LastSeen +| sort by RiskScore desc diff --git a/detection-rules/supply_chain_anomaly.kql b/detection-rules/supply_chain_anomaly.kql new file mode 100644 index 0000000..2bf4f47 --- /dev/null +++ b/detection-rules/supply_chain_anomaly.kql @@ -0,0 +1,158 @@ +// ============================================================ +// RetailShield — Supply Chain Anomaly Detection +// Rule ID : RS-SUP-001 +// MITRE ATT&CK : T1195 — Supply Chain Compromise +// Tactic : Initial Access +// Severity : High +// Frequency : Every 30 minutes | Lookback: 30 minutes +// Author : Tanvir Farhad — ShieldTech Ltd, London +// ============================================================ +// Detects compromised or malicious supplier API behaviour +// against the retail platform. Covers supplier keys accessing +// admin endpoints, new service principals created by supplier +// accounts, out-of-spec endpoint access, and mass data export. +// Retail-specific: tracks supplier_api_key patterns tied to +// logistics, payment, and stock management vendors. +// ============================================================ + +let LookbackWindow = 30m; +let BulkExportThreshold = 1000; // records exported above this is suspicious +let MinAnomalousRequests = 3; // minimum out-of-spec requests before alert + +// Admin / privileged endpoints suppliers must never call +let AdminEndpoints = dynamic([ + "/admin", "/management", "/config", "/users", + "/audit", "/security", "/settings", "/permissions" +]); + +// Sensitive data endpoints — acceptable only for authorised internal accounts +let SensitiveEndpoints = dynamic([ + "/customers", "/export", "/payment", "/finance", + "/employees", "/cardholder", "/transactions" +]); + +// Endpoints that are part of the agreed supplier integration spec +let AgreeableEndpoints = dynamic([ + "/inventory", "/stock", "/orders", "/delivery", + "/products", "/shipment", "/tracking", "/catalogue" +]); + +// ── Signal 1 — Supplier API key accessing admin / privileged endpoints ──────── +let SupplierAdminAccess = + AzureDiagnostics + | where ingestion_time() > ago(LookbackWindow) + | where ResourceType == "APIMANAGEMENT/SERVICE" + | extend APIKey = tostring(column_ifexists("apimSubscriptionId_s", "")) + | extend Endpoint = tostring(column_ifexists("requestPath_s", "")) + | where APIKey startswith "supplier_api_key" + | where Endpoint has_any (AdminEndpoints) + | project + TimeGenerated, + SignalType = "SupplierAdminAccess", + SupplierKey = APIKey, + Endpoint, + Method = tostring(column_ifexists("requestMethod_s", "")), + StatusCode = tostring(column_ifexists("responseCode_d", "")); + +// ── Signal 2 — New Azure AD service principal created by supplier account ────── +let NewServicePrincipal = + AuditLogs + | where ingestion_time() > ago(LookbackWindow) + | where OperationName has_any ("Add service principal", "Add application") + | extend InitiatedBy = tostring(InitiatedBy.user.userPrincipalName) + | where InitiatedBy has_any ("supplier", "vendor", "partner", "api", "3rd-party") + or InitiatedBy matches regex @"svc_[a-z0-9]+@" + | extend NewPrincipal = tostring(TargetResources[0].displayName) + | project + TimeGenerated, + SignalType = "NewServicePrincipal", + SupplierKey = InitiatedBy, + Endpoint = NewPrincipal, + Method = CorrelationId, + StatusCode = ""; + +// ── Signal 3 — Supplier accessing endpoints outside agreed integration spec ──── +let UnauthorisedEndpointAccess = + AzureDiagnostics + | where ingestion_time() > ago(LookbackWindow) + | where ResourceType == "APIMANAGEMENT/SERVICE" + | extend APIKey = tostring(column_ifexists("apimSubscriptionId_s", "")) + | extend Endpoint = tostring(column_ifexists("requestPath_s", "")) + | where APIKey startswith "supplier_api_key" + | where not(Endpoint has_any (AgreeableEndpoints)) + | where Endpoint has_any (SensitiveEndpoints) + | summarize + AccessCount = count(), + Endpoints = make_set(Endpoint), + FirstSeen = min(TimeGenerated), + LastSeen = max(TimeGenerated) + by APIKey + | where AccessCount >= MinAnomalousRequests + | project + TimeGenerated = FirstSeen, + SignalType = "UnauthorisedEndpointAccess", + SupplierKey = APIKey, + Endpoint = tostring(Endpoints), + Method = "", + StatusCode = tostring(AccessCount); + +// ── Signal 4 — Mass data export by supplier API key ────────────────────────── +let MassDataExport = + AzureDiagnostics + | where ingestion_time() > ago(LookbackWindow) + | where ResourceType == "APIMANAGEMENT/SERVICE" + | extend APIKey = tostring(column_ifexists("apimSubscriptionId_s", "")) + | extend Endpoint = tostring(column_ifexists("requestPath_s", "")) + | extend RecordsServed = toint(column_ifexists("responseBodyLength_d", 0)) + | where APIKey startswith "supplier_api_key" + | where Endpoint has_any (SensitiveEndpoints) + | summarize + TotalRecords = sum(RecordsServed), + RequestCount = count(), + Endpoints = make_set(Endpoint) + by APIKey, bin(TimeGenerated, LookbackWindow) + | where TotalRecords >= BulkExportThreshold + | project + TimeGenerated, + SignalType = "MassDataExport", + SupplierKey = APIKey, + Endpoint = tostring(Endpoints), + Method = "", + StatusCode = tostring(TotalRecords); + +// ── Union all signals ───────────────────────────────────────────────────────── +union SupplierAdminAccess, NewServicePrincipal, UnauthorisedEndpointAccess, MassDataExport +| summarize + SignalCount = count(), + Signals = make_set(SignalType), + FirstSeen = min(TimeGenerated), + LastSeen = max(TimeGenerated), + SupplierKey = take_any(SupplierKey), + Endpoints = make_set(Endpoint) + by bin(TimeGenerated, LookbackWindow) +| where SignalCount >= 1 +| extend + RiskScore = case(SignalCount >= 3, 90, SignalCount == 2, 78, 60), + MitreTechnique = "T1195", + MitreTactic = "Initial Access", + AlertSeverity = case(SignalCount >= 3, "CRITICAL", SignalCount == 2, "HIGH", "MEDIUM"), + Severity = case(SignalCount >= 2, "HIGH", "MEDIUM"), + PlaybookTrigger = "notify_soc", + DeviceName = "API-GATEWAY", + AlertTitle = strcat("Supply Chain Anomaly — Supplier Behavioural Deviation: ", SupplierKey) +| project + AlertTitle, + DeviceName, + SupplierKey, + AlertSeverity, + Severity, + RiskScore, + SignalCount, + Signals, + Endpoints, + MitreTechnique, + MitreTactic, + PlaybookTrigger, + FirstSeen, + LastSeen +| sort by RiskScore desc diff --git a/docs/mitre-mapping.md b/docs/mitre-mapping.md index 494c71b..dcfc86c 100644 --- a/docs/mitre-mapping.md +++ b/docs/mitre-mapping.md @@ -5,10 +5,62 @@ Full cross-reference of RetailShield detection rules against MITRE ATT&CK Enterp | Status | Tactic | Technique ID | Technique Name | Rule File | Severity | Playbook Trigger | |---|---|---|---|---|---|---| | ✅ Done | Initial Access | T1566.001 | Spearphishing Attachment | `phishing_detection.kql` | High | `quarantine_email` | -| 🔲 Planned | Collection | T1056.001 | Keylogging (POS) | `pos_anomaly.kql` | High | `suspend_terminal` | -| 🔲 Planned | Persistence | T1078 | Valid Accounts | `after_hours_access.kql` | Medium | `notify-soc` | -| 🔲 Planned | Credential Access | T1110.004 | Credential Stuffing | `credential_stuffing.kql` | High | `block_ip` | -| 🔲 Planned | Exfiltration | T1048 | Exfiltration Over Alt. Protocol | `data_exfiltration.kql` | Critical | `isolate_endpoint` | +| ✅ Done | Collection | T1056.001 | Keylogging / RAM Scraping | `pos_anomaly.kql` | High | `suspend_terminal` | +| ✅ Done | Persistence | T1078 | Valid Accounts (After-Hours) | `after_hours_access.kql` | Medium | `notify_soc` | +| ✅ Done | Credential Access | T1110.004 | Credential Stuffing | `credential_stuffing.kql` | High | `block_ip` | +| ✅ Done | Exfiltration | T1048 | Exfiltration Over Alt. Protocol | `data_exfiltration.kql` | Critical | `data_exfil_contain` | | ✅ Done | Impact | T1486 | Data Encrypted for Impact | `ransomware_indicator.kql` | Critical | `isolate_endpoint` | -| 🔲 Planned | Reconnaissance | T1598 | Phishing for Information | `ai_voice_fraud.kql` | High | `notify-soc` | -| 🔲 Planned | Initial Access | T1195 | Supply Chain Compromise | `supply_chain_anomaly.kql` | High | `block_ip` | +| ✅ Done | Reconnaissance | T1598 | Phishing for Information (Voice) | `ai_voice_fraud.kql` | High | `notify_soc` | +| ✅ Done | Initial Access | T1195 | Supply Chain Compromise | `supply_chain_anomaly.kql` | High | `notify_soc` | + +--- + +## Coverage Summary + +| Category | Count | Status | +|---|---|---| +| Techniques Monitored | 8 / 8 | ✅ Full Coverage | +| Rules Implemented | 8 | ✅ Complete | +| Playbooks Wired | 4 | `quarantine_email`, `block_ip`, `isolate_endpoint`, `notify_soc` | +| MITRE Tactics Covered | 6 | Initial Access, Persistence, Credential Access, Collection, Exfiltration, Impact | + +--- + +## Retail-Specific Threat Mapping + +| Retail Threat | MITRE Technique | RetailShield Rule | Priority | +|---|---|---|---| +| Phishing targeting finance/HR staff | T1566.001 | `phishing_detection.kql` | Critical | +| POS RAM scraping / keylogger | T1056.001 | `pos_anomaly.kql` | Critical | +| Service account misuse (off-hours) | T1078 | `after_hours_access.kql` | High | +| Distributed credential stuffing | T1110.004 | `credential_stuffing.kql` | High | +| DNS exfiltration from ERP/DB servers | T1048 | `data_exfiltration.kql` | Critical | +| Ransomware staging on POS network | T1486 | `ransomware_indicator.kql` | Critical | +| AI deepfake voice fraud (CFO fraud) | T1598 | `ai_voice_fraud.kql` | High | +| Compromised supplier API access | T1195 | `supply_chain_anomaly.kql` | High | + +--- + +## PCI DSS v4.0 Alignment + +| PCI DSS Requirement | Description | Covered By | Status | +|---|---|---|---| +| Req 1 | Network security controls | `after_hours_access.kql`, `supply_chain_anomaly.kql` | ✅ Covered | +| Req 2 | Secure configurations | CVE Scanner (`cve_scanner.py`) | ✅ Covered | +| Req 3 | Protect stored cardholder data | `data_exfiltration.kql`, `pos_anomaly.kql` | ✅ Covered | +| Req 4 | Cryptography in transit | CVE Scanner (Verifone TLS CVEs) | ✅ Covered | +| Req 5 | Anti-malware | `ransomware_indicator.kql` | ✅ Covered | +| Req 6 | Secure software development | CVE Scanner (patching status) | ✅ Covered | +| Req 7 | Need-to-know access control | `after_hours_access.kql` | 🔶 Partial | +| Req 8 | Identity and authentication | `credential_stuffing.kql` | ✅ Covered | +| Req 9 | Physical access controls | `pos_anomaly.kql` (terminal tampering) | 🔶 Partial | +| Req 10 | Logging and monitoring | All 8 KQL rules | ✅ Covered | +| Req 11 | Security testing | CVE Scanner (`cve_scanner.py`) | ✅ Covered | +| Req 12 | Organisational security policy | 🔲 Planned (v3.0) | 🔲 Planned | + +--- + +## Author + +**Tanvir Farhad** — ShieldTech Ltd, London +[github.com/TFT444/RetailShield](https://github.com/TFT444/RetailShield) diff --git a/frontend/api/brief.js b/frontend/api/brief.js new file mode 100644 index 0000000..a026449 --- /dev/null +++ b/frontend/api/brief.js @@ -0,0 +1,101 @@ +/** + * RetailShield — AI Threat Briefing Endpoint + * Vercel Serverless Function — POST /api/brief + * Calls the Anthropic Claude API to generate an executive-ready + * threat summary from the current dashboard state. + * + * Requires ANTHROPIC_API_KEY env var set in Vercel project settings. + * Falls back to a simulated briefing when the key is absent. + */ + +const FALLBACK_BRIEFING = (threats, score, vulnCount) => ` +RETAILSHIELD EXECUTIVE THREAT BRIEFING — ${new Date().toUTCString()} + +Security Score: ${score}/100 | Active Threats: ${threats.critical} Critical, ${threats.high} High | CVEs Outstanding: ${vulnCount} + +IMMEDIATE ACTIONS REQUIRED: +${threats.critical > 0 ? `• ${threats.critical} CRITICAL threat(s) detected — incident response team should be engaged immediately.` : ''} +${threats.active > 0 ? `• ${threats.active} active threat(s) currently uncontained — review live feed for isolation status.` : ''} +${vulnCount > 0 ? `• ${vulnCount} unpatched CVEs identified across retail infrastructure — prioritise critical severity patches within 48 hours.` : ''} + +Automated playbooks have been triggered for all active incidents. Manual analyst review required for items marked ACTIVE in the threat feed. + +This briefing was generated by RetailShield v2.0 — ShieldTech Ltd. +`.trim() + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const { threats = {}, score = 0, vulnSummary = {}, topThreats = [] } = req.body || {} + + const apiKey = process.env.ANTHROPIC_API_KEY + if (!apiKey) { + return res.status(200).json({ + briefing: FALLBACK_BRIEFING(threats, score, Object.values(vulnSummary).reduce((a, b) => a + b, 0)), + source: 'fallback', + }) + } + + const threatLines = topThreats.slice(0, 5).map( + (t, i) => `${i + 1}. ${t.name} (${t.mitre} — ${t.tactic}, ${t.severity.toUpperCase()}, Status: ${t.status})` + ).join('\n') + + const vulnTotal = Object.values(vulnSummary).reduce((a, b) => a + b, 0) + + const prompt = `You are a senior SOC analyst writing a concise executive briefing for a UK retail CISO. + +Current RetailShield dashboard state: +- Security Score: ${score}/100 +- Active Threats: ${threats.critical || 0} Critical, ${threats.high || 0} High, ${threats.active || 0} Active, ${threats.blocked || 0} Blocked +- CVE Vulnerability Summary: ${vulnSummary.critical || 0} Critical, ${vulnSummary.high || 0} High, ${vulnSummary.medium || 0} Medium +- Total unpatched CVEs: ${vulnTotal} + +Top active threats: +${threatLines || 'No active threats at this time.'} + +Write a professional executive briefing in exactly this format: +1. One sentence overall status (use plain English, not jargon) +2. Top 2-3 priority actions the business must take RIGHT NOW +3. Regulatory exposure (PCI DSS, UK GDPR Art.33 if relevant) +4. One closing sentence on what automated playbooks have already done + +Keep it under 200 words. Be direct and authoritative. Do not use bullet points — use numbered lists only for the priority actions. Do not start with "I" or "As an AI".` + + try { + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4-6', + max_tokens: 400, + messages: [{ role: 'user', content: prompt }], + }), + }) + + if (!response.ok) { + const err = await response.text() + console.error('Anthropic API error:', err) + return res.status(200).json({ + briefing: FALLBACK_BRIEFING(threats, score, vulnTotal), + source: 'fallback', + }) + } + + const data = await response.json() + const briefing = data.content?.[0]?.text || FALLBACK_BRIEFING(threats, score, vulnTotal) + + return res.status(200).json({ briefing, source: 'claude' }) + } catch (err) { + console.error('Brief generation error:', err) + return res.status(200).json({ + briefing: FALLBACK_BRIEFING(threats, score, vulnTotal), + source: 'fallback', + }) + } +} diff --git a/frontend/src/RetailShield.jsx b/frontend/src/RetailShield.jsx index b6f31ad..3448ce4 100644 --- a/frontend/src/RetailShield.jsx +++ b/frontend/src/RetailShield.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' -// ── Design tokens ───────────────────────────────────────────────────────────── +// ── Design tokens ─────────────────────────────────────────────────────────────────────────── const C = { bg: '#080808', surface: '#0f0f0f', @@ -22,7 +22,7 @@ const C = { const SEV = { critical: C.red, high: C.orange, medium: C.yellow, low: C.blue } const STA = { active: C.red, blocked: C.green, pending: C.orange } -// ── Threat data ─────────────────────────────────────────────────────────────── +// ── Threat data ─────────────────────────────────────────────────────────────────────────── const BASE_THREATS = [ { id: 1, name: 'Phishing Email — Finance Team', mitre: 'T1566.001', @@ -214,7 +214,6 @@ const BASE_THREATS = [ }, ] -// New threats injected during live simulation const LIVE_THREATS = [ { name: 'Brute Force — Admin Console', mitre: 'T1110.001', @@ -246,7 +245,6 @@ const LIVE_THREATS = [ }, ] -// ── Attack simulation events (5 threats, one per MITRE technique) ──────────── const ATTACK_SIM_EVENTS = [ { name: 'Spearphishing — CFO Impersonation [SIM]', mitre: 'T1566.001', @@ -255,20 +253,8 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] Macro-enabled .docm attachment delivered to CFO inbox from invoice-urgent@dr4gonm4il.com. SHA256 matches RetailIOCWatchlist entry added 6 minutes ago.', playbook: { trigger: 'quarantine_email', - auto: [ - 'Email quarantined across all mailboxes via Defender for O365', - 'Sender domain dr4gonm4il.com added to tenant block list', - 'SHA256 hash pushed to RetailIOCWatchlist', - 'CFO and EA notified via Teams', - 'Incident ticket raised: #SIM-001', - ], - manual: [ - 'Confirm CFO did not open attachment before quarantine', - 'Check for related campaign in last 72h mail gateway logs', - 'Review sender domain registration — likely <7 days old', - 'Escalate to CISO if attachment was executed', - 'Submit to Microsoft MSRC threat intelligence', - ], + auto: ['Email quarantined across all mailboxes via Defender for O365','Sender domain dr4gonm4il.com added to tenant block list','SHA256 hash pushed to RetailIOCWatchlist','CFO and EA notified via Teams','Incident ticket raised: #SIM-001'], + manual: ['Confirm CFO did not open attachment before quarantine','Check for related campaign in last 72h mail gateway logs','Review sender domain registration — likely <7 days old','Escalate to CISO if attachment was executed','Submit to Microsoft MSRC threat intelligence'], }, }, { @@ -278,20 +264,8 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] 1,247 failed logins across 31 accounts from 89 distinct IPs in 3 minutes. Credential pairs match leaked Retail Sector dump (Collection #7, 2025). 4 accounts compromised.', playbook: { trigger: 'block_ip', - auto: [ - '89 source IPs added to Azure WAF deny rules', - 'CAPTCHA enforcement enabled on /pos-admin/login', - '4 compromised accounts locked and password reset triggered', - 'Rate limiting set to 3 attempts / 5 min per account', - 'Indicators submitted to AbuseIPDB', - ], - manual: [ - 'Force reset all 31 targeted accounts preventatively', - 'Check if compromised accounts accessed transaction data', - 'Assess PCI DSS notification requirement', - 'Tune WAF rules for similar User-Agent rotation patterns', - 'Monitor for continued campaign over next 48h', - ], + auto: ['89 source IPs added to Azure WAF deny rules','CAPTCHA enforcement enabled on /pos-admin/login','4 compromised accounts locked and password reset triggered','Rate limiting set to 3 attempts / 5 min per account','Indicators submitted to AbuseIPDB'], + manual: ['Force reset all 31 targeted accounts preventatively','Check if compromised accounts accessed transaction data','Assess PCI DSS notification requirement','Tune WAF rules for similar User-Agent rotation patterns','Monitor for continued campaign over next 48h'], }, }, { @@ -301,20 +275,8 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] 847 files renamed with .dragon3 extension in 4 minutes. vssadmin delete shadows confirmed. C2 beacon to 91.234.55.12 (DragonForce infrastructure). Endpoint being isolated.', playbook: { trigger: 'isolate_endpoint', - auto: [ - 'WORKSTATION-SIM-07 isolated via Defender for Endpoint API', - 'Network access revoked at Azure Firewall', - 'C2 IP 91.234.55.12 added to block list across all firewalls', - 'Forensic memory snapshot and disk image initiated', - 'P1 incident raised — on-call SOC paged via PagerDuty', - ], - manual: [ - 'Confirm no lateral movement to adjacent workstations', - 'Scope shadow copy destruction across all network shares', - 'Engage IR retainer if spread confirmed to >3 hosts', - 'Start GDPR Art.33 72h notification clock', - 'Prepare operational comms for store management', - ], + auto: ['WORKSTATION-SIM-07 isolated via Defender for Endpoint API','Network access revoked at Azure Firewall','C2 IP 91.234.55.12 added to block list across all firewalls','Forensic memory snapshot and disk image initiated','P1 incident raised — on-call SOC paged via PagerDuty'], + manual: ['Confirm no lateral movement to adjacent workstations','Scope shadow copy destruction across all network shares','Engage IR retainer if spread confirmed to >3 hosts','Start GDPR Art.33 72h notification clock','Prepare operational comms for store management'], }, }, { @@ -324,20 +286,8 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] 3,847 DNS queries to exfil.d4t4pipe[.]xyz with 64-char base64 subdomains over 12 minutes. 2.1 GB staged from \\\\DB-SERVER\\customers\\. DNS blocked at resolver.', playbook: { trigger: 'data_exfil_contain', - auto: [ - 'DNS to d4t4pipe[.]xyz blocked at Sentinel-managed resolver', - 'Domain added to RetailIOCWatchlist and sinkholes', - 'db_svc_account password reset and sessions revoked', - 'Network flow evidence preserved in Log Analytics', - 'DLP alert raised for \\customers\\ folder bulk read', - ], - manual: [ - 'Determine initial access vector to DB-SERVER', - 'Assess what customer PII was staged — GDPR impact assessment', - 'Engage Data Protection Officer immediately', - 'Determine if exfiltration completed before DNS block', - 'Audit db_svc_account activity for prior 72h', - ], + auto: ['DNS to d4t4pipe[.]xyz blocked at Sentinel-managed resolver','Domain added to RetailIOCWatchlist and sinkholes','db_svc_account password reset and sessions revoked','Network flow evidence preserved in Log Analytics','DLP alert raised for \\customers\\ folder bulk read'], + manual: ['Determine initial access vector to DB-SERVER','Assess what customer PII was staged — GDPR impact assessment','Engage Data Protection Officer immediately','Determine if exfiltration completed before DNS block','Audit db_svc_account activity for prior 72h'], }, }, { @@ -347,19 +297,8 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] £125,000 BACS transfer request via AI voice call impersonating Regional MD. Caller asked to bypass dual-approval. AI confidence score 0.97. Payment blocked by finance team.', playbook: { trigger: 'notify_soc', - auto: [ - 'Call recording preserved under legal hold', - 'Spoofed number added to VOIP block list', - 'Finance team fraud awareness alert issued', - 'Report filed with Action Fraud (ref: SIM-NCSC-2026)', - ], - manual: [ - 'Verify with Regional MD via known direct mobile', - 'Issue mandatory voice fraud refresher to finance team', - 'Confirm dual-approval protocol was followed correctly', - 'Submit deepfake audio to NCSC AI threat analysis team', - 'Update payment policy: add video call re-confirmation step', - ], + auto: ['Call recording preserved under legal hold','Spoofed number added to VOIP block list','Finance team fraud awareness alert issued','Report filed with Action Fraud (ref: SIM-NCSC-2026)'], + manual: ['Verify with Regional MD via known direct mobile','Issue mandatory voice fraud refresher to finance team','Confirm dual-approval protocol was followed correctly','Submit deepfake audio to NCSC AI threat analysis team','Update payment policy: add video call re-confirmation step'], }, }, { @@ -369,19 +308,8 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] Interactive RDP login to domain controller at 02:58 UTC from 185.220.101.47 (Tor exit node). Service account svc_backup_sim — interactive sessions are outside baseline.', playbook: { trigger: 'notify_soc', - auto: [ - 'svc_backup_sim flagged for elevated monitoring', - 'Risky sign-in logged in Azure AD Identity Protection', - 'SOC Teams channel alerted with full session context', - 'Session commands captured to forensic audit trail', - ], - manual: [ - 'Verify whether svc_backup_sim ever logs in interactively', - 'Cross-reference IP against Tor exit node list', - 'Review all AD changes made during this session', - 'Disable account immediately if compromise confirmed', - 'Audit all accounts from same /24 IP range', - ], + auto: ['svc_backup_sim flagged for elevated monitoring','Risky sign-in logged in Azure AD Identity Protection','SOC Teams channel alerted with full session context','Session commands captured to forensic audit trail'], + manual: ['Verify whether svc_backup_sim ever logs in interactively','Cross-reference IP against Tor exit node list','Review all AD changes made during this session','Disable account immediately if compromise confirmed','Audit all accounts from same /24 IP range'], }, }, { @@ -391,24 +319,84 @@ const ATTACK_SIM_EVENTS = [ desc: '[SIMULATION] Unknown unsigned DLL (xf99ab.dll) injected into POS process on TILL-09. Transaction volume 5.1σ above 30-day baseline. RAM scraping pattern consistent with known POS malware.', playbook: { trigger: 'suspend_terminal', - auto: [ - 'POS-SIM-TILL-09 suspended via retail management API', - 'Store manager notified via Teams', - 'Memory dump initiated for DLL forensic analysis', - 'ServiceNow ticket raised with PCI DSS flag', - ], - manual: [ - 'Physically inspect TILL-09 for hardware skimmer', - 'Pull network capture from last 4 hours on switch port', - 'Identify origin of xf99ab.dll via software deployment logs', - 'Assess payment card data exposure window for PCI notification', - 'Re-image terminal before returning to service', - ], + auto: ['POS-SIM-TILL-09 suspended via retail management API','Store manager notified via Teams','Memory dump initiated for DLL forensic analysis','ServiceNow ticket raised with PCI DSS flag'], + manual: ['Physically inspect TILL-09 for hardware skimmer','Pull network capture from last 4 hours on switch port','Identify origin of xf99ab.dll via software deployment logs','Assess payment card data exposure window for PCI notification','Re-image terminal before returning to service'], }, }, ] -// ── SVG Security Posture Gauge ──────────────────────────────────────────────── +const VULN_INITIAL = { + lastScan: '2026-05-28T06:00:00Z', + totalAssets: 18, + summary: { critical: 8, high: 22, medium: 4, low: 0 }, + findings: [ + { id:'POS-TILL-01', product:'Oracle Xstore POS', version:'8.1', location:'Hounslow Branch', cat:'POS System', vulns:[ + { cve:'CVE-2025-44123', cvss:9.8, sev:'critical', title:'Unauthenticated RCE via Java deserialization in config sync', patch:true, exploit:true, mitre:'T1190' }, + { cve:'CVE-2025-31847', cvss:8.8, sev:'high', title:'SQL injection in transaction reporting module', patch:true, exploit:false, mitre:'T1190' }, + ]}, + { id:'POS-TILL-03', product:'NCR Aloha POS', version:'12.3', location:'Hammersmith Branch', cat:'POS System', vulns:[ + { cve:'CVE-2025-19284', cvss:9.1, sev:'critical', title:'Authentication bypass via malformed session token', patch:true, exploit:true, mitre:'T1078' }, + { cve:'CVE-2024-52891', cvss:7.8, sev:'high', title:'Local privilege escalation via insecure service dir', patch:true, exploit:false, mitre:'T1068' }, + ]}, + { id:'POS-TILL-05', product:'Toshiba TCx POS', version:'5.2', location:'Ealing Branch', cat:'POS System', vulns:[ + { cve:'CVE-2025-08374', cvss:8.1, sev:'high', title:'Stack buffer overflow in EMV payment parsing library', patch:true, exploit:false, mitre:'T1203' }, + { cve:'CVE-2024-47203', cvss:6.5, sev:'medium', title:'Plaintext credentials in world-readable config files', patch:true, exploit:false, mitre:'T1083' }, + ]}, + { id:'POS-TILL-06', product:'Verifone POS', version:'3.4', location:'Ealing Branch', cat:'POS System', vulns:[ + { cve:'CVE-2025-22916', cvss:9.1, sev:'critical', title:'Hardcoded admin credentials in management daemon', patch:true, exploit:true, mitre:'T1078' }, + { cve:'CVE-2024-38847', cvss:7.5, sev:'high', title:"IDOR allows access to other terminals' transactions", patch:true, exploit:false, mitre:'T1083' }, + ]}, + { id:'ERP-DYN-01', product:'Microsoft Dynamics Retail', version:'10.0.28', location:'Head Office', cat:'Stock Management', vulns:[ + { cve:'CVE-2024-61834', cvss:9.3, sev:'critical', title:'Authentication bypass via malformed OAuth token', patch:true, exploit:true, mitre:'T1078' }, + { cve:'CVE-2025-17293', cvss:8.2, sev:'high', title:'XXE injection in product import — reads server files', patch:true, exploit:false, mitre:'T1190' }, + ]}, + { id:'ERP-ORA-01', product:'Oracle Retail Merchandising', version:'21.0', location:'Head Office', cat:'Stock Management', vulns:[ + { cve:'CVE-2025-29481', cvss:9.8, sev:'critical', title:'RCE via deserialization in merchandise planning API', patch:true, exploit:true, mitre:'T1190' }, + { cve:'CVE-2024-73921', cvss:7.5, sev:'high', title:'Path traversal in report download endpoint', patch:true, exploit:false, mitre:'T1083' }, + ]}, + { id:'ERP-SAP-01', product:'SAP Retail', version:'S/4HANA 2021', location:'Head Office', cat:'Stock Management', vulns:[ + { cve:'CVE-2025-55921', cvss:8.6, sev:'high', title:'SSRF in integration layer exposes internal metadata', patch:true, exploit:false, mitre:'T1190' }, + { cve:'CVE-2025-43782', cvss:7.4, sev:'high', title:'Stored XSS in product catalogue via supplier input', patch:true, exploit:false, mitre:'T1059.007' }, + ]}, + { id:'ERP-JDA-01', product:'JDA Supply Chain', version:'9.2', location:'Head Office', cat:'Stock Management', vulns:[ + { cve:'CVE-2024-44918', cvss:8.1, sev:'high', title:'SQL injection in order management module', patch:true, exploit:false, mitre:'T1190' }, + { cve:'CVE-2025-61204', cvss:7.5, sev:'high', title:'IDOR exposes competitor purchase orders', patch:true, exploit:false, mitre:'T1083' }, + ]}, + { id:'TERM-VFN-01', product:'Verifone VX520', version:'2.1.0', location:'Hounslow Branch', cat:'Payment Terminal', vulns:[ + { cve:'CVE-2025-33741', cvss:9.4, sev:'critical', title:'PIN block extraction via differential power analysis', patch:true, exploit:false, mitre:'T1056.001' }, + { cve:'CVE-2024-55847', cvss:7.8, sev:'high', title:'Unsigned firmware accepted via ARP-spoofed update server', patch:true, exploit:false, mitre:'T1542' }, + ]}, + { id:'TERM-PAX-01', product:'PAX S920', version:'1.2', location:'Hounslow Branch', cat:'Payment Terminal', vulns:[ + { cve:'CVE-2025-37482', cvss:9.8, sev:'critical', title:'RCE via malformed EMV TLV packet over merchant LAN', patch:true, exploit:true, mitre:'T1190' }, + { cve:'CVE-2025-21847', cvss:8.4, sev:'high', title:'Root shell exposed via USB diagnostic interface', patch:true, exploit:true, mitre:'T1059' }, + ]}, + { id:'TERM-ING-01', product:'Ingenico iCT250', version:'6.0.0', location:'Ealing Branch', cat:'Payment Terminal', vulns:[ + { cve:'CVE-2025-48293', cvss:8.8, sev:'high', title:'Memory corruption in NFC NDEF handler — RCE via card', patch:true, exploit:false, mitre:'T1203' }, + { cve:'CVE-2024-92841', cvss:7.6, sev:'high', title:'Static Bluetooth PIN 0000 — relay attack possible', patch:true, exploit:true, mitre:'T1557' }, + ]}, + { id:'TERM-VFN-02', product:'Verifone P400', version:'3.0.1', location:'Hammersmith Branch', cat:'Payment Terminal', vulns:[ + { cve:'CVE-2025-12847', cvss:8.0, sev:'high', title:'Firmware downgrade attack via USB maintenance port', patch:true, exploit:false, mitre:'T1542.001' }, + { cve:'CVE-2024-38291', cvss:6.8, sev:'medium', title:'Weak TLS cipher suites (RC4/3DES) accepted in payment comms', patch:true, exploit:false, mitre:'T1600' }, + ]}, + { id:'PLAT-SHP-01', product:'Shopify POS', version:'9.1.0', location:'Online', cat:'Retail Platform', vulns:[ + { cve:'CVE-2025-28473', cvss:8.1, sev:'high', title:'Certificate pinning bypass enables on-path MITM attack', patch:true, exploit:false, mitre:'T1557' }, + { cve:'CVE-2025-41928', cvss:7.5, sev:'high', title:'API secret key exposed in iOS app bundle plist', patch:true, exploit:false, mitre:'T1552.001' }, + ]}, + { id:'PLAT-SQR-01', product:'Square POS', version:'5.28', location:'Online', cat:'Retail Platform', vulns:[ + { cve:'CVE-2025-53847', cvss:8.8, sev:'high', title:'No TLS cert validation — accepts self-signed certs', patch:true, exploit:false, mitre:'T1557.001' }, + { cve:'CVE-2024-82941', cvss:7.4, sev:'high', title:'Bluetooth pairing replay attack on card reader', patch:true, exploit:false, mitre:'T1557' }, + ]}, + { id:'PLAT-LSP-01', product:'Lightspeed Retail', version:'2024.1', location:'Online', cat:'Retail Platform', vulns:[ + { cve:'CVE-2025-44821', cvss:8.6, sev:'high', title:'Path traversal in file export reads server credentials', patch:true, exploit:false, mitre:'T1083' }, + { cve:'CVE-2024-63947', cvss:7.1, sev:'medium', title:'CSRF token reuse allows unauthorised admin actions', patch:true, exploit:false, mitre:'T1059.007' }, + ]}, + { id:'PLAT-RVL-01', product:'Revel POS', version:'4.7', location:'Online', cat:'Retail Platform', vulns:[ + { cve:'CVE-2025-17482', cvss:9.1, sev:'critical', title:'Hardcoded default admin credentials in iPad management', patch:true, exploit:true, mitre:'T1078' }, + { cve:'CVE-2024-57291', cvss:6.8, sev:'medium', title:'Card tokens stored in unencrypted local SQLite DB', patch:true, exploit:false, mitre:'T1005' }, + ]}, + ], +} + function SecurityGauge({ score }) { const cx = 100, cy = 95, r = 72 const startDeg = -210, endDeg = 30, totalArc = endDeg - startDeg @@ -423,152 +411,95 @@ function SecurityGauge({ score }) { return ( ) } -// ── Attack timeline bar chart ───────────────────────────────────────────────── function AttackTimeline({ tick }) { - const labels = ['18h', '19h', '20h', '21h', '22h', '23h', '00h', '01h', '02h', '03h', '04h', '05h'] - const base = [1, 0, 2, 1, 0, 3, 2, 1, 4, 3, 2, 1] - const counts = base.map((v, i) => i === labels.length - 1 ? Math.min(6, v + (tick % 3)) : v) + const labels = ['18h','19h','20h','21h','22h','23h','00h','01h','02h','03h','04h','05h'] + const base = [1,0,2,1,0,3,2,1,4,3,2,1] + const counts = base.map((v,i) => i === labels.length-1 ? Math.min(6, v+(tick%3)) : v) const max = Math.max(...counts, 1) return (
{threat.tactic} · {threat.device} · {threat.user} · {threat.time}
+{threat.tactic} · {threat.device} · {threat.user} · {threat.time}
{threat.desc}
+{threat.desc}
{threat.playbook.trigger}
+ {threat.playbook.trigger}
{briefing}
+