Skip to content
Open
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
34 changes: 33 additions & 1 deletion skills/cloud/azure-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ phase: [assess, operate]
frameworks: [CIS-Azure-v2.1.0]
difficulty: intermediate
time_estimate: "60-90min"
version: "1.0.0"
version: "1.0.1"
author: unitoneai
license: MIT
allowed-tools: Read, Grep, Glob
Expand Down Expand Up @@ -88,6 +88,28 @@ For detailed CIS benchmark checklist items with specific Terraform patterns, Bic

---

### Step 10.1: App Service Deployment-Plane Basic Authentication Evidence

For App Service, Function Apps, and deployment slots, verify the deployment plane separately from runtime authentication. Passing `auth_settings_v2`, `https_only`, TLS, client certificates, HTTP/2, or `ftps_state = "Disabled"` does not prove that Kudu/SCM, WebDeploy, ZipDeploy, Local Git, or FTP publishing credentials are blocked.

Apply these gates before marking App Service publishing posture as compliant:

- **AZ-APP-PUBLISH-01 -- Resource inventory:** enumerate every `Microsoft.Web/sites` and `Microsoft.Web/sites/slots` resource, including Linux Web Apps, Windows Web Apps, Function Apps, and deployment slots.
- **AZ-APP-PUBLISH-02 -- SCM policy evidence:** require `Microsoft.Web/sites/basicPublishingCredentialsPolicies/scm` or slot equivalent with `properties.allow = false`; Terraform evidence can include `webdeploy_publish_basic_authentication_enabled = false`.
- **AZ-APP-PUBLISH-03 -- FTP policy evidence:** require `Microsoft.Web/sites/basicPublishingCredentialsPolicies/ftp` or slot equivalent with `properties.allow = false`; Terraform evidence can include `ftp_publish_basic_authentication_enabled = false`.
- **AZ-APP-PUBLISH-04 -- Slot parity:** verify every deployment slot has the same SCM and FTP basic publishing credential policy as production.
- **AZ-APP-PUBLISH-05 -- Non-basic deployment replacement:** record the approved deployment method, such as Microsoft Entra ID, managed identity, federated service principal, OIDC-backed GitHub Actions, or Azure Pipelines service connection.
- **AZ-APP-PUBLISH-06 -- Credential invalidation:** after disabling publishing basic auth, require evidence that publish profiles, user-scope deployment credentials, stored service connections, and automation secrets were rotated, removed, or invalidated where they were previously exposed.
- **AZ-APP-PUBLISH-07 -- Policy and audit coverage:** verify Azure Policy or role controls prevent re-enabling basic publishing credentials, and that `AppServiceAuditLogs` or equivalent telemetry captures FTP/WebDeploy authorization attempts.
- **AZ-APP-PUBLISH-08 -- Unsupported evidence:** do not credit runtime-only controls, comments, ticket due dates, `ftps_state` alone, or successful Entra deployment as proof that SCM/FTP basic publishing credentials are disabled.

Use this supplemental output table for App Service publishing evidence:

| App/Slot | SCM Basic Auth | FTP Basic Auth | Evidence Source | Deployment Method | Credential Rotation | Audit/Policy Evidence | Status |
|----------|----------------|----------------|-----------------|-------------------|---------------------|-----------------------|--------|
| `<name>` | Disabled / Enabled / Not Evaluable | Disabled / Enabled / Not Evaluable | Terraform / ARM / Azure CLI / Policy export | Entra / OIDC / Managed Identity / Basic / Unknown | Rotated / Missing / Not Applicable | Present / Missing / Not Evaluable | Pass / Fail / Not Evaluable |

Treat enabled SCM or FTP basic publishing credentials on production apps or slots as **High** severity when deployment credentials or publish profiles are broadly accessible, and **Medium** when exposure is limited but policy/audit coverage is missing. Use **Not Evaluable** when the app inventory exists but the `basicPublishingCredentialsPolicies` export or equivalent Terraform fields are absent.

---

Expand Down Expand Up @@ -142,6 +164,12 @@ Produce the final report using the structure defined in the Output Format sectio
| 8 | Key Vault | X | Y | Z | nn% |
| 9 | App Service | X | Y | Z | nn% |

### Supplemental App Service Publishing Evidence

| App/Slot | SCM Basic Auth | FTP Basic Auth | Evidence Source | Deployment Method | Credential Rotation | Audit/Policy Evidence | Status |
|----------|----------------|----------------|-----------------|-------------------|---------------------|-----------------------|--------|
| <name> | Disabled / Enabled / Not Evaluable | Disabled / Enabled / Not Evaluable | <source> | <method> | <status> | <status> | Pass / Fail / Not Evaluable |

### Detailed Findings

#### [CIS X.Y.Z] <Recommendation Title>
Expand Down Expand Up @@ -200,6 +228,7 @@ Produce the final report using the structure defined in the Output Format sectio
4. **NSG rules using service tags.** A rule with `source_address_prefix = "Internet"` is equivalent to `0.0.0.0/0`. Both must be flagged for CIS 6.1 and 6.2.
5. **Key Vault purge protection is irreversible.** CIS 8.5 requires `purge_protection_enabled = true`. Note this cannot be disabled once enabled -- flag this for awareness during remediation.
6. **App Service TLS version on both Linux and Windows.** Check `azurerm_linux_web_app` and `azurerm_windows_web_app` resources separately.
7. **Runtime App Service controls are not publishing-plane controls.** `auth_settings_v2`, `https_only`, client certificates, and `ftps_state = "Disabled"` do not disable SCM/Kudu/WebDeploy/ZipDeploy basic authentication. Require `basicPublishingCredentialsPolicies/scm` and `basicPublishingCredentialsPolicies/ftp` evidence or equivalent Terraform fields.

---

Expand All @@ -225,10 +254,13 @@ Produce the final report using the structure defined in the Output Format sectio
- Azure Storage Security: https://learn.microsoft.com/en-us/azure/storage/common/storage-security-guide
- Azure Key Vault Best Practices: https://learn.microsoft.com/en-us/azure/key-vault/general/best-practices
- Azure App Service Security: https://learn.microsoft.com/en-us/azure/app-service/overview-security
- Disable basic authentication in Azure App Service deployments: https://learn.microsoft.com/en-us/azure/app-service/configure-basic-auth-disable
- Authentication types by deployment methods in Azure App Service: https://learn.microsoft.com/en-us/azure/app-service/deploy-authentication-types
- Terraform AzureRM Provider Documentation: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs

---

## Changelog

- **1.0.1** -- Adds App Service SCM/FTP publishing basic authentication evidence gates, output fields, severity guidance, and validation expectations.
- **1.0.0** -- Initial release. Full coverage of CIS Microsoft Azure Foundations Benchmark v2.1.0 sections 1 through 9.
104 changes: 104 additions & 0 deletions skills/cloud/azure-review/benchmark-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,107 @@ resource "azurerm_linux_web_app" {
}
}
```

### Supplemental -- App Service Deployment-Plane Basic Authentication

Review SCM/Kudu/WebDeploy/ZipDeploy and FTP publishing basic authentication separately from runtime App Service settings. `ftps_state = "Disabled"` blocks FTP protocol use, but it is not proof that the FTP or SCM publishing credential policies are disabled across apps and slots.

Required evidence:

- App and slot inventory for `Microsoft.Web/sites` and `Microsoft.Web/sites/slots`.
- SCM policy export showing `basicPublishingCredentialsPolicies/scm` with `properties.allow = false`.
- FTP policy export showing `basicPublishingCredentialsPolicies/ftp` with `properties.allow = false`.
- Terraform fields such as `webdeploy_publish_basic_authentication_enabled = false` and `ftp_publish_basic_authentication_enabled = false` where the AzureRM provider manages the app.
- Non-basic deployment method evidence, such as Microsoft Entra ID, OIDC-backed GitHub Actions, managed identity, or an Azure Pipelines service connection.
- Publish-profile and deployment-credential rotation/invalidation evidence if basic auth was previously enabled.
- Azure Policy, custom role, or monitoring evidence preventing or detecting re-enable attempts.
- `AppServiceAuditLogs` or equivalent diagnostic evidence for FTP/WebDeploy authorization attempts.

Terraform AzureRM example:

```hcl
resource "azurerm_linux_web_app" "api" {
name = "app-prod-api"
resource_group_name = azurerm_resource_group.prod.name
location = azurerm_resource_group.prod.location
service_plan_id = azurerm_service_plan.prod.id
https_only = true
ftp_publish_basic_authentication_enabled = false
webdeploy_publish_basic_authentication_enabled = false

site_config {
ftps_state = "Disabled"
minimum_tls_version = "1.2"
}
}
```

ARM/AzAPI child resource evidence:

```hcl
resource "azapi_resource" "api_scm_basic_auth" {
type = "Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01"
name = "scm"
parent_id = azurerm_linux_web_app.api.id
body = {
properties = {
allow = false
}
}
}

resource "azapi_resource" "api_ftp_basic_auth" {
type = "Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01"
name = "ftp"
parent_id = azurerm_linux_web_app.api.id
body = {
properties = {
allow = false
}
}
}
```

Slot parity example:

```hcl
resource "azurerm_linux_web_app_slot" "staging" {
name = "staging"
app_service_id = azurerm_linux_web_app.api.id
ftp_publish_basic_authentication_enabled = false
webdeploy_publish_basic_authentication_enabled = false

site_config {
ftps_state = "Disabled"
}
}
```

Azure CLI evidence commands:

```bash
az resource show \
--resource-group <resource-group> \
--namespace Microsoft.Web \
--resource-type basicPublishingCredentialsPolicies \
--parent sites/<app-name> \
--name scm \
--query properties.allow

az resource show \
--resource-group <resource-group> \
--namespace Microsoft.Web \
--resource-type basicPublishingCredentialsPolicies \
--parent sites/<app-name> \
--name ftp \
--query properties.allow
```

Review checks:

- Fail if either SCM or FTP publishing basic auth is enabled on an in-scope production app, Function App, or deployment slot.
- Fail if the app has runtime authentication enabled but no deployment-plane basic authentication policy evidence.
- Fail if production is disabled but a staging or deployment slot still permits SCM or FTP basic publishing credentials.
- Mark Not Evaluable when the inventory is present but policy export, Terraform field, or equivalent evidence is missing.
- Do not accept comments, planned tickets, or `ftps_state` alone as evidence that basic publishing credentials are disabled.
- Prefer policy-as-code or custom-role evidence that blocks `Microsoft.Web/sites/basicPublishingCredentialsPolicies/write` and `Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies/write` for lower-privileged operators.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Benign: App Service and staging slot disable SCM and FTP publishing basic auth
# while using non-basic deployment and audit/policy evidence.

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.110"
}
azapi = {
source = "Azure/azapi"
version = "~> 1.13"
}
}
}

resource "azurerm_resource_group" "prod" {
name = "rg-app-prod"
location = "eastus"
}

resource "azurerm_service_plan" "prod" {
name = "asp-prod"
resource_group_name = azurerm_resource_group.prod.name
location = azurerm_resource_group.prod.location
os_type = "Linux"
sku_name = "P1v3"
}

resource "azurerm_linux_web_app" "api" {
name = "app-prod-api"
resource_group_name = azurerm_resource_group.prod.name
location = azurerm_resource_group.prod.location
service_plan_id = azurerm_service_plan.prod.id
https_only = true
ftp_publish_basic_authentication_enabled = false
webdeploy_publish_basic_authentication_enabled = false

identity {
type = "SystemAssigned"
}

site_config {
ftps_state = "Disabled"
minimum_tls_version = "1.2"
http2_enabled = true
}

app_settings = {
DEPLOYMENT_AUTH_MODE = "oidc-federated-service-principal"
PUBLISH_PROFILE_ROTATED_AT = "2026-06-01T00:00:00Z"
}
}

resource "azapi_resource" "api_scm_basic_auth" {
type = "Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01"
name = "scm"
parent_id = azurerm_linux_web_app.api.id

body = {
properties = {
allow = false
}
}
}

resource "azapi_resource" "api_ftp_basic_auth" {
type = "Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01"
name = "ftp"
parent_id = azurerm_linux_web_app.api.id

body = {
properties = {
allow = false
}
}
}

resource "azurerm_linux_web_app_slot" "staging" {
name = "staging"
app_service_id = azurerm_linux_web_app.api.id
ftp_publish_basic_authentication_enabled = false
webdeploy_publish_basic_authentication_enabled = false

site_config {
ftps_state = "Disabled"
}
}

resource "azapi_resource" "slot_scm_basic_auth" {
type = "Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01"
name = "scm"
parent_id = azurerm_linux_web_app_slot.staging.id

body = {
properties = {
allow = false
}
}
}

resource "azapi_resource" "slot_ftp_basic_auth" {
type = "Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies@2022-09-01"
name = "ftp"
parent_id = azurerm_linux_web_app_slot.staging.id

body = {
properties = {
allow = false
}
}
}

resource "azurerm_monitor_diagnostic_setting" "appservice_audit" {
name = "send-appservice-audit-logs"
target_resource_id = azurerm_linux_web_app.api.id
log_analytics_workspace_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sec/providers/Microsoft.OperationalInsights/workspaces/law-sec"

enabled_log {
category = "AppServiceAuditLogs"
}
}

resource "azurerm_role_definition" "prevent_basic_auth_reenable" {
name = "Prevent App Service Basic Publishing Auth"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
description = "Prevents lower-privileged operators from enabling App Service SCM or FTP basic publishing credentials."

permissions {
actions = ["*"]
not_actions = [
"Microsoft.Web/sites/basicPublishingCredentialsPolicies/write",
"Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies/write",
]
}

assignable_scopes = ["/subscriptions/00000000-0000-0000-0000-000000000000"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Vulnerable: runtime App Service hardening is enabled, but deployment-plane SCM and
# FTP publishing basic auth remain enabled or unproven for production and slot.

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.110"
}
}
}

resource "azurerm_resource_group" "prod" {
name = "rg-app-prod"
location = "eastus"
}

resource "azurerm_service_plan" "prod" {
name = "asp-prod"
resource_group_name = azurerm_resource_group.prod.name
location = azurerm_resource_group.prod.location
os_type = "Linux"
sku_name = "P1v3"
}

resource "azurerm_linux_web_app" "api" {
name = "app-prod-api"
resource_group_name = azurerm_resource_group.prod.name
location = azurerm_resource_group.prod.location
service_plan_id = azurerm_service_plan.prod.id
https_only = true

# Runtime authentication does not protect Kudu/SCM, WebDeploy, ZipDeploy,
# Local Git, or FTP publishing endpoints.
auth_settings_v2 {
auth_enabled = true
}

# FTP protocol is disabled, but the publishing credential policies are still
# enabled. This must fail AZ-APP-PUBLISH-02 and AZ-APP-PUBLISH-03.
ftp_publish_basic_authentication_enabled = true
webdeploy_publish_basic_authentication_enabled = true

site_config {
ftps_state = "Disabled"
minimum_tls_version = "1.2"
http2_enabled = true
}

app_settings = {
DEPLOYMENT_AUTH_MODE = "publish-profile-basic-auth"
PUBLISH_PROFILE_ROTATION = "planned-next-quarter"
APPSERVICE_AUDIT_LOG_STATUS = "not-enabled"
}
}

resource "azurerm_linux_web_app_slot" "staging" {
name = "staging"
app_service_id = azurerm_linux_web_app.api.id

# Slot parity gap: staging still accepts SCM publishing basic auth and has no
# child basicPublishingCredentialsPolicies export proving both policies off.
webdeploy_publish_basic_authentication_enabled = true

site_config {
ftps_state = "Disabled"
}
}

resource "azurerm_source_control_token" "legacy_github" {
type = "GitHub"
token = var.legacy_publish_profile_token
}

variable "legacy_publish_profile_token" {
type = string
sensitive = true
}