Ayounsi has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/363296 )

Change subject: Add diffscan module.
......................................................................


Add diffscan module.

Bug: T169624
Change-Id: I19cf84c6e1a1a33af201ddef61eeba47c04cb949
---
M hieradata/labs.yaml
A modules/diffscan/files/diffscanpy
A modules/diffscan/manifests/init.pp
A modules/diffscan/templates/targets.txt.erb
A modules/profile/manifests/diffscan.pp
5 files changed, 570 insertions(+), 0 deletions(-)

Approvals:
  Muehlenhoff: Looks good to me, but someone else must approve
  jenkins-bot: Verified
  Ayounsi: Looks good to me, approved



diff --git a/hieradata/labs.yaml b/hieradata/labs.yaml
index 377d824..a30b000 100644
--- a/hieradata/labs.yaml
+++ b/hieradata/labs.yaml
@@ -116,3 +116,11 @@
   spice_hostname: 'labspice.wikimedia.org'
   scheduler_pool:
     - labvirt1001
+
+profile::diffscan::ipranges:
+  - 185.15.56.0/22
+  - 91.198.174.0/24
+  - 198.35.26.0/23
+  - 208.80.152.0/22
+profile::diffscan::emailto: [email protected]
+profile::diffscan::groupname: Labs-to-public-v4
diff --git a/modules/diffscan/files/diffscanpy 
b/modules/diffscan/files/diffscanpy
new file mode 100644
index 0000000..a35ae71
--- /dev/null
+++ b/modules/diffscan/files/diffscanpy
@@ -0,0 +1,479 @@
+#!/usr/bin/python2
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# Copyright (c) 2015 Mozilla Corporation
+# Author: [email protected]
+
+import sys
+import os
+import fcntl
+from string import Template
+import getopt
+import re
+import time
+import subprocess
+import tempfile
+import cPickle
+import errno
+import tempfile
+import shutil
+import calendar
+
+# Edit the nmap_scanoptions variable below to configure generic options
+# that are passed by nmap to the script. This script generates email using
+# the sendmail command, so also ensure that command is in your path when
+# it is run.
+
+# Change this to suit your environment
+#
+# Be sure to include -vv so hosts that are down are reported in the
+# output for correct tracking.
+nmap_scanoptions = '-vv -sS -PE -PS22,25,80,443,3306,8443,9100 -T4 ' + \
+        '--privileged'
+
+nmap_topports = Template('--top-ports $topports')
+nmap_logoptions = Template('-oG $tmppath')
+nmap_inoptions = Template('-iL $inpath')
+nmap_portspec = Template('-p $portspec')
+
+append_path = ':/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:' + \
+        '/usr/local/sbin:/usr/lib'
+
+class ScanData(object):
+    def __init__(self):
+        self.scantime = time.gmtime()
+        self.hosts = {}
+        self.dnsmap = {}
+        self.uphosts = []
+        self.downhosts = []
+
+    def get_hosts(self):
+        return self.hosts.keys()
+
+    def get_host_ports(self, h):
+        return self.hosts[h]
+
+    def open_exists(self, addr, port, proto):
+        if addr not in self.hosts:
+            return False
+        cand = [port, proto]
+        if cand not in self.hosts[addr]:
+            return False
+        return True
+
+    def total_services(self):
+        ret = 0
+        for i in self.hosts:
+            ret += len(self.hosts[i])
+        return ret
+
+    def add_open(self, addr, port, proto, hn):
+        if proto != 'tcp' and proto != 'udp':
+            raise Exception('unknown protocol %s' % proto)
+        if addr not in self.hosts:
+            self.hosts[addr] = []
+        self.dnsmap[addr] = hn
+        self.hosts[addr].append([int(port), proto])
+
+class Alert(object):
+    def __init__(self, host, port, proto, dns, open_prev, closed_prev,
+        statstr):
+        self.host = host
+        self.port = port
+        self.proto = proto
+        self.dns = dns
+        self.open_prev = open_prev
+        self.closed_prev = closed_prev
+        self.statstr = statstr
+
+    @staticmethod
+    def alert_header():
+        return 'STATUS HOST PORT PROTO OPREV CPREV DNS'
+
+    def __str__(self):
+        return '%s %s %s %s %s %s %s' % (self.statstr, self.host,
+            str(self.port), self.proto,
+            str(self.open_prev), str(self.closed_prev),
+            self.dns)
+
+class ScanState(object):
+    KEEP_SCANS = 7
+
+    def __init__(self):
+        self._lastscan = None
+        self._scanlist = []
+        self._alerts_open = []
+        self._alerts_closed = []
+        self._outfile = None
+
+    def up_trend(self):
+        ret = ''
+        for i in self._scanlist:
+            if len(ret) == 0:
+                ret = '%d' % len(i.uphosts)
+            else:
+                ret += ',%d' % len(i.uphosts)
+        return ret
+
+    def down_trend(self):
+        ret = ''
+        for i in self._scanlist:
+            if len(ret) == 0:
+                ret = '%d' % len(i.downhosts)
+            else:
+                ret += ',%d' % len(i.downhosts)
+        return ret
+
+    def register_outfile(self, o):
+        self._outfile = o
+
+    def clear_outfile(self):
+        self._outfile = None
+
+    def clear_alerts(self):
+        self._alerts_open = []
+        self._alerts_closed = []
+
+    def last_scan_total_services(self):
+        return self._lastscan.total_services()
+
+    def previous_scan_total_services(self):
+        if len(self._scanlist) > 1:
+            return self._scanlist[1].total_services()
+        return 0
+
+    def set_last(self, last):
+        self._lastscan = last
+        if len(self._scanlist) == self.KEEP_SCANS:
+            self._scanlist.pop()
+        self._scanlist.insert(0, last)
+        self.clear_alerts()
+
+    def calculate(self):
+        self.calculate_new_open()
+        self.calculate_new_closed()
+
+    def prev_service_status(self, addr, port, proto):
+        openprev = 0
+        closedprev = 0
+        if len(self._scanlist) <= 1:
+            return (0, 0)
+        for s in self._scanlist[1:]:
+            if s.open_exists(addr, port, proto):
+                openprev += 1
+            else:
+                closedprev += 1
+        return (openprev, closedprev)
+
+    def calculate_new_open(self):
+        if len(self._scanlist) <= 1:
+            return
+        for i in self._lastscan.get_hosts():
+            for p in self._lastscan.get_host_ports(i):
+                prevscan = self._scanlist[1]
+                if not prevscan.open_exists(i, p[0], p[1]):
+                    statstr = 'OPEN'
+                    dns = self._lastscan.dnsmap[i]
+                    # If this host isn't in the previous up or down list,
+                    # note it as a new host
+                    if (i not in prevscan.uphosts) and \
+                        (i not in prevscan.downhosts):
+                        statstr = 'OPENNEWHOST'
+                    openprev, closedprev = \
+                        self.prev_service_status(i, p[0], p[1])
+                    self._alerts_open.append(Alert(i, p[0], p[1], dns,
+                        openprev, closedprev, statstr))
+
+    def calculate_new_closed(self):
+        if len(self._scanlist) <= 1:
+            return
+        prevscan = self._scanlist[1]
+        for i in prevscan.get_hosts():
+            for p in prevscan.get_host_ports(i):
+                if not self._lastscan.open_exists(i, p[0], p[1]):
+                    statstr = 'CLOSED'
+                    # See if the host existed in the current scan, if it did
+                    # use that hostname, otherwise grab previous
+                    if i in self._lastscan.dnsmap:
+                        dns = self._lastscan.dnsmap[i]
+                    else:
+                        # If we didn't have a dns map entry for it, that means
+                        # the host wasn't even up, note this in the status
+                        statstr = 'CLOSEDDOWN'
+                        dns = prevscan.dnsmap[i]
+                    openprev, closedprev = \
+                        self.prev_service_status(i, p[0], p[1])
+                    self._alerts_closed.append(Alert(i, p[0], p[1], dns,
+                        openprev, closedprev, statstr))
+
+    def print_open_alerts(self):
+        self._outfile.write('%s\n' % Alert.alert_header())
+        for i in self._alerts_open:
+            self._outfile.write('%s\n' % str(i))
+
+    def print_closed_alerts(self):
+        self._outfile.write('%s\n' % Alert.alert_header())
+        for i in self._alerts_closed:
+            self._outfile.write('%s\n' % str(i))
+
+lockfile = None
+state = None
+tmpfile = None
+debugging = False
+myhost = None
+recip = None
+groupname = None
+topports = 2000
+portspec = None
+nosmtp = False
+
+statefile = './diffscan.state'
+outdir = './diffscan_out'
+
+def outdir_setup():
+    if not os.path.isdir(outdir):
+        os.mkdir(outdir, 0755)
+    if not os.access(outdir, os.W_OK):
+        sys.stderr.write('%s not writable\n' % outdir)
+        sys.exit(1)
+
+def copy_nmap_out(p):
+    tval = int(calendar.timegm(time.gmtime()))
+    pidval = os.getpid()
+    fname = os.path.join(outdir, 'nmap-%d-%d.out' % (tval, pidval))
+    shutil.copyfile(p, fname)
+
+def load_scanstate():
+    try:
+        f = open(statefile, 'r')
+    except IOError as e:
+        if e.errno == errno.ENOENT:
+            return ScanState()
+        else:
+            raise
+    ret = cPickle.load(f)
+    f.close()
+    return ret
+
+def write_scanstate():
+    f = open(statefile, 'w')
+    cPickle.dump(state, f)
+    f.close()
+
+def parse_output(path):
+    new = ScanData()
+
+    f = open(path, 'r')
+    while True:
+        buf = f.readline()
+        if buf == None:
+            break
+        if buf == '':
+            break
+        buf = buf.strip()
+        m = re.search('Host: (\S+) \(([^)]*)\).*Status: Up', buf)
+        if m != None:
+            addr = m.group(1)
+            new.uphosts.append(addr)
+        m = re.search('Host: (\S+) \(([^)]*)\).*Status: Down', buf)
+        if m != None:
+            addr = m.group(1)
+            new.downhosts.append(addr)
+        m = re.search('Host: (\S+) \(([^)]*)\).*Ports: (.*)$', buf)
+        if m != None:
+            addr = m.group(1)
+            hn = m.group(2)
+            if len(hn) == 0:
+                hn = 'unknown'
+            p = [x.split('/') for x in m.group(3).split(',')]
+            for i in p:
+                if i[1] != 'open':
+                    continue
+                new.add_open(addr.strip(), i[0].strip(), i[2].strip(), hn)
+    f.close()
+
+    state.set_last(new)
+
+def diffscan_fail_notify(errmsg):
+    if nosmtp:
+        return
+    buf = 'Subject: diffscan2 %s %s\n' % (groupname, myhost)
+    buf += 'From: diffscan2 <noreply@%s>\n' % myhost
+    buf += 'To: %s\n' % ','.join(recip)
+    buf += '\n'
+    buf += 'diffscan execution failed\n\n'
+    buf += '%s\n' % errmsg
+    sp = subprocess.Popen(['sendmail', '-t'], stdin=subprocess.PIPE)
+    sp.communicate(buf)
+
+def run_nmap(targets):
+    nmap_args = []
+    nmap_args += nmap_scanoptions.split()
+
+    tf = tempfile.mkstemp()
+    os.close(tf[0])
+    if portspec != None:
+        nmap_args += nmap_portspec.substitute(portspec=portspec).split()
+    else:
+        nmap_args += nmap_topports.substitute(topports=topports).split()
+    nmap_args += nmap_logoptions.substitute(tmppath=tf[1]).split()
+    nmap_args += nmap_inoptions.substitute(inpath=targets).split()
+
+    nfd = open('/dev/null', 'w')
+    try:
+        ret = subprocess.call(['nmap',] + nmap_args, stdout=nfd)
+    except Exception as e:
+        os.remove(tf[1])
+        diffscan_fail_notify('executing of nmap failed, %s' % str(e))
+        return False
+    nfd.close()
+
+    if ret != 0:
+        os.remove(tf[1])
+        diffscan_fail_notify('nmap failed with return code %d, exiting' % ret)
+        return False
+
+    parse_output(tf[1])
+
+    copy_nmap_out(tf[1])
+    os.remove(tf[1])
+    return True
+
+def usage():
+    sys.stdout.write('usage: diffscan.py [options] targets_file' \
+        ' recipients groupname\n\n' \
+        'options:\n\n' \
+        '\t-h\t\tusage information\n' \
+        '\t-m num\t\ttop ports to scan (2000, see nmap --top-ports)\n' \
+        '\t-n\t\tno smtp, write output to stdout (recipient ignored)\n' \
+        '\t-o path\t\tdirectory to save nmap output (./diffscan_out)\n' \
+        '\t-p spec\t\tinstead of top ports use port spec (see nmap -p)\n' \
+        '\t-s path\t\tpath to state file (./diffscan.state)\n\n')
+    sys.exit(0)
+
+def create_lock():
+    global lockfile
+
+    lfname = statefile + '.lock'
+    lockfile = open(lfname, 'w')
+    fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+    lockfile.write(str(os.getpid()))
+    lockfile.flush()
+
+def release_lock():
+    lfname = statefile + '.lock'
+
+    lockfile.close()
+    os.remove(lfname)
+
+def domain():
+    global statefile
+    global state
+    global outdir
+    global tmpfile
+    global debugging
+    global myhost
+    global recip
+    global groupname
+    global topports
+    global portspec
+    global nosmtp
+
+    os.environ['PATH'] = os.environ['PATH'] + append_path
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], 'dhm:no:p:s:')
+    except getopt.GetoptError:
+        usage()
+    for o, a in opts:
+        if o == '-h':
+            usage()
+        elif o == '-o':
+            outdir = a
+        elif o == '-p':
+            portspec = a
+        elif o == '-d':
+            debugging = True
+        elif o == '-m':
+            topports = a
+        elif o == '-n':
+            nosmtp = True
+        elif o == '-s':
+            statefile = a
+    if len(args) < 3:
+        usage()
+    targetfile = args[0]
+    recip = args[1].split(',')
+    groupname = args[2]
+
+    outdir_setup()
+
+    create_lock()
+
+    state = load_scanstate()
+
+    if not nosmtp:
+        tmpout = tempfile.mkstemp()
+        tmpfile = os.fdopen(tmpout[0], 'w')
+    else:
+        tmpfile = sys.stdout
+    state.register_outfile(tmpfile)
+
+    myhost = os.uname()[1]
+    tmpfile.write('Subject: diffscan2 %s %s\n' % (groupname, myhost))
+    tmpfile.write('From: diffscan2 <noreply@%s>\n' % myhost)
+    tmpfile.write('To: %s\n' % ','.join(recip))
+    tmpfile.write('\n')
+
+    tmpfile.write('diffscan2 results output\n\n')
+
+    if not run_nmap(targetfile):
+        if not nosmtp:
+            tmpfile.close()
+            os.remove(tmpout[1])
+        sys.exit(1)
+    state.calculate()
+    tmpfile.write('New Open Service List\n')
+    tmpfile.write('---------------------\n')
+    state.print_open_alerts()
+    tmpfile.write('\n')
+    tmpfile.write('New Closed Service List\n')
+    tmpfile.write('---------------------\n')
+    state.print_closed_alerts()
+
+    tmpfile.write('\n')
+    tmpfile.write('OPREV: number of times service was open in previous ' \
+        'scans\n')
+    tmpfile.write('CPREV: number of times service was closed in ' \
+        'previous scans\n')
+    tmpfile.write('maximum previous scans stored: %d\n' % state.KEEP_SCANS)
+    tmpfile.write('current total services: %d\n' % \
+        state.last_scan_total_services())
+    tmpfile.write('previous total services: %d\n' % \
+        state.previous_scan_total_services())
+    tmpfile.write('up trend: %s\n' % state.up_trend())
+    tmpfile.write('down trend: %s\n' % state.down_trend())
+
+    state.clear_outfile()
+    write_scanstate()
+
+    if not nosmtp:
+        tmpfile.close()
+
+        f = open(tmpout[1], 'r')
+        buf = f.read()
+        f.close()
+        if debugging:
+            sys.stdout.write(buf)
+        sp = subprocess.Popen(['sendmail', '-t'], stdin=subprocess.PIPE)
+        sp.communicate(buf)
+        os.remove(tmpout[1])
+
+    release_lock()
+
+if __name__ == '__main__':
+    domain()
+
+sys.exit(0)
diff --git a/modules/diffscan/manifests/init.pp 
b/modules/diffscan/manifests/init.pp
new file mode 100644
index 0000000..fa5aece
--- /dev/null
+++ b/modules/diffscan/manifests/init.pp
@@ -0,0 +1,51 @@
+# == Class: diffscan
+#
+# This class installs & manages diffscan,
+# an nmap wrapper for differential port scans.
+# See https://github.com/ameihm0912/diffscan2
+#
+# == Parameters
+#
+# [*ipranges*]
+#   The list of IP/masks to scan. See nmap doc for accepted formats.
+#
+# [*emailto*]
+#   Diff emails recipient. Defaults to "root".
+#
+# [*groupname*]
+#   An identifier to distinguish between several instances.
+#   Defaults to "diffscan".
+#
+class diffscan(
+    $ipranges={},
+    $emailto='[email protected]',
+    $groupname='diffscan'
+) {
+    file { '/srv/diffscan':
+        ensure => 'directory',
+        owner  => 'root',
+        group  => 'root',
+        mode   => '0775',
+    }
+    file { "/srv/diffscan/targets-${groupname}.txt":
+        ensure  => present,
+        owner   => 'root',
+        group   => 'root',
+        mode    => '0444',
+        content => template('diffscan/targets.txt.erb'),
+    }
+    file { '/srv/diffscan/diffscan.py':
+        ensure => present,
+        owner  => 'root',
+        group  => 'root',
+        mode   => '0554',
+        source => 'puppet:///modules/diffscan/diffscanpy',
+    }
+    cron { "diffscan-${groupname}":
+        ensure  => present,
+        user    => 'root',  # nmap needs root privileges
+        command => "/srv/diffscan/diffscan.py targets-${groupname}.txt 
${emailto} ${groupname}",
+        hour    => '0',
+    }
+
+}
diff --git a/modules/diffscan/templates/targets.txt.erb 
b/modules/diffscan/templates/targets.txt.erb
new file mode 100644
index 0000000..cec63fa
--- /dev/null
+++ b/modules/diffscan/templates/targets.txt.erb
@@ -0,0 +1,3 @@
+<% @ipranges.each do |iprange| %>
+<%= iprange %>
+<% end %>
diff --git a/modules/profile/manifests/diffscan.pp 
b/modules/profile/manifests/diffscan.pp
new file mode 100644
index 0000000..e9fb6fa
--- /dev/null
+++ b/modules/profile/manifests/diffscan.pp
@@ -0,0 +1,29 @@
+# == Class: diffscan
+#
+# This class installs & manages diffscan,
+# an nmap wrapper for differential port scans.
+# See https://github.com/ameihm0912/diffscan2
+#
+# == Parameters
+#
+# [*ipranges*]
+#   The list of IP/masks to scan. See nmap doc for accepted formats.
+#
+# [*emailto*]
+#   Diff emails recipient. Defaults to "root".
+#
+# [*groupname*]
+#   An identifier to distinguish between several instances.
+#   Defaults to "diffscan".
+#
+class profile::diffscan(
+  $ipranges=hiera('profile::diffscan::ipranges'),
+  $emailto=hiera('profile::diffscan::emailto'),
+  $groupname=hiera('profile::diffscan::groupname'),
+) {
+    class { '::diffscan':
+        ipranges  => $ipranges,
+        emailto   => $emailto,
+        groupname => $groupname,
+    }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/363296
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I19cf84c6e1a1a33af201ddef61eeba47c04cb949
Gerrit-PatchSet: 6
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Ayounsi <[email protected]>
Gerrit-Reviewer: Ayounsi <[email protected]>
Gerrit-Reviewer: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: Muehlenhoff <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to