[docs]classBaseObservation:"""Per-agent partial view of the network."""def__init__(self,agent_id:str):self.agent_id=agent_idself.visible_hosts={}self.active_sessions=[]self.objective_vector=np.zeros(5,dtype=np.float32)self.network_telemetry={}self.siem_alerts=[]
[docs]defupdate_from_state(self,global_state,_action_effects:list):"""Filter ``global_state`` down to what this agent can observe."""is_blue='blue'inself.agent_id.lower()is_commander='commander'inself.agent_id.lower()is_operator='operator'inself.agent_id.lower()ifglobal_state:known_ips=global_state.agent_knowledge.get(self.agent_id,set())foripinknown_ips:host=global_state.all_hosts.get(ip)ifhostisNone:continueifis_blue:self.visible_hosts[ip]={'state':'unknown','status':host.status,'decoy':host.decoy,}else:self.visible_hosts[ip]={'state':'compromised'ifhost.privilegein('User','Root')else'clean','status':host.status,'decoy':'unknown',}ifis_commanderoris_blue:buffer=getattr(global_state,'siem_log_buffer',None)or[]current_tick=getattr(global_state,'current_tick',0)forloginbuffer:arrival=log.get('arrival_tick',0)ifisinstance(log,dict)else0ifarrival<=current_tick:self.siem_alerts.append(log)self.network_telemetry['global_alert_level']=min(len(self.siem_alerts)/20.0,1.0)ifglobal_state:isolated_subnets=len({h.subnet_cidrforhinglobal_state.all_hosts.values()ifh.status=='isolated'})else:isolated_subnets=0self.network_telemetry['total_isolated_subnets']=isolated_subnetsself.network_telemetry['active_alerts']=len(self.siem_alerts)ifis_operator:self.objective_vector[2]=1.0
[docs]defto_numpy(self,max_size:int=256)->np.ndarray:"""Pack observation into a fixed-size float32 vector for the agent."""vector=np.zeros(max_size,dtype=np.float32)idx=0scalars=[]if'global_alert_level'inself.network_telemetry:scalars.append(self.network_telemetry['global_alert_level'])if'total_isolated_subnets'inself.network_telemetry:scalars.append(self.network_telemetry['total_isolated_subnets']/10.0)if'active_alerts'inself.network_telemetry:scalars.append(min(self.network_telemetry['active_alerts']/20.0,1.0))forvalinscalars+list(self.objective_vector):ifidx>=max_size:returnvectorvector[idx]=float(val)idx+=1foripinsorted(self.visible_hosts.keys()):ifidx+2>=max_size:breakdata=self.visible_hosts[ip]state_val=(1.0ifdata.get('state')=='compromised'else-1.0ifdata.get('state')=='clean'else0.0)vector[idx]=float(ip.split('.')[-1])/255.0vector[idx+1]=state_validx+=2returnvector