commit 67d5e7da55b9c7d555a54f0da0c87b0f705c36c8 Author: Guillaume BOURGEOIS Date: Mon Jun 22 10:50:43 2026 +0200 feat: initial official marketplace catalog - integrations/virustotal: VirusTotal v3 (request-based: IP & domain reports) - templates/: fully-commented manifest + script-command example - README: discovery rules, manifest schema, how to publish and wire into Riposte diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5447260 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# OS / editor noise +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Python +__pycache__/ +*.pyc +.venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d42445a --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Riposte Marketplace + +Official catalog of **integrations** for the [Riposte](https://gitea.riposte-labs.com/f3nris/riposte) SOAR platform. + +Riposte syncs this repository (a "git source") and lists every integration it +finds so operators can install them in one click. Nothing here is executed at +sync time — Riposte only reads manifests and scripts. + +--- + +## How discovery works + +When Riposte syncs a source it does a shallow `git clone` of the selected branch +and walks the whole tree looking for files named exactly **`manifest.yaml`**. + +- **Each `manifest.yaml` is one integration.** Its containing directory is the + package root. Put one integration per directory. +- **Scripts** are collected from the package directory **and** its `scripts/` + subdirectory: every `*.py` file becomes a command implementation, keyed by + filename without the extension. So a command with `id: get_ip_report` is backed + by `get_ip_report.py`. +- Files **not** named `manifest.yaml` are ignored as integration roots, so + templates like `manifest.example.yaml` are never ingested. + +``` +integrations/ +└── / + ├── manifest.yaml # required — the integration definition + └── scripts/ # optional — only for script-based commands + └── .py +``` + +--- + +## Manifest schema (`manifest.yaml`) + +```yaml +id: my_integration # required, unique, slug +name: My Integration # required, human name +version: 1.0.0 # semver +description: What it does. +category: enrichment # free text (enrichment, containment, ticketing…) + +# Per-instance configuration the operator fills when creating an instance. +# JSON-Schema shape: { properties: {...}, required: [...] }. +config_schema: + properties: + base_url: + type: string # string | number | boolean + description: API base URL + default: https://api.example.com/v1 + api_key: + type: string + description: API key + x-soar-sensitive: true # stored encrypted in the vault, never returned + required: + - api_key + +# Authentication methods, referenced by commands via `auth_ref`. +auth: + - id: apikey + type: api_key # api_key | bearer | basic | oauth2_client_credentials + in: header # header | query + name: x-apikey # header/query parameter name + value_template: "{{secret}}" # {{secret}} is replaced by the secret_field value + secret_field: api_key # which config_schema field holds the secret + +commands: + # --- Request-based command (recommended, no code) ----------------------- + - id: get_ip_report + name: Get IP report + description: Reputation for an IP address. + inputs_schema: + properties: + ip: + type: string + description: IP address to look up + required: + - ip + outputs_schema: + properties: {} + request: + method: GET # GET | POST | PUT | PATCH | DELETE + path: /ip/{ip} # {ip} is filled from inputs + query: [] # input names sent as query params + body: [] # input names sent as JSON body fields + auth_ref: apikey + + # --- Script-based command ----------------------------------------------- + # Omit `request` and provide scripts/.py instead. The script receives the + # resolved inputs + instance config and MUST print one JSON object to stdout. + - id: enrich_custom + name: Custom enrichment + description: Runs scripts/enrich_custom.py in a sandbox. + inputs_schema: + properties: + indicator: + type: string + required: + - indicator + outputs_schema: + properties: {} +``` + +A command is **request-based** when it has a `request:` block, or +**script-based** when a matching `scripts/.py` exists. Prefer +request-based commands: they need no sandbox and are easier to audit. + +--- + +## Adding this catalog to Riposte + +In Riposte → **Integrations → Marketplace → Add source**: + +| Field | Value | +|-------|-------| +| Name | Official marketplace | +| Git URL | `https://gitea.riposte-labs.com/f3nris/riposte-marketplace.git` | +| Branch | `main` | +| Provider | **Gitea** (sets the right auth scheme for private repos) | +| Token | a read-only token if the repo is private; leave empty if public | + +Then **Sync**. Discovered integrations appear in the marketplace, ready to install. + +--- + +## Contributing an integration + +1. Create `integrations//manifest.yaml` (one directory per integration). +2. Add `scripts/.py` only for script-based commands. +3. Bump `version` (semver) on every change — Riposte tracks versions per source. +4. Validate the YAML parses and `id`/`name` are set. +5. Open a merge request. + +See [`templates/manifest.example.yaml`](templates/manifest.example.yaml) for a +fully-commented starting point (that file is intentionally **not** ingested). diff --git a/integrations/virustotal/manifest.yaml b/integrations/virustotal/manifest.yaml new file mode 100644 index 0000000..9ae6b7d --- /dev/null +++ b/integrations/virustotal/manifest.yaml @@ -0,0 +1,61 @@ +id: virustotal +name: VirusTotal +version: 1.0.0 +description: VirusTotal API v3 — reputation lookups for IPs and domains. +category: enrichment + +config_schema: + properties: + base_url: + type: string + description: VirusTotal API v3 base URL + default: https://www.virustotal.com/api/v3 + api_key: + type: string + description: VirusTotal API key + x-soar-sensitive: true + required: + - api_key + +auth: + - id: apikey + type: api_key + in: header + name: x-apikey + value_template: "{{secret}}" + secret_field: api_key + +commands: + - id: get_ip_report + name: Get IP report + description: Reputation and last-analysis stats for an IP address. + inputs_schema: + properties: + ip: + type: string + description: IP address to look up + required: + - ip + outputs_schema: + properties: {} + request: + method: GET + path: /ip_addresses/{ip} + auth_ref: apikey + + - id: get_domain_report + name: Get domain report + description: Reputation and last-analysis stats for a domain. + inputs_schema: + properties: + domain: + type: string + description: Domain to look up + required: + - domain + outputs_schema: + properties: {} + request: + method: GET + path: /domains/{domain} + auth_ref: apikey diff --git a/templates/manifest.example.yaml b/templates/manifest.example.yaml new file mode 100644 index 0000000..0b752f9 --- /dev/null +++ b/templates/manifest.example.yaml @@ -0,0 +1,82 @@ +# ============================================================================= +# Integration manifest template — copy to integrations//manifest.yaml +# This file is named *.example.yaml on purpose so Riposte never ingests it +# (only files named exactly "manifest.yaml" are picked up). +# ============================================================================= + +id: example_integration # required · unique slug · [a-z0-9_] +name: Example Integration # required · shown in the UI +version: 1.0.0 # semver · bump on every change +description: One-line description of what this integration does. +category: enrichment # free text: enrichment | containment | ticketing | ... + +# --------------------------------------------------------------------------- +# config_schema — per-instance settings the operator fills in when creating an +# instance. JSON-Schema shape: { properties: {...}, required: [...] }. +# Property fields: type (string|number|boolean), description, default, +# and x-soar-sensitive: true for secrets (encrypted in the vault, never returned). +# --------------------------------------------------------------------------- +config_schema: + properties: + base_url: + type: string + description: API base URL + default: https://api.example.com/v1 + api_key: + type: string + description: API key / token + x-soar-sensitive: true + required: + - api_key + +# --------------------------------------------------------------------------- +# auth — authentication methods, referenced from commands via auth_ref. +# value_template: {{secret}} is replaced by the value of secret_field. +# api_key in header → name is the header (e.g. x-apikey, Authorization) +# bearer → typically name: Authorization, value_template: "Bearer {{secret}}" +# --------------------------------------------------------------------------- +auth: + - id: apikey + type: api_key # api_key | bearer | basic | oauth2_client_credentials + in: header # header | query + name: Authorization + value_template: "Bearer {{secret}}" + secret_field: api_key + +commands: + # --- Request-based command (recommended: declarative, no sandbox) --------- + - id: lookup + name: Lookup indicator + description: Fetches a report for an indicator over HTTP. + inputs_schema: + properties: + indicator: + type: string + description: The value to look up + required: + - indicator + outputs_schema: + properties: {} + request: + method: GET # GET | POST | PUT | PATCH | DELETE + path: /lookup/{indicator} # {indicator} is substituted from inputs + query: [] # input names to send as query params + body: [] # input names to send as JSON body fields + auth_ref: apikey + + # --- Script-based command ------------------------------------------------- + # No `request:` block. Provide scripts/.py next to this manifest. + # The script receives inputs in the global dict __inputs__ and MUST print + # exactly one JSON object to stdout (that becomes the command output). + # See templates/scripts/example_command.py. + - id: example_command + name: Example scripted command + description: Demonstrates a Python-backed command. + inputs_schema: + properties: + indicator: + type: string + required: + - indicator + outputs_schema: + properties: {} diff --git a/templates/scripts/example_command.py b/templates/scripts/example_command.py new file mode 100644 index 0000000..2056158 --- /dev/null +++ b/templates/scripts/example_command.py @@ -0,0 +1,25 @@ +"""Example script-based command implementation. + +Contract (Riposte sandbox): +- Inputs are available in the global dict `__inputs__` (already parsed JSON). +- `json`, `os` and `sys` are pre-imported — no need to import them. +- The script MUST print exactly ONE JSON object to stdout. That object becomes + the command's output (`data`). Anything else on stdout breaks parsing. +- On failure, raise an exception (the runner reports it) or exit non-zero. + +The filename (without .py) MUST match the command id in manifest.yaml, e.g. +this file `example_command.py` backs the command `id: example_command`. +""" + +# Inputs declared in the command's inputs_schema: +indicator = __inputs__.get("indicator", "") + +# ... call an API, compute, enrich, etc. ... +result = { + "indicator": indicator, + "verdict": "unknown", + "score": 0, +} + +# Emit the single JSON result object. +print(json.dumps(result))