skyveda 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skyveda might be problematic. Click here for more details.

Files changed (100) hide show
  1. skyveda/__init__.py +3 -0
  2. skyveda/__main__.py +5 -0
  3. skyveda/checks/azure/AZ-ACR-001.yaml +32 -0
  4. skyveda/checks/azure/AZ-ACR-002.yaml +34 -0
  5. skyveda/checks/azure/AZ-ACR-003.yaml +37 -0
  6. skyveda/checks/azure/AZ-AI-001.yaml +33 -0
  7. skyveda/checks/azure/AZ-AKS-002.yaml +32 -0
  8. skyveda/checks/azure/AZ-AKS-003.yaml +29 -0
  9. skyveda/checks/azure/AZ-AKS-004.yaml +27 -0
  10. skyveda/checks/azure/AZ-AKS-005.yaml +27 -0
  11. skyveda/checks/azure/AZ-AKS-006.yaml +31 -0
  12. skyveda/checks/azure/AZ-AKS-007.yaml +30 -0
  13. skyveda/checks/azure/AZ-AKS-008.yaml +33 -0
  14. skyveda/checks/azure/AZ-AKS-009.yaml +31 -0
  15. skyveda/checks/azure/AZ-AKS-010.yaml +35 -0
  16. skyveda/checks/azure/AZ-COSMOS-001.yaml +32 -0
  17. skyveda/checks/azure/AZ-COSMOS-002.yaml +35 -0
  18. skyveda/checks/azure/AZ-COST-001.yaml +32 -0
  19. skyveda/checks/azure/AZ-COST-002.yaml +29 -0
  20. skyveda/checks/azure/AZ-COST-003.yaml +28 -0
  21. skyveda/checks/azure/AZ-COST-004.yaml +29 -0
  22. skyveda/checks/azure/AZ-COST-005.yaml +28 -0
  23. skyveda/checks/azure/AZ-COST-006.yaml +34 -0
  24. skyveda/checks/azure/AZ-COST-007.yaml +49 -0
  25. skyveda/checks/azure/AZ-COST-008.yaml +35 -0
  26. skyveda/checks/azure/AZ-KV-001.yaml +30 -0
  27. skyveda/checks/azure/AZ-KV-002.yaml +32 -0
  28. skyveda/checks/azure/AZ-KV-003.yaml +37 -0
  29. skyveda/checks/azure/AZ-MSG-001.yaml +34 -0
  30. skyveda/checks/azure/AZ-NET-001.yaml +33 -0
  31. skyveda/checks/azure/AZ-NET-002.yaml +25 -0
  32. skyveda/checks/azure/AZ-NET-003.yaml +45 -0
  33. skyveda/checks/azure/AZ-NET-004.yaml +37 -0
  34. skyveda/checks/azure/AZ-NET-005.yaml +43 -0
  35. skyveda/checks/azure/AZ-NET-006.yaml +35 -0
  36. skyveda/checks/azure/AZ-NET-007.yaml +35 -0
  37. skyveda/checks/azure/AZ-NET-008.yaml +30 -0
  38. skyveda/checks/azure/AZ-PGSQL-001.yaml +35 -0
  39. skyveda/checks/azure/AZ-REDIS-001.yaml +31 -0
  40. skyveda/checks/azure/AZ-REDIS-002.yaml +30 -0
  41. skyveda/checks/azure/AZ-REL-001.yaml +27 -0
  42. skyveda/checks/azure/AZ-REL-002.yaml +33 -0
  43. skyveda/checks/azure/AZ-REL-003.yaml +33 -0
  44. skyveda/checks/azure/AZ-SQL-001.yaml +37 -0
  45. skyveda/checks/azure/AZ-SQL-002.yaml +28 -0
  46. skyveda/checks/azure/AZ-SQL-003.yaml +35 -0
  47. skyveda/checks/azure/AZ-SQL-004.yaml +31 -0
  48. skyveda/checks/azure/AZ-STG-001.yaml +29 -0
  49. skyveda/checks/azure/AZ-STG-002.yaml +29 -0
  50. skyveda/checks/azure/AZ-STG-003.yaml +31 -0
  51. skyveda/checks/azure/AZ-STG-004.yaml +33 -0
  52. skyveda/checks/azure/AZ-STG-005.yaml +32 -0
  53. skyveda/checks/azure/AZ-VM-001.yaml +33 -0
  54. skyveda/checks/azure/AZ-VM-002.yaml +37 -0
  55. skyveda/checks/azure/AZ-VM-003.yaml +34 -0
  56. skyveda/checks/azure/AZ-VM-004.yaml +42 -0
  57. skyveda/checks/azure/AZ-WEB-001.yaml +30 -0
  58. skyveda/checks/azure/AZ-WEB-002.yaml +30 -0
  59. skyveda/checks/azure/AZ-WEB-003.yaml +35 -0
  60. skyveda/checks/azure/AZ-WEB-004.yaml +29 -0
  61. skyveda/checks/azure/AZ-WEB-005.yaml +29 -0
  62. skyveda/checks/azure/AZ-WEB-006.yaml +32 -0
  63. skyveda/cli.py +450 -0
  64. skyveda/core/__init__.py +1 -0
  65. skyveda/core/billing.py +163 -0
  66. skyveda/core/checks.py +117 -0
  67. skyveda/core/config.py +83 -0
  68. skyveda/core/engine.py +92 -0
  69. skyveda/core/errors.py +57 -0
  70. skyveda/core/evaluators.py +150 -0
  71. skyveda/core/models.py +327 -0
  72. skyveda/core/offline.py +97 -0
  73. skyveda/core/provider.py +87 -0
  74. skyveda/core/retry.py +72 -0
  75. skyveda/core/scoring.py +52 -0
  76. skyveda/core/waivers.py +130 -0
  77. skyveda/providers/__init__.py +1 -0
  78. skyveda/providers/azure/__init__.py +21 -0
  79. skyveda/providers/azure/arm.py +189 -0
  80. skyveda/providers/azure/auth.py +142 -0
  81. skyveda/providers/azure/collectors.py +117 -0
  82. skyveda/providers/azure/costs.py +234 -0
  83. skyveda/providers/azure/evaluators.py +144 -0
  84. skyveda/providers/azure/provider.py +159 -0
  85. skyveda/providers/azure/resource_graph.py +202 -0
  86. skyveda/py.typed +0 -0
  87. skyveda/report/__init__.py +5 -0
  88. skyveda/report/fonts/DejaVuSans-Bold.ttf +0 -0
  89. skyveda/report/fonts/DejaVuSans.ttf +0 -0
  90. skyveda/report/fonts/LICENSE-DejaVu.txt +187 -0
  91. skyveda/report/html.py +70 -0
  92. skyveda/report/pdf.py +466 -0
  93. skyveda/report/summary.py +175 -0
  94. skyveda/report/templates/report.html.j2 +150 -0
  95. skyveda-0.3.0.dist-info/METADATA +137 -0
  96. skyveda-0.3.0.dist-info/RECORD +100 -0
  97. skyveda-0.3.0.dist-info/WHEEL +4 -0
  98. skyveda-0.3.0.dist-info/entry_points.txt +5 -0
  99. skyveda-0.3.0.dist-info/licenses/LICENSE +202 -0
  100. skyveda-0.3.0.dist-info/licenses/NOTICE +10 -0
skyveda/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Skyveda: read-only cloud platform health scanning."""
2
+
3
+ __version__ = "0.3.0"
skyveda/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow ``python -m skyveda``."""
2
+
3
+ from skyveda.cli import app
4
+
5
+ app()
@@ -0,0 +1,32 @@
1
+ schema_version: 1
2
+ id: AZ-ACR-001
3
+ title: Container registries have the admin user disabled
4
+ provider: azure
5
+ area: identity
6
+ severity: high
7
+ source:
8
+ type: resource_graph
9
+ query: |
10
+ // Azure's default for adminUserEnabled is false, but a missing value is read as enabled so
11
+ // that unknown data FAILS.
12
+ resources
13
+ | where type =~ 'microsoft.containerregistry/registries'
14
+ | extend adminUserEnabled = tobool(properties.adminUserEnabled), skuName = tostring(sku.name)
15
+ | project id, name, resourceGroup, location, skuName, adminUserEnabled,
16
+ compliant = coalesce(adminUserEnabled, true) == false
17
+ why: >-
18
+ The admin user is a single shared username and password with full push and pull rights. It
19
+ cannot be tied to a person, cannot be scoped down, and leaks easily. Entra ID identities and
20
+ role assignments are audited and can be limited to what each user or service needs.
21
+ fix:
22
+ summary: Disable the admin user and use Entra ID identities with AcrPull and AcrPush roles.
23
+ steps:
24
+ - Find what logs in with the admin credentials today (pipelines, App Service, scripts).
25
+ - Give each a managed identity or service principal with the AcrPull or AcrPush role.
26
+ - Disable the admin user.
27
+ cli: az acr update -g <resource-group> -n <registry> --admin-enabled false
28
+ terraform: |
29
+ # azurerm_container_registry
30
+ admin_enabled = false
31
+ references:
32
+ - https://learn.microsoft.com/azure/container-registry/container-registry-authentication#admin-account
@@ -0,0 +1,34 @@
1
+ schema_version: 1
2
+ id: AZ-ACR-002
3
+ title: Container registries do not allow anonymous pull
4
+ provider: azure
5
+ area: security
6
+ severity: high
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // properties.anonymousPullEnabled is false unless turned on (Standard and Premium only), and
12
+ // Basic registries cannot turn it on, so a missing value is read as false. The lab plants the
13
+ // fail case to prove the path.
14
+ resources
15
+ | where type =~ 'microsoft.containerregistry/registries'
16
+ | extend anonymousPullEnabled = coalesce(tobool(properties.anonymousPullEnabled), false),
17
+ sku = tostring(sku.name)
18
+ | project id, name, resourceGroup, location, sku, anonymousPullEnabled,
19
+ compliant = anonymousPullEnabled == false
20
+ why: >-
21
+ With anonymous pull, anyone on the Internet can download every image in the registry without
22
+ signing in. Images often contain source code, internal addresses and sometimes secrets baked in
23
+ at build time.
24
+ fix:
25
+ summary: Turn off anonymous pull and give clients that need images a role such as AcrPull.
26
+ steps:
27
+ - Find the clients that pull without signing in and give them an identity with the AcrPull role.
28
+ - Turn off anonymous pull on the registry.
29
+ cli: az acr update -n <registry> --anonymous-pull-enabled false
30
+ terraform: |
31
+ # azurerm_container_registry
32
+ anonymous_pull_enabled = false
33
+ references:
34
+ - https://learn.microsoft.com/azure/container-registry/anonymous-pull-access
@@ -0,0 +1,37 @@
1
+ schema_version: 1
2
+ id: AZ-ACR-003
3
+ title: Container registries are not open to all networks
4
+ provider: azure
5
+ area: network
6
+ severity: medium
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // Compliant when public network access is Disabled, or the firewall default action is Deny
12
+ // (both need the Premium SKU). A missing publicNetworkAccess is read as Enabled and a missing
13
+ // defaultAction as Allow: both are the Azure defaults, so unknown data fails.
14
+ resources
15
+ | where type =~ 'microsoft.containerregistry/registries'
16
+ | extend sku = tostring(sku.name),
17
+ publicNetworkAccess = coalesce(tostring(properties.publicNetworkAccess), 'Enabled'),
18
+ defaultAction = coalesce(tostring(properties.networkRuleSet.defaultAction), 'Allow')
19
+ | project id, name, resourceGroup, location, sku, publicNetworkAccess, defaultAction,
20
+ compliant = publicNetworkAccess =~ 'Disabled' or defaultAction =~ 'Deny'
21
+ why: >-
22
+ A registry that accepts connections from any network can be pushed to or pulled from anywhere
23
+ on the Internet with a leaked credential. A poisoned image pushed this way runs in every
24
+ cluster and app that pulls it.
25
+ fix:
26
+ summary: Use private endpoints, or limit the registry firewall to known networks (Premium SKU).
27
+ steps:
28
+ - Upgrade the registry to Premium if it is Basic or Standard.
29
+ - Add a private endpoint, or firewall rules for the build agents and clusters that use the registry.
30
+ - Set public network access to Disabled, or the default action to Deny.
31
+ cli: az acr update -n <registry> --public-network-enabled false # Premium registries only
32
+ terraform: |
33
+ # azurerm_container_registry (sku = "Premium")
34
+ public_network_access_enabled = false
35
+ references:
36
+ - https://learn.microsoft.com/azure/container-registry/container-registry-access-selected-networks
37
+ - https://learn.microsoft.com/azure/container-registry/container-registry-private-link
@@ -0,0 +1,33 @@
1
+ schema_version: 1
2
+ id: AZ-AI-001
3
+ title: Azure AI services accounts disable key-based authentication
4
+ provider: azure
5
+ area: identity
6
+ severity: medium
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // Covers Azure OpenAI and the other Azure AI services (type microsoft.cognitiveservices/accounts).
12
+ // properties.disableLocalAuth defaults to false (keys allowed). A missing value FAILS.
13
+ resources
14
+ | where type =~ 'microsoft.cognitiveservices/accounts'
15
+ | extend disableLocalAuth = tobool(properties.disableLocalAuth), serviceKind = tostring(kind)
16
+ | project id, name, resourceGroup, location, serviceKind, disableLocalAuth,
17
+ compliant = coalesce(disableLocalAuth, false)
18
+ why: >-
19
+ An API key lets anyone who holds it call the service on your bill and, for Azure OpenAI, send
20
+ it your data and prompts. Keys leak through code and config files and do not say who used them.
21
+ Entra ID access is scoped, audited and removed when no longer needed.
22
+ fix:
23
+ summary: Move applications to Entra ID authentication, then disable key-based access.
24
+ steps:
25
+ - Give the account a custom subdomain if it has none (Entra ID authentication needs one).
26
+ - Grant each application a role such as Cognitive Services User and switch it to its managed identity.
27
+ - Disable local (key-based) authentication on the account.
28
+ cli: az resource update --ids <account-id> --set properties.disableLocalAuth=true
29
+ terraform: |
30
+ # azurerm_cognitive_account
31
+ local_auth_enabled = false
32
+ references:
33
+ - https://learn.microsoft.com/azure/ai-services/disable-local-auth
@@ -0,0 +1,32 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-002
3
+ title: AKS clusters use Entra ID only, with local accounts disabled
4
+ provider: azure
5
+ area: kubernetes
6
+ severity: high
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ resources
12
+ | where type =~ 'microsoft.containerservice/managedclusters'
13
+ | extend disableLocalAccounts = tobool(properties.disableLocalAccounts),
14
+ entraManaged = tobool(properties.aadProfile.managed)
15
+ | project id, name, resourceGroup, location, disableLocalAccounts, entraManaged,
16
+ compliant = coalesce(disableLocalAccounts, false) and coalesce(entraManaged, false)
17
+ why: >-
18
+ Local Kubernetes accounts use static credentials that bypass Entra ID sign-in, conditional
19
+ access and audit. Anyone holding the admin kubeconfig keeps full control even after leaving.
20
+ fix:
21
+ summary: Integrate the cluster with Entra ID and disable local accounts.
22
+ steps:
23
+ - Make sure Entra ID integration is enabled and an admin group is set.
24
+ - Move any automation from the admin kubeconfig to Entra-based credentials.
25
+ - Disable local accounts.
26
+ cli: az aks update -g <resource-group> -n <cluster> --enable-aad --disable-local-accounts
27
+ terraform: |
28
+ # azurerm_kubernetes_cluster
29
+ local_account_disabled = true
30
+ azure_active_directory_role_based_access_control { azure_rbac_enabled = true }
31
+ references:
32
+ - https://learn.microsoft.com/azure/aks/manage-local-accounts-managed-azure-ad
@@ -0,0 +1,29 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-003
3
+ title: AKS API server is private or limited to authorised IP ranges
4
+ provider: azure
5
+ area: kubernetes
6
+ severity: high
7
+ source:
8
+ type: resource_graph
9
+ query: |
10
+ resources
11
+ | where type =~ 'microsoft.containerservice/managedclusters'
12
+ | extend privateCluster = tobool(properties.apiServerAccessProfile.enablePrivateCluster),
13
+ authorizedRanges = array_length(properties.apiServerAccessProfile.authorizedIPRanges)
14
+ | project id, name, resourceGroup, location, privateCluster, authorizedRanges,
15
+ compliant = coalesce(privateCluster, false) or coalesce(authorizedRanges, 0) > 0
16
+ why: >-
17
+ A Kubernetes API server open to the whole Internet can be attacked by anyone who finds it.
18
+ A private cluster or an allow-list of office and pipeline addresses removes that exposure.
19
+ fix:
20
+ summary: Limit API server access to authorised IP ranges, or use a private cluster.
21
+ steps:
22
+ - List the addresses that need kubectl access (offices, VPN, CI runners).
23
+ - Add them as authorised IP ranges, or plan a move to a private cluster.
24
+ cli: az aks update -g <resource-group> -n <cluster> --api-server-authorized-ip-ranges <cidr1>,<cidr2>
25
+ terraform: |
26
+ # azurerm_kubernetes_cluster
27
+ api_server_access_profile { authorized_ip_ranges = ["<cidr>"] }
28
+ references:
29
+ - https://learn.microsoft.com/azure/aks/api-server-authorized-ip-ranges
@@ -0,0 +1,27 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-004
3
+ title: AKS clusters separate system and user node pools
4
+ provider: azure
5
+ area: kubernetes
6
+ severity: medium
7
+ source:
8
+ type: resource_graph
9
+ query: |
10
+ resources
11
+ | where type =~ 'microsoft.containerservice/managedclusters'
12
+ | mv-expand pool = properties.agentPoolProfiles
13
+ | summarize systemPools = countif(tostring(pool.mode) =~ 'System'),
14
+ userPools = countif(tostring(pool.mode) =~ 'User')
15
+ by id, name, resourceGroup, location
16
+ | extend compliant = systemPools >= 1 and userPools >= 1
17
+ why: >-
18
+ When application pods share nodes with system components, a busy or faulty workload can starve
19
+ DNS and other critical services and take the whole cluster down.
20
+ fix:
21
+ summary: Run applications on a separate user node pool and keep the system pool for system pods.
22
+ steps:
23
+ - Add a user node pool sized for your workloads.
24
+ - Taint the system pool with CriticalAddonsOnly=true:NoSchedule and move workloads across.
25
+ cli: az aks nodepool add -g <resource-group> --cluster-name <cluster> -n userpool --mode User
26
+ references:
27
+ - https://learn.microsoft.com/azure/aks/use-system-pools
@@ -0,0 +1,27 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-005
3
+ title: AKS node pools are spread across availability zones
4
+ provider: azure
5
+ area: reliability
6
+ severity: medium
7
+ source:
8
+ type: resource_graph
9
+ query: |
10
+ resources
11
+ | where type =~ 'microsoft.containerservice/managedclusters'
12
+ | mv-expand pool = properties.agentPoolProfiles
13
+ | summarize pools = count(),
14
+ poolsWithoutZones = countif(coalesce(array_length(pool.availabilityZones), 0) < 2)
15
+ by id, name, resourceGroup, location
16
+ | extend compliant = poolsWithoutZones == 0
17
+ why: >-
18
+ A node pool in a single zone goes down with that zone. Spreading nodes across zones keeps
19
+ workloads running through a datacentre outage.
20
+ fix:
21
+ summary: Create node pools across at least two availability zones (zones are set at creation).
22
+ steps:
23
+ - Check the region supports availability zones.
24
+ - Create a replacement pool with zones 1, 2 and 3, move workloads, then remove the old pool.
25
+ cli: az aks nodepool add -g <resource-group> --cluster-name <cluster> -n pool2 --zones 1 2 3
26
+ references:
27
+ - https://learn.microsoft.com/azure/aks/availability-zones
@@ -0,0 +1,31 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-006
3
+ title: AKS clusters have the Azure Policy add-on enabled
4
+ provider: azure
5
+ area: security
6
+ severity: medium
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ resources
12
+ | where type =~ 'microsoft.containerservice/managedclusters'
13
+ | extend azurePolicyEnabled = tobool(properties.addonProfiles.azurepolicy.enabled)
14
+ // coalesce: a missing add-on profile or unknown value counts as not enabled, so it fails.
15
+ | project id, name, resourceGroup, location, azurePolicyEnabled,
16
+ compliant = coalesce(azurePolicyEnabled, false)
17
+ why: >-
18
+ Without the Azure Policy add-on, nothing stops a pod from running as root, mounting the host or
19
+ pulling from an untrusted registry. The add-on enforces guardrails when workloads are admitted.
20
+ fix:
21
+ summary: Enable the Azure Policy add-on, then assign the built-in Kubernetes policy initiative.
22
+ steps:
23
+ - Enable the add-on on the cluster.
24
+ - Assign the "Kubernetes cluster pod security baseline standards" initiative, in audit mode first.
25
+ - Move the policies to deny once the audit results are clean.
26
+ cli: az aks enable-addons -g <resource-group> -n <cluster> --addons azure-policy
27
+ terraform: |
28
+ # azurerm_kubernetes_cluster
29
+ azure_policy_enabled = true
30
+ references:
31
+ - https://learn.microsoft.com/azure/aks/use-azure-policy
@@ -0,0 +1,30 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-007
3
+ title: AKS clusters have an automatic upgrade channel set
4
+ provider: azure
5
+ area: reliability
6
+ severity: medium
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ resources
12
+ | where type =~ 'microsoft.containerservice/managedclusters'
13
+ | extend upgradeChannel = tolower(tostring(properties.autoUpgradeProfile.upgradeChannel))
14
+ // A missing channel becomes '' and fails, exactly like 'none'.
15
+ | project id, name, resourceGroup, location, upgradeChannel,
16
+ compliant = upgradeChannel in ('patch', 'stable', 'rapid', 'node-image')
17
+ why: >-
18
+ A cluster with no upgrade channel stays on its Kubernetes version until someone remembers to
19
+ upgrade it. Old versions fall out of support and miss security patches.
20
+ fix:
21
+ summary: Set an automatic upgrade channel, for example patch or stable.
22
+ steps:
23
+ - Pick a channel. "patch" applies only security and bug-fix releases and is the safest start.
24
+ - Set a planned maintenance window so upgrades happen at a quiet time.
25
+ cli: az aks update -g <resource-group> -n <cluster> --auto-upgrade-channel patch
26
+ terraform: |
27
+ # azurerm_kubernetes_cluster
28
+ automatic_upgrade_channel = "patch"
29
+ references:
30
+ - https://learn.microsoft.com/azure/aks/auto-upgrade-cluster
@@ -0,0 +1,33 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-008
3
+ title: AKS clusters enforce a network policy
4
+ provider: azure
5
+ area: network
6
+ severity: medium
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ resources
12
+ | where type =~ 'microsoft.containerservice/managedclusters'
13
+ | extend networkPolicy = tolower(tostring(properties.networkProfile.networkPolicy))
14
+ // A missing value becomes '' and fails, exactly like 'none'.
15
+ | project id, name, resourceGroup, location, networkPolicy,
16
+ compliant = networkPolicy in ('azure', 'calico', 'cilium')
17
+ why: >-
18
+ With no network policy every pod can talk to every other pod. One compromised workload can then
19
+ reach databases and internal services across the cluster.
20
+ fix:
21
+ summary: Use a network policy engine (Azure, Calico or Cilium) and write policies per namespace.
22
+ steps:
23
+ - The network policy is chosen at cluster creation, so most clusters need a replacement cluster.
24
+ - Start with a default-deny policy in each namespace, then allow the traffic that is needed.
25
+ cli: az aks create -g <resource-group> -n <cluster> --network-plugin azure --network-policy azure
26
+ terraform: |
27
+ # azurerm_kubernetes_cluster
28
+ network_profile {
29
+ network_plugin = "azure"
30
+ network_policy = "azure"
31
+ }
32
+ references:
33
+ - https://learn.microsoft.com/azure/aks/use-network-policies
@@ -0,0 +1,31 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-009
3
+ title: AKS clusters have Kubernetes RBAC enabled
4
+ provider: azure
5
+ area: kubernetes
6
+ severity: high
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // properties.enableRBAC: clusters created without it cannot turn it on later. A missing value FAILS.
12
+ resources
13
+ | where type =~ 'microsoft.containerservice/managedclusters'
14
+ | extend enableRBAC = tobool(properties.enableRBAC)
15
+ | project id, name, resourceGroup, location, enableRBAC,
16
+ compliant = coalesce(enableRBAC, false)
17
+ why: >-
18
+ Without Kubernetes RBAC, anyone who can reach the API server with valid credentials can do
19
+ anything in the cluster: read every secret, run any workload and change every setting. Roles
20
+ are the only way to give a team or a pod less than full control.
21
+ fix:
22
+ summary: Recreate the cluster with Kubernetes RBAC enabled (it cannot be turned on in place).
23
+ steps:
24
+ - Create a new cluster with RBAC and, ideally, Entra ID integration.
25
+ - Define roles and role bindings for each team and service account.
26
+ - Move workloads to the new cluster, then delete the old one.
27
+ terraform: |
28
+ # azurerm_kubernetes_cluster
29
+ role_based_access_control_enabled = true
30
+ references:
31
+ - https://learn.microsoft.com/azure/aks/concepts-identity#kubernetes-rbac
@@ -0,0 +1,35 @@
1
+ schema_version: 1
2
+ id: AZ-AKS-010
3
+ title: AKS clusters have workload identity enabled
4
+ provider: azure
5
+ area: kubernetes
6
+ severity: low
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // properties.securityProfile.workloadIdentity.enabled; it needs the OIDC issuer, shown as
12
+ // evidence. A missing value means not enabled and FAILS.
13
+ resources
14
+ | where type =~ 'microsoft.containerservice/managedclusters'
15
+ | extend workloadIdentity = tobool(properties.securityProfile.workloadIdentity.enabled),
16
+ oidcIssuer = tobool(properties.oidcIssuerProfile.enabled)
17
+ | project id, name, resourceGroup, location, workloadIdentity, oidcIssuer,
18
+ compliant = coalesce(workloadIdentity, false)
19
+ why: >-
20
+ Without workload identity, pods that call Azure usually carry a client secret or connection
21
+ string, stored as a Kubernetes secret, that can leak and is rarely rotated. Workload identity
22
+ lets each pod use an Entra ID identity instead, with no secret to steal.
23
+ fix:
24
+ summary: Enable the OIDC issuer and workload identity, then move pods off stored secrets.
25
+ steps:
26
+ - Enable the OIDC issuer and workload identity on the cluster.
27
+ - For each app, create a managed identity with a federated credential for its service account.
28
+ - Replace the app's stored secrets with the identity, then delete the secrets.
29
+ cli: az aks update -g <resource-group> -n <cluster> --enable-oidc-issuer --enable-workload-identity
30
+ terraform: |
31
+ # azurerm_kubernetes_cluster
32
+ oidc_issuer_enabled = true
33
+ workload_identity_enabled = true
34
+ references:
35
+ - https://learn.microsoft.com/azure/aks/workload-identity-overview
@@ -0,0 +1,32 @@
1
+ schema_version: 1
2
+ id: AZ-COSMOS-001
3
+ title: Cosmos DB accounts disable key-based authentication
4
+ provider: azure
5
+ area: identity
6
+ severity: medium
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // properties.disableLocalAuth defaults to false (keys allowed). A missing value FAILS.
12
+ resources
13
+ | where type =~ 'microsoft.documentdb/databaseaccounts'
14
+ | extend disableLocalAuth = tobool(properties.disableLocalAuth), apiKind = tostring(kind)
15
+ | project id, name, resourceGroup, location, apiKind, disableLocalAuth,
16
+ compliant = coalesce(disableLocalAuth, false)
17
+ why: >-
18
+ An account key gives full read and write access to every database in the account, never expires
19
+ on its own, and does not say who used it. Keys end up in config files and scripts and are hard
20
+ to rotate. Entra ID roles are scoped, audited and removed when a person leaves.
21
+ fix:
22
+ summary: Move applications to Entra ID role-based access, then disable key-based authentication.
23
+ steps:
24
+ - Grant each application a Cosmos DB data-plane role (for example Cosmos DB Built-in Data Contributor).
25
+ - Change the applications to authenticate with their managed identity instead of a key.
26
+ - Disable local (key-based) authentication on the account.
27
+ cli: az cosmosdb update -g <resource-group> -n <account> --disable-local-auth true
28
+ terraform: |
29
+ # azurerm_cosmosdb_account
30
+ local_authentication_enabled = false
31
+ references:
32
+ - https://learn.microsoft.com/azure/cosmos-db/how-to-setup-rbac#disable-local-auth
@@ -0,0 +1,35 @@
1
+ schema_version: 1
2
+ id: AZ-COSMOS-002
3
+ title: Cosmos DB accounts are not open to all networks
4
+ provider: azure
5
+ area: network
6
+ severity: high
7
+ tier: free
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // Compliant when public network access is Disabled, or when an IP or virtual network filter
12
+ // limits it. A missing publicNetworkAccess is read as Enabled (the Azure default) and FAILS.
13
+ // Any IP rule counts as a limit, even a broad one; the rule count is shown as evidence.
14
+ resources
15
+ | where type =~ 'microsoft.documentdb/databaseaccounts'
16
+ | extend publicNetworkAccess = coalesce(tostring(properties.publicNetworkAccess), 'Enabled'),
17
+ ipRules = coalesce(array_length(properties.ipRules), 0),
18
+ vnetFilter = coalesce(tobool(properties.isVirtualNetworkFilterEnabled), false)
19
+ | project id, name, resourceGroup, location, publicNetworkAccess, ipRules, vnetFilter,
20
+ compliant = publicNetworkAccess =~ 'Disabled' or ipRules > 0 or vnetFilter
21
+ why: >-
22
+ An account that accepts connections from any network relies on its keys alone. A leaked key can
23
+ then be used from anywhere on the Internet to read or change every database in the account.
24
+ fix:
25
+ summary: Use private endpoints and disable public access, or limit public access to known networks.
26
+ steps:
27
+ - List the networks and addresses that must reach the account.
28
+ - Add a private endpoint, or add IP and virtual network rules in the account's Networking settings.
29
+ - Set public network access to Disabled, or to selected networks only.
30
+ cli: az cosmosdb update -g <resource-group> -n <account> --public-network-access Disabled
31
+ terraform: |
32
+ # azurerm_cosmosdb_account
33
+ public_network_access_enabled = false
34
+ references:
35
+ - https://learn.microsoft.com/azure/cosmos-db/how-to-configure-firewall
@@ -0,0 +1,32 @@
1
+ schema_version: 1
2
+ id: AZ-COST-001
3
+ title: No unattached managed disks
4
+ provider: azure
5
+ area: cost
6
+ severity: medium
7
+ tier: pro
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // Approximate list prices in INR per GB-month (assumption; replaced by billed cost at MVP):
12
+ // Premium 13, Standard SSD 7, Standard HDD 4. Inlined: Resource Graph rejects `let`.
13
+ resources
14
+ | where type =~ 'microsoft.compute/disks'
15
+ | extend diskState = tostring(properties.diskState), sizeGb = toint(properties.diskSizeGB), sku = tostring(sku.name)
16
+ | extend compliant = diskState !~ 'Unattached'
17
+ | extend estMonthlyWasteInr = iff(compliant, 0.0, todouble(sizeGb) *
18
+ case(sku startswith 'Premium', 13.0, sku startswith 'StandardSSD', 7.0, 4.0))
19
+ | project id, name, resourceGroup, location, diskState, sizeGb, sku, compliant, estMonthlyWasteInr
20
+ cost:
21
+ field: estMonthlyWasteInr
22
+ why: >-
23
+ An unattached disk is billed every month for its full size but serves no workload. They are
24
+ usually left behind when virtual machines are deleted.
25
+ fix:
26
+ summary: Snapshot the disk if its data may be needed, then delete it.
27
+ steps:
28
+ - Confirm with the owner that the data is not needed, or take a snapshot first.
29
+ - Delete the disk.
30
+ cli: az disk delete -g <resource-group> -n <disk>
31
+ references:
32
+ - https://learn.microsoft.com/azure/virtual-machines/disks-find-unattached-portal
@@ -0,0 +1,29 @@
1
+ schema_version: 1
2
+ id: AZ-COST-002
3
+ title: No unused public IP addresses
4
+ provider: azure
5
+ area: cost
6
+ severity: low
7
+ tier: pro
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // Approximate list price of a Standard static public IP: INR 320 per month (assumption).
12
+ // Inlined: Resource Graph rejects `let`.
13
+ resources
14
+ | where type =~ 'microsoft.network/publicipaddresses'
15
+ | extend attachedTo = coalesce(tostring(properties.ipConfiguration.id), tostring(properties.natGateway.id)),
16
+ sku = tostring(sku.name), allocation = tostring(properties.publicIPAllocationMethod)
17
+ | extend compliant = isnotempty(attachedTo)
18
+ | extend estMonthlyWasteInr = iff(compliant, 0.0, 320.0)
19
+ | project id, name, resourceGroup, location, sku, allocation, compliant, estMonthlyWasteInr
20
+ cost:
21
+ field: estMonthlyWasteInr
22
+ why: >-
23
+ A static public IP that is not attached to anything is still billed, and an unused public
24
+ address is easy to forget and later attach to the wrong resource.
25
+ fix:
26
+ summary: Delete public IPs that are not attached to a network interface, load balancer or NAT gateway.
27
+ cli: az network public-ip delete -g <resource-group> -n <public-ip>
28
+ references:
29
+ - https://learn.microsoft.com/azure/virtual-network/ip-services/public-ip-addresses
@@ -0,0 +1,28 @@
1
+ schema_version: 1
2
+ id: AZ-COST-003
3
+ title: Stopped virtual machines are deallocated
4
+ provider: azure
5
+ area: cost
6
+ severity: medium
7
+ tier: pro
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ resources
12
+ | where type =~ 'microsoft.compute/virtualmachines'
13
+ | extend powerState = tostring(properties.extended.instanceView.powerState.code),
14
+ vmSize = tostring(properties.hardwareProfile.vmSize)
15
+ | project id, name, resourceGroup, location, vmSize, powerState,
16
+ compliant = powerState != 'PowerState/stopped'
17
+ cost:
18
+ # No list price: VM sizes and licences vary too much. The VM's billed cost is its compute.
19
+ field: null
20
+ basis: billed cost only
21
+ why: >-
22
+ A VM shut down from inside the operating system is "stopped" but still allocated, so its compute
23
+ is billed in full. Only a deallocated VM stops compute charges.
24
+ fix:
25
+ summary: Deallocate the VM from Azure (Stop in the portal), or delete it if no longer needed.
26
+ cli: az vm deallocate -g <resource-group> -n <vm>
27
+ references:
28
+ - https://learn.microsoft.com/azure/virtual-machines/states-billing
@@ -0,0 +1,29 @@
1
+ schema_version: 1
2
+ id: AZ-COST-004
3
+ title: No disk snapshots older than 90 days
4
+ provider: azure
5
+ area: cost
6
+ severity: low
7
+ tier: pro
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ // Snapshots older than 90 days; approximate list price INR 4.4 per GB-month (assumption).
12
+ // Inlined: Resource Graph rejects `let`.
13
+ resources
14
+ | where type =~ 'microsoft.compute/snapshots'
15
+ | extend created = todatetime(properties.timeCreated), sizeGb = toint(properties.diskSizeGB)
16
+ | extend ageDays = datetime_diff('day', now(), created)
17
+ | extend compliant = ageDays <= 90
18
+ | extend estMonthlyWasteInr = iff(compliant, 0.0, todouble(sizeGb) * 4.4)
19
+ | project id, name, resourceGroup, location, sizeGb, ageDays, compliant, estMonthlyWasteInr
20
+ cost:
21
+ field: estMonthlyWasteInr
22
+ why: >-
23
+ Snapshots are often taken before a change and then forgotten. Old snapshots keep billing every
24
+ month long after anyone could need them.
25
+ fix:
26
+ summary: Delete snapshots that are no longer needed, and use Azure Backup policies for retention.
27
+ cli: az snapshot delete -g <resource-group> -n <snapshot>
28
+ references:
29
+ - https://learn.microsoft.com/azure/virtual-machines/snapshot-copy-managed-disk
@@ -0,0 +1,28 @@
1
+ schema_version: 1
2
+ id: AZ-COST-005
3
+ title: No paid App Service plans without apps
4
+ provider: azure
5
+ area: cost
6
+ severity: medium
7
+ tier: pro
8
+ source:
9
+ type: resource_graph
10
+ query: |
11
+ resources
12
+ | where type =~ 'microsoft.web/serverfarms'
13
+ | extend tier = tostring(sku.tier), skuName = tostring(sku.name), sites = toint(properties.numberOfSites)
14
+ | where tier !in~ ('Free', 'Shared')
15
+ | project id, name, resourceGroup, location, tier, skuName, sites,
16
+ compliant = coalesce(sites, 0) > 0
17
+ cost:
18
+ # No list price: plan tiers and instance counts vary too much. The plan's bill is the waste.
19
+ field: null
20
+ basis: billed cost only
21
+ why: >-
22
+ A paid App Service plan is billed whether or not any app runs on it. Empty plans are usually
23
+ left over after apps are moved or deleted.
24
+ fix:
25
+ summary: Delete empty plans, or move apps from other plans onto them.
26
+ cli: az appservice plan delete -g <resource-group> -n <plan>
27
+ references:
28
+ - https://learn.microsoft.com/azure/app-service/overview-hosting-plans