[{"content":" Apache-2.0STIX 2.1OpenCTI connectorpre-1.0 What it is A STIX 2.1 data model and reference implementation that turns prompt-attack records — prompt injection, jailbreaks — into standard STIX objects any OpenCTI or STIX 2.1 platform can ingest.\nThe problem Most threat intelligence tooling has no first-class way to represent a prompt attack. A dozen vendors now collect them, but each in its own shape: a feed, an SDK, a report format. So an organisation that wants to store, attribute, score, and share a prompt attack the way it already handles every other indicator has nowhere standard to put it, and no way to move it between tools without a bespoke integration each time.\nThe design decision The choice worth defending is a vendor-neutral STIX 2.1 representation instead of a vendor SDK.\nAn SDK is the tempting path: it is turnkey, it ships a client and a feed, and it works the day you install it. But an SDK cannot, by construction, be the cross-vendor interchange format — it ties every consumer to one vendor\u0026rsquo;s client and one vendor\u0026rsquo;s feed. This project takes the other route: define the prompt attack as standard STIX 2.1 — an ai-prompt observable for the fact, indicators for the analysis, MITRE ATLAS technique mappings, OWASP-LLM labels, TLP markings — so any platform ingests it from any source with no vendor code in the path.\nThe tradeoff is real and worth naming. Standards-based interchange gives up turnkey convenience. It depends on platforms understanding the community ai-prompt observable and STIX extensions, which are themselves still evolving. It ships code, not a corpus, so you bring your own sources. And it is deliberately not a runtime detector — detection logic belongs in engines built for it. What you get for those costs is the one property an SDK can never have: it belongs to no vendor, so it can sit between them.\nWhat it doesn\u0026rsquo;t do Not a runtime detection engine. Detection logic belongs in tools built for it, such as NOVA. Not a new sharing format. It uses STIX 2.1 and existing community extensions rather than inventing another standard. Not a corpus. It ships code; sources are yours to bring. Limitations Early and unstable. Layout and interfaces will change; this is work in progress, not a frozen spec. Mapping is conservative, and prefix-anchored. ATLAS/OWASP mapping anchors each keyword to a leading word boundary, with a whole-word allowlist for short ambiguous keywords (dan, rag, worm, persona) that would otherwise collide with unrelated text. So inject matches injection — intended, cheap stemming — while dangerous no longer matches dan. Ambiguous input is left unmapped rather than guessed, which avoids false confidence at the cost of under-mapping; every mapping records how it was derived so a consumer can weight it. It inherits the maturity of what it builds on. It leans on community STIX extensions (the ai-prompt observable, stix2extensions) that are evolving in parallel. Two indicators per prompt has a cost. It emits both a STIX-pattern indicator (portable, exact, brittle to rewrites) and a NOVA-pattern indicator (durable against paraphrase, but needs a NOVA engine). That covers both correlation and behavioural matching, at the price of keeping two objects\u0026rsquo; metadata in sync per prompt. Prior art This builds on public work and is framed as complementary to it, not a replacement:\nThomas Roccia — the Indicator of Prompt Compromise concept and the NOVA framework. dogesec / David Greenwood — modelling prompts in STIX (the ai-prompt observable) and stix2extensions. 0DIN (Mozilla) — a jailbreak threat feed and the prompt-toolkit SDK, including prompt-similarity LSH signatures. 0DIN ships a vendor SDK and feed; this project defines a vendor-neutral STIX representation any platform can ingest from any source. The two are complementary. Push Security — the Pyramid of Pain in the AI era. Links Repository: https://github.com/ashwinvis98/adversarial-ai-cti Correlation building block: promptlsh Design rationale: docs/SPEC.md in the repository Status Apache-2.0, public, and early — layout and interfaces will change. Includes a reference OpenCTI enrichment connector that computes the correlation digest on ingest and links similar prompts.\n","permalink":"https://ashwinviswamithiran.com/projects/adversarial-ai-cti/","summary":"A vendor-neutral STIX 2.1 representation for prompt injection and jailbreaks, so any STIX platform can ingest them.","title":"adversarial-ai-cti"},{"content":" 16 claims traced to RESULTS.mdApache-2.0PyPIpre-1.0 What it is A similarity digest (fuzzy hash) for adversarial prompts: a fingerprint that stays similar when an attack is reworded, so near-duplicates line up instead of looking unrelated.\nThe problem Malware intelligence has fuzzy hashes — ssdeep, TLSH — where similar inputs produce similar digests, so a platform clusters a family on its own. Prompt attacks have no equivalent. Store a prompt keyed on its exact text and changing one word makes it look completely unrelated, so a feed of four hundred reworded jailbreaks reads as four hundred separate things. On a real corpus that redundancy is large, not hypothetical: on the full HackAPrompt attack set, 52.6% of prompts are exact duplicates after normalisation, before you even reach the reworded ones.\nThe design decision The load-bearing choice is what goes on the wire: a small lossy digest, or the embedding itself.\nIf two parties can exchange a few hundred bytes per prompt, the honest answer is ship an int8-quantised embedding, not a hash. Measured on WildJailbreak paraphrase pairs, an 8-bit quantised embedding holds essentially the entire retrieval ceiling (recall@1 0.767 vs 0.767 on a general model; 0.820 vs 0.820 domain-tuned) at roughly 384 bytes per vector. The 256-bit SimHash digest this tool emits is 32 bytes but gives up 11 to 21 points against that ceiling. So the semantic options are a genuine size/fidelity curve: full embedding (~1.5 KB, the ceiling) → int8-quant (~384 B, ~ceiling) → SimHash (32 B, −11–21 pts). You pick by byte budget and by how much you mind putting something near-invertible to an embedding on the wire.\nThe dependency-free lexical MinHash baseline (plm1) is deliberately not a point on that curve, and the page says so plainly rather than dressing it up. At 128 permutations it is about 1.1 KB — larger than the int8 embedding — and scores 0.537, below it. It is beaten on both size and accuracy. Its only justification is that it needs no ML at all: no model to download, pin, or run, fully deterministic and offline. That is a real reason to want it, and it is the only one.\nThe tradeoff accepted: choosing a portable digest over shipping embeddings buys cross-party correlation and data minimisation, and costs measurable accuracy. The tool ships the two ends (dependency-free lexical, 32-byte semantic) and treats the mid-size quantised option as measured-but-not-yet-built.\nSo when is the 32-byte digest the right choice? The measurement narrows it to two cases, and it is worth being precise about both. Where the byte budget genuinely binds — a fingerprint attached to every observable across a high-volume feed, where 32 bytes against 384 is a twelvefold difference in storage and index size. And where you would rather not hand a counterparty something close to invertible: a SimHash of an embedding is lossy in a way a quantised embedding is not. That is a difference in degree of exposure, not a privacy guarantee, and the limitations below say so plainly. If neither case applies, ship the int8 embedding — that is what the numbers say, and this page is not going to pretend otherwise.\nWhat it doesn\u0026rsquo;t do It is not an adversarially robust hash. The scheme is public and deterministic, so someone who knows it can evade it on purpose — reordering words defeats the lexical digest almost entirely, and targeted perturbation can move an embedding across a threshold. It clusters and triages a noisy feed; it is not a security boundary. It is not a detector or a classifier. It answers \u0026ldquo;are these two the same attack reworded,\u0026rdquo; not \u0026ldquo;is this an attack.\u0026rdquo; It does not anonymise. A digest is derived from the prompt and leaks information about it — for near-duplicates, by design. Data minimisation, not a privacy guarantee. Limitations Hashing costs accuracy. The shipping 256-bit digest trails the embedding ceiling by 11–21 points (e.g. domain-tuned centered 0.708 vs 0.820 at a 400-candidate pool). That is the price of 32 bytes. Lexical is order-sensitive. Under word reordering the lexical similarity collapses to ~0.004 — reshuffling evades it completely. Recall degrades with scale. Same-attack recall@1 drops as the candidate pool grows (0.708 at N=400 → 0.580 at N=1000, domain-tuned centered), as expected for nearest-neighbour retrieval. The cross-org number is not universal. The ~2.9x correlation gain over exact matching (35.1% vs 12.2% overlap sharing digests only) comes from random splits of a single high-redundancy corpus. It shows the exact-vs-digest gap on shared source material, not a portable rate. A genuinely cross-corpus test is still outstanding. The strongest backend is mildly in-distribution. The domain-tuned 0din model was fine-tuned on WildJailbreak-derived data; the clean general-model reference (bge-small, ~0.61 recall@1 at N=400) is the honest number to lead with. Links Repository: https://github.com/ashwinvis98/promptlsh Full reproducible evaluation: RESULTS.md STIX representation this plugs into: adversarial-ai-cti Status Apache-2.0, published on PyPI as promptlsh. Early and pre-1.0: the lexical digest (plm1) is implemented and tested; the semantic digest (pls1/pls1c) is implemented and evaluated, and experimental. The digest is carried on the observable today as a custom STIX property — x_promptlsh_digest, specified in adversarial-ai-cti — rather than a spec-defined one. Publishing it as a spec-defined property via a STIX 2.1 extension-definition is the outstanding step, and the reason that roadmap item is still open.\n","permalink":"https://ashwinviswamithiran.com/projects/promptlsh/","summary":"A portable similarity digest for prompt attacks, so reworded jailbreaks correlate instead of reading as unrelated items.","title":"promptlsh"},{"content":"How I keep an AI threat-intelligence knowledge base, built to scale to millions of documents, fresh in near real time without ever rebuilding it from scratch.\nThe dumbest version of this system (and why I almost built it) Let me describe the simplest possible way to keep an AI knowledge base up to date, because it\u0026rsquo;s the way almost everyone starts, and it\u0026rsquo;s a trap. It nearly caught me, for the reason these things usually catch people: it works. It works on the first day, it works on the test data, and it keeps working right up until the volume makes it unaffordable, by which point it\u0026rsquo;s load-bearing.\nI\u0026rsquo;m building an AI knowledge base of the threat landscape: the actors and the campaigns attributed to them, the malware families and tooling they run, the tactics, techniques and procedures behind those operations, the vulnerabilities they weaponise, the sectors and geographies they aim at, and the detections and mitigations that answer back. It\u0026rsquo;s already a vast library, built to scale to millions of documents, because the landscape it mirrors only ever grows.\nIntelligence arrives around the clock and almost none of it is a clean insert. A vendor publishes an alias that turns out to be a group you already track under another name. A CVE lands on CISA\u0026rsquo;s Known Exploited list and every actor profile touching it needs to say so. Activity that two vendors tracked separately turns out to be one operation, or one cluster turns out to be two. Attribution is provisional by nature and the corpus has to hold that. At any given moment a handful of documents need updating, a few new ones appear, and the overwhelming majority are exactly as they were.\nThe naive approach: rebuild everything, over and over. Re-fetch all the data, re-generate every document, and re-index the whole library into Amazon Bedrock Knowledge Bases. It\u0026rsquo;s simple. It\u0026rsquo;s obviously correct. And it is absurdly wasteful: the equivalent of reprinting an entire encyclopedia, cover to cover, because a handful of facts changed.\nI know exactly how wasteful, because I measured it. Rebuilding the knowledge base from scratch, the step where the AI re-reads and re-indexes every document, takes the better part of a day. At the scale I\u0026rsquo;m building toward, a from-scratch rebuild would grind on for days. Doing that on any regular basis would mean the system spends most of its life re-reading documents byte-for-byte identical to the last pass, burning compute to accomplish nothing.\nThe version I actually run applies each fresh wave of changes in minutes. Fast enough to keep the knowledge base current in near real time, however large it grows. And that last part is the whole secret: the cost of an update tracks what changed, not how much you\u0026rsquo;ve stored. The same quick refresh holds whether the library is a fraction of its eventual size or many times bigger.\nThis is the story of the difference. It\u0026rsquo;s not one clever trick. It\u0026rsquo;s the same stubborn idea applied at every layer of the system: never redo work you\u0026rsquo;ve already done. Let me walk you through it, because the ideas are simple and they apply to almost any data system you\u0026rsquo;ll ever build.\nWhat \u0026ldquo;keeping it fresh\u0026rdquo; actually involves First, the shape of the work. The pipeline runs as a repeating cycle of three acts:\nFetch: pull the latest intelligence from OpenCTI. Generate: turn that raw intelligence into the finished documents the assistant actually reads, a readable profile for each thing I track. (How those get written is a subject in its own right, and a later piece covers it.) Index: feed the updated documents to Amazon Bedrock Knowledge Bases so it can find them. The naive design would run all three acts over the entire dataset. My job was to make each act touch only what actually changed, because that\u0026rsquo;s what makes the cycle cheap enough to run again and again, keeping the knowledge base close to live. Act by act, then.\nIdea #1: leave a bookmark (the watermark) Threat intelligence does not arrive in tidy releases. Feeds publish continuously, reporting lands when somebody finishes an investigation rather than on a schedule, and the same event can reach you three times from three sources within a day. So the first question any refresh has to answer is the least glamorous one: what does \u0026ldquo;changed since last time\u0026rdquo; actually mean when nothing upstream is synchronised with anything else?\nThe answer is that the system leaves itself a bookmark. Every time the pipeline finishes a run, it writes down a timestamp: \u0026ldquo;I have processed everything up to this exact moment.\u0026rdquo; That saved timestamp is the watermark. On the next run, the system reads the bookmark and asks OpenCTI for one thing only: \u0026ldquo;give me everything modified after this time.\u0026rdquo; Everything older is already handled; it gets skipped entirely.\nIt\u0026rsquo;s the same thing you do with a book you\u0026rsquo;re reading over many nights. You don\u0026rsquo;t start from page one every evening to find your place; you open to the bookmark and carry on. The pipeline reads the world\u0026rsquo;s threat intelligence the same way, picking up where it stopped rather than wiping its memory and relearning the whole book each night. The watermark is the dog-ear.\nThere\u0026rsquo;s one subtlety worth mentioning because it bites people: clocks. The machine doing the fetching and the machine holding the data don\u0026rsquo;t have perfectly synchronized clocks, and records can be saved in the split-second the bookmark is written. So I deliberately rewind the bookmark by a small safety margin and accept a tiny bit of overlap. Re-processing a handful of records twice is harmless. Missing one because of a clock hiccup is not. When in doubt, overlap.\nIdea #2: one bookmark per topic, not one for the whole library Here\u0026rsquo;s where it gets a little smarter. The documents come in categories, and the thing to understand is that they move at wildly different speeds. Vulnerability records are a firehose: tens of thousands of new CVEs a year, plus a constant churn of scoring changes and reference updates on ones already published. Actor and campaign reporting is bursty and unpredictable: quiet for weeks, then a research team publishes and a dozen profiles need rewriting at once. The technique taxonomy, by contrast, barely moves. It gets a considered update a couple of times a year, and between those releases it is effectively a fixed reference frame.\nTreating that as one undifferentiated stream costs you. With a single bookmark for the whole library, a hiccup in the busiest category drags the slow, stable ones back with it, and you spend an afternoon regenerating a taxonomy that hasn\u0026rsquo;t meaningfully changed since spring.\nSo instead I keep a separate bookmark for each category. Each category tracks its own progress independently. The busy categories can be re-processed without disturbing the quiet ones; a problem in one category never drags the others back to square one. And it gives me a lovely operational lever: if I ever want to force a single category to rebuild completely, say because I improved how a particular kind of profile is written, I just erase that one bookmark. The system sees \u0026ldquo;no bookmark here,\u0026rdquo; assumes it\u0026rsquo;s starting fresh for that category, and rebuilds only that slice. Everything else keeps humming along incrementally.\nIdea #3: the sneaky problem — things change because their neighbors changed This one tripped me up. You\u0026rsquo;d think \u0026ldquo;what changed?\u0026rdquo; is obvious: whichever records were edited. That intuition holds up fine for a folder of PDFs. It falls apart in threat intelligence, because in this domain the facts that matter mostly aren\u0026rsquo;t attributes of anything. They\u0026rsquo;re claims about how two things relate. An actor uses a loader. A campaign is attributed to an actor. A technique turns up against a sector nobody had seen it aimed at before. None of that lives inside any single record. Which means the thing an analyst most needs to be current is precisely the thing least likely to trip an edited-since check.\nTake a well-documented actor, one of the ones with a decade of reporting behind it. Its own record is stable: aliases settled, description written years ago, attribution unmoved. Then a report drops tying it to tooling it hadn\u0026rsquo;t been associated with before. Nothing about the actor was edited. Nobody touched that record. But ask the assistant what that group runs, and the answer it gives is now behind the reporting.\nThat\u0026rsquo;s the failure mode worth designing against, because it is silent. A missing document announces itself. One that is present, confident, well formatted and quietly a fortnight behind the reporting is the one that burns an analyst mid-investigation.\nSo I stopped letting \u0026ldquo;edited\u0026rdquo; define stale. Freshness in a connected corpus isn\u0026rsquo;t a property a record carries by itself; it belongs to the record and its surroundings together. Each cycle the pipeline therefore asks the broader question: not only what moved, but what now reads differently because something near it moved. Whatever fails that test gets rewritten, whether or not a byte of its own data changed.\nAn honest note on how far that goes. Each relationship is written onto the documents configured to show it: often both endpoints, sometimes just one, depending on which view is worth carrying. The useful property is that the staleness check and the renderer read from the same configuration. So coverage is consistent by construction: a document gets refreshed exactly when a relationship it actually displays changes. There\u0026rsquo;s no gap where a card shows something but fails to notice it moved. Finding that set does still mean reading the relationship data and filtering afterwards rather than filtering as I read, so detection isn\u0026rsquo;t free. But the expensive stages downstream only ever see the short list that check produces, and that\u0026rsquo;s where the cost lives.\nIdea #4: don\u0026rsquo;t open a giant box to check if it\u0026rsquo;s empty A quick one. Intelligence sources rarely commit to one delivery style. Most of what lands is a delta: a small file saying here\u0026rsquo;s what\u0026rsquo;s new since you last asked. But sources also republish themselves in full from time to time — a schema change, a back-fill, a correction sweep, or just the publisher\u0026rsquo;s release cadence. Anyone who has consumed a feed for more than a few months has watched a routine morning pull turn into a full collection download without warning.\nThose re-issues are enormous and, in incremental mode, almost entirely redundant. The system already holds what\u0026rsquo;s inside them. Opening one costs time and a mountain of memory to learn nothing. So I peek at just the first sliver of each file, a tiny sip rather than the whole drink. That header tells me whether the file is a \u0026ldquo;full snapshot\u0026rdquo; or a \u0026ldquo;just-the-changes\u0026rdquo; file. If it\u0026rsquo;s a full snapshot, I skip it instantly without ever loading the rest. It\u0026rsquo;s the difference between reading the label on a box versus unpacking the entire box to find out what\u0026rsquo;s inside.\nIdea #5: do everything at once Even after I\u0026rsquo;ve narrowed the work down to \u0026ldquo;only what changed,\u0026rdquo; there\u0026rsquo;s the question of how to run it. The original design processed categories in a chain: finish the first, then start the next, then the next, one after another. Sequential. Simple. Slow: the total time was the sum of every category\u0026rsquo;s time, and one slow category held up all the rest.\nThe categories don\u0026rsquo;t depend on each other, and that\u0026rsquo;s a property of the domain rather than a convenience of the design. A CVE record doesn\u0026rsquo;t need an actor profile resolved before it can be written; a technique\u0026rsquo;s description doesn\u0026rsquo;t wait on a campaign. The connections matter enormously to the analyst reading the result, but preparing each category is independent work. So I run them simultaneously, one worker each, all firing at once. Total time drops from the sum of every category to roughly the single slowest one.\nThere\u0026rsquo;s a bonus I didn\u0026rsquo;t expect: isolation. Because each worker owns exactly one category (and, thanks to Idea #2, one bookmark), a failure is contained. If one worker trips over a malformed record — and in this domain it will, because feed quality varies and a single publisher can emit something that parses everywhere except your pipeline — the others finish normally and update their own bookmarks. I come back, fix that one slice, and re-run only it. In the old chained design, one stumble could stall everything behind it.\nIdea #6: don\u0026rsquo;t rewrite a document that didn\u0026rsquo;t actually change Here\u0026rsquo;s a subtle waste I caught late. One category can\u0026rsquo;t use the shortcuts above. Of how its documents get assembled I\u0026rsquo;ll only say that every one of them has to be looked at each run, changed or not; there\u0026rsquo;s a later piece on why. Fine. But re-examining a document and rewriting it are different things, and most of those come back identical to what\u0026rsquo;s already stored.\nSo before saving each freshly-generated profile, I compare it against the version already on disk. A quick fingerprint check. If they\u0026rsquo;re identical, I skip the write entirely. This one guard eliminated the vast majority of the writes for that category. That ratio isn\u0026rsquo;t luck: most of what a threat corpus re-examines on any given day genuinely hasn\u0026rsquo;t moved, because reporting arrives in bursts against a small part of the landscape while the rest sits still. Fewer writes means less work for the downstream indexer, which only re-reads what genuinely differs.\nWorth saying plainly: this guard runs on that one category, not across the board. It went in where the re-examine-everything constraint made it pay for itself immediately, and it hasn\u0026rsquo;t been generalised. Deliberate ordering rather than finished design.\nA war story: the file that tried to eat the whole pipeline No engineering post is complete without a scar, so here\u0026rsquo;s mine. One category refuses to play nice: the CPE dictionary (Common Platform Enumeration) that vulnerability data is keyed against, the list of vendors, products and versions that lets a CVE say precisely what it affects. The source has no \u0026ldquo;give me only what changed\u0026rdquo; option for it. None. Every run, I pull the whole thing. There\u0026rsquo;s no \u0026ldquo;since last time\u0026rdquo; here, only \u0026ldquo;everything, again.\u0026rdquo;\nThe first version saved each run\u0026rsquo;s download as a new, date-stamped file. Reasonable-sounding, and quietly catastrophic: a huge slug of nearly-identical data landing every cycle and never leaving. Then the part that actually hurt. Generation tried to load all of those snapshots at once, ran out of memory, and crashed. When generation dies the knowledge base stops updating, and every answer an analyst gets quietly rots.\nThe fix was embarrassingly simple. Instead of a new date-stamped file each run, I write to one fixed filename that overwrites itself. The unbounded pile collapsed to a single file, and that crash stopped being something I could have again.\nBe precise about what that buys, though. Overwriting removes one failure mode completely: generation can no longer be handed a growing pile. It does not make reading the file cheap — I still load it whole, and peak memory still tracks the size of the catalog. That ceiling is real and it\u0026rsquo;s still there. Claiming otherwise would be a nicer-sounding version of the mistake I\u0026rsquo;d already made once.\nThe decision I\u0026rsquo;d defend is the one that looks wasteful. Only a tiny fraction of that catalog currently links to a known-exploited vulnerability, and the lazy move would be to hard-code around that handful. I refused: the slice carrying real intelligence grows every month, and betting the architecture on today\u0026rsquo;s number is just scheduling the next crash.\nThe payoff Stack all of that together and a single incremental refresh compares to the \u0026ldquo;rebuild everything\u0026rdquo; alternative:\nYou pay the big cost once. After that, the work that dominates every refresh is proportional to what changed, not to what you have.\nThe first time I built the knowledge base, the full index genuinely did take the better part of a day. There\u0026rsquo;s no shortcut for a from-scratch build, and that\u0026rsquo;s fine: you pay it once. Every refresh since has finished in minutes. The expensive stages, generating documents and embedding them and indexing them, do work proportional to the change, not to the library. Some of the detection steps ahead of them still read more than they strictly need, so the refresh isn\u0026rsquo;t perfectly flat and I won\u0026rsquo;t pretend it is. But the part that dominates the clock is the part that stopped growing, which is why the number stays in the same handful of minutes as the library gets bigger.\nThe real lesson: be lazy on purpose Every idea here is a variation on one theme, and it outlives any specific technology:\nThe fastest work is the work you don\u0026rsquo;t do.\nNone of these is a breakthrough on its own. They\u0026rsquo;re the unglamorous, disciplined engineering that turns \u0026ldquo;technically works\u0026rdquo; into \u0026ldquo;works without anyone thinking about it,\u0026rdquo; which is the difference between a demo and a system an analyst will actually trust at two in the morning. The knowledge base behind the threat-intelligence assistant keeps itself current in near real time, each refresh finishing in about the time it takes to pour a coffee, and nobody has to babysit it. The work you skip on purpose is the work you never pay for twice.\n","permalink":"https://ashwinviswamithiran.com/writing/from-half-a-day-to-a-coffee-break/","summary":"Keeping a knowledge base current in minutes instead of half a day, by never rebuilding what has not changed.","title":"From Half a Day to a Coffee Break"},{"content":"I\u0026rsquo;m a security engineer in Denver. I build the data systems security teams depend on: intelligence pipelines, detection content, and the automation that connects them.\nRight now I\u0026rsquo;m at DISH Network (EchoStar), building a threat intelligence platform and a threat research program from scratch — a production CTI platform in AWS, ingestion across fifty-odd open-source, dark web, and commercial sources, and the automation that pushes what comes out of it into blocking and detection. Alongside that, a retrieval system over the threat graph, and coverage for adversarial-AI threats: jailbreak and prompt-injection corpora normalised into STIX and mapped to MITRE ATLAS.\nHow I got here I studied instrumentation and control engineering at NIT Trichy, which is not a security degree, and arrived at security through the operational end rather than the research end.\nMy first three years were at Wipro, on SIEM and SOC work: owning the technical workstream of a global SOC transition, migrating a 120TB QRadar deployment from on-premises to AWS, writing fifty-odd custom parsers, and modelling detection use cases against the log sources they fed. That period taught me the thing most of my writing comes back to — that the quality of a detection is capped by the quality of the pipeline underneath it, and the pipeline is where the unglamorous work lives.\nThen a master\u0026rsquo;s in computer science at CU Boulder, and a stint building LLM-backed backend services, before moving into threat intelligence platform engineering.\nHow I work Three things show up in everything on this site.\nDecisions, not features. The project pages here are decision records: the problem, the choice made, the tradeoff accepted, and what it doesn\u0026rsquo;t do. A README that argues for a design decision is worth more than the code it describes.\nMeasured, not asserted. If I claim a number, it\u0026rsquo;s reproducible on public data. The evaluation of my own similarity digest leads with the result that argues against it, because that\u0026rsquo;s the part worth trusting.\nCorrections in public. I\u0026rsquo;ve published numbers that turned out to be wrong and fixed them where anyone can see. That log is here, and it is deliberately not hidden.\nElsewhere GitHub · LinkedIn\n","permalink":"https://ashwinviswamithiran.com/about/","summary":"Security engineer in Denver. Threat intelligence platforms, detection pipelines, and the measurement work that keeps them honest.","title":"About"},{"content":"Everything I publish carries numbers, and some of them have been wrong. This is the log: what was stated, what it should have said, and where it was fixed.\nIt exists because a claim you cannot check is worth very little, and a track record of corrections is the only evidence that the checking is real.\n2026-08-17 \u0026middot; adversarial-ai-cti — docs/correlation-digest.md Stated the cross-organisation correlation gain as 4.4x. The correct figure is 2.9x (35.1% against 12.2%). The 4.4x came from an earlier evaluation run and survived into the docs after the numbers settled. Fixed in the repository. 2026-08-17 \u0026middot; promptlsh — RESULTS.md Stated the digest size advantage over a quantised embedding as roughly 10x. The correct ratio is roughly 12x — 384 bytes against 32. Fixed in the repository. ","permalink":"https://ashwinviswamithiran.com/corrections/","summary":"Numbers I have published and later found wrong, with what they should have said.","title":"Corrections"}]