Engagement profiles
An engagement profile tells rupu what kind of thing an engagement is about — source code, a binary, a network, a web application, a cloud account, a threat model — and therefore what a complete finding about it looks like.
Concepts
A profile is a small TOML file. It declares the asset kinds that exist in its domain, the coordinates that pin each one down, a completeness checklist that every finding must pass, and a depth ladder that records how far each asset has been examined. rupu ships 15 profiles. You can override any of them, or add your own by writing a TOML file — no Rust needed.
- Asset. A finding is always about something: a file, a function at an address, a service on a host, a route, a cloud resource. That thing is an asset. It has a kind, a locator and, optionally, a depth.
- Kind. Every kind belongs to a profile and carries its namespace:
network:service,web:route,binary:function. Kinds can form a hierarchy within their profile (aservicesits under ahost). - Routing. When a finding names an asset, its kind’s namespace picks the profile that owns it. That profile, and only that profile, judges the finding.
- Validation. The owning profile’s required completeness checks run against the finding’s report and the asset’s locator. A finding that fails is rejected, and the error lists each unmet check, so the agent can fix it and record it again.
- Depth. Each profile has a depth ladder, such as
discovered → enumerated → tested → exploited. It records how deeply each asset was examined, separately from any findings about it.
bash and whatever tooling they like; rupu records and validates the findings that result, not the traffic.Coordinates
A locator is the set of typed coordinates that pins an asset down. The core knows exactly 13 coordinate types, and every profile builds on them. Each is written as {"t": <tag>, "v": <value>}. A locator can be empty — a threat-model component has no coordinates at all.
| Tag | Value | Typical use |
|---|---|---|
path | string | file or archive path |
line_range | {"start": 10, "end": 24} | lines in a file |
symbol | string | function or symbol name |
commit | string | VCS commit |
sha256 | string | hash of a binary, image or package |
offset | integer | byte offset |
address | integer | virtual address |
host | string | hostname or IP |
port | {"number": 443, "proto": "tcp"} (tcp | udp | other) | network port |
url | string | URL |
http_route | {"method": "GET", "path": "/api/items"} | HTTP method + path |
param | string | request parameter |
resource_id | {"scheme": "arn", "id": "…"} | cloud resource, package URL, objective id |
Selecting profiles
rupu run --engagement-profile binary firmware-re "reverse ./dist/updater.bin" rupu run --engagement-profile network recon "map 10.20.0.0/24" rupu run --engagement-profile network,web assessor "..." # two profiles rupu run --engagement-profiles pentest assessor "..." # a composite rupu workflow run edge-sweep --engagement-profile network
| Surface | How | Notes |
|---|---|---|
rupu run | --engagement-profile <id> | Alias --engagement-profiles; repeat the flag or pass a comma-separated list. Sub-agents started with dispatch_agent inherit the selection. |
rupu workflow run | --engagement-profile <id> | Applies to every agent step and to action: findings.record steps. The selection is recorded on the run (engagement_profiles in run.json), so rupu workflow resume, workflow approve and a gate approved in the control plane continue under it. A recorded profile that no longer resolves fails the resume. |
| Agentiflows | engagement_profiles: (required) | rupu validates the definition against the profiles and passes the set to the lead and every unit. See Agentiflows. |
| CP agent launch API | engagement_profiles: [..] in the POST /api/agents/:name/run body | Passed to rupu run --engagement-profile, locally or on the chosen host (see the remote row). The web launcher doesn’t offer it. |
| Sessions, autoflows | — | These don’t accept a selection and always run on the code path. |
| Remote workflow units | inherited from rupu workflow run | A host: / distribute: unit gets the ids as rupu run --engagement-profile, on every transport. A host must advertise agent.engagement_profile (see Multi-host); otherwise the launch is refused, never run on the code path. An SSH remote too old for the flag rejects it and the launch fails. The host resolves the ids against its own profiles, so install a custom profile there too. |
rupu coverage rerun | — | Replays on the code path. |
- No selection gives the native
codepath. This is not the same as selectingcode: no engagement is active, soasset_markisn’t offered andassetarguments are ignored. - An unknown id fails the launch. It never falls back to a default.
- A kind claimed twice is an error.
pentest,networkfails becausepentestalready includesnetwork. - Overlay profiles load at launch. If a discovered profile file fails to parse, the launch fails, even if you didn’t select that profile.
The built-in catalog
code is the default. Each asset-kind entry gives its locator coordinates, with its parent kind in parentheses where it has one.
| Profile | Purpose | Asset kinds → coordinates | Depth ladder |
|---|---|---|---|
code | Source code review | repo → path; file (repo) → path, line_range; function (file) → path, symbol, line_range | unreviewed → reviewed |
binary | Binary reverse engineering | binary → sha256; function (binary) → sha256, address, symbol | located → disassembled → analyzed |
firmware | Firmware assessment | image → sha256; partition (image) → sha256, path; file (partition) → path, sha256; function (file) → path, address, symbol | acquired → extracted → unpacked → analyzed |
network | Network security assessment | host → host; service (host) → host, port | discovered → enumerated → tested → exploited |
web | Web application assessment | site → url; route (site) → url, http_route, param | mapped → crawled → tested → exploited |
api | API security assessment | service → url; endpoint (service) → url, http_route, param | mapped → tested → exploited |
cloud | Cloud configuration review | account → resource_id; resource (account) → resource_id | inventoried → policy-evaluated → validated |
sca | Software composition analysis | repo → path; dependency (repo) → path, resource_id | inventoried → resolved → triaged |
iac | Infrastructure-as-code review | repo → path; resource (repo) → path, line_range | mapped → reviewed |
secrets | Secret scanning | repo → path; secret (repo) → path, line_range, commit | scanned → triaged → confirmed |
container | Container image assessment | image → sha256; layer (image) → sha256, path; package (image) → resource_id | inventoried → scanned → triaged |
redteam | Red team operation | host → host; objective (host) → host, resource_id | planned → executed → validated |
threat-model | Threat model | system → none; component (system) → none | modeled → reviewed |
mobile | Mobile application assessment (composite: binary + web) | package → sha256 | unpacked → static → dynamic |
pentest | Penetration test (composite: network + web) | none of its own | none of its own |
Completeness, evidence and taxonomies
Every built-in check is required, and report_finding enforces them. Evidence blocks and taxonomies are advisory: rupu enforces them only when a check names one. Taxonomy matching ignores case, and a report’s legacy cwe list counts as CWE.
| Profile | Required checks | Evidence blocks | Taxonomies |
|---|---|---|---|
code | located: has path · has_root_cause | text, code_slice, diff | CWE, OWASP |
binary | evidence_has_listing: a disasm or hexdump block · has_root_cause · classified: CWE | text, code_slice, diff, hexdump, disasm | CWE, CVE |
firmware | evidence_has_listing: disasm, decompile or hexdump · classified: CWE | text, hexdump, disasm, decompile, table | CWE, CVE |
network | service_identified: host and port · has_root_cause · classified: CVE or CWE | text, table, scan_output, http_exchange, pcap_ref | CVE, CWE, CAPEC, ATT&CK |
web | route_identified: http_route or url · evidence_has_exchange: http_exchange · classified: OWASP or CWE | text, http_exchange, image, table | OWASP, CWE, CVE |
api | endpoint_identified: http_route · evidence_has_exchange: http_exchange | text, http_exchange, table | OWASP-API, CWE |
cloud | resource_identified: resource_id · classified: CIS or CWE | text, table, code_slice | CIS, CWE, ATT&CK |
sca | dependency_identified: resource_id (a purl) · classified: CVE or GHSA | table, text | CVE, GHSA |
iac | located: path and line_range · classified: CIS or CWE | code_slice, diff, table | CIS, CWE |
secrets | located: path and line_range · classified: CWE | code_slice, text | CWE |
container | component_identified: resource_id or path · classified: CVE | table, text | CVE |
redteam | mapped_to_attack: ATT&CK · has_narrative: description | text, scan_output, image | ATT&CK |
threat-model | has_threat: description · classified: STRIDE | text, image, table | STRIDE, CAPEC, ATT&CK |
mobile | package_identified: sha256, address or http_route (its own mobile:package kind only) | disasm, http_exchange, image | MASVS, OWASP, CWE |
pentest | none of its own | — | — |
Every built-in profile also declares a [bundle]: the agents and workflows an engagement of that kind typically uses. A bundle never grants tools. The stock security fleet is built from these bundles, and rupu’s own tests keep the two in lockstep.
Composites and routing
A composite is a profile with an includes list. Selecting one is shorthand for selecting its members, and each member stays its own routing target — the members are not merged. During a pentest run, a network:service finding is judged by network’s checks and a web:route finding by web’s. mobile adds its own root kind, mobile:package, alongside binary and web. includes is transitive; an include cycle, or an include of a profile that doesn’t exist, is a load error.
The stock security fleet
A profile says what a complete finding looks like; it doesn’t ship anyone to find them. The stock fleet does: 18 generic security-assessment agents and 3 workflows, one set for every built-in bundle. Install it once and every engagement, in any project, can draw on it.
rupu fleet list # what the fleet ships rupu fleet install # into ~/.rupu (or $RUPU_HOME) rupu fleet install --project # into this project's .rupu/ instead rupu fleet install --force # re-seed files you've edited
The fleet is plain files: agents/<name>.md and workflows/<id>.yaml, written exactly where rupu’s loaders look. A file that already exists is kept, so your edits survive a reinstall; each file is reported as CREATED, SKIPPED or OVERWROTE. --force overwrites them with the shipped versions. --project needs a .rupu/ (run rupu init first), and a project copy shadows the global one, as for any agent or workflow.
| Profile | Bundle agents | Bundle workflows |
|---|---|---|
network | assessment-lead, recon, service-analyst, exploit-verifier | network-assessment |
web | assessment-lead, crawler, appsec-tester | web-assessment |
api | assessment-lead, api-tester | api-assessment |
pentest | the network and web agents | network-assessment, web-assessment |
redteam | assessment-lead, redteam-operator | — |
code | code-auditor | — |
binary | binary-analyst | — |
firmware | firmware-analyst | — |
cloud | cloud-auditor | — |
sca | sca-auditor | — |
iac | iac-reviewer | — |
secrets | secret-scanner | — |
container | container-scanner | — |
threat-model | threat-modeler | — |
mobile | mobile-analyst | — |
- Specialists each do one job inside the authorized scope they’re given. Discovery agents (
recon,crawler) map andasset_markwhat they find but don’t test; analysts and testers verify before theyreport_finding, pinned to the asset with the evidence and classification their profile’s checks require;exploit-verifierindependently confirms or refutes claimed exposures. Destructive testing is ruled out in every prompt. assessment-leadis a generic agentiflow lead. It reads the scope and goals, lists the pool withagents.list/workflows.listrather than assuming names, then works in rounds — map, analyze in parallel batches, verify — until the scope is swept or the budget runs low.- The workflows chain the same agents for a fixed run:
network-assessmentis recon → analyze → verify,web-assessmentis crawl → test, andapi-assessmentis a single assess step. Each takes one required input,scope. - Every fleet agent runs in
bypassmode with a wide tool grant (*plus the findings and asset tools). That suits an authorized lab engagement; for anything else, read the files and narrow them before you run them.
Run a workflow under its profile so the asset and findings tools are active:
rupu fleet install rupu workflow run web-assessment --engagement-profile web \ --input scope="https://staging.example.test (exclude /admin/billing)"
Or hand the whole pool to the lead in an agentiflow:
# .rupu/agentiflows/lab-sweep.yaml (excerpt) lead: assessment-lead engagement_profiles: [network] pool: agents: [assessment-lead, recon, service-analyst, exploit-verifier] workflows: [network-assessment]
Recording assets
Two tools write assets, both to the target’s asset ledger, .rupu/coverage/<target>/assets.jsonl, next to findings.jsonl.
report_finding with an asset
When an engagement is active, report_finding takes an optional asset. So does the MCP findings.record tool a workflow action: step calls, with the same schema and handling.
{
"scope": "edge-sweep",
"report": { /* a complete finding report */ },
"asset": {
"kind": "network:service",
"coordinates": [
{ "t": "host", "v": "198.51.100.20" },
{ "t": "port", "v": { "number": 8443, "proto": "tcp" } }
],
"label": "198.51.100.20:8443 admin console"
}
}
When an engagement is active, rupu:
- Routes the finding by its namespaced
kind. If no active profile owns the kind, the call fails, and the error names the active set. - Checks completeness. Every required check of the owning profile is evaluated under the
fullfinding profile. If any check fails, the finding is rejected with the labels of the failing checks. Undersummarythere’s no report to evaluate, so this step is skipped. - Stamps the asset into
assets.jsonland the run’s coverage stream, so a coordinator collecting a remote unit receives it. The new line keeps the asset’s existing depth, parent, attributes and label, unless the call supplies a new label.
The finding record itself does not store an asset id. When no engagement is active, asset is ignored. When an engagement is active but the finding has no asset, it records on the plain path without a completeness check. rupu findings import records no asset.
asset_mark
{ "kind": "web:route",
"coordinates": [ { "t": "url", "v": "https://shop.example.test" },
{ "t": "http_route", "v": { "method": "POST", "path": "/cart/apply" } } ],
"depth": "tested" }
depth must be a rung of the owning profile’s ladder; any other value is an error that lists the ladder. Depth only moves forward: if an asset is already at exploited, marking it tested leaves it at exploited, and the tool returns the rung that ends up recorded (effective_depth). The engine offers asset_mark and report_finding to every agent in an engagement run, whatever the agent’s tools: list says. Without an active engagement, asset_mark doesn’t exist.
The ledger and its views
assets.jsonl only ever gets appended to, one full asset per line (id, kind, parent, locator, label, depth, attributes). When the file is read, the last line for each id wins. An asset’s id is <kind>:<16 hex>, derived from the kind plus the sorted locator, so the same host and port always map to the same asset, across reruns too. The profile’s label template is declarative and isn’t rendered: an asset’s label is whatever the caller passed, or else its kind.
Remote units’ asset lines arrive through the coverage stream and are merged into the coordinator. The control plane’s Security → Assets page lists every asset from every registered workspace, and an agentiflow’s Assets tab shows the same data for one run (both use GET /api/assets). Agentiflow coverage goals measure this ledger.
Writing a profile
Where profiles live
| Source | Location | Precedence |
|---|---|---|
| Built-in | compiled into the binary | lowest |
| Global | ~/.rupu/profiles/*.toml | overrides built-in |
| Project | .rupu/profiles/*.toml in the run’s workspace (for an agentiflow, the directory you launch from) | highest |
rupu identifies a profile by its id field; the file name doesn’t matter. A later source with the same id replaces the earlier profile completely — nothing is merged — so to change one check of a built-in, copy the whole built-in and edit the copy. Within one directory, files load in name order, and the last one wins. Only *.toml files are read, and a missing directory is fine.
Schema
All keys are strict: an unknown key in any table is a parse error. Put the top-level keys before the first [[asset_kinds]], or TOML assigns them to that kind and the file fails to parse.
| Key | Type | Meaning |
|---|---|---|
id | string, required | Profile id, and the namespace of its kinds (<id>:<kind>) |
name | string, required | Human-readable name |
includes | string list | Profiles this one includes (makes it a composite) |
evidence_blocks | string list | Evidence-block kinds the engagement expects (advisory) |
classification_systems | string list | Taxonomies the engagement uses (advisory) |
[[asset_kinds]] id | string, required | Bare kind id, namespaced as <profile>:<id> |
[[asset_kinds]] parent | string | Parent kind id in the same profile; omit for a root |
[[asset_kinds]] coordinates | string list | Coordinate tags an asset of this kind carries (descriptive) |
[[asset_kinds]] label | string, required | Label template such as "{host}:{port}" (descriptive, not rendered) |
[[completeness]] id / label | string, required | Check id; the label is shown in the rejection message when the check fails |
[[completeness]] required | bool, default true | false makes the check informational; it isn’t evaluated |
[[completeness]] satisfied_when | predicate, required | See below |
[coverage] enumerates | string list | Bare kind ids that agentiflow coverage goals count by default |
[coverage] depth_ladder | string list | Ordered rungs, shallowest first; the only depths asset_mark accepts. The last rung is a coverage goal’s default target. |
[bundle] agents / workflows / tools | string lists | The agents, workflows and tools an engagement of this kind typically uses. Descriptive only: never a grant, and nothing installs or loads them for you |
Predicates
| Predicate | True when |
|---|---|
{ has_field = "…" } | the report field is filled in: root_cause, remediation, impact, description, attack_vector, category (non-blank), replication_steps (non-empty), evidence (any claim or block) |
{ has_block_kind = "…" } | the report has at least one block of that kind: text, code_slice, diff, table, image, hexdump, disasm, decompile, http_exchange, scan_output, pcap_ref |
{ has_classification_system = "…" } | a classification uses that system (case-insensitive) |
{ locator_has_coordinate = "…" } | the asset’s locator has a coordinate with that tag |
{ min_severity = "…" } | rating.risk_rating is at least Low, Medium, High or Critical (capitalized) |
{ all = [ … ] } / { any = [ … ] } | every / at least one nested predicate holds |
That is the whole predicate grammar. It isn’t meant to grow into a scripting language.
Failure modes
| Mistake | When it shows up | What you see |
|---|---|---|
Unknown key, wrong type, or a top-level key after [[asset_kinds]] | launch, for any run with a selection | engagement profiles: unloadable profiles: <file>: profile parse: … — even if this profile wasn’t selected |
| Unreadable file or directory | launch | the same message, with the IO error |
| Missing include, or an include cycle | launch | profile `x` is included but not defined / include cycle at profile `x` |
| Selected id doesn’t exist | launch | unknown engagement profile `x` |
| Two selections contribute the same kind | launch | asset kind `x:y` is contributed by more than one selected profile |
| Typo in a predicate’s field, block kind or coordinate tag | the first finding that routes to the profile | that finding is rejected with completeness predicate error for profile … |
Worked example: an IoT fleet engagement
The profile below is invented for this guide. It covers fleets of embedded devices: a device is found by its address, it exposes services, and each firmware build is identified by its hash. Save it as .rupu/profiles/iot-fleet.toml:
id = "iot-fleet" name = "IoT device fleet assessment" evidence_blocks = ["text", "scan_output", "hexdump", "http_exchange", "table"] classification_systems = ["CWE", "CVE"] [[asset_kinds]] id = "device" coordinates = ["host"] label = "{host}" [[asset_kinds]] id = "service" parent = "device" coordinates = ["host", "port"] label = "{host}:{port}" [[asset_kinds]] id = "firmware" parent = "device" coordinates = ["host", "sha256"] label = "{sha256}" [[completeness]] id = "pinned" label = "Pinned to a device service or a firmware build" satisfied_when = { all = [ { locator_has_coordinate = "host" }, { any = [ { locator_has_coordinate = "port" }, { locator_has_coordinate = "sha256" } ] }, ] } [[completeness]] id = "evidence" label = "Scan output, hexdump or HTTP exchange attached" satisfied_when = { any = [ { has_block_kind = "scan_output" }, { has_block_kind = "hexdump" }, { has_block_kind = "http_exchange" }, ] } [[completeness]] id = "classified" label = "Classified (CWE or CVE)" satisfied_when = { any = [ { has_classification_system = "CWE" }, { has_classification_system = "CVE" } ] } [[completeness]] id = "has_fix" label = "Remediation for high-severity issues" required = false # informational: not evaluated satisfied_when = { has_field = "remediation" } [coverage] enumerates = ["device", "service", "firmware"] depth_ladder = ["discovered", "fingerprinted", "tested", "exploited"]
rupu run --engagement-profile iot-fleet fleet-recon "assess the lab segment 192.0.2.0/28"
During the run, the agent might call asset_mark with kind iot-fleet:device, host 192.0.2.7 and depth fingerprinted, then call report_finding with an iot-fleet:service asset pinned by host and port 23/tcp, and a report that has a scan_output block and a CWE classification. If the agent omits the port and the hash, the finding is rejected with Pinned to a device service or a firmware build, and the agent can add the missing coordinates and try again.
Profiles, finding profiles and concern catalogs
Three separate rupu features have names that sound alike. They don’t depend on each other:
| Engagement profile | Finding profile | Concern catalog | |
|---|---|---|---|
| Answers | What is this engagement about, and when is a finding about it complete? | How much does each finding record? | What should the agent check for, file by file? |
| Values | code, network, web, … or your own TOML | full (default) or summary | owasp-top10-2021, cwe-top25-2023, … or your own YAML |
| Chosen by | --engagement-profile, agentiflow engagement_profiles: | agent findingsProfile, workflow / step findings_profile, --findings-profile | the agent’s concerns: block |
| Lives in | built-in, ~/.rupu/profiles/, .rupu/profiles/ | built into rupu | built-in, ~/.rupu/concerns/, .rupu/concerns/ |
| Records | assets.jsonl + completeness-gated findings | the shape of each finding record | file × concern verdicts |
Completeness checks run only on a full report (see Findings); under summary an asset is still routed and recorded, but its checks are skipped. Concern catalogs are lists of things to check (see Coverage). They’re independent of the engagement: a code engagement can use owasp-top10-2021, and so can a run with no engagement. The bundled catalogs (rupu coverage templates list, or show <name> to print one):
| Catalog | Contents |
|---|---|
owasp-top10-2021 | OWASP Top 10 web risks, 2021 (10 concerns) |
owasp-api-top10-2023 | OWASP API Security Top 10, 2023 (10) |
cwe-top25-2023 | 2023 CWE Top 25 most dangerous weaknesses (25) |
cwe-software-development | MITRE CWE Software Development view (~399) |
cwe-research | MITRE CWE Research view (~944) |
stride | STRIDE threat categories (6) |
secrets-in-source | hardcoded secrets in source (1) |
code-smells | Fowler-style code smells (12) |
web-security-default | bundle: owasp-top10-2021 + cwe-top25-2023 + secrets-in-source |
api-security-default | bundle: owasp-api-top10-2023 + cwe-top25-2023 + secrets-in-source |