mirror of
https://github.com/wazuh/wazuh-indexer-plugins.git
synced 2025-12-10 14:32:28 -06:00
Implement Imposter mock server for CTI API (#661)
* Implement Imposter's mock for CTI endpoints * Update CHANGELOG * Add scripts to test the invalid scenarios * Update README * Fix typo * Improve directory structure and files naming Sanitized API references Removed filtered endpoints Add parametrization to servers urls * Add instance/me endpoint mock * Fix servers section * Implement ssl support * Simplify imposter environment, remove non-secure option * Update documentation * Add retries system to the Imposter environment * Implement retry system for pending-granted/rejected scenarios * Update catalog download example response to match real world --------- Co-authored-by: Alex Ruiz <alejandro.ruiz.becerra@wazuh.com>
This commit is contained in:
parent
f075ae7132
commit
cf87f635c6
@ -31,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
- Implement pre-processing ECS sources types sanitization [(#628)](https://github.com/wazuh/wazuh-indexer-plugins/pull/628)
|
||||
- Add Security Compliance fields to the WCS [(#643)](https://github.com/wazuh/wazuh-indexer-plugins/pull/643)
|
||||
- Initialize indexer content manager [(#651)](https://github.com/wazuh/wazuh-indexer-plugins/pull/651)
|
||||
- Implement Imposter mock server for CTI API [#661](https://github.com/wazuh/wazuh-indexer-plugins/pull/661)
|
||||
|
||||
### Dependencies
|
||||
- Upgrade to Gradle 8.14.3 [(#649)](https://github.com/wazuh/wazuh-indexer-plugins/pull/649)
|
||||
|
||||
8
plugins/content-manager/imposter/.gitignore
vendored
Normal file
8
plugins/content-manager/imposter/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Ignore SSL certificates and keys
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.csr
|
||||
|
||||
# Ignore OpenSSL temporary files
|
||||
*.cnf
|
||||
368
plugins/content-manager/imposter/README.md
Normal file
368
plugins/content-manager/imposter/README.md
Normal file
@ -0,0 +1,368 @@
|
||||
# Imposter Mock Server
|
||||
|
||||
This directory contains the configuration for an Imposter mock server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed on your machine
|
||||
- Ports 8443 available
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
imposter/
|
||||
├── imposter.sh # Helper script to start/stop/remove the server
|
||||
├── README.md # This file
|
||||
├── imposter-config.yml # Main Imposter configuration
|
||||
├── openapi.yml # OpenAPI specification
|
||||
├── images/ # Docker Compose configurations
|
||||
│ ├── compose.yml # Docker Compose file
|
||||
│ └── nginx/ # nginx reverse proxy for SSL
|
||||
│ └── nginx.conf # nginx SSL configuration
|
||||
└── scripts/ # Groovy response scripts
|
||||
├── tokenResponse.groovy # Token request logic
|
||||
├── tokenExchangeResponse.groovy # Token exchange logic
|
||||
├── instanceMeResponse.groovy # Instance me logic
|
||||
└── catalogResponse.groovy # Catalog endpoint logic
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start the Server (Recommended)
|
||||
|
||||
From the `imposter/` directory, run:
|
||||
```bash
|
||||
./imposter.sh up
|
||||
```
|
||||
|
||||
The server will start on `https://localhost:8443`.
|
||||
|
||||
> [!NOTE]
|
||||
> This environment automatically:
|
||||
> - Generates self-signed certificates for localhost (if not already present)
|
||||
> - Starts nginx as a reverse proxy with SSL termination
|
||||
> - Proxies requests to the Imposter container on port 8443
|
||||
|
||||
### 2. Verify the Server is Running
|
||||
|
||||
```bash
|
||||
curl -k https://localhost:8443/system/status
|
||||
```
|
||||
|
||||
### 3. Managing the Server
|
||||
|
||||
**Stop the server (without removing containers):**
|
||||
```bash
|
||||
./imposter.sh stop
|
||||
```
|
||||
|
||||
**Stop and remove the server:**
|
||||
```bash
|
||||
./imposter.sh down
|
||||
```
|
||||
|
||||
## Available Endpoints
|
||||
|
||||
> [!NOTE]
|
||||
> All endpoints are served via HTTPS on `https://localhost:8443`. Use the `-k` flag with curl commands to bypass SSL certificate verification (since we use self-signed certificates).
|
||||
|
||||
### 1. Token Request (CTI Authentication)
|
||||
|
||||
Request an CTI access token:
|
||||
|
||||
```bash
|
||||
curl -k -X POST https://localhost:8443/api/v1/instances/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
|
||||
-d "client_id=a17c21ed" \
|
||||
-d "device_code=NGU5OWFiNjQ5YmQwNGY3YTdmZTEyNzQ3YzQ1YSA"
|
||||
```
|
||||
|
||||
Note that as it follows the real implementation, it returns the "pending" state a few times before returning the access token.
|
||||
|
||||
**Expected Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"access_token": "AYjcyMzY3ZDhiNmJkNTY",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Use different `device_code` values to test different authentication flows:
|
||||
>
|
||||
> | `device_code` | Behavior |
|
||||
> |--------------------|----------------------------------------------------------------------------------------------|
|
||||
> | `` (default) | Returns `authorization_pending` for the first 4 requests, then grants the token. |
|
||||
> | `pending_rejected` | Returns `authorization_pending` for the first 4 requests, then returns `access_denied` |
|
||||
> | `pending` | Always returns `authorization_pending` (simulates a user who hasn't completed authorization) |
|
||||
> | `expired` | Always returns `expired_token` error |
|
||||
> | `granted` | Immediately returns an access token without pending state |
|
||||
|
||||
### 2. Token Exchange (Get Signed URL)
|
||||
|
||||
Exchange an access token for a signed URL:
|
||||
|
||||
```bash
|
||||
curl -k -X POST https://localhost:8443/api/v1/instances/token/exchange \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Authorization: Bearer AYjcyMzY3ZDhiNmJkNTY" \
|
||||
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
|
||||
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
|
||||
-d "requested_token_type=urn:wazuh:params:oauth:token-type:signed_url" \
|
||||
-d "resource=https://localhost:8443/api/v1/catalog/contexts/misp/consumer/virustotal/changes"
|
||||
```
|
||||
|
||||
**Expected Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"issued_token_type": "urn:wazuh:params:oauth:token-type:signed_url",
|
||||
"access_token": "https://localhost:8443/api/v1/catalog/contexts/misp/consumers/virustotal/changes?from_offset=0&to_offset=1000&with_empties=true&verify=1761383411-kJ9b8w%2BQ7kzRmF",
|
||||
"token_type": "N_A",
|
||||
"expires_in": 3600
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Instance Me (Get Plan Details)
|
||||
|
||||
Get current instance details and plan information:
|
||||
|
||||
**Pro Plan Response:**
|
||||
```bash
|
||||
curl -k -X GET https://localhost:8443/api/v1/instances/me \
|
||||
-H "Authorization: Bearer pro_token" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**Expected Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"organization": {
|
||||
"avatar": "https://acme.sl/avatar.png",
|
||||
"name": "ACME S.L."
|
||||
},
|
||||
"plans": [
|
||||
{
|
||||
"name": "Pro Plan Deluxe",
|
||||
"description": "Lorem ipsum…",
|
||||
"products": [
|
||||
{
|
||||
"type": "catalog:consumer:vulnerabilities",
|
||||
"identifier": "vulnerabilities-pro",
|
||||
"name": "Vulnerabilities Pro",
|
||||
"description": "Vulnerabilities updated as soon as they are added to the catalog",
|
||||
"resource": "https://localhost:8443/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
},
|
||||
{
|
||||
"type": "catalog:consumer:iocs",
|
||||
"identifier": "bad-guy-ips-pro",
|
||||
"name": "Bad Guy IPs",
|
||||
"description": "Dolor sit amet…",
|
||||
"resource": "https://localhost:8443/api/v1/catalog/plans/pro/contexts/bad-guy-ips/consumer/realtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cloud Plan Response:**
|
||||
```bash
|
||||
curl -k -X GET https://localhost:8443/api/v1/instances/me \
|
||||
-H "Authorization: Bearer cloud_token" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**Expected Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"organization": {
|
||||
"avatar": "https://acme.sl/avatar.png",
|
||||
"name": "ACME S.L."
|
||||
},
|
||||
"plans": [
|
||||
{
|
||||
"name": "Wazuh Cloud",
|
||||
"description": "Managed instances in AWS by Wazuh's professional staf that…",
|
||||
"products": [
|
||||
{
|
||||
"identifier": "assistance-24h",
|
||||
"type": "cloud:assistance:wazuh",
|
||||
"name": "Technical assistance 24h",
|
||||
"email": "cloud@wazuh.com",
|
||||
"phone": "+34 123 456 789"
|
||||
},
|
||||
{
|
||||
"identifier": "vulnerabilities-pro",
|
||||
"type": "catalog:consumer:vulnerabilities",
|
||||
"name": "Vulnerabilities Pro",
|
||||
"description": "Vulnerabilities updated as soon as they are added to the catalog",
|
||||
"resource": "https://localhost:8443/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
},
|
||||
{
|
||||
"identifier": "bad-guy-ips-pro",
|
||||
"type": "catalog:consumer:iocs",
|
||||
"name": "Bad Guy IPs",
|
||||
"description": "Dolor sit amet…",
|
||||
"resource": "https://localhost:8443/api/v1/catalog/plans/pro/contexts/bad-guy-ips/consumer/realtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Catalog Download (Use Token)
|
||||
|
||||
Download catalog using a signed URL:
|
||||
|
||||
```bash
|
||||
curl -k -X GET "https://localhost:8443/api/v1/catalog/contexts/misp/consumers/virustotal/changes?from_offset=0&to_offset=1000&with_empties=true&verify=1761383411-kJ9b8w%2BQ7kzRmF"
|
||||
```
|
||||
|
||||
**Expected Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "create",
|
||||
"context": "misp",
|
||||
"resource": "indicator/12345",
|
||||
"offset": 42,
|
||||
"version": 1,
|
||||
"payload": {
|
||||
"type": "ip-address",
|
||||
"value": "192.168.1.100",
|
||||
"threat_level": "high",
|
||||
"timestamp": "2025-11-17T10:30:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "update",
|
||||
"context": "misp",
|
||||
"resource": "indicator/12345",
|
||||
"offset": 43,
|
||||
"version": 2,
|
||||
"operations": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/threat_level",
|
||||
"value": "critical"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "delete",
|
||||
"context": "misp",
|
||||
"resource": "indicator/12345",
|
||||
"offset": 44,
|
||||
"version": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Different Scenarios
|
||||
|
||||
### Authorization Pending
|
||||
|
||||
```bash
|
||||
curl -k -X POST https://localhost:8443/api/v1/instances/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
|
||||
-d "client_id=test_client" \
|
||||
-d "device_code=pending_code"
|
||||
```
|
||||
|
||||
**Expected Response (400 Bad Request):**
|
||||
```json
|
||||
{
|
||||
"error": "authorization_pending",
|
||||
"error_description": "The authorization request is still pending"
|
||||
}
|
||||
```
|
||||
|
||||
### Invalid Token Exchange
|
||||
|
||||
```bash
|
||||
curl -k -X POST https://localhost:8443/api/v1/instances/token/exchange \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Authorization: Bearer invalid_token" \
|
||||
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
|
||||
```
|
||||
|
||||
**Expected Response (401 Unauthorized):**
|
||||
```json
|
||||
{
|
||||
"error": "invalid_token",
|
||||
"error_description": "The access token is invalid or expired"
|
||||
}
|
||||
```
|
||||
|
||||
### Missing Authorization Header (Instance Me)
|
||||
|
||||
```bash
|
||||
curl -k -X GET https://localhost:8443/api/v1/instances/me \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**Expected Response (401 Unauthorized):**
|
||||
```json
|
||||
{
|
||||
"error": "unauthorized_client",
|
||||
"error_description": "The provided token is invalid or expired"
|
||||
}
|
||||
```
|
||||
|
||||
## Stopping the Server
|
||||
|
||||
To stop the server, use the helper script:
|
||||
|
||||
```bash
|
||||
./imposter.sh stop # Stop containers without removing them
|
||||
./imposter.sh down # Stop and remove containers
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
To modify the mock responses:
|
||||
|
||||
1. Edit the OpenAPI specifications
|
||||
2. Update examples or add new response scenarios
|
||||
3. Restart the Docker container
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Port 8443 already in use:**
|
||||
|
||||
Check what's using the port and stop it:
|
||||
```bash
|
||||
lsof -i :8443
|
||||
# Or change the port in images/compose.yml under nginx ports section
|
||||
```
|
||||
|
||||
**SSL Certificate Issues:**
|
||||
|
||||
If you need to regenerate certificates:
|
||||
```bash
|
||||
rm -rf images/nginx/certs/*
|
||||
./imposter.sh up # Will regenerate certificates automatically
|
||||
```
|
||||
|
||||
**Containers not starting:**
|
||||
|
||||
Check the logs:
|
||||
```bash
|
||||
cd images/
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Imposter Documentation](https://docs.imposter.sh/)
|
||||
- [OpenAPI Specification](https://swagger.io/specification/)
|
||||
15
plugins/content-manager/imposter/images/compose.yml
Normal file
15
plugins/content-manager/imposter/images/compose.yml
Normal file
@ -0,0 +1,15 @@
|
||||
services:
|
||||
imposter:
|
||||
image: outofcoffee/imposter-openapi
|
||||
volumes:
|
||||
- ../:/opt/imposter/config
|
||||
ports: [] # Remove port exposure when using nginx
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/certs:/etc/nginx/certs:ro
|
||||
depends_on:
|
||||
- imposter
|
||||
21
plugins/content-manager/imposter/images/nginx/nginx.conf
Normal file
21
plugins/content-manager/imposter/images/nginx/nginx.conf
Normal file
@ -0,0 +1,21 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://imposter:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
plugins/content-manager/imposter/imposter-config.yml
Normal file
33
plugins/content-manager/imposter/imposter-config.yml
Normal file
@ -0,0 +1,33 @@
|
||||
plugin: openapi
|
||||
specFile: openapi.yml
|
||||
|
||||
# Enable stores for stateful behavior (retry system)
|
||||
stores:
|
||||
- name: token_requests
|
||||
type: memory
|
||||
|
||||
# Custom resource configuration for conditional responses
|
||||
resources:
|
||||
# Token endpoint - conditional responses based on device_code
|
||||
- path: /api/v1/instances/token
|
||||
method: POST
|
||||
response:
|
||||
scriptFile: scripts/tokenResponse.groovy
|
||||
|
||||
# Token exchange endpoint - conditional responses based on Authorization header
|
||||
- path: /api/v1/instances/token/exchange
|
||||
method: POST
|
||||
response:
|
||||
scriptFile: scripts/tokenExchangeResponse.groovy
|
||||
|
||||
# Instance me endpoint - conditional responses based on Authorization token
|
||||
- path: /api/v1/instances/me
|
||||
method: GET
|
||||
response:
|
||||
scriptFile: scripts/instanceMeResponse.groovy
|
||||
|
||||
# Catalog endpoint - conditional responses based on verify parameter
|
||||
- path: /api/v1/catalog/contexts/{context}/consumers/{consumer}/changes
|
||||
method: GET
|
||||
response:
|
||||
scriptFile: scripts/catalogResponse.groovy
|
||||
113
plugins/content-manager/imposter/imposter.sh
Executable file
113
plugins/content-manager/imposter/imposter.sh
Executable file
@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
IMAGES_DIR="$SCRIPT_DIR/images"
|
||||
CERTS_DIR="$IMAGES_DIR/nginx/certs"
|
||||
|
||||
usage() {
|
||||
echo -e "${BLUE}Usage: $0 {up|stop|down}${NC}"
|
||||
echo -e "${BLUE} up - Start the Imposter environment${NC}"
|
||||
echo -e "${BLUE} stop - Stop the Imposter environment without removing containers${NC}"
|
||||
echo -e "${BLUE} down - Remove the Imposter environment${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
generate_certs() {
|
||||
# Create certs directory if it doesn't exist
|
||||
mkdir -p "$CERTS_DIR"
|
||||
|
||||
# Generate self-signed certificates if they don't exist
|
||||
if [ ! -f "$CERTS_DIR/cert.pem" ] || [ ! -f "$CERTS_DIR/key.pem" ]; then
|
||||
echo -e "${GREEN}Generating self-signed SSL certificates for localhost...${NC}"
|
||||
openssl req -x509 -newkey rsa:4096 -nodes \
|
||||
-keyout "$CERTS_DIR/key.pem" \
|
||||
-out "$CERTS_DIR/cert.pem" \
|
||||
-days 365 \
|
||||
-subj "/C=US/ST=State/L=City/O=Wazuh/CN=localhost"
|
||||
|
||||
# Set proper permissions
|
||||
chmod 644 "$CERTS_DIR/cert.pem"
|
||||
chmod 600 "$CERTS_DIR/key.pem"
|
||||
|
||||
echo -e "${GREEN}Certificates generated successfully${NC}"
|
||||
else
|
||||
echo -e "${GREEN}Using existing certificates${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
start_environment() {
|
||||
echo -e "${BLUE}Setting up SSL environment for Imposter...${NC}"
|
||||
|
||||
generate_certs
|
||||
|
||||
# Change directory to images folder
|
||||
cd "$IMAGES_DIR"
|
||||
|
||||
# Stop any running containers
|
||||
echo -e "${GREEN}Stopping existing containers...${NC}"
|
||||
docker compose down 2>/dev/null || true
|
||||
|
||||
# Start the environment
|
||||
echo -e "${GREEN}Starting Docker Compose environment...${NC}"
|
||||
docker compose up -d
|
||||
|
||||
# Wait for services to be ready
|
||||
echo -e "${BLUE}Waiting for services to start...${NC}"
|
||||
sleep 5
|
||||
|
||||
# Test the endpoint
|
||||
echo -e "${GREEN}Testing endpoint...${NC}"
|
||||
if curl -k -s -o /dev/null -w "%{http_code}" https://localhost:8443/api/v1/instances/me | grep -q "200\|401"; then
|
||||
echo -e "${GREEN}✓ Environment is up and running!${NC}"
|
||||
echo -e "${BLUE}Access your mock at: https://localhost:8443${NC}"
|
||||
else
|
||||
echo -e "${BLUE}Environment started. Verify manually at: https://localhost:8443${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
}
|
||||
|
||||
stop_environment() {
|
||||
echo -e "${GREEN}Stopping Imposter environment...${NC}"
|
||||
cd "$IMAGES_DIR"
|
||||
docker compose stop
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
}
|
||||
|
||||
remove_environment() {
|
||||
echo -e "${GREEN}Removing Imposter environment...${NC}"
|
||||
cd "$IMAGES_DIR"
|
||||
docker compose down
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
}
|
||||
|
||||
# Check if at least one argument is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
# Parse command line arguments
|
||||
case $1 in
|
||||
up)
|
||||
start_environment
|
||||
;;
|
||||
stop)
|
||||
stop_environment
|
||||
;;
|
||||
down)
|
||||
remove_environment
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown command '$1'${NC}"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
|
||||
728
plugins/content-manager/imposter/openapi.yml
Normal file
728
plugins/content-manager/imposter/openapi.yml
Normal file
@ -0,0 +1,728 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Wazuh Console & CTI Mock API
|
||||
description: |
|
||||
Mock API for testing Wazuh instance OAuth flows and CTI catalog access.
|
||||
|
||||
This specification covers:
|
||||
- Token Request (Device Authorization Grant)
|
||||
- Token Exchange (for HMAC-signed URLs)
|
||||
- Catalog Download (CTI consumer changes access)
|
||||
version: 1.0.0
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
servers:
|
||||
- url: "{protocol}://{address}:{port}"
|
||||
variables:
|
||||
address:
|
||||
default: localhost
|
||||
port:
|
||||
default: '8443'
|
||||
protocol:
|
||||
enum:
|
||||
- "http"
|
||||
- "https"
|
||||
default: "https"
|
||||
|
||||
tags:
|
||||
- name: CTI Console
|
||||
description: CTI Console endpoints
|
||||
- name: CTI API
|
||||
description: Wazuh CTI resource endpoints
|
||||
|
||||
paths:
|
||||
/api/v1/instances/token:
|
||||
post:
|
||||
summary: Token Request
|
||||
description: |
|
||||
Poll for access token using device code from authorization flow.
|
||||
|
||||
This endpoint should be polled at the interval specified in the authorization response
|
||||
until the user completes authorization or the device code expires.
|
||||
operationId: tokenRequest
|
||||
tags:
|
||||
- CTI Console
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRequest'
|
||||
examples:
|
||||
valid_request:
|
||||
summary: Valid token request
|
||||
value:
|
||||
grant_type: urn:ietf:params:oauth:grant-type:device_code
|
||||
client_id: a17c21ed
|
||||
device_code: NGU5OWFiNjQ5YmQwNGY3YTdmZTEyNzQ3YzQ1YSA
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/TokenSuccess'
|
||||
'400':
|
||||
$ref: '#/components/responses/TokenError'
|
||||
|
||||
/api/v1/instances/token/exchange:
|
||||
post:
|
||||
summary: Token Exchange Request
|
||||
description: |
|
||||
Exchange access token for HMAC-signed URL to access CTI resources.
|
||||
|
||||
The signed URL is valid for 300 seconds (5 minutes) and grants access
|
||||
to the specific resource specified in the request.
|
||||
operationId: tokenExchange
|
||||
tags:
|
||||
- CTI Console
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenExchangeRequest'
|
||||
examples:
|
||||
catalog_access:
|
||||
summary: Request access to CTI catalog consumer changes
|
||||
value:
|
||||
grant_type: urn:ietf:params:oauth:grant-type:token-exchange
|
||||
subject_token_type: urn:ietf:params:oauth:token-type:access_token
|
||||
requested_token_type: urn:wazuh:params:oauth:token-type:signed_url
|
||||
resource: https://localhost:8080/api/v1/catalog/contexts/misp/consumer/virustotal/changes
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/TokenExchangeSuccess'
|
||||
'400':
|
||||
$ref: '#/components/responses/TokenExchangeError'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
|
||||
/api/v1/instances/me:
|
||||
get:
|
||||
summary: Get current instance details
|
||||
description: |
|
||||
Retrieve the current instance's organization and plan details.
|
||||
|
||||
Returns information about the organization and the plans associated with the instance,
|
||||
including available products and resources.
|
||||
operationId: getInstanceMe
|
||||
tags:
|
||||
- CTI Console
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/InstanceMeSuccess'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
|
||||
/api/v1/catalog/contexts/{context}/consumers/{consumer}/changes:
|
||||
get:
|
||||
summary: Catalog Download (Consumer Changes)
|
||||
description: |
|
||||
Download CTI catalog consumer changes using HMAC-signed URL.
|
||||
|
||||
This endpoint validates the HMAC signature in the query parameters
|
||||
and returns the requested threat intelligence changes.
|
||||
|
||||
The URL must include valid `verify` parameter with timestamp and signature.
|
||||
|
||||
**Notes:**
|
||||
- `from_offset` and `to_offset` are mandatory
|
||||
- Maximum number of returned changes is 1000
|
||||
- Empty update changes are not returned if `with_empties=false`
|
||||
- Empty creation changes are always returned
|
||||
- No authorization header needed when using signed URL
|
||||
operationId: catalogDownload
|
||||
tags:
|
||||
- CTI API
|
||||
parameters:
|
||||
- name: context
|
||||
in: path
|
||||
required: true
|
||||
description: The context name (e.g., "misp", "otx", "threatfox")
|
||||
schema:
|
||||
type: string
|
||||
example: misp
|
||||
- name: consumer
|
||||
in: path
|
||||
required: true
|
||||
description: The consumer name (e.g., "virustotal", "alienvault")
|
||||
schema:
|
||||
type: string
|
||||
example: virustotal
|
||||
- name: from_offset
|
||||
in: query
|
||||
required: true
|
||||
description: Offset of the last asked resource (offset excluded)
|
||||
schema:
|
||||
type: integer
|
||||
example: 0
|
||||
- name: to_offset
|
||||
in: query
|
||||
required: true
|
||||
description: Offset of the last resource to return (offset included)
|
||||
schema:
|
||||
type: integer
|
||||
example: 1000
|
||||
- name: with_empties
|
||||
in: query
|
||||
required: false
|
||||
description: If false, empty update changes are not returned (default true)
|
||||
schema:
|
||||
type: boolean
|
||||
default: true
|
||||
example: true
|
||||
- name: verify
|
||||
in: query
|
||||
required: true
|
||||
description: HMAC signature with format "timestamp-signature"
|
||||
schema:
|
||||
type: string
|
||||
example: 1761383411-kJ9b8w%2BQ7kzRmF
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/CatalogSuccess'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'422':
|
||||
$ref: '#/components/responses/UnprocessableEntity'
|
||||
|
||||
components:
|
||||
# --- Data Schemas ---
|
||||
schemas:
|
||||
TokenRequest:
|
||||
type: object
|
||||
description: Request parameters for polling to obtain access token.
|
||||
properties:
|
||||
grant_type:
|
||||
type: string
|
||||
description: OAuth 2.0 grant type for device authorization.
|
||||
enum:
|
||||
- urn:ietf:params:oauth:grant-type:device_code
|
||||
example: urn:ietf:params:oauth:grant-type:device_code
|
||||
client_id:
|
||||
type: string
|
||||
description: The instance UID.
|
||||
example: a17c21ed
|
||||
device_code:
|
||||
type: string
|
||||
description: The device verification code from authorization response.
|
||||
example: NGU5OWFiNjQ5YmQwNGY3YTdmZTEyNzQ3YzQ1YSA
|
||||
required:
|
||||
- grant_type
|
||||
- client_id
|
||||
- device_code
|
||||
|
||||
TokenResponse:
|
||||
type: object
|
||||
description: Response containing OAuth 2.0 access token.
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: The OAuth 2.0 access token.
|
||||
example: AYjcyMzY3ZDhiNmJkNTY
|
||||
refresh_token:
|
||||
type: string
|
||||
description: The refresh token for obtaining new access tokens.
|
||||
example: RjY2NjM5NzA2OWJjuE7c
|
||||
token_type:
|
||||
type: string
|
||||
description: The type of token issued.
|
||||
enum:
|
||||
- Bearer
|
||||
example: Bearer
|
||||
expires_in:
|
||||
type: integer
|
||||
description: The lifetime of the access token in seconds.
|
||||
example: 3600
|
||||
required:
|
||||
- access_token
|
||||
- token_type
|
||||
- expires_in
|
||||
|
||||
TokenExchangeRequest:
|
||||
type: object
|
||||
description: Request parameters for exchanging access token for signed URL.
|
||||
properties:
|
||||
grant_type:
|
||||
type: string
|
||||
description: OAuth 2.0 grant type for token exchange.
|
||||
enum:
|
||||
- urn:ietf:params:oauth:grant-type:token-exchange
|
||||
example: urn:ietf:params:oauth:grant-type:token-exchange
|
||||
subject_token_type:
|
||||
type: string
|
||||
description: The type of the subject token being exchanged.
|
||||
enum:
|
||||
- urn:ietf:params:oauth:token-type:access_token
|
||||
example: urn:ietf:params:oauth:token-type:access_token
|
||||
requested_token_type:
|
||||
type: string
|
||||
description: The type of token being requested.
|
||||
enum:
|
||||
- urn:wazuh:params:oauth:token-type:signed_url
|
||||
example: urn:wazuh:params:oauth:token-type:signed_url
|
||||
resource:
|
||||
type: string
|
||||
format: uri
|
||||
description: The full URL of the resource to access.
|
||||
example: https://localhost:8080/api/v1/catalog/contexts/misp/consumer/virustotal/changes
|
||||
required:
|
||||
- grant_type
|
||||
- subject_token_type
|
||||
- requested_token_type
|
||||
- resource
|
||||
|
||||
TokenExchangeResponse:
|
||||
type: object
|
||||
description: Response containing HMAC-signed URL for resource access.
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
format: uri
|
||||
description: The HMAC-signed URL for accessing the protected resource.
|
||||
example: https://localhost:8080/api/v1/catalog/contexts/misp/consumer/virustotal/changes?verify=1761383411-kJ9b8w%2BQ7kzRmF
|
||||
issued_token_type:
|
||||
type: string
|
||||
description: The type of token that was issued.
|
||||
enum:
|
||||
- urn:wazuh:params:oauth:token-type:signed_url
|
||||
example: urn:wazuh:params:oauth:token-type:signed_url
|
||||
expires_in:
|
||||
type: integer
|
||||
description: The lifetime of the signed URL in seconds.
|
||||
example: 300
|
||||
required:
|
||||
- access_token
|
||||
- issued_token_type
|
||||
- expires_in
|
||||
|
||||
CatalogChange:
|
||||
type: object
|
||||
description: A single change entry in the catalog.
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The type of change operation.
|
||||
enum:
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
example: create
|
||||
context:
|
||||
type: string
|
||||
description: The context name.
|
||||
example: misp
|
||||
resource:
|
||||
type: string
|
||||
description: The resource identifier.
|
||||
example: indicator/12345
|
||||
offset:
|
||||
type: integer
|
||||
description: The offset number of this change.
|
||||
example: 42
|
||||
version:
|
||||
type: integer
|
||||
description: The version number of the resource.
|
||||
example: 1
|
||||
payload:
|
||||
type: object
|
||||
description: The resource payload (for create operations).
|
||||
example:
|
||||
type: "ip-address"
|
||||
value: "192.168.1.100"
|
||||
threat_level: "high"
|
||||
timestamp: "2025-11-17T10:30:00Z"
|
||||
operations:
|
||||
type: array
|
||||
description: JSON Patch operations (for update operations).
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
op:
|
||||
type: string
|
||||
enum: [add, remove, replace, move, copy, test]
|
||||
path:
|
||||
type: string
|
||||
value:
|
||||
type: object
|
||||
description: The value to set (type depends on the operation)
|
||||
example:
|
||||
- op: replace
|
||||
path: /threat_level
|
||||
value: critical
|
||||
required:
|
||||
- type
|
||||
- context
|
||||
- resource
|
||||
- offset
|
||||
- version
|
||||
|
||||
CatalogChangesResponse:
|
||||
type: object
|
||||
description: Response containing catalog changes.
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
description: Array of catalog changes.
|
||||
items:
|
||||
$ref: '#/components/schemas/CatalogChange'
|
||||
required:
|
||||
- data
|
||||
|
||||
InstanceMeResponse:
|
||||
type: object
|
||||
description: Response containing instance details.
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
organization:
|
||||
type: object
|
||||
properties:
|
||||
avatar:
|
||||
type: string
|
||||
format: uri
|
||||
description: URL to the organization's avatar image.
|
||||
example: "https://acme.sl/avatar.png"
|
||||
name:
|
||||
type: string
|
||||
description: Organization name.
|
||||
example: "ACME S.L."
|
||||
plans:
|
||||
type: array
|
||||
description: List of plans associated with the instance.
|
||||
items:
|
||||
$ref: '#/components/schemas/Plan'
|
||||
required:
|
||||
- data
|
||||
|
||||
Plan:
|
||||
type: object
|
||||
description: A plan with associated products.
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Plan name.
|
||||
example: "Pro Plan Deluxe"
|
||||
description:
|
||||
type: string
|
||||
description: Plan description.
|
||||
example: "Lorem ipsum…"
|
||||
products:
|
||||
type: array
|
||||
description: List of products included in the plan.
|
||||
items:
|
||||
$ref: '#/components/schemas/Product'
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
- products
|
||||
|
||||
Product:
|
||||
type: object
|
||||
description: A product included in a plan.
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: Product type identifier.
|
||||
example: "catalog:consumer:vulnerabilities"
|
||||
identifier:
|
||||
type: string
|
||||
description: Unique product identifier.
|
||||
example: "vulnerabilities-pro"
|
||||
name:
|
||||
type: string
|
||||
description: Product name.
|
||||
example: "Vulnerabilities Pro"
|
||||
description:
|
||||
type: string
|
||||
description: Product description.
|
||||
example: "Vulnerabilities updated as soon as they are added to the catalog"
|
||||
resource:
|
||||
type: string
|
||||
format: uri
|
||||
description: Resource URL for catalog products.
|
||||
example: "https://localhost:8080/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: Contact email for assistance products.
|
||||
example: "cloud@wazuh.com"
|
||||
phone:
|
||||
type: string
|
||||
description: Contact phone for assistance products.
|
||||
example: "+34 123 456 789"
|
||||
required:
|
||||
- type
|
||||
- identifier
|
||||
- name
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response.
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: The error code.
|
||||
example: invalid_request
|
||||
error_description:
|
||||
type: string
|
||||
description: Human-readable error description.
|
||||
example: Missing required parameter
|
||||
errors:
|
||||
type: object
|
||||
description: Detailed validation errors by field.
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
from_offset: ["must be less than to_offset"]
|
||||
required:
|
||||
- error
|
||||
|
||||
# --- Response Schemas ---
|
||||
responses:
|
||||
# Success Responses
|
||||
TokenSuccess:
|
||||
description: OK - Access token granted.
|
||||
headers:
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
example: no-store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: Access token granted
|
||||
value:
|
||||
access_token: AYjcyMzY3ZDhiNmJkNTY
|
||||
refresh_token: RjY2NjM5NzA2OWJjuE7c
|
||||
token_type: Bearer
|
||||
expires_in: 3600
|
||||
|
||||
TokenExchangeSuccess:
|
||||
description: OK - Token exchange successful.
|
||||
headers:
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
example: no-store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenExchangeResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: HMAC-signed URL issued
|
||||
value:
|
||||
access_token: https://localhost:8080/api/v1/catalog/contexts/misp/consumer/virustotal/changes?verify=1761383411-kJ9b8w%2BQ7kzRmF
|
||||
issued_token_type: urn:wazuh:params:oauth:token-type:signed_url
|
||||
expires_in: 300
|
||||
|
||||
CatalogSuccess:
|
||||
description: OK - Catalog changes retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CatalogChangesResponse'
|
||||
examples:
|
||||
mixed_changes:
|
||||
summary: Catalog changes with create, update, and delete operations
|
||||
value:
|
||||
data:
|
||||
- type: create
|
||||
context: misp
|
||||
resource: indicator/12345
|
||||
offset: 42
|
||||
version: 1
|
||||
payload:
|
||||
type: "ip-address"
|
||||
value: "192.168.1.100"
|
||||
threat_level: "high"
|
||||
timestamp: "2025-11-17T10:30:00Z"
|
||||
- type: update
|
||||
context: misp
|
||||
resource: indicator/12345
|
||||
offset: 43
|
||||
version: 2
|
||||
operations:
|
||||
- op: replace
|
||||
path: /threat_level
|
||||
value: critical
|
||||
- type: delete
|
||||
context: misp
|
||||
resource: indicator/12345
|
||||
offset: 44
|
||||
version: 3
|
||||
|
||||
InstanceMeSuccess:
|
||||
description: OK - Instance details retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InstanceMeResponse'
|
||||
examples:
|
||||
pro_plan:
|
||||
summary: Pro Plan response
|
||||
value:
|
||||
data:
|
||||
organization:
|
||||
avatar: "https://acme.sl/avatar.png"
|
||||
name: "ACME S.L."
|
||||
plans:
|
||||
- name: "Pro Plan Deluxe"
|
||||
description: "Lorem ipsum…"
|
||||
products:
|
||||
- type: "catalog:consumer:vulnerabilities"
|
||||
identifier: "vulnerabilities-pro"
|
||||
name: "Vulnerabilities Pro"
|
||||
description: "Vulnerabilities updated as soon as they are added to the catalog"
|
||||
resource: "https://localhost:8080/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
- type: "catalog:consumer:iocs"
|
||||
identifier: "bad-guy-ips-pro"
|
||||
name: "Bad Guy IPs"
|
||||
description: "Dolor sit amet…"
|
||||
resource: "https://localhost:8080/api/v1/catalog/plans/pro/contexts/bad-guy-ips/consumer/realtime"
|
||||
cloud_plan:
|
||||
summary: Cloud Plan with Pro Products
|
||||
value:
|
||||
data:
|
||||
organization:
|
||||
avatar: "https://acme.sl/avatar.png"
|
||||
name: "ACME S.L."
|
||||
plans:
|
||||
- name: "Wazuh Cloud"
|
||||
description: "Managed instances in AWS by Wazuh's professional staf that…"
|
||||
products:
|
||||
- identifier: "assistance-24h"
|
||||
type: "cloud:assistance:wazuh"
|
||||
name: "Technical assistance 24h"
|
||||
email: "cloud@wazuh.com"
|
||||
phone: "+34 123 456 789"
|
||||
- identifier: "vulnerabilities-pro"
|
||||
type: "catalog:consumer:vulnerabilities"
|
||||
name: "Vulnerabilities Pro"
|
||||
description: "Vulnerabilities updated as soon as they are added to the catalog"
|
||||
resource: "https://localhost:8080/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
- identifier: "bad-guy-ips-pro"
|
||||
type: "catalog:consumer:iocs"
|
||||
name: "Bad Guy IPs"
|
||||
description: "Dolor sit amet…"
|
||||
resource: "https://localhost:8080/api/v1/catalog/plans/pro/contexts/bad-guy-ips/consumer/realtime"
|
||||
|
||||
# Error Responses
|
||||
TokenError:
|
||||
description: Bad Request - Token request failed.
|
||||
headers:
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
example: no-store
|
||||
Pragma:
|
||||
schema:
|
||||
type: string
|
||||
example: no-cache
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
authorization_pending:
|
||||
summary: User has not yet authorized
|
||||
value:
|
||||
error: authorization_pending
|
||||
expired_token:
|
||||
summary: Device code has expired
|
||||
value:
|
||||
error: expired_token
|
||||
error_description: The device code has expired
|
||||
|
||||
TokenExchangeError:
|
||||
description: Bad Request - Token exchange failed.
|
||||
headers:
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
example: no-store
|
||||
Pragma:
|
||||
schema:
|
||||
type: string
|
||||
example: no-cache
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalid_request:
|
||||
summary: Missing required parameter
|
||||
value:
|
||||
error: invalid_request
|
||||
error_description: Missing required parameter resource
|
||||
invalid_target:
|
||||
summary: Invalid resource endpoint
|
||||
value:
|
||||
error: invalid_target
|
||||
error_description: The resource parameter refers to an invalid endpoint
|
||||
|
||||
Unauthorized:
|
||||
description: Unauthorized - Authentication failed or token is invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalid_token:
|
||||
summary: Invalid or expired token
|
||||
value:
|
||||
error: unauthorized_client
|
||||
error_description: The provided token is invalid or expired
|
||||
|
||||
Forbidden:
|
||||
description: Forbidden - HMAC signature validation failed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalid_signature:
|
||||
summary: Invalid HMAC signature
|
||||
value:
|
||||
error: access_denied
|
||||
error_description: Invalid or expired HMAC signature
|
||||
|
||||
UnprocessableEntity:
|
||||
description: Unprocessable Entity - Invalid parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalid_offset_range:
|
||||
summary: Invalid offset range
|
||||
value:
|
||||
errors:
|
||||
from_offset: ["must be less than to_offset"]
|
||||
too_many_changes:
|
||||
summary: Too many changes requested
|
||||
value:
|
||||
errors:
|
||||
to_offset: ["range exceeds maximum of 1000 changes"]
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
OAuth 2.0 Bearer token authentication.
|
||||
Include the access token in the Authorization header:
|
||||
`Authorization: Bearer <access_token>`
|
||||
@ -0,0 +1,44 @@
|
||||
// Catalog Response Logic
|
||||
// Returns different responses based on verify parameter and query params
|
||||
|
||||
def verify = context.request.queryParams.verify?.toString()
|
||||
def fromOffsetStr = context.request.queryParams.from_offset?.toString()
|
||||
def toOffsetStr = context.request.queryParams.to_offset?.toString()
|
||||
def fromOffset = fromOffsetStr ? fromOffsetStr.toInteger() : null
|
||||
def toOffset = toOffsetStr ? toOffsetStr.toInteger() : null
|
||||
|
||||
// Check for missing verify parameter (401 Unauthorized)
|
||||
if (!verify) {
|
||||
respond()
|
||||
.withStatusCode(401)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "unauthorized_client", "error_description": "The provided token is invalid or expired"}')
|
||||
}
|
||||
// Check for invalid signature (403 Forbidden)
|
||||
else if (verify == "invalid" || verify == "expired" || verify.startsWith("0000")) {
|
||||
respond()
|
||||
.withStatusCode(403)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "access_denied", "error_description": "Invalid or expired HMAC signature"}')
|
||||
}
|
||||
// Check for invalid offset range (422 Unprocessable Entity)
|
||||
else if (fromOffset != null && toOffset != null && fromOffset >= toOffset) {
|
||||
respond()
|
||||
.withStatusCode(422)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"errors": {"from_offset": ["must be less than to_offset"]}}')
|
||||
}
|
||||
// Check if range exceeds maximum (422 Unprocessable Entity)
|
||||
else if (fromOffset != null && toOffset != null && (toOffset - fromOffset) > 1000) {
|
||||
respond()
|
||||
.withStatusCode(422)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"errors": {"to_offset": ["range exceeds maximum of 1000 changes"]}}')
|
||||
}
|
||||
// Success scenario
|
||||
else {
|
||||
respond()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"data": [{"type": "create", "context": "misp", "resource": "indicator/12345", "offset": 42, "version": 1, "payload": {"type": "ip-address", "value": "192.168.1.100", "threat_level": "high", "timestamp": "2025-11-17T10:30:00Z"}}, {"type": "update", "context": "misp", "resource": "indicator/12345", "offset": 43, "version": 2, "operations": [{"op": "replace", "path": "/threat_level", "value": "critical"}]}, {"type": "delete", "context": "misp", "resource": "indicator/12345", "offset": 44, "version": 3}]}')
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
// Instance Me Response Logic
|
||||
// Returns different responses based on Authorization header token
|
||||
|
||||
def authHeader = context.request.headers['Authorization']?.toString()
|
||||
|
||||
// Check if Authorization header is present
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
respond()
|
||||
.withStatusCode(401)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "unauthorized_client", "error_description": "The provided token is invalid or expired"}')
|
||||
return
|
||||
}
|
||||
|
||||
// Extract token from Authorization header
|
||||
def token = authHeader.substring(7) // Remove "Bearer " prefix
|
||||
|
||||
// Cloud plan scenario - token contains "cloud"
|
||||
if (token.toLowerCase().contains('cloud')) {
|
||||
respond()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('''
|
||||
{
|
||||
"data": {
|
||||
"organization": {
|
||||
"avatar": "https://acme.sl/avatar.png",
|
||||
"name": "ACME S.L."
|
||||
},
|
||||
"plans": [
|
||||
{
|
||||
"name": "Wazuh Cloud",
|
||||
"description": "Managed instances in AWS by Wazuh's professional staf that…",
|
||||
"products": [
|
||||
{
|
||||
"identifier": "assistance-24h",
|
||||
"type": "cloud:assistance:wazuh",
|
||||
"name": "Technical assistance 24h",
|
||||
"email": "cloud@wazuh.com",
|
||||
"phone": "+34 123 456 789"
|
||||
},
|
||||
{
|
||||
"identifier": "vulnerabilities-pro",
|
||||
"type": "catalog:consumer:vulnerabilities",
|
||||
"name": "Vulnerabilities Pro",
|
||||
"description": "Vulnerabilities updated as soon as they are added to the catalog",
|
||||
"resource": "https://localhost:8080/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
},
|
||||
{
|
||||
"identifier": "bad-guy-ips-pro",
|
||||
"type": "catalog:consumer:iocs",
|
||||
"name": "Bad Guy IPs",
|
||||
"description": "Dolor sit amet…",
|
||||
"resource": "https://localhost:8080/api/v1/catalog/plans/pro/contexts/bad-guy-ips/consumer/realtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
''')
|
||||
}
|
||||
// Pro plan scenario - default for any other valid token
|
||||
else {
|
||||
respond()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('''
|
||||
{
|
||||
"data": {
|
||||
"organization": {
|
||||
"avatar": "https://acme.sl/avatar.png",
|
||||
"name": "ACME S.L."
|
||||
},
|
||||
"plans": [
|
||||
{
|
||||
"name": "Pro Plan Deluxe",
|
||||
"description": "Lorem ipsum…",
|
||||
"products": [
|
||||
{
|
||||
"type": "catalog:consumer:vulnerabilities",
|
||||
"identifier": "vulnerabilities-pro",
|
||||
"name": "Vulnerabilities Pro",
|
||||
"description": "Vulnerabilities updated as soon as they are added to the catalog",
|
||||
"resource": "https://localhost:8080/api/v1/catalog/plans/pro/contexts/vulnerabilities/consumer/realtime"
|
||||
},
|
||||
{
|
||||
"type": "catalog:consumer:iocs",
|
||||
"identifier": "bad-guy-ips-pro",
|
||||
"name": "Bad Guy IPs",
|
||||
"description": "Dolor sit amet…",
|
||||
"resource": "https://localhost:8080/api/v1/catalog/plans/pro/contexts/bad-guy-ips/consumer/realtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
''')
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
// Token Exchange Response Logic
|
||||
// Returns different responses based on Authorization header
|
||||
|
||||
def authHeader = context.request.headers.Authorization?.toString()
|
||||
def resource = context.request.formParams.resource?.toString()
|
||||
|
||||
// Check for missing Authorization header
|
||||
if (!authHeader) {
|
||||
respond()
|
||||
.withStatusCode(401)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "unauthorized_client", "error_description": "The provided token is invalid or expired"}')
|
||||
}
|
||||
// Check for invalid token
|
||||
else if (authHeader == "Bearer invalid_token" || authHeader == "Bearer expired_token") {
|
||||
respond()
|
||||
.withStatusCode(401)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "unauthorized_client", "error_description": "The provided token is invalid or expired"}')
|
||||
}
|
||||
// Check for missing resource parameter
|
||||
else if (!resource) {
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "invalid_request", "error_description": "Missing required parameter resource"}')
|
||||
}
|
||||
// Check for invalid resource endpoint
|
||||
else if (resource && !resource.contains("localhost:8443")) {
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "invalid_target", "error_description": "The resource parameter refers to an invalid endpoint"}')
|
||||
}
|
||||
// Success scenario
|
||||
else {
|
||||
respond()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"access_token": "https://localhost:8443/api/v1/catalog/contexts/misp/consumers/virustotal/changes?from_offset=0&to_offset=1000&with_empties=true&verify=1761383411-kJ9b8w%2BQ7kzRmF", "issued_token_type": "urn:wazuh:params:oauth:token-type:signed_url", "expires_in": 300}')
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
// Token Request Response Logic
|
||||
// Returns different responses based on device_code parameter and request count
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
// Use a static map to track request counts across invocations
|
||||
@groovy.transform.Field
|
||||
static ConcurrentHashMap<String, Integer> requestCounts = new ConcurrentHashMap<>()
|
||||
|
||||
def deviceCode = context.request.formParams.device_code?.toString()
|
||||
|
||||
// Expired token scenario
|
||||
if (deviceCode == "expired") {
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "expired_token", "error_description": "The device code has expired"}')
|
||||
}
|
||||
// Pending authorization codes
|
||||
else if (deviceCode == "pending") {
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "authorization_pending"}')
|
||||
}
|
||||
// Success scenario - immediate grant
|
||||
else if (deviceCode == "granted") {
|
||||
respond()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"access_token": "AYjcyMzY3ZDhiNmJkNTY", "refresh_token": "RjY2NjM5NzA2OWJjuE7c", "token_type": "Bearer", "expires_in": 3600}')
|
||||
}
|
||||
// Handle pending/rejected flow
|
||||
else if (deviceCode == "pending_rejected") {
|
||||
def count = requestCounts.getOrDefault(deviceCode, 0) + 1
|
||||
requestCounts.put(deviceCode, count)
|
||||
|
||||
if (count <= 4) {
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "authorization_pending"}')
|
||||
} else {
|
||||
// Reject after 4 attempts
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "access_denied", "error_description": "Token authorization denied"}')
|
||||
}
|
||||
}
|
||||
// Handle pending/granted flow
|
||||
else {
|
||||
def count = requestCounts.getOrDefault(deviceCode, 0) + 1
|
||||
requestCounts.put(deviceCode, count)
|
||||
|
||||
// Return authorization_pending for first 4 attempts
|
||||
if (count <= 4) {
|
||||
respond()
|
||||
.withStatusCode(400)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Pragma", "no-cache")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"error": "authorization_pending"}')
|
||||
} else {
|
||||
// Grant token after 4 attempts
|
||||
respond()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Cache-Control", "no-store")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent('{"access_token": "AYjcyMzY3ZDhiNmJkNTY", "refresh_token": "RjY2NjM5NzA2OWJjuE7c", "token_type": "Bearer", "expires_in": 3600}')
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user