Nmap inventory (#32857)
* nmap inv plugin draft * more accurate regex * fix format
This commit is contained in:
parent
a4c82d5842
commit
5e2f215dd7
1 changed files with 158 additions and 0 deletions
158
lib/ansible/plugins/inventory/nmap.py
Normal file
158
lib/ansible/plugins/inventory/nmap.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: nmap
|
||||
plugin_type: inventory
|
||||
version_added: "2.5"
|
||||
short_description: Uses nmap to find hosts to target
|
||||
description:
|
||||
- Uses a YAML configuration file with a valid YAML extension.
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
- inventory_cache
|
||||
requirements:
|
||||
- nmap CLI installed
|
||||
options:
|
||||
address:
|
||||
description: Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.
|
||||
required: True
|
||||
exclude:
|
||||
description: list of addresses to exclude
|
||||
type: list
|
||||
ports:
|
||||
description: Enable/disable scanning for open ports
|
||||
type: boolean
|
||||
default: True
|
||||
ipv4:
|
||||
description: use IPv4 type addresses
|
||||
type: boolean
|
||||
default: True
|
||||
ipv6:
|
||||
description: use IPv6 type addresses
|
||||
type: boolean
|
||||
default: True
|
||||
notes:
|
||||
- At least one of ipv4 or ipv6 is required to be True, both can be True, but they cannot both be False.
|
||||
- 'TODO: add OS fingerprinting'
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
# inventory.config file in YAML format
|
||||
plugin: nmap
|
||||
strict: False
|
||||
network: 192.168.0.0/24
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
NAME = 'nmap'
|
||||
find_host = re.compile(r'^Nmap scan report for ([\w,.,-]+) \(([\w,.,:,\[,\]]+)\)')
|
||||
find_port = re.compile(r'^(\d+)/(\w+)\s+(\w+)\s+(\w+)')
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._nmap = None
|
||||
for path in os.environ.get('PATH').split(':'):
|
||||
candidate = os.path.join(path, 'nmap')
|
||||
if os.path.exists(candidate):
|
||||
self._nmap = candidate
|
||||
break
|
||||
|
||||
super(InventoryModule, self).__init__()
|
||||
|
||||
def verify_file(self, path):
|
||||
|
||||
valid = False
|
||||
if super(InventoryModule, self).verify_file(path):
|
||||
file_name, ext = os.path.splitext(path)
|
||||
|
||||
if not ext or ext in C.YAML_FILENAME_EXTENSIONS:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
def parse(self, inventory, loader, path, cache=False):
|
||||
|
||||
if self._nmap is None:
|
||||
raise AnsibleParserError('nmap inventory plugin requires the nmap cli tool to work')
|
||||
|
||||
super(InventoryModule, self).parse(inventory, loader, path, cache=cache)
|
||||
|
||||
self._read_config_data(path)
|
||||
|
||||
# setup command
|
||||
cmd = [self._nmap]
|
||||
if not self._options['ports']:
|
||||
cmd.append('-sP')
|
||||
|
||||
if self._options['ipv4'] and not self._options['ipv6']:
|
||||
cmd.append('-4')
|
||||
elif self._options['ipv6'] and not self._options['ipv4']:
|
||||
cmd.append('-6')
|
||||
elif not self._options['ipv6'] and not self._options['ipv4']:
|
||||
raise AnsibleParserError('One of ipv4 or ipv6 must be enabled for this plugin')
|
||||
|
||||
if self._options['exclude']:
|
||||
cmd.append('--exclude')
|
||||
cmd.append(','.join(self._options['exclude']))
|
||||
|
||||
cmd.append(self._options['address'])
|
||||
try:
|
||||
# execute
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise AnsibleParserError('Failed to run nmap, rc=%s: %s' % (p.returncode, to_native(stderr)))
|
||||
|
||||
# parse results
|
||||
host = None
|
||||
ip = None
|
||||
ports = []
|
||||
for line in stdout.splitlines():
|
||||
hits = self.find_host.match(line)
|
||||
if hits:
|
||||
if host is not None:
|
||||
self.inventory.set_variable(host, 'ports', ports)
|
||||
|
||||
# if dns only shows arpa, just use ip instead as hostname
|
||||
if hits.group(1).endswith('.in-addr.arpa'):
|
||||
host = hits.group(2)
|
||||
else:
|
||||
host = hits.group(1)
|
||||
|
||||
ip = hits.group(2)
|
||||
|
||||
if host is not None:
|
||||
# update inventory
|
||||
self.inventory.add_host(host)
|
||||
self.inventory.set_variable(host, 'ip', ip)
|
||||
ports = []
|
||||
continue
|
||||
|
||||
host_ports = self.find_port.match(line)
|
||||
if host is not None and host_ports:
|
||||
ports.append({'port': host_ports.group(1), 'protocol': host_ports.group(2), 'state': host_ports.group(3), 'service': host_ports.group(4)})
|
||||
continue
|
||||
|
||||
# TODO: parse more data, OS?
|
||||
|
||||
# if any lefotvers
|
||||
if host and ports:
|
||||
self.inventory.set_variable(host, 'ports', ports)
|
||||
|
||||
except Exception as e:
|
||||
raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)))
|
Loading…
Reference in a new issue