-
Notifications
You must be signed in to change notification settings - Fork 2
OpenGraph Add Your Own Edges
Use this page to add your own privilege-escalation / dangerous IAM edges to OCInferno OpenGraph in JSON, without writing Python. OCInferno only graphs IAM edges that lead to privilege escalation or are otherwise dangerous (see Default Priv Escalation Mode); this page is how you extend that set.
- Files You Edit
- How Matching Works
- Single-Permission Edge
- Multi-Permission Edge (Same Resource Token)
- Multi-Permission Bundle (Cross-Token AND)
- Field Reference
- Validation Workflow
| File | Purpose |
|---|---|
modules/opengraph/utilities/helpers/data/static_constants.json |
Rule definitions — the ALLOW_RULE_DEFS array. This is the single source of truth for default IAM edges. |
modules/opengraph/utilities/helpers/data/permission_mapping.json |
Verb→permission map (inspect/read/use/manage → service → resource-token → permission tokens). A permission you reference should be resolvable here so it is surfaced. |
Normalization/runtime mapping lives in modules/opengraph/utilities/helpers/constants.py
(_normalize_allow_rule_def, DEFAULT_ALLOW_EDGE_RULES). You do not edit Python to add a rule.
There are two matching layers:
-
Per-statement (single rule). Each
ALLOW_RULE_DEFSrule with amatchblock is tested against every allow statement: subject type + resource token (family-aware) + verbs + permissions. A match emits the rule's edge from the policy statement to a destination scope node. Rows are OR'd (any matching row emits its edge). -
Cross-token bundle (AND). A rule with a
bundleblock fires only when a principal holds all of several(permissions, resource_tokens)requirements at the same location. This is how you express multi-permission routes (e.g. create-instance'sINSTANCE_CREATE+VNIC_ATTACH+SUBNET_READtogether) purely in JSON.
Only add edges that represent privilege escalation or a dangerous capability — that is the project's default-mode contract. Redundant/benign edges are intentionally not graphed.
One permission is enough to emit the edge. Add an object to ALLOW_RULE_DEFS:
{
"id": "READ_FUNCTION_CONFIG",
"principal_group_key": "principals-non-service",
"match": {
"permissions_all": ["FN_FUNCTION_READ"],
"resource_tokens": ["fn-function"]
},
"destination": {
"token": "fn-function",
"node_type": "OCIResourceGroup",
"allow_specific": true
},
"edge": {
"label": "OCI_READ_FUNCTION_CONFIG",
"description": "Read Functions -> get each Application/Function config map (environment variables), which frequently holds API keys, DB passwords, and tokens.",
"status": "ACTIVE"
}
}This draws OCIPolicyStatement -> OCIResourceGroup(fn-function@<loc>) labeled
OCI_READ_FUNCTION_CONFIG for any principal granted FN_FUNCTION_READ there.
Note this specific example is intentionally not shipped by default: OCI_DUMP_SECRETS
is a deliberate exception to the "must represent PE" contract above because Vault/Secrets
Manager is purpose-built to hold credentials (reading a secret bundle carries a strong
prior that the payload is exploitable). A function's config map or an instance's metadata
are general-purpose fields that might contain secrets but were never designed to be a
secret store, so they're a materially weaker signal and don't meet the default-mode bar --
this JSON shape is shown only to illustrate the single-permission rule format, not as a
recommended addition.
When several permissions on the same resource token are required, list them in one
permissions_all (AND within the row):
{
"id": "CREATE_USER_API_KEY",
"match": { "permissions_all": ["USER_UPDATE", "USER_APIKEY_ADD"], "resource_tokens": ["users"] },
"destination": { "token": "users", "node_type": "OCIResourceGroup", "allow_specific": true },
"edge": { "label": "OCI_CREATE_USER_API_KEY", "description": "Create an API key for another user (API access as that user)." }
}Use permissions_any (OR) / verbs_all / verbs_any in match for other combinations.
When the permissions span different resource tokens and must all hold together, use a
bundle — no Python needed. The edge fires only when every requires_all requirement
is satisfied at the same location (each requirement = all permissions_all on at least one
of its resource_tokens):
{
"id": "CREATE_INSTANCE_BUNDLE",
"edge": { "label": "OCI_CAN_LAUNCH_INSTANCE", "description": "Launch a VM with the full prerequisite set, enabling instance-principal pivot." },
"destination": { "token": "new-compute-instance", "node_type": "OCIResourceGroup" },
"bundle": {
"requires_all": [
{ "permissions_all": ["INSTANCE_CREATE"], "resource_tokens": ["instances"] },
{ "permissions_all": ["VNIC_CREATE", "VNIC_ATTACH"], "resource_tokens": ["vnics"] },
{ "permissions_all": ["SUBNET_READ", "SUBNET_ATTACH"], "resource_tokens": ["subnets"] }
]
}
}Notes:
- Each bundle permission must be a graphed permission — it should also have a
single-permission rule (or be a known capability) so it is surfaced to the bundle
evaluator. The read examples above make
FN_FUNCTION_READ/INSTANCE_READusable in bundles. - A rule may carry
match(single/same-token) orbundle(cross-token). With nobundlerules present, the bundle evaluator is a no-op and the graph is unchanged.
| Field | Meaning |
|---|---|
id |
Unique rule slug. |
principal_group_key |
Which subject kinds the rule applies to (default principals-non-service). |
match.permissions_all / permissions_any
|
Permission tokens required (AND) / any-of (OR). |
match.verbs_all / verbs_any
|
Verb constraints (inspect/read/use/manage). |
match.resource_tokens |
Resource token(s) the statement must target (family-aware). |
destination.token |
Scope-node label; node id becomes <token>@<location>. |
destination.node_type |
Usually OCIResourceGroup. |
destination.allow_specific |
true expands <scope>@<loc> -> <specific_resource_ocid> when resources exist. |
edge.label |
The edge kind (keep unique + consistent with the playbook). |
edge.description |
What the edge means (shown in graph properties). |
bundle.requires_all[] |
Cross-token AND requirements; each {permissions_all, resource_tokens}. |
# 1) Add/adjust your rule in static_constants.json (ALLOW_RULE_DEFS).
# 2) Add a scenario under tests/integration/opengraph_golden_iam/scenarios/... and regen:
OCINFERNO_REGEN_GOLDEN=1 python -m pytest -q tests/integration/opengraph_golden_iam -k "<your_scenario>"
# 3) Regenerate any existing goldens that legitimately gained your new edge, then run green:
python -m pytest -q tests/integration/opengraph_golden_iam
ruff check .Adding a new default edge can legitimately change existing goldens (e.g. a manage
grant now records your read edge under includes_other_edges). Review the diff to confirm
it is purely additive, then commit the regenerated goldens.
Home | Getting Started | Auth | Workspace | Orchestration Modules | Downloads to Disk | Data View/Export | Operator Runbook | Troubleshooting and FAQ | OpenGraph - Node/Edge Tables | OpenGraph - Default Priv Escalation Mode | OpenGraph - IAM Conditionals | OpenGraph - Inheritance & IncludeAll | ConfigChecker - Static Config Checks | Module Development Guide
- Authentication Reference
- Workspace Instructions
- Orchestration Module Reference
- Enumeration Module Reference
- Exploit Module Reference
- Downloads to Disk
- Data View/Export
- Troubleshooting and FAQ