diff --git a/dicom_server/core/dicom_handlers.py b/dicom_server/core/dicom_handlers.py index ee50d198..14716779 100644 --- a/dicom_server/core/dicom_handlers.py +++ b/dicom_server/core/dicom_handlers.py @@ -28,16 +28,46 @@ def __init__( def handle_assoc(self, event): try: + assoc = event.assoc + calling_ae = assoc.requestor.ae_title.strip() + called_ae = assoc.requestor.requested_ae_title.strip() + ip_addr = assoc.requestor.address + + # 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(event.assoc.requestor.implementation_version_name) - if event.assoc.requestor.implementation_version_name + str(assoc.requestor.implementation_version_name) + if assoc.requestor.implementation_version_name else "N/A" ) - ip = str(event.assoc.requestor.address) - port = event.assoc.requestor.port + 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" ) @@ -311,3 +341,5 @@ def get_matching_instances(self, event, instances): ] return matching + + diff --git a/dicom_server/core/dicom_session_manager.py b/dicom_server/core/dicom_session_manager.py index fc828396..fceb45c2 100644 --- a/dicom_server/core/dicom_session_manager.py +++ b/dicom_server/core/dicom_session_manager.py @@ -9,6 +9,24 @@ class SessionCollector(ISessionCollector): + @inject + def record_rejected_assoc(self, ip, calling_ae, called_ae, reason, + redis: IRedisService = None): + + if not redis: + return + 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__( self, @@ -176,3 +194,6 @@ def session_locked(self): def set_session_id(self, s_id): self.session_info[sk.SESSION_ID.key] = s_id + + + 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" ) + 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 +