How to Advance a Case Through Report Management
Use this guide to move your own Report Management (RM) state through the case lifecycle: engage a case, defer it, re-engage it, and close it.
RM state is per participant, so every step here reports your own position and never anyone else's.
You finish at RM.CLOSED, with each transition recorded on the case ledger.
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.
-
A report you have received, or a case you are seated on.
- Your own participant record on the case. Engaging and deferring are decisions about your participation, not judgments about the report.
The state ladder
The flowchart below places each activity in the RM state it produces. Read it as the set of moves available from wherever you are now.
---
title: Report Management Moves and the States They Produce
---
flowchart TB
subgraph RM:RECEIVED
subgraph as:Offer
RmSubmitReport["Report Submission (RS)<br/>Offer(VulnerabilityReport)"]
end
end
subgraph RM:VALID
subgraph as:Accept
RmValidateReport["Report Valid (RV)<br/>Accept(Offer(VulnerabilityReport))"]
end
subgraph as:Create
CreateCase["Create Case<br/>Create(VulnerabilityCase)"]
end
end
subgraph RM:INVALID
subgraph as:TentativeReject
RmInvalidateReport["Report Invalid (RI)<br/>TentativeReject(Offer(VulnerabilityReport))"]
end
end
subgraph RM:ACCEPTED
subgraph as:Join
RmEngageCase["Report/Case Accepted (RA)<br/>Join(VulnerabilityCase)"]
end
end
subgraph RM:DEFERRED
subgraph as:Ignore
RmDeferCase["Report/Case Deferred (RD)<br/>Ignore(VulnerabilityCase)"]
end
end
subgraph RM:CLOSED
subgraph as:Leave
RmCloseCase["Close Case<br/>Leave(VulnerabilityCase)"]
end
subgraph as:Reject
RmCloseReport["Report Closed (RC)<br/>Reject(Offer(VulnerabilityReport))"]
end
end
d{Done?}
c{Close?}
p{Priority?}
v{Valid?}
start([Start])
start --> RmSubmitReport
RmSubmitReport --> v
v -->|y| RmValidateReport
v -->|n| RmInvalidateReport
RmInvalidateReport --> c
c -->|y| RmCloseReport
RmValidateReport --> CreateCase
CreateCase --> p
p -->|act| RmEngageCase
d -->|n| p
p -->|defer| RmDeferCase
RmEngageCase --> d
RmDeferCase --> d
c -->|n| v
d -->|y| RmCloseCase
Engage or defer a valid report
Once the report is at RM.VALID and a case exists, prioritize it and take one of two moves.
-
If you intend to work the case now, send Report/Case Accepted (RA), implemented in ActivityStreams as
Join(VulnerabilityCase). You are joining the case; your RM state becomesACCEPTEDas a result of joining, not as a separate claim about the report.Report/Case Accepted (RA)
Joins the case, moving the sender's RM state to Accepted. Engaging is a case-participation decision, so the verb names the case rather than the report.
- When… a Participant accepts the case for further action
- Send:
Join(VulnerabilityCase) - Send to: the case manager
- Sent by: a participant
- Reference: Report/Case Accepted (RA)
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Join", "id": "urn:uuid:ae5d2c31-17a1-4309-b27d-e6aa0f010d30", "name": "https://vultron.example/organizations/vendorco Join VendorCo Case #70930046", "content": "We're engaging this case.", "actor": "https://vultron.example/organizations/vendorco", "object": { "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "type": "VulnerabilityCase", "name": "VendorCo Case #70930046", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "attributedTo": "https://vultron.example/organizations/vendorco", "caseParticipants": [], "actorParticipantIndex": {}, "vulnerabilityReports": [], "caseStatuses": [ { "id": "urn:uuid:b1433c7d-659a-48f4-bd42-4fe704585f61", "type": "CaseStatus", "name": "NONE pxa", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "attributedTo": "https://vultron.example/organizations/vendorco", "emState": "NONE", "pxaState": "pxa", "@context": "https://certcc.github.io/Vultron/ns/context.jsonld" } ], "notes": [], "embargoRegister": [], "pendingEmbargoProposalIndex": {}, "caseActivity": [], "genesisHash": "3f3a0359462714e6979b2d77ae6e953e1381472e9bef61933073af26469fad37", "ledgerPosition": { "logIndex": -1, "entryHash": "3f3a0359462714e6979b2d77ae6e953e1381472e9bef61933073af26469fad37" }, "parentCases": [], "childCases": [], "siblingCases": [], "activeParticipants": [], "@context": "https://certcc.github.io/Vultron/ns/context.jsonld" } } -
If you intend to work it later, send Report/Case Deferred (RD),
Ignore(VulnerabilityCase). Deferring is likewise a decision about the case, and your RM state becomesDEFERREDas a result (MSM-01-004, MSM-01-005).Report/Case Deferred (RD)
Ignores the case, moving the sender's RM state to Deferred. Deferring is a case-participation decision, so the verb names the case rather than the report.
- When… a Participant defers further action on the case
- Send:
Ignore(VulnerabilityCase) - Send to: the case manager
- Sent by: a participant
- Reference: Report/Case Deferred (RD)
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Ignore", "id": "urn:uuid:ffd20ddc-40a0-4691-9e9e-0540c3c0abe2", "name": "https://vultron.example/organizations/vendorco Ignore VendorCo Case #70930046", "content": "We're deferring this case.", "actor": "https://vultron.example/organizations/vendorco", "object": { "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "type": "VulnerabilityCase", "name": "VendorCo Case #70930046", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "attributedTo": "https://vultron.example/organizations/vendorco", "caseParticipants": [], "actorParticipantIndex": {}, "vulnerabilityReports": [], "caseStatuses": [ { "id": "urn:uuid:b1433c7d-659a-48f4-bd42-4fe704585f61", "type": "CaseStatus", "name": "NONE pxa", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "attributedTo": "https://vultron.example/organizations/vendorco", "emState": "NONE", "pxaState": "pxa", "@context": "https://certcc.github.io/Vultron/ns/context.jsonld" } ], "notes": [], "embargoRegister": [], "pendingEmbargoProposalIndex": {}, "caseActivity": [], "genesisHash": "3f3a0359462714e6979b2d77ae6e953e1381472e9bef61933073af26469fad37", "ledgerPosition": { "logIndex": -1, "entryHash": "3f3a0359462714e6979b2d77ae6e953e1381472e9bef61933073af26469fad37" }, "parentCases": [], "childCases": [], "siblingCases": [], "activeParticipants": [], "@context": "https://certcc.github.io/Vultron/ns/context.jsonld" } }
Deferring is reversible.
To re-engage a deferred case, send the same Join(VulnerabilityCase) activity you would have sent the first time — there is no separate re-engagement activity, and the earlier deferral remains part of the case history.
Close your participation
Pick the closure activity that matches what exists.
-
If a case exists, send
Leave(VulnerabilityCase). The formal message set has no name for this closure, even though ADR-0050 makes it the canonical one. Theleave-casetrigger sends it for you, after running your pre-close integration step.Close case
Leaves the case, moving the sender's RM state to Closed.
- When… a Participant ends its participation in the case
- Send:
Leave(VulnerabilityCase) - Send to: the case manager
- Sent by: the case owner or a participant
- Reference: Close case
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Leave", "id": "urn:uuid:4b30e15d-d0b3-4cf4-9576-6f2af4d9b656", "name": "https://vultron.example/organizations/vendorco Leave VendorCo Case #70930046", "content": "We're closing this case.", "actor": "https://vultron.example/organizations/vendorco", "object": { "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "type": "VulnerabilityCase", "name": "VendorCo Case #70930046", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "attributedTo": "https://vultron.example/organizations/vendorco", "caseParticipants": [], "actorParticipantIndex": {}, "vulnerabilityReports": [], "caseStatuses": [ { "id": "urn:uuid:b1433c7d-659a-48f4-bd42-4fe704585f61", "type": "CaseStatus", "name": "NONE pxa", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "attributedTo": "https://vultron.example/organizations/vendorco", "emState": "NONE", "pxaState": "pxa", "@context": "https://certcc.github.io/Vultron/ns/context.jsonld" } ], "notes": [], "embargoRegister": [], "pendingEmbargoProposalIndex": {}, "caseActivity": [], "genesisHash": "3f3a0359462714e6979b2d77ae6e953e1381472e9bef61933073af26469fad37", "ledgerPosition": { "logIndex": -1, "entryHash": "3f3a0359462714e6979b2d77ae6e953e1381472e9bef61933073af26469fad37" }, "parentCases": [], "childCases": [], "siblingCases": [], "activeParticipants": [], "@context": "https://certcc.github.io/Vultron/ns/context.jsonld" } } -
If the report was invalidated before any case was created, send Report Closed (RC),
Reject(Offer(VulnerabilityReport)).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:87df6896-66f2-454b-8f9c-bc7a819f58b9", "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:fb4005bd-4a46-4cc4-bcd4-7adea1311bad", "name": "https://vultron.example/users/finndervul Offer FDR-8675309", "published": "2026-10-23T20:17:07+00:00", "updated": "2026-10-23T20:17:07+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" } } }
RM.CLOSED is terminal (ADR-0085).
There is no rejoin, so send the closure only when you are finished with the case.
Your own RM state does not change when you send the closure.
The CASE_MANAGER records the Leave, then records each step that moves you to RM.CLOSED as its own ledger entry, and your replica applies those entries when they arrive (CM-23-001, CM-23-004).
Do not write RM.CLOSED to your own replica in their place.
Those entries are the last case content you receive: after your closure, the CASE_MANAGER sends you no further ledger entries, case announcements, status broadcasts, or embargo announcements (CM-23-004).
If you are bound by an embargo that later ends early or gets shorter, the CASE_MANAGER still sends you a direct notice of that change (CM-31-009).
What your closure does to the case depends on whether you are the Case Owner.
- If you are not the Case Owner, only your own RM state advances to
RM.CLOSED. The case stays open for everyone else (CM-23-003). - If you are the Case Owner, your
Leave(VulnerabilityCase)closes the case. The CASE_MANAGER first expires every embargo Invite that still waits for a reply, and records each expiry as its own change to that invitee's consent row, nowTIMED_OUT(CM-23-014). It then advances you toRM.CLOSED, then itself, and records each step as a status entry (CM-23-002). The CASE_MANAGER's own status entry that reachesRM.CLOSEDis the last entry of the closure: from then on the case is fully closed. Every other participant keeps the RM state it already held — closure does not advance bystanders (CM-23-012). - If you want your departure recorded, leave before the Case Owner does (CM-23-015).
Once the case is fully closed, the CASE_MANAGER records nothing more for it (CM-23-013).
A
Leave(VulnerabilityCase)you send after that is declined: nothing is recorded and your RM state on the case ledger stays where it was. By default the CASE_MANAGER sends no reply to it. A CASE_MANAGER configured withpost_close_reply: rejectanswers it with anas:Rejectof yourLeaveinstead. - Any other activity you send for the case after it is fully closed, such as
Add(Note), is dropped: nothing is recorded, and the CASE_MANAGER never replies, whatever its configuration (ADR-0131). Do not wait for a reply to an activity sent after the case has closed.
An owner closure is refused while an embargo is active
If you are the Case Owner and the case still holds an active embargo — the Embargo Management (EM) state is ACTIVE or REVISE — the CASE_MANAGER declines your Leave(VulnerabilityCase) with an as:Reject and runs no part of the closure sequence (CM-23-011).
Terminate the embargo first: see How to Revise or Terminate an Embargo.
Verify
| What you sent | What to confirm |
|---|---|
Join(VulnerabilityCase) |
Your participant record shows rmState = ACCEPTED. |
Ignore(VulnerabilityCase) |
Your participant record shows rmState = DEFERRED. |
Leave(VulnerabilityCase) |
Your participant record shows rmState = CLOSED. Your replica holds no ledger entry after the last entry that records your closure. A Case Owner does not receive the CASE_MANAGER's own closure entries, because it is already closed when the CASE_MANAGER writes them; those entries are in the CASE_MANAGER's ledger. |
Reject(Offer(VulnerabilityReport)) |
Your RM state is CLOSED and no case was created. |
Each transition is committed to the case ledger, so the same rmState should be visible on the replica of every participant that has not closed, not only your own.
A participant that has already closed does not receive later transitions (CM-23-004).
See it end to end
Try it: vultron-demo manage-case
vultron-demo manage-case
Or with Docker Compose:
DEMO=manage-case docker compose -f docker/docker-compose.yml run --rm demo
The scenario runs the engage path, the defer-and-re-engage path, and the invalidate path.
Further reading
- Report Management (RM) Messages — the wire format and a rendered example for each activity above
- Case Management Messages — the wire format for
Leave(VulnerabilityCase) - Activity Vocabulary Design — why re-engagement is not an
as:Undo, and why the two closures use different verbs - Report Management — the state machine these activities drive
- Trigger API Reference — request schema and endpoint details for
create-case,engage-case,defer-case,add-report-to-case, andadd-object-to-case