Events are automatically converted to enriched IOCs shared with the community.
1. Overview
CHP (CRAABS Honeypot Protocol) allows any honeypot to forward events to CRAABS via a simple REST API. For each event received, the platform:
- Creates or enriches an IOC from the attacker IP
- Calculates a malicious score based on event type and frequency
- Maps automatically to the corresponding MITRE ATT&CK techniques
- Geolocates the source IP
- Triggers alerts if the score is critical
2. Getting a CHP key
Before writing any code you need a node key. The flow is deliberately manual, because a key grants write access to shared community intelligence.
- Sign in with a Developer or Pro account and open SOC > Honeypot Network.
- Register a new honeypot: give it a name, a type and a location. The location is a label for you, not a geolocation.
- Copy the generated
chp_...key. It is shown in full only once. - Store it as an environment variable on the sensor, never in the source tree.
- Send a first heartbeat. The node stays
pendinguntil an administrator approves it, and events sent before approval are rejected with403.
ping endpoint first. It exercises
authentication and network path without writing anything into the community database, so a
misconfigured client cannot pollute the shared dataset while you debug it.3. Authentication
Every request must include the CHP key in the X-Honeypot-Key header.
X-Honeypot-Key: chp_a1b2c3d4e5f6...
The key is prefixed with chp_ and is 60 characters long. Never log it in plaintext or commit it to source control.
For clients that cannot set a custom header, the key is also accepted as a bearer token:
Authorization: Bearer chp_a1b2c3d4e5f6...
4. Endpoints
GET/api/honeypot/ping
Heartbeat. Signals that the node is active. Call every 5 minutes.
curl -H "X-Honeypot-Key: chp_..." https://cti.craabs.com/api/honeypot/ping
# Response 200
{"status": "ok", "node": "SSH-Trap", "accepted": true, "protocol": "CHP/1.0"}
POST/api/honeypot/event
Sends a single event. Use this for real-time forwarding.
POST/api/honeypot/events/bulk
Sends up to 100 events in one request. Recommended for high-volume sensors.
# Body: {"events": [ {event}, {event}, ... ]}
# Response 201: {"accepted": 95, "rejected": 2, "iocs_created": 88}
GET/api/honeypot/status
Returns statistics for your node: event count, IOCs generated, last seen.
5. Event format
| Field | Type | Required | Description | Values |
|---|---|---|---|---|
| source_ip | string | Required | Attacker IP address | Valid IPv4 |
| honeypot_type | string | Optional | Lured service type | ssh, http, ftp, rdp, smtp, telnet, mysql, vnc, sip, modbus, generic, custom |
| event_type | string | Optional | Event nature | login_attempt, brute_force, exploit, payload, scan, connect, malware, command, generic |
| timestamp | string | Optional | ISO 8601 timestamp | 2026-05-14T10:30:00Z |
| source_port | integer | Optional | Attacker source port | 1-65535 |
| dest_port | integer | Optional | Destination port | 22, 80, 443... |
| username | string | Optional | Attempted username | max 200 chars |
| password | string | Optional | Attempted password | max 500 chars |
| payload | string | Optional | Raw payload (base64 recommended) | free text |
| severity | string | Optional | Estimated severity | low, medium, high, critical |
| tags | array | Optional | Additional tags | ["mirai", "scanner"] |
Unknown enum values are replaced, not rejected
If honeypot_type, event_type or severity holds a value
outside the list above, the request still succeeds but the field is silently rewritten to
custom, generic and medium respectively. This keeps a
misconfigured sensor from losing data, but it also means a typo will not raise an error.
Check the values you send: "ssh2" is stored as custom, not as an error.
Response to a single event
{
"status": "accepted",
"event_id": 42,
"ioc_id": 1337,
"ioc_value": "185.220.101.47",
"score": 65,
"protocol": "CHP/1.0"
}
ioc_id, ioc_value and score are null when the
event did not produce an indicator. The bulk endpoint returns counters instead:
{"accepted": 98, "rejected": 2, "iocs_created": 41}.
What the platform does with your event
Each accepted event goes through the same pipeline, so your sensor does not have to enrich anything itself:
- Deduplication. If the source IP already exists as an indicator, the existing record is reused rather than duplicated, and its score is raised.
- Scoring. A base score is derived from the event type, then increased when the same address is seen repeatedly, so persistent attackers rise above one-off scanners.
- Geolocation. Country, city, ASN and ISP are resolved automatically. Sending
countryyourself is optional and only used as a hint. - Sharing. The resulting indicator becomes available to the community, to the REST API and to SIEM feeds.
username and
password fields are stored to support pattern analysis across attackers. Send only
what your decoy actually captured. Never forward credentials from a real system, and never send
payloads containing personal data: this is shared community intelligence, not a private log store.6. Code examples
Python
import requests
from datetime import datetime, timezone
URL = "https://cti.craabs.com"
KEY = "chp_your_key_here"
HEADERS = {"X-Honeypot-Key": KEY, "Content-Type": "application/json"}
def send_event(source_ip, honeypot_type, event_type, **kwargs):
payload = {
"source_ip": source_ip,
"honeypot_type": honeypot_type,
"event_type": event_type,
"timestamp": datetime.now(timezone.utc).isoformat(),
**kwargs
}
r = requests.post(f"{URL}/api/honeypot/event", headers=HEADERS, json=payload, timeout=5)
return r.json()
# Heartbeat (call every 5 minutes)
def heartbeat():
requests.get(f"{URL}/api/honeypot/ping", headers=HEADERS, timeout=5)
# Example: SSH login attempt
result = send_event(
source_ip="185.220.101.47",
honeypot_type="ssh",
event_type="login_attempt",
dest_port=22,
username="root",
password="123456",
severity="medium",
tags=["brute_force"],
)
print(f"IOC #{result['ioc_id']} - score: {result['score']}")
Bash / curl
#!/bin/bash
URL="https://cti.craabs.com"
KEY="chp_your_key_here"
# Heartbeat
curl -s -H "X-Honeypot-Key: $KEY" "$URL/api/honeypot/ping"
# Send event
curl -s -X POST \
-H "X-Honeypot-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"source_ip": "185.220.101.47",
"honeypot_type": "ssh",
"event_type": "login_attempt",
"dest_port": 22,
"username": "admin",
"severity": "medium"
}' \
"$URL/api/honeypot/event"
Node.js
const axios = require('axios');
const client = axios.create({
baseURL: 'https://cti.craabs.com',
headers: { 'X-Honeypot-Key': 'chp_your_key_here' },
timeout: 5000,
});
async function sendEvent(event) {
const res = await client.post('/api/honeypot/event', {
...event,
timestamp: new Date().toISOString(),
});
return res.data;
}
// Heartbeat every 5 minutes
setInterval(() => client.get('/api/honeypot/ping'), 300_000);
// Example
sendEvent({
source_ip: '185.220.101.47',
honeypot_type: 'http',
event_type: 'scan',
dest_port: 80,
severity: 'low',
}).then(r => console.log('IOC:', r.ioc_id));
Go
package main
import (
"bytes"; "encoding/json"; "net/http"; "time"
)
const URL = "https://cti.craabs.com"
const KEY = "chp_your_key_here"
type Event struct {
SourceIP string `json:"source_ip"`
HoneypotType string `json:"honeypot_type"`
EventType string `json:"event_type"`
Timestamp string `json:"timestamp"`
DestPort int `json:"dest_port,omitempty"`
Severity string `json:"severity"`
Tags []string `json:"tags,omitempty"`
}
func SendEvent(e Event) error {
e.Timestamp = time.Now().UTC().Format(time.RFC3339)
body, _ := json.Marshal(e)
req, _ := http.NewRequest("POST", URL+"/api/honeypot/event", bytes.NewBuffer(body))
req.Header.Set("X-Honeypot-Key", KEY)
req.Header.Set("Content-Type", "application/json")
_, err := http.DefaultClient.Do(req)
return err
}
Cowrie SSH Honeypot integration
# cowrie_to_craabs.py - tail Cowrie JSON log and forward to CRAABS
import json, time, requests
KEY = "chp_your_key_here"
LOG = "/home/cowrie/var/log/cowrie/cowrie.json"
HEADERS = {"X-Honeypot-Key": KEY}
EVENT_MAP = {
"cowrie.login.failed": "login_attempt",
"cowrie.login.success": "login_success",
"cowrie.command.input": "command",
"cowrie.session.file_download":"payload",
}
def tail_log():
with open(LOG) as f:
f.seek(0, 2)
while True:
line = f.readline()
if line:
yield json.loads(line.strip())
else:
time.sleep(0.5)
for entry in tail_log():
if not entry.get("src_ip"):
continue
requests.post("https://cti.craabs.com/api/honeypot/event",
headers=HEADERS, timeout=5,
json={
"source_ip": entry["src_ip"],
"honeypot_type": "ssh",
"event_type": EVENT_MAP.get(entry.get("eventid"), "generic"),
"dest_port": 22,
"username": entry.get("username"),
"password": entry.get("password"),
"payload": entry.get("input"),
"timestamp": entry.get("timestamp"),
"severity": "high" if "success" in entry.get("eventid","") else "medium",
"tags": ["cowrie", "ssh"],
}
)
7. Error codes
| HTTP code | Meaning | Action |
|---|---|---|
| 201 | Event accepted | Continue normally |
| 400 | Invalid JSON or missing source_ip | Fix the payload |
| 401 | Missing or invalid CHP key | Check X-Honeypot-Key |
| 403 | Node not approved | Contact the platform administrator |
| 429 | Rate limit exceeded | Back off, switch to bulk mode, retry after 60s |
| 500 | Server error | Retry in 30s |
Errors are returned as JSON with an error field describing the problem, for
example {"error": "source_ip is required"}. A 403 specifically means
the node exists but is still pending administrator approval: the key is valid, so do not
regenerate it, simply wait for approval.
429 as a contract even if you rarely see it. Throttling
may be tightened as the network grows, and a client that already backs off correctly will keep
working unchanged. A sensor that assumes unlimited throughput will not.8. Best practices
- Send the heartbeat every 5 minutes so the platform can detect if your node goes offline.
- Use bulk mode if your honeypot generates more than 10 events per minute.
- Never hardcode the CHP key in public code. Use environment variables.
- Handle 429 and 500 errors with exponential backoff: wait 5s, then 15s, then 60s.
- Include the actual event timestamp, not the send time.
- For exploits and payloads, encode in base64 to avoid JSON encoding issues.
- Use
criticalseverity only for successful exploits or confirmed malware downloads. - Buffer to disk, not to memory. If the platform is unreachable, a sensor that queues in RAM loses everything on restart, which is exactly when an attacker is most active.
- Do not retry a 400 or a 401. These mean your payload or key is wrong; retrying the
same request forever only fills your logs. Retry
429and500only. - Cap your own send rate. A honeypot under a volumetric attack can generate thousands of events per second. Aggregate or sample before sending, otherwise your sensor becomes the noise it was meant to detect.
9. Limits and versioning
| Constraint | Value |
|---|---|
| Events per bulk request | 100 maximum |
| Heartbeat interval | 5 minutes recommended |
| username field | 200 characters |
| password field | 500 characters |
| tags field | 500 characters once joined |
| Transport | HTTPS only |
| Protocol version | CHP/1.0, returned in every response |
Values longer than the limits above are truncated rather than rejected, so an oversized payload will not cost you the event, but it will cost you the tail of the data.
The protocol follows additive versioning: new optional fields may appear in a minor release, but existing field names and semantics will not change without a major version bump. Parse responses tolerantly, ignoring unknown keys, and your connector will keep working across upgrades.