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
-
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 theOfferinstead.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" } } -
Send Report Submission (RS), implemented in ActivityStreams as
Offer(VulnerabilityReport), to the recipient's inbox, carrying the report as itsobject.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" } } -
To propose embargo terms for this report, carry an
EmbargoEventon the Offer asproposedEmbargo, with the report as itscontextand the end you want asendTime. 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 anInvite(Event)carrying the revisedEmbargoEventto 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 thesubmit-reporttrigger, state the end asproposed_embargo_end_timeand 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)andOffer(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, andreject-report