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.
- skyveda/__init__.py +3 -0
- skyveda/__main__.py +5 -0
- skyveda/checks/azure/AZ-ACR-001.yaml +32 -0
- skyveda/checks/azure/AZ-ACR-002.yaml +34 -0
- skyveda/checks/azure/AZ-ACR-003.yaml +37 -0
- skyveda/checks/azure/AZ-AI-001.yaml +33 -0
- skyveda/checks/azure/AZ-AKS-002.yaml +32 -0
- skyveda/checks/azure/AZ-AKS-003.yaml +29 -0
- skyveda/checks/azure/AZ-AKS-004.yaml +27 -0
- skyveda/checks/azure/AZ-AKS-005.yaml +27 -0
- skyveda/checks/azure/AZ-AKS-006.yaml +31 -0
- skyveda/checks/azure/AZ-AKS-007.yaml +30 -0
- skyveda/checks/azure/AZ-AKS-008.yaml +33 -0
- skyveda/checks/azure/AZ-AKS-009.yaml +31 -0
- skyveda/checks/azure/AZ-AKS-010.yaml +35 -0
- skyveda/checks/azure/AZ-COSMOS-001.yaml +32 -0
- skyveda/checks/azure/AZ-COSMOS-002.yaml +35 -0
- skyveda/checks/azure/AZ-COST-001.yaml +32 -0
- skyveda/checks/azure/AZ-COST-002.yaml +29 -0
- skyveda/checks/azure/AZ-COST-003.yaml +28 -0
- skyveda/checks/azure/AZ-COST-004.yaml +29 -0
- skyveda/checks/azure/AZ-COST-005.yaml +28 -0
- skyveda/checks/azure/AZ-COST-006.yaml +34 -0
- skyveda/checks/azure/AZ-COST-007.yaml +49 -0
- skyveda/checks/azure/AZ-COST-008.yaml +35 -0
- skyveda/checks/azure/AZ-KV-001.yaml +30 -0
- skyveda/checks/azure/AZ-KV-002.yaml +32 -0
- skyveda/checks/azure/AZ-KV-003.yaml +37 -0
- skyveda/checks/azure/AZ-MSG-001.yaml +34 -0
- skyveda/checks/azure/AZ-NET-001.yaml +33 -0
- skyveda/checks/azure/AZ-NET-002.yaml +25 -0
- skyveda/checks/azure/AZ-NET-003.yaml +45 -0
- skyveda/checks/azure/AZ-NET-004.yaml +37 -0
- skyveda/checks/azure/AZ-NET-005.yaml +43 -0
- skyveda/checks/azure/AZ-NET-006.yaml +35 -0
- skyveda/checks/azure/AZ-NET-007.yaml +35 -0
- skyveda/checks/azure/AZ-NET-008.yaml +30 -0
- skyveda/checks/azure/AZ-PGSQL-001.yaml +35 -0
- skyveda/checks/azure/AZ-REDIS-001.yaml +31 -0
- skyveda/checks/azure/AZ-REDIS-002.yaml +30 -0
- skyveda/checks/azure/AZ-REL-001.yaml +27 -0
- skyveda/checks/azure/AZ-REL-002.yaml +33 -0
- skyveda/checks/azure/AZ-REL-003.yaml +33 -0
- skyveda/checks/azure/AZ-SQL-001.yaml +37 -0
- skyveda/checks/azure/AZ-SQL-002.yaml +28 -0
- skyveda/checks/azure/AZ-SQL-003.yaml +35 -0
- skyveda/checks/azure/AZ-SQL-004.yaml +31 -0
- skyveda/checks/azure/AZ-STG-001.yaml +29 -0
- skyveda/checks/azure/AZ-STG-002.yaml +29 -0
- skyveda/checks/azure/AZ-STG-003.yaml +31 -0
- skyveda/checks/azure/AZ-STG-004.yaml +33 -0
- skyveda/checks/azure/AZ-STG-005.yaml +32 -0
- skyveda/checks/azure/AZ-VM-001.yaml +33 -0
- skyveda/checks/azure/AZ-VM-002.yaml +37 -0
- skyveda/checks/azure/AZ-VM-003.yaml +34 -0
- skyveda/checks/azure/AZ-VM-004.yaml +42 -0
- skyveda/checks/azure/AZ-WEB-001.yaml +30 -0
- skyveda/checks/azure/AZ-WEB-002.yaml +30 -0
- skyveda/checks/azure/AZ-WEB-003.yaml +35 -0
- skyveda/checks/azure/AZ-WEB-004.yaml +29 -0
- skyveda/checks/azure/AZ-WEB-005.yaml +29 -0
- skyveda/checks/azure/AZ-WEB-006.yaml +32 -0
- skyveda/cli.py +450 -0
- skyveda/core/__init__.py +1 -0
- skyveda/core/billing.py +163 -0
- skyveda/core/checks.py +117 -0
- skyveda/core/config.py +83 -0
- skyveda/core/engine.py +92 -0
- skyveda/core/errors.py +57 -0
- skyveda/core/evaluators.py +150 -0
- skyveda/core/models.py +327 -0
- skyveda/core/offline.py +97 -0
- skyveda/core/provider.py +87 -0
- skyveda/core/retry.py +72 -0
- skyveda/core/scoring.py +52 -0
- skyveda/core/waivers.py +130 -0
- skyveda/providers/__init__.py +1 -0
- skyveda/providers/azure/__init__.py +21 -0
- skyveda/providers/azure/arm.py +189 -0
- skyveda/providers/azure/auth.py +142 -0
- skyveda/providers/azure/collectors.py +117 -0
- skyveda/providers/azure/costs.py +234 -0
- skyveda/providers/azure/evaluators.py +144 -0
- skyveda/providers/azure/provider.py +159 -0
- skyveda/providers/azure/resource_graph.py +202 -0
- skyveda/py.typed +0 -0
- skyveda/report/__init__.py +5 -0
- skyveda/report/fonts/DejaVuSans-Bold.ttf +0 -0
- skyveda/report/fonts/DejaVuSans.ttf +0 -0
- skyveda/report/fonts/LICENSE-DejaVu.txt +187 -0
- skyveda/report/html.py +70 -0
- skyveda/report/pdf.py +466 -0
- skyveda/report/summary.py +175 -0
- skyveda/report/templates/report.html.j2 +150 -0
- skyveda-0.3.0.dist-info/METADATA +137 -0
- skyveda-0.3.0.dist-info/RECORD +100 -0
- skyveda-0.3.0.dist-info/WHEEL +4 -0
- skyveda-0.3.0.dist-info/entry_points.txt +5 -0
- skyveda-0.3.0.dist-info/licenses/LICENSE +202 -0
- skyveda-0.3.0.dist-info/licenses/NOTICE +10 -0
skyveda/__init__.py
ADDED
skyveda/__main__.py
ADDED
|
@@ -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
|