Skip to content

Commit 36ca196

Browse files
authored
feat: add orgs and scp visualization capability (#25)
* feat: add orgs and scp visualization capability * docs: add visualization and update bytes to character limit
1 parent d6d0e02 commit 36ca196

8 files changed

Lines changed: 196 additions & 40 deletions

File tree

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55

66
This project provides a Python module to aid in Service Control Policy (SCP) management in AWS accounts.
77

8-
SCPs have a current limit of 5 total per entity, and a size limit on each of 5120 bytes. This tool will merge selected SCPs into the fewest amount of policies, and optionally remove whitespace characters as they count toward the byte limit.
8+
SCPs have a current limit of 5 total per entity, and a size limit on each of 5120 characters. This tool will merge selected SCPs into the fewest amount of policies, and optionally remove whitespace characters as they count toward the character limit.
99

1010

1111
```mermaid
1212
stateDiagram-v2
1313
[SCPTool] --> Validate
1414
[SCPTool] --> Merge
15+
[SCPTool] --> Visualize
1516
Merge --> Validate
1617
Validate --> [*]
1718
Merge --> [*]
19+
Visualize --> [*]
1820
```
1921
## Using SCPkit
2022
SCPkit can be installed from PyPI
@@ -38,23 +40,36 @@ Optional validation with output locally:
3840
scpkit merge --sourcefiles /path/to/scps --outdir /path/to/directory --validate-after-merge --profile yourawsprofile
3941
```
4042

43+
### Creating a visualization of an AWS Organization, OUs, Accounts, and SCPs
44+
Creating this visualization requires you be authenticated with either the Org management account, or a delegated administrator. See the [AWS Documentation](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_delegate_policies_example_view_accts_orgs.html) page for more info on delegating Organizations.
45+
46+
This will output a graph pdf and graphviz data file in the specified directory (or local directory, if outdir is not specified.)
47+
48+
```
49+
scpkit visualize --profile yourawsprofile --outdir ./org-graph
50+
```
51+
Accounts are presented as ellipses, organizational units are rectangles, and SCPs are trapezoids.
52+
53+
![Visualization of an Organization](./visualize-org.png)
54+
4155
The full CLI is documented through docopt
4256
```
43-
SCPkit
57+
"""SCPkit
4458
Usage:
45-
main.py (validate | merge) [--sourcefiles sourcefiles] [--profile profile] [ --outdir outdir] [--validate-after-merge] [--readable]
59+
main.py (validate | merge | visualize) [--sourcefiles sourcefiles] [--profile profile] [ --outdir outdir] [--validate-after-merge] [--readable] [--console]
4660
4761
Options:
4862
-h --help Show this screen.
4963
--version Show version.
50-
--sourcefiles sourcefiles Directory path to SCP files in json format
64+
--sourcefiles sourcefiles Directory path to SCP files in json format or a single SCP file
5165
--outdir outdir Directory to write new SCP files [Default: ./]
5266
--profile profile AWS profile name
5367
--validate-after-merge Validate the policies after merging them
5468
--readable Leave indentation and some whitespace to make the SCPs readable
69+
--console Adds Log to console
70+
"""
5571
```
5672

57-
5873
## Local development
5974
From the root of the folder:
6075
```

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
boto3
2-
docopt
1+
boto3>=1.28.66
2+
docopt>=0.6.2
3+
graphviz>=0.20.1

scpkit/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""SCPkit
22
Usage:
3-
main.py (validate | merge) [--sourcefiles sourcefiles] [--profile profile] [ --outdir outdir] [--validate-after-merge] [--readable] [--console]
3+
main.py (validate | merge | visualize) [--sourcefiles sourcefiles] [--profile profile] [ --outdir outdir] [--validate-after-merge] [--readable] [--console]
44
55
Options:
66
-h --help Show this screen.
@@ -16,13 +16,17 @@
1616
from .src.validate import validate_policies
1717
from .src.merge import scp_merge
1818
from .src.util import get_files_in_dir
19+
from .src.visualize import visualize_policies
1920

2021
def main():
2122
arguments = {
2223
k.lstrip('-'): v for k, v in docopt(__doc__).items()
2324
}
2425

25-
arguments['scps'] = get_files_in_dir(arguments["sourcefiles"])
26+
if arguments.get("visualize"):
27+
visualize_policies(arguments['profile'], arguments['outdir'])
28+
else:
29+
arguments['scps'] = get_files_in_dir(arguments["sourcefiles"])
2630

2731
if arguments.get("merge"):
2832
scp_merge(**arguments)

scpkit/src/merge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ def merge_json(json_blobs):
3030

3131

3232
def make_policies(content, readable, max_size: int = 5120):
33-
"""Combines the policies in order, counts the bytes, and starts a new file when it goes over the limit.
33+
"""Combines the policies in order, counts the characters, and starts a new file when it goes over the limit.
3434
Theres probably a better way to do this with permutations, but that could also be resource intensive.
3535
3636
Args:
3737
content (list): List of Sid dictionaries (in order of smallest to largest preferred)
38-
max_size (int, optional): Max byte count. Defaults to 5120.
38+
max_size (int, optional): Max character count. Defaults to 5120.
3939
Returns:
4040
list: List of condensed SCP documents.
4141
"""

scpkit/src/util.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import boto3
23
from pathlib import Path
34
from .model import SCP
45

@@ -112,4 +113,48 @@ def make_actions_and_resources_lists(content):
112113
# no such thing as NotResource in SCPs - https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_syntax.html#scp-elements-table
113114
if type(sid.get("Resource")) is not list:
114115
sid["Resource"] = [sid.get("Resource")]
115-
return content
116+
return content
117+
118+
119+
def create_session(profile=None):
120+
"""Creates a boto session
121+
122+
Args:
123+
profile (string): AWS profile name
124+
125+
Returns:
126+
[object]: Authenticated Boto3 session
127+
"""
128+
if profile:
129+
return boto3.Session(profile_name=profile)
130+
else:
131+
return boto3.Session()
132+
133+
134+
def create_client(session, service):
135+
"""Creates a service client from a boto session
136+
137+
Args:
138+
session (object): Authenicated boto3 session
139+
service (string): service name to create the client for
140+
141+
Returns:
142+
[object]: client session for specific aws service (eg. accessanalyzer)
143+
"""
144+
return session.client(service)
145+
146+
147+
def paginate(service, method, **method_args):
148+
"""Paginates through the results of a method.
149+
150+
Args:
151+
service (boto3.client): The AWS service client.
152+
method (str): The name of the method to paginate.
153+
method_args (dict): The arguments to pass to the method.
154+
155+
Returns:
156+
list: A list of paginated results.
157+
"""
158+
paginator = service.get_paginator(method)
159+
results = paginator.paginate(**method_args)
160+
return results

scpkit/src/validate.py

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,4 @@
1-
import boto3
2-
3-
def create_session(profile=None):
4-
"""Creates a boto session
5-
6-
Args:
7-
profile (string): AWS profile name
8-
9-
Returns:
10-
[object]: Authenticated Boto3 session
11-
"""
12-
if profile:
13-
return boto3.Session(profile_name=profile)
14-
else:
15-
return boto3.Session()
16-
17-
18-
def create_client(session, service):
19-
"""Creates a service client from a boto session
20-
21-
Args:
22-
session (object): Authenicated boto3 session
23-
service (string): service name to create the client for
24-
25-
Returns:
26-
[object]: client session for specific aws service (eg. accessanalyzer)
27-
"""
28-
return session.client(service)
1+
from .util import create_session, create_client
292

303

314
def validate_policies(scps, profile, outdir=None, console=False):

scpkit/src/visualize.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from graphviz import Digraph
2+
from .util import create_session, paginate
3+
4+
5+
def add_child_nodes(ou_id, org_client, graph):
6+
"""Adds child nodes to the graph.
7+
8+
Args:
9+
ou_id (str): The ID of the organizational unit.
10+
org_client (boto3.client): The AWS Organizations client.
11+
graph (Digraph): The Graphviz Digraph object.
12+
13+
"""
14+
accounts = list_children(org_client, ou_id, 'ACCOUNT')
15+
ous = list_children(org_client, ou_id, 'ORGANIZATIONAL_UNIT')
16+
17+
children = accounts + ous
18+
19+
if children:
20+
for child in children:
21+
child_id = child['Id']
22+
child_type = child['Type']
23+
24+
if child_type == 'ACCOUNT':
25+
account = org_client.describe_account(AccountId=child_id).get('Account')
26+
account_name = account.get('Name')
27+
account_id = account.get('Id')
28+
29+
graph.node(child_id, label=account_name, shape='ellipse')
30+
graph.edge(ou_id, child_id)
31+
32+
policies = get_policies_for_entity(account_id, org_client)
33+
34+
add_policies_to_graph(graph, child_id, policies=policies)
35+
36+
# Get the name of the child (OU or Account)
37+
if child_type == 'ORGANIZATIONAL_UNIT':
38+
current_ou = org_client.describe_organizational_unit(OrganizationalUnitId=child_id).get('OrganizationalUnit')
39+
ou_name = current_ou.get('Name')
40+
current_ou_id = current_ou.get('Id')
41+
42+
graph.node(child_id, label=ou_name, shape='box')
43+
graph.edge(ou_id, child_id)
44+
45+
policies = get_policies_for_entity(current_ou_id, org_client)
46+
add_policies_to_graph(graph, child_id, policies=policies)
47+
48+
add_child_nodes(child_id, org_client, graph)
49+
50+
51+
def list_children(org_client, parent_id, child_type):
52+
"""Lists the children of a parent entity.
53+
54+
Args:
55+
org_client (boto3.client): The AWS Organizations client.
56+
parent_id (str): The ID of the parent entity.
57+
child_type (str): The type of the child entities to list.
58+
59+
Returns:
60+
list: A list of child entities.
61+
"""
62+
all_children = paginate(org_client, 'list_children', ParentId=parent_id, ChildType=child_type)
63+
children = [ child for page in all_children for child in page.get('Children')]
64+
return children
65+
66+
67+
def get_policies_for_entity(entity_id, org_client, filter='SERVICE_CONTROL_POLICY'):
68+
"""Gets the policies associated with an entity.
69+
70+
Args:
71+
entity_id (str): The ID of the entity.
72+
org_client (boto3.client): The AWS Organizations client.
73+
filter (str): The filter to apply when retrieving policies.
74+
75+
Returns:
76+
list: A list of policies associated with the entity.
77+
"""
78+
policies = org_client.list_policies_for_target(
79+
TargetId=entity_id,
80+
Filter=filter
81+
)
82+
return policies.get('Policies')
83+
84+
85+
def add_policies_to_graph(graph, entity_id, policies=None):
86+
"""Adds policies to the graph.
87+
88+
Args:
89+
graph (Digraph): The Graphviz Digraph object.
90+
entity_id (str): The ID of the entity.
91+
policies (list): A list of policies to add to the graph.
92+
"""
93+
if policies:
94+
for policy in policies:
95+
policy_name = policy.get('Name')
96+
graph.node(policy_name, label=policy_name, shape='trapezium')
97+
graph.edge(entity_id, policy_name)
98+
99+
100+
def visualize_policies(profile, outdir):
101+
session = create_session(profile)
102+
org_client = session.client('organizations')
103+
104+
# Initialize a Graphviz Digraph object
105+
graph = Digraph('AWS_Organizations', graph_attr={'rankdir':'LR'})
106+
107+
# Get the root information
108+
root_id = org_client.list_roots()['Roots'][0]['Id']
109+
graph.node(root_id, label="Root", shape='box')
110+
get_policies_for_entity(root_id, org_client)
111+
add_policies_to_graph(graph, root_id)
112+
113+
# Start building the tree
114+
add_child_nodes(root_id, org_client, graph)
115+
116+
# Output the graphical tree hierarchy to a file
117+
graph.render(directory=outdir, filename='aws_org_tree', view=True)
118+

visualize-org.png

68 KB
Loading

0 commit comments

Comments
 (0)