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
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ azurefox permissions

| Section | Commands |
| --- | --- |
| `core` | `inventory` |
| `identity` | `whoami`, `rbac`, `principals`, `permissions`, `privesc`, `role-trusts`, `lighthouse`, `auth-policies`, `managed-identities` |
| `config` | `arm-deployments`, `env-vars` |
| `secrets` | `keyvault`, `tokens-credentials` |
| `resource` | `automation`, `devops`, `acr`, `api-mgmt`, `databases`, `resource-trusts` |
| `storage` | `storage` |
| `network` | `nics`, `dns`, `endpoints`, `network-effective`, `network-ports` |
| `compute` | `workloads`, `app-services`, `functions`, `aks`, `vms`, `vmss`, `snapshots-disks` |
| orchestration | `chains` |
| `core` | [`inventory`](https://github.com/TacoRocket/AzureFox/wiki/Inventory) |
| `identity` | [`whoami`](https://github.com/TacoRocket/AzureFox/wiki/Whoami), [`rbac`](https://github.com/TacoRocket/AzureFox/wiki/RBAC), [`principals`](https://github.com/TacoRocket/AzureFox/wiki/Principals), [`permissions`](https://github.com/TacoRocket/AzureFox/wiki/Permissions), [`privesc`](https://github.com/TacoRocket/AzureFox/wiki/Privesc), [`role-trusts`](https://github.com/TacoRocket/AzureFox/wiki/Role-Trusts), [`lighthouse`](https://github.com/TacoRocket/AzureFox/wiki/Lighthouse), [`auth-policies`](https://github.com/TacoRocket/AzureFox/wiki/Auth-Policies), [`managed-identities`](https://github.com/TacoRocket/AzureFox/wiki/Managed-Identities) |
| `config` | [`arm-deployments`](https://github.com/TacoRocket/AzureFox/wiki/Arm-Deployments), [`env-vars`](https://github.com/TacoRocket/AzureFox/wiki/Env-Vars) |
| `secrets` | [`keyvault`](https://github.com/TacoRocket/AzureFox/wiki/Keyvault), [`tokens-credentials`](https://github.com/TacoRocket/AzureFox/wiki/Tokens-Credentials) |
| `resource` | [`automation`](https://github.com/TacoRocket/AzureFox/wiki/Automation), [`devops`](https://github.com/TacoRocket/AzureFox/wiki/Devops), [`acr`](https://github.com/TacoRocket/AzureFox/wiki/ACR), [`api-mgmt`](https://github.com/TacoRocket/AzureFox/wiki/API-Mgmt), [`databases`](https://github.com/TacoRocket/AzureFox/wiki/Databases), [`resource-trusts`](https://github.com/TacoRocket/AzureFox/wiki/Resource-Trusts) |
| `storage` | [`storage`](https://github.com/TacoRocket/AzureFox/wiki/Storage) |
| `network` | [`nics`](https://github.com/TacoRocket/AzureFox/wiki/Nics), [`dns`](https://github.com/TacoRocket/AzureFox/wiki/DNS), [`endpoints`](https://github.com/TacoRocket/AzureFox/wiki/Endpoints), [`network-effective`](https://github.com/TacoRocket/AzureFox/wiki/Network-Effective), [`network-ports`](https://github.com/TacoRocket/AzureFox/wiki/Network-Ports) |
| `compute` | [`workloads`](https://github.com/TacoRocket/AzureFox/wiki/Workloads), [`app-services`](https://github.com/TacoRocket/AzureFox/wiki/App-Services), [`functions`](https://github.com/TacoRocket/AzureFox/wiki/Functions), [`aks`](https://github.com/TacoRocket/AzureFox/wiki/AKS), [`vms`](https://github.com/TacoRocket/AzureFox/wiki/VMs), [`vmss`](https://github.com/TacoRocket/AzureFox/wiki/VMSS), [`snapshots-disks`](https://github.com/TacoRocket/AzureFox/wiki/Snapshots-Disks) |
| orchestration | [`chains`](https://github.com/TacoRocket/AzureFox/wiki/Chains) |

## Need A Test Lab?

Expand Down
697 changes: 560 additions & 137 deletions src/azurefox/chains/runner.py

Large diffs are not rendered by default.

117 changes: 114 additions & 3 deletions src/azurefox/collectors/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9818,7 +9818,7 @@ def _devops_package_feed_input(


def _devops_access_state_rank(value: object) -> int:
return {"write": 0, "read": 1, "exists-only": 2}.get(str(value or ""), 9)
return {"write": 0, "use": 1, "read": 2, "exists-only": 3}.get(str(value or ""), 9)


def _devops_apply_permission_proof(
Expand Down Expand Up @@ -10100,7 +10100,7 @@ def _devops_apply_secure_file_role_proof(
if best_role == "user":
return _devops_apply_permission_proof(
trusted_input,
access_state="read",
access_state="use",
can_poison=False,
evidence_basis="secure-file-role",
permission_source="azure-devops-library-security-role",
Expand Down Expand Up @@ -10476,7 +10476,112 @@ def _devops_target_clues(definition: dict[str, object]) -> list[str]:
for clue, keywords in patterns.items():
if any(keyword in text for keyword in keywords for text in strings):
clues.append(clue)
return clues
structured_clues = _devops_structured_target_clues(definition, broad_clues=clues)
clues.extend(
clue.split(":", 1)[0].strip() for clue in structured_clues if ":" in clue and clue
)
clues.extend(structured_clues)
return _dedupe_strings(clues)


def _devops_structured_target_clues(
definition: dict[str, object],
*,
broad_clues: list[str],
) -> list[str]:
structured: list[str] = []
lowered_broad_clues = {value.lower() for value in broad_clues}
for path, node in _recursive_nodes(definition):
if not isinstance(node, dict):
continue

app_service_name = _devops_named_target_input(
node,
token_groups=(
("azure", "web", "app", "name"),
("web", "app", "name"),
("app", "service", "name"),
),
)
if not app_service_name and "app service" in lowered_broad_clues:
app_service_name = _devops_named_target_input(
node,
token_groups=(("app", "name"),),
)
if app_service_name:
structured.append(f"App Service: {app_service_name}")

function_name = _devops_named_target_input(
node,
token_groups=(
("azure", "function", "app", "name"),
("function", "app", "name"),
),
)
if not function_name and "functions" in lowered_broad_clues:
function_name = _devops_named_target_input(
node,
token_groups=(("function", "name"),),
)
if function_name:
structured.append(f"Functions: {function_name}")

cluster_name = _devops_named_target_input(
node,
token_groups=(
("aks", "cluster", "name"),
("kubernetes", "cluster", "name"),
),
)
if not cluster_name and "aks/kubernetes" in lowered_broad_clues:
cluster_name = _devops_named_target_input(
node,
token_groups=(("cluster", "name"),),
)
if cluster_name:
structured.append(f"AKS/Kubernetes: {cluster_name}")

node_tokens = {
token for key in node for token in _devops_identifier_tokens(key)
} | set(_devops_path_tokens(path))
if "arm/bicep/terraform" in lowered_broad_clues or _devops_node_has_arm_target_context(
node_tokens
):
deployment_name = _devops_named_target_input(
node,
token_groups=(("deployment", "name"),),
)
if deployment_name:
structured.append(f"ARM/Bicep/Terraform: {deployment_name}")

return structured


def _devops_node_has_arm_target_context(tokens: set[str]) -> bool:
return bool(
"arm" in tokens
or "bicep" in tokens
or "terraform" in tokens
or ("resource" in tokens and "manager" in tokens)
)


def _devops_named_target_input(
node: dict[str, object],
*,
token_groups: tuple[tuple[str, ...], ...],
) -> str | None:
for key, value in node.items():
if not isinstance(value, str):
continue
cleaned_value = value.strip()
if not cleaned_value or _looks_like_expression(cleaned_value):
continue
key_tokens = set(_devops_identifier_tokens(key))
for token_group in token_groups:
if set(token_group).issubset(key_tokens):
return cleaned_value
return None


def _devops_secret_support_types(
Expand Down Expand Up @@ -10758,6 +10863,12 @@ def _devops_injection_clause(
return "current credentials can inject through " + ", ".join(
current_operator_injection_surface_types
)
if primary_trusted_input_access_state == "use":
if primary_trusted_input_type == "secure-file":
return (
f"current credentials can use {trusted_input} in pipeline context, but Azure "
"DevOps evidence here does not prove secure-file administration"
)
if primary_trusted_input_access_state == "read":
if primary_trusted_input_type == "pipeline-artifact":
return (
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/lab_tenant/devops.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@
"source_visibility_state": "visible",
"summary": "Build definition 'deploy-appservice-prod' in project 'prod-platform' exposes an Azure change path. trusted inputs include repository azure-repos:customer-portal@refs/heads/main. execution can start through pr-trigger. current credentials can poison repo-content through repository azure-repos:customer-portal@refs/heads/main. uses Azure-facing service connection(s) prod-appsvc-wif. references variable group(s) prod-appsvc-release. surfaces 3 secret-marked variable name(s). source clues ground likely Azure impact in App Service. Current credentials can poison repo-content through repository azure-repos:customer-portal@refs/heads/main. Check app-services for the named deployment target; review permissions and role-trusts for Azure control.",
"target_clues": [
"App Service"
"App Service",
"App Service: app-public-api"
],
"trigger_join_ids": [
"devops-trigger://contoso/prod-platform/23/pr-trigger",
Expand Down
3 changes: 2 additions & 1 deletion tests/golden/devops.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@
"22222222-2222-2222-2222-222222222222"
],
"target_clues": [
"App Service"
"App Service",
"App Service: app-public-api"
],
"risk_cues": [
"auto-triggered",
Expand Down
Loading
Loading