{"id":21975372,"url":"https://github.com/labyrinthinesecurity/silhouette","last_synced_at":"2025-07-09T05:33:27.417Z","repository":{"id":209697288,"uuid":"724691110","full_name":"labyrinthinesecurity/silhouette","owner":"labyrinthinesecurity","description":"An Azure SPN access minimizer","archived":false,"fork":false,"pushed_at":"2024-04-09T11:31:39.000Z","size":8697,"stargazers_count":10,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-09T20:16:14.017Z","etag":null,"topics":["automation","azure","devsecops","iam"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/labyrinthinesecurity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-CC-BY-4.0.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-11-28T15:43:09.000Z","updated_at":"2024-04-15T08:00:01.126Z","dependencies_parsed_at":"2024-03-18T09:56:40.593Z","dependency_job_id":"1be6c218-13b6-4b3a-89d2-3d5e3adde32c","html_url":"https://github.com/labyrinthinesecurity/silhouette","commit_stats":null,"previous_names":["labyrinthinesecurity/silhouette"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labyrinthinesecurity%2Fsilhouette","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labyrinthinesecurity%2Fsilhouette/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labyrinthinesecurity%2Fsilhouette/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labyrinthinesecurity%2Fsilhouette/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/labyrinthinesecurity","download_url":"https://codeload.github.com/labyrinthinesecurity/silhouette/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227160499,"owners_count":17740062,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","azure","devsecops","iam"],"created_at":"2024-11-29T15:51:47.614Z","updated_at":"2024-11-29T15:51:48.446Z","avatar_url":"https://github.com/labyrinthinesecurity.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Silhouette for Azure, a SPN access minimizer\n\n\u003cimg src=\"https://github.com/labyrinthinesecurity/silhouette/blob/main/silhouette_logo.png\" width=\"40%\"\u003e\n\n## introduction\n\nSilhouette runs through all your SPNs and groups them by similarity into clusters. This grouping is performed using machine learning.\n\nThen, within each cluster, it pulls the actual Azure RBAC permissions of all SPNs from Azure Entra and compares them to Azure Activity logs. \n\nFrom that analysis, Silhouette provide 3 per-cluster indicators:\n- Inner score (blue): a numerical representation of the RBAC privileges required for the cluster to run with minimal permissions, based on \"ground truth\" observations (Azure Activity Logs)\n- Outer score (orange): a numerical representation of the RBAC privileges directly or indirectly (through group membership) granted to the SPNs in the cluster\n- De-escalation reward (green): the difference between the two scores.\n\nThe first two indicators are generated with the help of a new norm called the *WAR norm*.\n\nThe last indicator (the reward) is generated with the help of the distance induced by the WAR norm: the *WAR distance*.\n\n\u003cimg src=\"https://github.com/labyrinthinesecurity/silhouette/blob/main/sil.PNG\" width=\"50%\"\u003e\n\nIn practice, it is not reasonable to try to de-escalate to the inner score, because the inner score is always scoped at resource level, by definition of LAW logs. To avoid generating an unscalable number of roles, you want to scope permissions at resource group level (or higher). Let's call it the desired score.\n\nYour desired score is always going to be a WAR norm belonging to the interval [inner score, outer score].\n\nThe de-escalation reward lets you quickly determine which cluster to tackle in priority: the highest the number, the more urgent to de-escalate.\n\n\u003cimg src=\"https://github.com/labyrinthinesecurity/silhouette/blob/main/outer.png\" width=\"50%\"\u003e\n\nFinally, Silhouette suggests per-cluster role definitions and role assignements that lets you reach a desired score.\n\n## De-escalation reward hierarchy\n\nThe silhouette metric calculates the distance of each cluster to the origin. The distance ranges from 0 (cluster has no rights at all) at the origin to 999 (cluster is Tenant admin). Mathematical metrics obey a strict hierarchy which allows to make accurate distance measurements between cluster permissions. \n\nBy measuring the golden source permissions of a cluster to the origin and its ground source permissions to the origin, the triangular inequality allows us to determine the distance between golden source and ground truth. This distance is precisely the de-escalation effort.\n\n\u003cimg src=\"https://github.com/labyrinthinesecurity/silhouette/blob/main/hier.PNG\" width=\"50%\"\u003e\n\n## Pre-requisites\n- Python 3.6 or later\n- An Azure Table to store Azure silhouette data\n- A Log Analytics Workspace where your Azure Activity Logs are centralized\n\n## Configure\n\nMost of the code is held in file common.py\n\nTo get started, you must set a few environment variables which are loaded into common.py:\n\n- wid: the Log Analytics workspace ID, where all activity logs are collected\n- account: the Azure storage account that silhouette will use to store its data\n- unused: name of an Azure table in the account, where all unused SPNs will be appended (unused means: no activity for the past 3 months)\n- orphans: name of an Azure table in the account, where Azure role assignments assigned to a deleted principal will be appended\n- build_groundsource: name of an Azure table in the account, where the ground truth will be stored (azure activity)\n- run_groundsource: name of an Azure table in the account, from where the ground truth will be read (for analytics)\n- build_goldensource: name of an Azure table in the account, where the golden source will be stored (azure RBAC roles and perms)\n- run_goldensource: name of an Azure table in the account, from where the golden source will be read (for analytics)\n\nThe first time you run silhouette (step 1 below), you don't have a run_goldensource and you don't have a run_groundsource.\n\nYou may also wish to adjust logsRetention, the Log Analytics retention parameter (in days), which is 90 days by default. Don't set this parameter to 0. This global variable is declared in common.py\n\n## Known current limitations (work in progress)\n\nSince Silhouette ultimately relies on Azure Activity Logs to perform its audit, read actions are not captured. (Azure Activity only captures write/delete and actions)\n\nFor now, roles related to role assignments or role definitions (from the Microsoft.Authorizations resource provider) are being ignored. \n\nFinally, roles assigned at any scope below resource groups are being ignored.\n\n### how to overcome current limitations?\n\nFor now, unsupported permissions must be added manually to any role definition proposed by Silhouette.\n\n## What about the data plane?\n\nResources logs, aka data plane actions, are not captured by Azure Activity. But there is a more important reason why data plane actions are not managed by Silhouette: it is because ranking such actions requires a deep understanding of the business value and the business requirements attached to the data. This value is highly customer dependent. \n\nA traditional approach for dealing with data value is to reason in terms of availability, integrity, confidentiality and auditability. Silhouette is not able to understand these notions at the moment, so it is not able to rank data plane actions or to automate custom roles for the data plane.\n\n# Silhouette calculation\n\n\u003cimg src=\"https://github.com/labyrinthinesecurity/silhouette/blob/main/rbac_distance.jpeg\" width=\"40%\"\u003e\n\n## Step 1: Collect sources from Azure backends\n\nSet build_goldensource, build_groundsource, unused and orphans tables to empty tables in your azure storage account.\n\n### Collect Golden source from Entra\n\nRun collect_goldensource.py to populate 3 tables: build_goldensource unused principals and orphans.\n\nIn the build_goldensource table that was just generated, pickup the UUID of the partition.\n\nSet this UUID to an environment variable called \"run_partition\"\n\nCreate a blob container named with this UUID.\n\n### Collect and cache ground source from Azure Log Analytics\n\nThen, run collect_groundsource.py to populate the build_groundsource table. It will also cache the logs in the blob container to save time for later.\n\nWarning: due to Azure throttling, this step takes a long time. Typically 4 to 20 hours in a typical production environment whith thousands of SPNs.\n\n\n## Step 2: Machine learning\n\nNow that the collect process, you may set run_goldensource and run_groundsource to the build_goldensource and build_groundsource tables, respectively.\n\nYou should then set your build_goldensource and build_groundsource variables to othertables, to avoid accidental overwritting of the run tables if you run collect.py once again!\n\n### Run k-means\n\nRun clusterize.py to group SPNs into similarity clusters. This should take less than a minute.\n\nThe script generates a CSV file to be consumed by minimize.py in step 3.\n\n## Step 3: Calculate and visualize current and 'default' desired silhouettes\n\nRun minimize.py\n\nThis will generate a CSV containing cluster ID, SPN counts per cluster, a default desired silhouette, the current silhouette, and the de-escalation reward to reach the default\n\n\n# Roles minimization\n\nThe default desired silhouettes generated by minimize.py are only suggestions. They can be fine-tuned for each cluster ID.\n\nWhat's more, it is possible to generate clusterwide roles based on desired silhouettes. This process is called roles minimization.\n\n## Customizing roles of a cluster\n\nPick a cluster ID from the above mentioned CSV and stick it to the function called in condensate.py\n\nA *condensate* is a generic term to describe a list of RBAC roles to be assigned to all SPNs in the cluster.\n\nFor example, if the cluster ID you want to condensate has cluster ID 7, you should set the function as follows:\n\n```\ngenerate_condensate(run_partition,\"7\",desired_silhouette='None')\n```\n\nNotice that:\n- the cluster ID is actually string \"7\", not number 7\n- run_partition is automatically retrieved from the environment variable, as explained above\n\n### The desired_silhouette parameter\n\nWhen desired_silhouette is None, condensate.py uses the default desired_silhouette which is the same as minimize.py\n\nYou may set desired_sihouette to any WAR norm value that meets your requirements for a cluster ID. Refer to the WAR norm table to tweak desired_sihouette accordingly\n\n\u003cimg src=\"https://github.com/labyrinthinesecurity/silhouette/blob/main/WARnormTable.PNG\" width=\"75%\"\u003e\n\n- Setting a desired silhouette to subscription level for A (+35) and R (+3) and resource group level for W (+300) requires the desired_silhouette parameter to be set to 35 + 3 + 300 = 338\n- Setting a desired_silhouette to resource group level for A (+30), subscription level for W (+400) and management group level for R (+4) requires the desired_silhouette parameter to be set to 30 + 400 +4 = 434\n\n\n```\ngenerate_condensate(run_partition,\"7\",desired_silhouette=434)\n```\n\n### Allowed ranges\n\nCurrently, desired_silhouette supports the following ranges for Write, Action and Read permissions:\n- For Write permissions: non-superadmin and below management group (so 400 or less), above or equal to resource group (so 300 or more)\n- For Action permissions: below management group (so 35 or less) above or equal to resource group (so 30 or more)\n- For Read permissions: below management group (so 3 or less) above or equal to resource group (so 2 or more)\n\n### Minimized roles\ncondensate.py will generate a couple of JSON files fine-tuned by the desired_silhouette parameter:\n- *ID-condensate.json* is a list of roles to be assigned to the cluster to reach desired_silhouette\n- *ID-feather.json* is reserved for future use\n\n### Recommendations\n\nGround truth permissions retrieved from Azure Activity always operate at the ressource or subresource level. This grain is often too fine to allow a scalable scoping of role defitinions. You want to scope roles at management group (this level is not currently supported), subscription, or, whenever possible, resource group level.\n\nIf you are ready to customize cluster roles, you should first try to set desired_silhouette to resource group level at most (desired_silhouette=332) \nIf this the resulting condensate generates too many roles, try to set one of W, A or R to subscription level\n\nWARNING: setting W, A or R to \"subscription\" level means that all W, A or R permissions encountered in Azure activity logs will be elevated to this level in the resulting condensate. This is not least privilege, so you should investigate the resulting condensate and attempt to lower some W, A or R permissions manually.\n\nHere are a few examples of silhouette configurations based on the WAR norm table (by increasing order of minimization):\n- 438 corresponds to subscription level for W, A and R\n- 433 corresponds to subscription level for W, resource group level for A and R\n- 338 corresponds to resource group level for W, subscription level for A and R \n- 333 corresponds to resource group level for W and A, subscirption level for R\n- 332 corresponds to resource group level for W, A and R\n\n### Manual review \nThe script condensate.py does W/A/R fine-tuning for you, but it MUST be reviewed manually, because it is not 100% acurate:\n\n1) as explained above, condensate.py will elevate all permissions to the max scope it is entitled by the desired silhouette \n2) generated roles are based on similarity between permissions of different SPNs, sometimes if common permissions are used in very different contexts, the grouping may be too coarse \n\n## A word on the reward...\n\nYou will end up with **only as many custom SPN role definitions as clusters**, hopefully meaning somewhere between 10 to 50 role definitions, depending of the complexity of your operating model. \n\nMuch more manageable than the usual bunch of nested groups with a mix up of haphazardly attached built-in and custom roles.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flabyrinthinesecurity%2Fsilhouette","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flabyrinthinesecurity%2Fsilhouette","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flabyrinthinesecurity%2Fsilhouette/lists"}