HTMLDocsAutomation Deprecated
Deprecated. The recommended way to generate documentation is via the demisto-sdk. See: https://xsoar.pan.dev/docs/integrations/integration-docs
python · Deprecated Content (Deprecated)
Source
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 import yaml from CommonServerUserPython import * CMD_ARGS_REGEX = re.compile(r'([\w_-]+)=((?:\"[^"]+\")|(?:`.+`)|(?:\"\"\".+\"\"\")|(?:[^ ]+)) ?', re.S) """STRING TEMPLATES""" OVERVIEW: str = '''<p> {overview} </p> ''' SETUP_CONFIGURATION: str = '''<h2>Configure {integration_name} on Demisto</h2> <ol> <li>Navigate to <strong>Settings</strong> > <strong>Integrations</strong> > <strong>Servers & Services</strong>.</li> <li>Search for {integration_name}.</li> <li> Click <strong>Add instance</strong> to create and configure a new integration instance. <ul> <li><strong>Name</strong>: a textual name for the integration instance.</li> {params_list} </ul> </li> <li> Click <strong>Test</strong> to validate the new instance. </li> </ol> ''' PARAMS_LIST: str = ' <li><strong>{param}</strong></li>' COMMANDS_HEADER: str = '''<h2>Commands</h2> <p> You can execute these commands from the Demisto CLI, as part of an automation, or in a playbook. After you successfully execute a command, a DBot message appears in the War Room with the command details. </p> <ol> {command_list} </ol> ''' PERMISSIONS_HEADER: str = '''<h2>Permissions</h2> <p>The following permissions are required for all commands.</p> <ul> <li>permission 1</li> <li>permission 2</li> </ul>''' COMMAND_LIST: str = ' <li><a href="#{command}" target="_self">{command_hr}: {command}</a></li>' PERMISSIONS_PER_COMMAND: str = ''' <h5>Required Permissions</h5> <p>The following permissions are required for this command.</p> <ul> <li>permission 1</li> <li>permission 2</li> </ul>''' SINGLE_COMMAND: str = '''<h3 id="{command_hr}">{index}. {command_hr}</h3> <hr> <p>{command_description}</p> <h5>Base Command</h5> <p> <code>{command}</code> </p> {permissions} <h5>Input</h5> {arg_table} <p> </p> <h5>Context Output</h5> {context_table} <p> </p> <h5>Command Example</h5> <p> <code>{command_example}</code> </p> {context_example} <h5>Human Readable Output</h5> <p> {hr_example} <!-- remove the following comments to manually add an image: --> <!-- <a href="insert URL to your image" target="_blank" rel="noopener noreferrer"><img src="insert URL to your image" alt="image" width="749" height="412"></a> --> </p> ''' CONTEXT_EXAMPLE: str = '''<h5>Context Example</h5> <pre> {context} </pre>''' ARG_TABLE: str = '''<table style="width:750px" border="2" cellpadding="6"> <thead> <tr> <th> <strong>Argument Name</strong> </th> <th> <strong>Description</strong> </th> <th> <strong>Required</strong> </th> </tr> </thead> <tbody> {records} </tbody> </table> ''' ARG_RECORD: str = ''' <tr> <td>{name}</td> <td>{description}</td> <td>{required}</td> </tr>''' CONTEXT_TABLE: str = '''<table style="width:750px" border="2" cellpadding="6"> <thead> <tr> <th> <strong>Path</strong> </th> <th> <strong>Type</strong> </th> <th> <strong>Description</strong> </th> </tr> </thead> <tbody> {records} </tbody> </table> ''' CONTEXT_RECORD: str = ''' <tr> <td>{path}</td> <td>{type}</td> <td>{description}</td> </tr>''' HTML_GENERIC_TABLE: str = '''<table style="width:750px" border="2" cellpadding="6"> <thead> <tr> {headers} </tr> </thead> <tbody> {records} </tbody> </table> ''' GENERIC_HEADER: str = ' <th><strong>{}</strong></th>' GENERIC_RECORD: str = ''' <tr> {data_fields} </tr>''' def get_yaml_obj(entry_id): data = {} # type: dict try: yml_file_path = demisto.getFilePath(entry_id)['path'] with open(yml_file_path, 'r') as yml_file: data = yaml.safe_load(yml_file) if not isinstance(data, dict): raise ValueError('not a yml file') except (ValueError, yaml.YAMLError) as exception: return_error('Failed to open integration file: {}'.format(exception)) return data def get_command_examples(entry_id): """ get command examples from command file @param entry_id: an entry ID of a command file or the content of such file @return: a list of command examples """ commands = [] # type: list errors = [] # type: list if entry_id is None: return commands, errors if re.match(r'[\d]+@[\d\w-]+', entry_id) is not None: examples_path = demisto.getFilePath(entry_id)['path'] with open(examples_path, 'r') as examples_file: commands = examples_file.read().split('\n') else: demisto.debug('failed to open command file, tried parsing as free text') commands = entry_id.split('\n') demisto.debug('found the following commands:\n{}'.format('\n* '.join(commands))) return commands, errors def build_example_dict(command_examples): """ gets an array of command examples, run them one by one and return a map of {base command -> (example command, markdown, outputs)} Note: if a command appears more then once, run all occurrences but stores only the first. """ examples = {} # type: dict errors = [] # type: list for example in command_examples: if example.startswith('!'): cmd, md_example, context_example, cmd_errors = run_command(example[1:]) errors.extend(cmd_errors) if cmd not in examples: examples[cmd] = (example, md_example, context_example) return examples, errors def extract_command(cmd_example): cmd = cmd_example args = dict() # type: dict if ' ' in cmd_example: cmd, args_str = cmd_example.split(' ', 1) args = {k: v.strip('"`') for k, v in CMD_ARGS_REGEX.findall(args_str)} return cmd, args def run_command(command_example): errors: list = [] context_example: str = '' md_example: str = '' cmd, kwargs = extract_command(command_example) try: res = demisto.executeCommand(cmd, kwargs) if is_error(res): demisto.results(res) raise RuntimeError() except (ValueError, RuntimeError) as exception: errors.append( 'Error encountered in the processing of command `{}`, error was: {}. '.format(command_example, str(exception)) + 'Please check your command inputs and outputs.') # errors.append('The provided example for cmd {} has failed: {}'.format(cmd, str(exception))) else: for entry in res: raw_context = entry.get('EntryContext', {}) if raw_context is not None: context = {k.split('(')[0]: v for k, v in raw_context.items()} context_example += json.dumps(context, indent=4) if entry.get('HumanReadable') is None: content = entry.get('Contents') if content: if isinstance(content, str): md_example += content elif isinstance(content, bytes): md_example += content.decode('utf-8') else: md_example += json.dumps(content) else: md_example += entry.get('HumanReadable') return cmd, md_example, context_example, errors def to_html_table(headers: list, data: list): records: list = [] for data_record in data: records.append(GENERIC_RECORD.format(data_fields='\n'.join(' <td>{}</td>'.format(field) for field in data_record))) return HTML_GENERIC_TABLE.format(headers='\n'.join(GENERIC_HEADER.format(h) for h in headers), records='\n'.join(records)) def human_readable_example_to_html(hr_sample): table_regex = re.compile(r'(\|(.*\|)+\s\|(?:---\|)+\s((?:\|(?:.*\|)+\s?)+))') hr_html: list = [] while hr_sample: if hr_sample.startswith('#'): title = hr_sample if '\n' in hr_sample: title, hr_sample = hr_sample.split('\n', 1) else: hr_sample = '' heading_size = len(title) - len(title.lstrip('#')) hr_html.append('<h{0}>{1}</h{0}>'.format(heading_size, title[heading_size + 1:])) continue table = table_regex.match(hr_sample) if table: headers = table.group(2).split('|')[:-1] data = [fields.split('|')[1:-1] for fields in table.group(3).strip().split('\n')] hr_html.append(to_html_table(headers, data)) truncate = len(table.group(0)) hr_sample = hr_sample[truncate:] continue paragraph: list = [] while hr_sample and not hr_sample.startswith('#') and not hr_sample.startswith('|'): if '\n' in hr_sample: paragraph_line, hr_sample = hr_sample.split('\n', 1) else: paragraph_line, hr_sample = hr_sample, '' if paragraph_line: paragraph.append(paragraph_line) if paragraph: hr_html.append('<p>\n{}\n</p>'.format('\n'.join(paragraph))) return '\n'.join(hr_html) def generate_use_case_section(title, data): html_section = [ '<h2>{}</h2>'.format(title), ] if not data: data = '' if os.linesep in data: html_section.append('<ul>') html_section.extend('<li>{}</li>'.format(s) for s in data.split(os.linesep)) html_section.append('</ul>') else: html_section.append('<p>{}</p>'.format(data)) return html_section def generate_section(title, data): html_section = [ '<h2>{}</h2>'.format(title), ] if data: if '\n' in data: html_section.append('<ul>') html_section.extend('<li>{}</li>'.format(s) for s in data.split('\n')) html_section.append('</ul>') else: html_section.append('<p>{}</p>\n'.format(data)) return '\n'.join(html_section) # Setup integration on Demisto def generate_setup_section(yaml_data): params_list = [ PARAMS_LIST.format(param=conf['display'] if conf.get('display') else conf['name']) for conf in yaml_data.get('configuration', [])] return SETUP_CONFIGURATION.format(params_list='\n'.join(params_list), integration_name=yaml_data['name']) # Commands def generate_commands_section(yaml_data, example_dict, should_include_permissions): errors: list = [] command_sections: list = [] commands = [cmd for cmd in yaml_data['script']['commands'] if not cmd.get('deprecated')] command_list = [COMMAND_LIST.format(command_hr=cmd['description'].rstrip('.'), command=cmd['name']) for cmd in commands] for i, cmd in enumerate(commands): cmd_section, cmd_errors = generate_single_command_section(i + 1, cmd, example_dict, should_include_permissions) command_sections.append(cmd_section) errors.extend(cmd_errors) return (COMMANDS_HEADER.format(command_list='\n'.join(command_list)) + '\n'.join(command_sections)), errors def generate_single_command_section(index, cmd, example_dict, should_include_permissions): cmd_example: str = example_dict.get(cmd['name']) errors: list = [] template: dict = { 'index': index, 'command_hr': cmd['name'], 'command': cmd['name'], 'command_description': cmd.get('description', ' '), 'permissions': PERMISSIONS_PER_COMMAND if should_include_permissions else '', } # Inputs arguments: list = cmd.get('arguments') if arguments is None: template['arg_table'] = 'There are no input arguments for this command.' else: arg_table: list = [] for arg in arguments: if not arg.get('description'): errors.append( 'Error! You are missing description in input {} of command {}'.format(arg['name'], cmd['name'])) required_status = 'Required' if arg.get('required') else 'Optional' arg_table.append(ARG_RECORD.format(name=arg['name'], description=arg.get('description'), required=required_status)) template['arg_table'] = ARG_TABLE.format(records='\n'.join(arg_table)) # Context output outputs: list = cmd.get('outputs') if outputs is None: template['context_table'] = 'There are no context output for this command.' else: context_table: list = [] for output in outputs: if not output.get('description'): errors.append( 'Error! You are missing description in output {} of command {}'.format(output['contextPath'], cmd['name'])) context_table.append(CONTEXT_RECORD.format(path=output['contextPath'], type=output.get('type', 'unknown'), description=output.get('description'))) template['context_table'] = CONTEXT_TABLE.format(records='\n'.join(context_table)) # Raw output: example_template, example_errors = generate_command_example(cmd, cmd_example) template.update(example_template) errors.extend(example_errors) return SINGLE_COMMAND.format(**template), errors def generate_command_example(cmd, cmd_example=None): errors: list = [] context_example = None md_example: str = '' if cmd_example: cmd_example, md_example, context_example = cmd_example else: cmd_example = ' ' errors.append('did not get any example for {}. please add it manually.'.format(cmd['name'])) example = { 'command_example': cmd_example, 'hr_example': human_readable_example_to_html(md_example), 'context_example': CONTEXT_EXAMPLE.format(context=context_example) if context_example else '', } return example, errors def generate_html_docs(args, yml_data, example_dict, errors): docs: str = '' # Overview overview = (args.get('overview', yml_data.get('description')) + '\n\nThis integration was integrated and tested with version xx of {}'.format(yml_data['name'])) docs += OVERVIEW.format(overview=overview) # Playbooks docs += generate_section('{} Playbook'.format(yml_data['name']), 'Populate this section with relevant playbook names.') # Use Cases docs += generate_section('Use Cases', args.get('useCases', 'Use case 1\nUse case 2')) # Detailed Descriptions docs += generate_section('Detailed Description', yml_data.get('detaileddescription', 'Populate this section with the .md file contents for detailed description.')) # Fetch Data docs += generate_section('Fetch Incidents', args.get('fetchedData', 'Populate this section with Fetch incidents data')) # # Setup integration to work with Demisto # docs.extend(generate_section('Configure {} on Demisto'.format(yml_data['name']), args.get('setupOnIntegration'))) # Setup integration on Demisto docs += (generate_setup_section(yml_data)) # Permissions if args.get('permissions') == 'global': docs += PERMISSIONS_HEADER # Commands command_section, command_errors = generate_commands_section(yml_data, example_dict, args.get('permissions') == 'per-command') docs += command_section errors.extend(command_errors) # Additional info docs += generate_section('Additional Information', args.get('addInfo')) # Known limitations docs += generate_section('Known Limitations', args.get('limit')) # Troubleshooting docs += generate_section('Troubleshooting', args.get('troubleshooting')) return docs def main(): args: dict = demisto.args() yml_data: dict = get_yaml_obj(args.get('entryID')) command_examples, errors = get_command_examples(args.get('commands')) example_dict, build_errors = build_example_dict(command_examples) errors.extend(build_errors) docs: str = generate_html_docs(args, yml_data, example_dict, errors) filename = '{}-documentation.html'.format(yml_data['name']) demisto.results({ 'Type': entryTypes['note'], 'ContentsFormat': formats['html'], 'Contents': docs, # 'HumanReadable': docs, }) demisto.results(fileResult(filename, docs, file_type=entryTypes['entryInfoFile'])) if errors: errors.append('Visit the documentation page for more details: ' 'https://github.com/demisto/content/tree/master/docs/integration_documentation') return_error('\n'.join('* {}'.format(e) for e in errors)) if __name__ == 'builtins': main()