From 958d40996c526199e549e16c6731a3e5b0895a54 Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:26:42 +0530 Subject: [PATCH 1/7] Update redis_handler.py Add redis support for rejected association events --- dicom_server/core/redis_handler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dicom_server/core/redis_handler.py b/dicom_server/core/redis_handler.py index fcad7900..0107b0af 100644 --- a/dicom_server/core/redis_handler.py +++ b/dicom_server/core/redis_handler.py @@ -3,6 +3,11 @@ class RedisClient(IRedisService): + def add_security_event(self, event): + key = "dicom:events:assoc_rejected" + payload = json.dumps(event) + self.redis_client.rpush(key, payload) + self.redis_client.ltrim(key, -10000, -1) def __init__(self, app_logger, exceptions_logger, redis_client): @@ -88,3 +93,4 @@ def update_files_integrity_state(self, changed_files): self.exceptions_logger.exception( "Unexpected error while adding integrity check identifier" ) + From cd936fb48d45031daeeb4938b388a73cfd7e6c8c Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:27:40 +0530 Subject: [PATCH 2/7] Update dicom_session_manager.py Add session collector method to log rejected associations --- dicom_server/core/dicom_session_manager.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dicom_server/core/dicom_session_manager.py b/dicom_server/core/dicom_session_manager.py index fc828396..0fef53ae 100644 --- a/dicom_server/core/dicom_session_manager.py +++ b/dicom_server/core/dicom_session_manager.py @@ -9,6 +9,18 @@ class SessionCollector(ISessionCollector): + + def record_rejected_assoc(self, ip, calling_ae, called_ae, reason): + event = { + "event": "assoc_rejected", + "timestamp": datetime.utcnow().isoformat(), + "ip": ip, + "calling_ae": calling_ae.decode(errors="ignore"), + "called_ae": called_ae.decode(errors="ignore"), + "reason": reason, + } + self.redis_client.add_security_event(event) + @inject def __init__( self, @@ -176,3 +188,4 @@ def session_locked(self): def set_session_id(self, s_id): self.session_info[sk.SESSION_ID.key] = s_id + From c96657ef1ebc29849cf260e5ada003152d080169 Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:28:17 +0530 Subject: [PATCH 3/7] Update dicom_session_service.py Expose record_rejected_assoc in ISessionCollector --- dicom_server/services/dicom_session_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dicom_server/services/dicom_session_service.py b/dicom_server/services/dicom_session_service.py index 9cd5a138..c615f0fa 100644 --- a/dicom_server/services/dicom_session_service.py +++ b/dicom_server/services/dicom_session_service.py @@ -3,6 +3,10 @@ class ISessionCollector(ABC): + @abstractmethod + def record_rejected_assoc(self, ip, calling_ae, called_ae, reason): + pass + @abstractmethod def session_started(self, ip, port, version_name) -> None: """Start the DICOM session on association request recieved""" @@ -37,3 +41,4 @@ def session_locked(self) -> bool: def set_session_id(self, session_id) -> None: """Set an identifier to the current session""" pass + From ad242d4e807b9a055153b1fd8b35e3c873742679 Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:31:00 +0530 Subject: [PATCH 4/7] Update dicom_handlers.py Log all association attempts for security monitoring (fix #68) --- dicom_server/core/dicom_handlers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dicom_server/core/dicom_handlers.py b/dicom_server/core/dicom_handlers.py index ee50d198..dd7a0ffc 100644 --- a/dicom_server/core/dicom_handlers.py +++ b/dicom_server/core/dicom_handlers.py @@ -28,7 +28,6 @@ def __init__( def handle_assoc(self, event): try: - version_name = ( str(event.assoc.requestor.implementation_version_name) if event.assoc.requestor.implementation_version_name @@ -36,6 +35,18 @@ def handle_assoc(self, event): ) ip = str(event.assoc.requestor.address) port = event.assoc.requestor.port + assoc = event.assoc + calling_ae = assoc.requestor.ae_title.strip() + called_ae = assoc.requestor.requested_ae_title.strip() + ip_addr = assoc.requestor.address + + try: + self.event_collector.record_rejected_assoc( + ip_addr, calling_ae, called_ae, "ASSOC_ATTEMPT" + ) + except Exception: + pass + self.event_collector.session_started(ip, port, version_name) except Exception as e: self.exceptions_logger.exception( @@ -311,3 +322,4 @@ def get_matching_instances(self, event, instances): ] return matching + From 4aa8c9132363759ea446a5a484d248618232a4ad Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:40:25 +0530 Subject: [PATCH 5/7] Update dicom_handlers.py Log all association attempts for security monitoring (fix #68) --- dicom_server/core/dicom_handlers.py | 48 ++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/dicom_server/core/dicom_handlers.py b/dicom_server/core/dicom_handlers.py index dd7a0ffc..14716779 100644 --- a/dicom_server/core/dicom_handlers.py +++ b/dicom_server/core/dicom_handlers.py @@ -28,27 +28,46 @@ def __init__( def handle_assoc(self, event): try: - version_name = ( - str(event.assoc.requestor.implementation_version_name) - if event.assoc.requestor.implementation_version_name - else "N/A" - ) - ip = str(event.assoc.requestor.address) - port = event.assoc.requestor.port assoc = event.assoc calling_ae = assoc.requestor.ae_title.strip() called_ae = assoc.requestor.requested_ae_title.strip() ip_addr = assoc.requestor.address - try: - self.event_collector.record_rejected_assoc( - ip_addr, calling_ae, called_ae, "ASSOC_ATTEMPT" - ) - except Exception: - pass + # CALLED AE validation + if called_ae not in ALLOWED_AE_TITLES: + try: + self.event_collector.record_rejected_assoc( + ip_addr, calling_ae, called_ae, "INVALID_CALLED_AE" + ) + except Exception: + pass + + assoc.reject(result=0x01, source=0x01, reason=0x07) + return + + # CALLING AE validation + if calling_ae not in ALLOWED_AE_TITLES: + try: + self.event_collector.record_rejected_assoc( + ip_addr, calling_ae, called_ae, "INVALID_CALLING_AE" + ) + except Exception: + pass + assoc.reject(result=0x01, source=0x01, reason=0x03) + return + + # Only valid associations reach here + version_name = ( + str(assoc.requestor.implementation_version_name) + if assoc.requestor.implementation_version_name + else "N/A" + ) + ip = str(ip_addr) + port = assoc.requestor.port self.event_collector.session_started(ip, port, version_name) - except Exception as e: + + except Exception: self.exceptions_logger.exception( "Unexpected error while handling association" ) @@ -323,3 +342,4 @@ def get_matching_instances(self, event, instances): return matching + From 56215112b11e6716ecb019e9563c637b910b56de Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:46:06 +0530 Subject: [PATCH 6/7] Update dicom_session_manager.py Fix duplicate record_rejected_assoc and use Redis injection --- dicom_server/core/dicom_session_manager.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dicom_server/core/dicom_session_manager.py b/dicom_server/core/dicom_session_manager.py index 0fef53ae..f8cf0250 100644 --- a/dicom_server/core/dicom_session_manager.py +++ b/dicom_server/core/dicom_session_manager.py @@ -9,8 +9,13 @@ class SessionCollector(ISessionCollector): - - def record_rejected_assoc(self, ip, calling_ae, called_ae, reason): + @inject + def record_rejected_assoc(self, ip, calling_ae, called_ae, reason, + redis: IRedisService = None): + + if not redis: + return + event = { "event": "assoc_rejected", "timestamp": datetime.utcnow().isoformat(), @@ -19,7 +24,9 @@ def record_rejected_assoc(self, ip, calling_ae, called_ae, reason): "called_ae": called_ae.decode(errors="ignore"), "reason": reason, } - self.redis_client.add_security_event(event) + + redis.add_security_event(event) + @inject def __init__( @@ -189,3 +196,4 @@ def session_locked(self): def set_session_id(self, s_id): self.session_info[sk.SESSION_ID.key] = s_id + From 71d17e9fde56d9dc8243a980e27b9e1e291d0404 Mon Sep 17 00:00:00 2001 From: Nishant <111688531+nishantxscooby@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:16:28 +0530 Subject: [PATCH 7/7] Update dicom_session_manager.py --- dicom_server/core/dicom_session_manager.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/dicom_server/core/dicom_session_manager.py b/dicom_server/core/dicom_session_manager.py index f8cf0250..fceb45c2 100644 --- a/dicom_server/core/dicom_session_manager.py +++ b/dicom_server/core/dicom_session_manager.py @@ -15,18 +15,17 @@ def record_rejected_assoc(self, ip, calling_ae, called_ae, reason, if not redis: return - - event = { - "event": "assoc_rejected", - "timestamp": datetime.utcnow().isoformat(), - "ip": ip, - "calling_ae": calling_ae.decode(errors="ignore"), - "called_ae": called_ae.decode(errors="ignore"), - "reason": reason, - } - - redis.add_security_event(event) - + try: + event = { + "timestamp": datetime.utcnow().isoformat(), + "ip": str(ip), + "calling_ae": calling_ae.decode(errors="ignore") if isinstance(calling_ae, bytes) else str(calling_ae), + "called_ae": called_ae.decode(errors="ignore") if isinstance(called_ae, bytes) else str(called_ae), + "reason": reason, + } + redis.add_security_event(event) + except Exception: + self.exceptions_logger.exception("Failed to record rejected association to Redis") @inject def __init__( @@ -197,3 +196,4 @@ def set_session_id(self, s_id): self.session_info[sk.SESSION_ID.key] = s_id +