From 3f7c82ed943cfe0dac557b62b45a10a5e89f2261 Mon Sep 17 00:00:00 2001 From: clearlotus-git Date: Wed, 5 Nov 2025 10:58:02 -0500 Subject: [PATCH] Complete Phase 2: Event correlation and adaptive responses --- .gitignore | 4 ++ logs/events_log.jsonl | 45 +++++++++++++ sim/sensor_simulator.py | 43 ++++++++++++ .../__pycache__/main.cpython-311.pyc | Bin 3660 -> 4891 bytes src/bridge_core/main.py | 31 +++++++-- src/intelligence/correlation_engine.py | 62 ++++++++++++++++++ 6 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 sim/sensor_simulator.py create mode 100644 src/intelligence/correlation_engine.py diff --git a/.gitignore b/.gitignore index e69de29..2bb5881 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +__pycache__/ +*.pyc +logs/ diff --git a/logs/events_log.jsonl b/logs/events_log.jsonl index 78df9ab..08a5d10 100644 --- a/logs/events_log.jsonl +++ b/logs/events_log.jsonl @@ -1 +1,46 @@ {"timestamp": "2025-11-05T04:49:15.063275+00:00", "source": "camera_node_1", "type": "intrusion", "details": {"zone": "loading_bay", "confidence": 0.87}} +{"timestamp": "2025-11-05T15:27:23.114703+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:27:35.974145+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:27:44.733306+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:28:22.105045+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:28:40.150320+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:28:49.425947+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:29:36.701314+00:00", "source": "camA", "type": "intrusion", "details": {"zone": "lobby"}} +{"timestamp": "2025-11-05T15:48:20.214370+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "MainGate", "confidence": 0.83}} +{"timestamp": "2025-11-05T15:48:24.273159+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "MainGate", "confidence": 0.99}} +{"timestamp": "2025-11-05T15:48:29.280312+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "MainGate", "confidence": 0.78}} +{"timestamp": "2025-11-05T15:48:32.298760+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "ServerRoom", "confidence": 0.96}} +{"timestamp": "2025-11-05T15:48:34.305271+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "MainGate", "confidence": 0.81}} +{"timestamp": "2025-11-05T15:48:36.318437+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "Warehouse", "confidence": 0.79}} +{"timestamp": "2025-11-05T15:48:39.328778+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "Warehouse", "confidence": 0.88}} +{"timestamp": "2025-11-05T15:48:42.336792+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "ServerRoom", "confidence": 0.96}} +{"timestamp": "2025-11-05T15:48:47.343460+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "MainGate", "confidence": 0.82}} +{"timestamp": "2025-11-05T15:48:49.350584+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "Warehouse", "confidence": 0.74}} +{"timestamp": "2025-11-05T15:48:53.355437+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "MainGate", "confidence": 0.9}} +{"timestamp": "2025-11-05T15:48:57.364895+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "ServerRoom", "confidence": 0.97}} +{"timestamp": "2025-11-05T15:49:00.368249+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "Lobby", "confidence": 0.97}} +{"timestamp": "2025-11-05T15:49:02.373533+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "ServerRoom", "confidence": 0.85}} +{"timestamp": "2025-11-05T15:49:04.381406+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "Lobby", "confidence": 0.99}} +{"timestamp": "2025-11-05T15:49:09.387040+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "MainGate", "confidence": 0.98}} +{"timestamp": "2025-11-05T15:49:13.392251+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "ServerRoom", "confidence": 0.84}} +{"timestamp": "2025-11-05T15:49:18.398145+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "Warehouse", "confidence": 0.82}} +{"timestamp": "2025-11-05T15:53:29.591057+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "ServerRoom", "confidence": 0.8}} +{"timestamp": "2025-11-05T15:53:32.597284+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "MainGate", "confidence": 0.86}} +{"timestamp": "2025-11-05T15:53:35.602629+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "MainGate", "confidence": 0.93}} +{"timestamp": "2025-11-05T15:53:40.612955+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "MainGate", "confidence": 0.86}} +{"timestamp": "2025-11-05T15:53:42.617961+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "Lobby", "confidence": 0.84}} +{"timestamp": "2025-11-05T15:53:47.627867+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "MainGate", "confidence": 0.89}} +{"timestamp": "2025-11-05T15:53:49.654096+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "ServerRoom", "confidence": 0.8}} +{"timestamp": "2025-11-05T15:53:54.663972+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "ServerRoom", "confidence": 0.92}} +{"timestamp": "2025-11-05T15:53:59.672196+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "Lobby", "confidence": 0.92}} +{"timestamp": "2025-11-05T15:54:04.685684+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "Warehouse", "confidence": 0.76}} +{"timestamp": "2025-11-05T15:54:07.695522+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "ServerRoom", "confidence": 0.89}} +{"timestamp": "2025-11-05T15:54:11.713202+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "Warehouse", "confidence": 0.9}} +{"timestamp": "2025-11-05T15:54:14.716615+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "ServerRoom", "confidence": 0.77}} +{"timestamp": "2025-11-05T15:54:16.725430+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "MainGate", "confidence": 0.87}} +{"timestamp": "2025-11-05T15:54:21.732386+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "Warehouse", "confidence": 0.81}} +{"timestamp": "2025-11-05T15:54:25.734890+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "Warehouse", "confidence": 0.71}} +{"timestamp": "2025-11-05T15:54:27.741517+00:00", "source": "simSensor-01", "type": "motion", "details": {"zone": "MainGate", "confidence": 0.88}} +{"timestamp": "2025-11-05T15:54:31.746401+00:00", "source": "simSensor-01", "type": "door_open", "details": {"zone": "MainGate", "confidence": 0.93}} +{"timestamp": "2025-11-05T15:54:34.749109+00:00", "source": "simSensor-01", "type": "access_denied", "details": {"zone": "ServerRoom", "confidence": 0.86}} +{"timestamp": "2025-11-05T15:54:39.753955+00:00", "source": "simSensor-01", "type": "intrusion", "details": {"zone": "Warehouse", "confidence": 0.85}} diff --git a/sim/sensor_simulator.py b/sim/sensor_simulator.py new file mode 100644 index 0000000..59ed083 --- /dev/null +++ b/sim/sensor_simulator.py @@ -0,0 +1,43 @@ +import requests +import json +import random +import time +import os +from dotenv import load_dotenv + +# -------- Load env vars -------- +load_dotenv() +API_KEY = os.getenv("API_KEY") +BRIDGE_URL = "http://127.0.0.1:8000/api/event" + +EVENT_TYPES = ["motion", "intrusion", "access_denied", "door_open"] +ZONES = ["Lobby", "Warehouse", "ServerRoom", "MainGate"] + +def send_event(event_type): + payload = { + "source": "simSensor-01", + "type": event_type, + "details": { + "zone": random.choice(ZONES), + "confidence": round(random.uniform(0.7, 0.99), 2) + } + } + headers = { + "Authorization": f"Bearer {API_KEY}", + "Content-Type": "application/json" + } + + try: + response = requests.post(BRIDGE_URL, headers=headers, data=json.dumps(payload)) + print(f"[SENT] {event_type} ({response.status_code})") + except Exception as e: + print(f"[ERROR] Could not reach Bridge: {e}") + +print("[INFO] SynAccel Sensor Simulator started. Press Ctrl+C to stop.") +try: + while True: + event = random.choice(EVENT_TYPES) + send_event(event) + time.sleep(random.randint(2, 5)) +except KeyboardInterrupt: + print("\n[INFO] Sensor simulation stopped.") diff --git a/src/bridge_core/__pycache__/main.cpython-311.pyc b/src/bridge_core/__pycache__/main.cpython-311.pyc index faab4bbdc3031fe337b5839f1d1cef2c6c7ab46a..620660bf9de10ee031c04fbd9375ca5ac9a155b2 100644 GIT binary patch delta 1994 zcma)7O>7fK6rT02ch^5_Y{z!�gH#k4!=m!e2yyK$^4_1qoW(N~S0>nXzkd>~y?| zO5`X=3UtRXz00Iwo<%p-(e! z=FR)wH*aRg&%cE4?s9#lD0T$$IPXY*Z=ZJsxytg_6je9v{LBzfYkgple z6m!jSxv6er+ad>k=rxJ^AjXgF#5b?|XC2u2Kv}c^0Qx#0000rY@U{o;EuLoZ2&B1@ zFv(#L2_`MrJ73;VlV3QWg?wB!Q%Yqy>k__N0JY=B!%?BiQ;binVH0ZpIhm4EI=$Qj$ri zWRy&iteSr~ZfUb9E=ZDELB%-g)9wgyYYP}H5C0ZUa6Wbt8e*!gu439L@qlPL+O#?G zgZ|yjW2%>E!?+O{ERDzJklIm)AhbE45r=J81csw6n=vI1BYhv+8^J)j^Z$^}hy{^; zUf{7~+JVrFr203a3X(P``yJviy4W=%7px@*+e*74H*>OfUEaap+-uVo<(^#y201um zQ`xnx3w9Q4SYdZvladdDUz@#)unQd9HXL`+Jz;C^HxD&XxeMsBSSJDo57_JzoARo7 z%x}KZg7}i(W`-0{$?~BHB4y&+P9$HfKMvX?G$+sG1+m zo!e4KXn>L9*E3 zGbC&96S)b>cBVm4hIJBrlZ_$$W1h=aFY`-=8mx!$1r$TT=2o^(SAzBflx)5EH?6;1U zrBFo*6^B-&c3o;;ll@QS(25)?c9i9CMGot77=m5x_r}XgS4HW%_Qo?wsUp!j!mS3{ z^^X1J!2TPKHN|tgvzRX{(TWnSB8#oB^zmwFdlhj?AE;TS@3wW-?YSd;D%}$n?4^U{ zV5}01-5)Lod&};9756^fY*n5G@DOj3nCdyB`;)1SwO7qvym;}d<*JChvq4D0Hp12P zjgxCmkM4cFxUU#rIQF!)d!@Cz+}c}d?S(~XOXrrZKKigWFFS`T&SBkb>=Ny|{YY`= z!X6l)-5+^yx;85d0~KLF7Y0^?;cuf$$!|Izb^b8<)AaK6kA=sD1G?ipYr^^b4)`|9 zT<1_fzuYej9hH^`Ina-f^o|_npBxrO2Bas)Inc2V?Y3j|?LmMW(nLc2D3iWKoy>~n z=~#C0EY{WK-!ZF`t_urie03o7^u|_|wSclF rc$%&$T2+aRba3y%^)p?Ztc^=MYU5`8oJW>mg=?-_Ht;n+WmW$_>1fa; delta 948 zcmZ8g&1(}u6rb6zY?Ixz`D!*HX@lC9wQ3Ox7Fw(pR0Q$jLC7U;JCin;Ch2TatSCL0 zgBQVc1O*j(ib8u33WD@jJc@WwEDM5vLd27ve6z9D^uhZL@BQAqk9|AOM?Yl3uS_$D zgnSK_F6*tZh2OWvmRp;t^@#5VAbJ}kv<2Fgd{ja-M*3*9ErB1jD;R+vlOPFg8@)_E z4va=j65j4V!X$uEXri}55t9Kz2a6JkG}U}kLRS!pHW7(Yy)8pY(5~by8sAJESpT0S z-kXvnny3u{TZcjUG>!9HIC*ujHxe3>XtLJ_4JD~XESf1QB-K=pWXclt2C~x|(*2#5 zl$A^qiZB(x#w^BnmAoo?w06?!1sqQc(h`t4fH7XtCN-bP@YmYS+^ArDIE?nxQiaev z>?Ba9`EVeK)BIduBBX)+0QBcuyt5K`h;<^9zI$jh%wGnN=_L3PeNQ~Z}PGVY4La=!S(dgkAB1uAN?i$I?Rb^9@|m=6BVKbeoFJ330F z_G4%E2NF+%8^M7Ap^O~|hKVitTk&ML<$r2Vsa4&& zr_5KYWxweoYx%9uELTaROwY6H2%e01CGJPSj*Kx(H4(}2YmtW3(fF6hH~ub~ok?Jr e2jE%5Ki-`1kALt(Mw0azCp*$Xq|Y(&EBpZj7{aCi diff --git a/src/bridge_core/main.py b/src/bridge_core/main.py index 9993e06..1ae0b49 100644 --- a/src/bridge_core/main.py +++ b/src/bridge_core/main.py @@ -7,6 +7,8 @@ from datetime import datetime, timezone import json from pathlib import Path +from src.intelligence.correlation_engine import correlate_events + # ---------- Load environment variables ---------- load_dotenv() @@ -46,6 +48,9 @@ class Event(BaseModel): def index(): return {"message": "SynAccel-Bridge API is running"} +recent_events = [] + + @app.post("/api/event") async def receive_event(event: Event, authorized: bool = Depends(verify_api_key)): """Receive and process a security or sensor event.""" @@ -59,13 +64,25 @@ async def receive_event(event: Event, authorized: bool = Depends(verify_api_key) logs_dir = Path("logs") logs_dir.mkdir(exist_ok=True) - log_file = logs_dir / "events_log.jsonl" with open(log_file, "a") as f: f.write(json.dumps(log_entry) + "\n") - - return { - "received": True, - "message": "Event logged successfully", - "data": event.dict() - } + + recent_events.append(log_entry) + if len(recent_events) > 20: + recent_events.pop(0) + + # --- 3. Run correlation analysis + alerts = correlate_events(recent_events) + + if alerts: + alerts_file = logs_dir / "alerts_log.jsonl" + with open(alerts_file, "a") as f: + for alert in alerts: + f.write(json.dumps(alert) + "\n") + for a in alerts: + print(f"[⚠] {a['severity'].upper()} | {a['message']}") + + # Normal response + print(f"[✔] Logged {event.type} from {event.source}") + return {"received": True, "alerts_triggered": len(alerts), "data": event.dict()} diff --git a/src/intelligence/correlation_engine.py b/src/intelligence/correlation_engine.py new file mode 100644 index 0000000..04c6207 --- /dev/null +++ b/src/intelligence/correlation_engine.py @@ -0,0 +1,62 @@ +from datetime import datetime, timezone, timedelta + +# A dictionary to remember when each source last triggered an alert +last_alert_time = {} + +def correlate_events(events): + """ + Analyze recent events for abnormal patterns. + Returns a list of generated alerts. + """ + alerts = [] + now = datetime.now(timezone.utc) + + # --- Parameters --- + INTRUSION_THRESHOLD = 3 # how many intrusions before alert + COOLDOWN_PERIOD = timedelta(seconds=60) # wait 60s before re-alerting same source + WINDOW = timedelta(seconds=30) # only look at events within last 30 s + + # --- Filter only recent events --- + recent_window = [ + e for e in events + if now - datetime.fromisoformat(e["timestamp"]) <= WINDOW + ] + + # --- Example 1: repeated intrusions from same source --- + intrusion_counts = {} + for e in recent_window: + if e["type"] == "intrusion": + intrusion_counts[e["source"]] = intrusion_counts.get(e["source"], 0) + 1 + + for src, count in intrusion_counts.items(): + if count >= INTRUSION_THRESHOLD: + # cooldown check + if src not in last_alert_time or now - last_alert_time[src] > COOLDOWN_PERIOD: + alerts.append({ + "timestamp": now.isoformat(), + "source": src, + "type": "intrusion_spike", + "severity": "high", + "message": f"{count} intrusion events from {src} within short timeframe" + }) + last_alert_time[src] = now # remember time of alert + + # --- Example 2: motion followed by access_denied from same source --- + for i in range(len(recent_window) - 1): + if ( + recent_window[i]["type"] == "motion" + and recent_window[i + 1]["type"] == "access_denied" + and recent_window[i]["source"] == recent_window[i + 1]["source"] + ): + src = recent_window[i]["source"] + if src not in last_alert_time or now - last_alert_time[src] > COOLDOWN_PERIOD: + alerts.append({ + "timestamp": now.isoformat(), + "source": src, + "type": "sequence_alert", + "severity": "medium", + "message": f"Motion followed by access_denied on {src}" + }) + last_alert_time[src] = now + + return alerts