XLog is a security testing and simulation platform that combines synthetic log generation, scenario-based attack telemetry, and AI-orchestrated workflows through MCP.
- Synthetic log generation in SYSLOG, CEF, LEEF, WINEVENT, JSON, Incident, XSIAM Parsed, and XSIAM CEF formats.
- Scenario-based telemetry with multi-step MITRE ATT&CK tactics from JSON scenarios or on-the-fly scenario input.
- Streaming workers that continuously send logs to UDP, TCP, HTTP(S), XSIAM PAPI, or XSIAM webhook collectors.
- Field catalog and observables tooling to discover supported fields and generate realistic observables and technology stacks.
- Simulation skills library with CRUD tooling for skill files (foundation, scenarios, validation, workflows).
- MCP integrations for CALDERA (abilities, adversaries, operations, agents, payloads) and XSIAM (XQL, datasets, lookups, assets, cases, issues).
- Web agent for chat-based orchestration and skills management built in Next.js with Gemini/Vertex support.
xlog/
├── app/ # GraphQL API for log generation and workers
├── scenarios/ # Scenario definitions (ready and drafts)
├── mcp/
│ ├── server/ # MCP server exposing XLog, CALDERA, XSIAM tools
│ └── agent/ # Next.js MCP chat and skills UI
├── scripts/ # Utility scripts
├── examples/ # GraphQL request examples
└── img/ # Documentation images
- Create your environment file:
cp .env.example .env
- Edit
.envwith your values.- Choose a strong shared token and set
MCP_TOKEN(the MCP server and agent use it for auth).
- Choose a strong shared token and set
- If using Vertex AI, create a GCP service account and grant Vertex AI access (for example
Vertex AI User), then put its JSON credentials in.envunderGOOGLE_APPLICATION_CREDENTIALS. - Start the full stack with Docker Compose:
docker compose up -d
- Verify containers are running (and healthy when healthchecks are defined):
docker compose ps docker inspect --format '{{.Name}} -> {{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' xlog xlog_mcp xlog_agent caldera - Verify services:
- XLog GraphQL API:
http://localhost:8999/ - MCP server:
http://localhost:8080/ - Agent UI:
http://localhost:3000/ - Caldera:
http://localhost:8888/
- XLog GraphQL API:
- Stop the stack when needed:
docker compose down
- Base template:
.env.example - Place
.envin the same directory asdocker-compose.yml(project root). If needed, usedocker compose --env-file <path> up -d. - Set
MCP_TOKENto a strong random value (long, hard to guess). - For Vertex AI auth, use a GCP service account JSON credential in
GOOGLE_APPLICATION_CREDENTIALS(service account must have Vertex AI permissions). - Keep
MCP_URL=http://xlog-mcp:8080/api/v1/stream/mcpfor container-to-container communication. - For external clients, set
MCP_URLto the host IP running the containers (do not uselocalhostor127.0.0.1), for example:http://10.10.0.6:8080/api/v1/stream/mcp. - Set credentials and tokens before startup (for example:
MCP_TOKEN,UI_USER,UI_PASSWORD, and your model/API credentials).
- If you use the Caldera container included in this compose stack, set
CALDERA_URL=http://caldera:8888in.env(container-to-container URL). - To access Caldera UI from another machine, use the IP of the host running Docker, for example:
http://10.10.0.6:8888(notlocalhostfrom a remote machine). - If you do not use the bundled Caldera image and instead use a remote Caldera server, set
CALDERA_URLto that remote host URL, for example:http://<remote-caldera-ip>:8888.
TECHNOLOGY_STACK in .env is a JSON object used by the agent to understand your environment and choose defaults.
Most important requirement:
- Update
log_destinationto your actual syslog receiver. - The agent uses
log_destination.full_addressas the default destination when sending simulated logs if the user does not explicitly provide a destination.
Example:
{
"stack_name": "Enterprise Security Stack",
"log_destination": {
"type": "syslog",
"protocol": "udp",
"host": "10.10.0.8",
"port": 514,
"full_address": "udp:10.10.0.8:514"
},
"vendors": [
{
"vendor": "Fortinet",
"product": "FortiGate",
"category": "Firewall",
"formats": ["CEF", "SYSLOG", "JSON"]
}
]
}If this value is not updated, simulated logs may be sent to the wrong default syslog destination.
Use one-line PEM values in .env (newlines escaped as \\n).
Windows PowerShell:
& {
# 1. Create the Certificate
$cert = New-SelfSignedCertificate -DnsName "localhost" `
-CertStoreLocation "cert:\CurrentUser\My" `
-NotAfter (Get-Date).AddYears(1) `
-KeyExportPolicy Exportable
# 2. Export Certificate to PEM
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$certB64 = [Convert]::ToBase64String($certBytes, [System.Base64FormattingOptions]::InsertLineBreaks)
$certPem = "-----BEGIN CERTIFICATE-----`n$certB64`n-----END CERTIFICATE-----"
$certPem | Set-Content -Path "localhost.crt"
# 3. Export Private Key to PKCS#8 PEM
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
$keyBytes = $null
if ($rsa.PSObject.Methods.Name -contains "ExportPkcs8PrivateKey") {
$keyBytes = $rsa.ExportPkcs8PrivateKey()
}
elseif ($rsa -is [System.Security.Cryptography.RSACng]) {
$keyBytes = $rsa.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
}
if ($null -ne $keyBytes) {
$keyB64 = [Convert]::ToBase64String($keyBytes, [System.Base64FormattingOptions]::InsertLineBreaks)
$keyPem = "-----BEGIN PRIVATE KEY-----`n$keyB64`n-----END PRIVATE KEY-----"
$keyPem | Set-Content -Path "localhost.key"
# 4. Generate .env safe strings
$certEnv = $certPem -replace "`r?`n", "\n"
$keyEnv = $keyPem -replace "`r?`n", "\n"
Write-Host "`n--- COPY THESE INTO YOUR .ENV FILE ---`n" -ForegroundColor Green
Write-Output "SSL_CERT_PEM=""$certEnv"""
Write-Output "SSL_KEY_PEM=""$keyEnv"""
} else {
Write-Error "Failed to extract private key bytes."
}
}Linux:
# Generate self-signed cert/key for localhost
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
-keyout localhost.key -out localhost.crt -subj "/CN=localhost"
# Convert files to .env-safe one-line values
CERT_ENV=$(awk 'NF {sub(/\r/, ""); printf "%s\\\\n",$0;}' localhost.crt)
KEY_ENV=$(awk 'NF {sub(/\r/, ""); printf "%s\\\\n",$0;}' localhost.key)
echo "SSL_CERT_PEM=${CERT_ENV}"
echo "SSL_KEY_PEM=${KEY_ENV}"Copy the printed SSL_CERT_PEM=... and SSL_KEY_PEM=... lines into your .env.
To enable XSIAM operations from the MCP server and agent:
- In Cortex XSIAM, create a new Standard API Key.
- Add the API key values to
.env:CORTEX_MCP_PAPI_URLCORTEX_MCP_PAPI_AUTH_HEADERCORTEX_MCP_PAPI_AUTH_ID
- In Cortex XSIAM, create a new issue that will be used as the remote execution context (issue war room).
- Put that issue ID in
.envas:PLAYGROUND_ID
PLAYGROUND_ID is required for agent-triggered remote XSIAM command execution in the issue war room context.
Use webhook delivery when you want logs sent over HTTP as an additional mechanism alongside SYSLOG/TCP/UDP destinations.
- Create a Cortex XSIAM HTTP Collector (or any compatible webhook collector).
- For Cortex XSIAM HTTP Collector, configure:
- Compression: disabled (
uncompressed) - Log format:
json - Vendor/Product: any values that match your use case
- Compression: disabled (
- Copy the collector endpoint URL and authentication key.
- Set in
.env:WEBHOOK_ENDPOINT=<collector_endpoint_url>WEBHOOK_KEY=<collector_authentication_key>
Authentication header behavior in XLog:
- XLog sends the webhook key in header:
Authorization - Source:
app/schema.py(_get_webhook_headers)
If you use a non-XSIAM webhook collector, it must accept the same Authorization header for auth, or you need to adapt the receiver/proxy to map this header accordingly.
The GraphQL endpoint is served at http://localhost:8999/ when running via Docker Compose.
getSupportedFields-> returns the supported fields list used for observables and required fields.generateFakeData(requestInput: DataFakerInput)-> returns a batch of synthetic logs.generateScenarioFakeData(requestInput: DetailedScenarioInput)-> returns multi-step scenario logs without starting workers.createDataWorker(requestInput: DataWorkerCreateInput)-> starts a streaming worker.createScenarioWorker(requestInput: ScenarioWorkerCreateInput)-> starts workers from ascenarios/ready/*.jsonfile.createScenarioWorkerFromQuery(requestInput: ScenarioQueryWorkerCreateInput)-> starts workers from inline scenario steps.listWorkers-> lists active workers.actionWorker(requestInput: DataWorkerActionInput)-> stops a worker or checks status.generateObservables(requestInput: GenerateObservablesInput)-> generates observables from threat intel feeds.
type(required):SYSLOG,CEF,LEEF,WINEVENT,JSON,Incident,XSIAM_Parsed,XSIAM_CEFcount(default1)vendor,product,versiondatetimeIso(YYYY-MM-DD HH:MM:SS)fields(comma-separated field list)observablesDict(object of supported fields)requiredFields(list of supported field enums)
name(required)tags(optional list)steps(list ofDetailedScenarioStep)
DetailedScenarioStep:
tactic,tacticId,technique,techniqueId,procedure,typelogs(list ofDataFakerInput)
type(required):SYSLOG,CEF,LEEF,WINEVENT,JSON,Incident,XSIAM_Parsed,XSIAM_CEFdestination(required):udp:host:port,tcp:host:port,https://...,XSIAM, orXSIAM_WEBHOOKcount(default1),interval(default2)vendor,product,versionfields,observablesDict,requiredFields,datetimeIsoverifySsl(defaultfalse)
When sending logs to a XSIAM custom HTTP collector (for example, alongside a SYSLOG destination), configure the collector with:
- Compression:
uncompressed - Log format:
json
scenario(required): filename without.jsoninscenarios/ready/destination(required)count,interval,vendor,datetimeIso,verifySsl
name(required)destination(required)tags(optional list)steps(list ofDetailedQueryScenarioStep)
DetailedQueryScenarioStep:
tactic,tacticId,technique,techniqueId,procedure,typelogs(list ofWorkerFakerInput)
WorkerFakerInput:
type(required),count,intervalvendor,product,versiondatetimeIso,fields,observablesDict,requiredFields,verifySsl
worker(required)action(required):STOPorSTATUS
count(required)observableType(required):IP,URL,SHA256,CVE,TERMSknown(defaultBAD):BADorGOOD
Generate fake data (SYSLOG):
query Example($input: DataFakerInput!) {
generateFakeData(requestInput: $input) {
count
type
data
}
}Generate fake data with observables (JSON):
query ExampleJson($input: DataFakerInput!) {
generateFakeData(requestInput: $input) {
count
type
data
}
}Example variables:
{
"input": {
"type": "JSON",
"count": 2,
"datetimeIso": "2025-01-05 18:29:25",
"observablesDict": {
"remoteIp": "203.0.113.10",
"localIp": "10.0.0.5",
"user": "svc-backup",
"url": "https://example.com/login"
}
}
}Create a worker:
query CreateWorker($input: DataWorkerCreateInput!) {
createDataWorker(requestInput: $input) {
worker
status
type
destination
}
}List workers:
query ListWorkers {
listWorkers {
worker
status
type
interval
destination
}
}Stop a worker:
query StopWorker($input: DataWorkerActionInput!) {
actionWorker(requestInput: $input) {
worker
status
}
}Generate scenario fake data:
query Scenario($input: DetailedScenarioInput!) {
generateScenarioFakeData(requestInput: $input) {
name
steps
}
}scenarios/README.md- Scenario format and examplesmcp/server/README.md- MCP server capabilities and setupmcp/agent/README.md- Next.js agent setup and usagemcp/server/skills/README.md- Skill library structure
