Skip to content
How-to guide

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 becomes ACCEPTED as 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 becomes DEFERRED as 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. The leave-case trigger 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, now TIMED_OUT (CM-23-014). It then advances you to RM.CLOSED, then itself, and records each step as a status entry (CM-23-002). The CASE_MANAGER's own status entry that reaches RM.CLOSED is 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 with post_close_reply: reject answers it with an as:Reject of your Leave instead.
  • 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