Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/azurefox/chains/compute_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,11 @@ def _compute_control_required_foothold(surface_row: dict, workload_row: dict) ->
if asset_kind == "ContainerInstance"
else "this public-facing service"
)
public_token_request_label = (
"make this public-facing container group ask Azure for its own token"
if asset_kind == "ContainerInstance"
else "make this public-facing service ask Azure for its own token"
)
internal_compute_label = (
"this container group" if asset_kind == "ContainerInstance" else "this workload"
)
Expand All @@ -849,8 +854,9 @@ def _compute_control_required_foothold(surface_row: dict, workload_row: dict) ->
if public_signal:
return (
"To turn this into downstream Azure access, an operator would need "
f"server-side execution in {public_compute_label}. AzureFox is a recon tool "
"and does not verify exploitation activity beyond what is explicitly stated here."
f"a way to {public_token_request_label}. AzureFox shows that "
f"{public_compute_label} is public and token-capable, but public reachability "
"alone does not prove that path."
)
return (
"To turn this into downstream Azure access, an operator would need a service-side "
Expand All @@ -863,9 +869,9 @@ def _compute_control_required_foothold(surface_row: dict, workload_row: dict) ->
if public_signal:
return (
"To turn this into downstream Azure access, an operator would need a "
"server-side request path from this public-facing workload to the Azure VM "
"metadata service. AzureFox is a recon tool and does not verify exploitation "
"activity beyond what is explicitly stated here."
"way to make this public-facing workload reach the Azure VM metadata service. "
"AzureFox shows that the workload is public and IMDS-backed, but public "
"reachability alone does not prove that path."
)
return (
f"To turn this into downstream Azure access, an operator would need host-level "
Expand Down
115 changes: 115 additions & 0 deletions src/azurefox/chains/presentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from __future__ import annotations


def compute_control_when_label(urgency: str) -> str:
labels = {
"pivot-now": "act now",
"review-soon": "review soon",
"bookmark": "keep in view",
}
return labels.get(urgency, urgency or "-")


def compute_control_token_path_label(insertion_point: str) -> str:
labels = {
"reachable service token request path": "service token request",
"public IMDS token path": "public VM metadata token",
"IMDS token path": "VM metadata token",
}
return labels.get(insertion_point, insertion_point or "-")


def compute_control_reach_from_here_label(insertion_point: str) -> str:
if insertion_point in {"reachable service token request path", "public IMDS token path"}:
return "public exposure visible; exploitation not proved"
return "current access does not show the start"


def compute_control_identity_label(target_names: list[object]) -> str:
names = [str(value) for value in target_names if str(value).strip()]
if not names:
return "not visible"
if len(names) == 1:
return names[0]
return "multiple possible: " + ", ".join(names)


def compute_control_proof_status_label(target_resolution: str) -> str:
labels = {
"path-confirmed": "confirmed",
"identity-choice-corroborated": "best current match",
"narrowed candidates": "multiple identities possible",
"visibility blocked": "limited visibility",
"tenant-wide candidates": "broad match only",
"service hint only": "early signal only",
"named target not visible": "named identity not visible",
}
return labels.get(target_resolution, "bounded")


def escalation_path_type_label(path_concept: str) -> str:
labels = {
"current-foothold-direct-control": "current foothold direct control",
"trust-expansion": "trust expansion",
}
return labels.get(path_concept, path_concept or "-")


def normalize_chain_payload_for_output(command: str, payload: dict) -> dict:
if command != "chains":
return payload
family = str(payload.get("family") or "")
if family not in {"compute-control", "escalation-path"}:
return payload
paths = payload.get("paths")
if not isinstance(paths, list):
return payload

normalized_payload = dict(payload)
normalized_payload["paths"] = [
normalize_chain_path_row(family, row) if isinstance(row, dict) else row for row in paths
]
return normalized_payload


def normalize_chain_path_row(family: str, row: dict) -> dict:
normalized_row = dict(row)
if family == "escalation-path":
normalized_row["starting_foothold"] = str(
row.get("starting_foothold") or row.get("asset_name") or ""
)
normalized_row["path_type"] = str(
row.get("path_type")
or escalation_path_type_label(str(row.get("path_concept") or ""))
)
normalized_row["note"] = str(row.get("why_care") or row.get("note") or "")
return normalized_row

insertion_point = str(row.get("insertion_point") or "")
urgency = str(row.get("urgency") or "")
target_resolution = str(row.get("target_resolution") or "")
target_names = row.get("target_names") or []

normalized_row["when"] = str(row.get("when") or compute_control_when_label(urgency))
normalized_row["reach_from_here"] = str(
row.get("reach_from_here")
or compute_control_reach_from_here_label(insertion_point)
)
normalized_row["compute_foothold"] = str(
row.get("compute_foothold") or row.get("asset_name") or ""
)
normalized_row["token_path"] = str(
row.get("token_path") or compute_control_token_path_label(insertion_point)
)
normalized_row["identity"] = str(
row.get("identity") or compute_control_identity_label(target_names)
)
normalized_row["azure_access"] = str(
row.get("azure_access") or row.get("stronger_outcome") or row.get("likely_impact") or ""
)
normalized_row["proof_status"] = str(
row.get("proof_status")
or compute_control_proof_status_label(target_resolution)
)
normalized_row["note"] = str(row.get("why_care") or row.get("note") or "")
return normalized_row
26 changes: 6 additions & 20 deletions src/azurefox/chains/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,39 +324,25 @@ class ChainFamilySpec:
"control the stronger identity instead of re-listing relationship-only leads."
),
best_current_examples=(
"privesc -> permissions",
"privesc -> role-trusts -> permissions",
"permissions",
"permissions -> role-trusts",
),
source_commands=(
ChainSourceSpec(
command="privesc",
minimum_fields=(
"starting_foothold",
"principal_id",
"path_type",
"current_identity",
"proven_path",
"missing_proof",
"next_review",
),
rationale=(
"Provides the current-foothold escalation triage rows that the chain family "
"can harden into a defended path story."
),
),
ChainSourceSpec(
command="permissions",
minimum_fields=(
"principal_id",
"display_name",
"priority",
"high_impact_roles",
"scope_count",
"scope_ids",
"privileged",
"is_current_identity",
),
rationale=(
"Provides the visible Azure control power behind the current foothold or "
"linked identity."
"Provides the direct current-identity and visible Azure-control evidence that "
"anchors the chain family's starting foothold."
),
),
ChainSourceSpec(
Expand Down
Loading
Loading