HTMLDocsAutomation
Deprecated. The recommended way to generate documentation is via the demisto-sdk. See: https://xsoar.pan.dev/docs/integrations/integration-docs
- Type
- python
- Pack
- DeprecatedContent
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()