DocumentationAutomation 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
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()