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:
@@ -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).
|
||||
Reference in New Issue
Block a user