This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository mum. See http://git.chorem.org/mum.git commit a804ac50ea506c40df18c2d8155143ba019f9c25 Author: Alexis Guilbaud <guilbaud@codelutin.com> Date: Fri Jan 23 15:32:47 2015 +0100 Initial project with python and angularjs --- app/app.py | 108 ++++++++++++++++++++++++++++++ app/detection_modules/Nagios_detection.py | 1 + app/detection_modules/SNMP_detection.py | 1 + app/detection_modules/SSH_detection.py | 100 +++++++++++++++++++++++++++ bootstrap.sh | 4 ++ bower.json | 8 +++ in-venv.sh | 7 ++ package.json | 28 ++++++++ requirements.txt | 4 ++ res.xml | 80 ++++++++++++++++++++++ run.sh | 3 + static/css/main.css | 35 ++++++++++ static/js/DetectCtrl.js | 44 ++++++++++++ static/js/controllers/app.js | 27 ++++++++ views/angular.html | 20 ++++++ views/index.html | 27 ++++++++ 16 files changed, 497 insertions(+) diff --git a/app/app.py b/app/app.py new file mode 100755 index 0000000..40910b6 --- /dev/null +++ b/app/app.py @@ -0,0 +1,108 @@ +__author__ = 'aguilbaud' + +from bottle import * +from xml.dom import minidom +import pexpect +import json + + +@route('/') +def index(section='home'): + return template('index') + +@route('/angular') +def angular(): + return template('angular') + +@post('/detect') +def detection(): + ip_range = request.forms.get('ip_range') + #Verification de la syntaxe de la plage d'entree + #TODO Il faudrait egalement verifier que la plage n'est pas trop grande, et que les valeurs soient <=255 + #=> p-e faire un appel pour chaque plage de 10 (voire 1 appel pour 1 IP) + if re.search('\d{1,3}(-\d{1,3})?:\d{1,3}(-\d{1,3})?:\d{1,3}(-\d{1,3})?:\d{1,3}(-\d{1,3})?', ip_range) \ + or ip_range == 'localhost': + # option -v3 permet d'avoir une preview du scan avec services up ou down, mais "desorganise" le xml... + child = pexpect.spawn('nmap', ['-A', ip_range, '-oX', 'res.xml']) + res = '' + try: + while child.isalive(): + child.expect('Completed', timeout=None) + res += child.before + '<br/>' + except pexpect.EOF: + res += ' A FINI' + parse_res() + except pexpect.TIMEOUT: + res += ' TIMEOUT' + return res + else: + return '<p>La plage d\'IP est mal formatee</p>' + + +def parse_res(): + # Ouverture du fichier xml avec le parseur minidom + root = minidom.parse("res.xml") + collection = root.documentElement + + # Recuperer tous les <host> de la collection + hosts = collection.getElementsByTagName("host") + + # Recuperation des noeuds de chaque <host> et affichage de leur attributss + # JSON = liste de dictionnaires + for host in hosts: + status = host.getElementsByTagName('status')[0] + address = host.getElementsByTagName('address')[0] + + dict_host = {} + dict_host['addr'] = address.getAttribute('addr') + dict_host['date'] = host.getAttribute('endtime') + dict_host['state'] = status.getAttribute('state') + dict_host['os'] = 'unknown' # par defaut + + hostnames_elem = host.getElementsByTagName('hostnames')[0] + hostnames = hostnames_elem.getElementsByTagName('hostname') + dict_hostname = {} + for hostname in hostnames: + dict_host['hostname'] = hostname.getAttribute("name") + + ports_elem = host.getElementsByTagName('ports')[0] + ports = ports_elem.getElementsByTagName('port') + list_dict_port = [] + for port in ports: + dict_port = {} + state = port.getElementsByTagName('state')[0] + service = port.getElementsByTagName('service')[0] + if service.hasAttribute("ostype"): + dict_host['os'] = service.getAttribute("ostype") + if state.getAttribute('state') == 'open': + dict_port['portid'] = port.getAttribute('portid') + dict_port['portname'] = service.getAttribute('name') + # Ajouter d'autres infos ? + list_dict_port.append(dict_port) + dict_host['openports'] = list_dict_port + # sauvegarde de l'host dans la base elasticsearch avec pour ID son IP + print dict_host['addr'] + pexpect.run('curl -XPUT \'localhost:9200/host/external/' + dict_host['addr'] + '?pretty\' -d \'' + + json.dumps(dict_host, sort_keys=True,indent=4, separators=(',', ': ')) + '\'') + +@error(404) +def error404(error): + return '<h1>Cette page n\'existe pas</h1>' + + +@get('/static/<filepath:path>') +def static(filepath): + return static_file(filepath, root='static') + +@get('/bower_components/<filepath:path>') +def bower_files(filepath): + return static_file(filepath, root='bower_components') + +@get('/getval') +def getVal(): + return "toto" + + +if __name__ == '__main__': + port = int(os.environ.get('PORT', 1337)) + run(host='0.0.0.0', port=port, debug=True, reloader=True) diff --git a/app/detection_modules/Nagios_detection.py b/app/detection_modules/Nagios_detection.py new file mode 100644 index 0000000..fcb43f2 --- /dev/null +++ b/app/detection_modules/Nagios_detection.py @@ -0,0 +1 @@ +__author__ = 'aguilbaud' diff --git a/app/detection_modules/SNMP_detection.py b/app/detection_modules/SNMP_detection.py new file mode 100644 index 0000000..fcb43f2 --- /dev/null +++ b/app/detection_modules/SNMP_detection.py @@ -0,0 +1 @@ +__author__ = 'aguilbaud' diff --git a/app/detection_modules/SSH_detection.py b/app/detection_modules/SSH_detection.py new file mode 100644 index 0000000..76f133c --- /dev/null +++ b/app/detection_modules/SSH_detection.py @@ -0,0 +1,100 @@ +import paramiko +import json + +# il faudrait p-e separer le traitement du resultat des commandes avec le lancement en lui mm +# si independant des protocoles utilises + +def connect(): + # TODO rendre la connection dynamique + key = paramiko.RSAKey.from_private_key_file("/home/aguilbaud/.ssh/id_rsa") + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect('127.0.0.1', username='aguilbaud', pkey=key) + + disconnect(ssh) + + +def disconnect(ssh): + ssh.close() + + +def run_detection(ssh): + detect_hardware(ssh) + detect_drives(ssh) + detect_os_version(ssh) + detect_non_updated_packages(ssh) + + +# Informations materielles globales +def detect_hardware(ssh): + cmd = "lshw -json" + stdin, stdout, stderr = ssh.exec_command(cmd) + res = "" + for line in stdout.read().splitlines(): + res += line + res_json = json.loads(res) + # TODO Traitement du resultat pour garder l'essentiel ? + return res_json + + +# Informations sur les partitions +def detect_drives(ssh): + cmd = "lsblk -r --output=NAME,SIZE,TYPE,MOUNTPOINT" + stdin, stdout, stderr = ssh.exec_command(cmd) + dict_total = {} + i = 1 + ignore = True + for line in stdout.read().splitlines(): + # On ignore la premiere ligne qui ne contient pas de valeurs + if ignore: + ignore = False + else: + dict_drive = {} + tab_elem = line.split() + dict_drive["name"] = tab_elem[0] + dict_drive["size"] = tab_elem[1] + dict_drive["type"] = tab_elem[2] + if len(tab_elem) > 3: + dict_drive["mountpoint"] = tab_elem[3] + else: + dict_drive["mountpoint"] = "none" + # meilleur nom pour chaque attribut ? + dict_total[dict_drive["name"]] = dict_drive + i = i + 1 + return json.dumps(dict_total) + + +def detect_os_version(ssh): + # kernel + cmd = "cat /proc/version" + stdin, stdout, stderr = ssh.exec_command(cmd) + res = stdout.read() + dict_total = {} + dict_total["kernel"] = res.split('#')[0] + + # os + cmd = "cat /etc/os-release" + stdin, stdout, stderr = ssh.exec_command(cmd) + for line in stdout.read().splitlines(): + tab_elem = line.split("=") + # pour retirer les "" sur tous les champs qui en possedent + tab_right = tab_elem[1].split('"') + if len(tab_right) == 1: + dict_total[str.lower(tab_elem[0])] = tab_right[0] + else: + dict_total[str.lower(tab_elem[0])] = tab_right[1] + # encore une fois, on recupere tout le contenu de la commande, p-e qu'il est possible d'enlever le superflu + return json.dumps(dict_total, indent=4, separators=(',', ': ')) + + +# dependant de la langue du systeme ? +def detect_non_updated_packages(ssh): + cmd = "apt-get upgrade -s" + stdin, stdout, stderr = ssh.exec_command(cmd) + res = stdout.read() + tab_res = res.split(':') + if len(tab_res) == 2: + return json.dumps({'non_updated_packages': False}) + else: + return json.dumps({'non_updated_packages': True}) \ No newline at end of file diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..f0b958e --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +venv/bin/python --version >/dev/null 2>&1 || virtualenv venv +./in-venv.sh pip install -r requirements.txt diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..8b8ed45 --- /dev/null +++ b/bower.json @@ -0,0 +1,8 @@ +{ + "name": "Mum", + "version": "0.0.1", + "dependencies": { + "bootstrap": "~3.2.0", + "angular-latest": "~1.3.9" + } +} diff --git a/in-venv.sh b/in-venv.sh new file mode 100755 index 0000000..9a7793e --- /dev/null +++ b/in-venv.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cmd="$1" +shift +. venv/bin/activate +export PYTHONPATH=$PWD/lib:$PYTHONPATH +exec "$cmd" "$@" diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c7d231 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "Mum", + "version": "1.0.0", + "description": "Simple online monitoring", + "main": "index.js", + "scripts": { + "test": "test_mum" + }, + "repository": { + "type": "git", + "url": "https://git.chorem.org/mum.git" + }, + "keywords": [ + "monitoring", + "web", + "angular", + "python", + "bottle", + "detection", + "nmap", + "modules" + ], + "author": "aguilbaud", + "license": "GPL", + "devDependencies": { + "grunt": "^0.4.5" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bd14cb8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +bottle==0.12.7 +pysnmp +pexpect +paramiko \ No newline at end of file diff --git a/res.xml b/res.xml new file mode 100644 index 0000000..1a97e6b --- /dev/null +++ b/res.xml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="file:///usr/bin/../share/nmap/nmap.xsl" type="text/xsl"?> +<!-- Nmap 6.40 scan initiated Wed Jan 21 17:17:28 2015 as: /usr/bin/nmap -A -oX res.xml localhost --> +<nmaprun scanner="nmap" args="/usr/bin/nmap -A -oX res.xml localhost" start="1421857048" startstr="Wed Jan 21 17:17:28 2015" version="6.40" xmloutputversion="1.04"> +<scaninfo type="connect" protocol="tcp" numservices="1000" services="1,3-4,6-7,9,13,17,19-26,30,32-33,37,42-43,49,53,70,79-85,88-90,99-100,106,109-111,113,119,125,135,139,143-144,146,161,163,179,199,211-212,222,254-256,259,264,280,301,306,311,340,366,389,406-407,416-417,425,427,443-445,458,464-465,481,497,500,512-515,524,541,543-545,548,554-555,563,587,593,616-617,625,631,636,646,648,666-668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800-801,808,843,873,880,888,898,900-9 [...] +<verbose level="0"/> +<debugging level="0"/> +<host starttime="1421857048" endtime="1421857172"><status state="up" reason="conn-refused" reason_ttl="0"/> +<address addr="127.0.0.1" addrtype="ipv4"/> +<hostnames> +<hostname name="localhost" type="user"/> +<hostname name="localhost" type="PTR"/> +</hostnames> +<ports><extraports state="closed" count="994"> +<extrareasons reason="conn-refused" count="994"/> +</extraports> +<port protocol="tcp" portid="22"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="ssh" product="OpenSSH" version="6.6.1p1 Debian 4~bpo70+1" extrainfo="protocol 2.0" ostype="Linux" method="probed" conf="10"><cpe>cpe:/a:openbsd:openssh:6.6.1p1</cpe><cpe>cpe:/o:linux:linux_kernel</cpe></service><script id="ssh-hostkey" output="1024 91:dc:b9:10:b4:5c:21:bd:9d:53:53:03:58:96:18:1f (DSA) 2048 11:aa:0d:50:8d:a4:f2:43:7e:86:b2:02:e9:29:ca:e2 (RSA) 256 cf:22:d5:4b:e4:48 [...] +<elem key="type">ssh-dss</elem> +<elem key="key">AAAAB3NzaC1kc3MAAACBANd0aGmv9wNOWbaMqW5VbHc5ybC7FSyg+8feAPUAkwOyLs1lw2X/VUbstHVjCDbSLfChtrh5apIq+z+zbBZS90qJSmjkS0ENKRV5pTG5op4iMDHEdKtxvoEo+6cUo4LiTgdD/TG7ckuVbu98oJDG0Ug+2Y6UNcJ2JdFnhiOxHOJhAAAAFQDE8Uvj6YjrnI+hT/fwHR9m1GW2pQAAAIAa6juP1IrZJYy1dzumFlGh3IL2FhPtR+jalO9Nd5Fbl5vFL/Qvn7AH87jTLUEUFEXyxwEDXD09rDhgXRpqOWPY68EOax4ElCvP3VcMEomrTwgTyxqTfkYiPfuEsrRYAlA+nTgK0ydXx4mgLfk0igHkNR0Ec8Njq7TVpV6ahgT19wAAAIA0FxIqx9VTOLQ1bmBDGlcdcAwLfDhWYqJJBrVtClf2s1kqcv7K4u6atbeOo/lCHHBMYS0h [...] +<elem key="bits">1024</elem> +<elem key="fingerprint">91dcb910b45c21bd9d5353035896181f</elem> +</table> +<table> +<elem key="type">ssh-rsa</elem> +<elem key="key">AAAAB3NzaC1yc2EAAAADAQABAAABAQDcc3P+5Izgre8YSBrDtgkZyqxrH3CkyELs/xyjIVvGEhPD4VPd5Gx7rOTeAHnyDzJ0ca+YGnJjUISh+FDWxrzk5QmO1nVNEG+bRaZcdok/kzgJnj58IfEIeVB/NCPQJkOPW7asD5/SowBVRynnViPd42OhQVeyfMkhI1PMf8QZRSteGOyX7XrMgCEkFbmlF3jZTruYrAp+TjItQv0p+v16fL69uBHCSh/j+aVm00l5S29sNmMex+NGgfFGDWBouz3y5SE3KyKz9ECKw0MH2M8tCv/Tj6PlS8kuAulsJLQQ3wO0u9o/UaLbKuLBwz7dBHPNrD1XY8zyIco8u4QdubqJ</elem> +<elem key="bits">2048</elem> +<elem key="fingerprint">11aa0d508da4f2437e86b202e929cae2</elem> +</table> +<table> +<elem key="type">ecdsa-sha2-nistp256</elem> +<elem key="key">AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBI0QKwQluKYrYw2Heu9AYeeSLXi9fBZZvk/acMftAaFwEoUZhgqEPQNyQODw1K+WsbtY4c4ZAS9l34hqqrX0ZTg=</elem> +<elem key="bits">256</elem> +<elem key="fingerprint">cf22d54be448cae1585dffebc266ff18</elem> +</table> +</script></port> +<port protocol="tcp" portid="25"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="smtp" product="Exim smtpd" version="4.80" hostname="bou.codelutin.home" method="probed" conf="10"><cpe>cpe:/a:exim:exim:4.80</cpe></service><script id="smtp-commands" output="bou.codelutin.home Hello localhost [127.0.0.1], SIZE 52428800, 8BITMIME, PIPELINING, HELP, Commands supported: AUTH HELO EHLO MAIL RCPT DATA NOOP QUIT RSET HELP "/></port> +<port protocol="tcp" portid="111"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="rpcbind" version="2-4" extrainfo="RPC #100000" method="probed" conf="10"/><script id="rpcinfo" output=" program version port/proto service 100000 2,3,4 111/tcp rpcbind 100000 2,3,4 111/udp rpcbind 100024 1 35529/udp status 100024 1 41686/tcp status "><table key="100000"> +<table key="udp"> +<table key="version"> +<elem>2</elem> +<elem>3</elem> +<elem>4</elem> +</table> +<elem key="port">111</elem> +</table> +<table key="tcp"> +<table key="version"> +<elem>2</elem> +<elem>3</elem> +<elem>4</elem> +</table> +<elem key="port">111</elem> +</table> +</table> +<table key="100024"> +<table key="tcp"> +<table key="version"> +<elem>1</elem> +</table> +<elem key="port">41686</elem> +</table> +<table key="udp"> +<table key="version"> +<elem>1</elem> +</table> +<elem key="port">35529</elem> +</table> +</table> +</script></port> +<port protocol="tcp" portid="631"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="ipp" product="CUPS" version="1.5" method="probed" conf="10"/><script id="http-methods" output="Potentially risky methods: PUT See http://nmap.org/nsedoc/scripts/http-methods.html"/><script id="http-robots.txt" output="1 disallowed entry /"/><script id="http-title" output="Home - CUPS 1.5.3"><elem key="title">Home - CUPS 1.5.3</elem> +</script></port> +<port protocol="tcp" portid="8080"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="http" product="Apache Tomcat/Coyote JSP engine" version="1.1" method="probed" conf="10"/><script id="http-methods" output="Potentially risky methods: PUT DELETE See http://nmap.org/nsedoc/scripts/http-methods.html"/><script id="http-open-proxy" output="Proxy might be redirecting requests"/><script id="http-title" output="Apache Tomcat"><elem key="title">Apache Tomcat</elem> +</script></port> +<port protocol="tcp" portid="9200"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="wap-wsp" servicefp="SF-Port9200-TCP:V=6.40%I=7%D=1/21%Time=54BFD123%P=x86_64-unknown-linux-gnu%r(GetRequest,1AA,"HTTP/1\.0\x20200\x20OK\r\nContent-Type:\x20application/json;\x20charset=UTF-8\r\nContent-Length:\x20339\r\n\r\n{\n\x20\x20\"status\"\x20:\x20200,\n\x20\x20\"name\"\x20:\x20\"Steven\x20Lang\",\n\x20\x20\"cluster_name\"\x20:\x20\&quo [...] +</ports> +<times srtt="294" rttvar="76" to="100000"/> +</host> +<runstats><finished time="1421857172" timestr="Wed Jan 21 17:19:32 2015" elapsed="123.96" summary="Nmap done at Wed Jan 21 17:19:32 2015; 1 IP address (1 host up) scanned in 123.96 seconds" exit="success"/><hosts up="1" down="0" total="1"/> +</runstats> +</nmaprun> diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..27e1d7f --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec ./in-venv.sh python app/app.py diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..b825ac2 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,35 @@ +/* Space out content a bit */ +body { + padding-top: 20px; + padding-bottom: 20px; +} + +/* Everything but the jumbotron gets side spacing for mobile first views */ +.header { + padding-left: 15px; + padding-right: 15px; +} + +/* Custom page header */ +.header { + border-bottom: 1px solid #e5e5e5; +} + +/* Make the masthead heading the same height as the navigation */ +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; + padding-bottom: 19px; +} + +/* Main marketing message and sign up button */ +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} diff --git a/static/js/DetectCtrl.js b/static/js/DetectCtrl.js new file mode 100644 index 0000000..08c781a --- /dev/null +++ b/static/js/DetectCtrl.js @@ -0,0 +1,44 @@ +var app = angular.module("myApp", []); + +/* Contrôleur principal +================================================== */ + +app.controller("MainCtrl", ["$scope", "userService", + function($scope, userService) { + $scope.state = userService.getState(); + } +]); + +/* Un premier service +================================================== */ + +app.service("userService", ["utilsService", "$interval", + function(utilsService, $timeout) { + //Variable privée + var state = { + state: "a", + }; + + //Getter pour la variable state + this.getState = function() { + //Cette méthode fait appel à un autre service + $http.get('/whatstate'). + success(function(data, status, headers, config) { + state = data + }). + error(function(data, status, headers, config) { + // called asynchronously if an error occurs + // or server returns response with an error status. + }); + return state; + }; + + //5 secondes après le chargement de la page, la valeur d'une propriété change dans le service. + //La view est mise à jour automatiquement + $interval(function() { + app.service.getState() + }, 5000); + + + } +]); diff --git a/static/js/controllers/app.js b/static/js/controllers/app.js new file mode 100644 index 0000000..19e467e --- /dev/null +++ b/static/js/controllers/app.js @@ -0,0 +1,27 @@ +angular.module('myAppName', []) + .controller('MainCtrl', function($scope,$http) { + $http.get("getval") + .success(function(response) {$scope.text = response;}); + /*$scope.title = "Boucle sur un objet javascript" + $scope.users = [ + {name:'Sébastien',age:21}, + {name:'Marion',age:24}, + {name:'Laura',age:25}, + {name:'Morgan',age:23} + ];*/ + }); + +//var MainCtrl = function ($scope) { + +//}; +//var SecondCtrl = function ($scope) { + +//}; +/*angular.module('myAppName', []) + .controller('SecondCtrl', function($scope) { + $scope.title = "Appel d'une fonction" + $scope.ohHey = function () { + alert("Oh hey"); + }; + }); +*/ diff --git a/views/angular.html b/views/angular.html new file mode 100644 index 0000000..449a7dd --- /dev/null +++ b/views/angular.html @@ -0,0 +1,20 @@ +<html lang="en" ng-app="myAppName"> +<head> + <meta charset="utf-8"> + <title>My HTML File</title> + <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css"> + <script src="bower_components/angular/angular.js"></script> + <script src="static/js/controllers/app.js"></script> +</head> +<body> + <!-- Un premier contrôleur --> + <div ng-controller="MainCtrl"> + <ul> + + {{ text }} + + </ul> + </div> + +</body> +</html> diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..7d285b9 --- /dev/null +++ b/views/index.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html lang="en" ng-app="" ng-controller="stateController"> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content=""> + <meta name="author" content=""> + + <title>Mum (Machines under monitoring)</title> + + <script src="bower_components/jquery/dist/jquery.js"></script> + <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script> + + <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen"> + <link href="static/css/main.css" rel="stylesheet" media="screen"> + +</head> +<body> + + <form action="/detect" method="post"> + Plage d'IP à scanner (exemple : 198.116.0.1-10) : <input name="ip_range" type="text" /> + <input value="Valider" type="submit" /> + </form> + +</body> +</html> -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.