Skip to content
How-to guide

How to Report a Vulnerability

This guide covers the opening exchange of Report Management (RM): a Reporter delivers a vulnerability report, and the recipient rules on it. Follow the Reporter steps to submit a report, or the recipient steps to accept one and open a case. You finish with the report at RM.VALID and a case created, or at RM.INVALID and closed.


Prerequisites

  • A running Vultron instance you can drive. The demo containers are the quickest route — see Tutorial: Run the Receive-Report Demo for the two steps that get you a container shell.
  • An actor identity on that instance, holding the role each step names.
  • Working knowledge of the ActivityStreams verb, object, and target shape, summarized in Vultron and ActivityPub.
  • Working knowledge of the case model: the objects a case is made of, in The Case Model, and who is allowed to change them, in The CASE_MANAGER and the Case Ledger. Where a guide's messages are case-scoped, they route through the CASE_MANAGER and their results appear on the case ledger.

  • The recipient's actor Uniform Resource Identifier (URI). A Reporter needs somewhere to send the report.

  • Nothing else. Report submission is the one exchange that does not require an existing case.

The exchange

The flowchart below shows every path from an unsent report to a closed report or a created case. Solid arrows are the common path; the dotted arrow is the shortcut taken when a recipient rules on a report without acknowledging it first.

---
title: Report Submission and the Receiving Participant's Ruling
---
flowchart TB
    subgraph as:Create
        RmCreateReport["Create Report<br/>Create(VulnerabilityReport)"]
        CreateCase["Create Case<br/>Create(VulnerabilityCase)"]
    end
    subgraph as:Read
        RmReadReport["Report Acknowledgment (RK)<br/>Read(Offer(VulnerabilityReport))"]
    end
    subgraph as:Offer
        RmSubmitReport["Report Submission (RS)<br/>Offer(VulnerabilityReport)"]
    end
    subgraph as:Accept
        RmValidateReport["Report Valid (RV)<br/>Accept(Offer(VulnerabilityReport))"]
    end
    subgraph as:TentativeReject
        RmInvalidateReport["Report Invalid (RI)<br/>TentativeReject(Offer(VulnerabilityReport))"]
    end
    subgraph as:Reject
        RmCloseReport["Report Closed (RC)<br/>Reject(Offer(VulnerabilityReport))"]
    end
    start([Start])
    start -.-> RmCreateReport
    start --> RmSubmitReport
    RmCreateReport --> RmSubmitReport
    RmSubmitReport -.-> v{Valid?}
    RmSubmitReport --> r{Read?}
    r -->|y| RmReadReport
    r -->|n| r
    v -->|y| RmValidateReport
    RmReadReport --> v
    v -->|n| RmInvalidateReport
    RmInvalidateReport --> c{Close?}
    c -->|y| RmCloseReport
    c -->|n| v
    RmValidateReport --> CreateCase

Submit a report

  1. Mint the report object with Create(VulnerabilityReport). If the report is being created and sent in the same breath, skip this step and carry the report object inline on the Offer instead.

    Create report

    Mints the vulnerability report object. It precedes the Offer that submits the report.

    • When… a Reporter has written up a vulnerability and is about to submit the report
    • Send: Create(VulnerabilityReport)
    • Send to: an actor outside any case
    • Sent by: any actor (usually: reporter)
    • Reference: Create report
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Create",
      "id": "urn:uuid:129ec48f-20ad-42f6-b23f-6e590366176d",
      "name": "https://vultron.example/users/finndervul Create FDR-8675309",
      "actor": "https://vultron.example/users/finndervul",
      "object": {
        "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
        "type": "VulnerabilityReport",
        "name": "FDR-8675309",
        "published": "2026-10-23T20:17:06+00:00",
        "updated": "2026-10-23T20:17:06+00:00",
        "content": "I found a vulnerability!",
        "attributedTo": "https://vultron.example/users/finndervul",
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
      }
    }
    
  2. Send Report Submission (RS), implemented in ActivityStreams as Offer(VulnerabilityReport), to the recipient's inbox, carrying the report as its object.

    Report Submission (RS)

    Offers the report to a new Participant. The Offer may carry proposed embargo terms.

    • When… a Reporter submits a vulnerability report to a Vendor or Coordinator
    • Send: Offer(VulnerabilityReport)
    • Send to: an actor outside any case
    • Sent by: any actor (usually: reporter)
    • Reference: Report Submission (RS)
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Offer",
      "id": "urn:uuid:48994fb6-8792-47e6-8f35-7eb5f7b509b9",
      "name": "https://vultron.example/users/finndervul Offer FDR-8675309",
      "to": [
        "https://vultron.example/organizations/vendorco"
      ],
      "actor": "https://vultron.example/users/finndervul",
      "object": {
        "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
        "type": "VulnerabilityReport",
        "name": "FDR-8675309",
        "published": "2026-10-23T20:17:06+00:00",
        "updated": "2026-10-23T20:17:06+00:00",
        "content": "I found a vulnerability!",
        "attributedTo": "https://vultron.example/users/finndervul",
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
      }
    }
    
  3. To propose embargo terms for this report, carry an EmbargoEvent on the Offer as proposedEmbargo, with the report as its context and the end you want as endTime. The recipient compares your terms with its own default when it creates the case and activates the shorter; the longer becomes a pending revision you can negotiate inside the agreed embargo (EP-04-003). The case's CASE_MANAGER then sends that revision as an Invite(Event) carrying the revised EmbargoEvent to the party whose terms won, on behalf of the party whose terms lost: if your terms were longer, the recipient is asked to accept them; if its default was longer, you are (EP-04-011). There is no separate negotiation before the report is sent (ADR-0096). Through the submit-report trigger, state the end as proposed_embargo_end_time and the Offer carries the terms for you.

Whose default the recipient compares yours with

The recipient's default is the EmbargoPolicy it has published: PUT /actors/{actor_id}/embargo-policy publishes one and GET on the same path shows it (EP-02). The policy compared with yours is the one on the recipient's own actor profile (EP-04-003). Under ADR-0041 the recipient's CaseActor creates the case, attributed to the recipient; the recipient sends its profile, policy included, inline as the actor of its case proposal, and the CaseActor reads the default from that profile only (CP-01-010). A recipient that has published nothing has no default to compare, and your terms apply at their stated length; only when no one states terms at all does the Protocol Default apply (EP-04-005 to EP-04-007).

The recipient's report reaches RM.RECEIVED on delivery. Offer(VulnerabilityReport) is the only RM message whose receipt moves the receiver's own RM state; every other RM message reports the sender's state.

One report, several recipients

Send a separate Offer(VulnerabilityReport) to each recipient. Each recipient runs its own RM state machine, so there is no broadcast form of report submission.


Rule on a received report

Choose one of three answers.

  • If you have read the report but are not ready to rule on it, send Report Acknowledgment (RK), Read(Offer(VulnerabilityReport)). See How to Acknowledge a Report.

    Report Acknowledgment (RK)

    Acknowledges receipt of the submitted report. Report submission is not replicated through the case ledger, so the acknowledgment is an explicit message.

    • When… a report receiver has read a submitted report
    • Send: Read(Offer(VulnerabilityReport))
    • Send to: an actor outside any case
    • Sent by: the addressee of the vulnerability report offer (usually: vendor, coordinator)
    • Reference: Report Acknowledgment (RK)
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Read",
      "id": "urn:uuid:dd6f5027-ff1a-4b33-abc9-8f84c2330448",
      "name": "https://vultron.example/organizations/vendorco Read https://vultron.example/users/finndervul Offer FDR-8675309",
      "content": "We've read the report. We'll get back to you soon.",
      "actor": "https://vultron.example/organizations/vendorco",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Offer",
        "id": "urn:uuid:ef33c6b7-321e-4024-b4bc-c708a38c1e3a",
        "name": "https://vultron.example/users/finndervul Offer FDR-8675309",
        "published": "2026-10-23T20:17:08+00:00",
        "updated": "2026-10-23T20:17:08+00:00",
        "to": [
          "https://vultron.example/organizations/vendorco"
        ],
        "actor": "https://vultron.example/users/finndervul",
        "object": {
          "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
          "type": "VulnerabilityReport",
          "name": "FDR-8675309",
          "published": "2026-10-23T20:17:06+00:00",
          "updated": "2026-10-23T20:17:06+00:00",
          "content": "I found a vulnerability!",
          "attributedTo": "https://vultron.example/users/finndervul",
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
        }
      }
    }
    
  • If the report is in scope and actionable, send Report Valid (RV), Accept(Offer(VulnerabilityReport)), then create the case. See How to Initialize a Case.

    Validate report

    Designates the report as valid, moving the sender's RM state to Valid.

    • When… a report receiver judges the report valid
    • Send: Accept(Offer(VulnerabilityReport))
    • Send to: the case manager
    • Sent by: a participant (usually: vendor, coordinator)
    • Reference: Validate report
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Accept",
      "id": "urn:uuid:cde198a0-1ff7-4755-b8aa-a54223d41a2f",
      "name": "https://vultron.example/organizations/vendorco Accept https://vultron.example/users/finndervul Offer FDR-8675309",
      "content": "We've validated the report. We'll be creating a case shortly.",
      "actor": "https://vultron.example/organizations/vendorco",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Offer",
        "id": "urn:uuid:f784de1e-1f71-4a85-a441-da245ee8ebfd",
        "name": "https://vultron.example/users/finndervul Offer FDR-8675309",
        "published": "2026-10-23T20:17:08+00:00",
        "updated": "2026-10-23T20:17:08+00:00",
        "to": [
          "https://vultron.example/organizations/vendorco"
        ],
        "actor": "https://vultron.example/users/finndervul",
        "object": {
          "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
          "type": "VulnerabilityReport",
          "name": "FDR-8675309",
          "published": "2026-10-23T20:17:06+00:00",
          "updated": "2026-10-23T20:17:06+00:00",
          "content": "I found a vulnerability!",
          "attributedTo": "https://vultron.example/users/finndervul",
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
        }
      }
    }
    
  • If the report is not actionable, send Report Invalid (RI), TentativeReject(Offer(VulnerabilityReport)). This holds the report open, so the Reporter can supply information that changes your decision.

    Invalidate report

    Designates the report as invalid, moving the sender's RM state to Invalid.

    • When… a report receiver judges the report invalid
    • Send: TentativeReject(Offer(VulnerabilityReport))
    • Send to: the case manager
    • Sent by: a participant (usually: vendor, coordinator)
    • Reference: Invalidate report
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "TentativeReject",
      "id": "urn:uuid:ba2f79b5-feeb-4261-ad86-3069782c4077",
      "name": "https://vultron.example/organizations/vendorco TentativeReject https://vultron.example/users/finndervul Offer FDR-8675309",
      "content": "We're declining this report as invalid. If you have a reason we should reconsider, please let us know. Otherwise we'll be closing it shortly.",
      "actor": "https://vultron.example/organizations/vendorco",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Offer",
        "id": "urn:uuid:302ed0bd-3b02-4410-a3af-57d955257630",
        "name": "https://vultron.example/users/finndervul Offer FDR-8675309",
        "published": "2026-10-23T20:17:08+00:00",
        "updated": "2026-10-23T20:17:08+00:00",
        "to": [
          "https://vultron.example/organizations/vendorco"
        ],
        "actor": "https://vultron.example/users/finndervul",
        "object": {
          "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
          "type": "VulnerabilityReport",
          "name": "FDR-8675309",
          "published": "2026-10-23T20:17:06+00:00",
          "updated": "2026-10-23T20:17:06+00:00",
          "content": "I found a vulnerability!",
          "attributedTo": "https://vultron.example/users/finndervul",
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
        }
      }
    }
    

If you invalidated the report, have decided not to reconsider, and no case holds it yet, send Report Closed (RC), Reject(Offer(VulnerabilityReport)), to close it. If the Reporter supplies new information that changes your decision, send Accept(Offer(VulnerabilityReport)) instead — RM.INVALID is not terminal.

Close report

Closes the report, moving the sender's RM state to Closed. Once a case holds the report, its Case Owner closes the case with Leave(VulnerabilityCase) instead, and every receiver refuses a Close(Report) for it.

  • When… a report receiver closes a report no case holds
  • Send: Reject(Offer(VulnerabilityReport))
  • Send to: an actor outside any case
  • Sent by: the addressee of the vulnerability report offer (usually: vendor, coordinator)
  • Reference: Close report
Example activity
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Reject",
  "id": "urn:uuid:e6388c3c-e3b4-4497-82ca-5cc821742bda",
  "name": "https://vultron.example/organizations/vendorco Reject https://vultron.example/users/finndervul Offer FDR-8675309",
  "content": "We're closing this report.",
  "actor": "https://vultron.example/organizations/vendorco",
  "object": {
    "@context": "https://www.w3.org/ns/activitystreams",
    "type": "Offer",
    "id": "urn:uuid:305b81f1-9968-42bf-96df-c01a53d8db79",
    "name": "https://vultron.example/users/finndervul Offer FDR-8675309",
    "published": "2026-10-23T20:17:08+00:00",
    "updated": "2026-10-23T20:17:08+00:00",
    "to": [
      "https://vultron.example/organizations/vendorco"
    ],
    "actor": "https://vultron.example/users/finndervul",
    "object": {
      "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
      "type": "VulnerabilityReport",
      "name": "FDR-8675309",
      "published": "2026-10-23T20:17:06+00:00",
      "updated": "2026-10-23T20:17:06+00:00",
      "content": "I found a vulnerability!",
      "attributedTo": "https://vultron.example/users/finndervul",
      "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
    }
  }
}

Closing a report is not closing a case

Reject(Offer(VulnerabilityReport)) applies only while the report has no case. Once a case holds the report, closure is Leave(VulnerabilityCase). By default the receiver creates the case when the report arrives, so this is the usual path. See How to Advance a Case Through Report Management.


Verify

What you sent What to confirm
Offer(VulnerabilityReport) The recipient holds the report and their RM state is RECEIVED.
Accept(Offer(VulnerabilityReport)) Your RM state is VALID and a VulnerabilityCase exists.
TentativeReject(Offer(VulnerabilityReport)) Your RM state is INVALID and the report is still open.
Reject(Offer(VulnerabilityReport)) Your RM state is CLOSED.

See it end to end

Try it: vultron-demo receive-report

vultron-demo receive-report

Or with Docker Compose:

DEMO=receive-report docker compose -f docker/docker-compose.yml run --rm demo

For a narrated walkthrough of the same run, see Tutorial: Run the Receive-Report Demo.

Try it: vultron-demo report-with-embargo — the negotiated path

vultron-demo report-with-embargo

Or with Docker Compose:

DEMO=report-with-embargo docker compose -f docker/docker-compose.yml run --rm demo

Every other demo reaches an active embargo by the default path, where the Reporter states no terms and tacitly accepts the recipient's default. This one exercises the negotiated path (EP-04-003): the Reporter proposes terms with the submission, and the run shows which terms became active, which were left pending as a revision, and that the case owner's default stays active until it decides whether to accept the longer terms (EP-09-006). It runs three times: the Reporter proposes shorter than the recipient's default, longer than it, and against a recipient that has published no default at all.


Further reading

  • Report Management (RM) Messages — the wire format and a rendered example for each activity above
  • Activity Vocabulary Design — why Create(VulnerabilityReport) and Offer(VulnerabilityReport) are separate activities, and why report closure and case closure use different verbs
  • Report Management — the state machine these activities drive
  • Trigger API Reference — request schema and endpoint details for submit-report, validate-report, invalidate-report, and reject-report