Cyber Threat Intelligence
Why CTI Pipelines Fail at Handoffs
Every stage has an owner and a test. Every boundary between stages has neither. That is where intelligence pipelines actually break — and the failure is invisible because each stage, examined alone, is working correctly.
- pipelines
- architecture
- provenance
- contracts
- measurement
Ask an engineer where a pipeline is likely to fail and they will name a stage: the collector, the enrichment service, the correlation engine. Ask what is monitored and they will describe the same list. Stages have owners, dashboards, unit tests, and error rates.
Now ask what sits between two stages. Usually the answer is a schema, a queue, or a table — and no owner at all.
That gap is where intelligence pipelines actually fail, and the failure has a property that makes it nearly undetectable: every stage, examined individually, is working correctly. There is no crash, no error rate, no alert. The data arrives, validates, and is processed. What was destroyed in transit is something no stage was responsible for preserving, because preservation was nobody’s job.
The unit of analysis is wrong
A pipeline is built as a sequence of stages, and it is tested as a sequence of stages. But it behaves as a sequence of translations — each boundary re-expresses the data in terms the next stage understands.
Translation is where meaning is lost. And unlike computation, translation loss is monotonic: a boundary can drop context, but it can never add any back. There is no mechanism by which stage four recovers what stage three discarded.
Two consequences follow directly, and they are the shape of almost every handoff failure I have looked at.
Loss compounds along the chain. If each boundary preserves 90% of what matters — a generous assumption — then after five boundaries roughly 59% survives. Nothing anywhere reports this, because each individual handoff looks nearly lossless.
Confidence moves opposite to evidence. This is the part worth sitting with. Each stage reports on the quality of what it received, not on the quality of what was originally observed. The correlation engine is confident because its inputs were well-formed. The report writer is confident because the correlation was clean. Meanwhile the actual evidential basis has been thinning at every boundary.
The final product has the most confidence and the least evidence, and nothing in the pipeline can see this, because no component has both ends in view.
That inversion is not an implementation bug. It is what you get by default from any architecture where each component validates its input rather than tracing it.
What a boundary actually destroys
“Provenance was lost” is too vague to act on. In practice there are five distinct losses, and they need different fixes.
1. Qualifiers detach from values
A record arrives as “observed in a sandbox detonation”, “historically associated with”, “reported by a single source”. It leaves as an indicator with a confidence field.
The qualifier was not contradicted. It was structurally homeless — the receiving schema had a slot for the value and no slot for the circumstance. So the value passed and the circumstance stopped at the border.
Downstream, that indicator is indistinguishable from one observed live in a customer network. The pipeline did not record a falsehood; it destroyed the difference between two things that must never be treated the same.
2. Unknown collapses into a default
The most expensive loss, and the easiest to introduce by accident.
Upstream, a field is genuinely undetermined — the check did not run, the source did not say, the parser could not tell. At the boundary it is serialised. And serialisation formats have no native way to say “we do not know” that survives a schema with a default.
So null becomes false. Missing becomes empty string. Absent becomes zero. Every one of
those is a positive assertion manufactured at a boundary out of an absence.
The asymmetry is what makes it costly: the manufactured value is almost always the benign
one. malicious: null becomes malicious: false. verified: unknown becomes
verified: false, which is then read as checked and clean. The pipeline generates its
most reassuring output precisely where it knows least, and it does so at a boundary that
nobody tests.
3. Time changes meaning
Every stage has a clock, and they are answering different questions.
- When the event happened.
- When it was observed.
- When it was collected.
- When it was processed.
- When the record was written.
A boundary that carries one timestamp field has silently chosen one of these and thrown
away the distinction. I have measured a gap of nearly twenty hours between when a
piece of work actually completed and when its record was written. If freshness downstream
is computed from the write time, the pipeline can look current purely from back-filling —
no new work required.
The general rule, and it applies far beyond pipelines: a timestamp must belong to the same observation as the number sitting next to it. Boundaries are where that pairing is most often broken, because each side has its own idea of which clock is the clock.
4. Scope shifts without a name change
count means per-record upstream and per-batch downstream. source means the immediate
supplier here and the original origin there. confidence means source reliability in one
stage and analytic confidence in the next.
Nothing fails. The field name matched, so the handoff was accepted. This is the failure that survives longest, because it produces plausible numbers that are simply answering a different question from the one the reader believes.
The source case is particularly costly in CTI, because it is exactly what turns five
republications of one claim into five apparently independent sources. Corroboration
depends on origin, and origin is the first thing a boundary flattens.
5. Identity is re-minted
Stage A assigns an internal ID. Stage B does not carry it, and assigns its own. The link between the two is now implicit — reconstructable only by matching on content, if at all.
Every downstream merge, dedupe or correlation decision made on the B-side ID is now unauditable from the A-side. And merge decisions, once their basis is unrecoverable, can never be reviewed.
Why no test catches any of this
Three reasons, and they compound.
Stage tests assert on output, not on what output enables. A collector test checks that records are produced and well-formed. It cannot check that the records still support a question that will be asked three stages later, because the test does not know that question exists.
Schema validation checks shape, not meaning. This is the crucial one. A schema
confirms that timestamp is a valid datetime. It cannot confirm that it is the right
datetime. It confirms confidence is a float in [0,1]. It cannot notice that the
producer meant source reliability and the consumer reads analytic confidence.
The type survived. The meaning did not. Validation reports success.
End-to-end tests use clean data. They are built from records that have all their fields populated — because that is what a test fixture looks like when a human writes it. The handoff failures live in records that are partially unknown, ambiguously sourced, or carrying a qualifier the fixture author never thought to include. The happy path is precisely the path where handoff loss does not occur.
There is also a plain organisational reason underneath all three. A stage has an owner who is accountable when it breaks. A boundary has two owners, which in practice means none. Each side is correct by its own contract. The failure belongs to the space between the contracts, and no on-call rotation covers a space.
The fix: make the boundary a thing that exists
The remedy is unglamorous. Stop treating handoffs as plumbing and give each one an explicit, owned contract that states what must travel, what may never be defaulted, and what the receiver is required to reject.
A workable contract answers six questions:
1. What must be present, with no default permitted?
Name the fields where a missing value must cause rejection rather than substitution. This
is the single highest-yield clause. If source_origin cannot be defaulted, circular
reporting cannot enter silently.
2. How is unknown represented, distinctly from every other value?
It needs its own representation that survives serialisation — a sentinel, a separate
presence flag, a three-valued type. Not null, if null also means “empty” or “not
applicable” anywhere else in the system.
3. Which clock is each time field?
Name it in the field, not in the documentation: observed_at, collected_at,
recorded_at. A field called timestamp is an unresolved question wearing the costume of
an answer.
4. What is the unit and scope of each quantity? Per record or per batch. Immediate source or original origin. Source reliability or analytic confidence. Where two stages genuinely mean different things, use different names — reusing the name is how the difference disappears.
5. Does the upstream identity survive? Every derived record carries a reference to the artefact it came from — a reference, not a summary. If a claim in the final product cannot be traced to a specific span in a specific source, it must be marked as ungrounded, not presented alongside grounded claims.
6. What does the receiver reject, and when was it last seen rejecting? A contract with no enforcement is documentation. A validator that has never been observed to reject anything is indistinguishable from one that is not running.
That last clause deserves emphasis because it is the cheapest and most skipped. Take each boundary, hand it a record with a deliberately missing required field, and confirm it goes red. It takes an afternoon. Until it is done, every green boundary in the pipeline is of unknown value — not because the checks are probably broken, but because working and not running produce identical output, and you have no observation that separates them.
A diagnostic you can run this week
Pick your longest pipeline. For each boundary, in order, answer:
- If the upstream sent a record with every optional field missing, what does the downstream store? Is any of it a value the upstream did not assert?
- Which clock does the downstream use for freshness, and does the upstream mean the same one by it?
- Can the downstream reconstruct the original origin of a claim, or only the immediate supplier?
- Does any field name mean something different on the two sides?
- Can a record in the final product be traced to a specific span in a specific source artefact?
- When did this boundary last reject something, and can you make it reject something now?
A pipeline that answers all six cleanly at every boundary is rare. More usefully, the questions produce a ranked list of where to start, and the answers are cheap to get — which is unusual for architectural work, and is the reason this is worth doing before any larger redesign.
The summary in one line
A pipeline is a chain of stages, but it fails as a chain of translations. Monitor the stages and you will watch a healthy system produce, slowly and with rising confidence, an answer that no longer has evidence underneath it.
The corroboration and confidence half of this problem — what has to be recorded so that a
judgement stays traceable across those boundaries — is implemented as code in
cti-confidence. The stage-by-stage view
of the same pipeline is in
From OSINT Collection to Actionable Cyber Threat Intelligence,
and the measurement problems that hide these failures in
Measuring Blind Spots in Intelligence Collection.
สรุปภาษาไทย
ถามวิศวกรว่าท่อข้อมูลน่าจะพังตรงไหน เขาจะตอบเป็น ขั้นตอน — ตัวเก็บข้อมูล ตัวเสริมข้อมูล ตัวหาความสัมพันธ์ และสิ่งที่เฝ้าดูก็เป็นรายการเดียวกันนั้น ทุกขั้นมีเจ้าของ มีแดชบอร์ด มีเทสต์
แต่ถ้าถามว่าอะไรอยู่ ระหว่าง สองขั้น คำตอบมักเป็นสคีมา คิว หรือตาราง — และไม่มีเจ้าของเลย
- 🔑 หน่วยวิเคราะห์ที่เราใช้อยู่ผิด — เราสร้างและทดสอบท่อเป็น ลำดับของขั้นตอน แต่มันทำงานเป็น ลำดับของการแปล และการแปลคือจุดที่ความหมายหาย ⛔ ความสูญเสียที่รอยต่อเป็นแบบ ทางเดียว — ขั้นที่สี่กู้สิ่งที่ขั้นที่สามทิ้งไปไม่ได้
- 🔴 ความเชื่อมั่นเคลื่อนสวนทางกับหลักฐาน — แต่ละขั้นรายงานคุณภาพของ สิ่งที่ตัวเองได้รับ ไม่ใช่คุณภาพของ สิ่งที่สังเกตได้ตอนต้นทาง ⇒ ผลผลิตปลายทางมีความมั่นใจมากที่สุด และมีหลักฐานน้อยที่สุด และไม่มีชิ้นส่วนใดมองเห็น เพราะไม่มีใครเห็นทั้งสองปลายพร้อมกัน
สิ่งที่รอยต่อทำลาย มี 5 แบบ ⛔ ไม่ใช่ “ที่มาหาย” ลอย ๆ:
① คำขยายหลุดจากค่า — “เห็นใน sandbox” / “เคยเกี่ยวข้องในอดีต” ไม่มีช่องในสคีมาปลายทาง
ค่าจึงผ่าน แต่บริบทหยุดอยู่ที่ชายแดน ② “ไม่ทราบ” ยุบเป็นค่าปริยาย — null→false,
หาย→สตริงว่าง ⇒ สร้างคำยืนยันขึ้นมาจากความว่างเปล่า และค่าที่ถูกสร้างมักเป็นค่าที่ “ดูปลอดภัย”
③ เวลาเปลี่ยนความหมาย — เกิดเมื่อไร/สังเกตเมื่อไร/เก็บเมื่อไร/บันทึกเมื่อไร ไม่เหมือนกัน
(ของจริงที่ผมวัดได้ ห่างกัน เกือบ 20 ชั่วโมง) ④ ขอบเขตเปลี่ยนโดยชื่อไม่เปลี่ยน —
source ฝั่งหนึ่งแปลว่าผู้ส่งมาให้ อีกฝั่งแปลว่าต้นกำเนิด ⑤ ตัวตนถูกออกใหม่ —
การรวมระเบียนที่สืบกลับไม่ได้ จะทบทวนไม่ได้ตลอดไป
ทำไมไม่มีเทสต์ไหนจับได้: ① เทสต์ของขั้นตรวจ ผลลัพธ์ ไม่ได้ตรวจว่าผลลัพธ์ยัง
ตอบคำถามที่จะถูกถามอีกสามขั้นถัดไป ได้ไหม ② สคีมาตรวจรูปร่าง ไม่ได้ตรวจความหมาย —
ยืนยันว่า timestamp เป็นวันที่ที่ถูกต้อง แต่ยืนยันไม่ได้ว่าเป็นวันที่ ตัวที่ใช่
🔑 ชนิดข้อมูลรอด ความหมายตาย แล้วการตรวจสอบรายงานว่าสำเร็จ ③ เทสต์ end-to-end
ใช้ข้อมูลสะอาด ซึ่งเป็นเส้นทางเดียวที่ความสูญเสียที่รอยต่อ ไม่เกิดขึ้น
และเหตุผลเชิงองค์กรที่อยู่ใต้ทั้งสามข้อ: ขั้นตอนมีเจ้าของหนึ่งคน รอยต่อมีเจ้าของสองคน ซึ่งในทางปฏิบัติแปลว่าไม่มีใครเลย แต่ละฝั่งถูกต้องตามสัญญาของตัวเอง ความล้มเหลวอยู่ในช่องว่างระหว่างสัญญา และไม่มีเวรคนไหนเฝ้าช่องว่าง
ทางแก้: ทำให้รอยต่อเป็นสิ่งที่มีตัวตนและมีเจ้าของ — สัญญาที่ระบุว่าอะไรต้องเดินทางไปด้วย อะไร ห้ามใส่ค่าปริยายเด็ดขาด และฝ่ายรับต้องปฏิเสธอะไร ตอบ 6 ข้อ: ① ฟิลด์ใดที่ขาดแล้ว ต้องปฏิเสธ ไม่ใช่เติมค่าแทน ② “ไม่ทราบ” แทนด้วยอะไรที่รอดการ serialize ③ ฟิลด์เวลาแต่ละตัว คือนาฬิกาเรือนไหน (ตั้งชื่อในฟิลด์ ⛔ ไม่ใช่ในเอกสาร) ④ หน่วยและขอบเขตของแต่ละปริมาณ ⑤ ตัวตนต้นทางรอดไหม ⑥ ฝ่ายรับปฏิเสธอะไร และครั้งล่าสุดที่เห็นมันปฏิเสธคือเมื่อไร
ข้อ ⑥ ถูกข้ามบ่อยที่สุดและถูกที่สุด — ป้อนระเบียนที่จงใจขาดฟิลด์บังคับ แล้วดูว่าแดงจริงไหม ใช้เวลาบ่ายเดียว ⛔ ก่อนทำข้อนี้ ไฟเขียวทุกดวงที่รอยต่อมีค่าไม่ทราบ ไม่ใช่เพราะด่านน่าจะพัง แต่เพราะ “ทำงานอยู่” กับ “ไม่ได้ทำงาน” ให้ผลหน้าตาเหมือนกัน และเรายังไม่มีการสังเกตใดที่แยกสองอย่างนี้ออกจากกัน