MISP threat intel lab: feeds, IoCs, STIX and the REST API
0 / 12 steps
The goal
Run MISP, the free threat intelligence platform that security teams use to collect and share indicators of compromise (IoCs), on your own computer. You will switch on two free public feeds and pull in hundreds of real, published threat reports, search them for an indicator, write up a pretend phishing campaign of your own with harmless indicators, label it with a TLP sharing level and a MITRE ATT&CK technique, export it as STIX 2.1 and CSV, and finally ask MISP’s REST API for a clean blocklist, the same call a SIEM or firewall makes to stay up to date.
Everything runs on your own machine, and nothing you create leaves it. MISP runs on your computer at http://localhost:8080 (Docker opens that port on every network interface, so keep your firewall on and don’t do this on an untrusted network). The feeds are read-only downloads of public reports. Your own event uses addresses and names that are reserved for documentation (203.0.113.0/24, 198.51.100.0/24 and example.com), so they can never point at anyone real. The feed data contains real malicious domains and IP addresses: search for them inside MISP, but don’t visit, ping or scan them. Step 12 removes everything.
Why this lab. CySA+ Domain 1 (Security Operations) expects you to know threat intelligence platforms by name, MISP, OTX and OpenCTI, and to understand where indicators come from, how they are shared and how they end up in your detection tools. Reading about STIX and TLP is easy to forget. Building an event, tagging it and pulling it back out through an API is not.
Steven saved a video promising a million free IoCs in ten minutes. The download is the ten minutes. Working out which of them still matter, when plenty of the reports are from 2015, is the actual job. — Nyx
Words you’ll meet
| Term | Meaning |
|---|---|
| IoC | Indicator of compromise: a clue that something bad happened or is happening, such as an IP address, domain, URL or file hash. |
| Threat intelligence platform (TIP) | A database built for IoCs and the reports around them. MISP, OpenCTI and AlienVault OTX are the three the exam names. |
| Feed | A list of indicators or full reports that someone publishes for others to download. MISP can pull feeds on a schedule. |
| Event | MISP’s name for one report: a title, a date, tags, and the indicators (MISP calls them attributes) that belong to it. |
| TLP | Traffic Light Protocol. A label that says how far a report may be shared: red (named people only), amber (your organization), green (your community), clear (anyone). |
| MITRE ATT&CK | A public catalog of attacker techniques, each with an ID such as T1566.002 (spearphishing link). MISP stores these as galaxy tags. |
| STIX / TAXII | STIX is a standard JSON format for threat intelligence. TAXII is the standard way to serve STIX over HTTPS so tools can pull it automatically. |
| Warninglist | A MISP list of values that are known to be harmless or reserved (documentation ranges, big public services). It stops you blocking the internet by accident. |
Before you start. You need Ubuntu (the VM from Lab 00, or Ubuntu 24.04 in WSL2 on Windows) with Docker Engine and the Compose plugin (the Docker security lab covers installing them), plus git, curl and jq (sudo apt install -y git curl jq). Plan on about 6 GB of free RAM and about 10 GB of free disk. Commands are typed as your normal user (the $ prompt); if Docker needs sudo on your machine, put it in front of each docker command. Most of the hour is waiting for downloads, so keep this page open.
Steps
01Check Docker and download MISP’s Docker setup
MISP’s own project publishes ready-made images and a Compose file at github.com/MISP/misp-docker. Check your tools, then clone it.
$ docker --version $ docker compose version $ free -h $ cd ~ && git clone https://github.com/MISP/misp-docker.git $ cd ~/misp-docker && git log -1 --format='%h %cs %s'
Docker version 29.9.3, build 5457199
Docker Compose version v2.40.3
total used free shared buff/cache available
Mem: 15Gi 664Mi 3.2Gi 2.8Mi 11Gi 14Gi
Swap: 4.0Gi 83Mi 3.9Gi
Cloning into 'misp-docker'...
27ec05e 2026-11-04 Bump misp-guard
You should see Docker 25 or newer and Compose 2.17 or newer (MISP’s stated minimums), at least 6 GB in the available column, and the date of the newest commit.
Why the official repo? MISP has several moving parts (a PHP web app, a MariaDB database, Valkey for caching, a separate modules service and an NGINX front end). The MISP project keeps a tested Compose file that wires them together, so you don’t have to. Note the commit you got; if a later commit changes something, that line tells you why your screen differs from ours.
02Set the address and cap the memory
MISP reads its settings from a file called .env. Copy the template, move the web server to port 8080 (so it can’t clash with anything already on port 80), shrink the database cache, and add a small override file that puts a memory ceiling on the hungriest containers.
$ cp template.env .env $ sed -i -e 's|^BASE_URL=.*|BASE_URL=http://localhost:8080|' -e 's|^# NGINX_HTTP_PORT=80|NGINX_HTTP_PORT=8080|' -e 's|^# NGINX_HTTPS_PORT=443|NGINX_HTTPS_PORT=8443|' .env $ echo 'INNODB_BUFFER_POOL_SIZE=512M' >> .env $ grep -E '^(CORE_TAG|MODULES_TAG|BASE_URL|NGINX_HTTP|INNODB)' .env $ cat > docker-compose.override.yml <<'EOF' services: db: mem_limit: 1g misp-core: mem_limit: 3g misp-modules: mem_limit: 1g redis: mem_limit: 512m EOF $ docker compose config 2>/dev/null | grep -E 'mem_limit|innodb-buffer'
CORE_TAG=v2.5.51
MODULES_TAG=v3.1.3
BASE_URL=http://localhost:8080
NGINX_HTTP_PORT=8080
NGINX_HTTPS_PORT=8443
INNODB_BUFFER_POOL_SIZE=512M
- --innodb-buffer-pool-size=512M
mem_limit: "1073741824"
mem_limit: "3221225472"
mem_limit: "1073741824"
mem_limit: "536870912"
The grep line shows the MISP version the template pins and your three changes. The last command proves Compose picked up the override file: four mem_limit lines and the smaller database buffer.
Why cap memory? Out of the box the database asks for a 2 GB buffer and the containers can grow as large as they like. On a lab machine that also runs a browser (or, in our case, video renders), that is how a test turns into a frozen PC. docker-compose.override.yml is loaded automatically next to docker-compose.yml, which means you never edit MISP’s own file and git pull keeps working. On a server with plenty of RAM, skip the override.
03Pull the images and start MISP
Download the six images (about 4.7 GB), start everything in the background, then wait until the login page answers.
$ docker compose pull -q $ docker compose up -d $ until curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/users/login | grep -q 200; do sleep 10; done; echo "MISP is answering" $ docker compose ps --format 'table {{.Service}}\t{{.Status}}' $ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}'
... misp-core Pulled db Pulled ... Container misp-docker-misp-modules-1 Healthy Container misp-docker-misp-core-1 Started Container misp-docker-misp-core-1 Waiting Container misp-docker-misp-core-1 Error dependency failed to start: container misp-docker-misp-core-1 is unhealthy MISP is answering SERVICE STATUS db Up 3 minutes (healthy) mail Up 3 minutes misp-core Up 3 minutes (healthy) misp-modules Up 3 minutes (healthy) misp-nginx Up 1 second (health: starting) redis Up 3 minutes (healthy) NAME MEM USAGE / LIMIT misp-docker-misp-nginx-1 13.32MiB / 15.15GiB misp-docker-misp-core-1 1.054GiB / 3GiB misp-docker-db-1 790.8MiB / 1GiB misp-docker-redis-1 8.473MiB / 512MiB misp-docker-misp-modules-1 436.5MiB / 1GiB misp-docker-mail-1 18.86MiB / 15.15GiB
Expect dozens of lines saying a variable “is not set. Defaulting to a blank string”; they are normal and we trimmed them. On a first start, up -d may end with “dependency failed to start: container misp-docker-misp-core-1 is unhealthy”. That happened to us. It only means MISP’s first-time setup (building the database, loading ATT&CK and the other galaxies) took longer than the health check is willing to wait, so Compose never started the web server. Leave the until loop running, give it a few minutes (misp-core took 3 minutes 43 seconds to turn healthy on our machine), then in a second terminal run cd ~/misp-docker && docker compose ps until misp-core says healthy, and run docker compose up -d once more. The loop then prints MISP is answering.
What just started. misp-core is the application and its background workers, db is MariaDB, redis is Valkey (a Redis-compatible cache), misp-modules runs optional enrichment plugins, misp-nginx is the only part exposed to your browser, and mail would relay email alerts if you configured it. If misp-core never turns healthy and docker compose logs misp-core shows “Address family not supported by protocol”, your machine has no IPv6: add DISABLE_IPV6=true to .env and run docker compose up -d again (we hit that on a second test machine, not on the WSL2 one).
04Log in and change the default password
Open http://localhost:8080 in a browser on the same computer (on Windows with WSL2, your normal Windows browser works). Sign in with the default account from MISP’s documentation: email admin@admin.test, password admin. MISP does not force you to change it (we checked), so do it now: open Global Actions → My Profile, click Change Password on the left, type a long new password twice, confirm with admin and click Submit. A green Password Changed. banner confirms it; store the new password in your password manager. Prefer the terminal, or locked yourself out later? MISP ships a command that does the same. Replace YOUR-NEW-PASSPHRASE with your own and keep the quotes.
$ docker compose exec -u www-data misp-core /var/www/MISP/app/Console/cake user change_pw admin@admin.test 'YOUR-NEW-PASSPHRASE'
Password for admin@admin.test changed.
A weak password is refused; our deliberately short test came back with “Password length requirement not met.” (MISP’s default policy wants at least 12 characters and a mix of character types).
Why this matters. Every MISP install starts with the same email and password, printed in the documentation, and nothing makes you change them. A threat intelligence server knows what you are hunting for and who you share with, which makes it a juicy target. Changing a published default is the first job on any new appliance, not just this one. The terminal version sets a flag that makes MISP ask for a fresh password at the next web sign-in, which is what you want when you set a password for someone else; add --no_password_change if you don’t want that.
05Make an API key and keep it secret
Almost everything else in this lab goes through MISP’s REST API, the same way a SIEM or a script would. Create a key for the admin account, save it in a file only you can read, and make a tiny shell helper called misp so you don’t have to repeat the headers.
$ docker compose exec -u www-data misp-core /var/www/MISP/app/Console/cake user change_authkey admin@admin.test | grep -oE '[A-Za-z0-9]{40}$' > ~/.misp_key $ chmod 600 ~/.misp_key && wc -c < ~/.misp_key $ export MISP=http://localhost:8080 KEY=$(cat ~/.misp_key) $ misp(){ curl -s -H "Authorization: $KEY" -H 'Accept: application/json' -H 'Content-Type: application/json' "$@"; } $ misp $MISP/servers/getVersion | jq '{version, perm_sync}' $ curl -s -o /dev/null -w '%{http_code}\n' -H 'Authorization: wrong-key' -H 'Accept: application/json' $MISP/servers/getVersion
41
{
"version": "2.5.51",
"perm_sync": true
}
403
The key is 40 letters and digits (the 41st character wc counts is the line break), and it is never printed on screen. getVersion answering with the version proves the key works. A wrong key gets 403.
Treat the key like a password. Anyone with it can read and change everything your account can, without ever seeing a login page. That’s why it lives in a file with chmod 600 and travels in the Authorization header instead of the URL (URLs end up in logs and browser history). In the web interface the same thing is under Administration → List Auth Keys → Add authentication key, where you can also give a key an expiry date and an allowed IP range. In a real deployment, give each tool its own key on its own low-privilege user, so you can revoke one without breaking the rest. If you open a new terminal later, run the export and misp(){ ... } lines again.
06Switch on two free feeds
A new MISP already knows about two public feeds in MISP’s own format, both switched off: the CIRCL OSINT Feed (run by the Luxembourg CERT that leads the MISP project) and Botvrij.eu. List them and turn both on.
$ misp $MISP/feeds | jq -r '.[].Feed | "\(.id) enabled=\(.enabled) \(.source_format) \(.name)"' $ misp -X POST $MISP/feeds/enable/1 | jq -r .message $ misp -X POST $MISP/feeds/enable/2 | jq -r .message
1 enabled=false misp CIRCL OSINT Feed 2 enabled=false misp The Botvrij.eu Data Feed enabled. Feed enabled.
Both feeds show enabled=false at first. In the web interface the same list is Sync Actions → Feeds, where the button Load default feed metadata adds about a hundred more free feeds (that is where the “million IoCs” videos get their numbers).
OSINT is a collection method. These are open-source intelligence feeds: anyone can read them, which is great for learning and for catching well-known threats, and also means attackers can read them too. Paid (closed-source) feeds and sharing communities such as ISACs add sources the public can’t see. CySA+ lists both, plus confidence: is the source timely, relevant to you, and accurate?
07Fetch the feeds
Fetching copies each published report into your MISP as a normal event. Botvrij is small, so fetch it from the command line and wait. CIRCL’s feed is roughly four times bigger, so hand it to MISP’s background workers and carry on.
$ time docker compose exec -u www-data misp-core /var/www/MISP/app/Console/cake Server fetchFeed 1 2 $ misp $MISP/users/statistics | jq -c '.stats | {event_count, attribute_count, correlation_count}' $ misp -X POST $MISP/feeds/fetchFromFeed/1 | jq -r .result
Job done.
real 2m7.654s
user 0m0.058s
sys 0m0.038s
{"event_count":"435","attribute_count":"28914","correlation_count":"219"}
Pull queued for background execution.
The arguments are the user (1 = admin) and the feed (2 = Botvrij). Job done. means it finished. The statistics show what arrived. The last command only queues the CIRCL download, so it returns at once; the event count keeps climbing while you work through the next steps.
Correlation is the quiet superpower. The correlation_count is the number of times MISP noticed the same value in two different events. That is how an analyst spots that today’s phishing domain was also in a report from last spring. In production, feeds are fetched on a schedule (this setup does it daily by default) rather than by hand.
08Search for an IOC
Pretend a proxy log just showed a workstation contacting api.skycloudcenter.com. Is that name known? Ask MISP. The first search is an exact match; the second uses % as a wildcard and also pulls the event’s tags.
$ misp -X POST $MISP/attributes/restSearch -d '{"value": "api.skycloudcenter.com"}' | jq -r '.response.Attribute[] | "event \(.event_id) \(.type) \(.value) to_ids=\(.to_ids) | \(.Event.info)"' $ misp -X POST $MISP/attributes/restSearch -d '{"value": "%skycloudcenter%", "includeEventTags": true}' | jq '.response.Attribute[0] | {event_id, category, type, value, date: .Event.date, org: .Event.Orgc.name, tag_count: (.Tag | length), first_tags: [.Tag[]?.name][0:3]}'
event 435 hostname api.skycloudcenter.com to_ids=true | The Chrysalis Backdoor: A Deep Dive into Lotus Blossom’s toolkit
{
"event_id": "435",
"category": "Network activity",
"type": "hostname",
"value": "api.skycloudcenter.com",
"date": "2026-02-03",
"org": "CUDESO",
"tag_count": 17,
"first_tags": [
"tlp:clear",
"misp-galaxy:threat-actor=\"LOTUS PANDA\"",
"misp-galaxy:mitre-attack-pattern=\"Malicious File - T1204.002\""
]
}
The name is in a Botvrij event about the Chrysalis backdoor, with to_ids=true, which means the author flagged it as good enough to detect on. The second search shows when the report was published, who wrote it, and the first of its tags: a TLP label, a threat-actor galaxy and ATT&CK techniques. Only search inside MISP; this is a real indicator, so don’t visit it.
From one clue to a whole story. That is enrichment: a bare domain from a log became a named actor, a date, a report to read and a list of techniques to hunt for on the same machine. Notice the date too. Timeliness is part of confidence; an IP address from a 2015 report may belong to someone innocent today. In the web interface the same search is the box at the top right, or Event Actions → Search Attributes.
09Write up your own event, with TLP and ATT&CK
Now be the analyst who found something. Your pretend phishing campaign has two IP addresses, a domain, a URL and a file hash, all of them reserved for documentation or made up. First switch on the TLP labels, then send the event as JSON, then switch on two warninglists and see what they say.
$ TLP=$(misp $MISP/taxonomies | jq -r '.[].Taxonomy | select(.namespace=="tlp") | .id'); misp -X POST $MISP/taxonomies/enable/$TLP | jq -r .message $ cat > ~/sg-event.json <<'EOF' {"Event": { "info": "SG LAB - fake invoice phishing (training data, not real)", "distribution": 0, "threat_level_id": 3, "analysis": 2, "Tag": [{"name": "tlp:green"}, {"name": "misp-galaxy:mitre-attack-pattern=\"Spearphishing Link - T1566.002\""}], "Attribute": [ {"type": "ip-dst", "category": "Network activity", "value": "203.0.113.45", "to_ids": true, "comment": "fake C2, RFC 5737 documentation range"}, {"type": "ip-dst", "category": "Network activity", "value": "198.51.100.23", "to_ids": true, "comment": "fake download server, RFC 5737"}, {"type": "domain", "category": "Network activity", "value": "invoice-portal.example.com", "to_ids": true, "comment": "fake phishing domain, RFC 2606"}, {"type": "url", "category": "Network activity", "value": "https://invoice-portal.example.com/pay/confirm", "to_ids": true}, {"type": "sha256", "category": "Payload delivery", "value": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "to_ids": true, "comment": "made-up hash"} ]}} EOF $ misp -X POST $MISP/events/add -d @$HOME/sg-event.json | tee ~/sg-event-out.json | jq '{id: .Event.id, attributes: .Event.attribute_count, tags: [.Event.Tag[].name], galaxy: [.Event.Galaxy[].GalaxyCluster[].value]}' $ EV=$(jq -r .Event.id ~/sg-event-out.json); echo "your event is number $EV" $ misp -X POST $MISP/warninglists/toggleEnable -d '{"name": ["List of RFC 5735 CIDR blocks", "List of RFC 6761 Special-Use Domain Names"], "enabled": true}' | jq -r .success $ misp -X POST $MISP/attributes/restSearch -d "{\"eventid\": $EV, \"includeWarninglistHits\": true}" | jq -r '.response.Attribute[] | "\(.value) -> \([.warnings[]?.warninglist_name] | join(", ") | if . == "" then "no warning" else . end)"'
Taxonomy enabled
{
"id": "482",
"attributes": "5",
"tags": [
"tlp:green",
"misp-galaxy:mitre-attack-pattern=\"Spearphishing Link - T1566.002\""
],
"galaxy": [
"Spearphishing Link - T1566.002"
]
}
your event is number 482
2 warninglist(s) enabled
203.0.113.45 -> List of RFC 5735 CIDR blocks
198.51.100.23 -> List of RFC 5735 CIDR blocks
invoice-portal.example.com -> no warning
https://invoice-portal.example.com/pay/confirm -> no warning
deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef -> no warning
MISP answers with your new event’s number (yours will differ from ours) and confirms both tags. The ATT&CK tag shows up again under galaxy, which means MISP matched it to the full T1566.002 entry with its description and links. With the two warninglists on, MISP flags both documentation-range IP addresses. It does not flag invoice-portal.example.com, because the special-use list holds example.com itself, not every name under it.
Reading the JSON. distribution: 0 means “your organization only”, so nothing would sync out even if you had sharing partners. threat_level_id: 3 is low and analysis: 2 is completed. tlp:green says the community may see it but it shouldn’t be posted publicly. to_ids marks each value as safe to turn into a detection rule. Where these indicators sit on the Pyramid of Pain: hashes, IPs and domains are the bottom layers, trivial for an attacker to change. The ATT&CK technique (spearphishing with a link) is near the top; it is behavior, and that is what still catches them next month. The same form in the web interface is Event Actions → Add Event.
10Export it as STIX 2.1 and CSV
Other tools rarely speak MISP’s own JSON. Export your event’s indicators as a STIX 2.1 bundle (the format TAXII servers and many SIEMs read) and as CSV (for a spreadsheet or a quick import).
$ misp -X POST $MISP/attributes/restSearch -d "{\"eventid\": $EV, \"returnFormat\": \"stix2\"}" -o ~/sg-event-stix2.json $ jq -r '.type, .objects[0].spec_version' ~/sg-event-stix2.json $ jq -r '[.objects[].type] | group_by(.) | map("\(.[0]) x\(length)") | join(", ")' ~/sg-event-stix2.json $ jq -r '.objects[] | select(.type=="indicator") | .pattern' ~/sg-event-stix2.json $ jq -r '.objects[] | select(.type=="attack-pattern") | "\(.name) \(.external_references[0].external_id)"' ~/sg-event-stix2.json $ misp -X POST $MISP/attributes/restSearch -d "{\"eventid\": $EV, \"returnFormat\": \"csv\"}" -o ~/sg-event.csv; cut -d, -f4,5 ~/sg-event.csv
bundle 2.1 attack-pattern x1, domain-name x1, file x1, indicator x5, ipv4-addr x2, network-traffic x2, observed-data x5, relationship x5, url x1 [network-traffic:dst_ref.type = 'ipv4-addr' AND network-traffic:dst_ref.value = '203.0.113.45'] [network-traffic:dst_ref.type = 'ipv4-addr' AND network-traffic:dst_ref.value = '198.51.100.23'] [domain-name:value = 'invoice-portal.example.com'] [url:value = 'https://invoice-portal.example.com/pay/confirm'] [file:hashes.'SHA-256' = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'] Spearphishing Link T1566.002 type,value "ip-dst","203.0.113.45" "ip-dst","198.51.100.23" "domain","invoice-portal.example.com" "url","https://invoice-portal.example.com/pay/confirm" "sha256","deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
The bundle holds an indicator per value, each with a STIX pattern and tied to the observed-data behind it by a “based-on” relationship, plus one attack-pattern object carrying the T1566.002 reference. The CSV is one row per indicator; we cut it down to the type and value columns.
What we found. We exported through /attributes/restSearch because the obvious route, /events/restSearch with STIX, came back as an empty bundle on this version (MISP 2.5.51) for every event that carried a galaxy tag; MISP’s log said the STIX converter refused to load the event. The attribute route converted the same data fine, galaxy included, but it does not carry the event-level TLP tag across as a STIX marking. Check that before you share STIX with anyone. TAXII is the next layer: a TAXII 2.1 server (OASIS’s open-source Medallion is the reference one) publishes these bundles so other tools can pull them on a schedule, the way you pulled feeds in step 7; MISP itself can push to one from Sync Actions → TAXII Servers.
11Feed a SIEM: pull a clean blocklist over the API
This is the bridge to everything else in a SOC. A SIEM, firewall or EDR asks MISP for “every indicator safe to detect on, of these types, shared at this level, changed recently”, as plain text, one per line. Try it on your event, publish the event, and try again. Then ask for the whole day’s list, first raw, then with warninglists enforced.
$ misp -X POST $MISP/attributes/restSearch -d "{\"eventid\": $EV, \"returnFormat\": \"text\", \"type\": \"ip-dst\"}"; echo "(end of list)" $ misp -X POST $MISP/events/publish/$EV | jq -r .message $ sleep 10; misp -X POST $MISP/attributes/restSearch -d "{\"eventid\": $EV, \"returnFormat\": \"text\", \"type\": \"ip-dst\"}" $ misp -X POST $MISP/attributes/restSearch -d '{"returnFormat": "text", "type": ["ip-dst", "domain"], "to_ids": 1, "tags": ["tlp:green"], "last": "1d"}' > ~/blocklist.txt; wc -l < ~/blocklist.txt; grep -E '203\.0\.113|198\.51\.100|example\.com' ~/blocklist.txt $ misp -X POST $MISP/attributes/restSearch -d '{"returnFormat": "text", "type": ["ip-dst", "domain"], "to_ids": 1, "tags": ["tlp:green"], "last": "1d", "enforceWarninglist": true}' > ~/blocklist.txt; wc -l < ~/blocklist.txt; grep -E '203\.0\.113|198\.51\.100|example\.com' ~/blocklist.txt $ misp $MISP/users/statistics | jq -c '.stats | {event_count, attribute_count}'
(end of list)
Job queued
203.0.113.45
198.51.100.23
4
203.0.113.45
198.51.100.23
invoice-portal.example.com
2
invoice-portal.example.com
{"event_count":"510","attribute_count":"44927"}
Before publishing, your event isn’t in the text export at all (just an empty line); after publishing, its two IP addresses are. The day’s list counts everything published in the last day and tagged tlp:green (the last filter goes by publish time, not by the report’s own date), so your count will differ from our 4, depending on how far the CIRCL download has got. Adding enforceWarninglist drops your two documentation-range addresses (and anything else on an enabled warninglist) and keeps the example.com name, which no enabled list covers.
Two lessons SOCs learn the hard way. First, publishing is the switch: a draft event is invisible to the tools that matter, which is a safety feature. Second, raw feeds contain things you must never block, such as reserved ranges, your own cloud provider or a public DNS resolver. Enforcing warninglists in the export is how you avoid an outage caused by your own threat intel. A Splunk or Wazuh job would run this same call every hour and load the result into a lookup table, then alert when a log line matches.
12Clean up
Stop and delete the containers and their volumes (the -v), delete the images MISP brought, then remove the folder, your key file and the exported files. sudo is needed for the folder because the containers created files in it as their own users.
$ cd ~/misp-docker && docker compose down -v $ docker image rm $(docker compose config --images) $ cd ~ && sudo rm -rf ~/misp-docker ~/.misp_key ~/sg-event.json ~/sg-event-out.json ~/sg-event-stix2.json ~/sg-event.csv ~/blocklist.txt $ docker ps -a; docker volume ls; docker images $ unset KEY
... Container misp-docker-misp-modules-1 Removed Volume misp-docker_mysql_data Removed Volume misp-docker_misp_guard_ca Removed Volume misp-docker_cache_data Removed Network misp-docker_default Removed Untagged: ghcr.io/misp/misp-docker/misp-core:latest Deleted: sha256:03c0b1d0b9e2e3c8261b987e9e911e8efec139d0ece4337285e5175b68817a31 ... Untagged: mariadb:10.11 Deleted: sha256:d769e37960821dfdcf096954f6e7ac89f747e116c1bba98e48edd265dfc7deae CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES DRIVER VOLUME NAME IMAGE ID DISK USAGE CONTENT SIZE EXTRA
The last command should show empty lists (or only containers and images you had before this lab). Afterwards Docker reported no images, containers or volumes left, and our disk use was back below where it started.
Don’t forget the key. Deleting MISP kills its keys, but the copy in ~/.misp_key and in your shell’s memory would still be lying around, which is why the last two commands remove both. Want to keep MISP instead? Run docker compose stop now and docker compose start later; your events and feeds survive in the volumes.
What you learned
- Threat intelligence platforms — MISP stores events, indicators, tags and correlations; OTX and OpenCTI do similar jobs and are named on the exam
- Feeds and OSINT — public feeds are free and fast to start with, and always need a confidence check: timely, relevant, accurate
- IoCs — atomic indicators (IP, domain, URL, hash) are cheap for attackers to change; ATT&CK techniques describe behavior and last longer
- TLP — red, amber, green, clear: how far a report may travel
- STIX and TAXII — STIX 2.1 is the JSON format, TAXII is how it is served; export and check what your tool actually emits
- APIs for enrichment — a key in a header, a restSearch call, and warninglists enforced, which is how intel reaches a SIEM safely
Tried it? Tell us
Nova built this lab from Steven’s ideas, and we ran every step once before publishing. If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.
NextYARA rules: describe malware so tools can find it→
Go deeper
- Splunk lab for beginners — search logs, then imagine a lookup table filled from step 11. Splunk add-ons for MISP can do exactly that
- Wazuh lab — a free SIEM in Docker; a custom integration script can check its alerts against MISP with the same restSearch call
- AlienVault OTX — a free hosted threat-intel community (needs a free account and its own API key); misp-modules (already running in this lab) includes an OTX enrichment module that uses that key
- Incident response lifecycle — where threat intel fits in preparation, detection and lessons learned
- Docker security lab — install Docker properly and lock down containers like the ones in this lab
- CompTIA CySA+ roadmap — this lab covers Domain 1, Security Operations: objectives 1.3 (threat-intelligence platforms), 1.4 (threat intelligence and IoCs) and 1.5 (data enrichment and APIs)
- MISP REST API reference — every endpoint and every restSearch filter
- Taylor Walton: MISP install, 1 million+ free IoCs in 10 minutes — a video walkthrough that loads many more feeds, from the SOCFortress channel
Original walkthrough. Every command on this page was run end to end on 5 Nov 2026 on Ubuntu 24.04 in WSL2 (16 threads, 15.5 GB RAM) with Docker Engine 29.9.3 and Compose v2.40.3, using misp-docker commit 27ec05e (MISP 2.5.51; the template pins misp-modules v3.1.3); outputs are from that run, trimmed where marked with ..., and the password step was also done in the web interface. Your event numbers, counts and timings will differ, and feed contents change daily. With the caps from step 2, the six containers together peaked at about 2.6 GB of RAM (sampled every 20 seconds, with the CIRCL download running) and used about 6.4 GB of disk (4.7 GB of images, 1.7 GB of database). The video linked above is Taylor Walton’s; this page is our own walkthrough, not a copy of it. StashGrid isn’t affiliated with CompTIA, CIRCL or the MISP project. If something doesn’t match what your machine does, tell me and I will fix it.