jazzure 0.1.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.
jazzure/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from jazzure.config.ToolConfig import __version__
2
+
3
+ __title__ = "jazzure"
jazzure/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ try:
2
+ from jazzure.main import main
3
+ except ModuleNotFoundError as e:
4
+ print("Mandatory dependencies are missing:", e)
5
+ print("Please install them with pip3 install -r requirements.txt")
6
+ exit(1)
7
+
8
+ if __name__ == "__main__":
9
+ exit(main())
@@ -0,0 +1,20 @@
1
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
2
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
3
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
4
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
5
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
6
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
7
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
8
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
9
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
10
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
11
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
12
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
13
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
14
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
15
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
16
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
17
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
18
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
19
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
20
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
@@ -0,0 +1,55 @@
1
+ # General information
2
+
3
+ As python SDK is not suitable for Azure environment actions discovery, I decided to parse Azure CLI which is quite well when using CLI (but can trigger headache to parse it 😅)
4
+ The idea is to map every service and functions/actions we can interrogate to perform some upper discovery logic.
5
+
6
+ # Technical information
7
+
8
+ ## AZ CLI versions
9
+
10
+ There are several version of this tool and some older ones can reveal more information.
11
+ So there is a function `azcli.AzCLI.retrieve_online_versions()` that can list every documented versions.
12
+ This is not used yet in the tool but in the future I will try to handle them.
13
+
14
+ ## AZ CLI crawler
15
+
16
+ The recon of all `az` has been done in version `2.90.0`.
17
+ The class `azcli.AzureCliCrawler` handles the parsing of `az`. It's quite long as we need to parse recursively all CLI possibilities with the `--help` arg.
18
+ The function `crawl()` is the main entrance of parsing and added some asyncio strategy to avoid long time running, even though it takes about 40min to complete 😅
19
+ A file in JSON format is created and contains every mapping of `az` commands with all information (see bellow a little extract):
20
+ ```json
21
+ { "command": "az rest",
22
+ "subgroups": [],
23
+ "commands": [],
24
+ "arguments": [
25
+ {
26
+ "name": "--uri",
27
+ "aliases": [
28
+ "--url",
29
+ "-u"
30
+ ],
31
+ "required": true,
32
+ "special_mode": "",
33
+ "description": "Request URL. If it doesn't start with a host, CLI assumes it as an Azure resource ID and prefixes it with the ARM endpoint of the current cloud shown by `az cloud show --query endpoints.resourceManager`. Common token {subscriptionId} will be replaced with the current subscription ID specified by `az account set`."
34
+ },
35
+ {
36
+ "name": "--body",
37
+ "aliases": [
38
+ "-b"
39
+ ],
40
+ "required": false,
41
+ "special_mode": "",
42
+ "description": "Request body. Use @{file} to load from a file. For quoting issues in different terminals, see https://github.com/Azure/azure- cli/blob/dev/doc/use_cli_effectively.md#quoting-issues."
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ Technical bonus detail :
49
+ - While parsing the output I discovered that ANSI chars were dropped randomly in some help and caused my parsing to fails for no reason...
50
+ - As it was quite painful some unit tests have been written to ensure good parsing of `az` inside `tests/test_AZCLI.py`
51
+
52
+
53
+
54
+
55
+
File without changes
@@ -0,0 +1,346 @@
1
+ import asyncio
2
+ import json
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import List, Union, Any
6
+
7
+ import requests
8
+ from bs4 import BeautifulSoup
9
+ from R2Log import logger
10
+ from strip_ansi import strip_ansi
11
+
12
+ from jazzure.utils import binary_installed, add_suffix_to_filename
13
+
14
+ class AzCLI:
15
+
16
+ RELEASE_NOTES_URL = "https://learn.microsoft.com/en-us/cli/azure/release-notes-azure-cli?view=azure-cli-latest"
17
+
18
+ @staticmethod
19
+ def run(*args: str) -> Any:
20
+ cmd = ["az", *args, "--output", "json"]
21
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
22
+ if result.returncode != 0:
23
+ if "Insufficient privileges" in result.stderr:
24
+ logger.error(f"Insufficient privileges to perform : \n{' '.join(cmd)}")
25
+ return None
26
+ #raise RuntimeError(f"Azure CLI command failed:\n$ {' '.join(cmd)}\n\n{result.stderr}")
27
+ return result.returncode, result.stderr
28
+ if not result.stdout.strip():
29
+ return None
30
+ return json.loads(result.stdout)
31
+
32
+ @staticmethod
33
+ def retrieve_local_version() -> Union[None|str]:
34
+ res = None
35
+ result = subprocess.run(["az", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
36
+ if result.returncode != 0:
37
+ if "Insufficient privileges" in result.stderr:
38
+ logger.error(f"Insufficient privileges to perform : az version")
39
+ return res
40
+ if not result.stdout.strip():
41
+ return res
42
+ for line in result.stdout.splitlines():
43
+ if line.startswith("azure-cli"):
44
+ single_sep_line = " ".join(line.split()).strip()
45
+ res = single_sep_line.split()[-1]
46
+ break
47
+ return res
48
+
49
+ @classmethod
50
+ def retrieve_online_versions(cls):
51
+ res = []
52
+ r = requests.get(cls.RELEASE_NOTES_URL, timeout=30)
53
+ r.raise_for_status()
54
+
55
+ soup = BeautifulSoup(r.text, "html.parser")
56
+
57
+ for p in soup.find_all("p"):
58
+ if p.text.startswith("Version "):
59
+ res.append(p.text.split(' ')[-1])
60
+ logger.debug(f"Found all versions of az CLI : {','.join(res)}")
61
+ return res
62
+
63
+ @staticmethod
64
+ def is_installed() -> bool:
65
+ return binary_installed("az")
66
+
67
+ class AzureCliCrawler:
68
+
69
+ OUTPUT_FILE = Path(__file__).parent / "azure_cli_commands.json"
70
+
71
+ def __init__(self):
72
+ self.commands = {}
73
+ self.version = AzCLI.retrieve_local_version()
74
+ self.visited = set()
75
+ self.stats = { "groups": 0, "commands": 0, "errors": 0 }
76
+
77
+ @staticmethod
78
+ async def run_az_help(path) -> tuple[int, str]:
79
+ """
80
+ Execute : az <path> --help
81
+ :return return code & text from help
82
+ """
83
+ async with asyncio.Semaphore(10):
84
+ command = ["az"] + path + ["--help"]
85
+ # synchronous method
86
+ # proc = subprocess.run( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace")
87
+
88
+ #async method
89
+ proc = await asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
90
+ stdout, stderr = await proc.communicate()
91
+
92
+ return proc.returncode, stdout.decode()
93
+
94
+ @staticmethod
95
+ def get_sections(lines: str):
96
+ """
97
+ Transform help in dict :
98
+ {
99
+ "Subgroups": [...],
100
+ "Commands": [...],
101
+ "Arguments": [...],
102
+ "Examples": [...],
103
+ ...
104
+ }
105
+ """
106
+ sections = {}
107
+ current_section = None
108
+ current_lines = []
109
+
110
+ for line in lines.splitlines():
111
+ if line and line[0].isalpha():
112
+ # save previous part
113
+ if current_lines:
114
+ sections[current_section] = "\n".join(current_lines)
115
+ current_lines = []
116
+ current_section = line.strip().replace(':','')
117
+ elif current_section is not None:
118
+ current_lines.append(line)
119
+ # save previous part
120
+ if current_lines:
121
+ sections[current_section] = "\n".join(current_lines)
122
+ return sections
123
+
124
+ @staticmethod
125
+ def parse_section(content: Union[List[str] | str]) -> List[str]:
126
+ """
127
+ Generic parser for section content
128
+ """
129
+ result = []
130
+ content = content.splitlines() if isinstance(content, str) else content
131
+ # Find first non-empty line
132
+ first_line = next((line for line in content if line.strip()), None)
133
+ if first_line is None:
134
+ return []
135
+ command_indent = len(first_line) - len(first_line.lstrip())
136
+ commands = []
137
+ for line in content:
138
+ if not line.strip():
139
+ continue
140
+ indentation = len(line) - len(line.lstrip())
141
+ if indentation == command_indent:
142
+ # because Az CLI return some colors and ANSI strings, we need to remove it for effective parsing
143
+ command = strip_ansi(line.strip().split(":", 1)[0]).strip()
144
+ # remove every [Preview] or whatever
145
+ if "[" in command:
146
+ command = strip_ansi(command.split("[")[0]).strip()
147
+ commands.append(command)
148
+ return commands
149
+
150
+ @staticmethod
151
+ def parse_subgroups(content: str) -> List[str]:
152
+ """
153
+ Example :
154
+ Subgroups:
155
+ app : Manage applications.
156
+ credential: Manage credentials.
157
+ :return ["app", "credential"]
158
+ """
159
+ return AzureCliCrawler.parse_section(content=content.splitlines())
160
+
161
+ @staticmethod
162
+ def parse_commands(content: str) -> List[str]:
163
+ """
164
+ Example :
165
+ Commands:
166
+ create : Create something.
167
+ delete : Delete something.
168
+ :return ["create", "delete"]
169
+ """
170
+ return AzureCliCrawler.parse_section(content=content.splitlines())
171
+
172
+ @staticmethod
173
+ def parse_arguments(content: Union[List[str]|str]):
174
+ arguments = []
175
+ current = None
176
+
177
+ def save_current():
178
+ #TODO parse description to retrieve "Allowed values"
179
+ nonlocal current
180
+ if current is None:
181
+ return
182
+ current["description"] = " ".join( line.strip() for line in current["description"] if line.strip() )
183
+ arguments.append(current)
184
+ current = None
185
+
186
+ content = content.splitlines() if isinstance(content, str) else content
187
+
188
+ for line in content:
189
+ if not line.strip():
190
+ continue
191
+
192
+ # Number of spaces before text
193
+ indentation = len(line) - len(line.lstrip())
194
+ stripped = line.strip()
195
+
196
+ # Arg is a line beginning with "--" AND is not necessary indented
197
+ if stripped.startswith("--") and indentation < 20:
198
+ save_current()
199
+ # Separate options & description parts
200
+ if ":" in stripped:
201
+ option_part, description = stripped.split(":", 1)
202
+ else:
203
+ option_part = stripped
204
+ description = ""
205
+
206
+ tokens = option_part.split()
207
+
208
+ option_names = []
209
+ required = False
210
+ special_mode = ""
211
+
212
+ for token in tokens:
213
+ if token == "[Required]":
214
+ required = True
215
+ elif token == "[Preview]":
216
+ special_mode = "preview"
217
+ elif token == "[Deprecated]":
218
+ special_mode = "deprecated"
219
+ elif token.startswith("-"):
220
+ option_names.append(token)
221
+
222
+ if not option_names:
223
+ continue
224
+
225
+ current = {
226
+ "name": option_names[0],
227
+ "aliases": option_names[1:],
228
+ "required": required,
229
+ "special_mode": special_mode,
230
+ "description": [],
231
+ }
232
+
233
+ if description.strip():
234
+ current["description"].append(description.strip())
235
+
236
+ # Line that continue the description
237
+ elif current is not None:
238
+ current["description"].append(stripped)
239
+ save_current()
240
+ return arguments
241
+
242
+ @staticmethod
243
+ def parse_examples(lines: Union[List[str]|str]) -> List[str]:
244
+ """ Keep examples as text """
245
+ examples = []
246
+ current = []
247
+ lines = lines.splitlines() if isinstance(lines, str) else lines
248
+ for line in lines:
249
+ # Keep orginal indentation
250
+ if line.strip():
251
+ current.append(line.rstrip())
252
+ elif current:
253
+ examples.append("\n".join(current))
254
+ current = []
255
+ if current:
256
+ examples.append("\n".join(current))
257
+ return examples
258
+
259
+ @staticmethod
260
+ def parse_help(path: List[str], help_text: str) -> dict:
261
+ sections = AzureCliCrawler.get_sections(help_text)
262
+ subgroups = AzureCliCrawler.parse_subgroups(sections.get("Subgroups", ""))
263
+ commands = AzureCliCrawler.parse_commands(sections.get("Commands", ""))
264
+ arguments_id = AzureCliCrawler.parse_arguments(sections.get("Id Arguments", ""))
265
+ arguments = AzureCliCrawler.parse_arguments(sections.get("Arguments", ""))
266
+ global_policy_arguments = AzureCliCrawler.parse_arguments(sections.get("Global Policy Arguments", ""))
267
+ global_arguments = AzureCliCrawler.parse_arguments(sections.get("Global Arguments", ""))
268
+ # find maybe other types of args
269
+ other_args = []
270
+ for section_name in list(sections.keys()):
271
+ if not section_name in ["Subgroups", "Commands", "Arguments", "Id Arguments", "Global Policy Arguments", "Global Arguments"]:
272
+ other_args.extend(AzureCliCrawler.parse_arguments(sections.get(section_name, [])))
273
+
274
+ examples = AzureCliCrawler.parse_examples(sections.get("Examples", []))
275
+ return {
276
+ "path": path,
277
+ "command": "az " + " ".join(path),
278
+ "subgroups": subgroups,
279
+ "commands": commands,
280
+ "arguments": arguments,
281
+ "arguments_id": arguments_id,
282
+ "global_policy_arguments": global_policy_arguments,
283
+ "global_arguments": global_arguments,
284
+ "other_args" : other_args,
285
+ "examples": examples,
286
+ # Useful later to parse again without parsing az cli again
287
+ "raw_help": help_text
288
+ }
289
+
290
+ async def crawl(self, path: Union[None|List[str]] = None):
291
+ path = [] if path is None else path
292
+ path_tuple = tuple(path)
293
+
294
+ if path_tuple in self.visited:
295
+ return
296
+ self.visited.add(path_tuple)
297
+ command_string = ("az" if not path else "az " + " ".join(path) )
298
+ logger.info(f"{command_string}")
299
+ returncode, help_text = await AzureCliCrawler.run_az_help(path)
300
+ if returncode != 0:
301
+ logger.error(f"Error1 detected on command '{command_string}' return code {returncode}")
302
+ self.stats["errors"] += 1
303
+ return
304
+ parsed = AzureCliCrawler.parse_help(path, help_text)
305
+ self.stats["groups"] += 1
306
+
307
+ #async def process_command(command):
308
+ for command in parsed.get("commands", []):
309
+ command_path = path + [command]
310
+ command_string = ("az "+ " ".join(command_path))
311
+ logger.debug(f"[CMD] {command_string}")
312
+ returncode, command_help = await AzureCliCrawler.run_az_help(command_path)
313
+ # we should never hit this condition if parsing worked properly ! But left in case
314
+ if returncode != 0:
315
+ logger.error(f"Error2 detected on command '{command_string}' return code : {returncode}")
316
+ self.stats["errors"] += 1
317
+ self.commands[" ".join(command_path)] = {
318
+ "path": command_path,
319
+ "command": command_string,
320
+ "error": True,
321
+ "returncode": returncode,
322
+ "raw_help": command_help
323
+ }
324
+ continue
325
+ command_data = AzureCliCrawler.parse_help(command_path, command_help)
326
+ self.commands[" ".join(command_path)] = command_data
327
+ self.stats["commands"] += 1
328
+ # Subgroups
329
+ workers = [ asyncio.create_task(self.crawl(path + [subgroup])) for subgroup in parsed.get("subgroups", []) ]
330
+ await asyncio.gather(*workers)
331
+
332
+
333
+ def save(self, filename: Path = OUTPUT_FILE) -> None:
334
+ final_filename = add_suffix_to_filename(path=filename, custom=f"_{self.version}")
335
+ output = {
336
+ "metadata": {
337
+ "command_count": self.stats["commands"],
338
+ "group_count": self.stats["groups"],
339
+ "error_count": self.stats["errors"]
340
+ },
341
+ "commands": self.commands
342
+ }
343
+ with final_filename.open("w", encoding="utf-8") as f:
344
+ json.dump(output, f, indent=2, ensure_ascii=False)
345
+ logger.success(f"File saved inside : {final_filename}")
346
+
@@ -0,0 +1,6 @@
1
+ __version__ = "0.1.0"
2
+
3
+ class Tool_Config:
4
+
5
+ version: str = __version__
6
+
File without changes
File without changes
@@ -0,0 +1,11 @@
1
+
2
+
3
+ """
4
+ Microsoft doc : https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/managed-identities-status
5
+ Managed identities for Azure resources provide an automatically managed identity in Microsoft Entra ID, enabling secure, credential-free authentication to Azure services
6
+
7
+ Difference between identities types : https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview
8
+
9
+ """
10
+
11
+