The Markdown summary reads as a page. It cannot be sorted or filtered, and an investigation aggregating several alerts, each with several evidences, is exactly where those are wanted: "Multiple User Accounts Were Disabled" is a question about which accounts, and the answer is a column. So one row per evidence, across every alert, into custom:rapid7_evidence. The table field reads an array of objects as it stands, so nothing is reshaped on the mapper side. It sits beside the summary rather than replacing it — one is read, the other is queried. Two decisions worth stating. Each row carries its alert's name first. An investigation aggregates several, and a row that does not say where it came from cannot be acted on. Column order is load-bearing, because it is the only ordering signal the field has: it takes the union of keys in order of first appearance, so whatever the first row names first is what an analyst sees without scrolling. Known keys lead — action, result, the source and target pairs, the host, the addresses — and the rest follow alphabetically. Width is capped at 30 columns, decided by how many rows carry each key rather than by which appeared first: an investigation mixing rule types has a sparse union, and a column present on one row out of eighty would otherwise push out one present on all of them. Exercised on a 187-column case — the five keys common to every row all survived, and the count is reported as evidence_columns_dropped rather than the loss being silent. Verified against the account-disabled investigation: two evidences become two rows of twenty columns, `alert` first, the two disabled accounts in target_account — checked through the table field's own reader, not just the generator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Riposte Marketplace
Official catalog of integrations for the 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.yamlis 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*.pyfile becomes a command implementation, keyed by filename without the extension. So a command withid: get_ip_reportis backed byget_ip_report.py. - Files not named
manifest.yamlare ignored as integration roots, so templates likemanifest.example.yamlare 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)
id: my_integration # required, unique, slug
name: My Integration # required, human name
version: 1.0.0 # semver
description: What it does.
changelog: "1.1.0 — notes" # optional: shown on update so operators can assess risk
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.
risk: read # read | safe_write | destructive — see below
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.
risk: read
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.
risk: — the agent approval gate
Every command declares a tier. It is what Riposte's agent approval gate reads:
read runs unattended, safe_write and destructive sit behind the operator's
configured threshold. An omitted risk is treated as destructive — safe, but
it makes a lookup as noisy as an isolation, so declare it rather than default it.
| Tier | What belongs there |
|---|---|
read |
Only queries. Changes nothing outside Riposte: lookups, searches, telemetry, fetching a job's result. |
safe_write |
Additive or reversible, inside the tool's own case/enrichment plane: comments, notes, tags, tickets, alert statuses, threat-intel records, sandbox submissions, collection jobs, outbound messages. |
destructive |
Changes the estate or weakens a control: containment and its removal, blocklists and allowlists, exclusions and detection rules, every delete, identity and credential operations, running code on a host, generic API escape hatches. |
Two rules the catalog applies consistently, because they are not obvious:
- Undoing a containment is tiered like the containment it lifts. Un-isolating a host or un-banning an IP mid-incident is its own decision, not a return to a neutral state.
- Retrieving a live secret is
destructive. It is aGET, but the tier describes the stake, not the HTTP verb.
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
- Create
integrations/<id>/manifest.yaml(one directory per integration). - Add
scripts/<command-id>.pyonly for script-based commands. - Bump
version(semver) on every change — Riposte tracks versions per source. - Validate the YAML parses and
id/nameare set. - Open a merge request.
See templates/manifest.example.yaml for a
fully-commented starting point (that file is intentionally not ingested).