Skip to content

Build a source pack others can subscribe to

From a hand-modified extension to a hosted JSON file: writing the sources, choosing placeholders, keeping internal hosts local, serving it with an ETag, and rotating it safely.

6 min readCoeus — Maintainerspackssources

Many investigators end up with the same artefact: a list of links, one per internal tool, with the entity spliced into a query string. It lives in a wiki page, a browser bookmarks folder, or a colleague's head. It is always slightly out of date, and it is never the same on two browsers.

A source pack is that list, written down once in a form a browser can subscribe to. This is how to build one.

Start from what you already have

Do not write the file from scratch. Open the Coeus dashboard, add three or four of your internal tools by hand on the Sources page, then use Export My sources. What comes out is a valid pack, and it is the same shape the importer accepts, so the round trip is lossless.

If you are coming from Coeus 1.x, or from a colleague's zip, use Sources → Import JSON first. It accepts four shapes and works out which it has: a modern pack, a 1.x tools list, a type map ({"ip": ["…", "…"], "domain": [...]}), or a bare array of source objects. The import report tells you what it dropped — a url key that is not a known type or group, an entry with no usable template — before you commit anything.

The Coeus sources page showing the built-in pack alongside a subscription
Sources: the built-in pack, My sources, and one subscription

The file

A pack is an object with four required fields and a list of sources.

{
  "schema": 1,
  "id": "acme-soc",
  "name": "ACME SOC",
  "version": "2026.09",
  "homepage": "https://example.com/soc",
  "sources": [
    {
      "id": "acme-reputation",
      "name": "ACME reputation",
      "desc": "House reputation score for hosts and addresses",
      "tags": ["reputation", "internal"],
      "url": {
        "ip": "https://intel.example.com/reputation?q={ioc}",
        "domain": "https://intel.example.com/reputation?q={ioc}",
        "url": "https://intel.example.com/reputation?q={ioc:noscheme}"
      }
    },
    {
      "id": "acme-case-search",
      "name": "ACME case search",
      "desc": "Past tickets mentioning this entity",
      "tags": ["cases"],
      "auth": true,
      "url": {
        "ip": "https://cases.example.com/search?q={ioc}&type={type}",
        "hash": "https://cases.example.com/search?q={ioc}&type={type}",
        "cve": "https://cases.example.com/search?q={ioc}&type={type}"
      }
    }
  ]
}

schema is always 1. id is kebab-case and has to be unique across the packs a person has installed, because it is what a subscription is keyed on. version is a string you choose; 2026.09 reads better in the Packs list than a git hash.

Inside a source, id must be unique within the pack, name is what appears in the right-click menu, and url needs at least one entry. auth: true puts a lock badge next to the source and means "this site will ask you to log in" — it is a heads-up, not a credential store. Coeus never holds credentials for a link source.

Choosing the url keys

A key is either a type id or a group id, and the concrete type always wins over its group. The groups are ip (ipv4, ipv6, cidr), hash (md5, sha1, sha256) and file (process, filepath). Everything else — domain, url, email, cve, asn, mac, btc, registry — stands alone.

That precedence is the feature. A malware repository that has a real page for SHA-256 and only a search box for the shorter digests is written as:

"url": {
  "sha256": "https://example.com/sample/{ioc}",
  "hash": "https://example.com/search?q={ioc}"
}

An MD5 falls through to the search; a SHA-256 goes straight to the sample page.

Choosing the placeholder

{ioc} URL-encodes the value and is the right answer nine times out of ten. The others exist for tools that want something else:

PlaceholderUse it when
{ioc}Anything in a query string. The default.
{ioc:raw}The tool sits behind a path segment and encoding breaks routing.
{ioc:noscheme}The tool wants a URL without http://, and encodes it itself.
{ioc:b64}base64url without padding — the VirusTotal URL id form.
{ioc:urlscan}An Elasticsearch query string, where : and / need escaping.
{ioc:defanged}An internal system that stores entities bracketed.
{domain}You have a URL or an email but the tool takes a hostname.
{type}One endpoint that dispatches on the type it was given.

The 1.x forms %s, %r and %urlscan still work byte for byte, so an old list keeps functioning after an import. Prefer the brace forms in anything new: %s does not encode, so a value with an & or a space in it silently truncates the URL you meant to build. Every rule, with a worked example per placeholder, is in the placeholder reference, which renders them live against a value you type.

Keep internal hosts local when you need to

Some hostnames should not be in a file at all. Mark a source you added yourself localOnly and it stays in chrome.storage.local in that browser: it is excluded from browser sync storage and from Export My sources, so it can never end up in a pack you publish by accident.

Hosting it

Serve the file over HTTPS with Content-Type: application/json. That is the only hard requirement. An internal web server, a repository's raw file endpoint, an S3 bucket behind SSO — all fine, as long as every intended browser can reach it.

Send an ETag if you can. Coeus refetches on the schedule in Settings with If-None-Match, so a pack that has not changed costs one 304 and no body. If the fetch fails, the previous copy stays in use and the Packs page shows the error; nobody's menu empties because a server was rebooting.

The first time someone adds the URL, Chrome asks for permission to read that one origin. Declining means the subscription is not added — there is no partial state.

In the earlier website plan, the account would host it: publish a pack there and it is served at /p/<slug>.json with the same ETag behaviour.

Rotating it

Three habits keep a shared pack calm:

Change ids only when you mean to

A source's id is its identity. Renaming acme-siem to acme-siem-v2 reads as "one source removed, one added" to everyone subscribed. Change name and desc freely; change id deliberately.

Bump version on every publish

Nothing enforces it, but the Packs page shows it, and "everyone is on 2026.09 except one laptop" is a five-second diagnosis instead of a twenty-minute one.

Test the template before you ship it

Paste a real entity into the "Try it" input on the source form. It renders the URL exactly as the launcher will. A %s where you meant {ioc} is invisible until someone looks up a URL with an ampersand in it.

Once the file is up, onboarding is one paste. The new investigator installs Coeus from the store, opens Packs, adds the URL, approves the origin, and their right-click menu matches everyone else's — including the ordering, which follows the pack.