Tutorial: Submit a report to a Vultron actor
In this tutorial, we will submit a vulnerability report to a running Vultron
actor and confirm that the actor received it.
We will start the reference implementation, create an actor to play the part of
a vendor, send that actor a Create(VulnerabilityReport) message, and read the
report back out of the actor's store.
This tutorial is for a developer who wants to see, first-hand, what it takes to
speak to a Vultron actor over the wire.
What we will learn
Vultron actors exchange ActivityStreams messages over HTTP. We will construct one such message, post it to an actor's Inbox, and watch the report land in the receiving actor's store. We will not cover the full report-management lifecycle here; the How to Report a Vulnerability how-to guide takes the story further.
The commands and the message on this page are rendered from
vultron.wire.as2.vocab.examples.submit_report_tutorial, the same module a
test drives end-to-end, so what you copy here is what the code actually does.
Prerequisites
You need the following before we begin:
- uv (the project's Python runner; it manages its own virtual environment)
curl(to send HTTP requests from a second terminal)- Git (to clone the repository)
You do not need to install Python or any dependencies by hand; uv run
resolves them on first use.
Step 1 — Clone the repository
First, get a local copy of the Vultron project:
git clone https://github.com/CERTCC/Vultron.git
cd Vultron
Tip
If you already have a local clone, cd into the repository root and run
git pull so you are up to date.
Step 2 — Start the Vultron API server
From the repository root, start the reference API server:
uv run uvicorn vultron.adapters.driving.fastapi.main:app --host 127.0.0.1 --port 7999
The first run downloads dependencies, so it may take a moment. When the server is ready, you should see:
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:7999 (Press CTRL+C to quit)
Leave this terminal running.
The server now serves the Vultron API at http://localhost:7999/api/v2.
Open a second terminal and confirm the server is live:
curl http://localhost:7999/api/v2/health/live
The server responds with:
{"status":"ok"}
Prefer containers?
You can also start the same API server with Docker Compose by running
docker compose -f docker/docker-compose.yml up api-dev, which exposes the
API at the same http://localhost:7999/api/v2 address.
The remaining steps are identical.
See Run the Receive-Report Demo for the
container-based tour.
Step 3 — Create an actor to receive the report
A report has to be sent to someone, so we first create an actor to play the part of a vendor. Run this in the second terminal:
curl -i -X POST http://localhost:7999/api/v2/actors/ \
-H 'Content-Type: application/json' \
-d '{
"name": "VendorCo",
"actor_type": "Organization",
"id": "vendorco"
}'
The server responds with 201 Created and the new actor record:
{"id":"http://localhost:7999/api/v2/actors/vendorco","type":"Organization","name":"VendorCo","published":"2026-01-01T00:00:00+00:00","updated":"2026-01-01T00:00:00+00:00"}
Your published and updated timestamps will differ; everything else matches.
Notice that the actor's id is the URL that reaches it.
Its Inbox — the endpoint where it receives messages — is that same URL with
/inbox/ appended.
Step 4 — Submit the report
Now we send the vendor a Create(VulnerabilityReport) message.
This is an ActivityStreams Vocabulary 2.0 (AS2) activity: a Create activity
whose object is the VulnerabilityReport we are reporting.
The to field addresses the message to our vendor by its id, the URL from Step 3:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "urn:uuid:11111111-1111-1111-1111-111111111111",
"name": "https://finder.example/users/finn Create FDR-0001",
"to": [
"http://localhost:7999/api/v2/actors/vendorco"
],
"actor": "https://finder.example/users/finn",
"object": {
"id": "https://finder.example/reports/report-001",
"type": "VulnerabilityReport",
"name": "FDR-0001",
"published": "2026-01-01T00:00:00+00:00",
"updated": "2026-01-01T00:00:00+00:00",
"content": "I found a vulnerability!",
"attributedTo": "https://finder.example/users/finn",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
},
"published": "2026-01-01T00:00:00+00:00"
}
Post it to the vendor's Inbox:
curl -i -X POST http://localhost:7999/api/v2/actors/vendorco/inbox/ \
-H 'Content-Type: application/json' \
-d '{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "urn:uuid:11111111-1111-1111-1111-111111111111",
"name": "https://finder.example/users/finn Create FDR-0001",
"to": [
"http://localhost:7999/api/v2/actors/vendorco"
],
"actor": "https://finder.example/users/finn",
"object": {
"id": "https://finder.example/reports/report-001",
"type": "VulnerabilityReport",
"name": "FDR-0001",
"published": "2026-01-01T00:00:00+00:00",
"updated": "2026-01-01T00:00:00+00:00",
"content": "I found a vulnerability!",
"attributedTo": "https://finder.example/users/finn",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
},
"published": "2026-01-01T00:00:00+00:00"
}'
The server responds with 202 Accepted:
HTTP/1.1 202 Accepted
The 202 Accepted status is the actor's acknowledgment.
It means the actor received the message and accepted it for processing.
Processing then happens in the background, so the response body is empty.
Switch to the terminal running the server. You should see the actor process the report:
INFO: Dispatching activity of type 'VulnerabilityReport' with semantics 'create_report'
INFO: BT execution completed: Status.SUCCESS after 1 ticks -
INFO: Dispatch: dispatched create_report activity_id=urn:uuid:11111111-1111-1111-1111-111111111111
The following diagram shows the exchange we just completed: a reporter posts the report to the vendor's Inbox, and the vendor acknowledges it.
---
title: Submitting a report to a Vultron actor
---
sequenceDiagram
autonumber
actor reporter as Reporter (curl)
participant inbox as Vendor Inbox
reporter->>inbox: POST /actors/vendorco/inbox/<br/>Create(VulnerabilityReport)
inbox-->>reporter: 202 Accepted
Note over inbox: create_report runs<br/>report stored
Step 5 — Confirm the actor received the report
The 202 Accepted told us the actor accepted the message.
Let's confirm the vendor actually stored the report by reading its store back:
curl http://localhost:7999/api/v2/actors/vendorco/datalayer/Reports/
The vendor returns the report we submitted, keyed by its id:
{
"https://finder.example/reports/report-001": {
"id": "https://finder.example/reports/report-001",
"type": "VulnerabilityReport",
"name": "FDR-0001",
"published": "2026-01-01T00:00:00+00:00",
"updated": "2026-01-01T00:00:00+00:00",
"content": "I found a vulnerability!",
"attributedTo": "https://finder.example/users/finn",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
}
The report, with our content of "I found a vulnerability!", is now in the
vendor's store.
The vendor received our report.
Step 6 — Stop the server
When you are finished, return to the first terminal and press Ctrl+C to stop the server.
What we accomplished
We have:
- started the Vultron reference implementation with a single
uv runcommand, - created an actor to receive a report,
- constructed a
Create(VulnerabilityReport)AS2 message and posted it to that actor's Inbox, and - confirmed, by reading the actor's store, that the report was received.
Next steps
- Go deeper on report submission — read
How to Report a Vulnerability
to see how a report is validated, accepted, and turned into a Case, and how
the
Offer(Report)activity fits alongsideCreate(Report). - See the full workflow run for you — follow Run the Receive-Report Demo to watch a finder and a vendor exchange reports end-to-end with the demo CLI.
- Read the source of these examples — the commands and the message above
come from
vultron/wire/as2/vocab/examples/submit_report_tutorial.py, and the flow is verified end-to-end intest/demo/test_submit_report_tutorial.py.
Reading path: You maintain a vulnerability tracker and want it to talk to your partners
This page is part of Send your first message.
Before this page:
After this page: