scripts/workbook_create.py (569 lines of code) (raw):

###################################################################### # # This script reads the checklist items from the latest checklist file # in Github (or from a local file) and generates an Azure Monitor # workbook in JSON format. # # Last updated: February 2023 # ###################################################################### import json import argparse import sys import os import requests import glob import uuid import re # Get input arguments parser = argparse.ArgumentParser(description='Generate Azure Monitor workbook from Azure Review Checklist') parser.add_argument('--checklist-file', dest='checklist_file', action='store', help='You can optionally supply a JSON file containing the checklist you want to dump to the Excel spreadsheet. Otherwise it will take the latest file from Github') parser.add_argument('--only-english', dest='only_english', action='store_true', default=False, help='if checklist files are specified, ignore the non-English ones and only generate a spreadsheet for the English version (default: False)') parser.add_argument('--find-all', dest='find_all', action='store_true', default=False, help='if checklist files are specified, find all the languages for the given checklists (default: False)') parser.add_argument('--technology', dest='technology', action='store', help='If you do not supply a JSON file with the checklist, you need to specify the technology from which the latest checklist will be downloaded from Github') parser.add_argument('--output-file', dest='output_file', action='store', help='You can optionally supply an Excel file where the checklist will be saved, otherwise it will be updated in-place') parser.add_argument('--output-path', dest='output_path', action='store', help='Folder where to store the results (using the same name as the input_file)') parser.add_argument('--blocks-path', dest='blocks_path', action='store', help='Folder where the building blocks to build the workbook are stored)') parser.add_argument('--create-arm-template', dest='create_arm_template', action='store_true', default=True, help='create an ARM template, additionally to the workbook JSON (default: False)') parser.add_argument('--category', dest='category', action='store', help='if the workbook should be restricted to a category containing the specified string') parser.add_argument('--query-size', dest='query_tile_size', action='store', help='size of the tiles containing the query results. Valid values are: tiny, small, medium (default: medium)') parser.add_argument('--counters', dest='counters', action='store_true', default=False, help='Whether compliance counters will be included in the workbook. Note that workbooks generated this way usually incur in ARG throttling limits. Default is False.') parser.add_argument('--tab-counters', dest='tab_counters', action='store_true', default=False, help='Whether compliance counters will be included in the workbook tabs. Note that workbooks generated this way usually incur in ARG throttling limits. Default is False.') parser.add_argument('--verbose', dest='verbose', action='store_true', default=False, help='run in verbose mode (default: False)') args = parser.parse_args() checklist_file = args.checklist_file technology = args.technology block_workbook = None block_link = None block_section = None block_query = None block_text = None # Set the query tile size if args.query_tile_size: if args.query_tile_size == 'tiny': query_size = 4 elif args.query_tile_size == 'small': query_size = 1 elif args.query_tile_size == 'medium': query_size = 0 else: if args.verbose: print("ERROR: invalid value for query-size: {0}".format(args.query_tile_size)) sys.exit(1) else: if args.verbose: print("DEBUG: Setting query tile size to medium (default)") query_size = 0 # Workbook building blocks def load_building_blocks(): # Define the blocks as global variables global block_workbook global block_link global block_section global block_query global block_text global block_invisible_parameter global block_arm # Set folder where to load from if args.blocks_path: blocks_path = args.blocks_path if args.verbose: print ("DEBUG: Setting building block folder to {0}".format(blocks_path)) else: print("ERROR: please use the argument --blocks-path to specify the location of the workbook building blocks.") sys.exit(1) # Load initial workbook building block block_file = os.path.join(blocks_path, 'block_workbook.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_workbook = json.load(f) except Exception as e: print("ERROR: Error when opening JSON workbook building block", block_file, "-", str(e)) sys.exit(0) # Load link building block block_file = os.path.join(blocks_path, 'block_link.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_link = json.load(f) except Exception as e: print("ERROR: Error when opening JSON workbook building block", block_file, "-", str(e)) sys.exit(0) # Load itemgroup (aka section, aka tab) building block if args.tab_counters: block_file = os.path.join(blocks_path, 'block_itemgroup_withcounters.json') else: block_file = os.path.join(blocks_path, 'block_itemgroup.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_section = json.load(f) except Exception as e: print("ERROR: Error when opening JSON workbook building block", block_file, "-", str(e)) sys.exit(0) # Load query building block block_file = os.path.join(blocks_path, 'block_query.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_query = json.load(f) except Exception as e: print("ERROR: Error when opening JSON workbook building block", block_file, "-", str(e)) sys.exit(0) # Load text building block block_file = os.path.join(blocks_path, 'block_text.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_text = json.load(f) except Exception as e: print("ERROR: Error when opening JSON workbook building block", block_file, "-", str(e)) sys.exit(0) # Load invisible parameter building block block_file = os.path.join(blocks_path, 'block_invisible_parameter.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_invisible_parameter = json.load(f) except Exception as e: print("ERROR: Error when opening JSON workbook building block", block_file, "-", str(e)) sys.exit(0) # Load ARM template building block block_file = os.path.join(blocks_path, 'block_arm.json') if args.verbose: print ("DEBUG: Loading file {0}...".format(block_file)) try: with open(block_file) as f: block_arm = json.load(f) except Exception as e: print("ERROR: Error when opening JSON ARM template building block", block_file, "-", str(e)) sys.exit(0) # Function that corrects format issues in the queries stored in JSON def fix_query_format(query_string): if query_string: query_string = str(query_string).replace('\\\\', '\\') # Replace a double escaping inverted bar ('\\\\') through a single one ('\') return query_string else: return None # Function that transforms a JSON string to be included in an ARM template def serialize_data(workbook_string): if workbook_string: # Escape double quotes workbook_string = str(workbook_string).replace('"', '\"') # Escape escape characters # workbook_string = str(workbook_string).replace('\\', '\\\\') # Undo the scaping for the newline character (otherwise the markdown in the workbook would look wrong). # Note that this might impact newline characters in queries! # workbook_string = str(workbook_string).replace('\\\\n', '\\n') return workbook_string else: return None # Returns the index of a specific item in the workbook content def workbook_item_index(workbook, item_name): for i in range(len(workbook['items'])): if workbook['items'][i]['name'] == item_name: if args.verbose: print('DEBUG: Found item {0} in workbook at index {1}'.format(item_name, i)) return i if args.verbose: print('ERROR: I could not find item {0} in workbook'.format(item_name)) return -1 # Returns the index of a specific item inside of a tab (item group) def tab_item_index(tab_object, item_name): for i in range(len(tab_object['content']['items'])): if tab_object['content']['items'][i]['name'] == item_name: if args.verbose: print('DEBUG: Found item {0} in workbook object {1} at index {2}'.format(item_name, tab_object['name'], i)) return i if args.verbose: print('ERROR: I could not find item {0} in workbook object {1}'.format(item_name, tab_object['name'])) return -1 # Main function to generate the workbook JSON def generate_workbook(output_file, checklist_data): # Initialize an empty workbook workbook = json.loads(json.dumps(block_workbook)) workbook_title = "## " + checklist_data['metadata']['name'] if args.category: workbook_title += ' - ' + args.category[0].upper() + args.category[1:] workbook_title += "\n\n---\n\nThis workbook has been automatically generated out of the checklists in the [Azure Review Checklists repo](https://github.com/Azure/review-checklists). This repo contains best practices and recommendations around generic Landing Zones as well as specific services such as Azure Virtual Desktop, Azure Kubernetes Service or Azure VMware Solution, to name a few. This repository of best practices is curated by Azure engineers, but open to anybody to contribute." workbook_title += "\n\nIf you see a problem in the queries that are part of this workbook, please open a Github issue [here](https://github.com/Azure/review-checklists/issues/new)." markdown_index = workbook_item_index(workbook, 'MarkdownHeader') workbook['items'][markdown_index]['content']['json'] = workbook_title # If not using counters in the main section, we can change some things in the workbook if not args.counters: if args.verbose: print("DEBUG: removing sections from workbook. Before removing, {0} items exist".format(len(workbook['items']))) # Setting width of markdown to 100% to avoid the counters to be displayed on the right workbook['items'][markdown_index]['customWidth'] = '100' # Deleting invisible parameter and tile items hidden_parameter_index = workbook_item_index(workbook, 'InvisibleParameters') workbook['items'].pop(hidden_parameter_index) tile_index = workbook_item_index(workbook, 'ProgressTile') workbook['items'].pop(tile_index) if args.verbose: print("DEBUG: removing sections from workbook. After removing, {0} items exist".format(len(workbook['items']))) # Decide whether we will match in the category, or subcategory, and update the corresponding variables if args.category: if args.verbose: print("DEBUG: creating tab list with subcategories list for categories containing the term {0}...".format(args.category)) tab_name_field = 'subcategory' tab_title_list = [x["subcategory"] for x in checklist_data.get("items") if (args.category.lower() in str(x["category"]).lower())] tab_title_list = list(set(tab_title_list)) checklist_items = [x for x in checklist_data.get("items") if (args.category.lower() in str(x["category"]).lower())] if args.verbose: print("DEBUG: {0} items match category {1}".format(str(len(checklist_items)), args.category)) else: if args.verbose: print("DEBUG: creating tab list with categories...") tab_name_field = 'category' tab_title_list = [x["name"] for x in checklist_data.get("categories")] checklist_items = checklist_data.get("items") if args.verbose: print("DEBUG: {0} items found in checklist".format(str(len(checklist_items)))) if args.verbose: print("DEBUG: created tab list: {0}".format(str(tab_title_list))) # Remove the cats/subcats without queries defined total_expected_queries = 0 tabs_to_remove = [] for tab_title in tab_title_list: if args.category: items_with_query = [x['guid'] for x in checklist_items if ((str(x['subcategory']) == tab_title) and ('graph' in x.keys()) and ('guid' in x.keys()))] else: items_with_query = [x['guid'] for x in checklist_items if ((x['category'] == tab_title) and ('graph' in x.keys()))] if args.verbose: print("DEBUG: Items with query matching subcategory {0}: {1}, length {2}".format(tab_title, str(items_with_query), str(len(items_with_query)))) if len(items_with_query) == 0: if args.verbose: print("DEBUG: Removing tab {0} from list, it doesn't seem to have any graph queries defined".format(tab_title)) tabs_to_remove.append(tab_title) else: total_expected_queries += len(items_with_query) if args.verbose: # print("DEBUG: Incrementing total_expected queries (type {0}) by {1} to {2}".format(str(type(total_expected_queries)), str(len(items_with_query)), str(total_expected_queries))) print("DEBUG: Leaving tab {0} in tab list, it has {1} graph queries".format(tab_title, str(len(items_with_query)))) for tab_title in tabs_to_remove: tab_title_list.remove(tab_title) # If verbose show the final tab list: if args.verbose: print("DEBUG: final tab list: {0}".format(str(tab_title_list))) print("DEBUG: {0} expected queries".format(str(total_expected_queries))) # Bidimensional array to hold the graphs queries (x=>tab_index, y=>query_index) queries=[] for tab_title in tab_title_list: queries.append([]) # Generate one tab in the workbook for each category/subcategory tab_id = 0 query_id = 0 tab_dict = {} links_index = workbook_item_index(workbook, 'Tabs') for tab_title in tab_title_list: tab_dict[tab_title] = tab_id # We will use this dict later to know where to put each query if args.verbose: print("DEBUG: Adding tab {0} to workbook...".format(tab_title)) # Create new link new_link = block_link.copy() new_link['id'] = str(uuid.uuid4()) # RANDOM GUID # The tab title depends if we are generating counters in the main section or not if args.counters: new_link['linkLabel'] = tab_title + ' ({Tab' + str(tab_id) + 'Success:value}/{Tab' + str(tab_id) + 'Total:value})' else: new_link['linkLabel'] = tab_title new_link['subTarget'] = 'tab' + str(tab_id) new_link['preText'] = tab_title # Create new section new_section = block_section.copy() new_section = json.loads(json.dumps(new_section.copy())) new_section['name'] = 'tab' + str(tab_id) new_section['conditionalVisibility']['value'] = 'tab' + str(tab_id) tab_title_index = tab_item_index(new_section, 'TabTitle') new_section['content']['items'][tab_title_index]['content']['json'] = "## " + tab_title new_section['content']['items'][tab_title_index]['name'] = 'tab' + str(tab_id) + 'title' # Add link to the main section in the workbook workbook['items'][links_index]['content']['links'].append(new_link.copy()) # I am getting crazy with Python variable references :( # Add section (group) workbook['items'].append(new_section) tab_id += 1 # Display dictionary in screen if on verbose if args.verbose: print("DEBUG: tab dictionary generated: {0}".format(str(tab_dict))) # We will keep track of which query goes to which tab in a dictionary query_id_dictionary = {} for tab_title in tab_title_list: query_id_dictionary[tab_title] = [] # For each checklist item, add a query to the workbook for item in checklist_data.get("items"): # We will append this to every query query_suffix = ' | extend onlyFailed = {OnlyFailed:label} | where compliant == 0 or not (onlyFailed == 1) | project-away onlyFailed' # Invisible parameter query suffix invisible_parameter_query_suffix = "| summarize Total = count(), Success = countif(compliant==1), Failed = countif(compliant==0) | extend SuccessPercent = iff(Total==0, 100, 100*toint(Success)/toint(Total)) | extend FullyCompliant = iff(SuccessPercent == 100, 'Yes', 'No') | project Query1Stats=tostring(pack_all())" # Read variables from JSON guid = item.get("guid") tab = item.get(tab_name_field) text = item.get("text") description = item.get("description") severity = item.get("severity") link = item.get("link") training = item.get("training") graph_query = fix_query_format(item.get("graph")) if graph_query: if tab in tab_title_list: if args.verbose: print("DEBUG: Adding sections to workbook for ARG query '{0}', length of query is {1}".format(str(graph_query), str(len(str(graph_query))))) # Add query ID to the dictionary query_id_dictionary[tab].append(query_id) # Create new text new_text = block_text.copy() new_text['name'] = 'querytext' + str(query_id) new_text['content']['json'] = text.strip(' ').strip('.') if link: new_text['content']['json'] += ". Check [this link](" + link + ") for further information." if training: new_text['content']['json'] += ". [This training](" + training + ") can help to educate yourself on this." # Create new query new_query = block_query.copy() new_query['name'] = 'query' + str(query_id) new_query['content']['query'] = graph_query + query_suffix new_query['content']['size'] = query_size # Add text and query to the workbook if args.counters: tab_id = tab_dict[tab] + len(block_workbook['items']) else: # If not using counters, we removed two sections... tab_id = tab_dict[tab] + len(block_workbook['items']) - 2 if args.verbose: print ("DEBUG: Adding text and query to tab ID {0} ({1} -> {2}) of {3} elements in workbook".format(str(tab_id), tab, tab_dict[tab], len(workbook['items']))) print ("DEBUG: Workbook object name is {0}".format(workbook['items'][tab_id]['name'])) new_new_text = json.loads(json.dumps(new_text.copy())) new_new_query = json.loads(json.dumps(new_query.copy())) workbook['items'][tab_id]['content']['items'].append(new_new_text) workbook['items'][tab_id]['content']['items'].append(new_new_query) # If using tab counters, add the hidden parameters to the workbook tab if args.tab_counters: tab_counter_index = tab_item_index(workbook['items'][tab_id], 'TabInvisibleParameters') # Add parameter with 'QueryStats' new_parameter = block_invisible_parameter.copy() new_parameter['query'] = graph_query + invisible_parameter_query_suffix new_parameter['name'] = 'Query' + str(query_id) + 'Stats' new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][tab_id]['content']['items'][tab_counter_index]['content']['parameters'].append(new_new_parameter) # Add parameter with 'QueryFullyCompliant new_parameter = block_invisible_parameter.copy() new_parameter['query'] = "{\"version\":\"1.0.0\",\"content\":\"{\\\"value\\\": \\\"{Query" + str(query_id) + "Stats:$.FullyCompliant}\\\"}\",\"transformers\":null}" new_parameter['queryType'] = 8 new_parameter.pop('crossComponentResources', None) # This key is only used for ARG-based queries new_parameter.pop('resourceType', None) new_parameter['name'] = "Query{0}FullyCompliant".format(query_id) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][tab_id]['content']['items'][tab_counter_index]['content']['parameters'].append(new_new_parameter) # If using global counters, add the hidden parameters to the workbook main section if args.counters: hidden_parameter_index = workbook_item_index(workbook, 'InvisibleParameters') # Add parameter with 'QueryStats' new_parameter = block_invisible_parameter.copy() new_parameter['query'] = graph_query + invisible_parameter_query_suffix new_parameter['name'] = 'Query' + str(query_id) + 'Stats' new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Add parameter with 'QueryFullyCompliant new_parameter = block_invisible_parameter.copy() new_parameter['query'] = "{\"version\":\"1.0.0\",\"content\":\"{\\\"value\\\": \\\"{Query" + str(query_id) + "Stats:$.FullyCompliant}\\\"}\",\"transformers\":null}" new_parameter['queryType'] = 8 new_parameter.pop('crossComponentResources', None) # This key is only used for ARG-based queries new_parameter.pop('resourceType', None) new_parameter['name'] = "Query{0}FullyCompliant".format(query_id) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Add query to the query array tab_id = tab_title_list.index(tab) queries[tab_id].append(graph_query) # Increment query counter query_id += 1 # The fact that a query is not in the list is normal if doing the workbook for a specific category (such as Networking) # else: # if args.verbose: # print("ERROR: Query {0} in section {1}, but section not in the section list!".format(graph_query, tab)) # If using tab counters, we need to add the invisible parameters for the total for each tab if args.tab_counters or args.counters: hidden_parameter_index = workbook_item_index(workbook, 'InvisibleParameters') wb_success_sum_query = '' wb_total_sum_query = '' if args.verbose: print("DEBUG: Adding tab counters to all tabs, here the query_id_dictionary: {0}...".format(str(query_id_dictionary))) for tab in query_id_dictionary: query_id_list = query_id_dictionary[tab] # Get the index for this tab in the workbook if args.tab_counters: if args.counters: tab_id = tab_dict[tab] + len(block_workbook['items']) else: # If not using counters, we removed two sections... tab_id = tab_dict[tab] + len(block_workbook['items']) - 2 if args.verbose: print("DEBUG: Getting the index in tab (workbook item {0}) for the item 'TabInvisibleParameters'...".format(tab_id)) tab_counter_index = tab_item_index(workbook['items'][tab_id], 'TabInvisibleParameters') # Debug if args.verbose: print("DEBUG: Adding tab counters (index in tab {3}) to Tab{0} {1} with index {2}...".format(tab_dict[tab], tab, tab_id, tab_counter_index)) # Add parameter for 'Section Success' new_parameter = block_invisible_parameter.copy() new_parameter['name'] = "Tab{0}Success".format(tab_dict[tab]) tab_success_sum_query = '' for tab_query_id in query_id_dictionary[tab]: if len(tab_success_sum_query) > 0: tab_success_sum_query += '+' tab_success_sum_query += '{Query' + str(tab_query_id) + 'Stats:$.Success}' if len(wb_success_sum_query) > 0: wb_success_sum_query += '+' wb_success_sum_query += '{Query' + str(tab_query_id) + 'Stats:$.Success}' new_parameter['criteriaData'] = [{"criteriaContext": {"operator": "Default", "resultValType": "expression", "resultVal": tab_success_sum_query }}] new_parameter.pop('crossComponentResources', None) new_parameter.pop('resourceType', None) new_parameter.pop('query', None) new_parameter.pop('queryType', None) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) if args.tab_counters: workbook['items'][tab_id]['content']['items'][tab_counter_index]['content']['parameters'].append(new_new_parameter) if args.counters: workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Add parameter for 'Section Total' new_parameter = block_invisible_parameter.copy() new_parameter['name'] = "Tab{0}Total".format(tab_dict[tab]) tab_total_sum_query = '' for tab_query_id in query_id_dictionary[tab]: if len(tab_total_sum_query) > 0: tab_total_sum_query += '+' tab_total_sum_query += '{Query' + str(tab_query_id) + 'Stats:$.Total}' if len(wb_total_sum_query) > 0: wb_total_sum_query += '+' wb_total_sum_query += '{Query' + str(tab_query_id) + 'Stats:$.Total}' new_parameter['criteriaData'] = [{"criteriaContext": {"operator": "Default", "resultValType": "expression", "resultVal": tab_total_sum_query }}] new_parameter.pop('crossComponentResources', None) new_parameter.pop('resourceType', None) new_parameter.pop('query', None) new_parameter.pop('queryType', None) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) if args.tab_counters: workbook['items'][tab_id]['content']['items'][tab_counter_index]['content']['parameters'].append(new_new_parameter) if args.counters: workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Add parameter for 'Section Percent' new_parameter = block_invisible_parameter.copy() new_parameter['name'] = "Tab{0}Percent".format(tab_dict[tab]) new_parameter['criteriaData'] = [{"criteriaContext": {"operator": "Default", "resultValType": "expression", "resultVal": 'round(100*{Tab' + str(tab_dict[tab]) + 'Success}/{Tab' + str(tab_dict[tab]) + 'Total})' }}] new_parameter.pop('crossComponentResources', None) new_parameter.pop('resourceType', None) new_parameter.pop('query', None) new_parameter.pop('queryType', None) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) if args.tab_counters: workbook['items'][tab_id]['content']['items'][tab_counter_index]['content']['parameters'].append(new_new_parameter) if args.counters: workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Adjust tile to use the percent parameter if args.tab_counters: tab_percent_tile_index = tab_item_index(workbook['items'][tab_id], 'TabPercentTile') if args.verbose: print("DEBUG: Adjusting tile (index in tab {3}) to use the percent parameter for Tab{0} {1} with index {2}...".format(tab_dict[tab], tab, tab_id, tab_percent_tile_index)) workbook['items'][tab_id]['content']['items'][tab_percent_tile_index]['content']['query'] = "{\"version\":\"1.0.0\",\"content\":\"{\\\"Column1\\\": \\\"{Tab" + str(tab_dict[tab]) + "Percent}\\\", \\\"Column2\\\": \\\"Percent of successful checks\\\"}\",\"transformers\":null}" # After going through the tabs, if we still need to add the total parameters to the workbook header: if args.counters: # Total new_parameter = block_invisible_parameter.copy() new_parameter['name'] = "WorkbookTotal" new_parameter['criteriaData'] = [{"criteriaContext": {"operator": "Default", "resultValType": "expression", "resultVal": wb_total_sum_query }}] new_parameter.pop('crossComponentResources', None) new_parameter.pop('resourceType', None) new_parameter.pop('query', None) new_parameter.pop('queryType', None) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Success new_parameter = block_invisible_parameter.copy() new_parameter['name'] = "WorkbookSuccess" new_parameter['criteriaData'] = [{"criteriaContext": {"operator": "Default", "resultValType": "expression", "resultVal": wb_success_sum_query }}] new_parameter.pop('crossComponentResources', None) new_parameter.pop('resourceType', None) new_parameter.pop('query', None) new_parameter.pop('queryType', None) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Percent new_parameter = block_invisible_parameter.copy() new_parameter['name'] = "WorkbookPercent" new_parameter['criteriaData'] = [{"criteriaContext": {"operator": "Default", "resultValType": "expression", "resultVal": 'round(100*{WorkbookSuccess}/{WorkbookTotal})' }}] new_parameter.pop('crossComponentResources', None) new_parameter.pop('resourceType', None) new_parameter.pop('query', None) new_parameter.pop('queryType', None) new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Configure the percentile index wb_percent_tile_index = workbook_item_index(workbook, 'ProgressTile') if args.verbose: print("DEBUG: Adjusting tile (index in tab {0}) to use the percent parameters...".format(wb_percent_tile_index)) workbook['items'][wb_percent_tile_index]['content']['query'] = "{\"version\":\"1.0.0\",\"content\":\"{\\\"WorkbookPercent\\\": \\\"{WorkbookPercent}\\\", \\\"SubTitle\\\": \\\"Percent of successful checks\\\"}\",\"transformers\":null}" workbook['items'][wb_percent_tile_index]['content']['queryType'] = 8 workbook['items'][wb_percent_tile_index]['content'].pop('crossComponentResources', None) workbook['items'][wb_percent_tile_index]['content'].pop('resourceType', None) # Store the number of queries processed in its own variable (will be checked when deciding whether generating output or not) num_of_queries = query_id if num_of_queries != total_expected_queries: if args.verbose: print('WARNING: Something is not quite right, I was expecting to process {0} queries, but I found {1}'.format(str(total_expected_queries), str(num_of_queries))) # If generating workbook with detailed counters # Disabling this code for now, global counters to be added in previous block if args.counters and False: # Add invisible parameters to the workbook with number of success and total items if args.verbose: print("DEBUG: Adding hidden parameters to workbook main section for {0} tabs...".format(str(len(queries)))) tab_id = 0 hidden_parameter_index = workbook_item_index(workbook, 'InvisibleParameters') for tab_title in tab_title_list: print("DEBUG: Adding hidden parameters for tab {0} - {1}, with {2} queries".format(str(tab_id), tab_title, str(len(queries[tab_id])))) # We shouldn't have any tabs without queries, but still... if len(queries[tab_id]) > 0: query_id = 0 summary_query = queries[tab_id][query_id] while query_id + 1 < len(queries[tab_id]): query_id += 1 summary_query += "| union ({0})".format(queries[tab_id][query_id]) success_query = summary_query + '| where compliant == 1 | summarize Total = tostring(count())' total_query = summary_query + '| summarize Total = tostring(count())' # Add parameter with Total elements new_parameter = block_invisible_parameter.copy() new_parameter['query'] = total_query new_parameter['name'] = 'Section' + str(tab_id) + 'Total' new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Add parameter with Success elements new_parameter = block_invisible_parameter.copy() new_parameter['query'] = success_query new_parameter['name'] = 'Section' + str(tab_id) + 'Success' new_new_parameter = json.loads(json.dumps(new_parameter.copy())) workbook['items'][hidden_parameter_index]['content']['parameters'].append(new_new_parameter) # Move to the next query tab_id += 1 # We can now adapt the query of the success percent tile tile_index = workbook_item_index(workbook, 'ProgressTile') tab_id = 0 total_formula = '' success_formula = '' for tab_title in tab_title_list: if len(total_formula) > 1: total_formula += '+' total_formula += '{Section' + str(tab_id) + 'Total:value}' if len(success_formula) > 1: success_formula += '+' success_formula += '{Section' + str(tab_id) + 'Success:value}' tab_id += 1 progress_query = 'resources | summarize count() | extend Total = ' + total_formula + ', Success = ' + success_formula + ' | extend SuccessPercent = round(toreal(Success)/toreal(Total), 2) * 100, SubTitle = \'Percent of compliant resources\'' workbook['items'][tile_index]['content']['query'] = progress_query # Dump the workbook to the output file or to console, if there was any query in the original checklist if args.verbose: print ("DEBUG: Starting output process to {0}...".format(output_file)) if num_of_queries > 0: if output_file: # Dump workbook JSON into a file workbook_string = json.dumps(workbook, indent=4) with open(output_file, 'w', encoding='utf-8') as f: f.write(workbook_string) f.close() # Create ARM template (optionally, if specified in the parameters) if args.create_arm_template: arm_output_file = os.path.splitext(output_file)[0] + '_template.json' if args.verbose: print ("DEBUG: Creating ARM template in file {0}...".format(arm_output_file)) block_arm['parameters']['workbookDisplayName']['defaultValue'] = checklist_data['metadata']['name'] if args.category: block_arm['parameters']['workbookDisplayName']['defaultValue'] += ' - ' + args.category[0].upper() + args.category[1:] block_arm['resources'][0]['properties']['serializedData'] = serialize_data(workbook_string) arm_string = json.dumps(block_arm, indent=4) with open(arm_output_file, 'w', encoding='utf-8') as f: f.write(arm_string) f.close() else: print(workbook_string) else: print("INFO: sorry, the analyzed checklist did not contain any graph query") def get_output_file(checklist_file_or_url, is_file=True): # If output file specified, use it if args.output_file: return args.output_file # Else, figure out one elif args.output_path: # First come up with an initial filename, depending if provided a file or an URL if is_file: output_file = os.path.basename(checklist_file_or_url) else: output_file = checklist_file_or_url.split('/')[-1] # Get filename without path and extension output_file = os.path.join(args.output_path, output_file) # If category specified, add to output file name if args.category: output_file = os.path.splitext(output_file)[0] + '_' + str(args.category).lower() + '.json' # If counters created, add 'counters' to output file name if args.counters: output_file = os.path.splitext(output_file)[0] + '_counters.json' # Return the final file name return os.path.splitext(output_file)[0] + '_workbook.json' ######## # Main # ######## # First thing of all, load the building blocks load_building_blocks() if args.verbose: print ("DEBUG: building blocks variables initialized:") print ("DEBUG: - Workbook: {0}".format(str(block_workbook))) print ("DEBUG: - Number of items: {0}".format(str(len(block_workbook['items'])))) print ("DEBUG: - Link: {0}".format(str(block_link))) print ("DEBUG: - Query: {0}".format(str(block_query))) # Download checklist or process from local file if checklist_file: checklist_file_list = checklist_file.split(" ") # Take only the English versions of the checklists (JSON files) checklist_file_list = [file[:-8] + '.en.json' for file in checklist_file_list if (os.path.splitext(file)[1] == '.json')] # Remove duplicates checklist_file_list = list(set(checklist_file_list)) # Go over the list(s) for checklist_file in checklist_file_list: if args.verbose: print("DEBUG: Opening checklist file", checklist_file) # Get JSON try: # Open file with open(checklist_file) as f: checklist_data = json.load(f) # Set output file variable output_file = get_output_file(checklist_file, is_file=True) # Generate workbook generate_workbook(output_file, checklist_data) # If error, just continue except Exception as e: print("ERROR: Error when processing JSON file", checklist_file, "-", str(e)) # sys.exit(0) else: # If no input files specified, fetch the latest from Github... if technology: checklist_url = "https://raw.githubusercontent.com/Azure/review-checklists/main/checklists/" + technology + "_checklist.en.json" else: checklist_url = "https://raw.githubusercontent.com/Azure/review-checklists/main/checklists/alz_checklist.en.json" if args.verbose: print("DEBUG: Downloading checklist file from", checklist_url) response = requests.get(checklist_url) # If download was successful if response.status_code == 200: if args.verbose: print ("DEBUG: File {0} downloaded successfully".format(checklist_url)) try: # Deserialize JSON to object variable checklist_data = json.loads(response.text) except Exception as e: print("Error deserializing JSON content: {0}".format(str(e))) sys.exit(1) # Set output files output_file = get_output_file(checklist_url, is_file=False) # Generate workbook generate_workbook(output_file, checklist_data)