DocumentationAutomation
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
from CommonServerPython import *
from CommonServerUserPython import *
import yaml # type: ignore
import sys
reload(sys)
sys.setdefaultencoding('utf-8') # pylint: disable=E1101
CMD_ARGS_REGEX = re.compile(r'([\w_-]+)=((\"[^"]+\")|(`.+`)|(\"\"\".+\"\"\")|([^ ]+)) ?', re.S)
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()
except (ValueError, yaml.YAMLError):
return_error('Failed to open integration file')
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 = dict([(k, v.strip('"`')) for k, v, _, _, _, _ in CMD_ARGS_REGEX.findall(args_str)])
return cmd, args
def run_command(command_example):
errors = []
context_example = ''
md_example = ''
cmd = command_example
try:
cmd, kwargs = extract_command(command_example)
res = demisto.executeCommand(cmd, kwargs)
for entry in res:
if is_error(entry):
demisto.results(res)
raise RuntimeError('something went wrong with your command: {}'.format(command_example))
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:
if entry.get('Contents') is not None:
content = entry.get('Contents')
if isinstance(content, STRING_TYPES):
md_example += content
else:
md_example += json.dumps(content)
else:
md_example += entry.get('HumanReadable')
except RuntimeError:
errors.append('The provided example for cmd {} has failed...'.format(cmd))
except Exception as e:
errors.append(
'Error encountered in the processing of command {}, error was: {}. '.format(cmd, str(e))
+ '. Please check your command inputs and outputs')
return cmd, md_example, context_example, errors
def add_lines(line):
output = re.findall(r'^\d+\..+', line, re.MULTILINE)
return output if output else [line]
def addErrorLines(scriptToScan, scriptType):
res = ''
if 'python' in scriptType:
errorKeys = ['return_error', 'raise ']
elif 'javascript' in scriptType:
errorKeys = ['throw ']
# Unsupported script type
else:
return res
linesToSkip = 0
scriptLines = scriptToScan.splitlines()
for idx in range(len(scriptLines)):
# Skip lines that were already scanned
if linesToSkip > 0:
linesToSkip -= 1
continue
line = scriptLines[idx]
if any(key in line for key in errorKeys):
if '(' in line:
bracketOpenIdx = line.index('(') + 1
if ')' in line:
bracketCloseIdx = line.index(')')
res += '* ' + line[bracketOpenIdx:bracketCloseIdx] + '\n'
# Handle multi line error
else:
res += '*' + ('' if len(line[bracketOpenIdx:].lstrip()) < 1 else ' ' + line[bracketOpenIdx:] + '\n')
while ')' not in scriptLines[idx + linesToSkip + 1]:
linesToSkip += 1
line = scriptLines[idx + linesToSkip]
res += ' ' + line.lstrip() + '\n'
# Adding last line of error
linesToSkip += 1
line = scriptLines[idx + linesToSkip]
bracketCloseIdx = line.index(')')
res += line[:bracketCloseIdx].lstrip() + '\n'
else:
firstMatchingErrorKey = next((key for key in errorKeys if key in line), False)
afterErrorKeyIdx = line.index(firstMatchingErrorKey) + len(firstMatchingErrorKey) # type: ignore
res += '* ' + line[afterErrorKeyIdx:] + '\n'
return res
def generate_section(title, data):
section = [
'## {}'.format(title),
'---',
'',
]
if data is not None:
section.extend(add_lines(data))
return section
# Setup integration on Demisto
def generate_setup_section(yaml_data):
section = [
'1. Navigate to __Settings__ > __Integrations__ > __Servers & Services__.',
'2. Search for {}.'.format(yaml_data['name']),
'3. Click __Add instance__ to create and configure a new integration instance.',
' * __Name__: a textual name for the integration instance.',
]
for conf in yaml_data['configuration']:
if conf['display']:
section.append(' * __{}__'.format(conf['display']))
else:
section.append(' * __{}__'.format(conf['name']))
section.append('4. Click __Test__ to validate the URLs, token, and connection.')
return section
# Commands
def generate_commands_section(yaml_data, example_dict):
errors = [] # type: list
section = [
'## Commands',
'---',
'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.'
]
commands = filter(lambda cmd: not cmd.get('deprecated', False), yaml_data['script']['commands'])
command_list = ['{}. {}'.format(i + 1, cmd['name']) for i, cmd in enumerate(commands)]
section.extend(command_list)
for i, cmd in enumerate(commands):
cmd_section, cmd_errors = generate_single_command_section(i, cmd, example_dict)
section.extend(cmd_section)
errors.extend(cmd_errors)
return section, errors
def generate_single_command_section(index, cmd, example_dict):
cmd_example = example_dict.get(cmd['name'])
errors = []
section = [
'### {}. {}'.format(index + 1, cmd['name']),
'---',
cmd.get('description', ' '),
'##### Required Permissions',
'**FILL IN REQUIRED PERMISSIONS HERE**',
'##### Base Command',
'',
'`{}`'.format(cmd['name']),
'##### Input',
'',
]
# Inputs
arguments = cmd.get('arguments')
if arguments is None:
section.append('There are no input arguments for this command.')
else:
section.extend([
'| **Argument Name** | **Description** | **Required** |',
'| --- | --- | --- |',
])
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'
section.append('| {} | {} | {} | '.format(arg['name'], stringEscapeMD(arg.get('description', ''), True, True),
required_status))
section.append('')
# Context output
section.extend([
'',
'##### Context Output',
'',
])
outputs = cmd.get('outputs')
if outputs is None:
section.append('There is no context output for this command.')
else:
section.extend([
'| **Path** | **Type** | **Description** |',
'| --- | --- | --- |'
])
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']))
section.append(
'| {} | {} | {} | '.format(output['contextPath'], output.get('type', 'unknown'),
output.get('description').encode('utf-8')))
section.append('')
# Raw output:
example_section, example_errors = generate_command_example(cmd, cmd_example)
section.extend(example_section)
errors.extend(example_errors)
return section, errors
def generate_command_example(cmd, cmd_example=None):
errors = []
context_example = None
md_example = ''
if cmd_example is not None:
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',
'```{}```'.format(cmd_example),
'',
]
if context_example:
example.extend([
'##### Context Example',
'```',
'{}'.format(context_example),
'```',
'',
])
example.extend([
'##### Human Readable Output',
'{}'.format(md_example),
'',
])
return example, errors
def main():
args = demisto.args()
yml_data = get_yaml_obj(args['entryID'])
command_examples, errors = get_command_examples(args.get('commands'))
example_dict, build_errors = build_example_dict(command_examples)
errors.extend(build_errors)
docs = [] # type: list
docs.extend(generate_section('Overview', args.get('overview', yml_data.get('description'))))
docs.append('This integration was integrated and tested with version xx of {}'.format(yml_data['name']))
# Playbooks
docs.extend(generate_section('{} Playbook'.format(yml_data['name']), None))
# Use-cases
docs.extend(generate_section('Use Cases', args.get('useCases')))
# 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.extend(generate_setup_section(yml_data))
# Fetched incidents data
docs.extend(generate_section('Fetched Incidents Data', args.get('fetchedData')))
# Commands
command_section, command_errors = generate_commands_section(yml_data, example_dict)
docs.extend(command_section)
errors.extend(command_errors)
# Additional info
docs.extend(generate_section('Additional Information', args.get('addInfo')))
# Known limitations
docs.extend(generate_section('Known Limitations', args.get('limit')))
# Troubleshooting
docs.extend(generate_section('Troubleshooting', args.get('troubleshooting')))
# Possible Errors
if args.get('withErrors', 'True') == 'True':
docs.extend([
'',
'## Possible Errors (DO NOT PUBLISH ON ZENDESK):',
])
docs.append(addErrorLines(yml_data['script']['script'], yml_data['script']['type']))
documentation_text = '\n'.join(docs)
filename = '{}-documentation.txt'.format(yml_data['name'])
demisto.results({
'Type': entryTypes['note'],
'ContentsFormat': formats['markdown'],
'Contents': documentation_text,
'HumanReadable': documentation_text,
})
demisto.results(fileResult(filename, documentation_text, file_type=entryTypes['entryInfoFile']))
if len(errors) != 0:
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__ == '__builtin__':
main()