f3nris b26257aaeb feat(rapid7-insightidr): render the detection as Markdown, and fix four gaps (v1.10.0)
An "Multiple User Accounts Were Disabled" investigation came back with one
entity — the actor — while the layout stayed nearly empty. Four separate
reasons, and the last one is why the first three keep recurring.

  evidences was an OBJECT, not an array. JSONata returns a bare object when the
  path yields one item, so the key had one shape on an investigation with a
  single alert and another with two. Bracketed to force an array.

  Only the actor was mapped. An ad_admin rule names who acted AND who it was
  done to, and on this alert the disabled accounts are the point of it. Target
  account, target user and both domains now map to the OCSF Target User group,
  the actor to Actor — the distinction that lets the sentence be read in one
  line.

  The domain controller was nowhere. A Windows event puts it in
  source_json.computerName and nowhere else, and source_json was skipped whole
  because the Azure AD one runs to kilobytes. Its top-level SCALARS are now
  read — bounded, and where computerName, osVersion and callerIpAddress live —
  while its nested part stays skipped.

  source_asset was the string "unknown". Mapped as-is it becomes an asset named
  "unknown" in the entity index, shared by every incident that had no asset: a
  pivot that groups things by the absence of a fact. Placeholders are dropped,
  and a key whose value cleans away to nothing is removed rather than kept
  empty, since "" is not nil and would still be written to its OCSF field.

Five entities now, from one: both accounts, both display names, the DC.

And the reason this kept happening. The layout builder's field picker is a
closed <select> over the OCSF catalogue in web/src/lib/ocsf.ts, so a path absent
from it cannot be placed on a layout at all — which is what `detection` and
`evidence` are, and what every unanticipated key will be. Mapping each one to a
real OCSF field is right where a field exists, but rule_keys_of_interest is an
open vocabulary: four spellings turned up in as many days.

So the whole of it is also rendered as a Markdown table into
custom:rapid7_summary — keys of interest first, since the rule itself says
those are what matter, then the evidence fields. Declare a custom field of type
markdown with that name, put it on the layout, and a new rule type is readable
the day it appears instead of after another mapping round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 18:04:14 +02:00

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.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)

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 a GET, 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

  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 for a fully-commented starting point (that file is intentionally not ingested).

S
Description
No description provided
Readme 6.1 MiB
Languages
Python 100%