Skip to content

Commit 985ed06

Browse files
DEVOPS-63 initial commit automation-to-remove-policies-using-python
1 parent 78c283e commit 985ed06

8 files changed

+753
-1
lines changed

.github/dependabot.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: /
5+
schedule:
6+
interval: "weekly"
7+
day: wednesday
8+
time: "11:30"
9+
timezone: Asia/Kolkata
10+
# Assignees to set on pull requests
11+
assignees:
12+
- "githubofkrishnadhas"
13+
# prefix specifies a prefix for all commit messages. When you specify a prefix for commit messages,
14+
# GitHub will automatically add a colon between the defined prefix and the commit message provided the
15+
# defined prefix ends with a letter, number, closing parenthesis, or closing bracket.
16+
commit-message:
17+
prefix: "dependabot python package"
18+
# Use reviewers to specify individual reviewers or teams of reviewers for all pull requests raised for a package manager.
19+
reviewers:
20+
- "devwithkrishna/admin"
21+
# Raise pull requests for version updates to pip against the `main` branch
22+
target-branch: "main"
23+
# Labels on pull requests for version updates only
24+
labels:
25+
- "pip dependencies"
26+
# Increase the version requirements for Composer only when required
27+
versioning-strategy: increase-if-necessary
28+
# Dependabot opens a maximum of five pull requests for version updates. Once there are five open pull requests from Dependabot,
29+
# Dependabot will not open any new requests until some of those open requests are merged or closed.
30+
# Use open-pull-requests-limit to change this limit.
31+
open-pull-requests-limit: 10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: automation-to-remove-policy-from-azure
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
subscription_name:
6+
description: 'Azure subscription name'
7+
required: true
8+
type: string
9+
default: 'TECH-CLOUDCOE-NONPROD'
10+
policy_name:
11+
description: 'Azure policy name - the policy to be removed from above subscription'
12+
default: 'tagging policy'
13+
type: string
14+
required: true
15+
jobs:
16+
automation-to-remove-policy-from-azure:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: git checkout
20+
uses: actions/checkout@v4
21+
- name: set up python 3.10
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: '3.10'
25+
- name: package installations
26+
run: |
27+
pip install pipenv
28+
pipenv install
29+
- name: execute python program
30+
env:
31+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
32+
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
33+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
34+
run: |
35+
pipenv run python3 remove_azure_policy.py --subscription_name ${{ inputs.subscription_name }} --policy_name ${{ inputs.policy_name }}
36+
- name: Completed
37+
run: |
38+
echo "program completed successfully"

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,5 @@ cython_debug/
157157
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160-
#.idea/
160+
.idea/
161+
*.json

Pipfile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
argparse = "=1.4.0"
8+
pytz = "=2024.1"
9+
python-dotenv = "=1.0.1"
10+
azure-mgmt-policyinsights = "=1.0.0"
11+
azure-identity = "=1.16.0"
12+
azure-mgmt-resource = "=23.0.1"
13+
azure-mgmt-resourcegraph = "=8.0.0"
14+
azure-core = "=1.30.1"
15+
typing = "*"
16+
17+
[requires]
18+
python_version = "3"

Pipfile.lock

+455
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+56
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,58 @@
11
# azure-automation-to-remove-policies-using-python
22
automation to remove policy from a subscription by name - especially tagging policies with subscription name and policy name as input
3+
4+
# References
5+
* https://learn.microsoft.com/en-us/python/api/azure-core/azure.core.exceptions.httpresponseerror?view=azure-python#azure-core-exceptions-httpresponseerror-message :heavy_check_mark:
6+
* https://learn.microsoft.com/en-us/python/api/azure-mgmt-resource/azure.mgmt.resource.policy.v2022_06_01.operations.policyassignmentsoperations?view=azure-python#azure-mgmt-resource-policy-v2022-06-01-operations-policyassignmentsoperations-delete :heavy_check_mark:
7+
* https://learn.microsoft.com/en-us/python/api/azure-mgmt-policyinsights/azure.mgmt.policyinsights.operations.operations?view=azure-python :heavy_check_mark:
8+
* https://learn.microsoft.com/en-us/python/api/azure-mgmt-resource/azure.mgmt.resource.policy.v2022_06_01.policyclient?view=azure-python :heavy_check_mark:
9+
10+
# What is azure policy
11+
12+
```
13+
Azure Policy helps to enforce organizational standards and to assess compliance at-scale.
14+
Through its compliance dashboard, it provides an aggregated view to evaluate the overall state of the environment,
15+
with the ability to drill down to the per-resource, per-policy granularity. It also helps to bring your resources to compliance through bulk remediation for existing resources and automatic remediation for new resources.
16+
```
17+
Read more here: [Azure Governance - Policy](https://learn.microsoft.com/en-us/azure/governance/policy/overview)
18+
19+
# What the code does
20+
21+
```
22+
As the name suggests, this is an automation to remove policy from a subscription by name especially tagging policies with subscription name and policy name as input
23+
```
24+
25+
# Athentication
26+
27+
```markdown
28+
AZURE_CLIENT_ID= "xxxxxxxxxxx"
29+
AZURE_CLIENT_SECRET = "xxxxxxxxxxx"
30+
AZURE_SUBSCRIPTION_ID = "xxxxxxxxxxx"
31+
AZURE_TENANT_ID = "xxxxxxxxxxx"
32+
```
33+
* Replace ` "xxxxxxxxxxx" ` with proper values
34+
35+
**Rest is taken care by `DefaultAzureCredential` from `azure-identity` module**
36+
37+
# How code works
38+
39+
| file name | funtions |
40+
|-----------|----------|
41+
| azure_resource_graph_query.py | this file takes an argument, your subscription name and returns subscription id |
42+
| remove_azure_policy.py | this is the main py file. takes subscription id and policy name as inputs and removes policy if present |
43+
44+
45+
How to run the program manually from cmd line:
46+
47+
`python3 remove_azure_policy.py --subscription_name "<Subscription name>" --policy_name "<Policy name>"`
48+
49+
- replace the subscription name and policy names and provide correct values.
50+
51+
# parameters
52+
53+
`Python-dotenv reads key-value pairs from a .env file and can set them as environment variables`
54+
55+
| input name | type | description | required |
56+
|-----------------|------|-------------------------------------------------------------|----------|
57+
| subscription_name | string | Azure subscription name. Default - `TECH-ARCHITECTS-NONPROD` | :heavy_check_mark: |
58+
| policy_name | string | Azure policy to be removed. | :heavy_check_mark: |

azure_resource_graph_query.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from azure.identity import DefaultAzureCredential
2+
import azure.mgmt.resourcegraph as arg
3+
import logging
4+
import os
5+
from dotenv import load_dotenv
6+
7+
def run_azure_rg_query(subscription_name: str):
8+
"""
9+
Run a resource graph query to get the subscription id of a subscription back
10+
:return: subscription_id str
11+
"""
12+
credential = DefaultAzureCredential()
13+
# Create Azure Resource Graph client and set options
14+
arg_client = arg.ResourceGraphClient(credential)
15+
16+
query = f"""
17+
resourcecontainers
18+
| where type == 'microsoft.resources/subscriptions' and name == '{subscription_name}'
19+
| project subscriptionId
20+
"""
21+
22+
print(f"query is {query}")
23+
24+
# Create query
25+
arg_query = arg.models.QueryRequest( query=query)
26+
27+
# Run query
28+
arg_result = arg_client.resources(arg_query)
29+
30+
# Show Python object
31+
# print(arg_result)
32+
subscription_id = arg_result.data[0]['subscriptionId']
33+
# print(f"Subscription ID is : {subscription_id}")
34+
return subscription_id
35+
36+
def main():
37+
"""
38+
To test the script
39+
:return:
40+
"""
41+
load_dotenv()
42+
logging.info("ARG query being prepared......")
43+
run_azure_rg_query(subscription_name="TECH-ARCHITECTS-NONPROD")
44+
logging.info("ARG query Completed......")
45+
46+
47+
if __name__ == "__main__":
48+
main()

remove_azure_policy.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import argparse
2+
import os
3+
import json
4+
from dotenv import load_dotenv
5+
from azure.identity import DefaultAzureCredential
6+
from azure.mgmt.policyinsights import PolicyInsightsClient
7+
from azure.core.exceptions import HttpResponseError
8+
from azure.mgmt.resource import PolicyClient
9+
from typing import List, Dict, Any
10+
from azure_resource_graph_query import run_azure_rg_query
11+
12+
13+
def list_azure_policy_in_a_subscription_scope(subscription_id:str):
14+
"""
15+
subscription name --> scope
16+
:return:
17+
"""
18+
try:
19+
credential = DefaultAzureCredential()
20+
policy_insights_client = PolicyInsightsClient(credential, subscription_id={subscription_id})
21+
policy_assignments = policy_insights_client.policy_states.list_query_results_for_subscription(policy_states_resource='latest',subscription_id=subscription_id)
22+
policy_assignments_list = []
23+
for assignment in policy_assignments:
24+
print(f"Policy Assignment ID: {assignment.policy_assignment_id}")
25+
print(f"Policy Assignment Name: {assignment.policy_assignment_name}")
26+
print(f"Policy Assignment Scope: {assignment.policy_assignment_scope}")
27+
print(f"Policy Definition ID: {assignment.policy_definition_id}")
28+
print(f"Policy Definition Name: {assignment.policy_definition_name}")
29+
print(f"Policy Assignment Created On: {assignment.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
30+
print("------------------------------")
31+
assignment_dict = {
32+
"policy_assignment_id": assignment.policy_assignment_id,
33+
"policy_assignment_name": assignment.policy_assignment_name,
34+
"policy_assignment_scope": assignment.policy_assignment_scope,
35+
"policy_definition_id": assignment.policy_definition_id,
36+
"policy_definition_name": assignment.policy_definition_name,
37+
"policy_assignment_created_on": assignment.timestamp.strftime('%Y-%m-%d %H:%M:%S')
38+
}
39+
policy_assignments_list.append(assignment_dict)
40+
file_name = f'azure_policy_assignment_{subscription_id}.json'
41+
print(file_name)
42+
# Assuming policy_assignments_list is the list of dictionaries
43+
with open(file_name, 'w') as json_file:
44+
json.dump(policy_assignments_list, json_file, indent=4)
45+
print(f"Policy assignments successfully retrieved and saved to {file_name} .")
46+
47+
return policy_assignments_list
48+
except HttpResponseError as ex:
49+
print(f"Failed to retrieve policy assignments. Error message: {ex.message}")
50+
51+
except Exception as ex:
52+
print(f"An error occurred: {ex}")
53+
54+
def validation_of_policy_name(policy_name: str,policy_assignments_list: list[dict[str, Any]]):
55+
"""
56+
This function is used to validate the provided policy is there in current subscription scoped aassignment
57+
:param policy_name:
58+
:return:
59+
"""
60+
for policy_assignment in policy_assignments_list:
61+
if policy_name in policy_assignment['policy_assignment_name']:
62+
print(f"Policy '{policy_name}' found in assignments")
63+
return policy_name, policy_assignment['policy_assignment_scope']
64+
raise Exception(f"Policy '{policy_name}' not found in assignments")
65+
66+
67+
def remove_azure_policy_from_subscription(credential,subscription_id: str, policy_name: str, scope:str):
68+
"""
69+
Remove the policy specified from the subscription level
70+
:param subscription_id:
71+
:param policy_name:
72+
:return:
73+
"""
74+
try:
75+
policy_client = PolicyClient(credential, subscription_id)
76+
policy_client.policy_assignments.delete(scope=scope, policy_assignment_name=policy_name)
77+
print(f"Policy '{policy_name}' removed from '{scope}' successfully.")
78+
except HttpResponseError as ex:
79+
print(f"Failed to delete policy assignment: {ex}")
80+
81+
def main():
82+
""" To test the code"""
83+
parser = argparse.ArgumentParser("Remove azure policies")
84+
parser.add_argument("--subscription_name", help="subscription name in azure", required=True, type=str)
85+
parser.add_argument("--policy_name", help="policy name in azure", required=True, type=str)
86+
87+
args = parser.parse_args()
88+
subscription_name = args.subscription_name
89+
policy_name = args.policy_name
90+
print(f"Process to remove azure policy - {policy_name} begining......")
91+
load_dotenv()
92+
credential = DefaultAzureCredential()
93+
subscription_id = run_azure_rg_query(subscription_name=subscription_name)
94+
print(f'Subscription id of {subscription_name} is : {subscription_id}')
95+
os.environ['subscription_id'] = subscription_id
96+
policy_assignments_list = list_azure_policy_in_a_subscription_scope(subscription_id=subscription_id)
97+
policy_name, policy_assignment_scope = validation_of_policy_name(policy_name=policy_name, policy_assignments_list=policy_assignments_list)
98+
if policy_name is not None:
99+
print(f'Removing policy {policy_name} on the scope {policy_assignment_scope}')
100+
remove_azure_policy_from_subscription(credential=credential,subscription_id=subscription_id, policy_name=policy_name, scope=policy_assignment_scope)
101+
102+
103+
104+
if __name__ == "__main__":
105+
main()

0 commit comments

Comments
 (0)