From 75400e08b699bdd8987b29027421038e1c9a51c9 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 21 Jun 2026 14:47:25 +0000 Subject: [PATCH 1/2] feat(frontend): Loss Prevention module with store risk scoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds LossPrevention.jsx — a full LP incident dashboard with: - KPI stat cards (open incidents, total exposure, high-risk stores, portfolio avg risk) - Filterable LP incident feed (by rule ID and severity) with clickable slide-in detail panel showing timeline, auto-defence, and recommendations - Store Risk Leaderboard with animated risk bars, trend indicators, and highest detection signal per store (covers issue #125) Wires the module into App.jsx (route 'lp') and activates Loss Prevention in Portal.jsx module grid. Depends on LP_INCIDENTS and LP_STORE_RISK exports from data.js (PR #150). Closes #124 Closes #125 --- frontend/src/App.jsx | 2 + frontend/src/modules/LossPrevention.jsx | 420 ++++++++++++++++++++++++ frontend/src/pages/Portal.jsx | 2 +- 3 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 frontend/src/modules/LossPrevention.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 35a3d45..269e1ba 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import ThreatDetection from './modules/ThreatDetection.jsx'; import VulnerabilityScanner from './modules/VulnerabilityScanner.jsx'; import ComplianceCentre from './modules/ComplianceCentre.jsx'; import DetectionRules from './modules/DetectionRules.jsx'; +import LossPrevention from './modules/LossPrevention.jsx'; export default function App() { const [route, setRoute] = useState('landing'); @@ -27,6 +28,7 @@ export default function App() { case 'vuln': return nav('portal')} />; case 'compliance': return nav('portal')} />; case 'rules': return nav('portal')} />; + case 'lp': return nav('portal')} />; default: return nav('portal')} onSignIn={() => nav('login')} />; } } diff --git a/frontend/src/modules/LossPrevention.jsx b/frontend/src/modules/LossPrevention.jsx new file mode 100644 index 0000000..a419d07 --- /dev/null +++ b/frontend/src/modules/LossPrevention.jsx @@ -0,0 +1,420 @@ +import { useState, useMemo } from 'react'; +import { + ShoppingCart, AlertTriangle, TrendingUp, TrendingDown, + Minus, X, Clock, User, Monitor, ChevronRight, + PoundSterling, Store, BarChart2, +} from 'lucide-react'; +import TopBar from '../components/TopBar.jsx'; +import SeverityBadge from '../components/SeverityBadge.jsx'; +import StatCard from '../components/StatCard.jsx'; +import { LP_INCIDENTS, LP_STORE_RISK } from '../lib/data.js'; +import { useBreakpoint } from '../lib/hooks.js'; + +const LP_COLOR = '#F97316'; +const LP_DIM = 'rgba(249,115,22,0.12)'; + +const SIGNAL_LABELS = { + HighVolumeVoidRefund: 'High-volume voids', + HighValueVoidNoOverride: 'High-value void, no override', + RapidVoidBurst: 'Rapid void burst', + BulkGiftCardActivation: 'Bulk card activation', + RapidCrossChannelRedemption: 'Cross-channel redemption', + HighValueActivationSession: 'High-value activation', + RepeatHighDiscountRelationship: 'Repeat discount relationship', + BelowAverageBasketForCustomer: 'Below-average basket', + HighDiscountConcentrationAtTerminal: 'Discount concentration', + AfterHoursHighValueSale: 'After-hours sale', + GhostEmployeeAfterHours: 'Ghost employee', + ClosedStoreTerminalBurst: 'Closed-store terminal burst', +}; + +const RULE_LABELS = { + 'LP-001': 'POS Void/Refund Abuse', + 'LP-002': 'Gift Card Rapid Redemption', + 'LP-003': 'Sweethearting', + 'LP-004': 'After-Hours POS', +}; + +function fmtDate(iso) { + if (!iso) return '—'; + return new Date(iso).toLocaleString('en-GB', { + day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit', timeZone:'UTC', + }); +} + +function fmtGBP(v) { + if (!v && v !== 0) return '—'; + return `£${v.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; +} + +function TrendIcon({ trend }) { + if (trend === 'up') return ; + if (trend === 'down') return ; + return ; +} + +function RiskBar({ score }) { + const color = score >= 80 ? '#DC2626' : score >= 60 ? '#F97316' : '#16A34A'; + return ( +
+
+
+
+ {score} +
+ ); +} + +function IncidentDetail({ inc, onClose }) { + if (!inc) return null; + return ( +
+
e.stopPropagation()} + style={{ + width:'min(520px, calc(100vw - 32px))', maxHeight:'calc(100vh - 32px)', + overflowY:'auto', background:'var(--surface)', + border:'1px solid var(--border)', borderRadius:'var(--r-card)', + boxShadow:'var(--shadow-lg)', display:'flex', flexDirection:'column', + }} + > + {/* Detail header */} +
+
+
+ + {inc.id} + {inc.ruleId} +
+
{inc.title}
+
+ +
+ +
+ + {/* Meta row */} +
+ {[ + { label:'Detection signal', value: SIGNAL_LABELS[inc.detectionSignal] || inc.detectionSignal }, + { label:'MTTD', value: `${inc.mttd} min` }, + { label:'Store', value: inc.storeId }, + { label:'Operator', value: inc.operatorId || '—' }, + { label:'Terminal', value: inc.terminalId || '—' }, + { label:'Risk score', value: inc.riskScore }, + { label:'Estimated loss', value: fmtGBP(inc.estimatedLossGBP) }, + { label:'Detected', value: fmtDate(inc.detectedAt) }, + ].map(row => ( +
+
{row.label}
+
{row.value}
+
+ ))} +
+ + {/* Description */} +
+
Description
+

{inc.description}

+
+ + {/* Timeline */} + {inc.timeline?.length > 0 && ( +
+
Event timeline
+
+ {inc.timeline.map((ev, i) => ( +
+
+
+ {i < inc.timeline.length - 1 &&
} +
+
+ {ev.time} + {ev.event} +
+
+ ))} +
+
+ )} + + {/* Auto-defence */} + {inc.autoDefence?.length > 0 && ( +
+
Automated response
+
+ {inc.autoDefence.map((a, i) => ( +
+
+ {a} +
+ ))} +
+
+ )} + + {/* Recommendations */} + {inc.recommendations?.length > 0 && ( +
+
Recommendations
+
+ {inc.recommendations.map((r, i) => ( +
+
+ {r} +
+ ))} +
+
+ )} +
+
+
+ ); +} + +export default function LossPrevention({ nav, onBack }) { + const { isMobile } = useBreakpoint(); + const [selectedInc, setSelectedInc] = useState(null); + const [filterRule, setFilterRule] = useState('All'); + const [filterSev, setFilterSev] = useState('All'); + + const incidents = LP_INCIDENTS; + const storeRisk = LP_STORE_RISK; + + const filtered = useMemo(() => incidents.filter(inc => { + if (filterRule !== 'All' && inc.ruleId !== filterRule) return false; + if (filterSev !== 'All' && inc.severity !== filterSev) return false; + return true; + }), [incidents, filterRule, filterSev]); + + const totalLoss = incidents.reduce((s, i) => s + (i.estimatedLossGBP || 0), 0); + const criticalCount = incidents.filter(i => i.severity === 'Critical').length; + const avgRisk = storeRisk.length > 0 ? Math.round(storeRisk.reduce((s, r) => s + r.riskScore, 0) / storeRisk.length) : 0; + const highRiskCount = storeRisk.filter(r => r.riskScore >= 80).length; + + const selectBtnStyle = (active) => ({ + padding:'4px 10px', borderRadius:'var(--r-btn)', fontSize:'11px', fontWeight:600, + cursor:'pointer', border:`1px solid ${active ? LP_COLOR : 'var(--border)'}`, + background: active ? LP_DIM : 'transparent', + color: active ? LP_COLOR : 'var(--text-dim)', + transition:'all var(--t)', + }); + + return ( +
+ + + +
+ + {/* Page header */} +
+
+ +
+
+

Loss Prevention

+

Financial fraud detection, void/refund abuse, and store risk scoring

+
+
+ + {/* KPI cards */} +
+ i.status !== 'Resolved').length} + sub={`${criticalCount} critical`} + accent={criticalCount > 0 ? '#DC2626' : LP_COLOR} + icon={AlertTriangle} + /> + + = 3 ? '#DC2626' : LP_COLOR} + icon={Store} + /> + = 70 ? '#DC2626' : avgRisk >= 50 ? LP_COLOR : '#16A34A'} + icon={BarChart2} + /> +
+ + {/* Main layout — two columns on wide screens */} +
+ + {/* Left: Incident feed */} +
+
+ + LP Incidents ({filtered.length}) + +
+ {['All', 'LP-001', 'LP-002', 'LP-003', 'LP-004'].map(r => ( + + ))} +
+ {['All', 'Critical', 'High'].map(s => ( + + ))} +
+
+ + {filtered.length === 0 ? ( +
+ No incidents match the selected filters. +
+ ) : ( +
+ {filtered.map(inc => ( +
setSelectedInc(inc)} + role="button" + tabIndex={0} + aria-label={`View incident ${inc.id}`} + onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') setSelectedInc(inc); }} + style={{ + background:'var(--card)', border:'1px solid var(--border)', + borderRadius:'var(--r-card)', padding:'14px 16px', + cursor:'pointer', transition:'all var(--t)', + display:'flex', flexDirection:'column', gap:'8px', + }} + onMouseEnter={e => { e.currentTarget.style.background='var(--card-hover)'; e.currentTarget.style.borderColor='rgba(249,115,22,0.25)'; e.currentTarget.style.transform='translateY(-1px)'; }} + onMouseLeave={e => { e.currentTarget.style.background='var(--card)'; e.currentTarget.style.borderColor='var(--border)'; e.currentTarget.style.transform='none'; }} + > +
+
+
+ + {inc.id} + + {inc.ruleId} + +
+
{inc.title}
+
+ +
+ +
+
+ + {inc.storeId} +
+ {inc.operatorId && ( +
+ + {inc.operatorId} +
+ )} +
+ + {fmtDate(inc.detectedAt)} +
+ {inc.estimatedLossGBP > 0 && ( + + {fmtGBP(inc.estimatedLossGBP)} + + )} +
+ +
+ Signal: {SIGNAL_LABELS[inc.detectionSignal] || inc.detectionSignal} +
+
+ ))} +
+ )} +
+ + {/* Right: Store Risk Leaderboard */} +
+ + Store Risk Leaderboard + +
+ {storeRisk.map((s, i) => ( +
+
+
+
+ + #{i + 1} + + + {s.storeName} + +
+
{s.storeId}
+
+
+ + {s.openIncidents > 0 && ( + + {s.openIncidents} open + + )} +
+
+ + {s.highestSignal && ( +
+ {SIGNAL_LABELS[s.highestSignal] || s.highestSignal} +
+ )} +
+ ))} +
+
+ +
+
+ +
+ Loss Prevention · 4 active detection rules · T1657 — Financial Theft + MITRE ATT&CK · Impact +
+ + {selectedInc && setSelectedInc(null)} />} +
+ ); +} diff --git a/frontend/src/pages/Portal.jsx b/frontend/src/pages/Portal.jsx index 7af1508..c3f4fbc 100644 --- a/frontend/src/pages/Portal.jsx +++ b/frontend/src/pages/Portal.jsx @@ -11,7 +11,7 @@ const MODULES = [ { id:'vuln', icon:Search, name:'Vulnerability Scanner', desc:'Web and network security assessment with severity-ranked findings.', color:'#D97706', active:true }, { id:'compliance', icon:FileCheck, name:'Compliance Centre', desc:'UK regulatory deadline tracking — ICO, NCSC, PCI DSS, and NIS2.', color:'#16A34A', active:true }, { id:'rules', icon:BookOpen, name:'Detection Rules', desc:'KQL rule management, MITRE ATT&CK coverage, and rule performance.', color:'#2563EB', active:true }, - { id:null, icon:ShoppingCart, name:'Loss Prevention', desc:'Financial fraud detection, void/refund abuse, and store risk scoring.', color:'#5A6478', active:false }, + { id:'lp', icon:ShoppingCart, name:'Loss Prevention', desc:'Financial fraud detection, void/refund abuse, and store risk scoring.', color:'#F97316', active:true }, { id:null, icon:Link2, name:'ChainShield', desc:'Supply chain and third-party supplier compromise detection.', color:'#5A6478', active:false }, ]; From e5b910ab3e577d443d0e6648e3edc93bd3a36627 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Tue, 23 Jun 2026 18:30:07 +0000 Subject: [PATCH 2/2] fix(lp-frontend): add LP_INCIDENTS and LP_STORE_RISK to data.js to fix Vercel build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vercel builds this branch in isolation — importing LP_INCIDENTS and LP_STORE_RISK from LossPrevention.jsx failed at bundle time because those exports were only present on the feature/lp-demo-data branch (PR #150), not here. Adding them directly to data.js on this branch so the preview deployment can build successfully. --- frontend/src/lib/data.js | 254 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/frontend/src/lib/data.js b/frontend/src/lib/data.js index 53eeaf5..d67cd25 100644 --- a/frontend/src/lib/data.js +++ b/frontend/src/lib/data.js @@ -487,6 +487,260 @@ export const COMPLIANCE_REGULATIONS = [ { id: 'NIS2', name: 'NIS2 Directive (UK equivalent)', deadline: '24 hours', authority: 'NCSC', threshold: 'Incidents affecting network and information systems', status: 'Monitored' }, ]; +export const LP_INCIDENTS = [ + { + id: 'LP-INC-001', + title: 'High-Volume Void Abuse — Store-031 Cashier', + severity: 'High', + tactic: 'Impact', + technique: 'T1657', + status: 'Investigating', + detectedAt: '2026-06-21T11:42:00Z', + mttd: 3, + ruleId: 'LP-001', + detectionSignal: 'HighVolumeVoidRefund', + storeId: 'Store-031', + operatorId: 'EMP-2204', + terminalId: 'POS-031-03', + transactionCount: 14, + totalValueGBP: 847.50, + hasManagerOverride: false, + riskScore: 80, + description: 'Cashier EMP-2204 processed 14 void/refund transactions totalling £847.50 in a 30-minute window at Store-031 — exceeding the threshold of 10 per window with no manager override recorded.', + timeline: [ + { time: '11:09', event: 'First void transaction processed by EMP-2204 at POS-031-03' }, + { time: '11:39', event: '14th void in 30-minute window — HighVolumeVoidRefund threshold crossed' }, + { time: '11:42', event: 'Alert triggered — lp-incident-response playbook invoked' }, + { time: '11:44', event: 'Store manager notified; POS permissions suspended pending review' }, + ], + affectedEntities: ['EMP-2204', 'POS-031-03', 'Store-031 till ledger'], + autoDefence: ['POS operator account suspended', 'Store manager alerted', 'CCTV timestamp flagged for review'], + recommendations: ['Reconcile void transactions against customer receipts', 'Require manager PIN for all voids above £20', 'Begin HR investigation if reconciliation fails'], + estimatedLossGBP: 847.50, + }, + { + id: 'LP-INC-002', + title: 'Critical Value Void — No Override — Store-017', + severity: 'Critical', + tactic: 'Impact', + technique: 'T1657', + status: 'Active', + detectedAt: '2026-06-21T14:08:00Z', + mttd: 1, + ruleId: 'LP-001', + detectionSignal: 'HighValueVoidNoOverride', + storeId: 'Store-017', + operatorId: 'EMP-0891', + terminalId: 'POS-017-01', + transactionCount: 3, + totalValueGBP: 1240.00, + hasManagerOverride: false, + riskScore: 95, + description: 'Three high-value void transactions totalling £1,240 processed at Store-017 POS-017-01 by EMP-0891 with no manager override recorded. Total exceeds the £500 Critical threshold.', + timeline: [ + { time: '14:01', event: 'First high-value void — £420.00 — no override on POS-017-01' }, + { time: '14:05', event: 'Second void — £380.00' }, + { time: '14:07', event: 'Third void — £440.00 — total reaches £1,240' }, + { time: '14:08', event: 'Critical alert fired — HighValueVoidNoOverride, RiskScore 95' }, + ], + affectedEntities: ['EMP-0891', 'POS-017-01', 'Store-017 refund ledger'], + autoDefence: ['Operator account suspended immediately', 'Manager PIN enforcement activated at terminal', 'Incident escalated to Loss Prevention team'], + recommendations: ['Conduct same-day investigation and reconciliation', 'Review 7-day void history for EMP-0891', 'Preserve CCTV footage for the 14:00–14:10 window'], + estimatedLossGBP: 1240.00, + }, + { + id: 'LP-INC-003', + title: 'Bulk Gift Card Activation — 12 Cards in 40 Minutes', + severity: 'High', + tactic: 'Impact', + technique: 'T1657', + status: 'Investigating', + detectedAt: '2026-06-21T10:22:00Z', + mttd: 6, + ruleId: 'LP-002', + detectionSignal: 'BulkGiftCardActivation', + storeId: 'Store-008', + operatorId: 'EMP-1155', + terminalId: 'POS-008-02', + activationCount: 12, + totalValueGBP: 600.00, + riskScore: 80, + description: 'EMP-1155 activated 12 gift cards totalling £600 in a 40-minute window at Store-008 — more than double the BulkActivationThresh of 5. No matching customer loyalty transactions recorded.', + timeline: [ + { time: '09:42', event: 'First gift card activated by EMP-1155 on POS-008-02' }, + { time: '10:01', event: 'Fifth card activated — BulkActivationThresh reached' }, + { time: '10:22', event: 'Twelfth card activated — alert triggered' }, + { time: '10:28', event: 'Gift card activation capability suspended at terminal' }, + ], + affectedEntities: ['EMP-1155', 'POS-008-02', '12 gift card accounts (GC-batch-9821xx)'], + autoDefence: ['Gift card activation suspended at POS-008-02', 'Card batch flagged in gift card system', 'Store manager notified'], + recommendations: ['Freeze the 12 activated cards pending investigation', 'Verify activation purpose with EMP-1155 and store manager', 'Cross-check cards against customer purchase records'], + estimatedLossGBP: 600.00, + }, + { + id: 'LP-INC-004', + title: 'Rapid Cross-Store Gift Card Redemption — Organised Fraud', + severity: 'Critical', + tactic: 'Impact', + technique: 'T1657', + status: 'Active', + detectedAt: '2026-06-20T16:14:00Z', + mttd: 4, + ruleId: 'LP-002', + detectionSignal: 'RapidCrossChannelRedemption', + storeId: 'Store-003', + operatorId: 'EMP-0312', + terminalId: 'POS-003-01', + activationCount: 8, + totalValueGBP: 480.00, + minutesSinceActivation: 18, + riskScore: 90, + description: '8 gift cards activated at Store-003 were redeemed online within 18 minutes of activation — well within the 30-minute cross-channel threshold. Consistent with organised gift card fraud using stolen card numbers.', + timeline: [ + { time: '15:52', event: '8 gift cards activated at Store-003 POS-003-01 by EMP-0312' }, + { time: '16:10', event: 'All 8 cards redeemed via online channel within 18 minutes of activation' }, + { time: '16:14', event: 'RapidCrossChannelRedemption alert fired — Critical, RiskScore 90' }, + { time: '16:15', event: 'Cards suspended; online redemption channel flagged' }, + ], + affectedEntities: ['EMP-0312', 'POS-003-01', '8 gift card accounts', 'Online redemption API'], + autoDefence: ['Affected cards frozen', 'Online channel velocity limit applied', 'Fraud team alerted'], + recommendations: ['Coordinate with online fraud team to trace redemption session', 'Investigate whether EMP-0312 acted alone or with an accomplice', 'Consider law enforcement referral if organised crime suspected'], + estimatedLossGBP: 480.00, + }, + { + id: 'LP-INC-005', + title: 'Sweethearting — Cashier Serving Same Customer 9 Times at 38% Discount', + severity: 'High', + tactic: 'Impact', + technique: 'T1657', + status: 'Investigating', + detectedAt: '2026-06-21T06:00:00Z', + mttd: 22, + ruleId: 'LP-003', + detectionSignal: 'RepeatHighDiscountRelationship', + storeId: 'Store-042', + operatorId: 'EMP-3301', + loyaltyCardId: 'LC-88821934', + transactionCount: 9, + avgDiscountRate: 0.38, + totalDiscountGBP: 314.20, + avgBasketGBP: 36.84, + riskScore: 75, + description: 'Cashier EMP-3301 served loyalty customer LC-88821934 nine times over 7 days with an average discount of 38% — substantially above the 30% threshold. No loyalty programme entitlement found for the customer profile.', + timeline: [ + { time: 'Day 1', event: 'First transaction flagged — 41% discount applied by EMP-3301' }, + { time: 'Days 2–6', event: '8 further transactions — all with discounts between 32% and 44%' }, + { time: 'Day 7 06:00', event: '7-day rolling analysis triggered RepeatHighDiscountRelationship alert' }, + { time: 'Day 7 06:22', event: 'Loss prevention manager notified; HR review scheduled' }, + ], + affectedEntities: ['EMP-3301', 'LC-88821934 (loyalty customer)', 'Store-042 discount ledger'], + autoDefence: ['Discount cap applied to EMP-3301 terminal pending review', 'LP manager alerted', 'Transaction history exported for HR'], + recommendations: ['Conduct full 30-day discount audit for EMP-3301', 'Cross-check LC-88821934 loyalty entitlement', 'Review CCTV to confirm whether all items were scanned'], + estimatedLossGBP: 314.20, + }, + { + id: 'LP-INC-006', + title: 'Discount Concentration — Cashier Responsible for 52% of Terminal Discounts', + severity: 'High', + tactic: 'Impact', + technique: 'T1657', + status: 'Investigating', + detectedAt: '2026-06-20T06:00:00Z', + mttd: 31, + ruleId: 'LP-003', + detectionSignal: 'HighDiscountConcentrationAtTerminal', + storeId: 'Store-019', + operatorId: 'EMP-0774', + terminalId: 'POS-019-04', + discountShare: 0.52, + totalDiscountGBP: 892.60, + riskScore: 70, + description: 'Cashier EMP-0774 was responsible for 52% of all discounts issued at POS-019-04 over the past 7 days — more than double the DiscountShareThresh of 40%. Peer comparison shows the terminal average is 11% per operator.', + timeline: [ + { time: 'Day 1–7', event: 'EMP-0774 discount share accumulates to 52% of POS-019-04 total' }, + { time: 'Day 7 06:00', event: '7-day analysis triggers HighDiscountConcentrationAtTerminal signal' }, + { time: 'Day 7 06:31', event: 'LP manager notified; discount activity review initiated' }, + ], + affectedEntities: ['EMP-0774', 'POS-019-04', 'Store-019 discount ledger'], + autoDefence: ['LP manager alerted', 'Discount log exported for peer comparison', 'Terminal flagged for enhanced monitoring'], + recommendations: ['Compare EMP-0774 basket values against terminal peers', 'Audit discount transactions for items not appearing on receipt', 'Consider covert observation if pattern continues'], + estimatedLossGBP: 892.60, + }, + { + id: 'LP-INC-007', + title: 'Ghost Employee — After-Hours POS Access — Store-007', + severity: 'Critical', + tactic: 'Impact', + technique: 'T1657', + status: 'Active', + detectedAt: '2026-06-21T02:31:00Z', + mttd: 2, + ruleId: 'LP-004', + detectionSignal: 'GhostEmployeeAfterHours', + storeId: 'Store-007', + operatorId: 'EMP-9901', + terminalId: 'POS-007-02', + transactionCount: 4, + totalValueGBP: 612.00, + hourOfDay: 2, + riskScore: 92, + description: 'Operator EMP-9901 processed 4 transactions totalling £612 at Store-007 at 02:31 AM — outside trading hours (06:00–22:00). No daytime shift record exists for EMP-9901 in the past 24 hours, matching the GhostEmployee pattern.', + timeline: [ + { time: '02:27', event: 'EMP-9901 logs into POS-007-02 — no active shift record in system' }, + { time: '02:31', event: '4 transactions processed totalling £612.00' }, + { time: '02:31', event: 'GhostEmployeeAfterHours alert fired — Critical, RiskScore 92' }, + { time: '02:33', event: 'Terminal locked remotely; security team alerted' }, + ], + affectedEntities: ['EMP-9901', 'POS-007-02', 'Store-007 till'], + autoDefence: ['Terminal remotely locked', 'Security team dispatched', 'EMP-9901 credential suspended'], + recommendations: ['Contact on-site security and review CCTV immediately', 'Verify whether EMP-9901 badge access records confirm physical presence', 'Consider law enforcement notification if unauthorised entry is confirmed'], + estimatedLossGBP: 612.00, + }, + { + id: 'LP-INC-008', + title: 'Closed-Store Terminal Burst — 5 Transactions Outside Hours', + severity: 'Critical', + tactic: 'Impact', + technique: 'T1657', + status: 'Investigating', + detectedAt: '2026-06-20T23:58:00Z', + mttd: 4, + ruleId: 'LP-004', + detectionSignal: 'ClosedStoreTerminalBurst', + storeId: 'Store-022', + operatorId: 'EMP-4417', + terminalId: 'POS-022-01', + transactionCount: 5, + totalValueGBP: 388.75, + hourOfDay: 23, + riskScore: 92, + description: '5 POS transactions processed at POS-022-01 in a 30-minute window starting at 23:50 — after the 22:00 store close. The terminal burst of 5 exceeds the ClosedStoreTerminalBurst threshold of 3.', + timeline: [ + { time: '23:50', event: 'First after-hours transaction on POS-022-01' }, + { time: '23:54', event: 'Third transaction — TerminalBurstThresh crossed' }, + { time: '23:58', event: 'ClosedStoreTerminalBurst alert — Critical, RiskScore 92' }, + { time: '00:02', event: 'Store manager contacted; badge access audit initiated' }, + ], + affectedEntities: ['EMP-4417', 'POS-022-01', 'Store-022 after-hours log'], + autoDefence: ['Terminal flagged; manager notified', 'Badge access log pulled for Store-022', 'Activity flagged for LP review'], + recommendations: ['Correlate transactions with badge entry records', 'Review CCTV footage at store entrance from 23:40 onwards', 'Determine if authorised (stock count) or investigate as till manipulation'], + estimatedLossGBP: 388.75, + }, +]; + +export const LP_STORE_RISK = [ + { storeId: 'Store-007', storeName: 'Oxford Street', riskScore: 92, openIncidents: 1, highestSignal: 'GhostEmployeeAfterHours', lastIncident: '2026-06-21T02:31:00Z', trend: 'up' }, + { storeId: 'Store-017', storeName: 'Canary Wharf', riskScore: 88, openIncidents: 1, highestSignal: 'HighValueVoidNoOverride', lastIncident: '2026-06-21T14:08:00Z', trend: 'up' }, + { storeId: 'Store-003', storeName: 'Westfield Strat.', riskScore: 84, openIncidents: 1, highestSignal: 'RapidCrossChannelRedemption', lastIncident: '2026-06-20T16:14:00Z', trend: 'up' }, + { storeId: 'Store-022', storeName: 'Bluewater', riskScore: 80, openIncidents: 1, highestSignal: 'ClosedStoreTerminalBurst', lastIncident: '2026-06-20T23:58:00Z', trend: 'stable'}, + { storeId: 'Store-031', storeName: 'Trafford Centre', riskScore: 74, openIncidents: 1, highestSignal: 'HighVolumeVoidRefund', lastIncident: '2026-06-21T11:42:00Z', trend: 'stable'}, + { storeId: 'Store-042', storeName: 'Meadowhall', riskScore: 68, openIncidents: 1, highestSignal: 'RepeatHighDiscountRelationship', lastIncident: '2026-06-21T06:00:00Z', trend: 'up' }, + { storeId: 'Store-019', storeName: 'Bullring', riskScore: 62, openIncidents: 1, highestSignal: 'HighDiscountConcentrationAtTerminal', lastIncident: '2026-06-20T06:00:00Z', trend: 'stable'}, + { storeId: 'Store-008', storeName: 'Brent Cross', riskScore: 58, openIncidents: 1, highestSignal: 'BulkGiftCardActivation', lastIncident: '2026-06-21T10:22:00Z', trend: 'down' }, + { storeId: 'Store-011', storeName: 'Lakeside', riskScore: 28, openIncidents: 0, highestSignal: null, lastIncident: null, trend: 'stable'}, + { storeId: 'Store-055', storeName: 'Arndale', riskScore: 21, openIncidents: 0, highestSignal: null, lastIncident: null, trend: 'down' }, +]; + export const SUBMISSION_HISTORY = [ { id: 'SUB-001', date: '2026-05-22T10:30:00Z', incident: 'INC-2801', regulation: 'UK GDPR', authority: 'ICO', status: 'Submitted', refNo: 'ICO-2026-051822' }, { id: 'SUB-002', date: '2026-05-22T08:15:00Z', incident: 'INC-2801', regulation: 'CSR Bill', authority: 'NCSC', status: 'Acknowledged', refNo: 'NCSC-IR-2026-4471' },