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
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# OS / editor noise
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
@@ -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/
|
||||||
|
└── <integration-id>/
|
||||||
|
├── manifest.yaml # required — the integration definition
|
||||||
|
└── scripts/ # optional — only for script-based commands
|
||||||
|
└── <command-id>.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/<id>.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/<command-id>.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/<id>/manifest.yaml` (one directory per integration).
|
||||||
|
2. Add `scripts/<command-id>.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).
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Integration manifest template — copy to integrations/<id>/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/<id>.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: {}
|
||||||
@@ -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))
|
||||||
Reference in New Issue
Block a user