Skip to main content
All Playbooks
intermediate45 minPlatform Engineer / SOC Lead

Ingest Threat Intelligence over TAXII Without Vendor Tools

What you will achieve

A daily TAXII 2.1 sync that walks the full isMalicious indicator set with curl and cron, stays incremental with added_after, and feeds your SIEM or TIP — using nothing but open standards.

curljqcronSplunkOpenCTIMISP
Jean-Vincent QUILICHINIJean-Vincent QUILICHINIAug 24, 2026

What You Will Achieve

TAXII 2.1 is just HTTP + JSON. You do not need a commercial TAXII client, a TIP license, or an integration marketplace to consume — or publish — standards-based threat intelligence. By the end of this playbook you will have:

  • Discovery and collection listing against a live TAXII 2.1 server with plain curl
  • STIX 2.1 bundle pulls parsed with jq, paginated with the next token
  • Incremental sync with added_after, including revocation handling
  • A cron job that keeps a local indicator file in sync, once a day
  • The same feed connected to Splunk/Elastic and OpenCTI/MISP native TAXII clients
  • Your own org's indicators shared back over the same standard (org-reported-* collections)

Everything below runs against https://api.ismalicious.com/taxii (also mirrored at /taxii2 — same handlers, for clients that hardcode that path).


Prerequisites

| Requirement | Details | | ---------------- | --------------------------------------------------------------------------------------- | | API key + secret | From Account Settings; used for every TAXII call | | Plan | Pro or Enterprise — lower plans get 403 UPGRADE_REQUIRED on the feed endpoints | | Tooling | curl, jq, cron (or a systemd timer) — that is the whole client |

Authentication — the server accepts three equivalent forms, so any standards-compliant client fits:

  • X-API-KEY: <base64(apiKey:apiSecret)> — single colon, no spaces
  • HTTP Basic with username = apiKey, password = apiSecret (curl -u apiKey:apiSecret)
  • HTTP Basic with any username and the Base64 credential as the password — for clients that only expose one secret field (legacy TAXII clients, Sentinel, MISP)
bash
export CREDENTIALS=$(printf '%s' "${API_KEY}:${API_SECRET}" | base64)

One thing worth knowing before you write a scheduler: on Pro and Enterprise, TAXII polls do not consume your monthly request quota. The feed is meant to be polled; only your plan's per-minute burst limit applies.


Step 1: Discover the API root

TAXII 2.1 starts with a discovery document that tells you where the API roots live. No vendor SDK — one GET:

bash
curl -sS \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii" | jq .

The response points at the single API root:

json
{
  "title": "isMalicious",
  "api_roots": ["/taxii/api-root"]
}

Fetch the root itself to see its capabilities (including max_content_length, which is why large pulls come back paginated rather than as one giant body):

bash
curl -sS \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii/api-root" | jq .

Step 2: List the collections

bash
curl -sS \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii/api-root/collections" | jq -r '.collections[].id'

Nine shared collections cover the catalogue, and three are scoped to your organization:

| Collection | Contents | | ------------------------------------------------------------------------ | ---------------------------------------------- | | malicious-domains / malicious-subdomains | Malicious domains, and subdomains only | | malicious-ips | Malicious IP addresses | | malicious-urls | Malicious URLs | | malicious-file-hashes | MD5 / SHA-1 / SHA-256 file hashes | | malware-iocs / ransomware-iocs | Category-filtered mixed IOCs | | c2-indicators | C2 / botnet infrastructure (IPs + domains) | | phishing-indicators | Phishing domains and URLs | | org-reported-ips / org-reported-domains / org-reported-file-hashes | Indicators your org submitted (see Step 9) |


Step 3: Pull a STIX bundle with curl and jq

Objects come back as a STIX 2.1 bundle: one identity object plus indicator objects.

bash
curl -sS -D /tmp/taxii-headers.txt \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii/api-root/collections/malicious-domains/objects?limit=100" \
  > /tmp/page.json

# Extract the STIX patterns
jq -r '.objects[] | select(.type == "indicator") | .pattern' /tmp/page.json

Patterns follow the standard STIX form, e.g. [domain-name:value = 'evil.example'], so any STIX-aware consumer parses them without custom code.

Page sizing: limit counts STIX objects (identity + indicators, so indicators cap at limit − 1). The default is 50; Pro accepts up to 1001 objects per page; Enterprise has no page cap — but responses are still split by a server-side size budget, so a very large request comes back as 200 with a next token rather than one oversized body. Either way, the pattern is the same: keep following next.


Step 4: Follow the next token to walk the whole collection

Pagination lives in both the response body and the headers:

  • Body: "more": true and a "next" token inside the bundle
  • Headers: X-TAXII-Has-More: true and X-TAXII-Next

Pass the token back as the next query parameter:

bash
NEXT=$(jq -r '.next // empty' /tmp/page.json)

curl -sS \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii/api-root/collections/malicious-domains/objects?limit=100&#x26;next=${NEXT}" \
  > /tmp/page2.json

Two rules keep a walk correct:

  1. A short page does not mean the end. Each request scans a fixed budget of backend keys, so a page can return fewer than limit indicators while more is still true. Stop only when next is absent.
  2. Repeat the same added_after / added_before on every page of a filtered walk — the token continues the scan, the filters define it.

Step 5: Sync incrementally with added_after

For the first full ingestion, omit the date filters entirely — that returns the most complete dataset, including indicators without intel timestamps (a date filter has to exclude anything it cannot place in a time window).

After that, poll with added_after set to your last successful sync:

bash
curl -sS \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii/api-root/collections/malicious-domains/objects?limit=1001&#x26;added_after=2026-08-23T06:00:00.000Z" \
  | jq -r '.objects[] | select(.type == "indicator") | .pattern'

Incremental pulls also carry revocations: the first page includes revoked: true indicators for entities removed from the dataset (false-positive cleanup) since your added_after timestamp. Honor them — drop the matching indicator from your local store:

bash
# Revoked patterns to delete locally
jq -r '.objects[] | select(.type == "indicator" and .revoked == true) | .pattern' /tmp/page.json

A full sync (no added_after) contains no revocations — there is nothing to revoke when you are pulling the current truth.

How often to poll: the shared collections are rebuilt by a nightly reload that starts at 02:00 UTC, so once a day around 06:00 UTC picks up the whole day's new indicators in one pass. Polling more often is allowed (and does not touch your quota) but returns the same set until the next rebuild. The exception is the org-reported-* collections, which update as your org submits — poll those as often as your workflow needs.


Step 6: Schedule a daily cron pull into a file

This script does everything above — full sync on first run, incremental with revocation handling afterwards — and appends newline-delimited STIX indicators to a dated file your downstream tooling can tail:

bash
#!/usr/bin/env bash
set -euo pipefail

B64=$(printf '%s' "${ISMALICIOUS_API_KEY:?}:${ISMALICIOUS_API_SECRET:?}" | base64)
BASE="https://api.ismalicious.com/taxii/api-root/collections/${COLLECTION:-malicious-domains}/objects"
DIR="/var/lib/ismalicious/taxii"
STATE="${DIR}/since"
OUT="${DIR}/indicators-$(date -u +%F).ndjson"
mkdir -p "$DIR"

NOW=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
SINCE=$(cat "$STATE" 2>/dev/null || true)
NEXT=""

while :; do
  QS="limit=1001"
  [[ -n "$SINCE" ]] &#x26;&#x26; QS="${QS}&#x26;added_after=${SINCE}" # first run: full sync, no filter
  [[ -n "$NEXT" ]] &#x26;&#x26; QS="${QS}&#x26;next=${NEXT}"

  PAGE=$(curl -sS -f \
    -H "X-API-KEY: ${B64}" \
    -H "Accept: application/taxii+json;version=2.1" \
    "${BASE}?${QS}")

  jq -c '.objects[] | select(.type == "indicator")' <<<"$PAGE" >>"$OUT"

  NEXT=$(jq -r '.next // empty' <<<"$PAGE")
  [[ -z "$NEXT" ]] &#x26;&#x26; break
done

echo "$NOW" >"$STATE"
echo "Synced $(wc -l <"$OUT") indicators into ${OUT}"

Schedule it after the nightly rebuild:

cron
0 6 * * * COLLECTION=malicious-domains /usr/local/bin/ismalicious-taxii-pull.sh

That is a complete standards-based ingestion pipeline: one shell script, zero vendor tools.


Step 7: Point a SIEM native TAXII input at the feed

Splunk and Elastic both ingest TAXII through their own threat-intelligence tooling (a TAXII client add-on on Splunk; the TAXII-capable threat intel integration on Elastic Agent). Whatever the client, a TAXII 2.1 input asks for the same five values — configure them as follows:

| Setting | Value | | -------------- | ---------------------------------------------------- | | Discovery URL | https://api.ismalicious.com/taxii | | API root URL | https://api.ismalicious.com/taxii/api-root/ | | TAXII version | 2.1 | | Collection | e.g. malicious-domains (one input per collection) | | Authentication | HTTP Basic — username apiKey, password apiSecret |

If the client only exposes a single credential/token field, use the legacy Basic form: any username, and Base64(apiKey:apiSecret) as the password. Set the poll interval to daily (see Step 5) — anything tighter just re-reads the same rebuild.


Step 8: Connect OpenCTI or MISP

Both platforms ship native TAXII 2.1 clients, so there is no connector to write — give them the API root and credentials from Step 7 and select the collections you want. Follow the platform-specific walkthroughs rather than duplicating them here:

Because both consume the standard STIX 2.1 bundles from Steps 3–5, the indicators land with their patterns, labels, and revocation semantics intact — version-superseding included (revocations reuse the original indicator ID with a newer modified timestamp).


Step 9: Share your own indicators over the same standard

Sharing is the other half of standards-based threat intel, and it works without vendor tools too. Submit indicators observed in your environment:

bash
curl -sS -X POST \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Content-Type: application/json" \
  "https://api.ismalicious.com/org/indicators" \
  -d '{
    "indicators": [
      { "type": "ip", "value": "203.0.113.10", "category": "malware", "comment": "IPS correlation" }
    ]
  }'
  • Batches of up to 100 indicators per call; duplicates upsert by (organization, type, value)
  • Submissions are auto-approved and isolated to your organization — they never enter the shared catalogue
  • Daily caps: Pro 200/day, Enterprise 100,000/day

Approved indicators are mirrored into the org-reported-ips, org-reported-domains, and org-reported-file-hashes TAXII collections. That means every consumer you wired up in Steps 6–8 — the cron job, the SIEM input, OpenCTI, MISP — can subscribe to your org's own intel through the exact same TAXII 2.1 interface, and these collections update as you submit rather than on the nightly rebuild. One standard, both directions.


Validation

Confirm the whole chain with a filtered one-day window:

bash
curl -sS -D - -o /tmp/check.json \
  -H "X-API-KEY: ${CREDENTIALS}" \
  -H "Accept: application/taxii+json;version=2.1" \
  "https://api.ismalicious.com/taxii/api-root/collections/malicious-ips/objects?limit=10&#x26;added_after=$(date -u -d 'yesterday' +%Y-%m-%dT06:00:00.000Z)" \
  | grep -i '^x-taxii'

jq '{objects: (.objects | length), more, next: (.next != null)}' /tmp/check.json

You should see Content-Type: application/stix+json;version=2.1, the X-TAXII-Has-More / X-TAXII-Next headers, and a bundle with indicators from the last rebuild. A 403 UPGRADE_REQUIRED means the key's plan is below Pro; a 401 means the credential encoding is wrong (single colon, no spaces).


Did this playbook work for you?

Run this playbook today

Free API key, 30 checks/month, no credit card. Need more volume? See pricing.

No credit card required · 30 free checks/month · Free API key