diff --git a/.github/workflows/azure-policy-exemption.yml b/.github/workflows/azure-policy-exemption.yml index 7b99c98..0e35c4e 100644 --- a/.github/workflows/azure-policy-exemption.yml +++ b/.github/workflows/azure-policy-exemption.yml @@ -1,53 +1,53 @@ -name: azure-policy-exemption -on: - workflow_dispatch: - inputs: - subscription_name: - description: 'From which subscription we need to provide exemption. the scope' - type: string - required: true - policy_name: - description: 'Policy Name to be given Exception to' - type: string - required: true - expires_after: - description: 'Policy exemption should be automatically revoked after how long' - type: string - required: true - unit: - description: 'Unit of time' - required: true - type: choice - options: - - hour - - day - - month -run-name: policy exemption for ${{ inputs.policy_name }} for ${{ inputs.expires_after }} ${{ inputs.unit }} -jobs: - azure-policy-exemption: - runs-on: ubuntu-latest - env: - AZURE_CLIENT_ID: ${{ secrets.OWNER_SP_APP_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.OWNER_SP_APP_SECRET }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install package mgmt tool - run: | - pip install poetry - poetry install - - - name: Execute program - run: | - poetry run python3 policy_exception.py --subscription_name "${{ inputs.subscription_name }}" --policy_name "${{ inputs.policy_name }}" --expires_after ${{ inputs.expires_after }} --unit ${{ inputs.unit }} - - - name: Completed - run: echo "Program execution completed" \ No newline at end of file +#name: azure-policy-exemption +#on: +# workflow_dispatch: +# inputs: +# subscription_name: +# description: 'From which subscription we need to provide exemption. the scope' +# type: string +# required: true +# policy_name: +# description: 'Policy Name to be given Exception to' +# type: string +# required: true +# expires_after: +# description: 'Policy exemption should be automatically revoked after how long' +# type: string +# required: true +# unit: +# description: 'Unit of time' +# required: true +# type: choice +# options: +# - hour +# - day +# - month +#run-name: policy exemption for ${{ inputs.policy_name }} for ${{ inputs.expires_after }} ${{ inputs.unit }} +#jobs: +# azure-policy-exemption: +# runs-on: ubuntu-latest +# env: +# AZURE_CLIENT_ID: ${{ secrets.OWNER_SP_APP_ID }} +# AZURE_CLIENT_SECRET: ${{ secrets.OWNER_SP_APP_SECRET }} +# AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - name: Set up python +# uses: actions/setup-python@v5 +# with: +# python-version: '3.11' +# +# - name: Install package mgmt tool +# run: | +# pip install poetry +# poetry install +# +# - name: Execute program +# run: | +# poetry run python3 policy_exception.py --subscription_name "${{ inputs.subscription_name }}" --policy_name "${{ inputs.policy_name }}" --expires_after ${{ inputs.expires_after }} --unit ${{ inputs.unit }} +# +# - name: Completed +# run: echo "Program execution completed" \ No newline at end of file diff --git a/README.md b/README.md index 6f7eb47..eb1d84b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ or operational needs. Automating the process of creating and managing policy exe Policy exemption automation simplifies the process of managing exceptions to cloud policies. +>[!NOTE] +> Policy Exemptions are applied at Subscription level scope + Here’s a brief overview of how it works: ## Triggering Automation: @@ -52,6 +55,35 @@ Use Azure python SDKs to create or update the policy exemption based on the prov * If value of expires_after is `4` and unit is `month` - the expiration will be after `4 months` of executing the job +# Streamlit UI + +* Using streamlit to Create pythin application + +![policy-exemption-example.jpeg](policy-exemption-example.jpeg) + +* Provide valid Subscription Name + +* This returns the Subscription Id corresponding to the Subscription name + +* If entered Subscription Name is not found, None value is returned for Subscription Id + +* Once a valid subscription Id is returned using it, it will show us all assigned policies at the subscription level + +* Select the policy from dropdown which needs exemption + +* Provide a expires after value like 1 or 2 or 10 or 4.5 etc and unit value like day or month or hour. + +* Click on Apply Exemption to apply exemption. + +# Run code locally + +* Clone the repository and change direcctory into this + +* Install all dependancies using `poetry install` + +* Execute `poetry run streamlit run .\streamlit_app.py` + + # Azure Python SDKs used use azure-identity for auth @@ -79,5 +111,5 @@ AZURE_CLIENT_SECRET: one of the service principal's client secrets >[!TIP] > The policy exemption name length must not exceed '64' characters. I am using the same as policy name for exception. -> You can choose to change it as you see fit +> You can choose to change it as you see fit diff --git a/azure_resource_graph_query.py b/azure_resource_graph_query.py index a521343..602739d 100644 --- a/azure_resource_graph_query.py +++ b/azure_resource_graph_query.py @@ -1,5 +1,6 @@ import azure.mgmt.resourcegraph as arg import logging +import streamlit as st from dotenv import load_dotenv from azure.identity import EnvironmentCredential @@ -27,6 +28,10 @@ def run_azure_rg_query(subscription_name: str): # Run query arg_result = arg_client.resources(arg_query) + if not arg_result.data: + # Handle the case where no subscription ID is found + st.error(f"No subscription found with the name '{subscription_name}'. Please verify the name and try again.") + return None subscription_id = arg_result.data[0]['subscriptionId'] print(f"Subscription ID is : {subscription_id}") return subscription_id @@ -38,7 +43,7 @@ def main(): """ load_dotenv() logging.info("ARG query being prepared......") - run_azure_rg_query(subscription_name="TECH-ARCHITECTS-NONPROD") + run_azure_rg_query(subscription_name="TECH-CLOUD-PROD") logging.info("ARG query Completed......") diff --git a/policy-exemption-example.jpeg b/policy-exemption-example.jpeg new file mode 100644 index 0000000..29ca89b Binary files /dev/null and b/policy-exemption-example.jpeg differ diff --git a/policy_exception.py b/policy_exception.py index 41ef8b8..2ee28ab 100644 --- a/policy_exception.py +++ b/policy_exception.py @@ -1,5 +1,6 @@ import os import argparse +import streamlit as st from dotenv import load_dotenv from azure.identity import EnvironmentCredential from azure.mgmt.resource.policy.v2022_06_01 import PolicyClient @@ -17,6 +18,12 @@ class PolicyAssignmentList(BaseModel): policy_definition_id : str scope : str +def get_policies(subscription_id: str): + # Retrieve all policies in the subscription + credential = EnvironmentCredential() + client = PolicyClient(credential=credential, subscription_id=subscription_id) + policy_assignment_list = client.policy_assignments.list() + return [policy.display_name for policy in policy_assignment_list] def extract_policy_data(subscription_id: str) -> PolicyAssignmentList: """ @@ -57,6 +64,7 @@ def verify_policy_is_available(subscription_id:str, policy_name: str): for policy in policy_assignment_list: if policy_name == policy.display_name: print(f"Found policy assignment for '{policy_name}' in scope {subscription_id}") + st.write(f"Found policy assignment for '{policy_name}' in scope {subscription_id}") # convert policy obj to dict policy_to_be_exempted = policy.__dict__ break # Exit the loop once the policy is found @@ -80,13 +88,13 @@ def create_exemption_for_policy(subscription_id: str, policy_name:str, expires_a policy_to_be_exempted = verify_policy_is_available(subscription_id=subscription_id, policy_name=policy_name) scope = f"/subscriptions/{subscription_id}" - + st.write(f"Scope of exemption is : /subscriptions/{subscription_id}") policy_exemption_name = f'exemption for {policy_name}' # - - HH:MM")> print(f'Policy Exemption name will be "{policy_exemption_name}"') + st.write(f'Policy Exemption name will be "{policy_exemption_name}"') expiry_date = calculate_expiry(expires_after=expires_after, unit=unit) policy_exemption_description = f'exemption for {policy_name}' - try: parameters = PolicyExemption( @@ -99,6 +107,7 @@ def create_exemption_for_policy(subscription_id: str, policy_name:str, expires_a exemption = client.policy_exemptions.create_or_update(scope=scope,policy_exemption_name=policy_exemption_name, parameters=parameters) print("Policy exemption created or updated successfully.") + st.write(f"Policy exemption created or updated successfully. Policy Exemption will expire at {expiry_date}") print(f'Policy Exemption will expire at {expiry_date}') except HttpResponseError as err: diff --git a/pyproject.toml b/pyproject.toml index 8fba5e6..8acbbe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ pydantic = "^2.8.2" python-dateutil = "^2.9.0.post0" pytz = "^2024.1" azure-core = "^1.30.2" +streamlit = "1.36.0" [build-system] diff --git a/streamlit_app.py b/streamlit_app.py new file mode 100644 index 0000000..abbd69f --- /dev/null +++ b/streamlit_app.py @@ -0,0 +1,42 @@ +import streamlit as st +from dotenv import load_dotenv +from policy_exception import create_exemption_for_policy, get_policies +from azure_resource_graph_query import run_azure_rg_query + + +def main(): + """run streamlit app""" + load_dotenv() + st.header("Azure Policy Exemption Tool", divider='rainbow') + subscription_name = st.text_input("Enter Subscription Name") + if subscription_name: + st.session_state.subscription_name = subscription_name + subscription_id = run_azure_rg_query(subscription_name=subscription_name) + st.session_state.subscription_id = subscription_id + st.success(f"Subscription ID of {subscription_name}: {subscription_id}") + else: + st.error(f"Subscription {subscription_name} not found") + if subscription_id: + # if 'subscription_id' in st.session_state: + policies = get_policies(subscription_id=subscription_id) + selected_policy = st.selectbox("Select a Policy", policies) + if selected_policy: + st.write(f"You selected: {selected_policy}") + st.session_state.selected_policy = selected_policy + + expires_after = st.text_input("Policy Will Expires After") + unit = st.selectbox("Unit", ["hour", "day", "month"]) + + # Print the exemption period + st.write(f"Policy exemption will expire after {expires_after} {unit}") + + # Run streamlit app by clicking submit + if st.button("Apply Exemption"): + # call policy exemption creation function + create_exemption_for_policy(subscription_id=subscription_id, policy_name=selected_policy, + expires_after=expires_after, unit=unit) + + + +if __name__ == "__main__": + main()