Open source

Cybersecurity tooling, built in the open

Three separate pieces of work with sFractal: a command and control system for security devices, a software bill of materials, plus a game that teaches security. All Elixir, all public.

Collaboration
sFractal and Podii
Repositories
13, public
Language
Elixir
My role
Lead plus contributor
  • Elixir
  • Phoenix LiveView
  • MQTT
  • OpenC2
  • SBOM
  • CycloneDX
  • OAuth

Everything here lives under github.com/sFractal-Podii, a long-running collaboration between sFractal and Podii. I lead the Podii team on it and write code on the codebases myself. On several of these repositories I am the largest human contributor.

These are three separate projects rather than one system. They share a language, a security bent and some people. Only the first is a single connected thing.

Project 1

Command and control for security devices

An implementation of OpenC2, the standard that lets one security system tell another what to do without a human in the middle.

OpenC2 is an OASIS standard for machine-to-machine command and control of cyber defence. When one system decides something must change, it says so to another system directly, in a form both understand, instead of a person copying instructions between two consoles.

It came out of work led by the NSA, which moved to OASIS as a technical committee in 2017. The framing it belongs to is Integrated Adaptive Cyber Defense, essentially the OODA loop applied to security operations and OpenC2 covers one phase of that loop: the acting. Observing and deciding already had standards. Carrying out the decision did not.

The problem it exists to solve is that attacks now happen faster than people can respond to them. If a detection system works out what is wrong in milliseconds, then a human reads an alert, opens a second vendor's console and types in a rule, the speed of the detection was wasted. OpenC2 is the vocabulary that lets the second step happen without the human, across products from different vendors written in different languages.

What the standard actually covers

Three roles, deliberately few

A Producer sends commands. A Consumer receives them. The Actuator is the Consumer that carries one out. That is the whole cast.

A command is an action and a target

Everything else is optional. A response, at minimum, is a status code. Keeping the required surface that small is what lets it run on constrained devices.

Actuator profiles say what a given device supports

The language describes what is expressible. A profile describes what a firewall, or an endpoint, or a log collector will actually accept. Only stateless packet filtering is finished.

Transfer is somebody else's problem

The language says nothing about how a message travels. Separate specifications cover HTTPS and MQTT, so the same command can cross either.

The design goals are stated plainly in the architecture specification: technology agnostic, concise, abstract, extensible. The first of those is the one that matters here. It is also the one a specification cannot achieve on its own.

2017 OASIS technical committee formed Nov 2019 Language spec v1.0 Nov 2019 SLPF profile v1.0 Dec 2021 Transfer via MQTT v1.0 2022 Architecture spec v1.0 SBOM profile still in draft
The standard is still being built. Ten actuator profiles are in development, including the one for software bills of materials that this implementation already answers.

Why write another implementation

A standard with one implementation is not really a standard. It is a specification with a reference build. Technology agnostic is a claim and the only thing that turns it into a fact is independent implementations, in unrelated languages, that interoperate without their authors having coordinated.

It is a plugfest implementation, not a demo

This codebase goes to OpenC2 interoperability plugfests, where implementations that have never met are pointed at each other. The repository still carries the artwork for the SBOM proof of concept plugfest, feeding the IACD community, CACAO playbooks, the Open Cybersecurity Alliance plus SCAP.

Elixir is not one of the usual suspects

Most reference work sits in the languages you would expect. An implementation on the BEAM tests whether the specification is genuinely portable or quietly shaped by the tools its authors happened to use.

It implements a profile that is still being written

The software bill of materials profile is in draft. Answering a query for one now, in a real device, is how a draft gets found wanting while it can still be changed.

Being strict is the contribution

A permissive implementation accepts things the standard does not allow and every one of those quietly makes the standard mean less. Refusing by name, with a reason, is what makes an interoperability test worth running.

The runtime suits the job

A consumer has to stay connected, survive the broker going away and handle messages arriving at once. That is the problem the BEAM was built for, so the interesting work stays in the validation rather than in the plumbing.

The specifications are public: the language specification, the architecture and the MQTT transfer specification.

Three pieces make up the working system. A library that understands the standard, a dashboard that issues commands, plus a device that receives them. The library is the part that matters, because it is what stands between a public message broker and a device doing something.

Producer dashboard Phoenix LiveView device, broker, command MQTT broker EMQX or HiveMQ QoS 1 Emqtt.Emqx GenServer, subscribed Oc2.Command decode, validate, run Phoenix.PubSub to TwinklyLive 64 divs in an 8 by 8 grid change colour TwinklyMaHa publish oc2/cmd/device/t01/{env} subscribe {"action":"set", "target":{"led":"on"}}
One command, end to end. The dashboard publishes JSON to a topic, the device is already subscribed to it. What arrives is validated before anything on screen moves.

What a command looks like

A command is JSON with four fields. The device has to decide, for every message that arrives, whether this is something it recognises, something it recognises but cannot do, or something it should refuse outright.

action target args command_id required required optional optional What to do. One of five verbs. query, set, cancel, allow, deny What to do it to, plus a specifier. an LED and its colour, a network range Whether a reply is wanted. exactly one arg is supported today A string the caller can match a reply to. rejected if it is not a string The pair of action and target is the whole vocabulary. Everything else is detail.
A command is four fields. Only the first two are required. Together they decide whether the device will act at all.

The vocabulary is a list, not a guess

The permitted combinations are written down. An action nobody implemented, or a pairing that makes no sense for this device, is rejected by name rather than by something further down the call stack failing at runtime.

features sbom hello_world led ipv4_net ipv6_net query set allow deny cancel A filled dot is a pair the device will act on. Everything else is refused by name, before any handler runs.
The permitted pairs are a list in the source, not a rule the parser infers. Adding a capability means adding a pair plus a handler, so nothing is ever half-supported.

Two of those pairs are worth pointing at. query features asks the device what it can do and it answers with the profiles it implements against version 0.9.5 of the specification. query sbom asks the device what it is made of, which is the thread running into the second project below.

Every stage can refuse and refusal travels

Validation is eight stages long. Each one is narrow enough to name exactly what it rejects and each begins by checking whether an earlier stage already failed. If one did, the stage returns the command untouched.

Jason .decode check_top check_action get_target check_target check_id check_args do_cmd /1 error? true short-circuits every remaining stage every stage begins by checking whether an earlier one already failed Each stage either enriches the struct or replaces it with an error. Nothing raises, nothing needs a try block.
Eight stages, each one narrow enough to name what it rejects. A command that fails any of them stops being processed at that point. It carries the reason with it to the end.

The effect is that nothing raises. There is no error handling wrapped around the pipeline, because a malformed command from a public broker is not an exception, it is an ordinary result carrying the reason it was refused. The transport turns that into a reply.

The transport knows nothing about the standard

The library takes a chunk of text and hands back a result. It has no idea where that text came from. That separation is why the same library serves a device over MQTT here and could serve one over HTTP without changing.

Producer picks the broker EMQX Emqtt.Emqx HiveMQ Emqtt.Hivemq oc2/cmd/device/t01/{env} oc2/cmd/device/t02/{env} device slot 1 device slot 2 {env} keeps staging and production apart on the same public broker. Swapping broker from a dropdown is a conformance test for the standard, not a demo of one vendor.
Each broker gets its own device slot. The environment is part of the topic. A staging device never sees a production command.

Why the device is a twin

Blinky is a Raspberry Pi with an LED matrix, the usual hello world of IoT. Twinkly is the same device in the cloud, where an eight by eight grid of divs stands in for the LEDs. It subscribes to the same topic, runs the same library and validates the same way. Only the last step differs.

Twinkly in rainbow mode. Each row takes the next colour in the palette, which is why the pattern appears to travel down the grid. On the physical Blinky these are diodes on a Raspberry Pi. Here they are 64 divs driven by the same command.

The talk stops depending on a suitcase

Demonstrating this at a conference otherwise means hardware in hand luggage, a bench to set it up on, plus a camera pointed at it so the back row can see.

A remote audience sees the real thing

Everyone watching gets the identical demo in their own browser rather than a video feed of somebody else's desk. Every connected browser updates at once.

The interesting half is unchanged

The command still crosses a public broker and still has to survive validation. Swapping diodes for divs changes nothing about the part worth showing.

Project 2

Knowing what your software is made of

A software bill of materials is an inventory of every dependency you ship, so that when a vulnerability lands in some package you can answer whether it affects you.

The question sounds simple until you try to answer it for something already running. Most inventories are produced by somebody auditing a system after the fact, which means they are correct on the day they were written and drifting from then on.

Each application generates its own at build A running device asked over OpenC2 CycloneDX one inventory format SbomPoc dashboard what is deployed, made of what query sbom The inventory is produced by the build, so it cannot drift from what actually shipped.
Two ways to learn what a system is made of. The build produces an inventory every time. A running device will answer the same question over the same channel used to command it.

SbomPoc is the dashboard reporting the state of the proof of concept. The part I find more interesting is that the applications generate their own inventory as a build step, in CycloneDX, so the answer is a by-product of shipping rather than a task somebody remembers to run. That is also why a device will answer query sbom over the same channel used to command it. Asking a machine what it is made of becomes the same kind of operation as telling it what to do.

Project 3

A game that teaches security

Quadblock Quiz is a falling-blocks game, extended from Grox.io's quadblocks, where the security lesson is built into the board rather than bolted on as a quiz.

Plenty of security training is a slide deck followed by a multiple choice test. This takes a different route. Play long enough and vulnerable bricks start appearing on your board, with their own colour so you cannot miss them. They do not go away on their own.

Blocks fall Tetris, brick, bottom A vulnerable brick past the threshold Answer a question QnA, by category Earn a power and clear the board its own colour, so you cannot miss it the threshold is configurable fix one vulnerability, or clear them all licence problems work the same way the loop keeps going until the board fills
The game loop. The powers are named after what they fix, which is the whole teaching device: a vulnerability you ignore keeps taking up space on the board.

Answering a security question correctly earns a power and the powers are named after what they remove: fix a vulnerability, fix a licence problem, or clear all of them at once. So the lesson arrives mechanically instead of being stated. An unfixed vulnerability keeps occupying space you need, licence problems behave the same way and the only route to clearing either is knowing the answer.

It gets taken to security conferences and run as a competition, with a contest layer holding state plus RSVPs on top of the game itself.

The question bank is content, not code

Questions live on disk in directories by category, found at runtime rather than compiled in and categories already answered are filtered out so a session does not repeat itself. Three build tasks validate the questions and the answers before anything ships, which means somebody writing questions never has to write Elixir and a malformed question fails in the pipeline rather than in front of a room.

Sign-in runs through GitHub, Google or LinkedIn, because at a conference stand nobody is going to create an account to try something for five minutes.

Think your organisation has outgrown its systems?

Let's figure out what is actually broken.