Skip to content

Commit b2369ea

Browse files
authored
Merge pull request #101 from TacoRocket/closeout-escalation-privesc-contract
Close out escalation and privesc contract cleanup
2 parents 79c3e61 + 64e8d04 commit b2369ea

17 files changed

Lines changed: 846 additions & 95 deletions

File tree

README.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,7 @@ pipx install azurefox
5353

5454
Start with the identity you have, then work outward toward movement and consequence:
5555

56-
```bash
57-
azurefox whoami
58-
azurefox permissions
59-
azurefox privesc
60-
azurefox role-trusts
61-
azurefox cross-tenant
62-
azurefox tokens-credentials
63-
azurefox chains
64-
```
56+
![AzureFox operator workflow demo](docs/media/demo.gif)
6557

6658
Typical flow:
6759
- `whoami`: confirm the current foothold, token context, and subscription scope

docs/media/demo.gif

870 KB
Loading

src/azurefox/chains/presentation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def compute_control_proof_status_label(target_resolution: str) -> str:
5050
def escalation_path_type_label(path_concept: str) -> str:
5151
labels = {
5252
"current-foothold-direct-control": "current foothold direct control",
53+
"app-permission-reach": "app-permission reach",
5354
"trust-expansion": "trust expansion",
5455
}
5556
return labels.get(path_concept, path_concept or "-")

src/azurefox/chains/registry.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,10 @@ class ChainFamilySpec:
319319
"success or multi-hop control without deeper evidence."
320320
),
321321
current_gap=(
322-
"The grouped runner still needs exact trust-to-control transformation data so "
323-
"trust-backed rows can explain how the current foothold could actually become or "
324-
"control the stronger identity instead of re-listing relationship-only leads."
322+
"The live family now joins current-foothold control, trust takeover, federated "
323+
"trust, and app-permission reach when the transform and stronger Azure control are "
324+
"explicit. Broader secret-bearing or support-only starts still sit outside this "
325+
"family, and ranking is still heuristic within the admitted current-foothold paths."
325326
),
326327
best_current_examples=(
327328
"permissions",

src/azurefox/chains/runner.py

Lines changed: 128 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -450,23 +450,20 @@ def _build_escalation_path_output(
450450
permission,
451451
)
452452
)
453-
trust_row = _build_escalation_trust_record(
453+
trust_rows = _build_escalation_trust_records(
454454
family_name,
455455
current_foothold_row,
456456
role_trusts_output.trusts,
457457
permissions_by_principal,
458458
current_foothold_id=current_foothold_id,
459459
current_foothold_permission=permission,
460460
)
461-
if trust_row is not None:
462-
paths.append(trust_row)
461+
paths.extend(trust_rows)
463462

464463
paths.sort(
465-
key=lambda item: (
466-
semantic_priority_sort_value(item.priority),
467-
_JOIN_QUALITY_ORDER.get(item.target_resolution, 9),
468-
item.asset_name,
469-
item.path_concept or "",
464+
key=lambda item: _escalation_path_sort_key(
465+
item,
466+
permissions_by_principal=permissions_by_principal,
470467
)
471468
)
472469

@@ -3119,6 +3116,11 @@ def _build_escalation_direct_control_record(
31193116
if part
31203117
)
31213118
next_review = current_foothold_next_review_hint()
3119+
why_care = (
3120+
f"The current foothold already has {stronger_outcome}. This row is already direct "
3121+
"Azure control, not a separate pivot hunt. AzureFox is not narrowing one exact "
3122+
"downstream action beyond the control already shown here."
3123+
)
31223124

31233125
return ChainPathRecord(
31243126
chain_id=f"escalation-path::{current_foothold.get('principal_id')}::current-foothold-direct-control",
@@ -3139,11 +3141,7 @@ def _build_escalation_direct_control_record(
31393141
insertion_point="Current foothold already holds high-impact RBAC on visible scope.",
31403142
path_concept="current-foothold-direct-control",
31413143
stronger_outcome=stronger_outcome,
3142-
why_care=(
3143-
f"The current foothold already has {stronger_outcome}. This row is already direct "
3144-
"Azure control, not a separate pivot hunt. AzureFox is not narrowing one exact "
3145-
"downstream action beyond the control already shown here."
3146-
),
3144+
why_care=why_care,
31473145
likely_impact=stronger_outcome,
31483146
confidence_boundary=confidence_boundary,
31493147
target_service="azure-control",
@@ -3159,8 +3157,7 @@ def _build_escalation_direct_control_record(
31593157
related_ids=[str(value) for value in current_foothold.get("related_ids") or [] if value],
31603158
)
31613159

3162-
3163-
def _build_escalation_trust_record(
3160+
def _select_escalation_trust_record(
31643161
family_name: str,
31653162
current_foothold: dict,
31663163
trusts: list,
@@ -3169,8 +3166,30 @@ def _build_escalation_trust_record(
31693166
current_foothold_id: str | None,
31703167
current_foothold_permission: dict | None,
31713168
) -> ChainPathRecord | None:
3172-
if not current_foothold_id:
3169+
records = _build_escalation_trust_records(
3170+
family_name,
3171+
current_foothold,
3172+
trusts,
3173+
permissions_by_principal,
3174+
current_foothold_id=current_foothold_id,
3175+
current_foothold_permission=current_foothold_permission,
3176+
)
3177+
if not records:
31733178
return None
3179+
return records[0]
3180+
3181+
3182+
def _build_escalation_trust_records(
3183+
family_name: str,
3184+
current_foothold: dict,
3185+
trusts: list,
3186+
permissions_by_principal: dict[str, dict],
3187+
*,
3188+
current_foothold_id: str | None,
3189+
current_foothold_permission: dict | None,
3190+
) -> list[ChainPathRecord]:
3191+
if not current_foothold_id:
3192+
return []
31743193

31753194
candidates: list[tuple[tuple[int, int, int, str], ChainPathRecord]] = []
31763195
federated_trusts_by_application_id: dict[str, list[dict]] = defaultdict(list)
@@ -3239,11 +3258,21 @@ def _build_escalation_trust_record(
32393258
)
32403259

32413260
if not candidates:
3242-
return None
3261+
return []
32433262

32443263
candidates.sort(key=lambda item: item[0])
3245-
return candidates[0][1]
3246-
3264+
selected: list[ChainPathRecord] = []
3265+
seen_targets: set[tuple[str, tuple[str, ...]]] = set()
3266+
for _, record in candidates:
3267+
dedupe_key = (
3268+
str(record.path_concept or ""),
3269+
tuple(str(value) for value in record.target_ids),
3270+
)
3271+
if dedupe_key in seen_targets:
3272+
continue
3273+
seen_targets.add(dedupe_key)
3274+
selected.append(record)
3275+
return selected
32473276

32483277
def _build_escalation_single_trust_record(
32493278
*,
@@ -3271,6 +3300,7 @@ def _build_escalation_single_trust_record(
32713300
usable_identity_result = str(trust_row.get("usable_identity_result") or "").strip()
32723301
defender_cut_point = str(trust_row.get("defender_cut_point") or "").strip()
32733302
trust_type = str(trust_row.get("trust_type") or "trust-expansion")
3303+
path_concept = _escalation_trust_path_concept(trust_type)
32743304

32753305
if not target_permission or not escalation_mechanism or not usable_identity_result:
32763306
return None
@@ -3287,7 +3317,7 @@ def _build_escalation_single_trust_record(
32873317
target_resolution=target_resolution,
32883318
target_count=1,
32893319
source_command="role-trusts",
3290-
path_concept="trust-expansion",
3320+
path_concept=path_concept,
32913321
)
32923322
)
32933323
confidence_boundary = (
@@ -3307,7 +3337,9 @@ def _build_escalation_single_trust_record(
33073337
why_care = f"{why_care} {defender_cut_point}"
33083338

33093339
return ChainPathRecord(
3310-
chain_id=f"escalation-path::{current_foothold_id}::trust-expansion::{target_permission_id}",
3340+
chain_id=(
3341+
f"escalation-path::{current_foothold_id}::{path_concept}::{target_permission_id}"
3342+
),
33113343
asset_id=str(current_foothold_id),
33123344
asset_name=str(
33133345
current_foothold.get("starting_foothold")
@@ -3323,7 +3355,7 @@ def _build_escalation_single_trust_record(
33233355
urgency=semantic.urgency,
33243356
visible_path=_escalation_visible_path(trust_type),
33253357
insertion_point=escalation_mechanism,
3326-
path_concept="trust-expansion",
3358+
path_concept=path_concept,
33273359
stronger_outcome=stronger_outcome,
33283360
why_care=why_care,
33293361
likely_impact=stronger_outcome,
@@ -3354,23 +3386,63 @@ def _escalation_trust_candidate_sort_key(
33543386
record: ChainPathRecord,
33553387
current_foothold_permission: dict | None,
33563388
target_permission: dict | None,
3357-
) -> tuple[int, int, int, int, int, str]:
3358-
path_preference_rank = {
3359-
"service-principal-owner": 0,
3360-
"federated-credential": 1,
3361-
"app-owner": 2,
3362-
"app-to-service-principal": 3,
3363-
}.get(clue_type, 9)
3389+
) -> tuple[int, int, int, int, int, int, str]:
33643390
return (
33653391
semantic_priority_sort_value(record.priority),
33663392
_JOIN_QUALITY_ORDER.get(record.target_resolution, 9),
3393+
-_permission_upgrade_score(current_foothold_permission, target_permission),
33673394
-len(_permission_new_scope_ids(current_foothold_permission, target_permission)),
33683395
-_permission_role_strength(target_permission),
3369-
path_preference_rank,
3396+
_escalation_path_effort_rank(record.path_concept, clue_type=clue_type),
33703397
record.chain_id,
33713398
)
33723399

33733400

3401+
def _escalation_path_sort_key(
3402+
record: ChainPathRecord,
3403+
*,
3404+
permissions_by_principal: dict[str, dict],
3405+
) -> tuple[int, int, int, int, int, str, str]:
3406+
current_foothold_permission = permissions_by_principal.get(record.asset_id)
3407+
target_permission = _escalation_record_target_permission(
3408+
record,
3409+
permissions_by_principal=permissions_by_principal,
3410+
)
3411+
return (
3412+
semantic_priority_sort_value(record.priority),
3413+
-_permission_upgrade_score(current_foothold_permission, target_permission),
3414+
-len(_permission_new_scope_ids(current_foothold_permission, target_permission)),
3415+
_escalation_path_effort_rank(record.path_concept, clue_type=record.clue_type),
3416+
_JOIN_QUALITY_ORDER.get(record.target_resolution, 9),
3417+
record.asset_name,
3418+
record.chain_id,
3419+
)
3420+
3421+
3422+
def _escalation_record_target_permission(
3423+
record: ChainPathRecord,
3424+
*,
3425+
permissions_by_principal: dict[str, dict],
3426+
) -> dict | None:
3427+
for target_id in record.target_ids:
3428+
target_permission = permissions_by_principal.get(str(target_id))
3429+
if target_permission is not None:
3430+
return target_permission
3431+
return None
3432+
3433+
3434+
def _escalation_path_effort_rank(path_concept: str | None, *, clue_type: str) -> int:
3435+
if path_concept == "current-foothold-direct-control":
3436+
return 0
3437+
if path_concept == "app-permission-reach":
3438+
return 1
3439+
return {
3440+
"federated-credential": 2,
3441+
"service-principal-owner": 3,
3442+
"app-owner": 4,
3443+
}.get(clue_type, 9)
3444+
3445+
33743446
def _escalation_target_permission_for_sort(
33753447
*,
33763448
trust_row: dict,
@@ -3396,6 +3468,12 @@ def _escalation_visible_path(trust_type: str) -> str:
33963468
return "Current foothold -> trust edge -> higher-value identity"
33973469

33983470

3471+
def _escalation_trust_path_concept(trust_type: str) -> str:
3472+
if trust_type == "app-to-service-principal":
3473+
return "app-permission-reach"
3474+
return "trust-expansion"
3475+
3476+
33993477
def _build_escalation_federated_takeover_record(
34003478
*,
34013479
family_name: str,
@@ -3542,6 +3620,14 @@ def _escalation_trust_note(
35423620
f"or used authentication material for service principal '{target_name}'."
35433621
)
35443622

3623+
if trust_type == "app-to-service-principal" and target_name:
3624+
return (
3625+
f"The current foothold already has application-permission reach into service "
3626+
f"principal '{target_name}'. {gain_text} AzureFox is not proving that the current "
3627+
f"foothold has already exercised one exact downstream action through "
3628+
f"'{target_name}'."
3629+
)
3630+
35453631
return (
35463632
f"The current foothold can reach '{target_permission_name}'. {gain_text} AzureFox is "
35473633
"not proving that the current foothold has already turned this trust edge into usable "
@@ -3607,9 +3693,18 @@ def _permission_adds_net_value(
36073693
return True
36083694
if _permission_new_scope_ids(current_permission, target_permission):
36093695
return True
3610-
return _permission_role_strength(target_permission) > _permission_role_strength(
3611-
current_permission
3612-
)
3696+
return _permission_upgrade_score(current_permission, target_permission) > 0
3697+
3698+
3699+
def _permission_upgrade_score(
3700+
current_permission: dict | None,
3701+
target_permission: dict | None,
3702+
) -> int:
3703+
if not target_permission:
3704+
return 0
3705+
current_strength = _permission_role_strength(current_permission)
3706+
target_strength = _permission_role_strength(target_permission)
3707+
return max(0, target_strength - current_strength)
36133708

36143709

36153710
def _permission_new_scope_ids(
@@ -3637,7 +3732,6 @@ def _permission_new_scope_ids(
36373732
def _permission_role_strength(permission_row: dict | None) -> int:
36383733
ranks = {
36393734
"contributor": 1,
3640-
"user access administrator": 2,
36413735
"owner": 3,
36423736
}
36433737
roles = (permission_row or {}).get("high_impact_roles") or []
@@ -3654,8 +3748,6 @@ def _permission_capability_text(permission_row: dict | None) -> str:
36543748
}
36553749
if "owner" in roles:
36563750
return "Owner-level Azure control, including role assignment"
3657-
if "user access administrator" in roles:
3658-
return "role-assignment control"
36593751
if "contributor" in roles:
36603752
return "write/change control"
36613753
return "meaningful Azure control"
@@ -3722,6 +3814,5 @@ def _scope_label(scope_id: str) -> str:
37223814
return f"resource '{name}'"
37233815
return "visible scope"
37243816

3725-
37263817
def _source_chain_id(family_name: str, asset_id: str, target_service: str) -> str:
37273818
return f"{family_name}::{asset_id}::{target_service}"

src/azurefox/chains/semantics.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,25 @@ def _escalation_path_semantics(context: ChainSemanticContext) -> ChainSemanticDe
293293
next_review="Check rbac for the exact assignment evidence behind the current foothold.",
294294
)
295295

296+
if context.path_concept == "app-permission-reach":
297+
if context.target_resolution == "path-confirmed":
298+
return ChainSemanticDecision(
299+
priority="medium",
300+
urgency="review-soon",
301+
next_review=(
302+
"Review the exact application-permission grant and the stronger target "
303+
"behind this path."
304+
),
305+
)
306+
return ChainSemanticDecision(
307+
priority="low",
308+
urgency="bookmark",
309+
next_review=(
310+
"Confirm whether this application-permission target adds meaningful Azure "
311+
"control beyond the current foothold."
312+
),
313+
)
314+
296315
if context.path_concept == "trust-expansion":
297316
if context.target_resolution == "path-confirmed":
298317
return ChainSemanticDecision(

0 commit comments

Comments
 (0)