Skip to content

OpenGraph Add Your Own Edges

WebbinRoot edited this page Sep 1, 2026 · 2 revisions

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.

Table of Contents

Files You Edit

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.

How Matching Works

There are two matching layers:

  1. Per-statement (single rule). Each ALLOW_RULE_DEFS rule with a match block 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).
  2. Cross-token bundle (AND). A rule with a bundle block 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's INSTANCE_CREATE + VNIC_ATTACH + SUBNET_READ together) 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.

Single-Permission Edge

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.

Multi-Permission Edge (Same Resource Token)

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.

Multi-Permission Bundle (Cross-Token AND)

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:

  1. 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_READ usable in bundles.
  2. A rule may carry match (single/same-token) or bundle (cross-token). With no bundle rules present, the bundle evaluator is a no-op and the graph is unchanged.

Field Reference

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}.

Validation Workflow

# 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.

Clone this wiki locally