← CCNP Security: SCOR core and operations
16 / 25 · 55 MIN

Telemetry: framing and evidence

Run a bounded local collector and distinguish transport, interpretation, retention and event correlation.

1. Define collection outcomes

An event passes through stages: generation, sending, receipt, parsing and retention in a queryable destination. Successful sendall does not prove every stage. The lab uses three synthetic messages and loopback-only sockets; lists it calls stored are in memory rather than durable storage. Run it with Python 3.13 and compare outcomes with the manifest without targeting a real SIEM. RFC 6587 has Historic status and describes legacy framing. The plaintext TCP exercise teaches message boundaries; it is not a recommendation to send production logs without protection. TLS and destination identity belong to a separate transport design.

2. Delimit bytes, not network calls

The first text has 64 characters but occupies 68 UTF-8 bytes, including its BOM marker. The octet-counting prefix measures payload bytes rather than characters or the prefix itself. The collector reads at most seven bytes per recv, accumulates fragments and emits a message only when the announced length arrives. Three messages may arrive fragmented or together; a recv call does not define a message. The second contains an internal newline. Splitting solely at newline would create another boundary, while length preserves content. This implementation does not parse or validate the full RFC 5424 schema: it tests byte framing.

3. Make failures visible

The parser rejects zero length, nondecimal prefixes, excessive headers and frames above its instructional 1024-byte bound. That value is not a universal syslog maximum. An incomplete body at EOF is marked truncated without inventing a complete message. In a second TCP connection, the collector receives and parses an event but deliberately omits retention. With UDP, all three datagrams arrive and only two enter the retained list. No network packet loss was induced: omission occurs after receipt. The result shows why send counts do not replace destination reconciliation or a persistence policy.

4. Correlate origin and time

For investigation, retain event time, receipt time, declared origin and transport identity when available. A TLS-authenticated relay may forward events from other hosts; its certificate alone does not prove that HOSTNAME was emitted by the named host. Unsynchronized clocks make timestamp ordering uncertain. The timeQuality element can communicate limitations but must reflect actual state rather than invented precision. Use identifiers and flow context to distinguish retransmissions, beginning/end events and separate occurrences. Do not remove everything with identical text: two legitimate denials may have exactly the same message.

5. Accept the pipeline using measurable criteria

In a SIEM integration, combine an identified synthetic event with destination search, field interpretation and availability delay. Measure queues, rejections, lag and free space, including an authorized interruption and recovery behavior. Capacity planning must reflect volume and retention, and sensitive data should be limited. In FTD connection events, the end usually adds session information; the beginning may matter for denial or timely detection. A long session without an end event does not yet prove broken collection. The lab does not validate Splunk, TLS syslog, throughput or disk. Summary: confirm destination outcomes and make gaps between transport and evidence explicit.

#!/usr/bin/env python3
"""Original bounded loopback framing lab; not a production syslog collector."""
import hashlib,json,platform,socket,threading
from pathlib import Path
MAX_FRAME=1024 # local teaching bound, not a protocol maximum
class Frames:
 def __init__(self):self.pending=b''self.messages=[]
 def feed(self,data):
 self.pending+=data
 while self.pending:
 cut=self.pending.find(b' ')
 if cut<0:
 if len(self.pending)>9:raise ValueError('header-too-long')
 break
 prefix=self.pending[:cut]
 if not prefix or prefix[:1]==b'0' or any(c<48 or c>57 for c in prefix):raise ValueError('invalid-length')
 if len(prefix)>9:raise ValueError('header-too-long')
 length=int(prefix)
 if length>MAX_FRAME:raise ValueError('frame-too-large')
 if len(self.pending)<cut+1+length:break
 self.messages.append(self.pending[cut+1:cut+1+length]);self.pending=self.pending[cut+1+length:]
 def finish(self):
 if self.pending:raise ValueError('truncated-frame')
 return self.messages

def frame(message):return str(len(message)).encode('ascii')+b' '+message

def tcp_exchange(payloads,discard=False):
 # No files or production addresses: listener bound to ephemeral loopback only.
 state={'stored':[],'received':[],'errors':[],'readSizes':[]}
 with socket.socketas listener:
 listener.bind(('127.0.0.1',0));listener.listen(1);listener.settimeout(4)
 def receive:
 try:
 with listener.accept[0]as conn:
 conn.settimeout(4);parser=Frames
 while True:
 part=conn.recv(7)
 if not part:break
 state['readSizes'].append(len(part));parser.feed(part)
 state['received']=parser.finish
 if not discard:state['stored']=list(state['received'])
 # stored means an in-memory teaching list, never durable storage.
 except Exception as exc:state['errors'].append(type(exc).__name__+': '+str(exc))
 thread=threading.Thread(target=receive);thread.start
 try:
 with socket.create_connection(listener.getsockname,timeout=4)as sender:
 for part in payloads:sender.sendall(part)
 sender.shutdown(socket.SHUT_WR)
 finally:thread.join(5)
 if thread.is_alive:raise RuntimeError('listener did not finish')
 if state['errors']:raise RuntimeError(state['errors'])
 return state

def main:
 checks=[]
 def check(label,condition):
 if not condition:raise AssertionError(label)
 checks.append(label)
 def rejected(label,payload,expected,finish=False):
 try:
 parser=Frames;parser.feed(payload)
 if finish:parser.finish
 except ValueError as e:check(label,str(e)==expected)
 else:raise AssertionError(label+' accepted')
 text=['<134>1 2026-10-07T10:00:00Z lab.test dr 1 EV01 - \ufeffação=permitida', '<132>1 2026-10-07T10:00:01Z lab.test dr 1 EV02 - \ufefflinha um\nlinha dois', '<131>1 2026-10-07T10:00:02Z lab.test dr 1 EV03 - \ufeffação=recusada']
 messages=[s.encode('utf-8')for s in text];stream=b''.join(map(frame,messages))
 check('UTF-8 byte count differs from character count',len(messages[0])>len(text[0]))
 check('frame prefix counts payload bytes',int(frame(messages[0]).split(b' ',1)[0])==len(messages[0]))
 parser=Frames
 for value in stream:parser.feed(bytes([value]))
 check('byte-at-a-time parser reconstructs three messages',parser.finish==messages)
 parser=Frames;parser.feed(stream)
 check('coalesced frames reconstruct three messages',parser.finish==messages)
 check('embedded newline stays inside second message',b'\n'in parser.messages[1]and len(parser.messages)==3)
 check('line splitting would create an extra boundary',len(messages[1].split(b'\n'))==2)
 for label,payload,error,finish in [('zero length rejected',b'0 x','invalid-length',False),('leading zero rejected',b'01 x','invalid-length',False),('negative length rejected',b'-1 x','invalid-length',False),('nondecimal length rejected',b'x hi','invalid-length',False),('oversize rejected before body',b'1025 ','frame-too-large',False),('header length bounded',b'1111111111','header-too-long',False),('incomplete body rejected at EOF',b'5 abc','truncated-frame',True),('incomplete prefix rejected at EOF',b'12','truncated-frame',True)]:rejected(label,payload,error,finish)
 parser=Frames;parser.feed(frame(b'x'*1024));check('configured maximum accepted',len(parser.finish[0])==1024)
 parser=Frames;parser.feed(b'');check('empty stream has no events',parser.finish==[])
 live=tcp_exchange([stream[:1],stream[1:5],stream[5:]])
 check('real TCP collector reconstructs all messages',live['received']==messages)
 check('collector in-memory list matches messages',live['stored']==messages)
 check('TCP receiver uses bounded reads',all(0<n<=7 for n in live['readSizes']))
 check('TCP message spans multiple recv calls',len(live['readSizes'])>3)
 check('UTF-8 retained after TCP framing',live['stored'][0].decode('utf-8')==text[0])
 check('embedded newline retained after TCP framing',live['stored'][1]==messages[1])
 discarded=tcp_exchange([frame(messages[0])],discard=True)
 check('sendall completed and receiver parsed discard case',discarded['received']==[messages[0]])
 check('parsed event can be intentionally unretained',discarded['stored']==[])
 with socket.socket(socket.AF_INET,socket.SOCK_DGRAM)as receiver, socket.socket(socket.AF_INET,socket.SOCK_DGRAM)as sender:
 receiver.bind(('127.0.0.1',0));receiver.settimeout(3);observed=[];kept=[];sent=[]
 for index,message in enumerate(messages):
 sent.append(sender.sendto(message,receiver.getsockname));data,_=receiver.recvfrom(2048);observed.append(data)
 if index!=1:kept.append(data) # Deliberate collector omission, not network loss.
 check('UDP sends return full datagram lengths',sent==list(map(len,messages)))
 check('three UDP datagrams actually received',observed==messages)
 check('collector deliberately retains only two UDP events',kept==[messages[0],messages[2]])
 check('UDP omission is visible against expected event set',messages[1]not in kept)
 check('no receiver threads remain',not any(t.name!='MainThread'and t.is_alivefor t in threading.enumerate))
 evidence={'passed':len(checks),'failed':0,'checks':checks,'python':platform.python_version,'scriptSHA256':hashlib.sha256(Path(__file__).read_bytes).hexdigest,'actualLoopbackTCP':True,'actualLoopbackUDP':True,'actualCiscoDevice':False,'actualIPsecTunnel':False,'tlsUsedInThisLab':False,'durableStorageTested':False,'networkPacketLossInjected':False,'collectorOmissionIntentional':True,'productionSyslogConformanceClaimed':False,'observations':{'messageByteLengths':list(map(len,messages)),'messageCharacterLengths':list(map(len,text)),'tcpReconstructed':len(live['received']),'tcpRetainedInMemory':len(live['stored']),'tcpDiscardCaseReceived':len(discarded['received']),'tcpDiscardCaseRetained':len(discarded['stored']),'udpReceived':len(observed),'udpRetainedInMemory':len(kept),'udpMissingMessageIds':['EV02'],'localMaxFrameBytes':MAX_FRAME},'cleanup':'All loopback sockets closed and receiver threads joined.'}
 print(json.dumps(evidence,ensure_ascii=False,indent=2))
if __name__=='__main__':main
IN PRACTICE

The TCP collector reconstructs three messages; another connection receives one and retains zero. UDP receives three, but deliberate omission leaves two in the list.

Common pitfalls

recv as a message; characters as bytes; sending as persistence; relay TLS as authenticated origin of every payload; silence as absence of events.

Related topics: Syslog · SIEM and correlation · Clocks and NTP

Take this idea with you

A telemetry pipeline needs correct framing, context and confirmation of the retained outcome.

Create account

Reference: Transmission of Syslog Messages over TCP · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security

CCNP® and Cisco® are registered trademarks of Cisco Systems, Inc. and/or its affiliates. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Cisco. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.